// Discover section carousel — a title, prev/next page controls, and a // clipped, sliding track of album cards. // // Pagination by slice (no scrollbar), mirroring the Svelte // DiscoverySection. Because there is no ScrollView the height is exactly // the card height — nothing injects extra vertical space. 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 icon; in property enabled: true; callback clicked(); width: 28px; height: 28px; border-radius: 14px; opacity: root.enabled ? 1.0 : 0.4; background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated; 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 { enabled: root.enabled; mouse-cursor: root.enabled ? pointer : default; clicked => { root.clicked(); } } } export component Carousel inherits VerticalLayout { in property section; callback album-clicked(string); callback media-action(string, string, string); property card-width: 220px; property card-height: 266px; property gap: 32px; property count: root.section.albums.length; property per-page: Math.max( 1, Math.floor((viewport.width + root.gap) / (root.card-width + root.gap))); property page-count: Math.max(1, Math.ceil(root.count / root.per-page)); property page: 0; // Self-correcting page index — stays valid when the window resizes. property current-page: Math.min(Math.max(0, root.page), root.page-count - 1); property step: root.per-page * (root.card-width + root.gap); property content-width: root.count * root.card-width + Math.max(0, root.count - 1) * root.gap; spacing: 12px; // --- Header: title + page controls ----------------------------------- HorizontalLayout { Text { text: root.section.title; color: Theme.text-primary; font-size: 18px; font-weight: Typography.semibold; vertical-alignment: center; horizontal-stretch: 1; } HorizontalLayout { spacing: 4px; VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-left.svg"); enabled: root.current-page > 0; clicked => { root.page = root.current-page - 1; } } } VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-right.svg"); enabled: root.current-page < root.page-count - 1; clicked => { root.page = root.current-page + 1; } } } } } // --- Track: clipped viewport, slides by page ------------------------- viewport := Rectangle { height: root.card-height; clip: true; Rectangle { x: -root.current-page * root.step; y: 0px; width: root.content-width; height: root.card-height; animate x { duration: 220ms; easing: ease-in-out; } HorizontalLayout { spacing: root.gap; for album in root.section.albums: AlbumCard { album: album; clicked(id) => { root.album-clicked(id); } media-action(kind, id, action) => { root.media-action(kind, id, action); } } } } // Cider-style edge fades — clipped cards dissolve into the // content background instead of being hard-cut. Rectangle { x: 0px; width: 56px; background: @linear-gradient(90deg, Theme.surface-main 0%, transparent 100%); opacity: root.current-page > 0 ? 1.0 : 0.0; animate opacity { duration: 150ms; } } Rectangle { x: parent.width - self.width; width: 56px; background: @linear-gradient(90deg, transparent 0%, Theme.surface-main 100%); opacity: root.current-page < root.page-count - 1 ? 1.0 : 0.0; animate opacity { duration: 150ms; } } } }