diff --git a/default/quickshell/omarchy-shell/Ui/README.md b/default/quickshell/omarchy-shell/Ui/README.md new file mode 100644 index 00000000..52edbc3e --- /dev/null +++ b/default/quickshell/omarchy-shell/Ui/README.md @@ -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 ` 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. diff --git a/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml b/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml index 7abdace8..a541f1af 100644 --- a/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml @@ -21,7 +21,7 @@ Item { function open(payloadJson) { closingFromHost = false 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 @@ -65,6 +65,9 @@ Item { // ---- cursor demo state -------------------------------------------------- property int cursorDemoIndex: 1 + property int pillDemoIndex: 1 + property string choiceDemoValue: "top" + property bool toggleDemoOn: true FloatingWindow { id: window @@ -83,21 +86,20 @@ Item { id: focusScope anchors.fill: parent focus: true - - Item { - id: focusSink - width: 1; height: 1 - focus: true - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Escape) { root.requestClose(); event.accepted = true } - if (event.key === Qt.Key_Right || event.text === "l") { - root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1) - event.accepted = true - } - if (event.key === Qt.Key_Left || event.text === "h") { - root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1) - event.accepted = true - } + // Esc + h/l for the cursor demo. Other keys (arrow keys, Page_Down, + // Home/End) propagate down to ScrollView's built-in scroll handling so + // keyboard scrolling works. AfterItem priority means a focused inner + // control would get its keys first — we don't have any here yet. + Keys.priority: Keys.AfterItem + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + root.requestClose(); event.accepted = true + } else if (event.text === "l") { + root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1) + event.accepted = true + } else if (event.text === "h") { + root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1) + event.accepted = true } } @@ -125,7 +127,7 @@ Item { font.bold: true } 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) font.family: root.fontFamily 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 ------------------------------------------- Column { 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 ------------------------------------------- Column { width: parent.width