pragma ComponentBehavior: Bound import Quickshell import QtQuick import QtQuick.Layouts import ZShell.Llm import ZShell.Config import qs.Modules.Sidebar.Chat.Content import qs.Services import qs.Components Item { id: root property alias list: list property alias model: list.model property bool highlight: false property bool slim: false signal requestExpand signal deleteChatRequest(content: ChatSession) signal loadChatRequest(content: ChatSession, index: int) signal newChatRequest onSlimChanged: { if (slim) modelsContainer.open = false; } CustomText { id: chatHeader anchors.left: parent.left anchors.margins: Tokens.padding.extraSmall anchors.right: parent.right anchors.top: parent.top color: Colors.palette.m3outline font.family: "CaskaydiaCove NF" font.pointSize: 13 font.weight: 500 text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "") } CustomListView { id: list anchors.bottom: modelsContainer.top anchors.left: parent.left anchors.right: parent.right anchors.top: chatHeader.bottom anchors.topMargin: Tokens.spacing.medium clip: true spacing: Tokens.spacing.medium add: Transition { Anim { from: list.width property: "x" } } remove: Transition { Anim { to: list.width property: "x" } Anim { to: 0 property: "opacity" } } displaced: Transition { Anim { property: "y" } } move: Transition { Anim { property: "y" } } delegate: ChatDelegate { id: chat required property int index implicitWidth: ListView.view.width highlighted: root.highlight && ChatState.chatSession === modelData slim: root.slim onHighlightedChanged: if (highlighted) list.currentIndex = index onClicked: content => root.loadChatRequest(content, index) onRemove: content => root.deleteChatRequest(content) } TapHandler { onTapped: { root.focus = true; ChatState.fabExpanded = false; modelsContainer.expanded = false; } } } ModelsContainer { id: modelsContainer anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: fabRoot.left anchors.margins: Tokens.padding.small anchors.rightMargin: Tokens.spacing.small rootParent: root fabRoot: fabRoot opacity: root.slim ? 0 : 1 visible: opacity > 0 Behavior on opacity { Anim { type: Anim.DefaultEffects } } } IconButton { id: fabRoot anchors.bottom: parent.bottom anchors.right: parent.right anchors.margins: Tokens.padding.small padding: 8 font.pointSize: Math.round(18 * 1.2) icon: root.slim ? "left_panel_open" : "add" isRound: ChatState.fabExpanded label.transform: Rotation { origin.y: fabRoot.label.height / 2 origin.x: fabRoot.label.width / 2 angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded || !ChatState.isWide || !ChatState.isWindow) ? 135 : 0 Behavior on angle { Anim {} } } onClicked: { if ((root.slim && !ChatState.narrowSidebarExpanded) || ChatState.shrinkSidebarOverride) { root.requestExpand(); ChatState.shrinkSidebarOverride = false; return; } ChatState.fabExpanded = !ChatState.fabExpanded; } Elevation { anchors.fill: parent level: fabRoot.stateLayer.containsMouse ? 4 : 3 radius: parent.radius z: -1 } } ColumnLayout { id: fabMenu anchors.bottom: fabRoot.top anchors.right: fabRoot.right anchors.bottomMargin: Tokens.padding.medium Repeater { id: fabRep model: ListModel { ListElement { name: "new chat" icon: "add" } ListElement { name: "pop out in window" icon: "open_in_new" } ListElement { name: "shrink sidebar" icon: "left_panel_close" } } CustomRect { id: fabMenuItem required property var modelData required property int index Layout.alignment: Qt.AlignRight implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2 radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2 color: Colors.palette.m3primaryContainer visible: { const n = modelData.name; const w = ChatState.isWindow; const wide = ChatState.isWide; if (n === "pop out in window" && w) return false; if (n === "shrink sidebar" && (!w || !wide)) return false; else return true; } clip: true Behavior on radius { Anim { type: Anim.DefaultEffects } } opacity: 0 states: State { name: "visible" when: ChatState.fabExpanded PropertyChanges { fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2 fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } } transitions: [ Transition { to: "visible" SequentialAnimation { PauseAnimation { duration: (fabRep.count - 1 - fabMenuItem.index) * 50 } ParallelAnimation { Anim { property: "implicitWidth" type: Anim.FastSpatial } Anim { property: "opacity" duration: Tokens.anim.durations.small easing: Tokens.anim.standard } } } }, Transition { from: "visible" SequentialAnimation { PauseAnimation { duration: fabMenuItem.index * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" type: Anim.FastSpatial } Anim { property: "opacity" duration: Tokens.anim.durations.small easing: Tokens.anim.standard } } } } ] RowLayout { id: fabMenuItemInner anchors.centerIn: parent spacing: Tokens.spacing.medium opacity: 0 MaterialIcon { text: fabMenuItem.modelData.icon color: Colors.palette.m3onPrimaryContainer fontStyle: Tokens.font.icon.large fill: 1 } CustomText { animate: true text: fabMenuItem.modelData.name font.capitalization: Font.Capitalize color: Colors.palette.m3onPrimaryContainer Layout.preferredWidth: implicitWidth Behavior on Layout.preferredWidth { Anim {} } } } StateLayer { id: sLayer onClicked: { const name = fabMenuItem.modelData.name; if (name === "new chat") root.newChatRequest(); else if (name === "pop out in window") Detach.create(); else if (name === "shrink sidebar") ChatState.shrinkSidebarOverride = true; ChatState.fabExpanded = false; } } } } } }