summaryrefslogtreecommitdiff
path: root/modules/controlcenter/components
diff options
context:
space:
mode:
Diffstat (limited to 'modules/controlcenter/components')
-rw-r--r--modules/controlcenter/components/DeviceDetails.qml71
-rw-r--r--modules/controlcenter/components/DeviceList.qml85
-rw-r--r--modules/controlcenter/components/PaneTransition.qml72
-rw-r--r--modules/controlcenter/components/SettingsHeader.qml38
-rw-r--r--modules/controlcenter/components/SliderInput.qml181
-rw-r--r--modules/controlcenter/components/SplitPaneLayout.qml112
-rw-r--r--modules/controlcenter/components/SplitPaneWithDetails.qml93
-rw-r--r--modules/controlcenter/components/WallpaperGrid.qml241
8 files changed, 893 insertions, 0 deletions
diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml
new file mode 100644
index 0000000..8cc9177
--- /dev/null
+++ b/modules/controlcenter/components/DeviceDetails.qml
@@ -0,0 +1,71 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.components.containers
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ property Session session
+ property var device: null
+
+ property Component headerComponent: null
+ property list<Component> sections: []
+
+ property Component topContent: null
+ property Component bottomContent: null
+
+ implicitWidth: layout.implicitWidth
+ implicitHeight: layout.implicitHeight
+
+ ColumnLayout {
+ id: layout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: Appearance.spacing.normal
+
+ Loader {
+ id: headerLoader
+
+ Layout.fillWidth: true
+ sourceComponent: root.headerComponent
+ visible: root.headerComponent !== null
+ }
+
+ Loader {
+ id: topContentLoader
+
+ Layout.fillWidth: true
+ sourceComponent: root.topContent
+ visible: root.topContent !== null
+ }
+
+ Repeater {
+ model: root.sections
+
+ Loader {
+ required property Component modelData
+
+ Layout.fillWidth: true
+ sourceComponent: modelData
+ }
+ }
+
+ Loader {
+ id: bottomContentLoader
+
+ Layout.fillWidth: true
+ sourceComponent: root.bottomContent
+ visible: root.bottomContent !== null
+ }
+ }
+}
+
diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml
new file mode 100644
index 0000000..75dd913
--- /dev/null
+++ b/modules/controlcenter/components/DeviceList.qml
@@ -0,0 +1,85 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import qs.components
+import qs.components.controls
+import qs.components.containers
+import qs.services
+import qs.config
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ property Session session: null
+ property var model: null
+ property Component delegate: null
+
+ property string title: ""
+ property string description: ""
+ property var activeItem: null
+ property Component headerComponent: null
+ property Component titleSuffix: null
+ property bool showHeader: true
+
+ signal itemSelected(var item)
+
+ spacing: Appearance.spacing.small
+
+ Loader {
+ id: headerLoader
+
+ Layout.fillWidth: true
+ sourceComponent: root.headerComponent
+ visible: root.headerComponent !== null && root.showHeader
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: root.headerComponent ? 0 : 0
+ spacing: Appearance.spacing.small
+ visible: root.title !== "" || root.description !== ""
+
+ StyledText {
+ visible: root.title !== ""
+ text: root.title
+ font.pointSize: Appearance.font.size.large
+ font.weight: 500
+ }
+
+ Loader {
+ sourceComponent: root.titleSuffix
+ visible: root.titleSuffix !== null
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+
+ property alias view: view
+
+ StyledText {
+ visible: root.description !== ""
+ Layout.fillWidth: true
+ text: root.description
+ color: Colours.palette.m3outline
+ }
+
+ StyledListView {
+ id: view
+
+ Layout.fillWidth: true
+ implicitHeight: contentHeight
+
+ model: root.model
+ delegate: root.delegate
+
+ spacing: Appearance.spacing.small / 2
+ interactive: false
+ clip: false
+ }
+}
+
diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml
new file mode 100644
index 0000000..d1814b5
--- /dev/null
+++ b/modules/controlcenter/components/PaneTransition.qml
@@ -0,0 +1,72 @@
+pragma ComponentBehavior: Bound
+
+import qs.config
+import QtQuick
+
+SequentialAnimation {
+ id: root
+
+ required property Item target
+ property list<PropertyAction> propertyActions
+
+ property real scaleFrom: 1.0
+ property real scaleTo: 0.8
+ property real opacityFrom: 1.0
+ property real opacityTo: 0.0
+
+ ParallelAnimation {
+ NumberAnimation {
+ target: root.target
+ property: "opacity"
+ from: root.opacityFrom
+ to: root.opacityTo
+ duration: Appearance.anim.durations.normal / 2
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.standardAccel
+ }
+
+ NumberAnimation {
+ target: root.target
+ property: "scale"
+ from: root.scaleFrom
+ to: root.scaleTo
+ duration: Appearance.anim.durations.normal / 2
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.standardAccel
+ }
+ }
+
+ ScriptAction {
+ script: {
+ for (let i = 0; i < root.propertyActions.length; i++) {
+ const action = root.propertyActions[i];
+ if (action.target && action.property !== undefined) {
+ action.target[action.property] = action.value;
+ }
+ }
+ }
+ }
+
+ ParallelAnimation {
+ NumberAnimation {
+ target: root.target
+ property: "opacity"
+ from: root.opacityTo
+ to: root.opacityFrom
+ duration: Appearance.anim.durations.normal / 2
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.standardDecel
+ }
+
+ NumberAnimation {
+ target: root.target
+ property: "scale"
+ from: root.scaleTo
+ to: root.scaleFrom
+ duration: Appearance.anim.durations.normal / 2
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.standardDecel
+ }
+ }
+}
+
diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml
new file mode 100644
index 0000000..c1ba148
--- /dev/null
+++ b/modules/controlcenter/components/SettingsHeader.qml
@@ -0,0 +1,38 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ required property string icon
+ required property string title
+
+ Layout.fillWidth: true
+ implicitHeight: column.implicitHeight
+
+ ColumnLayout {
+ id: column
+
+ anchors.centerIn: parent
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ text: root.icon
+ font.pointSize: Appearance.font.size.extraLarge * 3
+ font.bold: true
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: root.title
+ font.pointSize: Appearance.font.size.large
+ font.bold: true
+ }
+ }
+}
+
diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml
new file mode 100644
index 0000000..7348368
--- /dev/null
+++ b/modules/controlcenter/components/SliderInput.qml
@@ -0,0 +1,181 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ property string label: ""
+ property real value: 0
+ property real from: 0
+ property real to: 100
+ property real stepSize: 0
+ property var validator: null
+ property string suffix: "" // Optional suffix text (e.g., "×", "px")
+ property int decimals: 1 // Number of decimal places to show (default: 1)
+ property var formatValueFunction: null // Optional custom format function
+ property var parseValueFunction: null // Optional custom parse function
+
+ function formatValue(val: real): string {
+ if (formatValueFunction) {
+ return formatValueFunction(val);
+ }
+ // Default format function
+ // Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property)
+ if (validator && validator.bottom !== undefined && validator.decimals === undefined) {
+ return Math.round(val).toString();
+ }
+ // For DoubleValidator or no validator, use the decimals property
+ return val.toFixed(root.decimals);
+ }
+
+ function parseValue(text: string): real {
+ if (parseValueFunction) {
+ return parseValueFunction(text);
+ }
+ // Default parse function
+ if (validator && validator.bottom !== undefined) {
+ // Check if it's an integer validator
+ if (validator.top !== undefined && validator.top === Math.floor(validator.top)) {
+ return parseInt(text);
+ }
+ }
+ return parseFloat(text);
+ }
+
+ signal valueModified(real newValue)
+
+ property bool _initialized: false
+
+ spacing: Appearance.spacing.small
+
+ Component.onCompleted: {
+ // Set initialized flag after a brief delay to allow component to fully load
+ Qt.callLater(() => {
+ _initialized = true;
+ });
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ StyledText {
+ visible: root.label !== ""
+ text: root.label
+ font.pointSize: Appearance.font.size.normal
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledInputField {
+ id: inputField
+ Layout.preferredWidth: 70
+ validator: root.validator
+
+ Component.onCompleted: {
+ // Initialize text without triggering valueModified signal
+ text = root.formatValue(root.value);
+ }
+
+ onTextEdited: (text) => {
+ if (hasFocus) {
+ const val = root.parseValue(text);
+ if (!isNaN(val)) {
+ // Validate against validator bounds if available
+ let isValid = true;
+ if (root.validator) {
+ if (root.validator.bottom !== undefined && val < root.validator.bottom) {
+ isValid = false;
+ }
+ if (root.validator.top !== undefined && val > root.validator.top) {
+ isValid = false;
+ }
+ }
+
+ if (isValid) {
+ root.valueModified(val);
+ }
+ }
+ }
+ }
+
+ onEditingFinished: {
+ const val = root.parseValue(text);
+ let isValid = true;
+ if (root.validator) {
+ if (root.validator.bottom !== undefined && val < root.validator.bottom) {
+ isValid = false;
+ }
+ if (root.validator.top !== undefined && val > root.validator.top) {
+ isValid = false;
+ }
+ }
+
+ if (isNaN(val) || !isValid) {
+ text = root.formatValue(root.value);
+ }
+ }
+ }
+
+ StyledText {
+ visible: root.suffix !== ""
+ text: root.suffix
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.normal
+ }
+ }
+
+ StyledSlider {
+ id: slider
+
+ Layout.fillWidth: true
+ implicitHeight: Appearance.padding.normal * 3
+
+ from: root.from
+ to: root.to
+ stepSize: root.stepSize
+
+ // Use Binding to allow slider to move freely during dragging
+ Binding {
+ target: slider
+ property: "value"
+ value: root.value
+ when: !slider.pressed
+ }
+
+ onValueChanged: {
+ // Update input field text in real-time as slider moves during dragging
+ // Always update when slider value changes (during dragging or external updates)
+ if (!inputField.hasFocus) {
+ const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
+ inputField.text = root.formatValue(newValue);
+ }
+ }
+
+ onMoved: {
+ const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
+ root.valueModified(newValue);
+ if (!inputField.hasFocus) {
+ inputField.text = root.formatValue(newValue);
+ }
+ }
+ }
+
+ // Update input field when value changes externally (slider is already bound)
+ onValueChanged: {
+ // Only update if component is initialized to avoid issues during creation
+ if (root._initialized && !inputField.hasFocus) {
+ inputField.text = root.formatValue(root.value);
+ }
+ }
+}
+
diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml
new file mode 100644
index 0000000..8b4f0d9
--- /dev/null
+++ b/modules/controlcenter/components/SplitPaneLayout.qml
@@ -0,0 +1,112 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.effects
+import qs.config
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+RowLayout {
+ id: root
+
+ spacing: 0
+
+ property Component leftContent: null
+ property Component rightContent: null
+
+ property real leftWidthRatio: 0.4
+ property int leftMinimumWidth: 420
+ property var leftLoaderProperties: ({})
+ property var rightLoaderProperties: ({})
+
+ property alias leftLoader: leftLoader
+ property alias rightLoader: rightLoader
+
+ Item {
+ id: leftPane
+
+ Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio)
+ Layout.minimumWidth: root.leftMinimumWidth
+ Layout.fillHeight: true
+
+ ClippingRectangle {
+ id: leftClippingRect
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.normal
+ anchors.leftMargin: 0
+ anchors.rightMargin: Appearance.padding.normal / 2
+
+ radius: leftBorder.innerRadius
+ color: "transparent"
+
+ Loader {
+ id: leftLoader
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large + Appearance.padding.normal
+ anchors.leftMargin: Appearance.padding.large
+ anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
+
+ asynchronous: true
+ sourceComponent: root.leftContent
+
+ Component.onCompleted: {
+ for (const key in root.leftLoaderProperties) {
+ leftLoader[key] = root.leftLoaderProperties[key];
+ }
+ }
+ }
+ }
+
+ InnerBorder {
+ id: leftBorder
+
+ leftThickness: 0
+ rightThickness: Appearance.padding.normal / 2
+ }
+ }
+
+ Item {
+ id: rightPane
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ClippingRectangle {
+ id: rightClippingRect
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.normal
+ anchors.leftMargin: 0
+ anchors.rightMargin: Appearance.padding.normal / 2
+
+ radius: rightBorder.innerRadius
+ color: "transparent"
+
+ Loader {
+ id: rightLoader
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large * 2
+
+ asynchronous: true
+ sourceComponent: root.rightContent
+
+ Component.onCompleted: {
+ for (const key in root.rightLoaderProperties) {
+ rightLoader[key] = root.rightLoaderProperties[key];
+ }
+ }
+ }
+ }
+
+ InnerBorder {
+ id: rightBorder
+
+ leftThickness: Appearance.padding.normal / 2
+ }
+ }
+}
+
diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml
new file mode 100644
index 0000000..e873923
--- /dev/null
+++ b/modules/controlcenter/components/SplitPaneWithDetails.qml
@@ -0,0 +1,93 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import qs.components
+import qs.components.effects
+import qs.components.containers
+import qs.config
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ required property Component leftContent
+ required property Component rightDetailsComponent
+ required property Component rightSettingsComponent
+
+ property var activeItem: null
+ property var paneIdGenerator: function(item) { return item ? String(item) : ""; }
+
+ property Component overlayComponent: null
+
+ SplitPaneLayout {
+ id: splitLayout
+
+ anchors.fill: parent
+
+ leftContent: root.leftContent
+
+ rightContent: Component {
+ Item {
+ id: rightPaneItem
+
+ property var pane: root.activeItem
+ property string paneId: root.paneIdGenerator(pane)
+ property Component targetComponent: root.rightSettingsComponent
+ property Component nextComponent: root.rightSettingsComponent
+
+ function getComponentForPane() {
+ return pane ? root.rightDetailsComponent : root.rightSettingsComponent;
+ }
+
+ Component.onCompleted: {
+ targetComponent = getComponentForPane();
+ nextComponent = targetComponent;
+ }
+
+ Loader {
+ id: rightLoader
+
+ anchors.fill: parent
+
+ opacity: 1
+ scale: 1
+ transformOrigin: Item.Center
+
+ clip: false
+ asynchronous: true
+ sourceComponent: rightPaneItem.targetComponent
+ }
+
+ Behavior on paneId {
+ PaneTransition {
+ target: rightLoader
+ propertyActions: [
+ PropertyAction {
+ target: rightPaneItem
+ property: "targetComponent"
+ value: rightPaneItem.nextComponent
+ }
+ ]
+ }
+ }
+
+ onPaneChanged: {
+ nextComponent = getComponentForPane();
+ paneId = root.paneIdGenerator(pane);
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: overlayLoader
+
+ anchors.fill: parent
+ z: 1000
+ sourceComponent: root.overlayComponent
+ active: root.overlayComponent !== null
+ }
+}
+
diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml
new file mode 100644
index 0000000..5eab5b8
--- /dev/null
+++ b/modules/controlcenter/components/WallpaperGrid.qml
@@ -0,0 +1,241 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.components.images
+import qs.services
+import qs.config
+import Caelestia.Models
+import QtQuick
+
+GridView {
+ id: root
+
+ required property Session session
+
+ readonly property int minCellWidth: 200 + Appearance.spacing.normal
+ readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth))
+
+ cellWidth: width / columnsCount
+ cellHeight: 140 + Appearance.spacing.normal
+
+ model: Wallpapers.list
+
+ clip: true
+
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: root
+ }
+
+ delegate: Item {
+ required property var modelData
+ required property int index
+
+ width: root.cellWidth
+ height: root.cellHeight
+
+ readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent
+ readonly property real itemMargin: Appearance.spacing.normal / 2
+ readonly property real itemRadius: Appearance.rounding.normal
+
+ StateLayer {
+ anchors.fill: parent
+ anchors.leftMargin: itemMargin
+ anchors.rightMargin: itemMargin
+ anchors.topMargin: itemMargin
+ anchors.bottomMargin: itemMargin
+ radius: itemRadius
+
+ function onClicked(): void {
+ Wallpapers.setWallpaper(modelData.path);
+ }
+ }
+
+ StyledClippingRect {
+ id: image
+
+ anchors.fill: parent
+ anchors.leftMargin: itemMargin
+ anchors.rightMargin: itemMargin
+ anchors.topMargin: itemMargin
+ anchors.bottomMargin: itemMargin
+ color: Colours.tPalette.m3surfaceContainer
+ radius: itemRadius
+ antialiasing: true
+ layer.enabled: true
+ layer.smooth: true
+
+ CachingImage {
+ id: cachingImage
+
+ path: modelData.path
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectCrop
+ cache: true
+ visible: opacity > 0
+ antialiasing: true
+ smooth: true
+ sourceSize: Qt.size(width, height)
+
+ opacity: status === Image.Ready ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+
+ // Fallback if CachingImage fails to load
+ Image {
+ id: fallbackImage
+
+ anchors.fill: parent
+ source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
+ asynchronous: true
+ fillMode: Image.PreserveAspectCrop
+ cache: true
+ visible: opacity > 0
+ antialiasing: true
+ smooth: true
+ sourceSize: Qt.size(width, height)
+
+ opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+
+ Timer {
+ id: fallbackTimer
+
+ property bool triggered: false
+ interval: 800
+ running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
+ onTriggered: triggered = true
+ }
+
+ // Gradient overlay for filename
+ Rectangle {
+ id: filenameOverlay
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+
+ implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
+ radius: 0
+
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: Qt.rgba(Colours.palette.m3surface.r,
+ Colours.palette.m3surface.g,
+ Colours.palette.m3surface.b, 0)
+ }
+ GradientStop {
+ position: 0.3
+ color: Qt.rgba(Colours.palette.m3surface.r,
+ Colours.palette.m3surface.g,
+ Colours.palette.m3surface.b, 0.7)
+ }
+ GradientStop {
+ position: 0.6
+ color: Qt.rgba(Colours.palette.m3surface.r,
+ Colours.palette.m3surface.g,
+ Colours.palette.m3surface.b, 0.9)
+ }
+ GradientStop {
+ position: 1.0
+ color: Qt.rgba(Colours.palette.m3surface.r,
+ Colours.palette.m3surface.g,
+ Colours.palette.m3surface.b, 0.95)
+ }
+ }
+
+ opacity: 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Component.onCompleted: {
+ opacity = 1;
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.leftMargin: itemMargin
+ anchors.rightMargin: itemMargin
+ anchors.topMargin: itemMargin
+ anchors.bottomMargin: itemMargin
+ color: "transparent"
+ radius: itemRadius + border.width
+ border.width: isCurrent ? 2 : 0
+ border.color: Colours.palette.m3primary
+ antialiasing: true
+ smooth: true
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ MaterialIcon {
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Appearance.padding.small
+
+ visible: isCurrent
+ text: "check_circle"
+ color: Colours.palette.m3primary
+ font.pointSize: Appearance.font.size.large
+ }
+ }
+
+ StyledText {
+ id: filenameText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
+ anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
+ anchors.bottomMargin: Appearance.padding.normal
+
+ text: modelData.name
+ font.pointSize: Appearance.font.size.smaller
+ font.weight: 500
+ color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
+ elide: Text.ElideMiddle
+ maximumLineCount: 1
+ horizontalAlignment: Text.AlignHCenter
+
+ opacity: 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Component.onCompleted: {
+ opacity = 1;
+ }
+ }
+ }
+ }