feat(amm-ui): reshape liquidity position layout

Add responsive step navigation and a wider deposit workspace while preserving the shared create-pool and add-liquidity flow.
This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-14 20:03:34 -03:00
parent 2b97012956
commit abbfa08e90
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
3 changed files with 283 additions and 24 deletions

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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