feat(nosignal): interface redesign — slim glass bar + floating popouts

Convert the caelestia shell to the NoSignal "design_handoff_quickshell_shell"
spec: a slim translucent top bar with floating glass popouts, fixed dark-glass
chrome + Material You dynamic accent, square corners, JetBrains Mono.

- Theme singleton (services/Theme.qml): design tokens; accent = m3primary
  (dynamic), fixed glass/text, square radii.
- New slim bar as its own PanelWindow (modules/nsbar/NsBar.qml), additive in
  shell.qml; reuses caelestia services + components. Helpers: BarPill,
  NsWorkspaces (numbered, active=accent pill), NsOverlay (popout host, one-at-a-
  time via services/NsShell.qml, outside-click + Esc), components/NsPanel +
  components/NsSwitch.
- Old caelestia frame removed: BarWrapper collapsed; ContentWindow border 0 +
  borderTop 0; Exclusions zones 0. Drawer engine kept for launcher/OSD/lock.
- 9 popouts (modules/nsbar/panels/): Calendar, QuickSettings, Network,
  Bluetooth, Notifications, Audio, SystemTray, Overview, PowerMenu.
- Clock restyled (date muted + time DemiBold).
- nexus (Settings) reskinned: dark-glass window + cards, square corners
  (Nexus.qml, common/ConnectedRect.qml) — keeps all pages/functionality.

Launcher kept as-is. Font (JetBrainsMono) + rounding.scale=0 + Super+Alt+Space
nexus bind are caelestia/hypr CONFIG and ship in the OS, not this fork.
Next: full Material Symbols -> Nerd Font icon remap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday 2026-06-14 10:36:33 +01:00
parent 8bada1d61a
commit 36f6f5a157
24 changed files with 2341 additions and 115 deletions

43
components/NsPanel.qml Normal file
View file

@ -0,0 +1,43 @@
pragma ComponentBehavior: Bound
// NoSignal floating popout container: dark-glass card, 1px border, 4px radius.
// Content goes in a ColumnLayout (default children). Sizes to its content height;
// set `width` on the instance. A background MouseArea swallows clicks so clicking
// inside the panel doesn't fall through to the overlay's close-catcher.
import QtQuick
import QtQuick.Layouts
import qs.services
Rectangle {
id: root
property int pad: Theme.size.panelPad
property alias spacing: holder.spacing
default property alias content: holder.data
// anchoring hint read by NsOverlay: "center" | "right" | "trigger"
property string anchorMode: "right"
implicitHeight: holder.implicitHeight + root.pad * 2
color: Theme.panelBg
radius: Theme.radius.panel
border.width: 1
border.color: Theme.panelBorder
MouseArea {
anchors.fill: parent
hoverEnabled: true
// swallow clicks/hover so they don't reach the overlay close-catcher
}
ColumnLayout {
id: holder
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: root.pad
spacing: Theme.size.gap
}
}

46
components/NsSwitch.qml Normal file
View file

@ -0,0 +1,46 @@
pragma ComponentBehavior: Bound
// NoSignal toggle switch (animated 16px knob) used in panel headers.
import QtQuick
import qs.services
Rectangle {
id: root
property bool on: false
signal toggled
implicitWidth: 38
implicitHeight: 22
radius: 11
color: on ? Theme.accent : Theme.fillSubtle
Behavior on color {
ColorAnimation {
duration: Theme.hoverMs
}
}
Rectangle {
x: root.on ? parent.width - width - 3 : 3
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 16
implicitHeight: 16
radius: 8
color: root.on ? Theme.onAccent : Theme.text
Behavior on x {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggled()
}
}

View file

@ -24,7 +24,9 @@ Item {
readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness) readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness)
readonly property int contentHeight: Tokens.sizes.bar.innerWidth + padding * 2 readonly property int contentHeight: Tokens.sizes.bar.innerWidth + padding * 2
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentHeight : Config.border.thickness readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentHeight : Config.border.thickness
readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) // NoSignal redesign: the old caelestia bar is replaced by the slim NsBar
// PanelWindow. Collapse it (the drawer engine is kept for launcher/OSD/lock).
readonly property bool shouldBeVisible: false
property bool isHovered property bool isHovered
function closeTray(): void { function closeTray(): void {

View file

@ -1,113 +1,34 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
// NoSignal redesign: center clock module date (muted) + time (DemiBold),
// JetBrains Mono. The hover/active pill is applied by the bar module wrapper;
// this is just the content. Opens the Calendar popout.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Caelestia.Config
import qs.components import qs.components
import qs.services import qs.services
StyledRect { RowLayout {
id: root id: root
readonly property color colour: Colours.palette.m3tertiary spacing: 8
readonly property int padding: Config.bar.clock.background ? Tokens.padding.medium : Tokens.padding.extraSmall
readonly property var font: Tokens.font.body.builders.small.scale(1.1)
implicitHeight: Tokens.sizes.bar.innerWidth StyledText {
implicitWidth: layout.implicitWidth + root.padding * 2 Layout.alignment: Qt.AlignVCenter
text: Time.format("ddd d MMM")
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
font.weight: Font.Normal
}
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) StyledText {
radius: Tokens.rounding.full Layout.alignment: Qt.AlignVCenter
text: Time.format("hh:mm")
RowLayout { color: Theme.text
id: layout font.family: Theme.font.family
font.pixelSize: Theme.font.bar
anchors.centerIn: parent font.weight: Font.DemiBold
spacing: Tokens.spacing.small
Loader {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
active: Config.bar.clock.showIcon
visible: active
sourceComponent: MaterialIcon {
text: "calendar_month"
color: root.colour
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter
visible: Config.bar.clock.showDate
horizontalAlignment: StyledText.AlignHCenter
text: Time.format("ddd d")
font: Tokens.font.body.small
color: root.colour
}
Rectangle {
Layout.fillHeight: true
Layout.topMargin: root.padding
Layout.bottomMargin: root.padding
visible: Config.bar.clock.showDate
implicitWidth: 1
color: Colours.palette.m3outlineVariant
}
StyledText {
Layout.alignment: Qt.AlignVCenter
text: Time.hourStr
font: {
const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / hourMetrics.width);
return root.font.width(scale * 100).letterSpacing(scale).build();
}
color: root.colour
TextMetrics {
id: hourMetrics
font: root.font.build()
text: Time.hourStr
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter
text: ":"
font: root.font.build()
color: root.colour
}
StyledText {
Layout.alignment: Qt.AlignVCenter
text: Time.minuteStr
font: {
const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / minMetrics.width);
return root.font.width(scale * 100).letterSpacing(scale).build();
}
color: root.colour
TextMetrics {
id: minMetrics
font: root.font.build()
text: Time.minuteStr
}
}
Loader {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
active: GlobalConfig.services.useTwelveHourClock
visible: active
sourceComponent: StyledText {
text: Time.amPmStr.toLowerCase()
font: Tokens.font.body.builders.small.scale(0.9).build()
color: root.colour
}
}
} }
} }

View file

@ -34,8 +34,8 @@ StyledWindow {
property real fsTransitionProg: hasFullscreen ? 1 : 0 property real fsTransitionProg: hasFullscreen ? 1 : 0
readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows
readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg) readonly property real borderThickness: 0 // NoSignal: no screen-edge frame (NsBar owns the top)
readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg) readonly property real borderRounding: 0 // NoSignal: no rounded screen frame
readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg)
readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness
@ -157,7 +157,7 @@ StyledWindow {
radius: root.borderRounding radius: root.borderRounding
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset borderTop: -anchors.margins - root.sdfBorderOffset // NoSignal: no top frame
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
} }

