Compare commits

...
Sign in to create a new pull request.

24 commits

Author SHA1 Message Date
0ae8593113 fix(nosignal): F-sudo focus via hyprctl (stayfocused invalid in Hypr 0.55)
VM-cert of round 4 surfaced a Hyprland config-error banner: the shipped
`windowrule = stayfocused on, match:class nosignal-sudo` is rejected at parse
time — `stayfocused` is not a valid field in Hyprland 0.55's match: windowrule
grammar (only float/center/pin/size are). The line was skipped, so the prompt
never got the focus rule and an error banner showed on every boot.

Replace it: keep the dedicated nosignal-sudo class (float/center/pin/size, all
valid — set in the builder) and pull keyboard focus to the prompt from here via
`hyprctl dispatch focuswindow class:^(nosignal-sudo)$` (the handover's "or
activate it" option), nudged a few times to cover terminal startup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:26:30 +01:00
e42538896f fix(nosignal): hardware-test round 4 — Wi-Fi join, multi-monitor popouts, sudo focus
From the 2026-06-20 Acer muxless-Optimus hardware test:

- F-wifi (NsNetwork.qml): the Wi-Fi panel was display-only — no onClicked,
  no password field — so a secured network could not be joined from the GUI.
  Wire the existing Network service: click to connect; open/saved networks
  connect directly; secured-without-profile expands an inline password field
  (StyledTextField, echoMode Password). Also strip a stray NUL byte that the
  on-box edit left at the `?? " "` sentinel. HW-verified on the test laptop.
- F-multimon (NsShell.qml + NsBar.qml + NsOverlay.qml): bar popouts mirrored
  on every monitor. Add a `screen` identity to NsShell; pills pass their
  output name; each NsOverlay only renders when it owns the open panel ("" =
  unscoped, for the global power modal). HW-verified dual-monitor.
- F-sudo (SudoToggleRow.qml): the passwordless-sudo prompt used the shared
  TUI.float class with no focus rule, so keystrokes missed it and three blank
  tries tripped pam_faillock with no feedback. Use a dedicated `nosignal-sudo`
  class (builder ships float/center/pin/stayfocused rules) and call the new
  `nosignal-sudo-toggle enable-tui` wrapper, which keeps the terminal open on
  failure so the reason is visible.

Add .gitattributes (*.qml/*.conf/*.json text) for clean diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:49:18 +01:00
2cc089b41f fix(nosignal): hardware-test round 3 — notif Clear + Game Mode → DeckShift
F2: NsNotifications.qml "Clear" mirrors the service (Notifs.list.slice() →
n.close()) instead of n.notification?.dismiss() — cards now actually leave the
list, and it works for disk-restored notifications (whose notification is null).

C2: Toggles.qml gameMode delegate launches the guarded switch-to-gaming via
Quickshell.execDetached (debounced + disabled + 5s timer so a double-click can't
fire two SDDM restarts), and the tile is hidden entirely when DeckShift isn't
installed (Process probe for the gamescope session file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 06:34:08 +01:00
44b257ce85 feat(nosignal): CachyOS repo toggle + wire Quick Settings placebos
- CachyRepoToggleRow.qml (Settings -> Services): sudoless CachyOS optimized
  repos + linux-cachyos kernel switch, auto-tier (v4>v3); polls
  nosignal-cachy-repo status, runs enable/disable in a floating kitty. Inserted
  after SudoToggleRow in ServicesPage.qml.
- NsQuickSettings.qml: Night Light now drives hyprsunset (tile bound to the real
  pgrep'd process, not a flag); Airplane drives nmcli + bluetoothctl and binds to
  real radio state; VPN placebo tile removed (no backend).

Both built + live-tested on the reference box (06-17 PM). Helper script, sudoers
and hyprsunset dep ship in the nosignal builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:06:20 +01:00
53c5833de0 fix(nosignal): hardware-test feedback round 2 — weather, dashboard, tray
From the 2026-06-15 on-box hardware test (all 6 prior fixes confirmed; these
are the new findings, baked into the fork source rather than shipped as
pacman-hook patches, per the Phase-2 "we own the shell" approach):

- F-W1 (Weather.qml): fetch at startup. Stock only called reload() on a config
  CHANGE, so a saved location blanked after reboot and an empty/auto location
  never populated on a fresh boot. Add Component.onCompleted: reload() and
  switch the hourly Timer to reload() (re-detects auto/IP too).
- F-W3 (Weather.qml): 7-day forecast showed "undefined°" in °F mode — only
  maxTempC/minTempC were stored. Store rounded maxTempF/minTempF (and round °C).
- F-W2 (Weather.qml): empty/auto-IP location never populated — the in-shell
  ipinfo.io fetch didn't set loc (provider-specific; the city path works via
  the same Requests.get). Repair attempt: defensive JSON parse + error callback
  + fallback to geojs.io. Compiled Requests module not inspectable, so this is
  best-effort; a saved city still works regardless.
- F-D1a (modules/dashboard/*.qml): square the 25 card radii bound to the
  compiled Tokens.rounding.* Material defaults (which ignore shell.json
  rounding.scale=0) so the dashboard matches the square redesign.
- F-D1b (Glyphs.qml): remap the weather glyphs to the Material-weather family
  (every codepoint verified present in JetBrainsMono Nerd Font Mono); the old
  Font-Awesome codepoints were absent (rendered "?") and thunderstorm /
  snowing_heavy were unmapped.
- F-T1 (NsBar.qml): gate the system-tray pill on SystemTray.items count so it
  only shows when an app registers a tray icon (was opening an empty panel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:04:28 +01:00
f21e5b5eaf fix(nosignal): hardware-test feedback — additions icons, power menu, weather picker
From the 2026-06-15 hardware-test feedback (redesign passed end-to-end):

- Glyphs: map the 12 unmapped additions-page icon names (brush, cloud_sync,
  data_object, deployed_code, movie_edit, neurology, psychology, robot_2,
  smart_toy, sports_esports, videocam, vpn_lock) so the nexus Additional
  Software page stops rendering "?" boxes. Verified Nerd Font codepoints.
- Glyphs: cp() now uses String.fromCodePoint, not fromCharCode — the 5 MDI
  glyphs above 0xFFFF were being truncated by the 16-bit fromCharCode.
- Shortcuts: Super+Escape (session) now drives the redesigned full-screen
  Power Menu via NsShell.toggle("power"); old caelestia session panel is
  now dead code.
- nexus LanguageAndRegion: replace the "coming soon" weather placeholder with
  a real location input bound to GlobalConfig.services.weatherLocation (the
  Weather backend already geocodes / IP-auto-detects).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:02:53 +01:00
83c6b62c13 feat(nosignal): finish Nerd Font icon remap — core surfaces
Convert remaining reachable surfaces to NsIcon (launcher, utilities, windowinfo,
sidebar, dashboard, notification toast, shared controls IconTextButton/Menu/
SplitButton/ToggleButton/CollapsibleSection/CustomSpinBox, CoverArt). Handles
the `sourceComponent: MaterialIcon {` form and icon-via-alias controls.

- IconTextButton: alias -> plain `icon` property + bound NsIcon (cross-file alias
  to a custom property was rejected).
- Reverted IconButton `font:` (the controls-wide font->fontStyle sweep wrongly
  hit ButtonBase, which uses font not fontStyle).
- Glyphs map: +31 names (dashboard/utilities/launcher leftovers) — no "?" glyphs.

Old collapsed bar + old lock sub-components left as-is (unused, never render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:59:28 +01:00
c4738ae974 fix(nosignal): notification toasts — below the bar + no blob curve
- Position the toast wrapper at y:46 so toasts clear the 38px NsBar (they were
  overlapping it; caelestia's Content negates the wrapper top margin, so the
  offset must be on the wrapper's y, not its margin).
- Fully collapse the notifsBg blob (visible:false + implicitWidth/Height 0) —
  opacity/visible alone don't hide a BlobGroup member, which left a rounded
  corner peeking under the square card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:14:36 +01:00
f6a2584d31 fix(nosignal): square notification toasts + guard lock screen.name
- Notification toasts: square the cards + round urgency/icon badges
  (Tokens.rounding.full ignored rounding.scale) and hide/flatten the notifsBg
  blob (deformAmount 0, radius 0, opacity 0) so toasts are flat square cards.
- LockSurface: guard screen?.name to silence a transient init warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:00:16 +01:00
dcce2440f6 fix(nosignal): square the OSD (volume/brightness) to match the design
The volume/brightness OSD popped out as a rounded caelestia blob in the dynamic
surface colour. Square it + match the design glass:
- osd/Content: draw a flat square Theme.panelBg + 1px border background.
- ContentWindow: osdBg blob deformAmount 0 + radius 0 + opacity 0 (hidden;
  the OSD now owns its background).
- FilledSlider: track + handle radius -> 0 (Tokens.rounding.full ignored
  rounding.scale, so it stayed round).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:54:35 +01:00
16de3f16fe feat(nosignal): NoSignal logo + V1 on About; fully square the UI
- About page: replace caelestia AnimatedLogo with the NoSignal logo
  (assets/nosignal-logo.png), title "NoSignal", version reads "V1".
- Square every remaining curved element to match the no-curves design:
  avatars, toggle/icon circles, power/media buttons, slider tracks+handles,
  NsSwitch track+knob, notification dot, calendar today, lock avatar -> radius 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:42:08 +01:00
7c06b808ae feat(nosignal): redesign lock screen to match handoff
Rewrite the WlSessionLockSurface visuals to the NoSignal lock design while
keeping the secure session-lock + PAM machinery (Pam buffer/handleKey/state,
WlSessionLock unlock):

- Blurred screencopy wallpaper + dark scrim; top status row (lock+Locked /
  wifi/bt/battery); centred 108px ExtraLight clock + date; gradient avatar +
  username; password field (key glyph, masked dots, peach enter glyph, error
  border) fed to pam.handleKey; bottom now-playing strip + "Press Enter to
  unlock". JetBrains Mono / Nerd Font (NsIcon) / Theme tokens.
- Escape does not dismiss; correct password (PAM) unlocks; wrong password shows
  inline error. Verified in VM: renders, masks, unlocks.
- Glyphs: add login (enter arrow) + key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:30:20 +01:00
83d5de1ba3 feat(nosignal): Nerd Font icons in IconButton (controls-wide)
Convert the shared IconButton control to render via NsIcon, so every icon-button
across the shell (nexus, lock, utilities, launcher chrome, ...) uses Nerd Font.
Alias icon -> label.icon; internal MaterialIcon -> NsIcon (sizes from root.font
via fontStyle); dropped the Material-Symbols vertical-center fudge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:19:16 +01:00
cf3c0d823d feat(nosignal): Nerd Font icon remap — nexus (Settings)
Convert caelestia's nexus Settings to Nerd Font icons (drop-in NsIcon).

- NsIcon hardened into a MaterialIcon drop-in: accepts fontStyle (sizes from
  it, keeps the NF Mono family), plus no-op fill/grade/animate for API parity.
- Glyphs map extended with caelestia core names: nexus static icons, nav/page
  icons (palette, wallpaper, dashboard, devices_other, extension, globe, info,
  monitor, shuffle, workspaces, ...), weather + app/device icons.
- Converted all MaterialIcon in modules/nexus/ to NsIcon; fixed icon aliases
  (NavRow/SliderRow .text -> .icon); reverted IconButton font: (it has font,
  not fontStyle, and renders its own icon).

Remaining core surfaces still Material Symbols (no breakage): launcher, osd,
lock, sidebar, utilities — to convert next with any further Glyphs entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:16:01 +01:00
6a933aa260 feat(nosignal): Nerd Font icon remap — bar + popouts
Route the redesign's icons through JetBrains Mono Nerd Font instead of Material
Symbols, for visual consistency with the JetBrains Mono text.

- services/Glyphs.qml: Material-Symbol-name -> Nerd Font (Font Awesome range)
  codepoint map (via String.fromCharCode), covering bar/panel icons + the
  dynamic Icons.getX() returns; get() falls back to a "?" glyph.
- components/NsIcon.qml: reactive icon component (input `icon` = name -> mapped
  glyph), so dynamic icons (volume/battery/play-pause) stay live. Uses the
  "Mono" NF variant so glyphs center in a fixed cell.
- Converted all MaterialIcon usages in NsBar + the 9 panels to NsIcon.

caelestia core (nexus/launcher/osd/lock) still uses Material Symbols — no
breakage (mixed), to be converted next with an expanded Glyphs map.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:01:44 +01:00
36f6f5a157 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>
2026-06-14 10:36:33 +01:00
8bada1d61a feat(bar): popouts drop down from the top bar — Phase B
The bar hover fly-outs (wifi/audio/bluetooth/tray/active-window) slid out
sideways from the old left edge; now they drop DOWN from the top bar:

- ClipWrapper.qml: the collapsing/slide axis is now height (was width); the
  popout is positioned along the bar by x (from the hovered widget's x-centre,
  via currentCenter) and docks at the top edge; the dock<->detach animation
  guard moves to the x (along-bar) axis. The content slides in via a negative
  top margin instead of left margin.
- Wrapper.qml: mirror the size-transition animation guards (height is now the
  always-animated extent axis, width the gated along-bar axis).

Verified live in the dev VM: all popouts open downward under their widget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:34:44 +01:00
b1e2bb3bc6 feat(bar): horizontal top bar — Phase A geometry (NoSignal layout)
Convert the bar from a vertical left-edge strip to a horizontal top bar. The
shell had no orientation abstraction, so this flips the geometry across the bar
and the drawer/region/interaction layer:

- Bar.qml: ColumnLayout -> RowLayout; hit-testing (checkPopout/handleWheel),
  scroll halves and popout-center anchoring all keyed off x instead of y;
  first/last padding -> left/right margins.
- BarWrapper.qml: the bar's "thickness" is now its height (clampedHeight/
  contentHeight/implicitHeight); content anchors left/right/top.
- drawers: Exclusions (exclusive zone moved left->top), Panels (content inset
  topMargin = bar height), Regions + ContentWindow (panel->window coordinate
  mapping and the thick border flip left->top), Interactions (bar-region check
  y<barHeight, panel Y-origin uses bar height, scroll passes x, bar drag-reveal
  is vertical).
- widgets: Clock (Row, HH:MM with a colon), StatusIcons (Row), Workspaces +
  Workspace (Row), Tray (Row, expand chevron at the right) — each sizes to the
  bar height instead of width.

Verified live in the dev VM: top bar renders with centered-clock entries, the
exclusion zone keeps windows below the bar, and the dashboard drops down
correctly. Popout fly-outs still anchor sideways (Phase B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:26:55 +01:00
152923f5d8 fold: bake NoSignal shell patches into the fork (Phase 2)
We now own the pinned shell, so the runtime patches that the pacman-hook +
patch-script machinery re-applied after every caelestia-shell upgrade are
baked directly into source. Folds in all five caelestia-shell-targeting
patches from the builder's `os updates/`:

- Lock PAM (F1, blocker): ship faillock-free assets/pam.d/caelestia and point
  modules/lock/Pam.qml's passwd PamContext at it (config "passwd" -> "caelestia").
  A desktop screen-lock must never lock the user out of their own session.
- Updates page: add modules/nexus/pages/UpdatesPage.qml + register it in the
  first System slot of PageCompRegistry.qml.
- Additions page: add modules/nexus/pages/AdditionsPage.qml + register it in the
  Plugins slot; relabel Plugins -> Additions in PageRegistry.qml.
- Sudo toggle: add modules/nexus/common/SudoToggleRow.qml + insert it into
  ServicesPage.qml after the Smart colour scheme toggle.
- Wi-Fi wrong-password recovery: NetworkConnection.qml saved-profile branch now
  passes a real callback that forgets the bad profile and reopens the dialog.

The builder will drop the corresponding patch-*.sh calls + pacman hooks and
bump NOSIGNAL_SHELL_COMMIT to this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:08:59 +01:00
28allday
71288e2126 packaging: DEV.md — dev workflow + layout map for the fork
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:55:11 +01:00
28allday
f39dc4e6c7 packaging: dev-sync.sh background loop for seamless live VM preview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:51:21 +01:00
28allday
f13126eb53 packaging: one-shot dev-mount.sh for the live VM preview loop
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:38:55 +01:00
28allday
50a38546cd packaging: add dev-setup.sh for live VM shell preview (9p fork mount)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:31:18 +01:00
28allday
70e5639560 packaging: add nosignal-shell PKGBUILD (builds the fork, provides/conflicts caelestia-shell)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:08:11 +01:00
121 changed files with 4261 additions and 662 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
*.qml text
*.conf text
*.json text

BIN
assets/nosignal-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

20
assets/pam.d/caelestia Normal file
View file

@ -0,0 +1,20 @@
#%PAM-1.0
# Caelestia lock-screen authentication (NoSignal).
#
# Deliberately does NOT use pam_faillock: a desktop screen-lock must never lock
# the user out of their OWN running session. With faillock in the path (the old
# "passwd"->system-auth route), a few failed unlocks tripped a temporary account
# lock that then refused the *correct* password until /run/faillock cleared on
# reboot. See README.md / finding F1.
#
# pam_unix verifies the password supplied via the PAM conversation (the lock UI)
# using the setuid unix_chkpwd helper, so it works for the uid-1000 Quickshell
# process. Add pam_systemd_home below if/when systemd-homed users are supported.
#
# F8a: deliberately NO `nullok` — a screen lock must never accept an empty
# password. (Only changes behaviour for empty-password accounts, which are
# correctly rejected; normal password auth is unaffected.)
auth required pam_unix.so
account required pam_unix.so
password required pam_unix.so
session required pam_unix.so

View file

@ -12,10 +12,10 @@ ColumnLayout {
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
animate: true animate: true
text: root.icon icon: root.icon
fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).weight(Font.Bold).build() fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).weight(Font.Bold).build()
} }

32
components/NsIcon.qml Normal file
View file

@ -0,0 +1,32 @@
pragma ComponentBehavior: Bound
// NoSignal icon: renders a Nerd Font (JetBrainsMono NF) glyph from a Material
// Symbol name via the Glyphs map. Drop-in for MaterialIcon but the input is
// `icon` (the name) instead of `text`, so dynamic bindings stay reactive.
import QtQuick
import qs.services
Text {
id: root
property string icon: ""
property int size: 16
// MaterialIcon API parity so NsIcon is a drop-in (these are no-ops here
// Nerd Font has no fill/grade variable axes; sizing comes from fontStyle).
property real fill: 0
property int grade: 0
property bool animate: false
property var fontStyle: null
renderType: Text.NativeRendering
textFormat: Text.PlainText
text: Glyphs.get(root.icon)
// "Mono" variant centers each icon glyph in a fixed cell (the plain NF
// variant uses a wide advance, which left/right-shifts centered icons).
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: root.fontStyle ? (root.fontStyle.pixelSize > 0 ? root.fontStyle.pixelSize : Math.round((root.fontStyle.pointSize || 12) * 1.33)) : root.size
color: Theme.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}

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: 0
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: 0
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

@ -45,8 +45,8 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
} }
MaterialIcon { NsIcon {
text: "expand_more" icon: "expand_more"
rotation: root.expanded ? 180 : 0 rotation: root.expanded ? 180 : 0
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium

View file

@ -110,11 +110,11 @@ RowLayout {
} }
} }
MaterialIcon { NsIcon {
id: upIcon id: upIcon
anchors.centerIn: parent anchors.centerIn: parent
text: "keyboard_arrow_up" icon: "keyboard_arrow_up"
color: Colours.palette.m3onPrimary color: Colours.palette.m3onPrimary
} }
} }
@ -145,11 +145,11 @@ RowLayout {
onReleased: timer.stop() onReleased: timer.stop()
} }
MaterialIcon { NsIcon {
id: downIcon id: downIcon
anchors.centerIn: parent anchors.centerIn: parent
text: "keyboard_arrow_down" icon: "keyboard_arrow_down"
color: Colours.palette.m3onPrimary color: Colours.palette.m3onPrimary
} }
} }

View file

@ -16,7 +16,7 @@ Slider {
background: StyledRect { background: StyledRect {
color: Colours.layer(Colours.palette.m3surfaceContainer, 2) color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Tokens.rounding.full radius: 0
StyledRect { StyledRect {
anchors.left: parent.left anchors.left: parent.left
@ -51,7 +51,7 @@ Slider {
anchors.fill: parent anchors.fill: parent
color: Colours.palette.m3inverseSurface color: Colours.palette.m3inverseSurface
radius: Tokens.rounding.full radius: 0
MouseArea { MouseArea {
id: handleInteraction id: handleInteraction

View file

@ -6,7 +6,7 @@ import qs.services
ButtonBase { ButtonBase {
id: root id: root
property alias icon: label.text property alias icon: label.icon
readonly property alias label: label readonly property alias label: label
font: Tokens.font.icon.medium font: Tokens.font.icon.medium
@ -34,11 +34,10 @@ ButtonBase {
return h; return h;
} }
MaterialIcon { NsIcon {
id: label id: label
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 1 // AHHHHHHH material symbols whyyyy
color: root.onColour color: root.onColour
fontStyle: root.font fontStyle: root.font
fill: !root.isToggle || root.internalChecked ? 1 : 0 fill: !root.isToggle || root.internalChecked ? 1 : 0

View file

@ -7,7 +7,7 @@ import qs.services
ButtonBase { ButtonBase {
id: root id: root
property alias icon: iconLabel.text property string icon
property alias text: label.text property alias text: label.text
readonly property alias iconLabel: iconLabel readonly property alias iconLabel: iconLabel
@ -44,9 +44,10 @@ ButtonBase {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
id: iconLabel id: iconLabel
icon: root.icon
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
color: root.onColour color: root.onColour

View file

@ -157,9 +157,9 @@ MouseArea {
anchors.margins: Tokens.padding.medium anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
text: item.modelData?.icon ?? "" icon: item.modelData?.icon ?? ""
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
} }
@ -176,8 +176,8 @@ MouseArea {
active: item.modelData?.trailingIcon.length > 0 active: item.modelData?.trailingIcon.length > 0
visible: active visible: active
sourceComponent: MaterialIcon { sourceComponent: NsIcon {
text: item.modelData.trailingIcon icon: item.modelData.trailingIcon
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
} }
} }

View file

@ -64,12 +64,12 @@ Row {
anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4)
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
id: iconLabel id: iconLabel
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true animate: true
text: root.active?.activeIcon ?? root.fallbackIcon icon: root.active?.activeIcon ?? root.fallbackIcon
color: root.disabled ? root.disabledTextColour : root.textColour color: root.disabled ? root.disabledTextColour : root.textColour
fill: 1 fill: 1
} }
@ -116,13 +116,13 @@ Row {
onClicked: root.expanded = !root.expanded onClicked: root.expanded = !root.expanded
} }
MaterialIcon { NsIcon {
id: expandIcon id: expandIcon
anchors.centerIn: parent anchors.centerIn: parent
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4) anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
text: "expand_more" icon: "expand_more"
color: root.disabled ? root.disabledTextColour : root.textColour color: root.disabled ? root.disabledTextColour : root.textColour
rotation: root.expanded ? 180 : 0 rotation: root.expanded ? 180 : 0

View file

@ -54,12 +54,12 @@ StyledRect {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
id: toggleBtnIcon id: toggleBtnIcon
visible: !!text visible: !!text
fill: root.toggled ? 1 : 0 fill: root.toggled ? 1 : 0
text: root.icon icon: root.icon
color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
fontStyle: Tokens.font.icon.size(root.iconSize).build() fontStyle: Tokens.font.icon.size(root.iconSize).build()

View file

@ -57,11 +57,11 @@ Item {
} }
} }
MaterialIcon { NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
grade: 200 grade: 200
text: image.status === Image.Error ? "broken_image" : "art_track" icon: image.status === Image.Error ? "broken_image" : "art_track"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build() fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build()
opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0 opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0

View file

@ -54,8 +54,9 @@ Scope {
onPressed: { onPressed: {
if (root.hasFullscreen) if (root.hasFullscreen)
return; return;
const visibilities = Visibilities.getForActive(); // NoSignal: drive the redesigned full-screen Power Menu (NsOverlay),
visibilities.session = !visibilities.session; // not the old caelestia session panel (now dead code).
NsShell.toggle("power", 0);
} }
} }

View file

@ -10,14 +10,18 @@ import Caelestia.Config
import qs.components import qs.components
import qs.services import qs.services
ColumnLayout { // NoSignal: horizontal top bar. The widgets flow left->right in a RowLayout and
// the bar's "thickness" is its HEIGHT; hit-testing / scroll / popout anchoring all
// key off the x-axis (the position along the bar). Upstream caelestia is a
// vertical left-edge bar this is the NoSignal layout fork (Phase 3).
RowLayout {
id: root id: root
required property ShellScreen screen required property ShellScreen screen
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
required property bool fullscreen required property bool fullscreen
readonly property int vPadding: Tokens.padding.large readonly property int hPadding: Tokens.padding.large
function closeTray(): void { function closeTray(): void {
if (!Config.bar.tray.compact) if (!Config.bar.tray.compact)
@ -31,8 +35,8 @@ ColumnLayout {
} }
} }
function checkPopout(y: real): void { function checkPopout(x: real): void {
const ch = childAt(width / 2, y) as WrappedLoader; const ch = childAt(x, height / 2) as WrappedLoader;
if (ch?.id !== "tray") if (ch?.id !== "tray")
closeTray(); closeTray();
@ -43,24 +47,24 @@ ColumnLayout {
} }
const id = ch.id; const id = ch.id;
const top = ch.y; const left = ch.x;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) { if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
const items = (ch.item as StatusIcons).items; const items = (ch.item as StatusIcons).items;
const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); const icon = items.childAt(mapToItem(items, x, 0).x, items.height / 2);
if (icon) { if (icon) {
popouts.currentName = icon.name; popouts.currentName = icon.name;
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y); popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x);
popouts.hasCurrent = true; popouts.hasCurrent = true;
} }
} else if (id === "tray" && Config.bar.popouts.tray) { } else if (id === "tray" && Config.bar.popouts.tray) {
const tray = ch.item as Tray; const tray = ch.item as Tray;
if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, x, tray.implicitHeight / 2)))) {
const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); const index = Math.floor(((x - left - tray.padding * 2 + tray.spacing) / tray.layout.implicitWidth) * tray.items.count);
const trayItem = tray.items.itemAt(index); const trayItem = tray.items.itemAt(index);
if (trayItem) { if (trayItem) {
popouts.currentName = `traymenu${index}`; popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, trayItem.implicitWidth / 2, 0).x);
popouts.hasCurrent = true; popouts.hasCurrent = true;
} else { } else {
popouts.hasCurrent = false; popouts.hasCurrent = false;
@ -71,13 +75,13 @@ ColumnLayout {
} }
} else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) {
popouts.currentName = id.toLowerCase(); popouts.currentName = id.toLowerCase();
popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; popouts.currentCenter = (ch.item as Item).mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x ?? 0;
popouts.hasCurrent = true; popouts.hasCurrent = true;
} }
} }
function handleWheel(y: real, angleDelta: point): void { function handleWheel(x: real, angleDelta: point): void {
const ch = childAt(width / 2, y) as WrappedLoader; const ch = childAt(x, height / 2) as WrappedLoader;
if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) { if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) {
// Workspace scroll // Workspace scroll
const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor);
@ -86,14 +90,14 @@ ColumnLayout {
Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`); Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);
else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`); Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
} else if (y < screen.height / 2 && Config.bar.scrollActions.volume) { } else if (x < screen.width / 2 && Config.bar.scrollActions.volume) {
// Volume scroll on top half // Volume scroll on left half
if (angleDelta.y > 0) if (angleDelta.y > 0)
Audio.incrementVolume(); Audio.incrementVolume();
else if (angleDelta.y < 0) else if (angleDelta.y < 0)
Audio.decrementVolume(); Audio.decrementVolume();
} else if (Config.bar.scrollActions.brightness) { } else if (Config.bar.scrollActions.brightness) {
// Brightness scroll on bottom half // Brightness scroll on right half
const monitor = Brightness.getMonitorForScreen(screen); const monitor = Brightness.getMonitorForScreen(screen);
if (angleDelta.y > 0) if (angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement);
@ -115,7 +119,7 @@ ColumnLayout {
DelegateChoice { DelegateChoice {
roleValue: "spacer" roleValue: "spacer"
delegate: WrappedLoader { delegate: WrappedLoader {
Layout.fillHeight: enabled Layout.fillWidth: enabled
} }
} }
DelegateChoice { DelegateChoice {
@ -201,11 +205,11 @@ ColumnLayout {
} }
asynchronous: true asynchronous: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter
// Cursed ahh thing to add padding to first and last enabled components // Cursed ahh thing to add padding to first and last enabled components
Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0 Layout.leftMargin: findFirstEnabled() === this ? root.hPadding : 0
Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0 Layout.rightMargin: findLastEnabled() === this ? root.hPadding : 0
visible: enabled visible: enabled
active: enabled active: enabled

View file

@ -17,35 +17,40 @@ Item {
readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name)
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) // NoSignal horizontal top bar: the bar's "thickness" is its HEIGHT (was width
// for the upstream vertical left-edge bar). clampedHeight/contentHeight and
// implicitHeight carry the thickness; the wrapper fills the screen width.
readonly property int clampedHeight: Math.max(Config.border.minThickness, implicitHeight)
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 contentWidth: 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) ? contentWidth : 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 {
(content.item as Bar)?.closeTray(); (content.item as Bar)?.closeTray();
} }
function checkPopout(y: real): void { function checkPopout(x: real): void {
(content.item as Bar)?.checkPopout(y); (content.item as Bar)?.checkPopout(x);
} }
function handleWheel(y: real, angleDelta: point): void { function handleWheel(x: real, angleDelta: point): void {
(content.item as Bar)?.handleWheel(y, angleDelta); (content.item as Bar)?.handleWheel(x, angleDelta);
} }
clip: true clip: true
visible: width > Config.border.thickness visible: height > Config.border.thickness
implicitWidth: fullscreen ? 0 : Config.border.thickness implicitHeight: fullscreen ? 0 : Config.border.thickness
states: State { states: State {
name: "visible" name: "visible"
when: root.shouldBeVisible when: root.shouldBeVisible
PropertyChanges { PropertyChanges {
root.implicitWidth: root.contentWidth root.implicitHeight: root.contentHeight
} }
} }
@ -56,7 +61,7 @@ Item {
Anim { Anim {
target: root target: root
property: "implicitWidth" property: "implicitHeight"
} }
}, },
Transition { Transition {
@ -65,7 +70,7 @@ Item {
Anim { Anim {
target: root target: root
property: "implicitWidth" property: "implicitHeight"
type: Anim.Emphasized type: Anim.Emphasized
} }
} }
@ -74,14 +79,14 @@ Item {
Loader { Loader {
id: content id: content
anchors.top: parent.top anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top
active: root.shouldBeVisible active: root.shouldBeVisible
sourceComponent: Bar { sourceComponent: Bar {
width: root.contentWidth height: root.contentHeight
screen: root.screen screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
popouts: root.popouts // qmllint disable incompatible-type popouts: root.popouts // qmllint disable incompatible-type

View file

@ -1,106 +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)
implicitWidth: Tokens.sizes.bar.innerWidth StyledText {
implicitHeight: layout.implicitHeight + root.padding * 2 Layout.alignment: Qt.AlignVCenter
text: Time.format("ddd d MMM")
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) color: Theme.textMuted
radius: Tokens.rounding.full font.family: Theme.font.family
font.pixelSize: Theme.font.bar
ColumnLayout { font.weight: Font.Normal
id: layout
anchors.centerIn: parent
spacing: Tokens.spacing.small
Loader {
Layout.alignment: Qt.AlignHCenter
asynchronous: true
active: Config.bar.clock.showIcon
visible: active
sourceComponent: MaterialIcon {
text: "calendar_month"
color: root.colour
}
} }
StyledText { StyledText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter
visible: Config.bar.clock.showDate text: Time.format("hh:mm")
color: Theme.text
horizontalAlignment: StyledText.AlignHCenter font.family: Theme.font.family
text: Time.format("ddd\nd") font.pixelSize: Theme.font.bar
font: Tokens.font.body.small font.weight: Font.DemiBold
color: root.colour
}
Rectangle {
Layout.fillWidth: true
visible: Config.bar.clock.showDate
implicitHeight: 1
color: Colours.palette.m3outlineVariant
}
StyledText {
Layout.alignment: Qt.AlignHCenter
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.topMargin: -parent.spacing - 4
Layout.alignment: Qt.AlignHCenter
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.topMargin: -parent.spacing - 4
Layout.alignment: Qt.AlignHCenter
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

@ -20,16 +20,13 @@ StyledRect {
radius: Tokens.rounding.full radius: Tokens.rounding.full
clip: true clip: true
implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: Tokens.sizes.bar.innerWidth
implicitHeight: iconColumn.implicitHeight + Tokens.padding.medium * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) implicitWidth: iconColumn.implicitWidth + Tokens.padding.medium * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0)
ColumnLayout { RowLayout {
id: iconColumn id: iconColumn
anchors.left: parent.left anchors.centerIn: parent
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium / 2 spacing: Tokens.spacing.medium / 2
@ -261,7 +258,7 @@ StyledRect {
required property string name required property string name
asynchronous: true asynchronous: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter
visible: active visible: active
} }
} }

View file

@ -19,27 +19,27 @@ StyledRect {
property bool expanded property bool expanded
readonly property real nonAnimHeight: { readonly property real nonAnimWidth: {
if (!Config.bar.tray.compact) if (!Config.bar.tray.compact)
return layout.implicitHeight + padding * 2; return layout.implicitWidth + padding * 2;
return (expanded ? expandIcon.implicitHeight + layout.implicitHeight + spacing : expandIcon.implicitHeight) + padding * 2; return (expanded ? expandIcon.implicitWidth + layout.implicitWidth + spacing : expandIcon.implicitWidth) + padding * 2;
} }
clip: true clip: true
visible: height > 0 visible: width > 0
implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: Tokens.sizes.bar.innerWidth
implicitHeight: nonAnimHeight implicitWidth: nonAnimWidth
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Tokens.rounding.full radius: Tokens.rounding.full
Column { Row {
id: layout id: layout
anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top anchors.left: parent.left
anchors.topMargin: root.padding anchors.leftMargin: root.padding
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0 opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0
@ -86,37 +86,37 @@ StyledRect {
asynchronous: true asynchronous: true
anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter
anchors.bottom: parent.bottom anchors.right: parent.right
active: Config.bar.tray.compact && items.count > 0 active: Config.bar.tray.compact && items.count > 0
sourceComponent: Item { sourceComponent: Item {
implicitWidth: expandIconInner.implicitWidth implicitHeight: expandIconInner.implicitHeight
implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small implicitWidth: expandIconInner.implicitWidth - Tokens.padding.small
MaterialIcon { MaterialIcon {
id: expandIconInner id: expandIconInner
anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter
anchors.bottom: parent.bottom anchors.right: parent.right
anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.extraSmall : -Tokens.padding.extraSmall anchors.rightMargin: Config.bar.tray.background ? Tokens.padding.extraSmall : -Tokens.padding.extraSmall
text: "expand_less" text: "expand_less"
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
rotation: root.expanded ? 180 : 0 rotation: root.expanded ? 270 : 90
Behavior on rotation { Behavior on rotation {
Anim {} Anim {}
} }
Behavior on anchors.bottomMargin { Behavior on anchors.rightMargin {
Anim {} Anim {}
} }
} }
} }
} }
Behavior on implicitHeight { Behavior on implicitWidth {
Anim {} Anim {}
} }
} }

View file

@ -24,7 +24,7 @@ ColumnLayout {
readonly property bool isOccupied: occupied[ws] ?? false readonly property bool isOccupied: occupied[ws] ?? false
readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: size Layout.preferredHeight: size
spacing: 0 spacing: 0

View file

@ -27,8 +27,8 @@ StyledClippingRect {
property real blur: onSpecial ? 1 : 0 property real blur: onSpecial ? 1 : 0
implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: Tokens.sizes.bar.innerWidth
implicitHeight: layout.implicitHeight + Tokens.padding.small implicitWidth: layout.implicitWidth + Tokens.padding.small
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
@ -60,7 +60,7 @@ StyledClippingRect {
} }
} }
ColumnLayout { RowLayout {
id: layout id: layout
anchors.centerIn: parent anchors.centerIn: parent
@ -81,7 +81,7 @@ StyledClippingRect {
Loader { Loader {
asynchronous: true asynchronous: true
anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter
active: Config.bar.workspaces.activeIndicator active: Config.bar.workspaces.activeIndicator
sourceComponent: ActiveIndicator { sourceComponent: ActiveIndicator {

View file

@ -12,21 +12,24 @@ Item {
required property real borderThickness required property real borderThickness
readonly property alias content: content readonly property alias content: content
property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
visible: width > 0 && height > 0 visible: width > 0 && height > 0
clip: true clip: true
implicitWidth: content.implicitWidth * (1 - offsetScale) // NoSignal top bar: popouts drop DOWN from the bar. The collapsing axis is
implicitHeight: content.implicitHeight // now height (was width for the vertical left bar); position along the bar
// is x (from the hovered widget's x-centre), the docked edge is the top.
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight * (1 - offsetScale)
x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0 y: content.isDetached ? (parent.height - content.nonAnimHeight) / 2 : 0
y: { x: {
if (content.isDetached) if (content.isDetached)
return (parent.height - content.nonAnimHeight) / 2; return (parent.width - content.nonAnimWidth) / 2;
const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2; const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
const diff = parent.height - Math.floor(off + content.nonAnimHeight); const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.max(off, 0); return Math.max(off, 0);
@ -36,14 +39,14 @@ Item {
Anim {} Anim {}
} }
Behavior on x { Behavior on y {
Anim { Anim {
duration: content.animLength duration: content.animLength
easing: content.animCurve easing: content.animCurve
} }
} }
Behavior on y { Behavior on x {
enabled: root.offsetScale < 1 enabled: root.offsetScale < 1
Anim { Anim {
@ -58,8 +61,8 @@ Item {
screen: root.screen screen: root.screen
offsetScale: root.offsetScale offsetScale: root.offsetScale
anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left anchors.top: parent.top
anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale anchors.topMargin: (-implicitHeight - 5) * root.offsetScale
} }
} }

View file

@ -149,14 +149,14 @@ Item {
} }
} }
Behavior on implicitWidth { Behavior on implicitHeight {
Anim { Anim {
duration: root.animLength duration: root.animLength
easing: root.animCurve easing: root.animCurve
} }
} }
Behavior on implicitHeight { Behavior on implicitWidth {
enabled: root.offsetScale < 1 enabled: root.offsetScale < 1
Anim { Anim {

View file

@ -75,7 +75,7 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
radius: Tokens.rounding.large radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: "transparent" color: "transparent"
Flickable { Flickable {

View file

@ -21,7 +21,7 @@ GridLayout {
Layout.preferredWidth: Tokens.sizes.dashboard.userWidth Layout.preferredWidth: Tokens.sizes.dashboard.userWidth
Layout.fillHeight: true Layout.fillHeight: true
radius: Tokens.rounding.extraLarge radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
User { User {
id: user id: user
@ -37,7 +37,7 @@ GridLayout {
Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.preferredHeight: weather.implicitHeight Layout.preferredHeight: weather.implicitHeight
radius: Tokens.rounding.extraLarge * 1.5 radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
SmallWeather { SmallWeather {
id: weather id: weather
@ -49,7 +49,7 @@ GridLayout {
Layout.preferredWidth: dateTime.implicitWidth Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true Layout.fillHeight: true
radius: Tokens.rounding.large radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
DateTime { DateTime {
id: dateTime id: dateTime
@ -63,7 +63,7 @@ GridLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight Layout.preferredHeight: calendar.implicitHeight
radius: Tokens.rounding.extraLarge radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Calendar { Calendar {
id: calendar id: calendar
@ -78,7 +78,7 @@ GridLayout {
Layout.preferredWidth: resources.implicitWidth Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true Layout.fillHeight: true
radius: Tokens.rounding.large radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Resources { Resources {
id: resources id: resources
@ -92,7 +92,7 @@ GridLayout {
Layout.preferredWidth: media.implicitWidth Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true Layout.fillHeight: true
radius: Tokens.rounding.extraLarge * 2 radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Media { Media {
id: media id: media

View file

@ -102,11 +102,11 @@ Item {
CAnim {} CAnim {}
} }
MaterialIcon { NsIcon {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
text: "queue_music" icon: "queue_music"
fontStyle: Tokens.font.icon.builders.large.scale(2).build() fontStyle: Tokens.font.icon.builders.large.scale(2).build()
color: Colours.palette.m3onPrimaryContainer color: Colours.palette.m3onPrimaryContainer
} }

View file

@ -23,9 +23,9 @@ Item {
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "tune" icon: "tune"
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build() fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build()
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
} }

View file

@ -86,7 +86,7 @@ Item {
implicitHeight: parent.implicitHeight * 2 implicitHeight: parent.implicitHeight * 2
color: Colours.palette.m3primary color: Colours.palette.m3primary
radius: Tokens.rounding.full radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
} }
Behavior on x { Behavior on x {
@ -148,18 +148,18 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2
radius: Tokens.rounding.medium radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
onClicked: root.dashState.currentTab = tab.TabBar.index onClicked: root.dashState.currentTab = tab.TabBar.index
} }
MaterialIcon { NsIcon {
id: icon id: icon
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top anchors.bottom: label.top
text: tab.iconName icon: tab.iconName
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0 fill: tab.current ? 1 : 0
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium

View file

@ -67,7 +67,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small
radius: Tokens.rounding.extraLarge * 2 radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
RowLayout { RowLayout {
@ -76,9 +76,9 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
text: Weather.icon icon: Weather.icon
fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build() fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build()
color: Colours.palette.m3secondary color: Colours.palette.m3secondary
animate: true animate: true
@ -155,7 +155,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2 implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.large radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
ColumnLayout { ColumnLayout {
@ -180,9 +180,9 @@ Item {
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
} }
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: forecastItem.modelData.icon icon: forecastItem.modelData.icon
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
color: Colours.palette.m3secondary color: Colours.palette.m3secondary
} }
@ -209,15 +209,15 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 60 Layout.preferredHeight: 60
radius: Tokens.rounding.medium radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
text: detailRoot.icon icon: detailRoot.icon
color: detailRoot.colour color: detailRoot.colour
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -252,8 +252,8 @@ Item {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: weatherStat.icon icon: weatherStat.icon
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
color: weatherStat.colour color: weatherStat.colour
} }

View file

@ -62,7 +62,7 @@ CustomMouseArea {
StateLayer { StateLayer {
color: Colours.palette.m3primary color: Colours.palette.m3primary
radius: pressed ? Tokens.rounding.small : Tokens.rounding.large radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
disabled: { disabled: {
const now = new Date(); const now = new Date();
return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();

View file

@ -65,10 +65,10 @@ Item {
Anim {} Anim {}
} }
MaterialIcon { NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: res.icon icon: res.icon
font: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
color: res.fgColour color: res.fgColour
} }
} }

View file

@ -13,14 +13,14 @@ Item {
Component.onCompleted: Weather.reload() Component.onCompleted: Weather.reload()
MaterialIcon { NsIcon {
id: icon id: icon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
animate: true animate: true
text: Weather.icon icon: Weather.icon
color: Colours.palette.m3secondary color: Colours.palette.m3secondary
fontStyle: Tokens.font.icon.builders.extraLarge.scale(1.6).build() fontStyle: Tokens.font.icon.builders.extraLarge.scale(1.6).build()
} }

View file

@ -75,8 +75,8 @@ Item {
asynchronous: true asynchronous: true
active: pfp.status !== Image.Ready active: pfp.status !== Image.Ready
sourceComponent: MaterialIcon { sourceComponent: NsIcon {
text: "person_add" icon: "person_add"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
fill: 1 fill: 1
@ -120,9 +120,9 @@ Item {
} }
} }
MaterialIcon { NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "person_edit" icon: "person_edit"
color: Colours.palette.m3onPrimary color: Colours.palette.m3onPrimary
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
} }
@ -187,9 +187,9 @@ Item {
CAnim {} CAnim {}
} }
MaterialIcon { NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "clock_arrow_up" icon: "clock_arrow_up"
color: Colours.palette.m3onTertiaryContainer color: Colours.palette.m3onTertiaryContainer
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
@ -216,7 +216,7 @@ Item {
implicitWidth: 10 implicitWidth: 10
implicitHeight: 10 implicitHeight: 10
radius: Tokens.rounding.full radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer color: Colours.palette.m3secondaryContainer
} }
@ -229,7 +229,7 @@ Item {
implicitWidth: 15 implicitWidth: 15
implicitHeight: 15 implicitHeight: 15
radius: Tokens.rounding.full radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer color: Colours.palette.m3secondaryContainer
} }
@ -240,7 +240,7 @@ Item {
anchors.leftMargin: -Tokens.padding.medium anchors.leftMargin: -Tokens.padding.medium
y: Tokens.padding.extraSmall y: Tokens.padding.extraSmall
radius: Tokens.rounding.largeIncreased radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer color: Colours.palette.m3secondaryContainer
implicitWidth: wmLabel.implicitWidth + Tokens.padding.medium * 2 implicitWidth: wmLabel.implicitWidth + Tokens.padding.medium * 2
implicitHeight: wmLabel.implicitHeight + Tokens.padding.small * 2 implicitHeight: wmLabel.implicitHeight + Tokens.padding.small * 2
@ -251,11 +251,11 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
MaterialIcon { NsIcon {
id: wmIcon id: wmIcon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "select_window" icon: "select_window"
color: Colours.palette.m3onSecondaryContainer color: Colours.palette.m3onSecondaryContainer
fontStyle: wmText.font fontStyle: wmText.font
} }

View file

@ -176,7 +176,7 @@ Item {
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2 implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2 implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
color: Colours.palette.m3primaryContainer color: Colours.palette.m3primaryContainer
radius: Tokens.rounding.full radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
LoadingIndicator { LoadingIndicator {
id: shape id: shape
@ -212,9 +212,9 @@ Item {
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "sentiment_sad" icon: "sentiment_sad"
fontStyle: Tokens.font.icon.builders.large.scale(2).build() fontStyle: Tokens.font.icon.builders.large.scale(2).build()
color: Colours.palette.m3outline color: Colours.palette.m3outline
} }

View file

@ -20,9 +20,9 @@ Item {
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
z: 1 z: 1
MaterialIcon { NsIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12) Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
text: "lyrics" icon: "lyrics"
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }

View file

@ -33,7 +33,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0 anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
group: blobGroup group: blobGroup
radius: Tokens.rounding.medium radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Behavior on anchors.margins { Behavior on anchors.margins {
Anim {} Anim {}
@ -50,7 +50,7 @@ Item {
implicitHeight: parent.height implicitHeight: parent.height
group: blobGroup group: blobGroup
radius: Tokens.rounding.medium radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
deformScale: 0.00001 deformScale: 0.00001
states: State { states: State {
@ -197,11 +197,11 @@ Item {
hoverEnabled: true hoverEnabled: true
onClicked: root.open = !root.open onClicked: root.open = !root.open
MaterialIcon { NsIcon {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
text: "more_vert" icon: "more_vert"
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
} }

View file

@ -12,7 +12,7 @@ StyledClippingRect {
property real animPerc: UPower.displayDevice.percentage property real animPerc: UPower.displayDevice.percentage
color: Colours.palette.m3secondaryContainer color: Colours.palette.m3secondaryContainer
radius: Tokens.rounding.large radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) || Config.dashboard.performance.showStorage || Config.dashboard.performance.showMemory ? Tokens.sizes.dashboard.perfBattWidth : Tokens.sizes.dashboard.perfBattWidthSingle implicitWidth: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) || Config.dashboard.performance.showStorage || Config.dashboard.performance.showMemory ? Tokens.sizes.dashboard.perfBattWidth : Tokens.sizes.dashboard.perfBattWidthSingle
implicitHeight: Tokens.sizes.dashboard.perfBattHeight implicitHeight: Tokens.sizes.dashboard.perfBattHeight
@ -39,7 +39,7 @@ StyledClippingRect {
implicitHeight: parent.height * root.animPerc implicitHeight: parent.height * root.animPerc
color: Colours.palette.m3secondary color: Colours.palette.m3secondary
radius: Tokens.rounding.extraSmall radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
clip: true clip: true
Contents { Contents {
@ -65,9 +65,9 @@ StyledClippingRect {
spacing: 0 spacing: 0
MaterialIcon { NsIcon {
Layout.leftMargin: -Tokens.padding.extraSmall Layout.leftMargin: -Tokens.padding.extraSmall
text: "battery_full" icon: "battery_full"
color: contents.accentColour color: contents.accentColour
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
} }
@ -115,8 +115,8 @@ StyledClippingRect {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
MaterialIcon { NsIcon {
text: "bolt" icon: "bolt"
color: contents.accentColour color: contents.accentColour
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
fill: 1 fill: 1

View file

@ -17,7 +17,7 @@ StyledRect {
required property real temperature required property real temperature
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Tokens.sizes.dashboard.perfHeroCardWidth implicitWidth: Tokens.sizes.dashboard.perfHeroCardWidth
implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.large, usageShape.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2 implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.large, usageShape.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2
@ -40,11 +40,11 @@ StyledRect {
Anim {} Anim {}
} }
MaterialIcon { NsIcon {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
text: root.icon icon: root.icon
color: root.accent color: root.accent
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
@ -84,9 +84,9 @@ StyledRect {
Layout.leftMargin: -Tokens.padding.extraSmall Layout.leftMargin: -Tokens.padding.extraSmall
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
MaterialIcon { NsIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.08) Layout.topMargin: Math.round(fontInfo.pointSize * 0.08)
text: root.temperature > 90 ? "thermometer_alert" : "thermometer" icon: root.temperature > 90 ? "thermometer_alert" : "thermometer"
color: root.temperature > 90 ? Colours.palette.m3error : root.accent color: root.temperature > 90 ? Colours.palette.m3error : root.accent
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
fill: 1 fill: 1

View file

@ -12,7 +12,7 @@ StyledRect {
readonly property color accent: Colours.palette.m3tertiary readonly property color accent: Colours.palette.m3tertiary
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.medium radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2 implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
@ -31,8 +31,8 @@ StyledRect {
Layout.leftMargin: -Tokens.padding.extraSmall Layout.leftMargin: -Tokens.padding.extraSmall
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: "memory_alt" icon: "memory_alt"
fill: 1 fill: 1
color: root.accent color: root.accent
fontStyle: Tokens.font.icon.builders.medium.weight(Font.DemiBold).build() // DemiBold to fix fill issues fontStyle: Tokens.font.icon.builders.medium.weight(Font.DemiBold).build() // DemiBold to fix fill issues

View file

@ -10,7 +10,7 @@ StyledRect {
id: root id: root
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Tokens.sizes.dashboard.perfNetworkCardWidth implicitWidth: Tokens.sizes.dashboard.perfNetworkCardWidth
implicitHeight: Tokens.sizes.dashboard.perfNetworkCardHeight implicitHeight: Tokens.sizes.dashboard.perfNetworkCardHeight
@ -30,8 +30,8 @@ StyledRect {
RowLayout { RowLayout {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: "swap_vert" icon: "swap_vert"
color: Colours.palette.m3primary color: Colours.palette.m3primary
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
@ -105,8 +105,8 @@ StyledRect {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: "download" icon: "download"
color: Colours.palette.m3tertiary color: Colours.palette.m3tertiary
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
@ -136,8 +136,8 @@ StyledRect {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: "upload" icon: "upload"
color: Colours.palette.m3secondary color: Colours.palette.m3secondary
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
@ -167,8 +167,8 @@ StyledRect {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: "history" icon: "history"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }

View file

@ -14,7 +14,7 @@ StyledRect {
readonly property real percentage: Storage.primaryDisk?.perc ?? 0 readonly property real percentage: Storage.primaryDisk?.perc ?? 0
color: Colours.tPalette.m3surfaceContainer color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraExtraLarge radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
@ -55,9 +55,9 @@ StyledRect {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 spacing: 0
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "hard_drive" icon: "hard_drive"
color: root.accent color: root.accent
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }

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
@ -85,14 +85,14 @@ StyledWindow {
Region { Region {
id: emptyRegion id: emptyRegion
x: panels.notifications.x + bar.implicitWidth x: panels.notifications.x + root.borderThickness
y: panels.notifications.y + root.borderThickness y: panels.notifications.y + bar.implicitHeight
width: panels.notifications.width width: panels.notifications.width
height: panels.notifications.height height: panels.notifications.height
Region { Region {
x: root.width - width x: root.width - width
y: panels.osdWrapper.y + root.borderThickness y: panels.osdWrapper.y + bar.implicitHeight
width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness
height: panels.osd.height height: panels.osd.height
} }
@ -155,9 +155,9 @@ StyledWindow {
anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers
group: blobGroup group: blobGroup
radius: root.borderRounding radius: root.borderRounding
borderLeft: bar.implicitWidth - 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: root.borderThickness - 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
} }
@ -180,7 +180,7 @@ StyledWindow {
panel: panels.sessionWrapper panel: panels.sessionWrapper
deformAmount: 0.2 deformAmount: 0.2
x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth x: panels.sessionWrapper.x + panels.session.x + root.borderThickness
implicitWidth: panels.session.width implicitWidth: panels.session.width
} }
@ -198,8 +198,10 @@ StyledWindow {
id: osdBg id: osdBg
panel: panels.osdWrapper panel: panels.osdWrapper
deformAmount: 0.25 deformAmount: 0 // NoSignal: square OSD (no organic blob deformation)
x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth radius: 0
opacity: 0 // NoSignal: OSD draws its own flat square glass bg (osd/Content)
x: panels.osdWrapper.x + panels.osd.x + root.borderThickness
implicitWidth: panels.osd.width implicitWidth: panels.osd.width
} }
@ -207,6 +209,12 @@ StyledWindow {
id: notifsBg id: notifsBg
panel: panels.notifications panel: panels.notifications
deformAmount: 0 // NoSignal: square notification toasts
radius: 0
opacity: 0
visible: false
implicitWidth: 0 // collapse the blob entirely (toasts draw their own square cards)
implicitHeight: 0
} }
PanelBg { PanelBg {
@ -226,7 +234,7 @@ StyledWindow {
panel: panels.popoutsWrapper panel: panels.popoutsWrapper
deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1
x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth x: panels.popoutsWrapper.x + panels.popouts.x + root.borderThickness - panels.popouts.width * extraWidth
implicitWidth: panels.popouts.width * (1 + extraWidth) implicitWidth: panels.popouts.width * (1 + extraWidth)
Behavior on extraWidth { Behavior on extraWidth {
@ -292,8 +300,9 @@ StyledWindow {
BarWrapper { BarWrapper {
id: bar id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom
screen: root.screen screen: root.screen
visibilities: visibilities visibilities: visibilities
@ -310,8 +319,8 @@ StyledWindow {
property real deformAmount: 0.15 property real deformAmount: 0.15
group: blobGroup group: blobGroup
x: panel.x + bar.implicitWidth x: panel.x + root.borderThickness
y: panel.y + root.borderThickness y: panel.y + bar.implicitHeight
implicitWidth: panel.width implicitWidth: panel.width
implicitHeight: panel.height implicitHeight: panel.height
radius: Tokens.rounding.extraLarge radius: Tokens.rounding.extraLarge

View file

@ -14,11 +14,11 @@ Scope {
ExclusionZone { ExclusionZone {
anchors.left: true anchors.left: true
exclusiveZone: root.bar.exclusiveZone
} }
ExclusionZone { ExclusionZone {
anchors.top: true anchors.top: true
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

@ -23,27 +23,29 @@ CustomMouseArea {
property bool osdShortcutActive property bool osdShortcutActive
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
// NoSignal top bar: the bar's thickness now insets the TOP, so panel Y-origins
// are offset by bar.implicitHeight; the left inset is just the border thickness.
function withinPanelHeight(panel: Item, x: real, y: real): bool { function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = root.borderThickness + panel.y; const panelY = root.bar.implicitHeight + panel.y;
return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding;
} }
function withinPanelWidth(panel: Item, x: real, y: real): bool { function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = bar.implicitWidth + panel.x; const panelX = root.borderThickness + panel.x;
return x >= panelX - Config.border.rounding && x <= panelX + panel.width + Config.border.rounding; return x >= panelX - Config.border.rounding && x <= panelX + panel.width + Config.border.rounding;
} }
function inLeftPanel(panel: Item, x: real, y: real): bool { function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < bar.implicitWidth + panel.x + panel.width && withinPanelHeight(panel, x, y); return x < root.borderThickness + panel.x + panel.width && withinPanelHeight(panel, x, y);
} }
function inRightPanel(panel: Item, x: real, y: real): bool { function inRightPanel(panel: Item, x: real, y: real): bool {
return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); return x > Math.min(width - Config.border.minThickness, root.borderThickness + panel.x) && withinPanelHeight(panel, x, y);
} }
function inTopPanel(panel: Item, x: real, y: real): bool { function inTopPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property
return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y); return y < Math.max(Config.border.minThickness, root.bar.implicitHeight + panelHeight) && withinPanelWidth(panel, x, y);
} }
function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool {
@ -54,8 +56,8 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (fullscreen) if (fullscreen)
return; return;
if (event.x < bar.implicitWidth) { if (event.y < bar.implicitHeight) {
bar.handleWheel(event.y, event.angleDelta); bar.handleWheel(event.x, event.angleDelta);
} }
} }
@ -102,15 +104,15 @@ CustomMouseArea {
return; return;
} }
// Show bar in non-exclusive mode on hover // Show bar in non-exclusive mode on hover (top edge)
if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) if (!visibilities.bar && Config.bar.showOnHover && y < bar.clampedHeight)
bar.isHovered = true; bar.isHovered = true;
// Show/hide bar on drag // Show/hide bar on drag (vertical, from the top edge)
if (pressed && dragStart.x < bar.clampedWidth) { if (pressed && dragStart.y < bar.clampedHeight) {
if (dragX > Config.bar.dragThreshold) if (dragY > Config.bar.dragThreshold)
visibilities.bar = true; visibilities.bar = true;
else if (dragX < -Config.bar.dragThreshold) else if (dragY < -Config.bar.dragThreshold)
visibilities.bar = false; visibilities.bar = false;
} }
@ -128,7 +130,7 @@ CustomMouseArea {
root.panels.osd.hovered = true; root.panels.osd.hovered = true;
} }
const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, root.borderThickness + panels.sidebar.x);
// Show/hide session on drag // Show/hide session on drag
if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) {
@ -213,9 +215,9 @@ CustomMouseArea {
utilitiesShortcutActive = false; utilitiesShortcutActive = false;
} }
// Show popouts on hover // Show popouts on hover (top bar)
if (x < bar.implicitWidth) { if (y < bar.implicitHeight) {
bar.checkPopout(y); bar.checkPopout(x);
} else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) { } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) {
popouts.hasCurrent = false; popouts.hasCurrent = false;
bar.closeTray(); bar.closeTray();

View file

@ -36,7 +36,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: borderThickness anchors.margins: borderThickness
anchors.leftMargin: bar.implicitWidth anchors.topMargin: bar.implicitHeight
Item { Item {
id: osdWrapper id: osdWrapper
@ -70,7 +70,7 @@ Item {
sessionPanel: sessionWrapper sessionPanel: sessionWrapper
utilitiesPanel: utilities utilitiesPanel: utilities
anchors.top: parent.top y: 46 // NoSignal: drop the toasts below the 38px NsBar (+ gap)
anchors.right: parent.right anchors.right: parent.right
} }

View file

@ -15,10 +15,10 @@ Region {
readonly property real borderThickness: win.contentItem.Config.border.thickness readonly property real borderThickness: win.contentItem.Config.border.thickness
readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness
x: bar.clampedWidth + win.dragMaskPadding x: clampedThickness + win.dragMaskPadding
y: clampedThickness + win.dragMaskPadding y: bar.clampedHeight + win.dragMaskPadding
width: win.width - bar.clampedWidth - clampedThickness - win.dragMaskPadding * 2 width: win.width - clampedThickness * 2 - win.dragMaskPadding * 2
height: win.height - clampedThickness * 2 - win.dragMaskPadding * 2 height: win.height - bar.clampedHeight - clampedThickness - win.dragMaskPadding * 2
intersection: Intersection.Xor intersection: Intersection.Xor
R { R {
@ -75,8 +75,8 @@ Region {
component R: Region { component R: Region {
required property Item panel required property Item panel
x: panel.x + root.bar.implicitWidth x: panel.x + root.borderThickness
y: panel.y + root.borderThickness y: panel.y + root.bar.implicitHeight
width: panel.width width: panel.width
height: panel.height height: panel.height
intersection: Intersection.Subtract intersection: Intersection.Subtract

View file

@ -58,14 +58,14 @@ Item {
implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight) implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight)
MaterialIcon { NsIcon {
id: searchIcon id: searchIcon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.padding anchors.leftMargin: root.padding
text: "search" icon: "search"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
} }
@ -145,7 +145,7 @@ Item {
} }
} }
MaterialIcon { NsIcon {
id: clearIcon id: clearIcon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -163,7 +163,7 @@ Item {
return 1; return 1;
} }
text: "close" icon: "close"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
MouseArea { MouseArea {

View file

@ -116,8 +116,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MaterialIcon { NsIcon {
text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" icon: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge

View file

@ -25,11 +25,11 @@ Item {
anchors.rightMargin: Tokens.padding.medium anchors.rightMargin: Tokens.padding.medium
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
MaterialIcon { NsIcon {
id: icon id: icon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.modelData?.icon ?? "" icon: root.modelData?.icon ?? ""
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.builders.large.scale(1.3).build() fontStyle: Tokens.font.icon.builders.large.scale(1.3).build()
} }

View file

@ -79,8 +79,8 @@ Item {
anchors.right: parent.right anchors.right: parent.right
active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id) active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id)
sourceComponent: MaterialIcon { sourceComponent: NsIcon {
text: "favorite" icon: "favorite"
fill: 1 fill: 1
color: Colours.palette.m3primary color: Colours.palette.m3primary
} }

View file

@ -40,8 +40,8 @@ Item {
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
text: "function" icon: "function"
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@ -105,14 +105,14 @@ Item {
} }
} }
MaterialIcon { NsIcon {
id: icon id: icon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Tokens.padding.medium anchors.rightMargin: Tokens.padding.medium
text: "open_in_new" icon: "open_in_new"
color: Colours.palette.m3onTertiary color: Colours.palette.m3onTertiary
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
} }

View file

@ -92,8 +92,8 @@ Item {
active: `${root.modelData?.name} ${root.modelData?.flavour}` === Schemes.currentScheme active: `${root.modelData?.name} ${root.modelData?.flavour}` === Schemes.currentScheme
sourceComponent: MaterialIcon { sourceComponent: NsIcon {
text: "check" icon: "check"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
} }

View file

@ -26,10 +26,10 @@ Item {
anchors.rightMargin: Tokens.padding.medium anchors.rightMargin: Tokens.padding.medium
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
MaterialIcon { NsIcon {
id: icon id: icon
text: root.modelData?.icon ?? "" icon: root.modelData?.icon ?? ""
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -68,8 +68,8 @@ Item {
active: root.modelData?.variant === Schemes.currentVariant active: root.modelData?.variant === Schemes.currentVariant
sourceComponent: MaterialIcon { sourceComponent: NsIcon {
text: "check" icon: "check"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
} }

View file

@ -57,9 +57,9 @@ Item {
implicitWidth: Tokens.sizes.launcher.wallpaperWidth implicitWidth: Tokens.sizes.launcher.wallpaperWidth
implicitHeight: implicitWidth / 16 * 9 implicitHeight: implicitWidth / 16 * 9
MaterialIcon { NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "image" icon: "image"
color: Colours.tPalette.m3outline color: Colours.tPalette.m3outline
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build() fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
} }

View file

@ -1,11 +1,22 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
// NoSignal lock screen surface (interface redesign 2026-06-14).
// Secure ext-session-lock content: blurred wallpaper + scrim, top status row,
// centred clock/date/avatar/username/password, bottom now-playing strip.
// Reuses caelestia's Pam (buffer + handleKey + state) for PAM auth and the
// WlSessionLock unlock signal input is fed to pam.handleKey, never compared
// in QML. JetBrains Mono / Nerd Font / NoSignal Theme tokens.
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Effects import QtQuick.Effects
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import Caelestia.Config import Caelestia.Config
import qs.components import qs.components
import qs.services import qs.services
import qs.utils
WlSessionLockSurface { WlSessionLockSurface {
id: root id: root
@ -13,153 +24,26 @@ WlSessionLockSurface {
required property WlSessionLock lock required property WlSessionLock lock
required property Pam pam required property Pam pam
readonly property alias unlocking: unlockAnim.running readonly property bool errored: root.pam.state === "error" || root.pam.state === "fail"
contentItem.Config.screen: screen.name contentItem.Config.screen: screen?.name ?? ""
contentItem.Tokens.screen: screen.name contentItem.Tokens.screen: screen?.name ?? ""
color: "transparent" color: "transparent"
Connections { Connections {
function onUnlock(): void { function onUnlock(): void {
unlockAnim.start(); root.lock.locked = false;
} }
target: root.lock target: root.lock
} }
SequentialAnimation { // Blurred wallpaper + scrim
id: unlockAnim
ParallelAnimation {
Anim {
target: lockContent
properties: "implicitWidth,implicitHeight"
to: lockContent.size
}
Anim {
target: lockBg
property: "radius"
to: lockContent.radius
}
Anim {
target: content
property: "scale"
to: 0
}
Anim {
target: content
property: "opacity"
to: 0
type: Anim.StandardSmall
}
Anim {
target: lockIcon
property: "opacity"
to: 1
type: Anim.StandardLarge
}
Anim {
target: background
property: "opacity"
to: 0
type: Anim.StandardLarge
}
SequentialAnimation {
PauseAnimation {
duration: Tokens.anim.durations.small
}
Anim {
type: Anim.Standard
target: lockContent
property: "opacity"
to: 0
}
}
}
PropertyAction {
target: root.lock
property: "locked"
value: false
}
}
ParallelAnimation {
id: initAnim
running: true
Anim {
target: background
property: "opacity"
to: 1
type: Anim.StandardLarge
}
SequentialAnimation {
ParallelAnimation {
Anim {
target: lockContent
property: "scale"
to: 1
type: Anim.FastSpatial
}
Anim {
target: lockContent
property: "rotation"
to: 360
duration: Tokens.anim.durations.expressiveFastSpatial
easing: Tokens.anim.standardAccel
}
}
ParallelAnimation {
Anim {
target: lockIcon
property: "rotation"
to: 360
easing: Tokens.anim.standardDecel
}
Anim {
type: Anim.DefaultEffects
target: lockIcon
property: "opacity"
to: 0
}
Anim {
type: Anim.DefaultEffects
target: content
property: "opacity"
to: 1
}
Anim {
target: content
property: "scale"
to: 1
}
Anim {
target: lockBg
property: "radius"
to: lockContent.Tokens.rounding.extraLarge * 1.5
}
Anim {
target: lockContent
property: "implicitWidth"
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio
}
Anim {
target: lockContent
property: "implicitHeight"
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult
}
}
}
}
ScreencopyView { ScreencopyView {
id: background id: background
anchors.fill: parent anchors.fill: parent
captureSource: root.screen captureSource: root.screen
opacity: 0
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
@ -168,57 +52,265 @@ WlSessionLockSurface {
blur: 1 blur: 1
blurMax: 64 blurMax: 64
blurMultiplier: 1 blurMultiplier: 1
brightness: -0.38
saturation: 0.1
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.5)
}
// Keyboard capture (feed PAM; Escape does nothing)
Item {
anchors.fill: parent
focus: true
Component.onCompleted: forceActiveFocus()
Keys.onPressed: event => root.pam.handleKey(event)
}
// Top status row
RowLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 22
anchors.leftMargin: 28
anchors.rightMargin: 28
RowLayout {
spacing: 7
NsIcon {
icon: "lock"
color: Theme.textMuted
size: 15
}
StyledText {
text: "Locked"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
} }
} }
Item { Item {
id: lockContent Layout.fillWidth: true
}
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 RowLayout {
readonly property int radius: size / 4 * Tokens.rounding.scale spacing: 14
NsIcon {
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
color: Theme.textMuted
size: 15
}
NsIcon {
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth_connected" : "bluetooth_disabled"
color: Theme.textMuted
size: 15
}
RowLayout {
spacing: 6
visible: UPower.displayDevice.isLaptopBattery
NsIcon {
icon: Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged].includes(UPower.displayDevice.state))
color: Theme.textMuted
size: 15
}
StyledText {
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
}
}
// Centre block
ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: size spacing: 0
implicitHeight: size
rotation: 180 StyledText {
scale: 0 Layout.alignment: Qt.AlignHCenter
text: Time.format("hh:mm")
color: Theme.text
font.family: Theme.font.family
font.pixelSize: 108
font.weight: Font.ExtraLight
font.letterSpacing: 3
}
StyledRect { StyledText {
id: lockBg Layout.alignment: Qt.AlignHCenter
text: Time.format("ddd, d MMMM yyyy")
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: 15
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 64
implicitWidth: 74
implicitHeight: 74
radius: 0
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
NsIcon {
anchors.centerIn: parent
icon: "person"
color: Theme.onAccent
size: 34
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 14
text: SysInfo.user || "user"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: 14
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
implicitWidth: 312
implicitHeight: 44
radius: Theme.radius.panel
color: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.55)
border.width: 1
border.color: root.errored ? Qt.rgba(224 / 255, 116 / 255, 106 / 255, 0.6) : Qt.rgba(1, 1, 1, 0.1)
RowLayout {
anchors.fill: parent anchors.fill: parent
color: Colours.palette.m3surface anchors.leftMargin: 14
radius: parent.radius anchors.rightMargin: 12
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 spacing: 10
layer.enabled: true NsIcon {
layer.effect: MultiEffect { icon: "key"
shadowEnabled: true color: Theme.textMuted
blurMax: 15 size: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) }
StyledText {
Layout.fillWidth: true
text: root.pam.buffer.length > 0 ? "•".repeat(root.pam.buffer.length) : "Enter password"
color: root.pam.buffer.length > 0 ? Theme.text : Theme.textFaint
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.body
font.letterSpacing: root.pam.buffer.length > 0 ? 2 : 0
}
NsIcon {
icon: "login"
color: root.pam.buffer.length > 0 ? Theme.accent : Theme.textFaint
size: 16
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onClicked: if (root.pam.buffer.length > 0)
root.pam.passwd.start()
}
}
} }
} }
MaterialIcon { StyledText {
id: lockIcon Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
Layout.preferredHeight: 14
text: root.errored ? "Incorrect — try again" : (root.pam.lockMessage || "")
color: Theme.danger
font.family: Theme.font.family
font.pixelSize: 12
}
}
// Bottom strip
RowLayout {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: 26
anchors.leftMargin: 28
anchors.rightMargin: 28
RowLayout {
spacing: 10
visible: Players.active !== null
Rectangle {
implicitWidth: 42
implicitHeight: 42
radius: Theme.radius.panel
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "lock" icon: "music_note"
fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build() color: Theme.onAccent
rotation: 180 size: 18
}
} }
Content { ColumnLayout {
id: content spacing: 0
StyledText {
text: Players.active?.trackTitle || ""
color: Theme.text
elide: Text.ElideRight
Layout.maximumWidth: 280
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
text: Players.active?.trackArtist || ""
color: Theme.textMuted
elide: Text.ElideRight
Layout.maximumWidth: 280
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
anchors.centerIn: parent Item {
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased Layout.fillWidth: true
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased }
lock: root StyledText {
opacity: 0 text: "Press Enter to unlock"
scale: 0 color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: 11
} }
} }
} }

View file

@ -40,7 +40,12 @@ Scope {
PamContext { PamContext {
id: passwd id: passwd
config: "passwd" // NoSignal: faillock-free lock auth (finding F1). The stock "passwd"
// service routes through pam_faillock, which can lock the user out of
// their OWN session after a few failed unlocks refusing even the
// correct password until /run/faillock clears on reboot. The bundled
// "caelestia" service (assets/pam.d/caelestia) uses plain pam_unix.
config: "caelestia"
configDirectory: Quickshell.shellDir + "/assets/pam.d" configDirectory: Quickshell.shellDir + "/assets/pam.d"
onMessageChanged: { onMessageChanged: {

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

@ -73,10 +73,20 @@ QtObject {
// System // System
Component { Component {
PlaceholderComp {} // Updates (NoSignal)
StackPage {
Component {
UpdatesPage {}
}
}
}, },
Component { Component {
PlaceholderComp {} // Additions (NoSignal)
StackPage {
Component {
AdditionsPage {}
}
}
}, },
// Shell // Shell
@ -155,11 +165,11 @@ QtObject {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.padding.extraSmall spacing: Tokens.padding.extraSmall
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "handyman" icon: "handyman"
color: Colours.palette.m3outlineVariant color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
} }
StyledText { StyledText {

View file

@ -50,9 +50,9 @@ QtObject {
category: "system" category: "system"
}, },
{ {
label: qsTr("Plugins"), label: qsTr("Additions"),
icon: "extension", icon: "extension",
description: qsTr("Manage plugins"), description: qsTr("Install optional software"),
category: "system" category: "system"
}, },

View file

@ -58,13 +58,13 @@ ItemList {
radius: Tokens.rounding.full radius: Tokens.rounding.full
color: device.active ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer color: device.active ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon { NsIcon {
id: devIcon id: devIcon
anchors.centerIn: parent anchors.centerIn: parent
text: root.iconName icon: root.iconName
color: device.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer color: device.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
fill: device.active ? 1 : 0 fill: device.active ? 1 : 0
Behavior on fill { Behavior on fill {
@ -80,10 +80,10 @@ ItemList {
elide: Text.ElideRight elide: Text.ElideRight
} }
MaterialIcon { NsIcon {
text: "check" icon: "check"
color: Colours.palette.m3primary color: Colours.palette.m3primary
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
opacity: device.active ? 1 : 0 opacity: device.active ? 1 : 0
Behavior on opacity { Behavior on opacity {

View file

@ -0,0 +1,73 @@
// CachyRepoToggleRow.qml (NoSignal) Settings -> Services toggle to enable /
// disable the CachyOS pacman repositories. Auto-detects the best x86-64 microarch
// tier the CPU supports (v4 > v3). Live state polls `nosignal-cachy-repo status`
// (no root). enable/disable run in a floating terminal so the pacman/download
// output stays visible; the grant is passwordless via sudo -n
// (/etc/sudoers.d/02-nosignal-cachy). enable adds repos, converts userspace to
// the optimized builds (pacman -Suu, pacman pinned stock), and installs the
// linux-cachyos kernel; disable reverts to stock and removes the cachy kernel
// (keeping stock `linux` bootable). A reboot switches the running kernel.
//
// Untracked file under modules/nexus/common auto-discovered as the type
// `CachyRepoToggleRow` via `import qs.modules.nexus.common`. Survives caelestia
// upgrades; pairs with the one-line insert in ServicesPage.qml.
import QtQuick
import Quickshell.Io
import qs.modules.nexus.common
ToggleRow {
id: root
property bool repoOn: false // NB: not `enabled` (reserved Item property)
text: qsTr("CachyOS repositories")
subtext: repoOn
? qsTr("On — CachyOS optimized builds + linux-cachyos kernel (reboot to run it)")
: qsTr("Switch to CachyOS optimized builds + linux-cachyos kernel (reboot after)")
onToggled: {
if (checked)
enableProc.running = true;
else
disableProc.running = true;
reconcile.restart();
}
// --- live state -----------------------------------------------------------
Process {
id: statusProc
command: ["/usr/local/bin/nosignal-cachy-repo", "status"]
stdout: StdioCollector {
onStreamFinished: {
root.repoOn = text.trim() === "enabled";
root.checked = root.repoOn; // drive switch from real state
}
}
}
// --- actions (passwordless; shown in a terminal for pacman output) ---------
Process {
id: enableProc
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "sudo -n /usr/local/bin/nosignal-cachy-repo enable; printf '\\nPress Enter to close...'; read _"]
onExited: reconcile.restart()
}
Process {
id: disableProc
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "sudo -n /usr/local/bin/nosignal-cachy-repo disable; printf '\\nPress Enter to close...'; read _"]
onExited: reconcile.restart()
}
// --- polling --------------------------------------------------------------
Timer {
interval: 5000
repeat: true
running: true
triggeredOnStart: true
onTriggered: statusProc.running = true
}
Timer {
id: reconcile
interval: 2000
onTriggered: statusProc.running = true
}
}

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
} }

View file

@ -37,11 +37,11 @@ ConnectedRect {
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: root.placeholderIcon icon: root.placeholderIcon
color: Colours.palette.m3outline color: Colours.palette.m3outline
font: Tokens.font.icon.large fontStyle: Tokens.font.icon.large
animate: true animate: true
} }

View file

@ -10,7 +10,7 @@ import qs.modules.nexus.common
ConnectedRect { ConnectedRect {
id: root id: root
property alias icon: icon.text property alias icon: icon.icon
property alias label: label.text property alias label: label.text
property alias status: status.text property alias status: status.text
@ -32,11 +32,11 @@ ConnectedRect {
anchors.rightMargin: Tokens.padding.largeIncreased anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
id: icon id: icon
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
ColumnLayout { ColumnLayout {
@ -63,10 +63,10 @@ ConnectedRect {
} }
} }
MaterialIcon { NsIcon {
text: "chevron_right" icon: "chevron_right"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
} }
} }

View file

@ -11,7 +11,7 @@ import qs.modules.nexus.common
ConnectedRect { ConnectedRect {
id: root id: root
property alias icon: icon.text property alias icon: icon.icon
property alias label: label.text property alias label: label.text
property alias valueLabel: valueLabel.text property alias valueLabel: valueLabel.text
property real value property real value
@ -28,11 +28,11 @@ ConnectedRect {
anchors.topMargin: Tokens.padding.large anchors.topMargin: Tokens.padding.large
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
id: icon id: icon
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
ColumnLayout { ColumnLayout {

View file

@ -0,0 +1,102 @@
// SudoToggleRow.qml (NoSignal) Settings -> Services toggle for time-boxed
// passwordless sudo. Reflects live state by polling `nosignal-sudo-toggle
// status` (no root needed); enabling opens a floating terminal for the ONE
// password prompt, disabling runs passwordless inside the active window.
//
// Untracked file under modules/nexus/common auto-discovered as the type
// `SudoToggleRow` via `import qs.modules.nexus.common`. Survives caelestia
// upgrades; only the one-line insert in ServicesPage.qml is re-applied by hook.
import QtQuick
import Quickshell.Io
import qs.modules.nexus.common
ToggleRow {
id: root
property bool active: false
property int remaining: 0
text: qsTr("Passwordless sudo (15 min)")
subtext: active
? qsTr("On — %1 min left. Auto-reverts; a reboot also clears it.").arg(remaining)
: qsTr("Run sudo without a password for 15 minutes, then it reverts")
onToggled: {
if (checked) {
enableProc.running = true; // needs a password -> floating terminal
focusTimer.ticks = 0;
focusTimer.restart(); // then pull keyboard focus to the prompt
} else {
disableProc.running = true; // no password inside the active window
}
reconcile.restart();
}
// --- live state -----------------------------------------------------------
Process {
id: statusProc
command: ["nosignal-sudo-toggle", "status"]
stdout: StdioCollector {
onStreamFinished: {
const p = text.trim().split(/\s+/);
root.active = p[0] === "active";
root.remaining = parseInt(p[1] || "0") || 0;
root.checked = root.active; // drive switch from real state
}
}
}
// --- actions --------------------------------------------------------------
Process {
id: enableProc
// Dedicated window class (nosignal-sudo) so the shipped hypr windowrules
// (float/center/pin/stayfocused) force this prompt to GRAB keyboard focus.
// With the shared TUI.float class and no focus rule the prompt did not get
// keystrokes -> three blank tries tripped pam_faillock and the switch just
// snapped back with no feedback (hardware bug 2026-06-20). `enable-tui`
// runs the sudo prompt as the user and keeps the terminal open on failure
// so the reason is visible instead of vanishing.
command: ["kitty", "--class", "nosignal-sudo", "-e", "nosignal-sudo-toggle", "enable-tui"]
}
Process {
id: disableProc
command: ["sudo", "-n", "nosignal-sudo-toggle", "disable"]
}
// Force keyboard focus onto the password prompt once it has mapped. Hyprland
// 0.55's match: windowrule grammar has no working `stayfocused`, so we
// "activate it" from here instead (the prompt uses the dedicated nosignal-sudo
// class). Two nudges cover slow terminal startup.
Process {
id: focusProc
command: ["hyprctl", "dispatch", "focuswindow", "class:^(nosignal-sudo)$"]
}
Timer {
id: focusTimer
interval: 400
repeat: true
triggeredOnStart: false
property int ticks: 0
onTriggered: {
focusProc.running = true;
ticks += 1;
if (ticks >= 3) {
ticks = 0;
stop();
}
}
}
// --- polling --------------------------------------------------------------
Timer {
interval: 5000
repeat: true
running: true
triggeredOnStart: true
onTriggered: statusProc.running = true
}
Timer {
id: reconcile
interval: 2000
onTriggered: statusProc.running = true
}
}

View file

@ -86,11 +86,11 @@ VerticalFadeFlickable {
radius: Tokens.rounding.full radius: Tokens.rounding.full
color: item.isCurrentPage ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer color: item.isCurrentPage ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon { NsIcon {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 1 anchors.verticalCenterOffset: 1
text: item.modelData.icon icon: item.modelData.icon
color: item.isCurrentPage ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer color: item.isCurrentPage ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.builders.medium.weight(Font.Medium).build() fontStyle: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
grade: 25 grade: 25

View file

@ -37,8 +37,8 @@ StyledRect {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { NsIcon {
text: "search" icon: "search"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Io import Quickshell.Io
import Caelestia import Caelestia
import Caelestia.Config import Caelestia.Config
@ -61,22 +62,27 @@ PageBase {
width: parent.width - Tokens.padding.largeIncreased * 2 width: parent.width - Tokens.padding.largeIncreased * 2
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
AnimatedLogo { Image {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: implicitWidth Layout.preferredWidth: 104
Layout.preferredHeight: implicitHeight Layout.preferredHeight: 104
source: Quickshell.shellDir + "/assets/nosignal-logo.png"
sourceSize.width: 208
sourceSize.height: 208
fillMode: Image.PreserveAspectFit
smooth: true
} }
StyledText { StyledText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.small Layout.topMargin: Tokens.spacing.small
text: "Caelestia" text: "NoSignal"
font: Tokens.font.headline.builders.large.width(110).build() font: Tokens.font.headline.builders.large.width(110).build()
} }
StyledText { StyledText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: CUtils.version ? `v${CUtils.version}` : "…" text: "V1"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.medium font: Tokens.font.body.medium
} }

View file

@ -0,0 +1,120 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
// NoSignal: the Additions settings page (replaces the upstream Plugins
// placeholder). Optional software installed on demand from official sources
// (pacman repos / upstream installers no AUR, no Flatpak). Items come from
// the additions.json manifest via the status cache written by
// nosignal-additions; Install runs in a visible floating terminal so
// git/sudo/pacman output and prompts stay in front of the user.
PageBase {
id: root
title: qsTr("Additions")
property var status: ({})
readonly property var items: status.items || []
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// The Process objects live INSIDE the layout (its `data` accepts
// non-visual objects) PageBase's default property is a single
// `Item`, so declaring them at page level kills the whole shell.
// Same pattern as UpdatesPage / the upstream AboutPage.
// Read the cached status (instant).
Process {
id: readProc
running: true
command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/additions-status.json\" 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.status = JSON.parse(text);
} catch (e) {
root.status = {};
}
}
}
}
// Refresh the cache on demand (re-runs every item's check).
Process {
id: checkProc
command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" status >/dev/null 2>&1"]
onExited: readProc.running = true
}
// Run an installer in a visible floating terminal.
Process {
id: installProc
property string addId: ""
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" install " + addId + "; printf '\\nPress Enter to close...'; read _"]
onExited: readProc.running = true
}
SectionHeader {
text: qsTr("Optional software")
}
Repeater {
model: root.items
NavRow {
required property var modelData
required property int index
first: index === 0
last: index === root.items.length - 1
icon: modelData.icon || "extension"
label: modelData.name
status: modelData.installed ? qsTr("Installed") : modelData.desc
onClicked: {
if (!modelData.installed && !installProc.running) {
installProc.addId = modelData.id;
installProc.running = true;
}
}
}
}
InfoRow {
visible: root.items.length === 0
first: true
last: true
label: qsTr("No additions manifest")
value: "—"
}
SectionHeader {
text: qsTr("Actions")
}
NavRow {
first: true
last: true
icon: "refresh"
label: qsTr("Re-check installed state")
status: installProc.running ? qsTr("Install running in terminal…") : (checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the list above"))
onClicked: {
if (!checkProc.running)
checkProc.running = true;
}
}
}
}

View file

@ -97,9 +97,9 @@ PageBase {
anchors.rightMargin: Tokens.padding.largeIncreased anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
text: "tune" icon: "tune"
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
ColumnLayout { ColumnLayout {
@ -123,10 +123,10 @@ PageBase {
} }
} }
MaterialIcon { NsIcon {
text: "chevron_right" icon: "chevron_right"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
} }
} }

View file

@ -93,13 +93,13 @@ PageBase {
radius: Tokens.rounding.full radius: Tokens.rounding.full
color: device.connected ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer color: device.connected ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon { NsIcon {
id: deviceIcon id: deviceIcon
anchors.centerIn: parent anchors.centerIn: parent
text: Icons.getBluetoothIcon(device.modelData?.icon ?? "") icon: Icons.getBluetoothIcon(device.modelData?.icon ?? "")
color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
fill: device.connected ? 1 : 0 fill: device.connected ? 1 : 0
opacity: device.textOpacity opacity: device.textOpacity
@ -193,9 +193,9 @@ PageBase {
Anim {} Anim {}
} }
MaterialIcon { NsIcon {
text: "add" icon: "add"
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
StyledText { StyledText {

View file

@ -92,41 +92,71 @@ PageBase {
text: qsTr("Weather") text: qsTr("Weather")
} }
// Placeholder until the map-based location picker lands // Location for the weather service. Weather.qml accepts a city name
// (geocoded) or a "lat,lon" string, and auto-detects via IP when empty.
ConnectedRect { ConnectedRect {
Layout.fillWidth: true Layout.fillWidth: true
first: true first: true
last: true last: true
implicitHeight: comingSoon.implicitHeight + Tokens.padding.extraLarge * 2 implicitHeight: weatherLayout.implicitHeight + weatherLayout.anchors.margins * 2
ColumnLayout { ColumnLayout {
id: comingSoon id: weatherLayout
anchors.centerIn: parent anchors.fill: parent
width: parent.width - Tokens.padding.largeIncreased * 2 anchors.margins: Tokens.padding.medium
spacing: Tokens.padding.extraSmall anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
MaterialIcon { spacing: Tokens.spacing.small
Layout.alignment: Qt.AlignHCenter
text: "map"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
}
StyledText { StyledText {
Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true
text: qsTr("Location picker coming soon") text: qsTr("Location")
color: Colours.palette.m3outlineVariant font: Tokens.font.body.small
font: Tokens.font.title.small elide: Text.ElideRight
}
StyledRect {
Layout.fillWidth: true
implicitHeight: locField.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.small
color: Colours.tPalette.m3surfaceContainerLowest
border.color: Colours.palette.m3outlineVariant
Behavior on border.color {
CAnim {}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: locField.focus = true
}
StyledTextField {
id: locField
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Tokens.padding.large
anchors.rightMargin: Tokens.padding.large
text: GlobalConfig.services.weatherLocation
placeholderText: qsTr("City name or lat,long — empty = auto (IP)")
placeholderTextColor: Colours.palette.m3onSurfaceVariant
color: Colours.palette.m3onSurface
onEditingFinished: GlobalConfig.services.weatherLocation = text
}
} }
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: qsTr("Choose your weather location on a map in a future update") text: qsTr("Leave empty to detect your location automatically.")
color: Colours.palette.m3outlineVariant color: Colours.palette.m3outline
font: Tokens.font.body.small font: Tokens.font.label.small
} }
} }
} }

View file

@ -131,10 +131,10 @@ PageBase {
anchors.rightMargin: Tokens.padding.extraLarge anchors.rightMargin: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
text: Icons.getNetworkIcon(network.modelData.strength) icon: Icons.getNetworkIcon(network.modelData.strength)
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
opacity: network.textOpacity opacity: network.textOpacity
} }
@ -165,10 +165,10 @@ PageBase {
Component { Component {
id: iconComp id: iconComp
MaterialIcon { NsIcon {
text: network.modelData.active ? "settings" : "lock" icon: network.modelData.active ? "settings" : "lock"
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
opacity: network.textOpacity opacity: network.textOpacity
} }
} }
@ -218,9 +218,9 @@ PageBase {
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
text: "add" icon: "add"
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
StyledText { StyledText {

View file

@ -211,6 +211,16 @@ PageBase {
onToggled: GlobalConfig.services.smartScheme = checked onToggled: GlobalConfig.services.smartScheme = checked
} }
// NoSignal: time-boxed passwordless sudo (15 min) toggle
SudoToggleRow {
Layout.fillWidth: true
}
// NoSignal: CachyOS repos enable/disable (sudoless, auto-tier)
CachyRepoToggleRow {
Layout.fillWidth: true
}
SelectRow { SelectRow {
Layout.fillWidth: true Layout.fillWidth: true
last: true last: true

View file

@ -0,0 +1,139 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
// NoSignal: the Updates settings page (replaces the upstream placeholder).
// Reads the JSON cache written by nosignal-update-check; "Update now" runs
// nosignal-update in a floating terminal (sudo + pacman prompts stay visible).
PageBase {
id: root
title: qsTr("Updates")
property var status: ({})
function count(v) {
return v === undefined ? "…" : String(v);
}
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// The Process objects live INSIDE the layout (its `data` accepts
// non-visual objects) PageBase's default property is a single
// `Item`, so declaring them at page level kills the whole shell
// ("Cannot assign Process to QQuickItem*"). Same pattern as the
// upstream AboutPage.
// Read the cached status (instant; the user timer keeps it fresh).
Process {
id: readProc
running: true
command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/update-status.json\" 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.status = JSON.parse(text);
} catch (e) {
root.status = {};
}
}
}
}
// Refresh the cache on demand.
Process {
id: checkProc
command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-update-check\" >/dev/null 2>&1"]
onExited: readProc.running = true
}
// Run the real update in a visible floating terminal.
Process {
id: updateProc
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-update\"; \"$HOME/.local/bin/nosignal-update-check\" >/dev/null 2>&1; printf '\\nPress Enter to close...'; read _"]
onExited: readProc.running = true
}
SectionHeader {
text: qsTr("Pending updates")
}
InfoRow {
first: true
label: qsTr("Official packages")
value: root.count(root.status.repo)
}
InfoRow {
label: qsTr("AUR packages")
value: root.count(root.status.aur)
}
InfoRow {
label: qsTr("Flatpak")
value: root.count(root.status.flatpak)
}
InfoRow {
last: true
label: qsTr("NoSignal layer")
subtext: qsTr("Idempotent migrations applied by nosignal-update")
value: root.status.migrations_pending === undefined ? "…" : (root.status.migrations_pending > 0 ? qsTr("%1 migration(s) pending").arg(root.status.migrations_pending) : qsTr("up to date"))
}
SectionHeader {
text: qsTr("History")
}
InfoRow {
first: true
label: qsTr("Last full upgrade")
value: root.status.last_upgrade || "—"
}
InfoRow {
last: true
label: qsTr("Last checked")
value: root.status.checked ? root.status.checked.replace("T", " ").substring(0, 16) : "—"
}
SectionHeader {
text: qsTr("Actions")
}
NavRow {
first: true
icon: "refresh"
label: qsTr("Check for updates now")
status: checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the counts above")
onClicked: {
if (!checkProc.running)
checkProc.running = true;
}
}
NavRow {
last: true
icon: "system_update_alt"
label: qsTr("Update now")
status: updateProc.running ? qsTr("Running in terminal…") : qsTr("Opens a terminal: snapshot, packages, NoSignal layer")
onClicked: {
if (!updateProc.running)
updateProc.running = true;
}
}
}
}

View file

@ -46,11 +46,11 @@ PageBase {
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "hide_image" icon: "hide_image"
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
} }
StyledText { StyledText {

View file

@ -125,10 +125,10 @@ PageBase {
anchors.rightMargin: Tokens.padding.largeIncreased anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
MaterialIcon { NsIcon {
text: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "") icon: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "")
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
opacity: newDevice.textOpacity opacity: newDevice.textOpacity
} }

View file

@ -72,11 +72,11 @@ PageBase {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 spacing: 0
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "delete" icon: "delete"
color: forgetBtn.onColour color: forgetBtn.onColour
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
StyledText { StyledText {
@ -126,11 +126,11 @@ PageBase {
ColumnLayout { ColumnLayout {
spacing: 0 spacing: 0
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: root.connected ? "close" : "add" icon: root.connected ? "close" : "add"
color: connectBtn.inactiveOnColour color: connectBtn.inactiveOnColour
font: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
animate: true animate: true
} }

View file

@ -22,11 +22,11 @@ PageBase {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.padding.extraSmall spacing: Tokens.padding.extraSmall
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "handyman" icon: "handyman"
color: Colours.palette.m3outlineVariant color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
} }
StyledText { StyledText {

View file

@ -168,9 +168,9 @@ PageBase {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
MaterialIcon { NsIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: "hide_image" icon: "hide_image"
color: Colours.palette.m3outline color: Colours.palette.m3outline
fontStyle: Tokens.font.icon.extraLarge fontStyle: Tokens.font.icon.extraLarge
} }

View file

@ -63,7 +63,7 @@ Item {
anchors.rightMargin: root.clampedPadding anchors.rightMargin: root.clampedPadding
color: "transparent" color: "transparent"
radius: Tokens.rounding.large radius: 0
StyledListView { StyledListView {
id: list id: list

View file

@ -23,7 +23,7 @@ StyledRect {
property bool expanded: Config.notifs.openExpanded property bool expanded: Config.notifs.openExpanded
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.large radius: 0
implicitHeight: inner.implicitHeight implicitHeight: inner.implicitHeight
@ -116,7 +116,7 @@ StyledRect {
visible: root.hasImage || root.hasAppIcon visible: root.hasImage || root.hasAppIcon
sourceComponent: StyledClippingRect { sourceComponent: StyledClippingRect {
radius: Tokens.rounding.full radius: 0
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
implicitWidth: TokenConfig.sizes.notifs.image implicitWidth: TokenConfig.sizes.notifs.image
implicitHeight: TokenConfig.sizes.notifs.image implicitHeight: TokenConfig.sizes.notifs.image
@ -147,7 +147,7 @@ StyledRect {
anchors.bottom: root.hasImage ? image.bottom : undefined anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: StyledRect { sourceComponent: StyledRect {
radius: Tokens.rounding.full radius: 0
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image
implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image
@ -177,8 +177,8 @@ StyledRect {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 1 anchors.verticalCenterOffset: 1
sourceComponent: MaterialIcon { sourceComponent: NsIcon {
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) icon: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
} }
@ -358,17 +358,17 @@ StyledRect {
implicitHeight: expandIcon.implicitHeight implicitHeight: expandIcon.implicitHeight
StateLayer { StateLayer {
radius: Tokens.rounding.full radius: 0
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
onClicked: root.expanded = !root.expanded onClicked: root.expanded = !root.expanded
} }
MaterialIcon { NsIcon {
id: expandIcon id: expandIcon
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: root.expanded ? -1 : 1 anchors.verticalCenterOffset: root.expanded ? -1 : 1
text: "expand_more" icon: "expand_more"
fontStyle: Tokens.font.icon.medium fontStyle: Tokens.font.icon.medium
rotation: root.expanded ? 180 : 0 rotation: root.expanded ? 180 : 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()
}
}

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

@ -0,0 +1,260 @@
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 Quickshell.Services.SystemTray
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, win.modelData.name)
NsIcon {
icon: "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, win.modelData.name)
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, win.modelData.name)
NsIcon {
icon: 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 only shown when an app actually registers a tray
// icon, otherwise the pill just opened an empty panel (F-T1).
IconPill {
icon: "widgets"
panel: "tray"
visible: SystemTray.items.values.length > 0
}
// 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, win.modelData.name)
NsIcon {
icon: 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: 0
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, win.modelData.name)
NsIcon {
icon: 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, win.modelData.name)
NsIcon {
icon: p.icon
color: p.active ? Theme.accent : p.iconColour
}
}
}
}

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

@ -0,0 +1,166 @@
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
// This overlay owns the panel only when NsShell.screen names this output
// (or is "" = unscoped, e.g. the global power modal show on all screens).
// Without this, every screen's overlay rendered the panel mirrored popups.
readonly property bool onThisScreen: NsShell.screen === "" || NsShell.screen === modelData.name
screen: modelData
visible: NsShell.open !== "" && ov.onThisScreen
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" && ov.onThisScreen
sourceComponent: powerC
Component {
id: powerC
Panels.NsPowerMenu {}
}
}
Loader {
id: loader
active: NsShell.open !== "" && NsShell.open !== "power" && ov.onThisScreen
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
NsIcon {
icon: "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
}
NsIcon {
icon: "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
NsIcon {
icon: s.icon
color: Theme.textMuted
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 6
radius: 0
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 0
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: 0
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
NsIcon {
anchors.centerIn: parent
icon: 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: 0
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"
NsIcon {
anchors.centerIn: parent
icon: parent.icon
color: Theme.textMuted
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.activated()
}
}
}

View file

@ -0,0 +1,237 @@
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.
//
// FIX 2026-06-20 (hardware test): the shipped panel was display-only the row
// MouseArea had no onClicked and there was no password field, so a secured Wi-Fi
// network could not be joined from the GUI (user had to drop to nmcli/nmtui).
// This wires the existing Network service: click a row to connect; open or
// already-saved networks connect directly; a secured network with no saved
// profile expands an inline password field (StyledTextField, echoMode Password).
// NsOverlay grants OnDemand keyboard focus while visible, so the field is typable.
// NOTE: authored on the test machine but NOT yet built/cert'd review in builder.
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.utils
import qs.components
import qs.components.controls
NsPanel {
id: root
implicitWidth: 340
anchorMode: "right"
// ssid of the secured row currently showing its inline password field ("" = none)
property string expandedSsid: ""
// Decide what a click on a network row does.
function activate(m): void {
if (!m)
return;
if (m.active) {
// Already connected -> tapping disconnects.
Network.disconnectFromNetwork();
root.expandedSsid = "";
return;
}
if (!m.isSecure || Network.hasSavedProfile(m.ssid)) {
// Open network, or we already hold the PSK -> connect straight away.
Network.connectToNetworkWithPasswordCheck(m.ssid, m.isSecure, () => {}, m.bssid);
root.expandedSsid = "";
} else {
// Secured + no saved profile -> reveal the inline password field.
root.expandedSsid = (root.expandedSsid === m.ssid) ? "" : m.ssid;
}
}
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: ColumnLayout {
id: rowWrap
required property var modelData
readonly property bool connected: modelData?.active ?? false
readonly property bool expanded: root.expandedSsid === (modelData?.ssid ?? " ")
Layout.fillWidth: true
spacing: 2
function doConnect(): void {
const m = rowWrap.modelData;
if (!m || pwField.text.length === 0)
return;
Network.connectToNetwork(m.ssid, pwField.text, m.bssid, () => {});
pwField.text = "";
root.expandedSsid = "";
}
Rectangle {
id: row
Layout.fillWidth: true
implicitHeight: 40
radius: Theme.radius.button
color: rowWrap.connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
NsIcon {
icon: Icons.getNetworkIcon(rowWrap.modelData?.strength ?? 0)
color: rowWrap.connected ? Theme.accent : Theme.text
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: rowWrap.modelData?.ssid ?? "?"
color: Theme.text
elide: Text.ElideRight
Layout.fillWidth: true
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
visible: rowWrap.connected
text: "Connected"
color: Theme.accent
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
NsIcon {
visible: rowWrap.modelData?.isSecure ?? false
icon: "lock"
color: Theme.textFaint
}
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activate(rowWrap.modelData)
}
}
// inline password entry only for a secured network with no saved profile
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: 4
spacing: 8
visible: rowWrap.expanded
onVisibleChanged: {
if (visible)
pwField.forceActiveFocus();
}
StyledTextField {
id: pwField
Layout.fillWidth: true
echoMode: TextField.Password
placeholderText: "Password"
onAccepted: rowWrap.doConnect()
}
Rectangle {
implicitWidth: 72
implicitHeight: 28
radius: Theme.radius.button
color: cma.containsMouse ? Theme.accent : Theme.accentSoft
StyledText {
anchors.centerIn: parent
text: "Connect"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
MouseArea {
id: cma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: rowWrap.doConnect()
}
}
}
}
}
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,179 @@
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: 0
color: Notifs.dnd ? Theme.accentSoft : dndMa.containsMouse ? Theme.hover : "transparent"
NsIcon {
anchors.centerIn: parent
icon: 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: {
// Mirror the service's own clear() / clearNotifs shortcut:
// NotifData.close() sets `closed` (drops it from notClosed so the
// card disappears), removes it from Notifs.list, dismisses the
// server notification and destroys it. Calling
// `notification?.dismiss()` directly cleared nothing it never
// touched the list, and is a no-op for notifications restored from
// disk (their `notification` is null). Iterate a slice() copy so
// mutating the list mid-loop is safe.
for (const n of Notifs.list.slice())
n.close();
}
}
}
}
// 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
NsIcon {
anchors.centerIn: parent
icon: 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
NsIcon {
anchors.centerIn: parent
visible: cell.occupied
icon: "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
NsIcon {
Layout.alignment: Qt.AlignHCenter
icon: 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,424 @@
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 drives hyprsunset; Airplane drives the
// NetworkManager + Bluetooth radios. (VPN tile removed pending a real backend.)
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
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
// Night Light state tracks the real hyprsunset process rather than a local
// flag, so the tile stays correct across shell reloads or changes made
// elsewhere. Seeded on load (probe runs immediately), reconciled on a timer,
// and re-checked right after a toggle.
property bool nightLightOn: false
Process {
id: nightProbe
command: ["pgrep", "-x", "hyprsunset"]
running: true
stdout: StdioCollector {
onStreamFinished: root.nightLightOn = text.trim().length > 0
}
}
Timer {
interval: 3000
running: true
repeat: true
onTriggered: nightProbe.running = true
}
Timer {
id: nightRefresh
interval: 500
repeat: false
onTriggered: nightProbe.running = true
}
// Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Rectangle {
implicitWidth: 40
implicitHeight: 40
radius: 0
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: 0
color: pwrMa.containsMouse ? Theme.accentSoft : Theme.fillSubtle
NsIcon {
anchors.centerIn: parent
icon: "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"
on: root.nightLightOn
onToggled: {
if (root.nightLightOn)
Quickshell.execDetached(["pkill", "-x", "hyprsunset"]);
else
Quickshell.execDetached(["sh", "-c", "pkill -x hyprsunset; hyprsunset -t 3500"]);
nightRefresh.restart();
}
}
QSToggle {
id: airTog
icon: "airplanemode_active"
label: "Airplane"
// Reflects the real radio state: airplane is "On" when both Wi-Fi and
// Bluetooth are down. Network.wifiEnabled tracks nmcli; the adapter
// tracks BlueZ, so the tile self-corrects if radios change elsewhere.
readonly property bool radiosOff: !Network.wifiEnabled && !(Bluetooth.defaultAdapter?.enabled ?? false)
sub: radiosOff ? "On" : "Off"
on: radiosOff
onToggled: {
if (radiosOff)
Quickshell.execDetached(["sh", "-c", "nmcli radio all on; bluetoothctl power on"]);
else
Quickshell.execDetached(["sh", "-c", "nmcli radio all off; bluetoothctl power off"]);
}
}
}
// 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
}
}
NsIcon {
anchors.centerIn: parent
icon: "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: 0
color: tile.on ? Theme.accent : Qt.rgba(1, 1, 1, 0.06)
NsIcon {
anchors.centerIn: parent
icon: 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
NsIcon {
icon: s.icon
color: Theme.textMuted
}
Rectangle {
id: track
Layout.fillWidth: true
implicitHeight: 6
radius: 0
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 0
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: 0
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: 0
color: mbm.containsMouse ? Theme.hover : "transparent"
NsIcon {
anchors.centerIn: parent
icon: 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
}
}

View file

@ -24,6 +24,15 @@ Item {
implicitWidth: layout.implicitWidth + Tokens.padding.large + layout.anchors.horizontalCenterOffset * 2 implicitWidth: layout.implicitWidth + Tokens.padding.large + layout.anchors.horizontalCenterOffset * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
// NoSignal: flat square glass background matching the design popouts
Rectangle {
anchors.fill: parent
color: Theme.panelBg
radius: 0
border.width: 1
border.color: Theme.panelBorder
}
ColumnLayout { ColumnLayout {
id: layout id: layout

Some files were not shown because too many files have changed in this diff Show more