feat(qbz-slint): header redesign, Lucide icons, scrollbar fixes

- Replace every Unicode glyph placeholder with Lucide icons via QbzIcon
  (player transport, carousel nav, album-card overlay, album-page
  actions, gear, search, checkbox, sidebar).
- Header: the search field is centered to the window width and gets a
  search icon; the header and player bar now use the elevated surface,
  so all chrome (header, sidebar, player bar) shares one color over the
  darker content area.
- The sidebar user card moves to the header as a user element (name +
  QBZ logo) that opens a dropdown — Settings, Keyboard Shortcuts,
  Documentation, Report an Issue, About, plus Log Out and Close. Log Out
  clears the session and returns to login; Close quits the app.
- Content ScrollViews disable the horizontal scrollbar — no view scrolls
  horizontally, so the phantom reserved lane is gone.

cc
This commit is contained in:
vicrodh committed 2026-05-18 14:58:58 -06:00
1 parent 636b3e8a06
commit 8c27de5651
13 files changed
+389 -165

No files matched your search

+28 -18
View File
@@ -10,10 +10,11 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { Spacing } from "../foundation/spacing.slint";
import { AlbumState, AlbumTrackItem, NavState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ScrollView } from "std-widgets.slint";
component CircleAction inherits Rectangle {
in property <string> glyph;
in property <image> icon;
in property <bool> primary: false;
in property <length> diameter: 40px;
callback clicked();
@@ -25,12 +26,13 @@ component CircleAction inherits Rectangle {
? (ta.has-hover ? Theme.accent-hover : Theme.accent)
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
Text {
text: root.glyph;
color: root.primary ? Theme.accent-text : Theme.text-secondary;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: 17px;
height: 17px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: root.primary ? Theme.accent-text : Theme.text-secondary;
}
ta := TouchArea {
@@ -133,6 +135,7 @@ export component AlbumPageView inherits Rectangle {
background: Theme.surface-main;
ScrollView {
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
padding-left: 24px;
padding-right: 24px;
@@ -151,11 +154,14 @@ export component AlbumPageView inherits Rectangle {
}
back-row := HorizontalLayout {
spacing: 6px;
Text {
text: "‹";
color: back.has-hover ? Theme.text-secondary : Theme.text-muted;
font-size: 18px;
vertical-alignment: center;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/arrow-left.svg");
width: 16px;
height: 16px;
tint: back.has-hover ? Theme.text-secondary : Theme.text-muted;
}
}
Text {
text: @tr("Back");
@@ -222,18 +228,22 @@ export component AlbumPageView inherits Rectangle {
HorizontalLayout {
alignment: start;
spacing: 12px;
CircleAction { glyph: "▶"; primary: true; diameter: 44px; }
VerticalLayout {
alignment: center;
CircleAction { glyph: "⇄"; }
CircleAction {
icon: @image-url("../assets/icons/play.svg");
primary: true;
diameter: 44px;
}
VerticalLayout {
alignment: center;
CircleAction { glyph: "♡"; }
CircleAction { icon: @image-url("../assets/icons/shuffle.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { glyph: "⋯"; }
CircleAction { icon: @image-url("../assets/icons/heart.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/ellipsis.svg"); }
}
}
}
+9 -1
View File
@@ -10,7 +10,7 @@ import { LoginScreen } from "login/LoginScreen.slint";
import { AppShell } from "shell/AppShell.slint";
// Re-export the state globals so the Rust layer can populate them.
export { HomeState, AlbumState, NavState } from "state.slint";
export { HomeState, AlbumState, NavState, SessionState } from "state.slint";
// Which top-level screen is shown. The app starts on `splash` while it
// restores a saved session, then resolves to `shell` or `login`.
@@ -32,6 +32,8 @@ export component AppWindow inherits Window {
callback start-offline();
callback open-tos();
callback open-album(string);
callback logout();
callback close-app();
if root.screen == AppScreen.splash: SplashScreen { }
@@ -54,5 +56,11 @@ export component AppWindow inherits Window {
open-album(id) => {
root.open-album(id);
}
logout => {
root.logout();
}
close-app => {
root.close-app();
}
}
}
+14 -12
View File
@@ -11,13 +11,14 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { Spacing } from "../foundation/spacing.slint";
import { AlbumCardItem } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
// Circular hover-overlay action button.
component OverlayButton inherits Rectangle {
in property <string> glyph;
in property <image> icon;
in property <bool> primary: false;
in property <length> diameter: 36px;
in property <length> glyph-size: 16px;
in property <length> icon-size: 16px;
width: root.diameter;
height: root.diameter;
@@ -26,12 +27,13 @@ component OverlayButton inherits Rectangle {
border-width: root.primary ? 0px : 1.5px;
border-color: #ffffffe6;
Text {
text: root.glyph;
color: root.primary ? #000000 : #ffffff;
font-size: root.glyph-size;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: root.icon-size;
height: root.icon-size;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: root.primary ? #000000 : #ffffff;
}
}
@@ -109,17 +111,17 @@ export component AlbumCard inherits Rectangle {
VerticalLayout {
alignment: center;
OverlayButton { glyph: "♡"; }
OverlayButton { icon: @image-url("../assets/icons/heart.svg"); }
}
OverlayButton {
glyph: "▶";
icon: @image-url("../assets/icons/play.svg");
primary: true;
diameter: 44px;
glyph-size: 17px;
icon-size: 18px;
}
VerticalLayout {
alignment: center;
OverlayButton { glyph: "⋯"; glyph-size: 18px; }
OverlayButton { icon: @image-url("../assets/icons/ellipsis.svg"); }
}
}
+11 -9
View File
@@ -9,9 +9,10 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavButton inherits Rectangle {
in property <string> glyph;
in property <image> icon;
in property <bool> enabled: true;
callback clicked();
@@ -23,12 +24,13 @@ component NavButton inherits Rectangle {
? Theme.surface-hover
: Theme.surface-elevated;
Text {
text: root.glyph;
color: root.enabled ? Theme.text-primary : Theme.text-muted;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: root.enabled ? Theme.text-primary : Theme.text-muted;
}
ta := TouchArea {
@@ -77,7 +79,7 @@ export component Carousel inherits VerticalLayout {
VerticalLayout {
alignment: center;
NavButton {
glyph: "‹";
icon: @image-url("../assets/icons/chevron-left.svg");
enabled: root.current-page > 0;
clicked => {
root.page = root.current-page - 1;
@@ -87,7 +89,7 @@ export component Carousel inherits VerticalLayout {
VerticalLayout {
alignment: center;
NavButton {
glyph: "›";
icon: @image-url("../assets/icons/chevron-right.svg");
enabled: root.current-page < root.page-count - 1;
clicked => {
root.page = root.current-page + 1;
+20 -7
View File
@@ -10,6 +10,7 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { HomeState } from "../state.slint";
import { Carousel } from "Carousel.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ScrollView } from "std-widgets.slint";
component Tab inherits Rectangle {
@@ -50,8 +51,18 @@ component GenreButton inherits Rectangle {
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
HorizontalLayout {
padding-left: 14px;
padding-left: 12px;
padding-right: 14px;
spacing: 7px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/list-filter.svg");
width: 14px;
height: 14px;
tint: Theme.text-secondary;
}
}
Text {
text: @tr("Filter by genre");
color: Theme.text-secondary;
@@ -76,12 +87,13 @@ component GearButton inherits Rectangle {
border-radius: 4px;
background: ta.has-hover ? Theme.surface-hover : transparent;
Text {
text: "⚙";
color: ta.has-hover ? Theme.text-primary : Theme.text-muted;
font-size: 17px;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: @image-url("../assets/icons/settings.svg");
width: 18px;
height: 18px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
ta := TouchArea {
@@ -144,6 +156,7 @@ export component HomeView inherits Rectangle {
// --- Scrollable sections -----------------------------------------
ScrollView {
vertical-stretch: 1;
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
padding: 32px;
spacing: 28px;
@@ -1,6 +1,7 @@
// Small square checkbox primitive matching the QBZ dark theme.
import { Theme } from "../foundation/semantic-colors.slint";
import { QbzIcon } from "QbzIcon.slint";
export component QbzCheckbox inherits Rectangle {
in-out property <bool> checked: false;
@@ -13,14 +14,14 @@ export component QbzCheckbox inherits Rectangle {
border-width: root.checked ? 0px : 1.5px;
border-color: Theme.text-muted;
Text {
text: "✓";
QbzIcon {
source: @image-url("../assets/icons/check.svg");
visible: root.checked;
color: Theme.accent-text;
font-size: 13px;
font-weight: 700;
horizontal-alignment: center;
vertical-alignment: center;
width: 12px;
height: 12px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.accent-text;
}
TouchArea {
+10 -1
View File
@@ -13,13 +13,22 @@ import { AlbumPageView } from "../album/AlbumPageView.slint";
export component AppShell inherits Rectangle {
callback open-album(string);
callback logout();
callback close-app();
background: Theme.surface-main;
VerticalLayout {
spacing: 0px;
HeaderBar { }
HeaderBar {
logout => {
root.logout();
}
close-app => {
root.close-app();
}
}
Rectangle {
height: Layout.hairline;
background: Theme.border-subtle;
+185 -22
View File
@@ -1,11 +1,13 @@
// Top header bar — primary navigation tabs and the search field.
// Top header bar — primary navigation tabs, a window-centered search
// field, and the user element with its dropdown menu.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { LineEdit } from "std-widgets.slint";
import { SessionState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavTab inherits Rectangle {
in property <string> label;
@@ -37,35 +39,196 @@ component NavTab inherits Rectangle {
}
}
export component HeaderBar inherits Rectangle {
height: Layout.header-height;
background: Theme.surface-main;
component MenuItem inherits Rectangle {
in property <image> icon;
in property <string> label;
callback clicked();
height: 34px;
background: ta.has-hover ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: Spacing.md;
padding-right: Spacing.md;
spacing: Spacing.xs;
padding-left: 12px;
padding-right: 18px;
spacing: 10px;
VerticalLayout {
alignment: center;
HorizontalLayout {
spacing: Spacing.xs;
NavTab { label: "Discover"; active: true; }
NavTab { label: "Library"; }
NavTab { label: "Local Library"; }
NavTab { label: "My QBZ"; }
QbzIcon {
source: root.icon;
width: 15px;
height: 15px;
tint: Theme.text-secondary;
}
}
VerticalLayout {
alignment: center;
Text {
text: root.label;
color: Theme.text-secondary;
font-size: 13px;
vertical-alignment: center;
horizontal-stretch: 1;
HorizontalLayout {
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
export component HeaderBar inherits Rectangle {
callback logout();
callback close-app();
height: Layout.header-height;
background: Theme.surface-card;
// --- Nav tabs (left) -------------------------------------------------
VerticalLayout {
x: Spacing.md;
height: parent.height;
alignment: center;
HorizontalLayout {
spacing: Spacing.xs;
NavTab { label: "Discover"; active: true; }
NavTab { label: "Library"; }
NavTab { label: "Local Library"; }
NavTab { label: "My QBZ"; }
}
}
// --- Search field, centered to the header width ----------------------
Rectangle {
width: 320px;
height: 32px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
border-radius: 6px;
border-width: 1px;
border-color: Theme.border-subtle;
background: Theme.surface-elevated;
HorizontalLayout {
padding-left: 10px;
padding-right: 10px;
spacing: 7px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 14px;
height: 14px;
tint: Theme.text-muted;
}
}
Text {
text: @tr("Search");
color: Theme.text-muted;
font-size: 13px;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
}
// --- User element + dropdown (right) ---------------------------------
user-area := TouchArea {
width: user-row.preferred-width + 16px;
height: 36px;
x: parent.width - self.width - Spacing.md;
y: (parent.height - self.height) / 2;
mouse-cursor: pointer;
clicked => {
menu.show();
}
Rectangle {
width: 100%;
height: 100%;
border-radius: Radius.sm;
background: user-area.has-hover ? Theme.surface-hover : transparent;
}
user-row := HorizontalLayout {
x: 8px;
height: parent.height;
spacing: 8px;
VerticalLayout {
alignment: center;
Text {
text: SessionState.user-name == ""
? @tr("Guest")
: SessionState.user-name;
color: Theme.text-primary;
font-size: Typography.body;
}
}
VerticalLayout {
alignment: center;
Image {
source: @image-url("../assets/qbz-logo.png");
width: 28px;
height: 28px;
image-fit: contain;
}
}
}
}
menu := PopupWindow {
x: parent.width - 234px - Spacing.md;
y: Layout.header-height - 4px;
width: 234px;
height: 254px;
Rectangle {
background: Theme.surface-elevated;
border-radius: Radius.md;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 24px;
drop-shadow-color: Theme.card-shadow;
drop-shadow-offset-y: 6px;
VerticalLayout {
padding-top: 6px;
padding-bottom: 6px;
MenuItem {
icon: @image-url("../assets/icons/settings.svg");
label: @tr("Settings");
}
MenuItem {
icon: @image-url("../assets/icons/keyboard.svg");
label: @tr("Keyboard Shortcuts");
}
MenuItem {
icon: @image-url("../assets/icons/book-open.svg");
label: @tr("Documentation");
}
MenuItem {
icon: @image-url("../assets/icons/bug.svg");
label: @tr("Report an Issue");
}
MenuItem {
icon: @image-url("../assets/icons/info.svg");
label: @tr("About");
}
Rectangle {
width: 360px;
search := LineEdit {
placeholder-text: "Search";
height: 1px;
background: Theme.border-subtle;
}
MenuItem {
icon: @image-url("../assets/icons/log-out.svg");
label: @tr("Log Out");
clicked => {
root.logout();
}
}
MenuItem {
icon: @image-url("../assets/icons/x.svg");
label: @tr("Close");
clicked => {
root.close-app();
}
}
}
+26 -21
View File
@@ -1,18 +1,18 @@
// Bottom now-playing / transport bar.
//
// Shell-frame increment: controls are laid out and hover-reactive but
// not yet wired to playback. Real playback state arrives in a later
// milestone.
// Controls are laid out and hover-reactive but not yet wired to
// playback. Real playback state arrives in a later milestone.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component GlyphButton inherits Rectangle {
in property <string> glyph;
in property <length> glyph-size: 18px;
component IconButton inherits Rectangle {
in property <image> icon;
in property <length> icon-size: 18px;
callback clicked();
width: 34px;
@@ -20,12 +20,13 @@ component GlyphButton inherits Rectangle {
border-radius: Radius.sm;
background: ta.has-hover ? Theme.surface-hover : transparent;
Text {
text: root.glyph;
font-size: root.glyph-size;
color: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: root.icon-size;
height: root.icon-size;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
}
ta := TouchArea {
@@ -38,7 +39,7 @@ component GlyphButton inherits Rectangle {
export component PlayerBar inherits Rectangle {
height: Layout.player-bar-height;
background: Theme.surface-main;
background: Theme.surface-card;
VerticalLayout {
spacing: 0px;
@@ -65,11 +66,14 @@ export component PlayerBar inherits Rectangle {
alignment: center;
HorizontalLayout {
spacing: 2px;
GlyphButton { glyph: "⇄"; }
GlyphButton { glyph: "⏮"; }
GlyphButton { glyph: "▶"; glyph-size: 22px; }
GlyphButton { glyph: "⏭"; }
GlyphButton { glyph: "↻"; }
IconButton { icon: @image-url("../assets/icons/shuffle.svg"); }
IconButton { icon: @image-url("../assets/icons/skip-back.svg"); }
IconButton {
icon: @image-url("../assets/icons/play.svg");
icon-size: 22px;
}
IconButton { icon: @image-url("../assets/icons/skip-forward.svg"); }
IconButton { icon: @image-url("../assets/icons/repeat.svg"); }
}
}
@@ -94,7 +98,7 @@ export component PlayerBar inherits Rectangle {
font-size: Typography.body;
}
Text {
text: "QBZ Slint POC";
text: "QBZ MVP";
color: Theme.text-muted;
font-size: 12px;
}
@@ -108,8 +112,9 @@ export component PlayerBar inherits Rectangle {
HorizontalLayout {
alignment: end;
spacing: Spacing.sm;
GlyphButton { glyph: "♡"; }
GlyphButton { glyph: "⤢"; }
IconButton { icon: @image-url("../assets/icons/heart.svg"); }
IconButton { icon: @image-url("../assets/icons/maximize-2.svg"); }
IconButton { icon: @image-url("../assets/icons/volume-2.svg"); }
VerticalLayout {
alignment: center;
Rectangle {
+13 -54
View File
@@ -1,13 +1,13 @@
// Left navigation sidebar — playlists list plus the user card.
// Left navigation sidebar — the playlists list.
//
// The playlist list is empty until real playlist data is wired (the
// sidebar is a later milestone). No placeholder/fixture rows.
// sidebar is a later milestone). The user element now lives in the
// header, not at the bottom of the sidebar.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ScrollView } from "std-widgets.slint";
export component Sidebar inherits Rectangle {
@@ -29,66 +29,25 @@ export component Sidebar inherits Rectangle {
vertical-alignment: center;
horizontal-stretch: 1;
}
Text {
text: "+";
color: Theme.text-muted;
font-size: 18px;
vertical-alignment: center;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/plus.svg");
width: 16px;
height: 16px;
tint: Theme.text-muted;
}
}
}
ScrollView {
vertical-stretch: 1;
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
spacing: 2px;
// Real playlist rows are populated when the sidebar
// milestone wires playlist data. No fixtures.
}
}
Rectangle {
height: 56px;
border-radius: Radius.md;
background: Theme.surface-elevated;
HorizontalLayout {
padding: 10px;
spacing: Spacing.sm;
Rectangle {
width: 36px;
height: 36px;
border-radius: Radius.pill;
background: Theme.accent;
Text {
text: "Q";
color: Theme.accent-text;
font-weight: Typography.semibold;
horizontal-alignment: center;
vertical-alignment: center;
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: "QBZ User";
color: Theme.text-primary;
font-size: Typography.body;
}
Text {
text: "QBZ MVP";
color: Theme.text-muted;
font-size: 11px;
}
}
Text {
text: "⚙";
color: Theme.text-muted;
font-size: 16px;
vertical-alignment: center;
}
}
}
}
}
+5
View File
@@ -51,3 +51,8 @@ export global AlbumState {
export global NavState {
in-out property <bool> showing-album: false;
}
// Authenticated user, for the header user element.
export global SessionState {
in property <string> user-name: "";
}