refactor(myqbz): button audit — app primitives + fix glyph centering

Phase-2 Slice 12c (user directive: use the app's circular controls, no invented
buttons; fix the glyph-stuck-left bug).
- Hero §3.6: deleted the local ActionCircle; the 4 hero controls are now the
  shared CircleAction (Play primary = filled white disc + black glyph; the loading
  spinner is now available = more 1:1). Added `enabled` to CircleAction (additive).
- All MyQBZ modal footers (Create / Edit / Mix / AddTo / Discography) now use the
  shared QbzPrimaryButton (+ a new `destructive` variant for Delete) and a new
  shared SecondaryButton (promoted from AppearanceSettings' inline copy).
  QbzPrimaryButton auto-sizes as a footer chip while staying full-width in login
  (verified: login stretches it in a VerticalLayout; footers take preferred width).
- Kept legitimately-local icon+label controls (grid "+ New" CTA, AddToMixtape
  picker chips, toolbar sort/filter/view-mode pills) but verified every glyph is
  centered.
- Fixed 3 glyph-stuck-left bugs (DiscographyBuilder checkboxes used non-snapped
  centering -> the Math.round(.../1px)*1px crisp idiom).

Backend crates untouched. cargo check -p qbz-slint green.

cc
This commit is contained in:
vicrodh committed 2026-06-09 08:29:11 -06:00
1 parent 6cbd51427d
commit 16e6232220
9 files changed
+184 -282

No files matched your search

@@ -21,6 +21,8 @@ import { Radius } from "../foundation/radius.slint";
import { MyQbzAddState, MyQbzAddActions, MyQbzAddRow } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
import { LineEdit } from "std-widgets.slint";
// A single labelled radio option in the create-panel Kind toggle.
@@ -156,50 +158,45 @@ component AddPickRow inherits Rectangle {
}
}
// A small footer button (Cancel / Back / create-buttons).
component FooterButton inherits Rectangle {
// The icon+label "+ Mixtapes / + Collections" picker-footer chips. Kept as a
// local secondary pill (rule #3 of the button audit: legitimately icon+label,
// so it does NOT collapse to the icon-only CircleAction). The leading icon sits
// in a centered [icon · gap · label] group — never jammed at the left edge.
component FooterIconButton inherits Rectangle {
in property <string> label;
in property <bool> primary: false;
in property <bool> enabled-look: true;
in property <image> icon;
in property <bool> has-icon: false;
callback clicked();
height: 38px;
border-radius: Radius.sm;
opacity: root.enabled-look ? 1.0 : 0.5;
background: root.primary
? (ta.has-hover ? #ffffffd6 : Theme.accent)
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
border-width: root.primary ? 0px : 1px;
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
HorizontalLayout {
padding-left: root.primary ? 20px : 16px;
padding-right: root.primary ? 20px : 16px;
padding-left: 16px;
padding-right: 16px;
spacing: 6px;
alignment: center;
if root.has-icon: VerticalLayout {
VerticalLayout {
alignment: center;
QbzIcon {
source: root.icon;
width: 14px;
height: 14px;
tint: root.primary ? #ffffff : Theme.text-primary;
tint: Theme.text-primary;
}
}
Text {
text: root.label;
color: root.primary ? #ffffff : Theme.text-primary;
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
vertical-alignment: center;
}
}
ta := TouchArea {
mouse-cursor: root.enabled-look ? pointer : default;
mouse-cursor: pointer;
clicked => {
if (root.enabled-look) {
root.clicked();
}
root.clicked();
}
}
}
@@ -376,10 +373,9 @@ export component AddToMixtapeModal inherits Rectangle {
HorizontalLayout {
padding: 12px;
spacing: 8px;
FooterButton {
FooterIconButton {
horizontal-stretch: 1;
label: @tr("Mixtapes");
has-icon: true;
icon: @image-url("../assets/icons/plus.svg");
clicked => {
MyQbzAddActions.show-create("mixtape");
@@ -387,10 +383,9 @@ export component AddToMixtapeModal inherits Rectangle {
}
// Hidden for track/playlist payloads (collections hold
// whole albums only).
if !MyQbzAddState.restrict-to-mixtape: FooterButton {
if !MyQbzAddState.restrict-to-mixtape: FooterIconButton {
horizontal-stretch: 1;
label: @tr("Collections");
has-icon: true;
icon: @image-url("../assets/icons/plus.svg");
clicked => {
MyQbzAddActions.show-create("collection");
@@ -460,25 +455,25 @@ export component AddToMixtapeModal inherits Rectangle {
}
// Spacer pushes the footer down.
Rectangle { vertical-stretch: 1; }
// Footer: Back + Create & Add.
// Footer: Back (secondary) + Create & Add (primary) —
// the app's shared labeled primitives.
HorizontalLayout {
alignment: end;
spacing: 8px;
FooterButton {
SecondaryButton {
label: @tr("Back");
enabled-look: !MyQbzAddState.create-busy;
height: 38px;
enabled: !MyQbzAddState.create-busy;
clicked => {
MyQbzAddActions.create-back();
}
}
FooterButton {
QbzPrimaryButton {
label: @tr("Create & Add");
primary: true;
enabled-look: root.can-create;
height: 38px;
enabled: root.can-create;
clicked => {
if (root.can-create) {
MyQbzAddActions.create-and-add();
}
MyQbzAddActions.create-and-add();
}
}
}
@@ -14,6 +14,8 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { MyQbzCreateState, MyQbzCreateActions } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
import { LineEdit } from "std-widgets.slint";
// A single labelled radio option in the Kind toggle.
@@ -163,55 +165,26 @@ export component CreateMyQbzModal inherits Rectangle {
}
}
// Cancel + Create.
// Cancel + Create — the app's shared labeled primitives
// (auto-width, 38px footer chips).
HorizontalLayout {
alignment: end;
spacing: 8px;
// Cancel (secondary).
Rectangle {
width: cancel-label.preferred-width + 32px;
SecondaryButton {
label: @tr("Cancel");
height: 38px;
border-radius: Radius.sm;
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
cancel-label := Text {
text: @tr("Cancel");
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
cancel-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
MyQbzCreateActions.close();
}
clicked => {
MyQbzCreateActions.close();
}
}
// Create (primary) — label reuses the empty-state CTA.
Rectangle {
width: create-label.preferred-width + 40px;
QbzPrimaryButton {
label: root.is-mixtape ? @tr("+ New Mixtape") : @tr("+ New Collection");
height: 38px;
border-radius: Radius.sm;
opacity: root.can-create ? 1.0 : 0.5;
background: create-ta.has-hover ? #ffffffd6 : Theme.accent;
create-label := Text {
text: root.is-mixtape ? @tr("+ New Mixtape") : @tr("+ New Collection");
color: #ffffff;
font-size: Typography.body;
font-weight: Typography.semibold;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
create-ta := TouchArea {
mouse-cursor: root.can-create ? pointer : default;
clicked => {
if (root.can-create) {
MyQbzCreateActions.submit();
}
}
enabled: root.can-create;
clicked => {
MyQbzCreateActions.submit();
}
}
}
@@ -14,6 +14,8 @@ import { Typography } from "../foundation/typography.slint";
import { DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
// ── Concrete dark-theme values (spec §0) ──────────────────────────────────
// Resolved here as global consts so the whole view shares one source.
@@ -314,8 +316,8 @@ component CandidateRow inherits Rectangle {
source: @image-url("../assets/icons/check.svg");
width: 11px;
height: 11px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: D.btn-primary-text;
}
TouchArea {
@@ -718,8 +720,8 @@ export component DiscographyBuilderView inherits Rectangle {
source: @image-url("../assets/icons/check.svg");
width: 11px;
height: 11px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: D.btn-primary-text;
}
// Indeterminate dash.
@@ -728,8 +730,8 @@ export component DiscographyBuilderView inherits Rectangle {
width: 9px;
height: 2px;
background: D.accent;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
TouchArea {
mouse-cursor: pointer;
@@ -819,64 +821,30 @@ export component DiscographyBuilderView inherits Rectangle {
font-size: 13px;
}
}
// .footer-actions.
// .footer-actions — the app's shared labeled primitives.
HorizontalLayout {
spacing: 8px;
alignment: end;
// Cancel (secondary-btn).
Rectangle {
width: cancel-row.preferred-width + 32px;
property <bool> create-disabled:
DiscoBuilderState.creating
|| DiscoBuilderState.selected-count == 0
|| DiscoBuilderState.collection-name == "";
// Cancel (secondary).
SecondaryButton {
label: @tr("Cancel");
height: 36px;
background: cancel-ta.has-hover ? D.bg-hover : D.bg-secondary;
border-radius: 8px;
border-width: 1px;
border-color: D.bg-tertiary;
cancel-row := HorizontalLayout {
alignment: center;
Text {
text: @tr("Cancel");
color: D.text-primary;
font-size: 13px;
font-weight: 600;
vertical-alignment: center;
}
}
cancel-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.back();
}
clicked => {
DiscoBuilderActions.back();
}
}
// Create (primary-btn).
create-btn := Rectangle {
property <bool> disabled:
DiscoBuilderState.creating
|| DiscoBuilderState.selected-count == 0
|| DiscoBuilderState.collection-name == "";
width: create-row.preferred-width + 40px;
// Create (primary).
QbzPrimaryButton {
label: DiscoBuilderState.creating
? @tr("Loading...") : @tr("Create Collection");
height: 36px;
background: D.accent;
border-radius: 8px;
opacity: create-btn.disabled ? 0.45 : 1.0;
create-row := HorizontalLayout {
alignment: center;
Text {
text: DiscoBuilderState.creating
? @tr("Loading...") : @tr("Create Collection");
color: D.btn-primary-text;
font-size: 13px;
font-weight: 600;
vertical-alignment: center;
}
}
TouchArea {
mouse-cursor: create-btn.disabled ? default : pointer;
clicked => {
if (!create-btn.disabled) {
DiscoBuilderActions.create();
}
}
enabled: !parent.create-disabled;
clicked => {
DiscoBuilderActions.create();
}
}
}
@@ -25,6 +25,7 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, TrackItem, NavState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { CircleAction } from "../primitives/CircleAction.slint";
import { TrackRow } from "../primitives/TrackRow.slint";
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
@@ -32,46 +33,6 @@ import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
import { CollectionMosaic } from "CollectionMosaic.slint";
// --- Hero action button (.action-btn-circle family) ---------------------
// Ghost circle (translucent fill + faint ring) homologated with the album /
// playlist transport rows; `.primary` keeps the same ghost look (the Tauri
// CTA fills accent but our transport row family is ghost everywhere). `enabled`
// dims + disables (item-count == 0).
component ActionCircle inherits Rectangle {
in property <image> icon;
in property <bool> primary: false;
in property <length> diameter: 44px;
in property <bool> enabled: true;
in property <length> glyph: 19px;
callback clicked();
width: root.diameter;
height: root.diameter;
border-radius: root.diameter / 2;
opacity: root.enabled ? 1.0 : 0.4;
background: root.primary
? (ac-ta.has-hover ? #ffffffd6 : #ffffff)
: (ac-ta.has-hover ? Theme.surface-hover : transparent);
border-width: root.primary ? 0px : 1px;
border-color: ac-ta.has-hover ? Theme.text-disabled : Theme.border-subtle;
QbzIcon {
source: root.icon;
width: root.glyph;
height: root.glyph;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: root.primary ? #000000 : (ac-ta.has-hover ? Theme.text-primary : Theme.text-secondary);
}
ac-ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
root.clicked();
}
}
}
}
// --- One sort / type-filter dropdown row --------------------------------
component MenuOption inherits Rectangle {
in property <string> label;
@@ -874,22 +835,21 @@ export component MixtapeDetailView inherits Rectangle {
HorizontalLayout {
spacing: 8px;
alignment: start;
// (1) Play — primary.
ActionCircle {
// (1) Play — primary (filled white disc + black glyph;
// the shared CircleAction.primary, spec §3.6).
CircleAction {
icon: @image-url("../assets/icons/play-fill.svg");
primary: true;
diameter: 44px;
glyph: 20px;
enabled: MyQbzDetailState.item-count > 0;
clicked => { MyQbzDetailActions.play-all(); }
}
// (2) Shuffle.
VerticalLayout {
alignment: center;
ActionCircle {
CircleAction {
icon: @image-url("../assets/icons/shuffle.svg");
diameter: 40px;
glyph: 18px;
enabled: MyQbzDetailState.item-count > 0;
clicked => { MyQbzDetailActions.shuffle(); }
}
@@ -897,10 +857,9 @@ export component MixtapeDetailView inherits Rectangle {
// (3) DJ-mix (DjIcon) — STUB click (modal is a later slice).
VerticalLayout {
alignment: center;
ActionCircle {
CircleAction {
icon: @image-url("../assets/icons/turntable.svg");
diameter: 40px;
glyph: 18px;
enabled: MyQbzDetailState.item-count > 0;
clicked => { MyQbzDetailActions.dj-mix(); }
}
@@ -911,10 +870,9 @@ export component MixtapeDetailView inherits Rectangle {
overflow-anchor := Rectangle {
width: 40px;
height: 40px;
ActionCircle {
CircleAction {
icon: @image-url("../assets/icons/ellipsis.svg");
diameter: 40px;
glyph: 18px;
clicked => { overflow-menu.show(); }
}
overflow-menu := PopupWindow {
+20 -50
View File
@@ -16,6 +16,8 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { MyQbzEditState, MyQbzEditActions } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
import { LineEdit, TextEdit } from "std-widgets.slint";
export component MyQbzEditModal inherits Rectangle {
@@ -96,67 +98,35 @@ export component MyQbzEditModal inherits Rectangle {
wrap: word-wrap;
}
// --- Footer (m-footer) -----------------------------------
// --- Footer (m-footer) — shared labeled primitives -------
HorizontalLayout {
alignment: end;
spacing: 8px;
// Cancel (secondary).
Rectangle {
width: cancel-label.preferred-width + 32px;
SecondaryButton {
label: @tr("Cancel");
height: 38px;
border-radius: Radius.sm;
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
cancel-label := Text {
text: @tr("Cancel");
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
cancel-ta := TouchArea {
mouse-cursor: MyQbzEditState.busy ? default : pointer;
clicked => {
if (!MyQbzEditState.busy) {
MyQbzEditActions.close();
}
}
enabled: !MyQbzEditState.busy;
clicked => {
MyQbzEditActions.close();
}
}
// Submit — primary (rename/description) or destructive
// (delete).
Rectangle {
width: submit-label.preferred-width + 40px;
// (delete) variant of the shared QbzPrimaryButton.
QbzPrimaryButton {
label: root.is-delete ? @tr("Delete") : @tr("Save");
height: 38px;
border-radius: Radius.sm;
opacity: root.can-submit ? 1.0 : 0.5;
background: root.is-delete
? (submit-ta.has-hover ? #e57373d9 : #e57373)
: (submit-ta.has-hover ? #ffffffd6 : Theme.accent);
submit-label := Text {
text: root.is-delete ? @tr("Delete") : @tr("Save");
color: #ffffff;
font-size: Typography.body;
font-weight: Typography.semibold;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
submit-ta := TouchArea {
mouse-cursor: root.can-submit ? pointer : default;
clicked => {
if (root.can-submit) {
if (root.is-rename) {
MyQbzEditActions.submit-rename();
} else if (root.is-description) {
MyQbzEditActions.submit-description();
} else {
MyQbzEditActions.confirm-delete();
}
}
destructive: root.is-delete;
enabled: root.can-submit;
clicked => {
if (root.is-rename) {
MyQbzEditActions.submit-rename();
} else if (root.is-description) {
MyQbzEditActions.submit-description();
} else {
MyQbzEditActions.confirm-delete();
}
}
}
+13 -44
View File
@@ -23,6 +23,8 @@ import { Radius } from "../foundation/radius.slint";
import { MyQbzMixState, MyQbzMixActions } from "../state.slint";
import { QbzSlider } from "../primitives/QbzSlider.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
export component MyQbzMixModal inherits Rectangle {
visible: MyQbzMixState.open;
@@ -150,61 +152,28 @@ export component MyQbzMixModal inherits Rectangle {
}
}
// --- Footer (.tm-actions, right-aligned) -----------------
// --- Footer (.tm-actions, right-aligned) — shared primitives
HorizontalLayout {
alignment: end;
spacing: 12px;
// Cancel (secondary).
Rectangle {
width: cancel-label.preferred-width + 32px;
SecondaryButton {
label: @tr("Cancel");
height: 38px;
border-radius: Radius.sm;
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
cancel-label := Text {
text: @tr("Cancel");
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
cancel-ta := TouchArea {
mouse-cursor: MyQbzMixState.busy ? default : pointer;
clicked => {
if (!MyQbzMixState.busy) {
MyQbzMixActions.close();
}
}
enabled: !MyQbzMixState.busy;
clicked => {
MyQbzMixActions.close();
}
}
// Shuffle / Add to queue (primary) — mixModal.confirm.
Rectangle {
width: confirm-label.preferred-width + 40px;
QbzPrimaryButton {
label: @tr("Add to queue");
height: 38px;
border-radius: Radius.sm;
opacity: root.can-shuffle ? 1.0 : 0.5;
background: confirm-ta.has-hover && root.can-shuffle
? #ffffffd6
: Theme.accent;
confirm-label := Text {
text: @tr("Add to queue");
color: #ffffff;
font-size: Typography.body;
font-weight: Typography.semibold;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
confirm-ta := TouchArea {
mouse-cursor: root.can-shuffle ? pointer : default;
clicked => {
if (root.can-shuffle) {
MyQbzMixActions.shuffle();
}
}
enabled: root.can-shuffle;
clicked => {
MyQbzMixActions.shuffle();
}
}
}
@@ -17,6 +17,9 @@ export component CircleAction inherits Rectangle {
// While the album's first track is fetching, the icon swaps for a spinner.
in property <bool> loading: false;
in property <length> diameter: 40px;
// When false the button dims and the click is gated (e.g. an empty
// collection's hero CTAs). Additive — existing callers default to enabled.
in property <bool> enabled: true;
// Optional hover tooltip (empty = none), using the global TooltipOverlay.
in property <string> tooltip;
callback clicked();
@@ -24,6 +27,7 @@ export component CircleAction inherits Rectangle {
width: root.diameter;
height: root.diameter;
border-radius: root.diameter / 2;
opacity: root.enabled ? 1.0 : 0.4;
// Overlay-matched contrast: solid white for the primary play, semi-
// transparent white fill + bright 1.5px ring otherwise.
background: root.primary
@@ -52,9 +56,11 @@ export component CircleAction inherits Rectangle {
}
ta := TouchArea {
mouse-cursor: pointer;
mouse-cursor: root.enabled ? pointer : default;
clicked => {
root.clicked();
if (root.enabled) {
root.clicked();
}
}
changed has-hover => {
if (self.has-hover && root.tooltip != "") {
@@ -7,22 +7,35 @@ import { Typography } from "../foundation/typography.slint";
export component QbzPrimaryButton inherits Rectangle {
in property <string> label;
in property <bool> enabled: true;
// Destructive variant (e.g. the Delete confirm) — a red fill instead of the
// accent. Additive; existing callers default to the accent primary.
in property <bool> destructive: false;
callback clicked();
height: 48px;
border-radius: Radius.sm;
opacity: root.enabled ? 1.0 : 0.5;
background: ta.has-hover && root.enabled ? Theme.accent-hover : Theme.accent;
background: root.destructive
? (ta.has-hover && root.enabled ? #e57373d9 : #e57373)
: (ta.has-hover && root.enabled ? Theme.accent-hover : Theme.accent);
animate background { duration: 150ms; }
Text {
text: root.label;
color: Theme.accent-text;
font-size: Typography.button;
font-weight: Typography.semibold;
horizontal-alignment: center;
vertical-alignment: center;
// A padded layout drives the preferred-width (label + 20px each side) so the
// button auto-sizes as a footer chip inside an `alignment: end` row, while
// still stretching full-width when the parent layout stretches it (login).
HorizontalLayout {
alignment: center;
padding-left: 20px;
padding-right: 20px;
Text {
text: root.label;
color: Theme.accent-text;
font-size: Typography.button;
font-weight: Typography.semibold;
horizontal-alignment: center;
vertical-alignment: center;
}
}
ta := TouchArea {
@@ -0,0 +1,50 @@
// Labeled SECONDARY button — the subdued sibling of QbzPrimaryButton, used for
// Cancel / Back actions. Promoted to a shared primitive from the inline copy in
// AppearanceSettings so the My QBZ modals + the Discography Builder footer read
// identically (the button audit, spec 12c). A bordered surface-elevated chip
// with a centered label; `enabled` dims + gates the click.
//
// Default 160px width matches AppearanceSettings; callers that need an
// auto-width chip (the right-aligned modal footers) set `width` explicitly.
import { Theme } from "../foundation/semantic-colors.slint";
import { Radius } from "../foundation/radius.slint";
import { Typography } from "../foundation/typography.slint";
export component SecondaryButton inherits Rectangle {
in property <string> label;
in property <bool> enabled: true;
callback clicked();
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated;
opacity: root.enabled ? 1.0 : 0.4;
// Padded layout → preferred-width = label + 16px each side, so the button
// auto-sizes as a footer chip; AppearanceSettings sets `width: 160px`
// explicitly to keep its original fixed footprint.
HorizontalLayout {
alignment: center;
padding-left: 16px;
padding-right: 16px;
Text {
text: root.label;
color: Theme.text-secondary;
font-size: Typography.body;
font-weight: Typography.medium;
horizontal-alignment: center;
vertical-alignment: center;
}
}
ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
root.clicked();
}
}
}
}