diff --git a/amm-ui/Main.qml b/amm-ui/Main.qml deleted file mode 100644 index 55caa68..0000000 --- a/amm-ui/Main.qml +++ /dev/null @@ -1,30 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -Item { - width: 400 - height: 300 - - ColumnLayout { - anchors.centerIn: parent - spacing: 12 - - Text { - Layout.alignment: Qt.AlignHCenter - text: "Hello from ui_qml_example!" - font.pixelSize: 18 - } - - Button { - Layout.alignment: Qt.AlignHCenter - text: "Call Core Module" - onClicked: { - // The logos bridge is injected by the host application. - // Uncomment to call a backend module: - // var result = logos.callModule("my_module", "myMethod", ["arg"]) - console.log("Button clicked") - } - } - } -} diff --git a/amm-ui/metadata.json b/amm-ui/metadata.json index 8cb08f5..7ebad2b 100644 --- a/amm-ui/metadata.json +++ b/amm-ui/metadata.json @@ -4,7 +4,7 @@ "type": "ui_qml", "category": "amm", "description": "UI module for the AMM program", - "view": "Main.qml", + "view": "qml/Main.qml", "icon": "icons/amm.png", "dependencies": [], diff --git a/amm-ui/qml/Main.qml b/amm-ui/qml/Main.qml new file mode 100644 index 0000000..52fa1ba --- /dev/null +++ b/amm-ui/qml/Main.qml @@ -0,0 +1,152 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property var tokenData: [ + { symbol: "TOK1", name: "Token 1", color: "#627eea", letter: "E", address: "0x0000000000000000000000000000000000000000", usdPrice: 2392.70 }, + { symbol: "TOK2", name: "Token 2", color: "#2775ca", letter: "$", address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", usdPrice: 1.00 }, + { symbol: "TOK3", name: "Token 3", color: "#26a17b", letter: "T", address: "0xdac17f958d2ee523a2206206994597c13d831ec7", usdPrice: 1.00 }, + { symbol: "TOK4", name: "Token 4", color: "#f7931a", letter: "B", address: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", usdPrice: 63500 }, + { symbol: "TOK5", name: "Token 5", color: "#627eea", letter: "E", address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", usdPrice: 2392.70 }, + { symbol: "TOK6", name: "Token 6", color: "#9b59b6", letter: "L", address: "0x1337000000000000000000000000000000000cafe", usdPrice: 0.42 } + ] + + // ── Theme ───────────────────────────────────────────────────────────────── + QtObject { + id: theme + property bool isDark: false + property var colors: isDark ? dark : light + + readonly property var light: ({ + background: "#f7f7f5", + cardBg: "#ffffff", + inputBg: "#f0f0ee", + panelBg: "#e8e8e4", + panelHoverBg: "#ddddd8", + textPrimary: "#111111", + textSecondary: "#777770", + textPlaceholder: "#bbbbb5", + border: Qt.rgba(0,0,0,0.08), + borderStrong: Qt.rgba(0,0,0,0.10), + divider: Qt.rgba(0,0,0,0.06), + ctaBg: "#111111", + ctaHoverBg: "#2a2a28", + selection: "#b5c4a5", + noTokenCircle: "#c8c8c4", + orb1: "#7a8c6a", + orb2: "#b5c4a5", + orb3: "#7a8c6a", + orb4: "#c8d4b8" + }) + + readonly property var dark: ({ + background: "#0d0d12", + cardBg: "#1a1a22", + inputBg: "#222230", + panelBg: "#2a2a38", + panelHoverBg: "#363650", + textPrimary: "#ffffff", + textSecondary: "#888899", + textPlaceholder: "#444455", + border: Qt.rgba(1,1,1,0.08), + borderStrong: Qt.rgba(1,1,1,0.10), + divider: Qt.rgba(1,1,1,0.06), + ctaBg: "#2d1530", + ctaHoverBg: "#3d1f40", + selection: "#4c1d4b", + noTokenCircle: "#444455", + orb1: "#627eea", + orb2: "#9b59b6", + orb3: "#fc72ff", + orb4: "#26a17b" + }) + } + + // ── Root background ─────────────────────────────────────────────────────── + Rectangle { + anchors.fill: parent + color: theme.colors.background + Behavior on color { ColorAnimation { duration: 300 } } + + // Theme toggle + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 16 + width: 44; height: 24; radius: 12 + color: theme.colors.panelBg + border.color: theme.colors.border + border.width: 1 + Text { + anchors.centerIn: parent + text: theme.isDark ? "☀" : "☾" + font.pixelSize: 13 + color: theme.colors.textSecondary + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: theme.isDark = !theme.isDark + } + } + + // Decorative orbs + Rectangle { x: -180; y: -120; width: 560; height: 560; radius: 280; color: theme.colors.orb1; opacity: 0.07 } + Rectangle { x: parent.width - 280; y: parent.height - 320; width: 480; height: 480; radius: 240; color: theme.colors.orb2; opacity: 0.09 } + Rectangle { x: parent.width - 200; y: -80; width: 380; height: 380; radius: 190; color: theme.colors.orb3; opacity: 0.05 } + Rectangle { x: 40; y: parent.height - 260; width: 320; height: 320; radius: 160; color: theme.colors.orb4; opacity: 0.08 } + + ColumnLayout { + anchors.centerIn: parent + spacing: 28 + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Logos AMM" + color: theme.colors.textPrimary + font.pixelSize: 48 + font.weight: Font.Bold + } + + SwapCard { + id: swapCard + Layout.alignment: Qt.AlignHCenter + theme: theme + tokens: root.tokenData + width: Math.min(480, root.width - 32) + + onRequestTokenSelect: function(side) { + tokenModal.targetSide = side + tokenModal.open() + } + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Buy and sell crypto on LEZ." + textFormat: Text.RichText + color: theme.colors.textSecondary + font.pixelSize: 15 + horizontalAlignment: Text.AlignHCenter + } + } + + TokenSelectorModal { + id: tokenModal + anchors.fill: parent + z: 10 + theme: theme + tokens: root.tokenData + + property string targetSide: "sell" + + onTokenSelected: function(tok) { + swapCard.setToken(targetSide, tok) + tokenModal.close() + } + } + } +} diff --git a/amm-ui/qml/SwapCard.qml b/amm-ui/qml/SwapCard.qml new file mode 100644 index 0000000..9b72e2f --- /dev/null +++ b/amm-ui/qml/SwapCard.qml @@ -0,0 +1,153 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Rectangle { + id: root + + property var theme + property var tokens: [] + property var sellToken: null + property var buyToken: null + property string sellAmount: "" + + signal requestTokenSelect(string side) + + function setToken(side, token) { + if (side === "sell") root.sellToken = token + else root.buyToken = token + } + + readonly property string buyAmount: { + if (!sellToken || !buyToken || sellAmount === "") return "" + var amt = parseFloat(sellAmount) + if (isNaN(amt) || amt <= 0) return "" + var result = amt * sellToken.usdPrice / buyToken.usdPrice + return result >= 1 ? result.toFixed(2) : result.toFixed(6) + } + + readonly property string sellUsd: { + if (!sellToken || sellAmount === "") return "" + var amt = parseFloat(sellAmount) + if (isNaN(amt)) return "" + var val = amt * sellToken.usdPrice + return "~$" + val.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",") + } + + readonly property string buyUsd: { + if (!buyToken || buyAmount === "") return "" + var amt = parseFloat(buyAmount) + if (isNaN(amt)) return "" + var val = amt * buyToken.usdPrice + return "~$" + val.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",") + } + + radius: 24 + color: theme.colors.cardBg + border.color: theme.colors.border + border.width: 1 + implicitWidth: 480 + implicitHeight: cardLayout.implicitHeight + 16 + + Behavior on color { ColorAnimation { duration: 300 } } + + ColumnLayout { + id: cardLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 8 + spacing: 0 + + TokenInput { + Layout.fillWidth: true + theme: root.theme + label: "Sell" + amount: root.sellAmount + usdValue: root.sellUsd + token: root.sellToken + readOnly: false + onInputEdited: function(v) { root.sellAmount = v } + onTokenClicked: root.requestTokenSelect("sell") + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 40 + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: theme.colors.divider + } + + Rectangle { + anchors.centerIn: parent + width: 36; height: 36; radius: 18 + color: swapHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg + border.color: theme.colors.borderStrong + border.width: 1 + Behavior on color { ColorAnimation { duration: 120 } } + + Text { + anchors.centerIn: parent + text: "↓" + color: theme.colors.textPrimary + font.pixelSize: 16 + } + + MouseArea { + id: swapHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + var tmp = root.sellToken + root.sellToken = root.buyToken + root.buyToken = tmp + } + } + } + } + + TokenInput { + Layout.fillWidth: true + theme: root.theme + label: "Buy" + amount: root.buyAmount + usdValue: root.buyUsd + token: root.buyToken + readOnly: true + onTokenClicked: root.requestTokenSelect("buy") + } + + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.bottomMargin: 8 + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.preferredHeight: 56 + radius: 20 + color: ctaHover.containsMouse ? theme.colors.ctaHoverBg : theme.colors.ctaBg + Behavior on color { ColorAnimation { duration: 120 } } + + Text { + anchors.centerIn: parent + text: "Swap" + color: "#ffffff" + font.pixelSize: 17 + font.weight: Font.Medium + } + + MouseArea { + id: ctaHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + } + } +} diff --git a/amm-ui/qml/TokenInput.qml b/amm-ui/qml/TokenInput.qml new file mode 100644 index 0000000..86193b7 --- /dev/null +++ b/amm-ui/qml/TokenInput.qml @@ -0,0 +1,133 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +Rectangle { + id: root + + property var theme + property string label: "" + property string amount: "" + property string usdValue: "" + property var token: null + property bool readOnly: false + + signal tokenClicked() + signal inputEdited(string newValue) + + Binding { + target: tiInput + property: "text" + value: root.amount + when: root.readOnly + } + + radius: 16 + color: theme.colors.inputBg + implicitHeight: 110 + + Behavior on color { ColorAnimation { duration: 300 } } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 14 + anchors.bottomMargin: 14 + spacing: 8 + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: root.label + color: theme.colors.textSecondary + font.pixelSize: 14 + } + + Item { + Layout.fillWidth: true + height: 44 + + TextInput { + id: tiInput + anchors.fill: parent + color: theme.colors.textPrimary + font.pixelSize: 36 + font.weight: Font.Bold + readOnly: root.readOnly + selectionColor: theme.colors.selection + clip: true + onTextChanged: { if (!root.readOnly) root.inputEdited(text) } + validator: RegularExpressionValidator { + regularExpression: /^[0-9]*\.?[0-9]*$/ + } + } + + Text { + anchors.fill: parent + text: "0" + color: theme.colors.textPlaceholder + font: tiInput.font + visible: tiInput.text === "" && !tiInput.activeFocus + verticalAlignment: Text.AlignVCenter + } + } + + Text { + text: root.usdValue + color: theme.colors.textSecondary + font.pixelSize: 13 + visible: root.usdValue !== "" + } + } + + Rectangle { + height: 40 + radius: 20 + color: tokenBtnHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg + implicitWidth: tokenBtnRow.implicitWidth + 24 + Behavior on color { ColorAnimation { duration: 120 } } + + RowLayout { + id: tokenBtnRow + anchors.centerIn: parent + spacing: 6 + + Rectangle { + width: 24; height: 24; radius: 12 + color: root.token ? root.token.color : theme.colors.noTokenCircle + visible: root.token !== null + Text { + anchors.centerIn: parent + text: root.token ? root.token.letter : "" + color: "#ffffff" + font.pixelSize: 10 + font.weight: Font.Bold + } + } + + Text { + text: root.token ? root.token.symbol : "Select token" + color: theme.colors.textPrimary + font.pixelSize: 15 + font.weight: root.token ? Font.Medium : Font.Normal + } + + Text { + text: "▼" + color: theme.colors.textSecondary + font.pixelSize: 10 + } + } + + MouseArea { + id: tokenBtnHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.tokenClicked() + } + } + } +} diff --git a/amm-ui/qml/TokenListItem.qml b/amm-ui/qml/TokenListItem.qml new file mode 100644 index 0000000..697341a --- /dev/null +++ b/amm-ui/qml/TokenListItem.qml @@ -0,0 +1,76 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property var theme + property string tokenName: "" + property string tokenSymbol: "" + property string tokenAddress: "" + property string tokenColor: "#627eea" + property string tokenLetter: "" + + signal clicked() + + implicitHeight: 56 + + Rectangle { + anchors.fill: parent + radius: 12 + color: hoverArea.containsMouse ? theme.colors.panelBg : "transparent" + Behavior on color { ColorAnimation { duration: 120 } } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + spacing: 12 + + Rectangle { + width: 36; height: 36; radius: 18 + color: root.tokenColor + Text { + anchors.centerIn: parent + text: root.tokenLetter + color: "#ffffff" + font.pixelSize: 14 + font.weight: Font.Bold + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: root.tokenName + color: theme.colors.textPrimary + font.pixelSize: 15 + elide: Text.ElideRight + Layout.fillWidth: true + } + + RowLayout { + spacing: 6 + Text { text: root.tokenSymbol; color: theme.colors.textSecondary; font.pixelSize: 12 } + Text { + text: root.tokenAddress !== "" + ? root.tokenAddress.substring(0, 6) + "..." + root.tokenAddress.slice(-4) + : "" + color: theme.colors.textPlaceholder + font.pixelSize: 12 + } + } + } + } + + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } + } +} diff --git a/amm-ui/qml/TokenSelectorModal.qml b/amm-ui/qml/TokenSelectorModal.qml new file mode 100644 index 0000000..99f80a0 --- /dev/null +++ b/amm-ui/qml/TokenSelectorModal.qml @@ -0,0 +1,173 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property var theme + property var tokens: [] + property string searchText: "" + + signal tokenSelected(var token) + + visible: false + + function open() { + root.visible = true + searchText = "" + searchField.text = "" + searchField.forceActiveFocus() + } + + function close() { + root.visible = false + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.4) + MouseArea { anchors.fill: parent; onClicked: root.close() } + } + + Rectangle { + anchors.centerIn: parent + width: Math.min(480, root.width - 32) + height: Math.min(600, root.height - 64) + radius: 24 + color: theme.colors.cardBg + border.color: theme.colors.border + border.width: 1 + + Behavior on color { ColorAnimation { duration: 300 } } + + MouseArea { anchors.fill: parent; onClicked: {} } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + RowLayout { + Layout.fillWidth: true + Text { + Layout.fillWidth: true + text: "Select a token" + color: theme.colors.textPrimary + font.pixelSize: 18 + font.weight: Font.Bold + } + Rectangle { + width: 32; height: 32; radius: 16 + color: closeHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg + Behavior on color { ColorAnimation { duration: 120 } } + Text { anchors.centerIn: parent; text: "✕"; color: theme.colors.textSecondary; font.pixelSize: 14 } + MouseArea { + id: closeHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.close() + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 48 + radius: 16 + color: theme.colors.inputBg + border.color: searchField.activeFocus ? theme.colors.borderStrong : theme.colors.border + border.width: 1 + Behavior on border.color { ColorAnimation { duration: 150 } } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 14 + anchors.rightMargin: 14 + spacing: 8 + Text { text: "⌕"; color: theme.colors.textSecondary; font.pixelSize: 20 } + TextInput { + id: searchField + Layout.fillWidth: true + color: theme.colors.textPrimary + font.pixelSize: 15 + selectionColor: theme.colors.selection + onTextChanged: root.searchText = text + Text { + anchors.fill: parent + text: "Search tokens" + color: theme.colors.textPlaceholder + font: searchField.font + visible: searchField.text === "" && !searchField.activeFocus + verticalAlignment: Text.AlignVCenter + } + } + } + } + + Text { text: "Popular tokens"; color: theme.colors.textSecondary; font.pixelSize: 13 } + + Flow { + Layout.fillWidth: true + spacing: 8 + Repeater { + model: root.tokens.slice(0, 5) + delegate: Rectangle { + height: 40 + radius: 20 + color: pillHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg + border.color: theme.colors.border + border.width: 1 + width: pillRow.implicitWidth + 24 + Behavior on color { ColorAnimation { duration: 120 } } + RowLayout { + id: pillRow + anchors.centerIn: parent + spacing: 6 + Rectangle { + width: 22; height: 22; radius: 11 + color: modelData.color + Text { anchors.centerIn: parent; text: modelData.letter; color: "#ffffff"; font.pixelSize: 10; font.weight: Font.Bold } + } + Text { text: modelData.symbol; color: theme.colors.textPrimary; font.pixelSize: 13; font.weight: Font.Medium } + } + MouseArea { + id: pillHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.tokenSelected(modelData) + } + } + } + } + + Text { text: "Tokens by 24H volume"; color: theme.colors.textSecondary; font.pixelSize: 13 } + + ListView { + id: tokenList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 2 + model: root.tokens.filter(function(t) { + if (root.searchText === "") return true + var q = root.searchText.toLowerCase() + return t.symbol.toLowerCase().indexOf(q) !== -1 || + t.name.toLowerCase().indexOf(q) !== -1 + }) + delegate: TokenListItem { + width: tokenList.width + theme: root.theme + tokenName: modelData.name + tokenSymbol: modelData.symbol + tokenAddress: modelData.address + tokenColor: modelData.color + tokenLetter: modelData.letter + onClicked: root.tokenSelected(modelData) + } + } + } + } +}