// SPDX-License-Identifier: GPL-2.0-only // Copyright (C) 2026 Danilo M. // // One credential slot, two stages. The username shows first; confirming it // slides the username out and the password in. Confirming the password emits // submitted(). A back chevron, or Esc, returns to the username. import QtQuick import QtQuick.Controls.Basic import QtQuick.Effects Item { id: creds // Emitted when the password stage is confirmed, and on input so the panel // can restart its idle timer. signal submitted() signal wake() property string stage: "user" readonly property string username: usernameField.text readonly property string password: passwordField.text readonly property bool passwordStage: stage === "pass" implicitHeight: 44 function confirm() { if (!creds.passwordStage) { if (usernameField.text.length > 0) creds.stage = "pass" } else { creds.submitted() } } function clearPassword() { passwordField.text = "" } function focusStage() { if (creds.passwordStage) passwordField.forceActiveFocus() else usernameField.forceActiveFocus() } onStageChanged: focusStage() Component.onCompleted: focusStage() // Back chevron, password stage only. The width animates so the slot grows // and shrinks with it. Item { id: back anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: creds.passwordStage ? 44 : 0 height: 44 clip: true visible: width > 0 Behavior on width { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } Rectangle { anchors.fill: parent radius: 6 color: config.stringValue("surface0") opacity: backMouse.containsMouse ? 1 : 0 } Image { id: backIcon anchors.centerIn: parent width: 22 height: 22 source: "../icons/chevron-left.svg" sourceSize.width: width sourceSize.height: height visible: false } MultiEffect { anchors.fill: backIcon source: backIcon colorization: 1.0 colorizationColor: backMouse.containsMouse ? config.stringValue("accent") : config.stringValue("subtext0") } MouseArea { id: backMouse anchors.fill: parent hoverEnabled: true onClicked: { creds.clearPassword() creds.stage = "user" } } } Button { id: confirmButton anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: 44 height: 44 padding: 0 background: Rectangle { radius: 6 color: config.stringValue("accent") } contentItem: Item { Image { id: confirmIcon anchors.centerIn: parent width: 22 height: 22 source: "../icons/chevron-right.svg" sourceSize.width: width sourceSize.height: height visible: false } MultiEffect { anchors.fill: confirmIcon source: confirmIcon colorization: 1.0 colorizationColor: config.stringValue("base") } } onClicked: creds.confirm() } // The sliding slot. Both fields live here, clipped, and slide across. Item { id: slot anchors.left: back.right anchors.leftMargin: 8 anchors.right: confirmButton.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter height: 44 clip: true TextField { id: usernameField width: slot.width height: slot.height text: userModel.lastUser !== undefined ? userModel.lastUser : "" placeholderText: "Username" activeFocusOnTab: !creds.passwordStage font.family: "Noto Sans" color: config.stringValue("text") palette.base: config.stringValue("surface0") palette.text: config.stringValue("text") palette.placeholderText: config.stringValue("overlay0") palette.highlight: config.stringValue("accent") palette.highlightedText: config.stringValue("base") background: Rectangle { radius: 6 color: config.stringValue("surface0") } x: creds.passwordStage ? -slot.width : 0 opacity: creds.passwordStage ? 0 : 1 Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } onAccepted: creds.confirm() onTextChanged: creds.wake() Keys.onPressed: event => { creds.wake() event.accepted = false } } TextField { id: passwordField width: slot.width height: slot.height echoMode: TextInput.Password placeholderText: "Password" activeFocusOnTab: creds.passwordStage font.family: "Noto Sans" color: config.stringValue("text") palette.base: config.stringValue("surface0") palette.text: config.stringValue("text") palette.placeholderText: config.stringValue("overlay0") palette.highlight: config.stringValue("accent") palette.highlightedText: config.stringValue("base") background: Rectangle { radius: 6 color: config.stringValue("surface0") } x: creds.passwordStage ? 0 : slot.width opacity: creds.passwordStage ? 1 : 0 Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } onAccepted: creds.confirm() Keys.onPressed: event => { creds.wake() if (event.key === Qt.Key_Escape) { creds.clearPassword() creds.stage = "user" event.accepted = true } else { event.accepted = false } } } } }