Gallery covers new components; add qs.Ui kit README
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
# qs.Ui — Omarchy shell UI kit
|
||||||
|
|
||||||
|
Reusable QML components for omarchy-shell's built-in panels and any
|
||||||
|
third-party plugin that wants a consistent look and behaviour. Every
|
||||||
|
type is exposed under the `qs.Ui` module via the `qmldir` next to this
|
||||||
|
file, so consumers write:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
Item {
|
||||||
|
PanelSeparator { foreground: someColor }
|
||||||
|
PillButton { text: "Click me"; onClicked: ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Theme tokens (corner radius, accent, focus styling) live on the
|
||||||
|
`qs.Commons.Style` singleton; every Ui component already binds to them,
|
||||||
|
so toggling `omarchy style corners <sharp|round>` updates every
|
||||||
|
consumer in one shot.
|
||||||
|
|
||||||
|
## Visual reference
|
||||||
|
|
||||||
|
Run the dev gallery to see every component live:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
omarchy-shell-ipc shell summon omarchy.dev-gallery '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The gallery renders the real components (not copies), so it doubles as
|
||||||
|
a smoke test — if a component starts misbehaving the gallery is the
|
||||||
|
fastest place to see it.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
Grouped by what they're for, not alphabetically.
|
||||||
|
|
||||||
|
### Panel surfaces
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `KeyboardPanel` | Layer-shell popup with `WlrKeyboardFocus.Exclusive`. Use for panels summoned from a bar widget that need keyboard focus on map (j/k navigation, inline editors). |
|
||||||
|
| `PopupCard` | xdg-popup based panel. Use for click/hover overlays that don't need to steal keyboard focus (calendar, weather flyout). |
|
||||||
|
|
||||||
|
### Cursor model
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `CursorSurface` | Rectangle that paints fill + 1px border when `hasCursor` is true, and fill alone when `current` is true. The foundation for "single highlight on screen across keyboard and mouse" rows. Never read `containsMouse` for visuals — bind `hasCursor` from the panel's cursor state instead. |
|
||||||
|
| `PanelKeyCatcher` | Item that emits semantic signals: `moveRequested(dx, dy)`, `activateRequested()`, `closeRequested()`, `deleteRequested()`, `textKey(string)`. Drop inside a panel root, wire signals to the panel's state mutators. `blocked: true` freezes everything except — nothing; it's a hard gate for inline editors. |
|
||||||
|
|
||||||
|
### Interactive primitives
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `PillButton` | Rounded button with optional icon + label + tooltip. Has `active`, `hasCursor` (keyboard cursor), and `enabled`. Hover and keyboard cursor render identically (fill + border) via the shared `hot` state. |
|
||||||
|
| `CursorPill` | `PillButton` that participates in a panel's single-cursor model. Adds a `hovered(bool)` signal so the panel can update its cursor state on mouse enter/leave. Use for DNS-pill / header-pill / segmented-choice patterns. |
|
||||||
|
| `ChoiceButton` | A single button in a mutually-exclusive choice group (segmented control). `selected` uses accent fill+border; focus uses `Style.focusBorderColor` so keyboard nav reads differently from selection. |
|
||||||
|
| `Toggle` | Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. |
|
||||||
|
| `PanelActionButton` | 22×22 right-edge action button (confirm, forget, unpair). `hoverColor` swaps between default foreground tint and urgent (red) tint. |
|
||||||
|
| `PanelSlider` | Volume/progress slider. Drag, click track, or wheel. `moved(value)` fires per change, `released(value)` once at end. (Named to avoid colliding with `QtQuick.Controls.Slider`.) |
|
||||||
|
| `WidgetButton` | Bar widget chrome — for the strip itself, not for inside panels. |
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `PanelSeparator` | 1px alpha rule between sections. `strength` tweaks opacity. |
|
||||||
|
| `PanelSectionHeader` | Small-bold label that introduces a section ("DNS provider", "Wi-Fi networks"). |
|
||||||
|
| `PanelToolTip` | Styled wrapper around Qt's `ToolTip`. Drop inside the hovered item and bind `visible` to the hover state. Use property names `panelForeground`/`panelBackground` (not `foreground`/`background`) to avoid clashing with `ToolTip`'s built-ins. |
|
||||||
|
|
||||||
|
## Theme integration
|
||||||
|
|
||||||
|
`qs.Commons.Style` exposes:
|
||||||
|
|
||||||
|
- `cornerRadius` — mirrors `~/.local/state/omarchy/toggles/quickshell-menu.json`, hot-reloaded
|
||||||
|
- `focusBorderColor`, `focusFillColor`, `focusBorderWidth` — derived from `Color.accent`
|
||||||
|
- `hotFill` — the standard hover/cursor tint (foreground at 0.12 alpha)
|
||||||
|
|
||||||
|
`qs.Commons.Color` exposes the foundational palette plus per-surface
|
||||||
|
roles loaded from the theme's `colors.toml` + `shell.toml`. Components
|
||||||
|
in this kit default-bind to `Color.foreground` / `Color.accent` /
|
||||||
|
`Color.background` so a caller with no explicit theme just works.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Components are stateless about the values they display. They emit
|
||||||
|
signals and let the caller mutate. Don't bake panel-specific state
|
||||||
|
machines into kit components.
|
||||||
|
- Mouse hover and keyboard cursor should converge on a single visual
|
||||||
|
state. Bind `hasCursor` from the panel root; have hover handlers
|
||||||
|
update the same root state via `onHovered`.
|
||||||
|
- Property names follow the underlying QML type's convention. Where a
|
||||||
|
clash is unavoidable (`Toggle`/`ToolTip` already define `background`
|
||||||
|
in QQC), the kit uses a `panel*` prefix.
|
||||||
|
|
||||||
|
## Adding a new component
|
||||||
|
|
||||||
|
1. Write the QML file in this directory.
|
||||||
|
2. Add a line to `qmldir`: `TypeName 1.0 TypeName.qml`.
|
||||||
|
3. Add a live demo section to `plugins/dev-gallery/GalleryPanel.qml`
|
||||||
|
using the real component — copy/paste reimplementations defeat the
|
||||||
|
smoke-test value of the gallery.
|
||||||
|
4. Update the table above.
|
||||||
@@ -21,7 +21,7 @@ Item {
|
|||||||
function open(payloadJson) {
|
function open(payloadJson) {
|
||||||
closingFromHost = false
|
closingFromHost = false
|
||||||
window.visible = true
|
window.visible = true
|
||||||
Qt.callLater(function() { if (focusSink) focusSink.forceActiveFocus() })
|
Qt.callLater(function() { if (scrollArea) scrollArea.forceActiveFocus() })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-initiated close (`shell hide`). Visibility flips without
|
// Host-initiated close (`shell hide`). Visibility flips without
|
||||||
@@ -65,6 +65,9 @@ Item {
|
|||||||
|
|
||||||
// ---- cursor demo state --------------------------------------------------
|
// ---- cursor demo state --------------------------------------------------
|
||||||
property int cursorDemoIndex: 1
|
property int cursorDemoIndex: 1
|
||||||
|
property int pillDemoIndex: 1
|
||||||
|
property string choiceDemoValue: "top"
|
||||||
|
property bool toggleDemoOn: true
|
||||||
|
|
||||||
FloatingWindow {
|
FloatingWindow {
|
||||||
id: window
|
id: window
|
||||||
@@ -83,21 +86,20 @@ Item {
|
|||||||
id: focusScope
|
id: focusScope
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
// Esc + h/l for the cursor demo. Other keys (arrow keys, Page_Down,
|
||||||
Item {
|
// Home/End) propagate down to ScrollView's built-in scroll handling so
|
||||||
id: focusSink
|
// keyboard scrolling works. AfterItem priority means a focused inner
|
||||||
width: 1; height: 1
|
// control would get its keys first — we don't have any here yet.
|
||||||
focus: true
|
Keys.priority: Keys.AfterItem
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
if (event.key === Qt.Key_Escape) { root.requestClose(); event.accepted = true }
|
if (event.key === Qt.Key_Escape) {
|
||||||
if (event.key === Qt.Key_Right || event.text === "l") {
|
root.requestClose(); event.accepted = true
|
||||||
root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1)
|
} else if (event.text === "l") {
|
||||||
event.accepted = true
|
root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1)
|
||||||
}
|
event.accepted = true
|
||||||
if (event.key === Qt.Key_Left || event.text === "h") {
|
} else if (event.text === "h") {
|
||||||
root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1)
|
root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1)
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ Item {
|
|||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: "Live previews of every type exported from qs.Ui. Use this as the visual reference when porting panels or building plugins. Press h/l to walk the cursor demo, Esc to close."
|
text: "Live previews of every type exported from qs.Ui. Use this as the visual reference when porting panels or building plugins. Scroll for more. Esc to close."
|
||||||
color: Qt.darker(root.foreground, 1.4)
|
color: Qt.darker(root.foreground, 1.4)
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
@@ -396,6 +398,63 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- CursorPill --------------------------------------------------
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "CursorPill"
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "PillButton with panel-cursor wiring. Bind hasCursor to your cursor state and onHovered to update it on mouse enter; clicks come from PillButton's clicked() signal. Use this for any \"pick one in a row\" UI (wifi DNS pills, bluetooth header actions). Click below or press h/l to walk the demo cursor."
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: cpRow.implicitHeight + 24
|
||||||
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||||
|
radius: 6
|
||||||
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: cpRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 14
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ["DHCP", "Cloudflare", "Google", "Custom"]
|
||||||
|
CursorPill {
|
||||||
|
required property string modelData
|
||||||
|
required property int index
|
||||||
|
text: modelData
|
||||||
|
foreground: root.foreground
|
||||||
|
tooltipBackground: root.background
|
||||||
|
tooltipForeground: root.foreground
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
tooltipText: "Pick " + modelData
|
||||||
|
hasCursor: root.pillDemoIndex === index
|
||||||
|
active: modelData === "Cloudflare"
|
||||||
|
onHovered: function(h) { if (h) root.pillDemoIndex = index }
|
||||||
|
onClicked: root.pillDemoIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- PanelActionButton -------------------------------------------
|
// ---- PanelActionButton -------------------------------------------
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -587,6 +646,92 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- ChoiceButton --------------------------------------------------
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "ChoiceButton"
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "A single button in a mutually-exclusive choice group. Selected styling uses the accent fill+border; focus styling uses the Style.focusBorderColor outline so keyboard nav can land on a non-selected option without it reading as the chosen one."
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: choiceRow.implicitHeight + 24
|
||||||
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||||
|
radius: 6
|
||||||
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: choiceRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 14
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ["top", "right", "bottom", "left"]
|
||||||
|
ChoiceButton {
|
||||||
|
required property string modelData
|
||||||
|
text: modelData
|
||||||
|
foreground: root.foreground
|
||||||
|
background: root.background
|
||||||
|
accent: root.accent
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
selected: root.choiceDemoValue === modelData
|
||||||
|
onClicked: root.choiceDemoValue = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Toggle --------------------------------------------------------
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Toggle"
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. Same focus tokens as ChoiceButton."
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle {
|
||||||
|
width: parent.width
|
||||||
|
label: "Transparent bar"
|
||||||
|
description: "Hide the bar background so the wallpaper shows through."
|
||||||
|
foreground: root.foreground
|
||||||
|
accent: root.accent
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
checked: root.toggleDemoOn
|
||||||
|
onClicked: root.toggleDemoOn = !root.toggleDemoOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Composed example -------------------------------------------
|
// ---- Composed example -------------------------------------------
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
Reference in New Issue
Block a user