The sidebar was a flat scan of thirty-one rows; now it reads like a settings app. Multi-subject categories (Input, Network & Sharing, Applications, Users & Accounts, Privacy & Security, System) carry an Appearance-style tab strip above the page, drawn by the shell so the leaf pages themselves are untouched. The taxonomy lives in one new file, services/SettingsRoutes.qml; the sidebar, the strip, route validation, search breadcrumbs, and both generators derive from it. ShellState.settingsPage still holds leaf ids, so every deep link, IPC call, and search result keeps working — and now lands on the exact tab. Dictation moves out of Sound onto its own page under Input, with a handoff back to Sound for the microphone. The strip scrolls when System's nine tabs outgrow a tiled window. All 161 contracts pass. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
341 lines
12 KiB
QML
341 lines
12 KiB
QML
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
|
||
|
||
function footerText(): string {
|
||
if (Health.checks.length === 0)
|
||
return Health.diagnosticUnavailable ? "Health check unavailable" : "Checking the desktop";
|
||
if (Health.status === "error")
|
||
return "Desktop needs attention";
|
||
if (Health.status === "warning") {
|
||
const count = Health.summary.warnings + Health.summary.errors;
|
||
return count + (count === 1 ? " health observation" : " health observations");
|
||
}
|
||
return "Desktop is healthy";
|
||
}
|
||
|
||
function footerColor(): color {
|
||
if (Health.checks.length === 0)
|
||
return Health.diagnosticUnavailable ? Theme.danger : Theme.fgMuted;
|
||
if (Health.status === "error")
|
||
return Theme.danger;
|
||
if (Health.status === "warning")
|
||
return Theme.warn;
|
||
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")
|
||
}
|
||
}
|