View file

@ -18,7 +18,7 @@ Scope {
ExclusionZone { ExclusionZone {
anchors.top: true anchors.top: true
exclusiveZone: root.bar.exclusiveZone exclusiveZone: 0 // NoSignal: NsBar owns the top exclusive zone
} }
ExclusionZone { ExclusionZone {
@ -32,7 +32,7 @@ Scope {
component ExclusionZone: StyledWindow { component ExclusionZone: StyledWindow {
screen: root.screen screen: root.screen
name: "border-exclusion" name: "border-exclusion"
exclusiveZone: contentItem.Config.border.thickness exclusiveZone: 0 // NoSignal: no screen-edge frame
mask: Region {} mask: Region {}
implicitWidth: 1 implicitWidth: 1
implicitHeight: 1 implicitHeight: 1

View file

@ -16,7 +16,7 @@ Item {
onClose: root.close() onClose: root.close()
} }
property color blobColour: Colours.tPalette.m3surfaceContainerLow property color blobColour: Theme.panelBg // NoSignal: fixed dark-glass window surface
signal close signal close
@ -30,7 +30,7 @@ Item {
BlobGroup { BlobGroup {
id: blobGroup id: blobGroup
smoothing: root.Tokens.rounding.medium smoothing: 0 // NoSignal: crisp corners (no organic blob smoothing)
color: root.blobColour color: root.blobColour
} }
@ -38,7 +38,7 @@ Item {
anchors.fill: parent anchors.fill: parent
group: blobGroup group: blobGroup
opacity: root.blobColour.a opacity: root.blobColour.a
radius: Tokens.rounding.large radius: Theme.radius.panel // NoSignal: sharp 4px corners
borderLeft: navPane.width + navPane.anchors.margins * 2 borderLeft: navPane.width + navPane.anchors.margins * 2
borderRight: Tokens.padding.medium borderRight: Tokens.padding.medium

View file

@ -7,9 +7,10 @@ StyledRect {
property bool first property bool first
property bool last property bool last
color: Colours.tPalette.m3surfaceContainer // NoSignal: dark-glass cards, 4px outer corners (connected-list look)
topLeftRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall color: Theme.fillSubtle
topRightRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall topLeftRadius: first ? Theme.radius.panel : 0
bottomLeftRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall topRightRadius: first ? Theme.radius.panel : 0
bottomRightRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall bottomLeftRadius: last ? Theme.radius.panel : 0
bottomRightRadius: last ? Theme.radius.panel : 0
} }

46
modules/nsbar/BarPill.qml Normal file
View file

@ -0,0 +1,46 @@
pragma ComponentBehavior: Bound
// NoSignal bar module pill: hover-highlightable (radius 5, hover bg); when its
// panel is open it uses accent.soft bg + accent foreground. Children go in a
// centered Row. `active` drives the open/highlight state; `triggered()` fires on
// click (the bar wires this to toggle its openPanel string).
import QtQuick
import qs.services
Rectangle {
id: root
property bool active: false
property int hPad: 9
signal triggered
default property alias content: inner.data
implicitWidth: inner.implicitWidth + hPad * 2
implicitHeight: 26
radius: Theme.radius.barModule
color: active ? Theme.accentSoft : mouse.containsMouse ? Theme.hover : "transparent"
Behavior on color {
ColorAnimation {
duration: Theme.hoverMs
}
}
Row {
id: inner
anchors.centerIn: parent
spacing: 6
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.triggered()
}
}

257
modules/nsbar/NsBar.qml Normal file
View file

@ -0,0 +1,257 @@
pragma ComponentBehavior: Bound
// NoSignal slim top bar (interface redesign 2026-06-14).
// Flat 38px translucent-glass PanelWindow at the top edge. Three zones:
// left : logo · | · workspaces · | · window-title
// center: clock
// right : media · bell · wifi · bt · volume · battery · | · power
// Each interactive module is a BarPill (hover bg; active = accent.soft + accent
// fg). `openPanel` is the single shell-level "one panel open at a time" string;
// clicking a pill toggles it. The popup panels themselves are built next for
// now a click just highlights the pill. Reuses the existing service singletons.
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import qs.services
import qs.utils
import qs.components
import qs.modules.bar.components as BC
import qs.modules.bar.components.workspaces as WS
Variants {
model: Screens.screens
PanelWindow {
id: win
required property ShellScreen modelData
screen: modelData
color: "transparent"
WlrLayershell.namespace: "nsbar" // for Hyprland blur layerrule
WlrLayershell.layer: WlrLayer.Top
anchors.top: true
anchors.left: true
anchors.right: true
implicitHeight: Theme.size.barHeight
exclusiveZone: Theme.size.barHeight
readonly property int hPad: 12
readonly property int gap: 6
// Glass surface + 1px bottom border
Rectangle {
anchors.fill: parent
color: Theme.barBg
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.barBorder
}
}
// Left zone
RowLayout {
anchors.left: parent.left
anchors.leftMargin: win.hPad
anchors.verticalCenter: parent.verticalCenter
spacing: win.gap
BC.OsIcon {}
Divider {}
NsWorkspaces {}
Divider {}
BarPill {
id: titlePill
active: NsShell.open === "overview"
onTriggered: NsShell.toggle("overview", mapToItem(null, width / 2, 0).x)
MaterialIcon {
text: "desktop_windows"
color: titlePill.active ? Theme.accent : Theme.textMuted
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Hypr.activeToplevel?.title ?? "Desktop"
elide: Text.ElideRight
width: Math.min(implicitWidth, 220)
color: titlePill.active ? Theme.accent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
}
// Center zone: clock
BarPill {
id: clockPill
anchors.centerIn: parent
active: NsShell.open === "calendar"
onTriggered: NsShell.toggle("calendar", mapToItem(null, width / 2, 0).x)
BC.Clock {}
}
// Right zone
RowLayout {
anchors.right: parent.right
anchors.rightMargin: win.hPad
anchors.verticalCenter: parent.verticalCenter
spacing: win.gap
// Media / now-playing
BarPill {
id: mediaPill
visible: Players.active !== null
active: NsShell.open === "quicksettings"
onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x)
MaterialIcon {
text: Players.active?.isPlaying ? "pause" : "music_note"
color: mediaPill.active ? Theme.accent : Theme.green
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Players.active?.trackTitle || "Awake"
elide: Text.ElideRight
width: Math.min(implicitWidth, 90)
color: mediaPill.active ? Theme.accent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
// System tray
IconPill {
icon: "widgets"
panel: "tray"
}
// Notifications bell (with unread dot overlaid on the icon)
BarPill {
id: bellPill
active: NsShell.open === "notifications"
onTriggered: NsShell.toggle("notifications", mapToItem(null, width / 2, 0).x)
MaterialIcon {
text: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none"
color: bellPill.active ? Theme.accent : Theme.text
Rectangle {
visible: Notifs.notClosed.length > 0
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -1
anchors.topMargin: 1
implicitWidth: 6
implicitHeight: 6
radius: 3
color: Theme.accent
}
}
}
// Wi-Fi
IconPill {
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
panel: "network"
}
// Bluetooth
IconPill {
icon: {
if (!Bluetooth.defaultAdapter?.enabled)
return "bluetooth_disabled";
if (Bluetooth.devices.values.some(d => d.connected))
return "bluetooth_connected";
return "bluetooth";
}
panel: "bluetooth"
}
// Volume
IconPill {
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
panel: "audio"
}
// Battery / Quick Settings always present (control-centre trigger):
// battery + % on laptops, a "tune" icon on desktops.
BarPill {
id: batteryPill
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
active: NsShell.open === "quicksettings"
onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x)
MaterialIcon {
text: batteryPill.laptop ? Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)) : "tune"
color: batteryPill.active ? Theme.accent : !batteryPill.laptop || !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? Theme.text : Theme.danger
fill: 1
}
StyledText {
visible: batteryPill.laptop
anchors.verticalCenter: parent.verticalCenter
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
color: batteryPill.active ? Theme.accent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
Divider {}
// Power
IconPill {
icon: "power_settings_new"
panel: "power"
iconColour: Theme.danger
}
}
// Reusable bits
component Divider: Rectangle {
Layout.alignment: Qt.AlignVCenter
implicitWidth: 1
implicitHeight: 16
color: Theme.barBorder
}
// Icon-only pill bound to the shell openPanel state
component IconPill: BarPill {
id: p
property string icon
property string panel
property color iconColour: Theme.text
active: NsShell.open === panel
onTriggered: NsShell.toggle(panel, mapToItem(null, width / 2, 0).x)
MaterialIcon {
text: p.icon
color: p.active ? Theme.accent : p.iconColour
}
}
}
}

161
modules/nsbar/NsOverlay.qml Normal file
View file

@ -0,0 +1,161 @@
pragma ComponentBehavior: Bound
// NoSignal popup overlay: one full-screen layer per screen that hosts the active
// floating panel (driven by NsShell.open). A transparent click-catcher behind the
// panel closes on outside-click; Escape also closes. Panels are positioned below
// the bar, anchored center / right / under-trigger per their `anchorMode`.
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.services
import "panels" as Panels
Variants {
model: Screens.screens
PanelWindow {
id: ov
required property ShellScreen modelData
screen: modelData
visible: NsShell.open !== ""
color: "transparent"
WlrLayershell.namespace: "nspanels"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
exclusiveZone: 0
// outside-click closes
MouseArea {
anchors.fill: parent
onClicked: NsShell.close()
}
// Escape closes
Item {
anchors.fill: parent
focus: ov.visible
Keys.onEscapePressed: NsShell.close()
}
// full-screen modal: Power Menu
Loader {
anchors.fill: parent
active: NsShell.open === "power"
sourceComponent: powerC
Component {
id: powerC
Panels.NsPowerMenu {}
}
}
Loader {
id: loader
active: NsShell.open !== "" && NsShell.open !== "power"
y: Theme.size.barHeight + 6
x: {
if (!item)
return 0;
const w = item.implicitWidth;
const mode = item.anchorMode ?? "right";
if (mode === "center")
return Math.round((ov.width - w) / 2);
if (mode === "trigger")
return Math.max(8, Math.min(ov.width - w - 8, NsShell.anchorX - w / 2));
return ov.width - w - 12; // right
}
// small enter slide
opacity: active ? 1 : 0
transform: Translate {
y: loader.active ? 0 : -5
}
Behavior on opacity {
NumberAnimation {
duration: 120
}
}
sourceComponent: {
switch (NsShell.open) {
case "calendar":
return calendarC;
case "quicksettings":
return quickSettingsC;
case "network":
return networkC;
case "bluetooth":
return bluetoothC;
case "notifications":
return notificationsC;
case "audio":
return audioC;
case "tray":
return trayC;
case "overview":
return overviewC;
default:
return null;
}
}
Component {
id: calendarC
Panels.NsCalendar {}
}
Component {
id: quickSettingsC
Panels.NsQuickSettings {}
}
Component {
id: networkC
Panels.NsNetwork {}
}
Component {
id: bluetoothC
Panels.NsBluetooth {}
}
Component {
id: notificationsC
Panels.NsNotifications {}
}
Component {
id: audioC
Panels.NsAudio {}
}
Component {
id: trayC
Panels.NsSystemTray {}
}
Component {
id: overviewC
Panels.NsOverview {}
}
}
}
}

View file

@ -0,0 +1,85 @@
pragma ComponentBehavior: Bound
// NoSignal workspaces module: numbered 1..N, no capsule. Active = filled accent
// pill with dark (onAccent) text + workspace name (e.g. "2 code"); occupied =
// bright number; empty = faint number. Click switches workspace.
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.services
import qs.components
RowLayout {
id: root
spacing: 4
Repeater {
model: Config.bar.workspaces.shown
delegate: Rectangle {
id: ws
required property int index
readonly property int wsId: index + 1
readonly property var hw: Hypr.workspaces.values.find(w => w.id === wsId) ?? null
readonly property bool active: Hypr.activeWsId === wsId
readonly property bool occupied: (hw?.lastIpcObject.windows ?? 0) > 0
readonly property string wsName: {
const n = hw?.name ?? "";
return n && n !== String(wsId) && !n.startsWith("special") ? n : "";
}
Layout.alignment: Qt.AlignVCenter
implicitWidth: row.implicitWidth + (active ? 16 : 10)
implicitHeight: 22
radius: Theme.radius.barModule
color: active ? Theme.accent : mouse.containsMouse ? Theme.hover : "transparent"
Behavior on color {
ColorAnimation {
duration: Theme.hoverMs
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
RowLayout {
id: row
anchors.centerIn: parent
spacing: 5
StyledText {
text: ws.wsId
color: ws.active ? Theme.onAccent : ws.occupied ? Theme.text : Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
font.weight: ws.active ? Font.DemiBold : Font.Normal
}
StyledText {
visible: ws.active && ws.wsName !== ""
text: ws.wsName
color: Theme.onAccent
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Hypr.dispatch(`workspace ${ws.wsId}`)
}
}
}
}

View file

@ -0,0 +1,162 @@
pragma ComponentBehavior: Bound
// NoSignal Audio Mixer popout (off the volume pill). Output device row, master
// slider, per-app application sliders, input (mic) slider. Wired to Pipewire
// via the Audio service.
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.components
NsPanel {
id: root
implicitWidth: 340
anchorMode: "right"
// output device
RowLayout {
Layout.fillWidth: true
spacing: 8
MaterialIcon {
text: "speaker"
color: Theme.textMuted
}
StyledText {
Layout.fillWidth: true
text: Audio.sink?.description || Audio.sink?.name || "Output"
color: Theme.text
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
MaterialIcon {
text: "expand_more"
color: Theme.textFaint
}
}
AudioSlider {
icon: Audio.muted ? "volume_off" : "volume_up"
value: Audio.volume
onMoved: v => Audio.setVolume(v)
}
// applications
StyledText {
visible: Audio.streams.length > 0
text: "APPLICATIONS"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.label
font.letterSpacing: Theme.font.trackingLabel
}
Repeater {
model: Audio.streams
delegate: AudioSlider {
required property var modelData
Layout.fillWidth: true
icon: "graphic_eq"
label: modelData?.properties?.["application.name"] || modelData?.description || modelData?.name || "App"
value: modelData?.audio?.volume ?? 0
onMoved: v => {
if (modelData?.audio)
modelData.audio.volume = v;
}
}
}
// input
StyledText {
text: "INPUT"
Layout.topMargin: 2
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.label
font.letterSpacing: Theme.font.trackingLabel
}
AudioSlider {
icon: Audio.sourceMuted ? "mic_off" : "mic"
value: Audio.sourceVolume
onMoved: v => Audio.setSourceVolume(v)
}
component AudioSlider: ColumnLayout {
id: s
property string icon
property string label: ""
property real value: 0
signal moved(real v)
Layout.fillWidth: true
spacing: 3
StyledText {
visible: s.label !== ""
text: s.label
color: Theme.textMuted
elide: Text.ElideRight
Layout.fillWidth: true
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
RowLayout {
Layout.fillWidth: true
spacing: 10
MaterialIcon {
text: s.icon
color: Theme.textMuted
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 6
radius: 3
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 3
color: Theme.accent
}
Rectangle {
x: parent.width * Math.max(0, Math.min(1, s.value)) - width / 2
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 14
implicitHeight: 14
radius: 7
color: Theme.accent
}
MouseArea {
anchors.fill: parent
onPressed: e => s.moved(Math.max(0, Math.min(1, e.x / width)))
onPositionChanged: e => {
if (pressed)
s.moved(Math.max(0, Math.min(1, e.x / width)));
}
}
}
StyledText {
text: Math.round(s.value * 100) + "%"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
}

View file

@ -0,0 +1,150 @@
pragma ComponentBehavior: Bound
// NoSignal Bluetooth popout (off the BT pill). Title + switch; device rows (icon
// tile peach-tinted if connected, name, status, battery %). Footer "Bluetooth
// settings". Wired to Quickshell.Bluetooth (BlueZ).
import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import qs.services
import qs.utils
import qs.components
NsPanel {
id: root
implicitWidth: 340
anchorMode: "right"
readonly property var adapter: Bluetooth.defaultAdapter
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Bluetooth"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
Item {
Layout.fillWidth: true
}
NsSwitch {
on: root.adapter?.enabled ?? false
onToggled: if (root.adapter)
root.adapter.enabled = !root.adapter.enabled
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
visible: root.adapter?.enabled ?? false
Repeater {
model: Bluetooth.devices
delegate: Rectangle {
id: row
required property BluetoothDevice modelData
readonly property bool connected: modelData?.connected ?? false
Layout.fillWidth: true
implicitHeight: 44
radius: Theme.radius.button
color: ma.containsMouse ? Theme.hover : "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
Rectangle {
implicitWidth: 30
implicitHeight: 30
radius: 0
color: row.connected ? Theme.accentSoft : Theme.fillSubtle
MaterialIcon {
anchors.centerIn: parent
text: Icons.getBluetoothIcon(row.modelData?.icon ?? "")
color: row.connected ? Theme.accent : Theme.text
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: row.modelData?.name ?? "?"
color: Theme.text
elide: Text.ElideRight
Layout.fillWidth: true
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
text: row.connected ? "Connected" : (row.modelData?.paired ?? false) ? "Paired" : "Not connected"
color: row.connected ? Theme.accent : Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
StyledText {
visible: row.connected && (row.modelData?.battery ?? 0) > 0
text: Math.round((row.modelData?.battery ?? 0) * 100) + "%"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
}
}
StyledText {
visible: Bluetooth.devices.values.length === 0
text: "No devices"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
StyledText {
visible: !(root.adapter?.enabled ?? false)
text: "Bluetooth is off"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 4
implicitHeight: 1
color: Theme.barBorder
}
StyledText {
text: "Bluetooth settings"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}

View file

@ -0,0 +1,175 @@
pragma ComponentBehavior: Bound
// NoSignal Calendar popout (off the center clock). Month grid (Mon-first),
// today = filled accent circle. Below a divider, a TODAY events section
// (empty-state for now; real events can be wired from ICS/khal later).
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.components
NsPanel {
id: root
implicitWidth: 304
anchorMode: "center"
property int monthOffset: 0
readonly property date base: new Date(Time.date.getFullYear(), Time.date.getMonth() + monthOffset, 1)
readonly property int year: base.getFullYear()
readonly property int month: base.getMonth()
readonly property bool isCurrentMonth: monthOffset === 0
readonly property int todayDate: Time.date.getDate()
readonly property var cells: {
const first = new Date(year, month, 1);
const lead = (first.getDay() + 6) % 7; // Monday-first
const dim = new Date(year, month + 1, 0).getDate();
const arr = [];
for (let i = 0; i < lead; i++)
arr.push(0);
for (let d = 1; d <= dim; d++)
arr.push(d);
return arr;
}
// Header: month + year, prev/next
RowLayout {
Layout.fillWidth: true
ColumnLayout {
spacing: 0
StyledText {
text: root.base.toLocaleString(Qt.locale(), "MMMM")
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
StyledText {
text: root.year
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
Item {
Layout.fillWidth: true
}
NavBtn {
icon: "chevron_left"
onActivated: root.monthOffset--
}
NavBtn {
icon: "chevron_right"
onActivated: root.monthOffset++
}
}
// Grid: weekday header + days
GridLayout {
Layout.fillWidth: true
columns: 7
rowSpacing: 2
columnSpacing: 0
Repeater {
model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
StyledText {
required property string modelData
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: modelData
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.label
}
}
Repeater {
model: root.cells
Item {
required property int modelData
readonly property bool today: root.isCurrentMonth && modelData === root.todayDate
Layout.fillWidth: true
implicitHeight: 30
Rectangle {
anchors.centerIn: parent
visible: parent.modelData > 0
implicitWidth: 26
implicitHeight: 26
radius: 13
color: parent.today ? Theme.accent : "transparent"
StyledText {
anchors.centerIn: parent
text: parent.parent.modelData > 0 ? parent.parent.modelData : ""
color: parent.parent.today ? Theme.onAccent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
}
}
}
// Divider
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 4
implicitHeight: 1
color: Theme.barBorder
}
// TODAY events
StyledText {
text: "TODAY"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.label
font.letterSpacing: Theme.font.trackingLabel
}
StyledText {
text: "No events"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
component NavBtn: Rectangle {
property string icon
signal activated
implicitWidth: 24
implicitHeight: 24
radius: Theme.radius.button
color: ma.containsMouse ? Theme.hover : "transparent"
MaterialIcon {
anchors.centerIn: parent
text: parent.icon
color: Theme.textMuted
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.activated()
}
}
}

View file

@ -0,0 +1,139 @@
pragma ComponentBehavior: Bound
// NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks
// (signal glyph accent if connected, SSID, Connected label, lock if secured);
// connected row = accent.soft. Footer "Network settings". Wired to Network/Nmcli.
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.utils
import qs.components
NsPanel {
id: root
implicitWidth: 340
anchorMode: "right"
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Wi-Fi"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
Item {
Layout.fillWidth: true
}
NsSwitch {
on: Network.wifiEnabled
onToggled: Network.toggleWifi()
}
}
// network list
ColumnLayout {
Layout.fillWidth: true
spacing: 2
visible: Network.wifiEnabled
Repeater {
model: Network.networks
delegate: Rectangle {
id: row
required property var modelData
readonly property bool connected: modelData?.active ?? false
Layout.fillWidth: true
implicitHeight: 40
radius: Theme.radius.button
color: connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
MaterialIcon {
text: Icons.getNetworkIcon(row.modelData?.strength ?? 0)
color: row.connected ? Theme.accent : Theme.text
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: row.modelData?.ssid ?? "?"
color: Theme.text
elide: Text.ElideRight
Layout.fillWidth: true
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
visible: row.connected
text: "Connected"
color: Theme.accent
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
MaterialIcon {
visible: row.modelData?.isSecure ?? false
text: "lock"
color: Theme.textFaint
}
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
}
}
StyledText {
visible: Network.networks.length === 0
text: "No networks found"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
StyledText {
visible: !Network.wifiEnabled
text: "Wi-Fi is off"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
// footer
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 4
implicitHeight: 1
color: Theme.barBorder
}
StyledText {
text: "Network settings"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}

View file

@ -0,0 +1,171 @@
pragma ComponentBehavior: Bound
// NoSignal Notifications popout (off the bell). Header (count + DND + Clear),
// scrollable cards (tinted icon, title, body, time), empty state. Uses Notifs.
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.utils
import qs.components
NsPanel {
id: root
implicitWidth: 380
anchorMode: "right"
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Notifications"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
StyledText {
text: "· " + Notifs.notClosed.length + " new"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
Item {
Layout.fillWidth: true
}
Rectangle {
implicitWidth: 26
implicitHeight: 26
radius: 13
color: Notifs.dnd ? Theme.accentSoft : dndMa.containsMouse ? Theme.hover : "transparent"
MaterialIcon {
anchors.centerIn: parent
text: Notifs.dnd ? "do_not_disturb_on" : "dark_mode"
color: Notifs.dnd ? Theme.accent : Theme.textMuted
}
MouseArea {
id: dndMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Notifs.dnd = !Notifs.dnd
}
}
StyledText {
text: "Clear"
color: clearMa.containsMouse ? Theme.accent : Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
MouseArea {
id: clearMa
anchors.fill: parent
anchors.margins: -6
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
for (const n of Notifs.notClosed)
n.notification?.dismiss();
}
}
}
}
// cards
ColumnLayout {
Layout.fillWidth: true
spacing: 6
Repeater {
model: Notifs.notClosed
delegate: Rectangle {
id: card
required property var modelData
Layout.fillWidth: true
implicitHeight: cardRow.implicitHeight + 20
radius: Theme.radius.button
color: Theme.fillSubtle
RowLayout {
id: cardRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 10
Rectangle {
Layout.alignment: Qt.AlignTop
implicitWidth: 30
implicitHeight: 30
radius: 0
color: Theme.accentSoft
MaterialIcon {
anchors.centerIn: parent
text: Icons.getNotifIcon(card.modelData?.summary ?? "", card.modelData?.urgency ?? 1)
color: Theme.accent
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
RowLayout {
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
text: card.modelData?.summary ?? ""
color: Theme.text
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
font.weight: Font.Medium
}
StyledText {
text: card.modelData?.timeStr ?? ""
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
StyledText {
Layout.fillWidth: true
visible: text !== ""
text: card.modelData?.body ?? ""
color: Theme.textMuted
wrapMode: Text.Wrap
maximumLineCount: 3
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
}
}
StyledText {
visible: Notifs.notClosed.length === 0
Layout.topMargin: 6
text: "No notifications"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
}

View file

@ -0,0 +1,113 @@
pragma ComponentBehavior: Bound
// NoSignal Workspaces overview (off the window-title module). Row of workspace
// thumbnails (active = accent border + glow), number + name below, click to
// switch. Wired to Hypr.
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.services
import qs.components
NsPanel {
id: root
implicitWidth: 5 * 124 + 16 * 2 + Theme.size.panelPad * 2 - 16
anchorMode: "center"
readonly property int shown: Config.bar.workspaces.shown
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Workspaces"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
Item {
Layout.fillWidth: true
}
StyledText {
text: root.shown + " desktops"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
RowLayout {
Layout.fillWidth: true
spacing: 16
Repeater {
model: root.shown
delegate: ColumnLayout {
id: cell
required property int index
readonly property int wsId: index + 1
readonly property var hw: Hypr.workspaces.values.find(w => w.id === wsId) ?? null
readonly property bool active: Hypr.activeWsId === wsId
readonly property bool occupied: (hw?.lastIpcObject.windows ?? 0) > 0
readonly property string wsName: {
const n = hw?.name ?? "";
return n && n !== String(wsId) && !n.startsWith("special") ? n : "";
}
spacing: 6
Rectangle {
implicitWidth: 108
implicitHeight: 68 // 16:10-ish
radius: Theme.radius.tile
color: Qt.rgba(0, 0, 0, cell.occupied ? 0.35 : 0.5)
border.width: cell.active ? 2 : 1
border.color: cell.active ? Theme.accent : Theme.panelBorder
MaterialIcon {
anchors.centerIn: parent
visible: cell.occupied
text: "web_asset"
color: Theme.textFaint
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Hypr.dispatch(`workspace ${cell.wsId}`);
NsShell.close();
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 5
StyledText {
text: cell.wsId
color: cell.active ? Theme.accent : Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
font.weight: cell.active ? Font.DemiBold : Font.Normal
}
StyledText {
visible: cell.wsName !== ""
text: cell.wsName
color: cell.active ? Theme.accent : Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
}
}
}
}

View file

@ -0,0 +1,143 @@
pragma ComponentBehavior: Bound
// NoSignal Power Menu full-screen modal overlay. Big clock + date, then 5
// action tiles (Lock, Suspend, Log Out, Reboot, Shut Down=danger). Esc / click
// scrim cancels. Rendered full-screen by NsOverlay (open === "power").
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.components
Item {
id: root
function run(cmd: string): void {
Quickshell.execDetached(["sh", "-c", cmd]);
NsShell.close();
}
// scrim
Rectangle {
anchors.fill: parent
color: Theme.overlayBg
MouseArea {
anchors.fill: parent
onClicked: NsShell.close()
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 28
// big clock + date
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 2
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("hh:mm")
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.clockBig
font.weight: Font.Light
font.letterSpacing: Theme.font.trackingClockBig
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("dddd, d MMMM")
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.body
}
}
// action tiles
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 14
Tile {
icon: "lock"
label: "Lock"
onActivated: root.run("loginctl lock-session")
}
Tile {
icon: "bedtime"
label: "Suspend"
onActivated: root.run("systemctl suspend")
}
Tile {
icon: "logout"
label: "Log Out"
onActivated: root.run("hyprctl dispatch exit")
}
Tile {
icon: "restart_alt"
label: "Reboot"
onActivated: root.run("systemctl reboot")
}
Tile {
icon: "power_settings_new"
label: "Shut Down"
danger: true
onActivated: root.run("systemctl poweroff")
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: "Press Esc to cancel"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
component Tile: Rectangle {
id: tile
property string icon
property string label
property bool danger: false
signal activated
implicitWidth: 96
implicitHeight: 96
radius: Theme.radius.panel
color: ma.containsMouse ? Theme.accentSoft : Theme.panelBg
border.width: 1
border.color: ma.containsMouse ? (tile.danger ? Theme.danger : Theme.accent) : Theme.panelBorder
ColumnLayout {
anchors.centerIn: parent
spacing: 10
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: tile.icon
color: tile.danger ? Theme.danger : Theme.text
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: tile.label
color: tile.danger ? Theme.danger : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: tile.activated()
}
}
}

View file

@ -0,0 +1,382 @@
pragma ComponentBehavior: Bound
// NoSignal Quick Settings popout (off the media / battery pill). Avatar + user +
// uptime + power button; 2-col toggle grid; brightness + volume sliders; media
// card. Wired to the real services (Network, Bluetooth, Notifs, Audio,
// Brightness, Players); Night Light / Airplane / VPN are stateful toggles for now.
import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import qs.services
import qs.utils
import qs.components
NsPanel {
id: root
implicitWidth: 380
anchorMode: "right"
readonly property var brightMon: Brightness.monitors[0] ?? null
// Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Rectangle {
implicitWidth: 40
implicitHeight: 40
radius: 20
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
StyledText {
anchors.centerIn: parent
text: (SysInfo.user || "?").charAt(0).toUpperCase()
color: Theme.onAccent
font.family: Theme.font.family
font.pixelSize: Theme.font.body
font.weight: Font.Bold
}
}
ColumnLayout {
spacing: 0
StyledText {
text: SysInfo.user || "user"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
StyledText {
text: "uptime " + SysInfo.uptime
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
Item {
Layout.fillWidth: true
}
Rectangle {
implicitWidth: 34
implicitHeight: 34
radius: 17
color: pwrMa.containsMouse ? Theme.accentSoft : Theme.fillSubtle
MaterialIcon {
anchors.centerIn: parent
text: "power_settings_new"
color: Theme.danger
}
MouseArea {
id: pwrMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NsShell.show("power", 0)
}
}
}
// Toggle grid
GridLayout {
Layout.fillWidth: true
columns: 2
rowSpacing: 8
columnSpacing: 8
QSToggle {
icon: "wifi"
label: "Wi-Fi"
sub: Network.wifiEnabled ? Network.active?.ssid ?? "On" : "Off"
on: Network.wifiEnabled
onToggled: Network.toggleWifi()
}
QSToggle {
icon: "bluetooth"
label: "Bluetooth"
sub: Bluetooth.defaultAdapter?.enabled ? "On" : "Off"
on: Bluetooth.defaultAdapter?.enabled ?? false
onToggled: if (Bluetooth.defaultAdapter)
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
}
QSToggle {
icon: "do_not_disturb_on"
label: "Do Not Disturb"
sub: Notifs.dnd ? "On" : "Off"
on: Notifs.dnd
onToggled: Notifs.dnd = !Notifs.dnd
}
QSToggle {
id: nightTog
icon: "nightlight"
label: "Night Light"
sub: on ? "Warm 3500K" : "Off"
onToggled: on = !on
}
QSToggle {
id: airTog
icon: "airplanemode_active"
label: "Airplane"
sub: on ? "On" : "Off"
onToggled: on = !on
}
QSToggle {
id: vpnTog
icon: "vpn_key"
label: "VPN"
sub: on ? "On" : "Off"
onToggled: on = !on
}
}
// Sliders
QSSlider {
icon: "brightness_6"
value: root.brightMon?.brightness ?? 0
visible: root.brightMon !== null
onMoved: v => root.brightMon?.setBrightness(v)
}
QSSlider {
icon: Audio.muted ? "volume_off" : "volume_up"
value: Audio.volume
onMoved: v => Audio.setVolume(v)
}
// Media card
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 4
implicitHeight: 80
radius: Theme.radius.tile
color: Theme.fillSubtle
visible: Players.active !== null
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
Rectangle {
implicitWidth: 56
implicitHeight: 56
radius: 0
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
MaterialIcon {
anchors.centerIn: parent
text: "music_note"
color: Theme.onAccent
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
StyledText {
Layout.fillWidth: true
text: Players.active?.trackTitle || "Awake"
color: Theme.text
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.body
font.weight: Font.DemiBold
}
StyledText {
Layout.fillWidth: true
text: Players.active?.trackArtist || ""
color: Theme.textMuted
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
RowLayout {
spacing: 16
Layout.topMargin: 2
MediaBtn {
icon: "skip_previous"
onActivated: Players.previous()
}
MediaBtn {
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
onActivated: Players.togglePlaying()
}
MediaBtn {
icon: "skip_next"
onActivated: Players.next()
}
}
}
}
}
// Reusable bits
component QSToggle: Rectangle {
id: tile
property string icon
property string label
property string sub
property bool on: false
signal toggled
Layout.fillWidth: true
implicitHeight: 56
radius: Theme.radius.button
color: tile.on ? Theme.accentSoft : Theme.fillSubtle
border.width: tile.on ? 1 : 0
border.color: Theme.accent
RowLayout {
anchors.fill: parent
anchors.margins: 11
spacing: 10
Rectangle {
implicitWidth: 30
implicitHeight: 30
radius: 15
color: tile.on ? Theme.accent : Qt.rgba(1, 1, 1, 0.06)
MaterialIcon {
anchors.centerIn: parent
text: tile.icon
color: tile.on ? Theme.onAccent : Theme.text
}
}
ColumnLayout {
spacing: 0
StyledText {
text: tile.label
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
font.weight: Font.Medium
}
StyledText {
text: tile.sub
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: tile.toggled()
}
}
component QSSlider: RowLayout {
id: s
property string icon
property real value: 0
signal moved(real v)
Layout.fillWidth: true
spacing: 10
MaterialIcon {
text: s.icon
color: Theme.textMuted
}
Rectangle {
id: track
Layout.fillWidth: true
implicitHeight: 6
radius: 3
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 3
color: Theme.accent
}
Rectangle {
x: parent.width * Math.max(0, Math.min(1, s.value)) - width / 2
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 14
implicitHeight: 14
radius: 7
color: Theme.accent
}
MouseArea {
anchors.fill: parent
onPressed: e => s.moved(Math.max(0, Math.min(1, e.x / width)))
onPositionChanged: e => {
if (pressed)
s.moved(Math.max(0, Math.min(1, e.x / width)));
}
}
}
StyledText {
text: Math.round(s.value * 100) + "%"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
component MediaBtn: Rectangle {
property string icon
signal activated
implicitWidth: 28
implicitHeight: 28
radius: 14
color: mbm.containsMouse ? Theme.hover : "transparent"
MaterialIcon {
anchors.centerIn: parent
text: parent.icon
color: Theme.text
}
MouseArea {
id: mbm
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.activated()
}
}
}

View file

@ -0,0 +1,100 @@
pragma ComponentBehavior: Bound
// NoSignal System Tray popout (off the tray pill). Grid of StatusNotifierItems;
// left-click activates, right-click opens the item's menu. Uses SystemTray.
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray
import qs.services
import qs.components
NsPanel {
id: root
implicitWidth: 260
anchorMode: "right"
StyledText {
text: "System Tray"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
GridLayout {
Layout.fillWidth: true
columns: 4
rowSpacing: 8
columnSpacing: 8
visible: SystemTray.items.values.length > 0
Repeater {
model: SystemTray.items
delegate: ColumnLayout {
id: cell
required property var modelData
Layout.alignment: Qt.AlignHCenter
spacing: 4
Rectangle {
Layout.alignment: Qt.AlignHCenter
implicitWidth: 44
implicitHeight: 44
radius: Theme.radius.tile
color: cellMa.containsMouse ? Theme.hover : Theme.fillSubtle
Image {
anchors.centerIn: parent
width: 22
height: 22
source: cell.modelData?.icon ?? ""
sourceSize.width: 44
sourceSize.height: 44
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: cellMa
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.RightButton)
cell.modelData?.display(cell, 0, height);
else if (cell.modelData?.onlyMenu)
cell.modelData?.display(cell, 0, height);
else
cell.modelData?.activate();
}
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: 50
text: cell.modelData?.title || cell.modelData?.id || ""
color: Theme.textMuted
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
}
StyledText {
visible: SystemTray.items.values.length === 0
text: "No tray items"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}

35
services/NsShell.qml Normal file
View file

@ -0,0 +1,35 @@
pragma Singleton
pragma ComponentBehavior: Bound
// NoSignal shell-level popup state. "One panel open at a time" `open` is the
// id of the active panel ("" = none). `anchorX` is the scene x of the trigger
// (a bar pill) center, used to position under-trigger / right-anchored popups.
// The NsBar pills call toggle(); the NsOverlay reads `open` and renders the panel.
import QtQuick
import Quickshell
Singleton {
id: root
property string open: ""
property real anchorX: 0
function toggle(id: string, x: real): void {
if (root.open === id) {
root.open = "";
} else {
root.open = id;
root.anchorX = x;
}
}
function show(id: string, x: real): void {
root.open = id;
root.anchorX = x;
}
function close(): void {
root.open = "";
}
}

91
services/Theme.qml Normal file
View file

@ -0,0 +1,91 @@
pragma Singleton
pragma ComponentBehavior: Bound
// NoSignal design-system tokens (interface redesign 2026-06-14).
// Fixed dark-glass chrome + text, but the ACCENT is Material You (wallpaper-
// derived) per the user's choice. Components in the redesigned bar/panels read
// from here instead of hard-coding hex values. See the design handoff README.
//
// NB: the "backdrop blur" in the spec is done at the compositor level (Hyprland
// layerrules blur on the shell namespaces), not in QML these fills are the
// translucent surface colours that sit over that blur.
import QtQuick
import Quickshell
import qs.services
Singleton {
id: root
// Accent: Material You, dynamic from wallpaper
readonly property color accent: Colours.palette.m3primary
readonly property color onAccent: Colours.palette.m3onPrimary // contrasting text/icon on accent fills
// accent.soft translucent accent for active-module bg / selected rows
readonly property color accentSoft: Qt.alpha(accent, 0.15)
readonly property color accentSofter: Qt.alpha(accent, 0.13)
// Secondary semantic accents (fixed)
readonly property color blue: "#8FB5D6" // avatar gradient, event tint
readonly property color green: "#86C09A" // now-playing tint
readonly property color danger: "#E0746A" // power button + Shut Down
// Surfaces: fixed dark glass
readonly property color barBg: Qt.rgba(15 / 255, 18 / 255, 24 / 255, 0.60)
readonly property color barBorder: Qt.rgba(1, 1, 1, 0.06)
readonly property color panelBg: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.90)
readonly property color panelBorder: Qt.rgba(1, 1, 1, 0.09)
readonly property color overlayBg: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.82)
// panel drop shadow + inset highlight
readonly property color shadow: Qt.rgba(0, 0, 0, 0.55)
readonly property color insetHighlight: Qt.rgba(1, 1, 1, 0.05)
// Text (fixed)
readonly property color text: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.92)
readonly property color textMuted: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.50)
readonly property color textFaint: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.34)
// Fills / states (fixed)
readonly property color hover: Qt.rgba(1, 1, 1, 0.07)
readonly property color fillSubtle: Qt.rgba(1, 1, 1, 0.05)
// Typography
readonly property string fontFamily: "JetBrainsMono Nerd Font"
readonly property QtObject font: QtObject {
readonly property string family: root.fontFamily
readonly property int bar: 12
readonly property int panelTitle: 13
readonly property int body: 12
readonly property int bodySmall: 11
readonly property int label: 10 // section labels (uppercase, tracked)
readonly property int meta: 10 // times, %, device sub
readonly property int clockBig: 52 // power-menu clock
readonly property real trackingLabel: 1.6
readonly property real trackingBar: -0.1
readonly property real trackingClockBig: 2
}
// Shape
// NoSignal: no curved corners anywhere all rectangular elements are square.
readonly property QtObject radius: QtObject {
readonly property int panel: 0
readonly property int button: 0
readonly property int barModule: 0
readonly property int tile: 0
readonly property int full: 1000 // genuine circles only (toggle icons, avatar, switch knob)
}
// Spacing / sizing
readonly property QtObject size: QtObject {
readonly property int barHeight: 38 // exclusive zone
readonly property int popoutTopRight: 44 // right-anchored panels
readonly property int popoutTopCenter: 48 // centered panels
readonly property int panelPad: 16 // 14-18
readonly property int gap: 12 // 8-16
readonly property int rowMin: 30 // panel row hit target
readonly property int barIcon: 16 // 15-17
}
// Animation
readonly property int hoverMs: 120
}

View file

@ -6,6 +6,7 @@
import "modules" import "modules"
import "modules/drawers" import "modules/drawers"
import "modules/nsbar"
import "modules/background" import "modules/background"
import "modules/areapicker" import "modules/areapicker"
import "modules/lock" import "modules/lock"
@ -17,7 +18,9 @@ ShellRoot {
GSFLoader {} GSFLoader {}
Background {} Background {}
Drawers {} Drawers {} // NoSignal: kept for launcher/OSD/lock; its bar is collapsed (see BarWrapper)
NsBar {} // NoSignal redesign: slim glass top bar
NsOverlay {} // NoSignal redesign: floating popout panels
AreaPicker {} AreaPicker {}
Lock { Lock {
id: lock id: lock