feat(slint): collapsible sidebar, Queue/Lyrics panels, nav menus

Shell chrome (steps 1-3):
- Left sidebar is collapsible — a toggle in the nav cluster (left of the
  [<] [>] pair) and a draggable right edge that resizes 200-420px and
  collapses below 160px. ShellState global drives it.
- Queue and Lyrics right sidebars: 320px panels rendered as layout
  siblings, so opening one pushes the content area narrower rather than
  overlaying it. The header captions/queue buttons toggle them
  (mutually exclusive) and light up when active.
- The four nav tabs (Discover/Library/Local Library/My QBZ) open
  dropdown menus listing their sub-sections; Discover -> Home is wired.

Detail views (steps 4-5):
- Track rows: the even-row tint is now clearly fainter than the hover
  tint so the two states no longer look identical.
- The favorite and offline-cache glyphs in album track rows are real
  buttons (hover state, emit media-action).
- Album header artist link uses the metadata text color, bold, instead
  of the accent blue.
- Album description and artist biography span the full content width
  below the header block.

cc
This commit is contained in:
vicrodh committed 2026-05-18 21:54:31 -06:00
1 parent 99e3513d68
commit 33df7bce76
10 files changed
+461 -80

No files matched your search

+80 -53
View File
@@ -56,9 +56,11 @@ component TrackRow inherits Rectangle {
height: 56px; height: 56px;
border-radius: 8px; border-radius: 8px;
// The even-row tint is clearly fainter than the hover tint so the two
// never read as the same state.
background: ta.has-hover background: ta.has-hover
? Theme.surface-hover ? #ffffff16
: (Math.mod(root.index, 2) == 1 ? Theme.surface-card : transparent); : (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
// Row hover/background detector — declared before the content so the // Row hover/background detector — declared before the content so the
// more-button keeps its own clicks. // more-button keeps its own clicks.
@@ -144,28 +146,52 @@ component TrackRow inherits Rectangle {
vertical-alignment: center; vertical-alignment: center;
} }
// Favorite column (visual). // Favorite button.
Rectangle { VerticalLayout {
width: 28px; alignment: center;
QbzIcon { Rectangle {
source: @image-url("../assets/icons/heart.svg"); width: 28px;
width: 16px; height: 28px;
height: 16px; border-radius: Radius.sm;
x: (parent.width - self.width) / 2; background: fav-ta.has-hover ? Theme.surface-elevated : transparent;
y: (parent.height - self.height) / 2; QbzIcon {
tint: Theme.text-muted; source: @image-url("../assets/icons/heart.svg");
width: 16px;
height: 16px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: fav-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
fav-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.media-action("track", root.track.id, "favorite");
}
}
} }
} }
// Offline-cache column (visual). // Offline-cache button.
Rectangle { VerticalLayout {
width: 28px; alignment: center;
QbzIcon { Rectangle {
source: @image-url("../assets/icons/cloud-download.svg"); width: 28px;
width: 16px; height: 28px;
height: 16px; border-radius: Radius.sm;
x: (parent.width - self.width) / 2; background: dl-ta.has-hover ? Theme.surface-elevated : transparent;
y: (parent.height - self.height) / 2; QbzIcon {
tint: Theme.text-muted; source: @image-url("../assets/icons/cloud-download.svg");
width: 16px;
height: 16px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: dl-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
dl-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.media-action("track", root.track.id, "download");
}
}
} }
} }
@@ -377,11 +403,13 @@ export component AlbumPageView inherits Rectangle {
} }
artist-text := Text { artist-text := Text {
text: AlbumState.artist; text: AlbumState.artist;
// Same color as the surrounding metadata,
// bold to read as a link.
color: artist-area.has-hover color: artist-area.has-hover
? Theme.accent-hover ? Theme.text-primary
: Theme.accent; : Theme.text-secondary;
font-size: Typography.heading; font-size: Typography.heading;
font-weight: Typography.medium; font-weight: Typography.bold;
} }
} }
} }
@@ -393,34 +421,6 @@ export component AlbumPageView inherits Rectangle {
overflow: elide; overflow: elide;
} }
// Editorial description — fills the metadata column's
// width (the dead space right of the artwork), truncated,
// with a Read more link to the full modal.
if AlbumState.description != "": Rectangle { height: 12px; }
if AlbumState.description != "": Text {
text: AlbumState.description-short;
color: Theme.text-secondary;
font-size: Typography.legal;
wrap: word-wrap;
}
if AlbumState.description != AlbumState.description-short: Rectangle { height: 4px; }
if AlbumState.description != AlbumState.description-short: read-more := TouchArea {
width: read-more-row.preferred-width;
height: 20px;
mouse-cursor: pointer;
clicked => {
root.show-description = true;
}
read-more-row := HorizontalLayout {
Text {
text: @tr("Read more");
color: read-more.has-hover ? Theme.accent-hover : Theme.accent;
font-size: Typography.legal;
vertical-alignment: center;
}
}
}
Rectangle { height: 20px; } Rectangle { height: 20px; }
HorizontalLayout { HorizontalLayout {
alignment: start; alignment: start;
@@ -474,6 +474,33 @@ export component AlbumPageView inherits Rectangle {
} }
} }
// Editorial description — full content width (the artwork no
// longer caps it), truncated, with a Read more link.
if AlbumState.description != "": Rectangle { height: 18px; }
if AlbumState.description != "": Text {
text: AlbumState.description-short;
color: Theme.text-secondary;
font-size: Typography.body;
wrap: word-wrap;
}
if AlbumState.description != AlbumState.description-short: Rectangle { height: 4px; }
if AlbumState.description != AlbumState.description-short: read-more := TouchArea {
width: read-more-row.preferred-width;
height: 20px;
mouse-cursor: pointer;
clicked => {
root.show-description = true;
}
read-more-row := HorizontalLayout {
Text {
text: @tr("Read more");
color: read-more.has-hover ? Theme.accent-hover : Theme.accent;
font-size: Typography.legal;
vertical-alignment: center;
}
}
}
Rectangle { height: 20px; } Rectangle { height: 20px; }
Rectangle { Rectangle {
height: 1px; height: 1px;
+13 -10
View File
@@ -54,9 +54,11 @@ component TrackRow inherits Rectangle {
height: 52px; height: 52px;
border-radius: 8px; border-radius: 8px;
// The even-row tint is clearly fainter than the hover tint so the two
// never read as the same state.
background: ta.has-hover background: ta.has-hover
? Theme.surface-hover ? #ffffff16
: (Math.mod(root.index, 2) == 1 ? Theme.surface-card : transparent); : (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
ta := TouchArea { ta := TouchArea {
mouse-cursor: default; mouse-cursor: default;
@@ -253,14 +255,6 @@ export component ArtistPageView inherits Rectangle {
font-weight: Typography.bold; font-weight: Typography.bold;
overflow: elide; overflow: elide;
} }
Rectangle { height: 12px; }
Text {
text: ArtistState.bio;
visible: ArtistState.bio != "";
color: Theme.text-secondary;
font-size: Typography.body;
wrap: word-wrap;
}
Rectangle { height: 18px; } Rectangle { height: 18px; }
HorizontalLayout { HorizontalLayout {
alignment: start; alignment: start;
@@ -290,6 +284,15 @@ export component ArtistPageView inherits Rectangle {
} }
} }
// Biography — full content width, below the header block.
if ArtistState.bio != "": Rectangle { height: 18px; }
if ArtistState.bio != "": Text {
text: ArtistState.bio;
color: Theme.text-secondary;
font-size: Typography.body;
wrap: word-wrap;
}
Rectangle { height: 36px; } Rectangle { height: 36px; }
if ArtistState.loading && ArtistState.top-tracks.length == 0: Text { if ArtistState.loading && ArtistState.top-tracks.length == 0: Text {
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

+8 -1
View File
@@ -6,10 +6,12 @@
// content well); no hairline borders. // content well); no hairline borders.
import { Theme } from "../foundation/semantic-colors.slint"; import { Theme } from "../foundation/semantic-colors.slint";
import { NavState, ContentView } from "../state.slint"; import { NavState, ContentView, ShellState } from "../state.slint";
import { Sidebar } from "Sidebar.slint"; import { Sidebar } from "Sidebar.slint";
import { HeaderBar } from "HeaderBar.slint"; import { HeaderBar } from "HeaderBar.slint";
import { PlayerBar } from "PlayerBar.slint"; import { PlayerBar } from "PlayerBar.slint";
import { QueueSidebar } from "QueueSidebar.slint";
import { LyricsSidebar } from "LyricsSidebar.slint";
import { HomeView } from "../discover/HomeView.slint"; import { HomeView } from "../discover/HomeView.slint";
import { AlbumPageView } from "../album/AlbumPageView.slint"; import { AlbumPageView } from "../album/AlbumPageView.slint";
import { ArtistPageView } from "../artist/ArtistPageView.slint"; import { ArtistPageView } from "../artist/ArtistPageView.slint";
@@ -105,6 +107,11 @@ export component AppShell inherits Rectangle {
} }
} }
} }
// Right-side panel — a layout sibling of the content area,
// so it shrinks the content rather than overlaying it.
if ShellState.right-panel == 1: QueueSidebar { }
if ShellState.right-panel == 2: LyricsSidebar { }
} }
PlayerBar { } PlayerBar { }
+120 -12
View File
@@ -13,7 +13,7 @@ import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint"; import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint"; import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint"; import { Radius } from "../foundation/radius.slint";
import { SessionState, NavState, ContentView } from "../state.slint"; import { SessionState, NavState, ContentView, ShellState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavTab inherits Rectangle { component NavTab inherits Rectangle {
@@ -97,15 +97,104 @@ component MenuItem inherits Rectangle {
} }
} }
// Square icon button for the header's right-side controls. // Text-only menu row for the nav-tab dropdowns (no leading icon).
component NavMenuItem inherits Rectangle {
in property <string> label;
callback clicked();
height: 32px;
background: ta.has-hover ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: 14px;
padding-right: 22px;
Text {
text: root.label;
color: Theme.text-secondary;
font-size: 13px;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
// A nav tab plus its dropdown menu. Clicking the tab opens a
// PopupWindow positioned just below it, styled like the hamburger menu.
// Menu rows are visual placeholders for now; the optional `home-item`
// index marks the one row that keeps the app on the working Home view.
component NavTabWithMenu inherits Rectangle {
in property <image> icon;
in property <string> label;
in property <bool> active: false;
in property <bool> show-icon: true;
in property <[string]> items;
// Index of the row wired to Home (-1 = none). Clicking it switches
// to ContentView.home; all other rows are harmless no-ops.
in property <int> home-item: -1;
width: tab.width;
height: 30px;
tab := NavTab {
icon: root.icon;
label: root.label;
active: root.active;
show-icon: root.show-icon;
clicked => {
menu.show();
}
}
menu := PopupWindow {
x: 0;
y: parent.height + 6px;
width: 188px;
height: menu-box.preferred-height;
menu-box := 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;
for entry[idx] in root.items: NavMenuItem {
label: entry;
clicked => {
if (idx == root.home-item) {
NavState.view = ContentView.home;
}
}
}
}
}
}
}
// Square icon button for the header's right-side controls. `active`
// gives it a highlighted look when its associated panel is open.
component HeaderIconButton inherits Rectangle { component HeaderIconButton inherits Rectangle {
in property <image> icon; in property <image> icon;
in property <bool> active: false;
callback clicked(); callback clicked();
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: Radius.sm; border-radius: Radius.sm;
background: ta.has-hover ? Theme.surface-hover : transparent; background: root.active ? Theme.surface-elevated
: ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon { QbzIcon {
source: root.icon; source: root.icon;
@@ -113,7 +202,8 @@ component HeaderIconButton inherits Rectangle {
height: 19px; height: 19px;
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary; tint: root.active ? Theme.accent
: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
} }
ta := TouchArea { ta := TouchArea {
@@ -141,26 +231,34 @@ export component HeaderBar inherits Rectangle {
y: (root.height - self.height) / 2; y: (root.height - self.height) / 2;
height: 30px; height: 30px;
spacing: Spacing.xs; spacing: Spacing.xs;
NavTab { NavTabWithMenu {
icon: @image-url("../assets/icons/compass.svg"); icon: @image-url("../assets/icons/compass.svg");
label: "Discover"; label: @tr("Discover");
active: true; active: true;
show-icon: root.show-tab-icons; show-icon: root.show-tab-icons;
items: [@tr("Home"), @tr("Editor's Picks"), @tr("For You")];
// Row 0 = Home, the one working destination.
home-item: 0;
} }
NavTab { NavTabWithMenu {
icon: @image-url("../assets/icons/library-big.svg"); icon: @image-url("../assets/icons/library-big.svg");
label: "Library"; label: @tr("Library");
show-icon: root.show-tab-icons; show-icon: root.show-tab-icons;
items: [@tr("Tracks"), @tr("Albums"), @tr("Artists"),
@tr("Playlists"), @tr("Labels")];
} }
NavTab { NavTabWithMenu {
icon: @image-url("../assets/icons/hard-drive.svg"); icon: @image-url("../assets/icons/hard-drive.svg");
label: "Local Library"; label: @tr("Local Library");
show-icon: root.show-tab-icons; show-icon: root.show-tab-icons;
items: [@tr("Albums"), @tr("Artists"), @tr("Folders"),
@tr("Tracks")];
} }
NavTab { NavTabWithMenu {
icon: @image-url("../assets/icons/cassette-tape.svg"); icon: @image-url("../assets/icons/cassette-tape.svg");
label: "My QBZ"; label: @tr("My QBZ");
show-icon: root.show-tab-icons; show-icon: root.show-tab-icons;
items: [@tr("Collections"), @tr("Mixtapes")];
} }
} }
@@ -206,11 +304,21 @@ export component HeaderBar inherits Rectangle {
width: 112px; width: 112px;
height: 36px; height: 36px;
spacing: 2px; spacing: 2px;
// Lyrics panel toggle.
HeaderIconButton { HeaderIconButton {
icon: @image-url("../assets/icons/captions.svg"); icon: @image-url("../assets/icons/captions.svg");
active: ShellState.right-panel == 2;
clicked => {
ShellState.right-panel = ShellState.right-panel == 2 ? 0 : 2;
}
} }
// Queue panel toggle.
HeaderIconButton { HeaderIconButton {
icon: @image-url("../assets/icons/list-ordered.svg"); icon: @image-url("../assets/icons/list-ordered.svg");
active: ShellState.right-panel == 1;
clicked => {
ShellState.right-panel = ShellState.right-panel == 1 ? 0 : 1;
}
} }
HeaderIconButton { HeaderIconButton {
icon: @image-url("../assets/icons/menu.svg"); icon: @image-url("../assets/icons/menu.svg");
@@ -0,0 +1,85 @@
// Right-side Lyrics panel.
//
// Rendered as a layout sibling of the content area (see AppShell), so it
// shrinks the content rather than overlaying it. Closing sets
// ShellState.right-panel = 0. The body is a structural placeholder;
// lyric contents are wired in a later milestone.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ShellState } from "../state.slint";
component CloseButton inherits Rectangle {
callback clicked();
width: 28px;
height: 28px;
border-radius: Radius.sm;
background: ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
export component LyricsSidebar inherits Rectangle {
width: 320px;
background: Theme.surface-card;
VerticalLayout {
spacing: 0px;
// Header row — title + close button.
HorizontalLayout {
padding-left: Spacing.md;
padding-right: Spacing.sm;
padding-top: Spacing.sm;
padding-bottom: Spacing.sm;
height: 44px;
Text {
text: @tr("Lyrics");
color: Theme.text-primary;
font-size: Typography.heading;
font-weight: Typography.semibold;
vertical-alignment: center;
horizontal-stretch: 1;
}
VerticalLayout {
alignment: center;
CloseButton {
clicked => {
ShellState.right-panel = 0;
}
}
}
}
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// Body — placeholder until lyric contents are wired.
Rectangle {
vertical-stretch: 1;
Text {
text: @tr("No lyrics");
color: Theme.text-muted;
font-size: Typography.body;
}
}
}
}
+14 -1
View File
@@ -6,7 +6,7 @@
// Rust nav history records every move. // Rust nav history records every move.
import { Theme } from "../foundation/semantic-colors.slint"; import { Theme } from "../foundation/semantic-colors.slint";
import { NavState } from "../state.slint"; import { NavState, ShellState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavBtn inherits Rectangle { component NavBtn inherits Rectangle {
@@ -48,6 +48,19 @@ export component NavButtons inherits Rectangle {
btn-row := HorizontalLayout { btn-row := HorizontalLayout {
padding: 3px; padding: 3px;
spacing: 4px; spacing: 4px;
// Leading button: collapses / expands the left sidebar. Lives
// here so it appears just left of the [<] [>] pair in every view.
NavBtn {
icon: @image-url("../assets/icons/panel-left.svg");
clicked => {
ShellState.sidebar-collapsed = !ShellState.sidebar-collapsed;
}
}
// 1px separator between the collapse button and the history pair.
Rectangle {
width: 1px;
background: Theme.border-subtle;
}
NavBtn { NavBtn {
icon: @image-url("../assets/icons/chevron-left.svg"); icon: @image-url("../assets/icons/chevron-left.svg");
enabled: NavState.can-back; enabled: NavState.can-back;
@@ -0,0 +1,85 @@
// Right-side Queue panel.
//
// Rendered as a layout sibling of the content area (see AppShell), so it
// shrinks the content rather than overlaying it. Closing sets
// ShellState.right-panel = 0. The body is a structural placeholder;
// queue contents are wired in a later milestone.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ShellState } from "../state.slint";
component CloseButton inherits Rectangle {
callback clicked();
width: 28px;
height: 28px;
border-radius: Radius.sm;
background: ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
export component QueueSidebar inherits Rectangle {
width: 320px;
background: Theme.surface-card;
VerticalLayout {
spacing: 0px;
// Header row — title + close button.
HorizontalLayout {
padding-left: Spacing.md;
padding-right: Spacing.sm;
padding-top: Spacing.sm;
padding-bottom: Spacing.sm;
height: 44px;
Text {
text: @tr("Queue");
color: Theme.text-primary;
font-size: Typography.heading;
font-weight: Typography.semibold;
vertical-alignment: center;
horizontal-stretch: 1;
}
VerticalLayout {
alignment: center;
CloseButton {
clicked => {
ShellState.right-panel = 0;
}
}
}
}
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// Body — placeholder until queue contents are wired.
Rectangle {
vertical-stretch: 1;
Text {
text: @tr("Nothing queued");
color: Theme.text-muted;
font-size: Typography.body;
}
}
}
}
+42 -3
View File
@@ -3,18 +3,31 @@
// The playlist list is empty until real playlist data is wired (the // The playlist list is empty until real playlist data is wired (the
// sidebar is a later milestone). The user element now lives in the // sidebar is a later milestone). The user element now lives in the
// header, not at the bottom of the sidebar. // header, not at the bottom of the sidebar.
//
// The sidebar is collapsible: its rendered width is driven by
// ShellState (0px when collapsed, otherwise the user-set width). A thin
// drag handle on the right edge resizes it, clamped to [200px, 420px];
// dragging below ~160px collapses it entirely.
import { Theme } from "../foundation/semantic-colors.slint"; import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint"; import { Spacing } from "../foundation/spacing.slint";
import { Layout } from "../foundation/layout.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ShellState } from "../state.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
export component Sidebar inherits Rectangle { export component Sidebar inherits Rectangle {
width: Layout.sidebar-width; // Rendered width — 0 when collapsed, otherwise the user-set width.
width: ShellState.sidebar-collapsed ? 0px : ShellState.sidebar-width;
// The sidebar sits one surface above the content area, matching the // The sidebar sits one surface above the content area, matching the
// QBZ dark theme (--bg-secondary sidebar over --bg-primary content). // QBZ dark theme (--bg-secondary sidebar over --bg-primary content).
background: Theme.surface-card; background: Theme.surface-card;
// Clip so content doesn't spill while the width animates to 0.
clip: true;
animate width { duration: 160ms; easing: ease-in-out; }
// Captured sidebar width at pointer-down, so drag deltas are
// measured from a stable origin.
property <length> drag-start-width: ShellState.sidebar-width;
VerticalLayout { VerticalLayout {
padding: Spacing.md; padding: Spacing.md;
@@ -22,7 +35,7 @@ export component Sidebar inherits Rectangle {
HorizontalLayout { HorizontalLayout {
Text { Text {
text: "PLAYLISTS"; text: @tr("PLAYLISTS");
color: Theme.text-muted; color: Theme.text-muted;
font-size: 11px; font-size: 11px;
letter-spacing: 1.5px; letter-spacing: 1.5px;
@@ -50,4 +63,30 @@ export component Sidebar inherits Rectangle {
} }
} }
} }
// Drag handle — a thin strip pinned to the right edge. Resizes the
// sidebar; below ~160px it collapses instead of resizing.
drag-handle := TouchArea {
x: parent.width - self.width;
width: 6px;
height: parent.height;
mouse-cursor: ew-resize;
pointer-event(e) => {
if (e.kind == PointerEventKind.down) {
root.drag-start-width = ShellState.sidebar-width;
}
}
moved => {
if (self.pressed) {
if (root.drag-start-width + (self.mouse-x - self.pressed-x) < 160px) {
ShellState.sidebar-collapsed = true;
} else {
ShellState.sidebar-width = Math.min(Math.max(
root.drag-start-width + (self.mouse-x - self.pressed-x),
200px), 420px);
}
}
}
}
} }
+10
View File
@@ -168,6 +168,16 @@ export global ArtistState {
in property <bool> loading: false; in property <bool> loading: false;
} }
// Shell chrome state — collapsible left sidebar and right-side panels.
// Globals are imported directly by the shell components, so toggles
// need no callback plumbing.
export global ShellState {
in-out property <bool> sidebar-collapsed: false;
in-out property <length> sidebar-width: 280px;
// 0 = none, 1 = queue, 2 = lyrics
in-out property <int> right-panel: 0;
}
// Authenticated user, for the header user element. // Authenticated user, for the header user element.
export global SessionState { export global SessionState {
in property <string> user-name: ""; in property <string> user-name: "";