import QtQuick import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls as Controls import org.kde.kirigami as Kirigami Kirigami.ApplicationWindow { id: root title: "Configuration initiale du poste" visibility: Window.FullScreen color: "#eef2f7" property int currentStep: 0 property var steps: [ { "title": "Bienvenue", "subtitle": "Présentation" }, { "title": "Mot de passe", "subtitle": "Accès de secours" }, { "title": "YubiKey", "subtitle": "PIN personnel" }, { "title": "Terminé", "subtitle": "Poste prêt" } ] property bool passwordStepValid: currentPassword.text.trim().length > 0 && newPassword.text.length > 0 && confirmPassword.text.length > 0 && newPassword.text === confirmPassword.text property bool pinStepValid: currentPin.text.length >= 4 && newPin.text.length >= 4 && confirmPin.text.length >= 4 && newPin.text === confirmPin.text // En production, l'état réel vient du backend et des marqueurs locaux. // La simple saisie des champs ne signifie jamais que l'opération a réussi. property bool workflowComplete: provisioning.complete property bool closeAttempted: false property string operationError: "" function goNext() { if (currentStep === 0) currentStep = 1 } function goBack() { // Une étape déjà appliquée au système n'est jamais rejouée. if (currentStep === 1 && !provisioning.passwordDone) currentStep = 0 } Component.onCompleted: { if (provisioning.complete) currentStep = 3 else if (provisioning.passwordDone) currentStep = 2 else currentStep = 0 } Connections { target: provisioning function onPasswordChangeFinished(success, message) { root.operationError = success ? "" : message if (success) root.currentStep = 2 } function onPinChangeFinished(success, message) { root.operationError = success ? "" : message if (success) root.currentStep = 3 } } onClosing: function(close) { if (!workflowComplete) { close.accepted = false closeAttempted = true closeWarningTimer.restart() } } onVisibilityChanged: { if (!workflowComplete && visibility !== Window.FullScreen) visibility = Window.FullScreen } Timer { id: closeWarningTimer interval: 3500 repeat: false onTriggered: root.closeAttempted = false } // Indicateur volontairement informatif : il ne bloque jamais // la validation du mot de passe. La longueur est privilégiée, // la diversité des caractères apporte un bonus visuel. function passwordStrengthLevel(password) { if (password.length === 0) return 0 var classes = 0 if (/[a-z]/.test(password)) classes += 1 if (/[A-Z]/.test(password)) classes += 1 if (/[0-9]/.test(password)) classes += 1 if (/[^A-Za-z0-9]/.test(password)) classes += 1 var level = 1 if (password.length >= 16) level = classes >= 2 ? 4 : 3 else if (password.length >= 12) level = classes >= 2 ? 3 : 2 else if (password.length >= 8) level = classes >= 3 ? 2 : 1 // Quelques motifs manifestement faibles : on réduit // l'indication sans empêcher l'utilisateur de continuer. var lower = password.toLowerCase() if (lower === "password" || lower === "motdepasse" || lower === "azerty" || lower === "qwerty" || lower === "12345678" || /^(.)\1+$/.test(password)) { level = 1 } return level } function passwordStrengthLabel(level) { if (level === 1) return "Faible" if (level === 2) return "Moyen" if (level === 3) return "Bon" if (level === 4) return "Très bon" return "Non évalué" } function passwordStrengthColor(level) { if (level === 1) return "#dc2626" if (level === 2) return "#d97706" if (level === 3) return "#2563eb" if (level === 4) return "#16a34a" return "#cbd5e1" } function passwordStrengthHint(level) { if (level === 1) return "Privilégiez surtout la longueur : une phrase de passe de 12 caractères ou plus est un bon point de départ." if (level === 2) return "Correct pour un usage courant, mais quelques caractères supplémentaires amélioreraient sensiblement la robustesse." if (level === 3) return "Bonne robustesse apparente : la longueur et la diversité sont satisfaisantes." if (level === 4) return "Très bonne robustesse apparente : mot de passe long et suffisamment diversifié." return "Saisissez votre nouveau mot de passe pour obtenir une indication de robustesse." } component PrimaryButton: Controls.Button { id: control implicitHeight: 48 implicitWidth: 170 leftPadding: 24 rightPadding: 24 contentItem: Controls.Label { text: control.text font.pixelSize: 15 font.weight: Font.DemiBold color: "white" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { radius: 12 color: control.down ? "#1e40af" : control.hovered ? "#2563eb" : "#1d4ed8" Behavior on color { ColorAnimation { duration: 120 } } } } component SecondaryButton: Controls.Button { id: control implicitHeight: 48 implicitWidth: 140 leftPadding: 22 rightPadding: 22 contentItem: Controls.Label { text: control.text font.pixelSize: 15 font.weight: Font.Medium color: "#334155" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { radius: 12 color: control.down ? "#dbe3ee" : control.hovered ? "#edf2f7" : "transparent" border.width: 1 border.color: "#cbd5e1" } } component SecretField: Controls.TextField { id: field property bool secretVisible: false Layout.fillWidth: true implicitHeight: 50 echoMode: secretVisible ? TextInput.Normal : TextInput.Password passwordCharacter: "●" font.pixelSize: 15 leftPadding: 16 rightPadding: 56 selectByMouse: true background: Rectangle { radius: 10 color: field.activeFocus ? "#ffffff" : "#f8fafc" border.width: field.activeFocus ? 2 : 1 border.color: field.activeFocus ? "#2563eb" : "#cbd5e1" } Controls.ToolButton { id: visibilityButton anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter width: 38 height: 38 hoverEnabled: true focusPolicy: Qt.NoFocus Accessible.name: field.secretVisible ? "Masquer la valeur" : "Afficher la valeur" onClicked: field.secretVisible = !field.secretVisible contentItem: Kirigami.Icon { source: field.secretVisible ? "view-hidden" : "view-visible" implicitWidth: 20 implicitHeight: 20 color: visibilityButton.hovered ? "#2563eb" : "#64748b" } background: Rectangle { radius: 8 color: visibilityButton.pressed ? "#dbeafe" : visibilityButton.hovered ? "#eff6ff" : "transparent" Behavior on color { ColorAnimation { duration: 100 } } } } } component FieldLabel: RowLayout { id: fieldLabel property string iconName: "dialog-password" property string labelText: "" spacing: 8 Kirigami.Icon { source: fieldLabel.iconName implicitWidth: 17 implicitHeight: 17 color: "#64748b" } Controls.Label { text: fieldLabel.labelText color: "#334155" font.pixelSize: 13 font.weight: Font.DemiBold } } component StepDot: Rectangle { required property int stepIndex width: 34 height: 34 radius: 17 color: stepIndex < root.currentStep ? "#22c55e" : stepIndex === root.currentStep ? "#ffffff" : "#17345f" border.width: stepIndex === root.currentStep ? 2 : 0 border.color: "#93c5fd" Controls.Label { anchors.centerIn: parent text: parent.stepIndex < root.currentStep ? "✓" : (parent.stepIndex + 1) color: parent.stepIndex === root.currentStep ? "#0f2b50" : "white" font.pixelSize: 14 font.weight: Font.Bold } } RowLayout { anchors.fill: parent spacing: 0 // ----------------------------------------------------- // Colonne gauche : identité + progression // ----------------------------------------------------- Rectangle { Layout.preferredWidth: Math.max(330, root.width * 0.29) Layout.fillHeight: true gradient: Gradient { GradientStop { position: 0.0; color: "#081a33" } GradientStop { position: 0.55; color: "#0b2a50" } GradientStop { position: 1.0; color: "#123d70" } } ColumnLayout { anchors.fill: parent anchors.margins: 44 spacing: 0 RowLayout { spacing: 14 Rectangle { width: 54 height: 54 radius: 16 color: "#1d4ed8" Controls.Label { anchors.centerIn: parent text: "N" color: "white" font.pixelSize: 25 font.weight: Font.Bold } } ColumnLayout { spacing: 2 Controls.Label { text: "NixOS Workstations" color: "white" font.pixelSize: 19 font.weight: Font.DemiBold } Controls.Label { text: "Configuration du poste" color: "#a8c1e1" font.pixelSize: 13 } } } Item { Layout.preferredHeight: 70 } Controls.Label { text: "VOTRE CONFIGURATION" color: "#7fa4d1" font.pixelSize: 11 font.weight: Font.Bold font.letterSpacing: 1.5 } Item { Layout.preferredHeight: 26 } Repeater { model: root.steps delegate: Item { required property int index required property var modelData Layout.fillWidth: true Layout.preferredHeight: 78 RowLayout { anchors.fill: parent spacing: 16 // Colonne de largeur fixe : tous les marqueurs et tous // les titres commencent exactement au même endroit. Item { Layout.preferredWidth: 42 Layout.fillHeight: true StepDot { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter stepIndex: index } Rectangle { visible: index < root.steps.length - 1 anchors.top: parent.top anchors.topMargin: 34 anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter width: 2 color: index < root.currentStep ? "#22c55e" : "#28517e" } } ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: 3 Controls.Label { Layout.fillWidth: true text: modelData.title color: index === root.currentStep ? "white" : "#c1d3e8" font.pixelSize: 15 font.weight: index === root.currentStep ? Font.DemiBold : Font.Medium horizontalAlignment: Text.AlignLeft } Controls.Label { Layout.fillWidth: true text: modelData.subtitle color: "#7699c2" font.pixelSize: 12 horizontalAlignment: Text.AlignLeft } } } } } Item { Layout.fillHeight: true } Rectangle { Layout.fillWidth: true implicitHeight: 86 radius: 14 color: "#102f55" border.width: 1 border.color: "#214b78" RowLayout { anchors.fill: parent anchors.margins: 16 spacing: 12 Controls.Label { text: "🛡" font.pixelSize: 23 } ColumnLayout { Layout.fillWidth: true spacing: 3 Controls.Label { text: "Configuration locale" color: "white" font.pixelSize: 13 font.weight: Font.DemiBold } Controls.Label { Layout.fillWidth: true text: "Les secrets définitifs ne seront pas enregistrés dans Git." wrapMode: Text.WordWrap color: "#91add0" font.pixelSize: 11 } } } } Item { Layout.preferredHeight: 20 } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 6 Kirigami.Icon { source: "lock" implicitWidth: 14 implicitHeight: 14 color: "#6289b6" } Controls.Label { text: "Assistant obligatoire jusqu'à la fin" color: "#6289b6" font.pixelSize: 11 } } } } // ----------------------------------------------------- // Partie droite // ----------------------------------------------------- Rectangle { id: contentArea Layout.fillWidth: true Layout.fillHeight: true color: "#eef2f7" ColumnLayout { anchors.fill: parent anchors.leftMargin: Math.max(48, contentArea.width * 0.07) anchors.rightMargin: Math.max(48, contentArea.width * 0.07) anchors.topMargin: 34 anchors.bottomMargin: 34 spacing: 20 RowLayout { Layout.fillWidth: true Rectangle { implicitWidth: previewLabel.implicitWidth + 28 implicitHeight: 32 radius: 16 color: provisioning.busy ? "#eff6ff" : "#f0fdf4" border.width: 1 border.color: provisioning.busy ? "#bfdbfe" : "#bbf7d0" Controls.Label { id: previewLabel anchors.centerIn: parent text: provisioning.busy ? "OPÉRATION SÉCURISÉE EN COURS" : "FINALISATION SÉCURISÉE DU POSTE" color: provisioning.busy ? "#1d4ed8" : "#166534" font.pixelSize: 11 font.weight: Font.Bold } } Item { Layout.fillWidth: true } Rectangle { implicitWidth: lockStatusRow.implicitWidth + 24 implicitHeight: 34 radius: 17 color: root.closeAttempted ? "#fef2f2" : "#f8fafc" border.width: 1 border.color: root.closeAttempted ? "#fecaca" : "#cbd5e1" RowLayout { id: lockStatusRow anchors.centerIn: parent spacing: 7 Kirigami.Icon { source: root.workflowComplete ? "lock-open" : "lock" implicitWidth: 16 implicitHeight: 16 color: root.closeAttempted ? "#dc2626" : "#64748b" } Controls.Label { text: root.closeAttempted ? "Terminez la configuration avant de quitter" : root.workflowComplete ? "Configuration complète" : "Configuration en cours" color: root.closeAttempted ? "#b91c1c" : "#475569" font.pixelSize: 11 font.weight: Font.DemiBold } } } } Rectangle { Layout.fillWidth: true Layout.fillHeight: true radius: 24 color: "white" border.width: 1 border.color: "#dce3ec" ColumnLayout { anchors.fill: parent anchors.margins: Math.max(34, Math.min(64, parent.width * 0.055)) spacing: 24 // Petit indicateur de progression supérieur RowLayout { Layout.fillWidth: true spacing: 10 Repeater { model: root.steps.length Rectangle { required property int index Layout.fillWidth: true implicitHeight: 5 radius: 3 color: index <= root.currentStep ? "#2563eb" : "#e2e8f0" Behavior on color { ColorAnimation { duration: 180 } } } } } StackLayout { id: wizard Layout.fillWidth: true Layout.fillHeight: true currentIndex: root.currentStep // --------------------------------- // 0 - Bienvenue // --------------------------------- Item { ColumnLayout { anchors.fill: parent spacing: 20 Item { Layout.fillHeight: true } Rectangle { Layout.preferredWidth: 86 Layout.preferredHeight: 86 Layout.alignment: Qt.AlignHCenter radius: 24 color: "#eff6ff" border.width: 1 border.color: "#bfdbfe" Controls.Label { anchors.centerIn: parent text: "✦" color: "#2563eb" font.pixelSize: 42 font.weight: Font.Bold } } Controls.Label { Layout.fillWidth: true text: "Bienvenue sur votre nouveau poste" color: "#0f172a" font.pixelSize: 32 font.weight: Font.Bold horizontalAlignment: Text.AlignHCenter } Controls.Label { Layout.maximumWidth: 720 Layout.alignment: Qt.AlignHCenter text: "Quelques étapes rapides vont personnaliser vos moyens d'authentification et finaliser la préparation de votre environnement de travail." wrapMode: Text.WordWrap color: "#64748b" font.pixelSize: 16 lineHeight: 1.25 horizontalAlignment: Text.AlignHCenter } Item { Layout.preferredHeight: 12 } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 14 Repeater { model: [ { "icon": "🔑", "title": "Mot de passe", "text": "Définir votre accès local de secours" }, { "icon": "🔐", "title": "YubiKey", "text": "Personnaliser votre PIN FIDO2" } ] Rectangle { required property var modelData Layout.preferredWidth: 290 Layout.preferredHeight: 128 radius: 16 color: "#f8fafc" border.width: 1 border.color: "#e2e8f0" RowLayout { anchors.fill: parent anchors.margins: 18 spacing: 14 Controls.Label { text: modelData.icon font.pixelSize: 28 } ColumnLayout { Layout.fillWidth: true spacing: 5 Controls.Label { text: modelData.title color: "#0f172a" font.pixelSize: 15 font.weight: Font.DemiBold } Controls.Label { Layout.fillWidth: true text: modelData.text wrapMode: Text.WordWrap color: "#64748b" font.pixelSize: 12 } } } } } } Item { Layout.fillHeight: true } PrimaryButton { Layout.alignment: Qt.AlignHCenter text: "Commencer" onClicked: root.goNext() } } } // --------------------------------- // 1 - Mot de passe // --------------------------------- Item { ColumnLayout { anchors.fill: parent spacing: 18 RowLayout { Layout.fillWidth: true spacing: 20 Rectangle { Layout.preferredWidth: 80 Layout.preferredHeight: 80 radius: 22 color: "#eff6ff" border.width: 1 border.color: "#bfdbfe" Kirigami.Icon { anchors.centerIn: parent source: "dialog-password" implicitWidth: 38 implicitHeight: 38 color: "#2563eb" } } ColumnLayout { Layout.fillWidth: true spacing: 5 Controls.Label { text: "Mot de passe local" color: "#0f172a" font.pixelSize: 29 font.weight: Font.Bold } Controls.Label { Layout.fillWidth: true text: "Ce mot de passe reste disponible comme solution de secours si votre YubiKey est momentanément indisponible." wrapMode: Text.WordWrap color: "#64748b" font.pixelSize: 15 lineHeight: 1.2 } } } Rectangle { Layout.fillWidth: true implicitHeight: 70 radius: 12 color: "#eff6ff" border.width: 1 border.color: "#bfdbfe" RowLayout { anchors.fill: parent anchors.margins: 16 spacing: 12 Kirigami.Icon { source: "emblem-information" implicitWidth: 22 implicitHeight: 22 color: "#2563eb" } Controls.Label { Layout.fillWidth: true text: "Le changement est effectué localement par le mécanisme NixOS prévu à cet effet. Votre nouveau mot de passe n’est ni enregistré dans Git, ni transmis au service informatique." wrapMode: Text.WordWrap color: "#1e40af" font.pixelSize: 13 } } } Item { Layout.preferredHeight: 4 } FieldLabel { iconName: "dialog-password" labelText: "Mot de passe temporaire actuel" } SecretField { id: currentPassword placeholderText: "Saisissez le mot de passe temporaire" } FieldLabel { iconName: "document-edit" labelText: "Nouveau mot de passe" } SecretField { id: newPassword placeholderText: "Choisissez votre mot de passe personnel" } // Indicateur de robustesse purement informatif. // Il ne participe pas à la condition d'activation // du bouton Continuer. Rectangle { id: strengthCard Layout.fillWidth: true implicitHeight: 112 radius: 12 color: "#f8fafc" border.width: 1 border.color: "#e2e8f0" property int strengthLevel: root.passwordStrengthLevel(newPassword.text) ColumnLayout { anchors.fill: parent anchors.margins: 14 spacing: 8 RowLayout { Layout.fillWidth: true spacing: 8 Kirigami.Icon { source: "security-high" implicitWidth: 17 implicitHeight: 17 color: root.passwordStrengthColor(strengthCard.strengthLevel) } Controls.Label { text: "Robustesse du mot de passe" color: "#334155" font.pixelSize: 12 font.weight: Font.DemiBold } Item { Layout.fillWidth: true } Controls.Label { text: root.passwordStrengthLabel(strengthCard.strengthLevel) color: root.passwordStrengthColor(strengthCard.strengthLevel) font.pixelSize: 12 font.weight: Font.Bold } } RowLayout { Layout.fillWidth: true spacing: 6 Repeater { model: 4 Rectangle { required property int index Layout.fillWidth: true implicitHeight: 7 radius: 4 color: index < parent.parent.parent.strengthLevel ? root.passwordStrengthColor(strengthCard.strengthLevel) : "#e2e8f0" Behavior on color { ColorAnimation { duration: 140 } } } } } Controls.Label { Layout.fillWidth: true text: root.passwordStrengthHint(strengthCard.strengthLevel) wrapMode: Text.WordWrap color: "#64748b" font.pixelSize: 11 } } } Controls.Label { text: "Indication uniquement : un mot de passe faible n'est pas bloqué." color: "#94a3b8" font.pixelSize: 11 } FieldLabel { iconName: "dialog-ok-apply" labelText: "Confirmation" } SecretField { id: confirmPassword placeholderText: "Confirmez votre nouveau mot de passe" } Controls.Label { visible: confirmPassword.text.length > 0 && newPassword.text !== confirmPassword.text text: "Les deux nouveaux mots de passe ne correspondent pas." color: "#dc2626" font.pixelSize: 12 } Rectangle { visible: root.operationError.length > 0 && root.currentStep === 1 Layout.fillWidth: true implicitHeight: errorPasswordRow.implicitHeight + 24 radius: 12 color: "#fef2f2" border.width: 1 border.color: "#fecaca" RowLayout { id: errorPasswordRow anchors.fill: parent anchors.margins: 12 spacing: 10 Kirigami.Icon { source: "dialog-error" implicitWidth: 20 implicitHeight: 20 color: "#dc2626" } Controls.Label { Layout.fillWidth: true text: root.operationError wrapMode: Text.WordWrap color: "#991b1b" font.pixelSize: 12 } } } Item { Layout.fillHeight: true } RowLayout { Layout.fillWidth: true SecondaryButton { text: "Retour" onClicked: root.goBack() } Item { Layout.fillWidth: true } PrimaryButton { text: provisioning.busy ? "Modification…" : "Enregistrer et continuer" enabled: root.passwordStepValid && !provisioning.busy opacity: enabled ? 1.0 : 0.45 onClicked: { root.operationError = "" var oldValue = currentPassword.text var newValue = newPassword.text var confirmationValue = confirmPassword.text provisioning.changePassword(oldValue, newValue, confirmationValue) // Les champs graphiques sont vidés immédiatement après transmission // au backend afin de réduire leur durée de présence en mémoire UI. currentPassword.text = "" newPassword.text = "" confirmPassword.text = "" } } } } } // --------------------------------- // 2 - YubiKey // --------------------------------- Item { ColumnLayout { anchors.fill: parent spacing: 18 RowLayout { Layout.fillWidth: true spacing: 20 Rectangle { Layout.preferredWidth: 80 Layout.preferredHeight: 80 radius: 22 color: "#f0fdf4" border.width: 1 border.color: "#bbf7d0" Controls.Label { anchors.centerIn: parent text: "🔐" font.pixelSize: 34 } } ColumnLayout { Layout.fillWidth: true spacing: 5 Controls.Label { text: "PIN de votre YubiKey" color: "#0f172a" font.pixelSize: 29 font.weight: Font.Bold } Controls.Label { Layout.fillWidth: true text: "Remplacez le PIN temporaire de préparation par un PIN personnel que vous serez seul à connaître." wrapMode: Text.WordWrap color: "#64748b" font.pixelSize: 15 } } } Rectangle { Layout.fillWidth: true implicitHeight: 78 radius: 12 color: "#fffbeb" border.width: 1 border.color: "#fde68a" RowLayout { anchors.fill: parent anchors.margins: 16 spacing: 12 Controls.Label { text: "⌨" color: "#b45309" font.pixelSize: 23 } Controls.Label { Layout.fillWidth: true text: "Vérifiez Verr Num avant d'utiliser le pavé numérique. En cas de doute, utilisez les chiffres situés au-dessus des lettres." wrapMode: Text.WordWrap color: "#92400e" font.pixelSize: 13 } } } FieldLabel { iconName: "dialog-password" labelText: "PIN temporaire actuel" } SecretField { id: currentPin placeholderText: "PIN temporaire" } FieldLabel { iconName: "security-high" labelText: "Nouveau PIN" } SecretField { id: newPin placeholderText: "Nouveau PIN personnel" } FieldLabel { iconName: "dialog-ok-apply" labelText: "Confirmation du nouveau PIN" } SecretField { id: confirmPin placeholderText: "Confirmez le nouveau PIN" } Controls.Label { visible: confirmPin.text.length > 0 && newPin.text !== confirmPin.text text: "Les deux nouveaux PIN ne correspondent pas." color: "#dc2626" font.pixelSize: 12 } Rectangle { visible: root.operationError.length > 0 && root.currentStep === 2 Layout.fillWidth: true implicitHeight: errorPinRow.implicitHeight + 24 radius: 12 color: "#fef2f2" border.width: 1 border.color: "#fecaca" RowLayout { id: errorPinRow anchors.fill: parent anchors.margins: 12 spacing: 10 Kirigami.Icon { source: "dialog-error" implicitWidth: 20 implicitHeight: 20 color: "#dc2626" } Controls.Label { Layout.fillWidth: true text: root.operationError wrapMode: Text.WordWrap color: "#991b1b" font.pixelSize: 12 } } } Item { Layout.fillHeight: true } RowLayout { Layout.fillWidth: true SecondaryButton { visible: !provisioning.passwordDone text: "Retour" enabled: !provisioning.busy onClicked: root.goBack() } Item { Layout.fillWidth: true } PrimaryButton { text: provisioning.busy ? "Configuration…" : "Configurer la YubiKey" enabled: root.pinStepValid && !provisioning.busy opacity: enabled ? 1.0 : 0.45 onClicked: { root.operationError = "" var oldValue = currentPin.text var newValue = newPin.text var confirmationValue = confirmPin.text provisioning.changePin(oldValue, newValue, confirmationValue) currentPin.text = "" newPin.text = "" confirmPin.text = "" } } } } } // --------------------------------- // 3 - Terminé // --------------------------------- Item { ColumnLayout { anchors.fill: parent spacing: 18 Item { Layout.fillHeight: true } Rectangle { Layout.preferredWidth: 96 Layout.preferredHeight: 96 Layout.alignment: Qt.AlignHCenter radius: 48 color: "#dcfce7" border.width: 1 border.color: "#86efac" Kirigami.Icon { anchors.centerIn: parent source: "dialog-ok-apply" implicitWidth: 52 implicitHeight: 52 color: "#16a34a" } } Controls.Label { Layout.fillWidth: true text: "Votre poste est prêt" color: "#0f172a" font.pixelSize: 32 font.weight: Font.Bold horizontalAlignment: Text.AlignHCenter } Controls.Label { Layout.maximumWidth: 680 Layout.alignment: Qt.AlignHCenter text: "Votre mot de passe local de secours et le PIN personnel de votre YubiKey ont été configurés avec succès. Vos nouveaux secrets restent personnels et ne sont pas enregistrés dans Git." wrapMode: Text.WordWrap color: "#64748b" font.pixelSize: 15 lineHeight: 1.25 horizontalAlignment: Text.AlignHCenter } Item { Layout.preferredHeight: 12 } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 12 Repeater { model: [ { "icon": "dialog-password", "title": "Mot de passe", "text": "Accès local configuré" }, { "icon": "security-high", "title": "YubiKey", "text": "PIN personnel configuré" }, { "icon": "computer-laptop", "title": "Poste", "text": "Configuration finalisée" } ] Rectangle { required property var modelData Layout.preferredWidth: 205 Layout.preferredHeight: 126 radius: 16 color: "#f8fafc" border.width: 1 border.color: "#e2e8f0" ColumnLayout { anchors.fill: parent anchors.margins: 16 spacing: 7 RowLayout { Layout.fillWidth: true Rectangle { Layout.preferredWidth: 38 Layout.preferredHeight: 38 radius: 11 color: "#eff6ff" Kirigami.Icon { anchors.centerIn: parent source: modelData.icon implicitWidth: 21 implicitHeight: 21 color: "#2563eb" } } Item { Layout.fillWidth: true } Rectangle { Layout.preferredWidth: 25 Layout.preferredHeight: 25 radius: 13 color: "#dcfce7" Kirigami.Icon { anchors.centerIn: parent source: "dialog-ok-apply" implicitWidth: 15 implicitHeight: 15 color: "#16a34a" } } } Controls.Label { text: modelData.title color: "#0f172a" font.pixelSize: 14 font.weight: Font.DemiBold } Controls.Label { Layout.fillWidth: true text: modelData.text color: "#64748b" font.pixelSize: 11 wrapMode: Text.WordWrap } } } } } Item { Layout.fillHeight: true } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 12 PrimaryButton { text: "Commencer à travailler" enabled: root.workflowComplete && !provisioning.busy opacity: enabled ? 1.0 : 0.45 onClicked: root.close() } } } } } } } } } } }