diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg
new file mode 100644
index 000000000..88214527a
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg b/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg
new file mode 100644
index 000000000..5eb188422
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg
new file mode 100644
index 000000000..5b5a33c9f
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg
new file mode 100644
index 000000000..57e2ee132
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml
new file mode 100644
index 000000000..8a336e8ad
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml
@@ -0,0 +1,87 @@
+pragma Singleton
+import Quickshell
+
+Singleton {
+ id: root
+ /**
+ * @param { string } summary
+ * @returns { string }
+ */
+ function findSuitableMaterialSymbol(summary = "") {
+ const defaultType = 'chat';
+ if (summary.length === 0) return defaultType;
+
+ const keywordsToTypes = {
+ 'reboot': 'restart_alt',
+ 'record': 'screen_record',
+ 'battery': 'power',
+ 'power': 'power',
+ 'screenshot': 'screenshot_monitor',
+ 'welcome': 'waving_hand',
+ 'time': 'scheduleb',
+ 'installed': 'download',
+ 'configuration reloaded': 'reset_wrench',
+ 'unable': 'question_mark',
+ "couldn't": 'question_mark',
+ 'config': 'reset_wrench',
+ 'update': 'update',
+ 'ai response': 'neurology',
+ 'control': 'settings',
+ 'upsca': 'compare',
+ 'music': 'queue_music',
+ 'install': 'deployed_code_update',
+ 'input': 'keyboard_alt',
+ 'preedit': 'keyboard_alt',
+ 'startswith:file': 'folder_copy', // Declarative startsWith check
+ };
+
+ const lowerSummary = summary.toLowerCase();
+
+ for (const [keyword, type] of Object.entries(keywordsToTypes)) {
+ if (keyword.startsWith('startswith:')) {
+ const startsWithKeyword = keyword.replace('startswith:', '');
+ if (lowerSummary.startsWith(startsWithKeyword)) {
+ return type;
+ }
+ } else if (lowerSummary.includes(keyword)) {
+ return type;
+ }
+ }
+
+ return defaultType;
+ }
+
+ /**
+ * @param { number | string | Date } timestamp
+ * @returns { string }
+ */
+ function getFriendlyNotifTimeString(timestamp) {
+ if (!timestamp) return '';
+ const messageTime = new Date(timestamp);
+ const now = new Date();
+ const diffMs = now.getTime() - messageTime.getTime();
+
+ // Less than 1 minute
+ if (diffMs < 60000)
+ return 'Now';
+
+ // Same day - show relative time
+ if (messageTime.toDateString() === now.toDateString()) {
+ const diffMinutes = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+
+ if (diffHours > 0) {
+ return `${diffHours}h`;
+ } else {
+ return `${diffMinutes}m`;
+ }
+ }
+
+ // Yesterday
+ if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString())
+ return 'Yesterday';
+
+ // Older dates
+ return Qt.formatDateTime(messageTime, "MMMM dd");
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml
index b590f22ac..ad250d9b6 100644
--- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml
+++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml
@@ -1,5 +1,5 @@
import qs.modules.common
-import "notification_utils.js" as NotificationUtils
+import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import Quickshell
diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml
index b20b470fa..fc612dcb1 100644
--- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml
+++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml
@@ -1,7 +1,6 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
-import "notification_utils.js" as NotificationUtils
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -136,7 +135,7 @@ MouseArea { // Notification group area
}
clip: true
- implicitHeight: expanded ?
+ implicitHeight: root.expanded ?
row.implicitHeight + padding * 2 :
Math.min(80, row.implicitHeight + padding * 2)
@@ -157,8 +156,8 @@ MouseArea { // Notification group area
Layout.alignment: Qt.AlignTop
Layout.fillWidth: false
image: root?.multipleNotifications ? "" : notificationGroup?.notifications[0]?.image ?? ""
- appIcon: notificationGroup?.appIcon
- summary: notificationGroup?.notifications[root.notificationCount - 1]?.summary
+ appIcon: root.notificationGroup?.appIcon
+ summary: root.notificationGroup?.notifications[root.notificationCount - 1]?.summary
urgency: root.notifications.some(n => n.urgency === NotificationUrgency.Critical.toString()) ?
NotificationUrgency.Critical : NotificationUrgency.Normal
}
diff --git a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js b/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js
deleted file mode 100644
index 1f879baa2..000000000
--- a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js
+++ /dev/null
@@ -1,82 +0,0 @@
-
-/**
- * @param { string } summary
- * @returns { string }
- */
-function findSuitableMaterialSymbol(summary = "") {
- const defaultType = 'chat';
- if(summary.length === 0) return defaultType;
-
- const keywordsToTypes = {
- 'reboot': 'restart_alt',
- 'record': 'screen_record',
- 'battery': 'power',
- 'power': 'power',
- 'screenshot': 'screenshot_monitor',
- 'welcome': 'waving_hand',
- 'time': 'scheduleb',
- 'installed': 'download',
- 'configuration reloaded': 'reset_wrench',
- 'unable': 'question_mark',
- "couldn't": 'question_mark',
- 'config': 'reset_wrench',
- 'update': 'update',
- 'ai response': 'neurology',
- 'control': 'settings',
- 'upsca': 'compare',
- 'music': 'queue_music',
- 'install': 'deployed_code_update',
- 'input': 'keyboard_alt',
- 'preedit': 'keyboard_alt',
- 'startswith:file': 'folder_copy', // Declarative startsWith check
- };
-
- const lowerSummary = summary.toLowerCase();
-
- for (const [keyword, type] of Object.entries(keywordsToTypes)) {
- if (keyword.startsWith('startswith:')) {
- const startsWithKeyword = keyword.replace('startswith:', '');
- if (lowerSummary.startsWith(startsWithKeyword)) {
- return type;
- }
- } else if (lowerSummary.includes(keyword)) {
- return type;
- }
- }
-
- return defaultType;
-}
-
-/**
- * @param { number | string | Date } timestamp
- * @returns { string }
- */
-const getFriendlyNotifTimeString = (timestamp) => {
- if (!timestamp) return '';
- const messageTime = new Date(timestamp);
- const now = new Date();
- const diffMs = now.getTime() - messageTime.getTime();
-
- // Less than 1 minute
- if (diffMs < 60000)
- return 'Now';
-
- // Same day - show relative time
- if (messageTime.toDateString() === now.toDateString()) {
- const diffMinutes = Math.floor(diffMs / 60000);
- const diffHours = Math.floor(diffMs / 3600000);
-
- if (diffHours > 0) {
- return `${diffHours}h`;
- } else {
- return `${diffMinutes}m`;
- }
- }
-
- // Yesterday
- if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString())
- return 'Yesterday';
-
- // Older dates
- return Qt.formatDateTime(messageTime, "MMMM dd");
-};
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml
index 74cb35f8b..b3377cc64 100644
--- a/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml
@@ -11,8 +11,8 @@ WButton {
colBackground: Looks.colors.bg2
colBackgroundHover: Looks.colors.bg2Hover
colBackgroundActive: Looks.colors.bg2Active
- border.color: Looks.colors.bg2Border
+ property color colBorder: Looks.colors.bg2Border
+ property color colBorderToggled: Looks.colors.accent
+ border.color: checked ? colBorderToggled : colBorder
border.width: root.pressed ? 2 : 1
-
-
}
diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml
index 68e836b66..be75cc30a 100644
--- a/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml
@@ -14,6 +14,7 @@ Item {
property real radius: Looks.radius.large
property alias border: borderRect
property alias borderColor: borderRect.border.color
+ property alias borderWidth: borderRect.border.width
implicitWidth: borderRect.implicitWidth
implicitHeight: borderRect.implicitHeight
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml
index c222e41c4..2f2b788a4 100644
--- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml
@@ -54,35 +54,19 @@ FooterRectangle {
Layout.fillWidth: true
}
- SmallBorderedIconButton {
- leftPadding: 12
- rightPadding: 12
- implicitWidth: focusButtonContent.implicitWidth + leftPadding + rightPadding
+ SmallBorderedIconAndTextButton {
+ iconName: TimerService.pomodoroRunning ? "stop" : "play"
+ text: TimerService.pomodoroRunning ? Translation.tr("End session") : Translation.tr("Focus")
onClicked: {
if (TimerService.pomodoroRunning) {
- TimerService.togglePomodoro()
- TimerService.resetPomodoro()
+ TimerService.togglePomodoro();
+ TimerService.resetPomodoro();
} else {
- TimerService.togglePomodoro()
+ TimerService.togglePomodoro();
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarRight", "toggle"]);
}
}
-
- contentItem: Row {
- id: focusButtonContent
- spacing: 4
- FluentIcon {
- icon: TimerService.pomodoroRunning ? "stop" : "play"
- filled: true
- implicitSize: 14
- anchors.verticalCenter: parent.verticalCenter
- }
- WText {
- anchors.verticalCenter: parent.verticalCenter
- text: TimerService.pomodoroRunning ? Translation.tr("End session") : Translation.tr("Focus")
- }
- }
}
}
}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml
index ed6677854..797cee28a 100644
--- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml
@@ -19,16 +19,40 @@ WBarAttachedPanelContent {
property bool collapsed: false
- contentItem: Column {
+ contentItem: ColumnLayout {
anchors {
horizontalCenter: parent.horizontalCenter
- top: root.barAtBottom ? undefined : parent.top
- bottom: root.barAtBottom ? parent.bottom : undefined
+ top: parent.top
+ bottom: parent.bottom
}
spacing: 12
+ Item {
+ id: notificationArea
+ Layout.fillHeight: true
+ implicitWidth: notificationPane.implicitWidth
+
+ WPane {
+ id: notificationPane
+ anchors {
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ }
+ contentItem: NotificationPaneContent {
+ implicitWidth: calendarColumnLayout.implicitWidth
+ implicitHeight: Notifications.list.length > 0 ? (notificationArea.height - notificationPane.borderWidth * 2) : 230
+
+ Behavior on implicitHeight {
+ animation: Looks.transition.enter.createObject(this)
+ }
+ }
+ }
+ }
+
WPane {
contentItem: ColumnLayout {
+ id: calendarColumnLayout
spacing: 0
DateHeader {
Layout.fillWidth: true
@@ -37,7 +61,9 @@ WBarAttachedPanelContent {
}
}
- WPanelSeparator { visible: !root.collapsed }
+ WPanelSeparator {
+ visible: !root.collapsed
+ }
CalendarWidget {
Layout.fillWidth: true
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml
new file mode 100644
index 000000000..860451fc3
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml
@@ -0,0 +1,25 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.services
+import qs.modules.common
+import qs.modules.common.widgets
+import qs.modules.common.functions
+import qs.modules.waffle.looks
+
+WBorderlessButton {
+ id: headerButton
+ Layout.fillWidth: false
+ implicitWidth: 16
+ implicitHeight: 16
+ color: "transparent"
+
+ contentItem: Item {
+ FluentIcon {
+ anchors.centerIn: parent
+ implicitSize: 16
+ icon: headerButton.icon.name
+ color: headerButton.hovered && !headerButton.pressed ? Looks.colors.fg : Looks.colors.fg1
+ }
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml
new file mode 100644
index 000000000..368829865
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml
@@ -0,0 +1,81 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.services
+import qs.modules.common
+import qs.modules.common.widgets
+import qs.modules.common.functions
+import qs.modules.waffle.looks
+
+BodyRectangle {
+ id: root
+ anchors.fill: parent
+ implicitHeight: 230
+
+ ColumnLayout {
+ id: contentLayout
+ anchors.fill: parent
+ anchors.margins: 4
+
+ spacing: 12
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.leftMargin: 12
+ Layout.rightMargin: 12
+ Layout.topMargin: 8
+
+ spacing: 8
+
+ WText {
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideRight
+ text: Translation.tr("Notifications")
+ font.pixelSize: Looks.font.pixelSize.large
+ }
+
+ SmallBorderedIconButton {
+ icon.name: "alert-snooze"
+ checked: Notifications.silent
+ onClicked: {
+ Notifications.silent = !Notifications.silent;
+ }
+ }
+
+ SmallBorderedIconAndTextButton {
+ visible: Notifications.list.length > 0
+ iconVisible: false
+ text: Translation.tr("Clear all")
+ onClicked: {
+ Notifications.discardAllNotifications();
+ }
+ }
+ }
+
+ StyledListView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ animateAppearance: false
+ clip: true
+
+ model: Notifications.appNameList
+ delegate: WNotificationGroup {
+ required property int index
+ required property var modelData
+ width: ListView.view.width
+ notificationGroup: Notifications.groupsByAppName[modelData]
+ }
+
+ EmptyPlaceholder {
+ visible: Notifications.list.length === 0
+ anchors.centerIn: parent
+ }
+ }
+ }
+
+ component EmptyPlaceholder: WText {
+ horizontalAlignment: Text.AlignHCenter
+ text: Translation.tr("No new notifications")
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml
new file mode 100644
index 000000000..c4331a7dc
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml
@@ -0,0 +1,34 @@
+import QtQuick
+import qs
+import qs.services
+import qs.modules.common
+import qs.modules.waffle.looks
+
+SmallBorderedIconButton {
+ id: root
+
+ property bool iconVisible: true
+ property string iconName: ""
+ property bool iconFilled: true
+
+ leftPadding: 12
+ rightPadding: 12
+ implicitWidth: focusButtonContent.implicitWidth + leftPadding + rightPadding
+
+ contentItem: Row {
+ id: focusButtonContent
+ spacing: 4
+
+ FluentIcon {
+ visible: root.iconVisible
+ icon: root.iconName
+ filled: root.iconFilled
+ implicitSize: 14
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ WText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.text
+ }
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml
index 1236779a7..f7006ca7f 100644
--- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml
@@ -13,6 +13,7 @@ WBorderedButton {
anchors.centerIn: parent
implicitSize: 12
icon: root.icon.name
+ color: root.fgColor
}
}
}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml
new file mode 100644
index 000000000..a658695d1
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml
@@ -0,0 +1,30 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import org.kde.kirigami as Kirigami
+import qs.services
+import qs.modules.common
+import qs.modules.common.widgets
+import qs.modules.common.functions
+import qs.modules.waffle.looks
+
+Item {
+ id: root
+
+ property string icon: ""
+ property real implicitSize: 16
+ implicitWidth: implicitSize
+ implicitHeight: implicitSize
+
+ Kirigami.Icon {
+ anchors.fill: parent
+ implicitWidth: root.implicitSize
+ implicitHeight: root.implicitSize
+
+ source: root.icon || fallback
+ fallback: `${Looks.iconsPath}/apps.svg`
+ roundToIconSize: false
+ isMask: !root.icon
+ color: Looks.colors.fg
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml
new file mode 100644
index 000000000..837c7c016
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml
@@ -0,0 +1,93 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.services
+import qs.modules.common
+import qs.modules.common.widgets
+import qs.modules.common.functions
+import qs.modules.waffle.looks
+
+MouseArea {
+ id: root
+
+ required property var notificationGroup
+ readonly property var notifications: notificationGroup?.notifications ?? []
+
+ implicitWidth: contentLayout.implicitWidth
+ implicitHeight: contentLayout.implicitHeight
+
+ ColumnLayout {
+ id: contentLayout
+ anchors.fill: parent
+ spacing: 4
+
+ GroupHeader {
+ id: notifHeader
+ Layout.fillWidth: true
+ Layout.margins: 11
+ }
+
+ ListView {
+ Layout.fillWidth: true
+ implicitWidth: notifHeader.implicitWidth
+ implicitHeight: contentHeight
+ interactive: false
+ spacing: 4
+ model: ScriptModel {
+ values: root.notifications.slice().reverse()
+ }
+ delegate: WSingleNotification {
+ required property var modelData
+ width: ListView.view.width
+ notification: modelData
+ }
+ }
+ }
+
+ component GroupHeader: MouseArea {
+ id: headerMouseArea
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+
+ implicitWidth: appHeader.implicitWidth
+ implicitHeight: appHeader.implicitHeight
+
+ RowLayout {
+ id: appHeader
+ anchors.fill: parent
+ spacing: 7
+
+ WNotificationAppIcon {
+ Layout.alignment: Qt.AlignVCenter
+ icon: root.notificationGroup?.appIcon ?? ""
+ }
+
+ WText {
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideRight
+ text: root.notificationGroup?.appName ?? ""
+ }
+
+ // NotificationHeaderButton { // TODO: More notification functionality needed so we can have this button
+ // visible: headerMouseArea.containsMouse
+ // Layout.leftMargin: 25
+ // Layout.rightMargin: 25
+ // icon.name: "more-horizontal"
+ // }
+
+ NotificationHeaderButton {
+ visible: headerMouseArea.containsMouse
+ Layout.rightMargin: 3
+ icon.name: "dismiss"
+ onClicked: {
+ root.notifications.forEach(notif => {
+ Qt.callLater(() => {
+ Notifications.discardNotification(notif.notificationId);
+ });
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml
new file mode 100644
index 000000000..42a832adb
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml
@@ -0,0 +1,59 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Services.Notifications
+import qs.services
+import qs.modules.common
+import qs.modules.common.widgets
+import qs.modules.common.functions
+import qs.modules.waffle.looks
+
+MouseArea {
+ id: root
+
+ required property var notification
+ property bool expanded: false
+
+ implicitHeight: contentItem.implicitHeight
+ implicitWidth: contentItem.implicitWidth
+
+ Rectangle {
+ id: contentItem
+ anchors.fill: parent
+ color: Looks.colors.bgPanelBody
+ radius: Looks.radius.medium
+ property real padding: 12
+ implicitHeight: notificationContent.implicitHeight + padding * 2
+ implicitWidth: notificationContent.implicitWidth + padding * 2
+ border.width: 1
+ border.color: Looks.applyContentTransparency(Looks.colors.ambientShadow)
+
+ ColumnLayout {
+ id: notificationContent
+ anchors.fill: parent
+ anchors.margins: contentItem.padding
+
+ RowLayout {
+ Layout.fillWidth: true
+ WText {
+ text: NotificationUtils.getFriendlyNotifTimeString(root.notification?.time)
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ WText {
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ text: root.notification.summary
+ }
+ WText {
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ wrapMode: Text.Wrap
+ maximumLineCount: root.expanded ? 100 : 1
+ }
+ }
+ }
+ }
+}