Fold thirty-one settings pages into fifteen categories with tabs
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
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
// sat below a wallpaper grid and an entire lock screen. Tabs are not a way to
|
||||
// make a short page look organised; they are for when the page is long enough
|
||||
// that the order stops being a suggestion and starts being a burial.
|
||||
//
|
||||
// The strip scrolls horizontally when it overflows: the Settings window tiles
|
||||
// down to 900px, and the System category carries nine tabs. The active tab is
|
||||
// always brought into view, and edge fades say there is more to the side.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
@@ -27,55 +31,110 @@ Item {
|
||||
width: parent ? parent.width : 620
|
||||
implicitHeight: 40
|
||||
|
||||
Row {
|
||||
id: strip
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 2
|
||||
Flickable {
|
||||
id: flick
|
||||
|
||||
Repeater {
|
||||
model: root.tabs
|
||||
anchors.fill: parent
|
||||
contentWidth: strip.implicitWidth
|
||||
contentHeight: height
|
||||
flickableDirection: Flickable.HorizontalFlick
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
interactive: contentWidth > width
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
id: tab
|
||||
function reveal(item: Item): void {
|
||||
if (contentWidth <= width)
|
||||
return;
|
||||
const target = item.x - (flick.width - item.width) / 2;
|
||||
contentX = Math.max(0, Math.min(contentWidth - width, target));
|
||||
}
|
||||
|
||||
required property var modelData
|
||||
Row {
|
||||
id: strip
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 2
|
||||
|
||||
readonly property bool active: String(tab.modelData.value) === root.current
|
||||
Repeater {
|
||||
model: root.tabs
|
||||
|
||||
implicitWidth: caption.implicitWidth + 30
|
||||
implicitHeight: 38
|
||||
delegate: Item {
|
||||
id: tab
|
||||
|
||||
Text {
|
||||
id: caption
|
||||
anchors.centerIn: parent
|
||||
text: String(tab.modelData.label ?? "")
|
||||
color: tab.active ? Theme.fg : (hover.hovered ? Theme.fgDim : Theme.fgMuted)
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: tab.active ? Font.DemiBold : Font.Medium
|
||||
}
|
||||
required property var modelData
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 2
|
||||
radius: 1
|
||||
visible: tab.active
|
||||
color: Theme.accent
|
||||
}
|
||||
readonly property bool active: String(tab.modelData.value) === root.current
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
implicitWidth: caption.implicitWidth + 30
|
||||
implicitHeight: 38
|
||||
|
||||
TapHandler {
|
||||
onTapped: root.selected(String(tab.modelData.value))
|
||||
onActiveChanged: if (active) flick.reveal(tab)
|
||||
Component.onCompleted: if (active) flick.reveal(tab)
|
||||
|
||||
Text {
|
||||
id: caption
|
||||
anchors.centerIn: parent
|
||||
text: String(tab.modelData.label ?? "")
|
||||
color: tab.active ? Theme.fg : (hover.hovered ? Theme.fgDim : Theme.fgMuted)
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: tab.active ? Font.DemiBold : Font.Medium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 2
|
||||
radius: 1
|
||||
visible: tab.active
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: root.selected(String(tab.modelData.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
target: null
|
||||
onWheel: event => {
|
||||
const delta = event.angleDelta.x !== 0 ? event.angleDelta.x : event.angleDelta.y;
|
||||
flick.contentX = Math.max(0, Math.min(flick.contentWidth - flick.width, flick.contentX - delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
width: 18
|
||||
height: parent.height
|
||||
visible: flick.contentX > 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0; color: Theme.bg }
|
||||
GradientStop { position: 1; color: Theme.alpha(Theme.bg, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: 18
|
||||
height: parent.height
|
||||
visible: flick.contentWidth > flick.width && flick.contentX < flick.contentWidth - flick.width - 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0; color: Theme.alpha(Theme.bg, 0) }
|
||||
GradientStop { position: 1; color: Theme.bg }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
Reference in New Issue
Block a user