diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index d567948..fef642f 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -39,6 +39,9 @@ AmmActionCard { property string tokenResolutionMessage: "" property string confirmedPoolStatus: "" property var activePoolQuote: ({}) + property string headingText: qsTr("New position") + property string headingDetail: "" + property bool showRefreshAction: true readonly property var quotePayload: root.flowState.quote || ({}) readonly property bool contextLoading: root.flowState.contextLoading === true @@ -101,7 +104,9 @@ AmmActionCard { signal draftChanged signal refreshRequested - implicitHeight: content.implicitHeight + 32 + readonly property int contentPadding: width >= 600 ? 24 : 16 + + implicitHeight: content.implicitHeight + root.contentPadding * 2 implicitWidth: 480 Component.onCompleted: Qt.callLater(root.ensurePair) @@ -148,7 +153,7 @@ AmmActionCard { id: content anchors.fill: parent - anchors.margins: 16 + anchors.margins: root.contentPadding spacing: 16 RowLayout { @@ -160,7 +165,7 @@ AmmActionCard { spacing: 3 Text { - text: qsTr("New position") + text: root.headingText color: root.theme.colors.textPrimary font.pixelSize: 18 font.weight: Font.DemiBold @@ -168,7 +173,8 @@ AmmActionCard { } Text { - text: root.contextStatusText() + text: root.headingDetail.length > 0 + ? root.headingDetail : root.contextStatusText() color: root.theme.colors.textSecondary font.pixelSize: 12 elide: Text.ElideRight @@ -190,6 +196,7 @@ AmmActionCard { Layout.preferredWidth: 36 Layout.preferredHeight: 36 enabled: !root.contextLoading && !root.submitting + visible: root.showRefreshAction Accessible.name: qsTr("Refresh position data") ToolTip.visible: hovered ToolTip.text: Accessible.name diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml index 503a680..297b6d9 100644 --- a/apps/amm/qml/pages/LiquidityPage.qml +++ b/apps/amm/qml/pages/LiquidityPage.qml @@ -1,5 +1,10 @@ import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import QtQml + +import Logos.Controls +import Logos.Icons import Logos.Wallet import "../components/liquidity" @@ -12,8 +17,11 @@ Item { property var runtime: null readonly property NewPositionFlow flow: newPositionFlow - readonly property int pageMargin: 16 - readonly property int preferredWidth: 480 + readonly property int pageMargin: width < 640 ? 16 : 24 + readonly property int contentMaxWidth: 1200 + readonly property bool wideLayout: width >= 760 + readonly property int stepRailWidth: Math.max(210, Math.min(330, + (width - pageMargin * 2) * 0.30)) AmmTheme { id: theme @@ -38,34 +46,194 @@ Item { anchors.fill: parent clip: true contentWidth: width - contentHeight: Math.max(height, form.y + form.implicitHeight + root.pageMargin) + contentHeight: Math.max(height, pageLayout.y + pageLayout.implicitHeight + root.pageMargin) enabled: !confirmationDialog.opened flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds - NewPositionForm { - id: form + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + ColumnLayout { + id: pageLayout x: Math.max(root.pageMargin, (scroll.width - width) / 2) - y: root.pageMargin - width: Math.max(0, Math.min(root.preferredWidth, scroll.width - root.pageMargin * 2)) - theme: theme - newPositionContext: newPositionFlow.newPositionContext - flowState: newPositionFlow.viewState + y: root.wideLayout ? 32 : 16 + width: Math.max(0, Math.min(root.contentMaxWidth, + scroll.width - root.pageMargin * 2)) + spacing: 24 - onQuoteRequested: function(immediate, quoteRequest) { - newPositionFlow.scheduleQuote(immediate, quoteRequest) + RowLayout { + Layout.fillWidth: true + spacing: 16 + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: qsTr("New position") + color: theme.colors.textPrimary + font.pixelSize: 30 + font.weight: Font.Bold + font.letterSpacing: 0 + } + + Text { + Layout.fillWidth: true + text: form.contextStatusText() + color: theme.colors.textSecondary + font.pixelSize: 12 + elide: Text.ElideRight + } + } + + LogosIconButton { + objectName: "refreshPositionButton" + iconSource: LogosIcons.refresh + iconColor: theme.colors.textSecondary + iconSize: 18 + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + enabled: !newPositionFlow.contextLoading && !newPositionFlow.submitting + Accessible.name: qsTr("Refresh position data") + ToolTip.visible: hovered + ToolTip.text: Accessible.name + onClicked: newPositionFlow.refreshContext(true) + } } - onConfirmationRequested: function(snapshot) { - confirmationDialog.openWithSnapshot(snapshot) + Rectangle { + id: compactSteps + + objectName: "compactPositionSteps" + Layout.fillWidth: true + implicitHeight: compactStepRow.implicitHeight + 32 + visible: !root.wideLayout + radius: 16 + color: theme.colors.cardBg + border.color: theme.colors.border + border.width: 1 + + RowLayout { + id: compactStepRow + + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + StepMarker { + Layout.fillWidth: true + colors: theme.colors + stepNumber: 1 + label: qsTr("Select pair and fees") + active: !form.hasPair + complete: form.hasPair + } + + Rectangle { + Layout.preferredWidth: 20 + Layout.preferredHeight: 1 + color: form.hasPair ? theme.colors.ctaBg : theme.colors.divider + } + + StepMarker { + Layout.fillWidth: true + colors: theme.colors + stepNumber: 2 + label: form.missingPool + ? qsTr("Set price and deposit") + : qsTr("Enter deposit amounts") + active: form.hasPair + } + } } - onTokenResolveRequested: function(tokenId) { - newPositionFlow.resolveToken(tokenId) - } + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: root.wideLayout ? 40 : 0 - onDraftChanged: newPositionFlow.draftChanged() - onRefreshRequested: newPositionFlow.refreshContext(true) + Rectangle { + id: positionStepRail + + objectName: "positionStepRail" + Layout.preferredWidth: root.stepRailWidth + Layout.alignment: Qt.AlignTop + implicitHeight: verticalSteps.implicitHeight + 40 + visible: root.wideLayout + radius: 16 + color: theme.colors.cardBg + border.color: theme.colors.border + border.width: 1 + + ColumnLayout { + id: verticalSteps + + anchors.fill: parent + anchors.margins: 20 + spacing: 0 + + StepMarker { + Layout.fillWidth: true + colors: theme.colors + stepNumber: 1 + label: qsTr("Select token pair and fees") + active: !form.hasPair + complete: form.hasPair + } + + Rectangle { + Layout.leftMargin: 17 + Layout.preferredWidth: 1 + Layout.preferredHeight: 28 + color: form.hasPair ? theme.colors.ctaBg : theme.colors.divider + } + + StepMarker { + Layout.fillWidth: true + colors: theme.colors + stepNumber: 2 + label: form.missingPool + ? qsTr("Set price and deposit amounts") + : qsTr("Enter deposit amounts") + active: form.hasPair + } + } + } + + NewPositionForm { + id: form + + objectName: "newPositionForm" + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + theme: theme + headingText: form.hasPair ? qsTr("Deposit tokens") : qsTr("Select pair") + headingDetail: form.hasPair + ? qsTr("Specify the token amounts for your liquidity contribution.") + : qsTr("Choose two tokens and a fee tier for this position.") + showRefreshAction: false + newPositionContext: newPositionFlow.newPositionContext + flowState: newPositionFlow.viewState + + onQuoteRequested: function(immediate, quoteRequest) { + newPositionFlow.scheduleQuote(immediate, quoteRequest) + } + + onConfirmationRequested: function(snapshot) { + confirmationDialog.openWithSnapshot(snapshot) + } + + onTokenResolveRequested: function(tokenId) { + newPositionFlow.resolveToken(tokenId) + } + + onDraftChanged: newPositionFlow.draftChanged() + onRefreshRequested: newPositionFlow.refreshContext(true) + } + } } } @@ -87,7 +255,6 @@ Item { function onQuoteRefreshRequested(immediate) { form.requestQuote(immediate) } - } Component { @@ -108,4 +275,63 @@ Item { newPositionFlow.confirm(snapshot) } } + + component StepMarker: RowLayout { + id: marker + + required property var colors + required property int stepNumber + required property string label + property bool active: false + property bool complete: false + + spacing: 12 + + Rectangle { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + radius: 18 + color: marker.active + ? marker.colors.ctaBg + : marker.complete ? marker.colors.selection : marker.colors.inputBg + border.color: marker.active || marker.complete + ? marker.colors.ctaBg : marker.colors.borderStrong + border.width: 1 + Accessible.ignored: true + + Text { + anchors.centerIn: parent + text: marker.stepNumber + color: marker.active + ? marker.colors.background + : marker.complete ? marker.colors.ctaBg : marker.colors.textPlaceholder + font.pixelSize: 13 + font.weight: Font.DemiBold + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + Layout.fillWidth: true + text: qsTr("Step %1").arg(marker.stepNumber) + color: marker.active || marker.complete + ? marker.colors.textSecondary : marker.colors.textPlaceholder + font.pixelSize: 11 + elide: Text.ElideRight + } + + Text { + Layout.fillWidth: true + text: marker.label + color: marker.active || marker.complete + ? marker.colors.textPrimary : marker.colors.textSecondary + font.pixelSize: 13 + font.weight: marker.active ? Font.DemiBold : Font.Normal + wrapMode: Text.Wrap + } + } + } } diff --git a/apps/amm/tests/qml/tst_LiquidityPage.qml b/apps/amm/tests/qml/tst_LiquidityPage.qml index f4980e4..afc9b4a 100644 --- a/apps/amm/tests/qml/tst_LiquidityPage.qml +++ b/apps/amm/tests/qml/tst_LiquidityPage.qml @@ -40,6 +40,32 @@ TestCase { } } + function test_positionLayoutUsesDesktopRailAndCompactProgress() { + var page = createTemporaryObject(pageComponent, testCase) + verify(page) + page.visible = true + wait(0) + + var rail = findChild(page, "positionStepRail") + var compactSteps = findChild(page, "compactPositionSteps") + var form = findChild(page, "newPositionForm") + verify(rail) + verify(compactSteps) + verify(form) + + compare(page.wideLayout, true) + verify(rail.width > 0) + verify(form.width > rail.width) + + page.width = 600 + wait(0) + + compare(page.wideLayout, false) + verify(compactSteps.implicitHeight > 0) + verify(form.width > 0) + verify(form.width <= page.width - 32) + } + Component { id: runtimeComponent