import QtQuick import qs.config import qs.services Rectangle { id: root property string selectedPage: "home" property string query: searchInput.text.trim().toLowerCase() signal pageRequested(string page) readonly property var results: SettingsSearch.search(root.query) onQueryChanged: { if (sidebarScroll) sidebarScroll.contentY = 0; } // Search results say where a hit will land ("System › Storage"), because // with tabbed categories the page name alone no longer locates it. function pageLabel(page: string): string { return SettingsRoutes.breadcrumb(page); } // One row per category. SettingsRoutes owns the taxonomy; a category with // tabs is opened at its first available tab by ShellState's resolution. readonly property var destinations: SettingsRoutes.categories width: 272 color: Theme.alpha(Theme.bgDark, 0.96) border.width: 0 Column { id: sidebarHeader anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.leftMargin: 18 anchors.rightMargin: 18 anchors.topMargin: 18 height: implicitHeight spacing: 12 Rectangle { width: parent.width height: 36 radius: 10 color: Theme.alpha(Theme.fg, searchArea.containsMouse || searchInput.activeFocus ? 0.105 : 0.065) border.width: 1 border.color: searchInput.activeFocus ? Theme.alpha(Theme.accent, 0.5) : Theme.alpha(Theme.fg, 0.055) Text { anchors.left: parent.left anchors.leftMargin: 11 anchors.verticalCenter: parent.verticalCenter text: "\u{F0349}" color: Theme.fgDim font.family: Theme.fontMono font.pixelSize: 14 } TextInput { id: searchInput anchors.left: parent.left anchors.leftMargin: 35 anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter color: Theme.fg selectionColor: Theme.accent selectedTextColor: Theme.bgDark font.family: Theme.fontFamily font.pixelSize: Theme.fontSize clip: true Text { anchors.fill: parent visible: !searchInput.text && !searchInput.activeFocus text: "Search settings" color: Theme.fgMuted font: searchInput.font verticalAlignment: Text.AlignVCenter } } MouseArea { id: searchArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor onClicked: searchInput.forceActiveFocus() } } } Flickable { id: sidebarScroll anchors.left: parent.left anchors.right: parent.right anchors.top: sidebarHeader.bottom anchors.bottom: healthFooter.top anchors.leftMargin: 18 anchors.rightMargin: 18 anchors.topMargin: 12 anchors.bottomMargin: 12 contentWidth: width contentHeight: scrollContent.implicitHeight flickableDirection: Flickable.VerticalFlick boundsBehavior: Flickable.StopAtBounds clip: true Column { id: scrollContent width: sidebarScroll.width // ── Search results ────────────────────────────────────────────── // Typing searches the settings themselves, not page names. Column { id: searchResults width: parent.width spacing: 3 visible: root.query !== "" Text { width: parent.width leftPadding: 4 bottomPadding: 4 text: root.results.length === 0 ? "Nothing matches" : root.results.length + (root.results.length === 1 ? " result" : " results") color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } Repeater { model: root.results Rectangle { id: hit required property var modelData width: parent.width height: 44 radius: 10 color: hitMouse.containsMouse ? Theme.alpha(Theme.fg, 0.08) : "transparent" border.width: 0 Column { anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: 12 anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter spacing: 1 Text { width: parent.width text: hit.modelData.label color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize elide: Text.ElideRight } Text { width: parent.width text: hit.modelData.kind === "shortcut" ? hit.modelData.detail : root.pageLabel(hit.modelData.page) color: hit.modelData.kind === "shortcut" ? Theme.accent : Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall elide: Text.ElideRight } } MouseArea { id: hitMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { root.pageRequested(hit.modelData.page); searchInput.text = ""; } } } } } Column { id: navigationList width: parent.width spacing: 4 visible: root.query === "" Repeater { model: root.destinations Rectangle { id: navItem required property var modelData // A row lights up when it owns the current leaf, so // System stays highlighted while you sit on Storage. readonly property bool active: SettingsRoutes.categoryOf(root.selectedPage).page === modelData.page width: parent.width height: 40 radius: 10 color: navItem.active ? Theme.alpha(Theme.accent, 0.17) : (navMouse.containsMouse ? Theme.alpha(Theme.fg, Theme.hoverAlpha * 0.55) : Theme.alpha(Theme.fg, 0)) border.width: navItem.active ? 1 : 0 border.color: Theme.alpha(Theme.accent, 0.26) Rectangle { width: 2 height: 18 radius: 1 anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter visible: navItem.active gradient: Gradient { GradientStop { position: 0; color: Theme.accent } GradientStop { position: 1; color: Theme.accentSecondary } } } Text { anchors.left: parent.left anchors.leftMargin: 13 anchors.verticalCenter: parent.verticalCenter width: 25 text: navItem.modelData.icon color: navItem.active ? Theme.accent : Theme.fgDim font.family: Theme.fontMono font.pixelSize: 15 } Text { anchors.left: parent.left anchors.leftMargin: 47 anchors.right: parent.right anchors.rightMargin: 9 anchors.verticalCenter: parent.verticalCenter text: navItem.modelData.label color: navItem.active ? Theme.fg : Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: navItem.active ? Font.Medium : Font.Normal elide: Text.ElideRight } MouseArea { id: navMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.pageRequested(navItem.modelData.page) } } } } } } Rectangle { id: healthFooter anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 54 activeFocusOnTab: true color: footerTap.hovered || activeFocus ? Theme.alpha(Theme.fg, 0.07) : Theme.alpha(Theme.bg, 0.35) border.width: activeFocus ? 2 : 0 border.color: Theme.accent // Health owns the verdict; this footer only renders it. It used to // decide for itself, in the opposite order to the Health page's hero, // and the two then disagreed out loud whenever a health check failed // after a successful one -- see Health.headlineState. function footerText(): string { return Health.headline; } function footerColor(): color { switch (Health.tone) { case "danger": return Theme.danger; case "warn": return Theme.warn; case "muted": return Theme.fgMuted; default: return Theme.ok; } } Rectangle { width: 7 height: 7 radius: 4 anchors.left: parent.left anchors.leftMargin: 19 anchors.verticalCenter: parent.verticalCenter color: healthFooter.footerColor() } Text { anchors.left: parent.left anchors.leftMargin: 36 anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: healthFooter.footerText() color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall elide: Text.ElideRight } TapHandler { id: footerTap onTapped: root.pageRequested("services") } Keys.onReturnPressed: root.pageRequested("services") Keys.onSpacePressed: root.pageRequested("services") } }