updated design

This commit is contained in:
2026-08-11 16:34:09 +02:00
parent 3e02c3f36a
commit 39f925cb58
17 changed files with 859 additions and 19 deletions
+2
View File
@@ -18,6 +18,8 @@
!brave-flags.conf !brave-flags.conf
!chromium-flags.conf !chromium-flags.conf
!README.md !README.md
!plans/
!DESIGN-IS-2026-08-11/
!rice.png !rice.png
!rice-2.png !rice-2.png
!rice-3.png !rice-3.png
+36
View File
@@ -0,0 +1,36 @@
# Takemi shell design audit — scope
## Audited artifact
- Running Persona 5-inspired desktop shell shown in the two supplied 1920×1080 screenshots.
- Quickshell implementation under `quickshell/`, including the top bar, right rail,
pop-outs, tray menus, power overlay, notifications/OSD, and lock screen.
- Shared design tokens and primitives in `quickshell/config/` and
`quickshell/components/`.
## User and task
- Primary user: the workstation owner using the shell throughout the day.
- Primary task: identify system state and reach common desktop controls quickly,
without the theme obscuring application content.
## Constraints
- Preserve the Persona 5 / Tae Takemi identity.
- Preserve the crimson, black, and white palette and screen-print character.
- Stack remains Quickshell, Hyprland, and Fluxo.
- The result must work at desktop scale, with keyboard and pointer input, and
remain legible during long sessions.
## References
- Persona 5 interface language: asymmetric editorial composition, hard ink,
decisive silhouettes, halftone texture, and rapid card-like motion.
- Current implementation and supplied screenshots are the concrete baseline;
fidelity to the game is subordinate to daily desktop usefulness.
## Out of scope
- Application-level themes outside the shell.
- Replacing the wallpaper or character artwork.
- Changing the desktop stack.
+114
View File
@@ -0,0 +1,114 @@
# Evidence
## 1. Innovative
- The shell applies one custom QML geometry and print system across panels,
backdrops, motion, and textures (`quickshell/config/Theme.qml:118-153`,
`quickshell/components/P5Panel.qml:7-56`).
- This refreshes standard desktop bars/popovers, but the interaction model is
still conventional bar + rail + popup + modal.
## 2. Useful
- The permanent bar exposes workspaces, active window, media, resources, and
time in one slab (`quickshell/bar/Bar.qml:67-122`).
- Power and lock are keyboard-operable; bar, rail, tray, and most pop-out
actions remain pointer-only (`quickshell/overlays/PowerMenu.qml:242-261`,
`quickshell/lock/LockFace.qml:290-311`,
`quickshell/bar/RailIcon.qml:104-124`).
- The primary surfaces contain at least 41 interactive targets; the live total
grows with workspaces, devices, streams, notifications, and tray entries.
## 3. Aesthetic
- A clear system exists: spacing `[6,12,20,32]`, type
`[9,11,12,15,20,34,92]`, 12 literal palette colors, one nominal 14° angle,
and shared 10px cuts (`quickshell/config/Theme.qml:19-49,92-148`).
- Runtime type exceptions include 8, 30, 42, and 156.4px; local geometry also
introduces several non-token cuts and leans.
- White on crimson is 3.68:1 and fails normal-text AA where selected destructive
actions or unread badges use that pairing
(`quickshell/components/P5Button.qml:32-73`,
`quickshell/bar/NotifButton.qml:38-58`).
- Screenshot #1: permanent chrome is visually continuous and disciplined, but
competes with an already high-contrast Persona wallpaper/frame.
- Screenshot #2: the left headline/right action-stack composition is strong;
the repeated stripe field and still-legible desktop behind it dilute focus.
## 4. Understandable
- The power screen labels all actions and prints its keyboard map
(`quickshell/overlays/PowerMenu.qml:31-38,216-261`).
- Rail icons expose different left/right/middle/scroll behaviors without visible
instruction (`quickshell/bar/Rail.qml:76-132`).
- Several state labels behave as inverse actions: “Wi-Fi On,” Bluetooth “On,”
“Scanning,” and “Silenced” toggle those states off.
- “WHO GOES THERE,” “WRONG,” “HELD,” “PROCS,” “VRAM,” and unlabeled load values
favor voice or jargon over immediate comprehension.
## 5. Unobtrusive
- Normal chrome consumes only a 40px top bar and 48px right rail
(`quickshell/config/Theme.qml:109-116`).
- The full-screen modes add stripes, a wedge, oversized split text, attribution,
six slabs, and a hint simultaneously
(`quickshell/overlays/PowerMenu.qml:50-228`).
- Twelve action identities are repeated between permanent chrome and detail
panels; repetition improves reach but adds competing affordances.
## 6. Honest
- No marketing inflation or dark patterns were found. Power labels map directly
to their system action (`quickshell/overlays/PowerMenu.qml:31-38,230-239`).
- “n HELD” counts all history rather than suppressed items
(`quickshell/popouts/NotifPopout.qml:27`,
`quickshell/services/Notifs.qml:36-44`).
- Muted OSDs display 0% even when stored volume remains nonzero
(`quickshell/overlays/Osd.qml:51-64,172-185`).
- A visually disabled slider can still emit changes
(`quickshell/components/P5Slider.qml:9-12,48-84`).
## 7. Long-lasting
- The system is based on reusable primitives rather than per-screen decoration.
- Literal `P5` branding, the quote attribution, and the esports-like continuous
frame tie the result closely to one entertainment reference and visual era
(`quickshell/bar/Sigil.qml:31`,
`quickshell/overlays/PowerMenu.qml:88-118`).
## 8. Thorough
- Empty, loading, failure, focus, and disabled states exist.
- A rendered success state is absent; generic buttons lack a disabled state;
focus treatment is concentrated in power and lock; explicit accessible names
and roles total zero (`quickshell/components/P5Button.qml:6-107`).
- The locks disabled state is clear, but `P5Slider` can look disabled while
remaining interactive.
## 9. Environmentally friendly
- The shell has 0 JavaScript bytes, about 259KB of QML, 1.4KB of local texture
assets, no declared HTTP request, and a measured load proxy below two seconds.
- Seven Fluxo reads recur in polling batches; playing media creates 36 idle
animation loops (`quickshell/services/Sys.qml:78-102`,
`quickshell/bar/MediaPill.qml:125-153`).
- No reduced-motion branch exists (`quickshell/config/Theme.qml:139-148`).
## 10. As little design as possible
- The one-slab bar avoids a row of redundant cards
(`quickshell/bar/Bar.qml:8-14,40-63`).
- The audited tree reaches seven declarative levels, contains twelve repeated
action identities, and includes removable atmospheric elements in modal and
lock compositions.
- The permanent surface, transient surface, and cinematic surface currently
share too much of the same visual intensity instead of forming a hierarchy.
## Known gaps
- Dynamic application strings, device counts, and menu depth cannot be fixed
from static source.
- Exact compositor frame time, GPU cost, and live accessibility-tree output
were not available.
- Screenshot evidence covers the normal desktop and power overlay; lock-screen
judgments also use its implementation source.
+43
View File
@@ -0,0 +1,43 @@
# Rams scorecard
1. **Good design is innovative — 2/3**
Evidence: one custom print/geometry system refreshes conventional shell surfaces (`01-evidence.md#1-innovative`).
Justification: the expression is distinctive, but it does not introduce a new desktop interaction pattern.
2. **Good design makes a product useful — 2/3**
Evidence: core state and actions are direct, while most shell controls require a pointer (`01-evidence.md#2-useful`).
Justification: the primary tasks work quickly, but keyboard and discoverability gaps add friction.
3. **Good design is aesthetic — 2/3**
Evidence: the token system is strong, with several type/geometry exceptions and a 3.68:1 text pairing (`01-evidence.md#3-aesthetic`).
Justification: the visual system is unmistakable, but contrast and intensity inconsistencies prevent a fully resolved score.
4. **Good design makes a product understandable — 1/3**
Evidence: multiple rail gestures are undisclosed and several state labels read like actions (`01-evidence.md#4-understandable`).
Justification: more than three controls require prior knowledge or experimentation.
5. **Good design is unobtrusive — 1/3**
Evidence: compact permanent chrome expands into visually competing full-screen decoration (`01-evidence.md#5-unobtrusive`).
Justification: the theme sometimes becomes the figure when the users task should remain the figure.
6. **Good design is honest — 2/3**
Evidence: no dark patterns exist, but held-count, muted-volume, and disabled-slider presentation diverge from behavior (`01-evidence.md#6-honest`).
Justification: the mismatches are minor rather than deceptive, but prevent a one-to-one mapping everywhere.
7. **Good design is long-lasting — 2/3**
Evidence: reusable primitives coexist with literal franchise and gaming-HUD markers (`01-evidence.md#7-long-lasting`).
Justification: the craft will last, while the most literal references may date the surface.
8. **Good design is thorough — 1/3**
Evidence: success, generic disabled, broad focus, and accessibility metadata are missing or rough (`01-evidence.md#8-thorough`).
Justification: three or more important state families remain incomplete.
9. **Good design is environmentally friendly — 1/3**
Evidence: source weight is small, but recurring polling and idle motion have no reduced-motion gate (`01-evidence.md#9-environmentally-friendly`).
Justification: the runtime is lean yet motion is always on when its conditions are met.
10. **Good design is as little design as possible — 1/3**
Evidence: restrained single slabs coexist with repeated affordances and multi-layer modal atmosphere (`01-evidence.md#10-as-little-design-as-possible`).
Justification: at least three decorative or duplicated elements could be removed without breaking a task.
**Total: 15/30**
+30
View File
@@ -0,0 +1,30 @@
# Verdict — REDESIGN
The strict Rams verdict is **REDESIGN (15/30)**: preserve the excellent brand
tokens and working shell primitives, but redesign the hierarchy so permanent,
transient, and cinematic surfaces no longer compete at the same intensity.
## Highest-leverage moves
1. **Principles #4 and #8 — make behavior legible:** give every icon one primary
click action, add concise Persona-styled hover labels, expose secondary actions
inside its pop-out, and add real focus/disabled/accessibility states. Evidence:
`01-evidence.md#4-understandable` and `#8-thorough`.
2. **Principles #5 and #10 — introduce three intensity tiers:** quiet permanent
chrome, expressive transient panels, theatrical full-screen scenes; reserve
stripes, large split type, and strong halftone for the latter two. Evidence:
`01-evidence.md#5-unobtrusive` and `#10-as-little-design-as-possible`.
3. **Principles #3 and #6 — assign semantics by treatment, not red alone:** use
white fill for selection, a red rule/slash for active state, solid red only
for destructive/error, and pulse only for urgency; fix white-on-red small text.
Evidence: `01-evidence.md#3-aesthetic` and `#6-honest`.
4. **Principles #3 and #7 — move the chaos to macro-composition:** keep clean
module geometry, but vary panel silhouette, title placement, and scale by
context instead of repeating the same framed HUD grammar everywhere. Evidence:
`01-evidence.md#3-aesthetic` and `#7-long-lasting`.
5. **Principles #2 and #9 — add a calm mode:** stop idle equalizer/pulse/marquee
motion when reduced motion is requested and reduce polling when a detail panel
is closed. Evidence: `01-evidence.md#2-useful` and `#9-environmentally-friendly`.
This is a hierarchy redesign, not an instruction to discard the palette,
textures, panel engine, services, or current functionality.
+41
View File
@@ -0,0 +1,41 @@
# Planning handoff
```text
/make-plan Redesign the Takemi Persona 5 desktop shell. Current design failed audit at 15/30 with critical gaps in principles #4 understandable, #5 unobtrusive, #8 thorough, #9 environmentally friendly, and #10 as little design as possible.
Verdict paragraph:
> The strict Rams verdict is REDESIGN (15/30): preserve the excellent brand tokens and working shell primitives, but redesign the hierarchy so permanent, transient, and cinematic surfaces no longer compete at the same intensity.
Why redesign and not refine: the total is below the 20-point refine threshold because the same high-intensity visual and interaction grammar spans permanent chrome, transient controls, and full-screen scenes.
Preserve from current design:
- Crimson/black/white palette, type scale foundation, and motion timings in quickshell/config/Theme.qml:19-148.
- Single-slab bar/rail architecture in quickshell/bar/Bar.qml:8-63 and quickshell/bar/Rail.qml:9-64.
- Reusable P5Panel geometry/texture engine in quickshell/components/P5Panel.qml:7-248.
- Existing services and feature coverage; this is an information/design hierarchy change, not a functionality cut.
Discard:
- One-intensity-fits-all use of stripes, halftone, split type, slashes, and repeated framed geometry. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#5-unobtrusive. Caused failure on principles #5 and #10.
- Undisclosed multi-button/multi-gesture behavior on icon-only permanent controls. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#4-understandable. Caused failure on principles #4 and #8.
Top moves:
1. Principles #4 and #8 — make behavior legible: give every icon one primary click action, add concise Persona-styled hover labels, expose secondary actions inside its pop-out, and add real focus/disabled/accessibility states. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#4-understandable and #8-thorough.
2. Principles #5 and #10 — introduce three intensity tiers: quiet permanent chrome, expressive transient panels, theatrical full-screen scenes; reserve stripes, large split type, and strong halftone for the latter two. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#5-unobtrusive and #10-as-little-design-as-possible.
3. Principles #3 and #6 — assign semantics by treatment, not red alone: use white fill for selection, a red rule/slash for active state, solid red only for destructive/error, and pulse only for urgency; fix white-on-red small text. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#3-aesthetic and #6-honest.
4. Principles #3 and #7 — move the chaos to macro-composition: keep clean module geometry, but vary panel silhouette, title placement, and scale by context instead of repeating the same framed HUD grammar everywhere. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#3-aesthetic and #7-long-lasting.
5. Principles #2 and #9 — add a calm mode: stop idle equalizer/pulse/marquee motion when reduced motion is requested and reduce polling when a detail panel is closed. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#2-useful and #9-environmentally-friendly.
Redesign priorities:
1. Principle #4 — every visible control communicates its primary action without memorized mouse-button lore.
2. Principles #5/#10 — visual intensity increases deliberately from chrome to pop-out to modal.
3. Principles #3/#6 — color, fill, outline, pattern, and motion each have one stable semantic job.
Deliverables:
- New information architecture, not derived mechanically from the old layout.
- New labeled primary flow compared side-by-side with the current shell.
- Empty/loading/error/success/focus/disabled state matrix.
- Migration path that preserves existing service behavior and keybinds.
- Cutover criteria based on discoverability, contrast, reduced motion, and visual hierarchy.
Guard against porting the old structure under new styling, running both hierarchies indefinitely, following generic gaming-HUD trends, or discarding the preserved brand tokens and primitives.
```
+1 -1
View File
@@ -1 +1 @@
monitor=eDP-1, disable
+1 -1
View File
@@ -14,7 +14,7 @@ bindr = ALT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh toggle
bindr = ALT SHIFT, c, exec, ~/.config/hypr/scripts/droidcam-ctrl.sh toggle bindr = ALT SHIFT, c, exec, ~/.config/hypr/scripts/droidcam-ctrl.sh toggle
# general binds # general binds
bind = $mainMod, M, exec, pdfs-prompt bind = $mainMod, M, exec, pdfs-prompt --dmenu --menu 'fuzzel --dmenu --width 60'
bind = $mainMod, B, exec, fluxo bt menu bind = $mainMod, B, exec, fluxo bt menu
bind = $mainMod, RETURN, exec, $terminal bind = $mainMod, RETURN, exec, $terminal
bind = $mainMod SHIFT, Q, killactive, bind = $mainMod SHIFT, Q, killactive,
+503
View File
@@ -0,0 +1,503 @@
# Takemi design hierarchy redesign
## Outcome
Turn the existing Persona 5 shell into a three-level visual system:
1. **Chrome** — quiet, glanceable, and subordinate to applications.
2. **Transient** — expressive pop-outs with distinct compositions and obvious controls.
3. **Cinematic** — theatrical power and lock scenes with focused hierarchy.
Preserve the crimson/black/white palette, services, keybinds, one-slab bar/rail,
`P5Panel`, session-lock security, and current functional coverage. This plan changes
visual hierarchy and interaction clarity, not the desktop stack.
## Non-goals
- Do not replace Quickshell, Hyprland, Fluxo, PAM, or the service layer.
- Do not add new accent hues or generic glass/blur cards.
- Do not reproduce Persona 5 screens literally; keep this a usable desktop shell.
- Do not build a second “control center” that duplicates every existing pop-out.
- Do not rewrite tray-menu recursion or the popup coordinator unless verification
exposes a concrete defect.
## Definition of done
- Permanent chrome remains readable but no longer competes with application content.
- Each primary rail icon has one obvious click action and a concise hover label.
- Selected, active, destructive, error, and urgent states are distinguishable by
treatment as well as color.
- All small text meets WCAG AA; black-on-crimson replaces white-on-crimson where needed.
- Pinned pop-outs and modal controls are keyboard reachable and visibly focused.
- Power actions that end the session require a deliberate confirmation step.
- Lock has idle, editing, busy, failure, and visible success compositions.
- Reduced motion survives process restarts and stops all nonessential loops.
- `qmllint`, live reload, interaction checks, and 1920×1080 plus 1366×768 visual
captures pass without new warnings.
---
## Phase 0 — API and pattern lock
### Allowed APIs
- **Quickshell 0.3.0 `PopupWindow`:** `visible`, `anchor`, `grabFocus`, `screen`.
Configure placement through `anchor.item/window`, `edges`, `gravity`, `margins`,
`adjustment`; call `anchor.updateAnchor()` when a visible trigger moves.
[Official PopupWindow docs](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupWindow/)
and [PopupAnchor docs](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupAnchor/).
- **Popup bounds:** combine `PopupAdjustment.Flip | Slide | Resize` where content
can genuinely resize. Use `QsWindow.mask: Region { item: ... }` for transparent
overshoot bleed only after testing transformed entry animation.
[PopupAdjustment](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupAdjustment/),
[Region](https://quickshell.org/docs/v0.3.0/types/Quickshell/Region/).
- **Pinned dismissal:** current `HyprlandFocusGrab.active/windows/onCleared` is valid
only after a visible window exists (`components/Popout.qml:83-97`). Do not combine
it casually with `PopupWindow.grabFocus`; choose one owner per opening mode.
[HyprlandFocusGrab](https://quickshell.org/docs/v0.3.0/types/Quickshell.Hyprland/HyprlandFocusGrab/).
- **Durable preferences:** `FileView` + `JsonAdapter`, using
`Quickshell.statePath("design-preferences.json")`, `watchChanges`, `reload()`, and
`writeAdapter()`. `PersistentProperties` survives reload only, not restart.
[FileView](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/FileView/),
[JsonAdapter](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/JsonAdapter/).
- **Accessibility:** `Accessible.role/name/description/focusable/focused/pressed`,
`Accessible.onPressAction`, `onIncreaseAction`, and `onDecreaseAction`; decorative
duplicates use `Accessible.ignored: true`.
[Qt Accessible](https://doc.qt.io/qt-6/qml-qtquick-accessible.html).
- **Focus:** `FocusScope`, `focusPolicy: Qt.StrongFocus`, `activeFocusOnTab`,
`KeyNavigation`, `Keys`, and `forceActiveFocus(Qt.PopupFocusReason)`.
[Qt focus model](https://doc.qt.io/qt-6/qtquick-input-focus.html).
- **Disabled controls:** disable the root `Item`; this suppresses pointer and keyboard
input and propagates to children. Explicitly bind `HoverHandler.enabled` when a
disabled control must not react visually.
[Qt Item](https://doc.qt.io/qt-6/qml-qtquick-item.html).
- **System contrast:**
`Application.styleHints.accessibility.contrastPreference === Qt.HighContrast`.
Qt 6.11 has no documented reduced-motion preference, so motion policy is owned by
`Preferences`.
- **Secure lock:** retain `WlSessionLock`/`WlSessionLockSurface` in
`lock/Lock.qml:12-28`; `Overlay + Exclusive` is not a secure replacement.
[WlSessionLock](https://quickshell.org/docs/v0.3.0/types/Quickshell.Wayland/WlSessionLock/).
### Repository patterns to copy
- Copy singleton structure from `config/Theme.qml:1-16` and `services/Actions.qml:1-10`.
- Copy popup show/hide ownership from `components/Popout.qml:58-116,150-190`.
- Copy keyboard roving selection from `overlays/PowerMenu.qml:242-261`.
- Copy development-safe lock preview boundary from `lock/Lock.qml:7-27` and
`lockpreview.qml`; never style by repeatedly locking the real session.
- Copy one-slab chrome composition from `bar/Bar.qml:40-63` and `bar/Rail.qml:43-64`.
### Verification
- Record `qs --version` and Qt version in the implementation PR: expected Quickshell
0.3.0 and Qt 6.11.1.
- Run `qmllint` on each copied API in a minimal changed component before broad rollout.
- Use `qs ipc show` to confirm existing IPC surface before adding typed functions.
### Guards
- No deprecated `PopupWindow.parentWindow`, `relativeX`, or `relativeY`.
- No direct `QsWindow.width/height`; use implicit sizes.
- No `WlrKeyboardFocus.Exclusive` as a lock substitute.
- No stock `QsMenuAnchor` or default Qt tooltip visuals inside the custom theme.
- No claimed system reduced-motion detection; it does not exist in the documented API.
---
## Phase 1 — Establish the semantic design foundation
### 1.1 Durable preferences
Create `quickshell/config/Preferences.qml` using the Phase 0 `FileView`/`JsonAdapter`
pattern. Persist:
- `reducedMotion: false`
- `tooltips: true`
- `highIntensityChrome: false` only as a migration escape hatch; default remains calm
Expose typed setters through the existing `IpcHandler` in
`services/Actions.qml:60-68`, such as `setReducedMotion(enabled: bool): void`. Do not
build a settings UI in this phase.
### 1.2 Three explicit surface presets
- Add `components/ChromeSurface.qml`: a thin `P5Panel` preset with opaque flat fill,
one keyline, no halftone, no drop, and an active slash only when requested.
- Refine `popouts/PopoutSurface.qml:7-23` as the transient preset: halftone becomes
opt-in by composition rather than present on every panel; retain the hard offset
and stronger border.
- Keep `components/P5Backdrop.qml` cinematic-only. Bar, rail, tooltips, cards, and OSD
must not instantiate it.
Do not add a generic numeric “intensity” prop to `P5Panel`; explicit presets make
the hierarchy visible in code and prevent arbitrary intermediate tiers.
### 1.3 Semantic treatments
Document and encode in `config/Theme.qml`:
| State | Fill | Mark | Text | Motion |
|---|---|---|---|---|
| Rest | surface | neutral keyline | white | none |
| Hover | surface | short red wipe | white | one short lunge |
| Focus/selected | white | black keyline | black | snap once |
| Active | surface | persistent red slash | white | none |
| Destructive/error | solid red | black keyline | black | shake only on error |
| Urgent | surface | red pulse mark | white | bounded pulse |
| Disabled | surface | muted outline | muted | none |
Replace small white-on-crimson labels with black-on-crimson; measured contrast improves
from 3.68:1 to 5.70:1. Do not add a new status color.
### 1.4 Base-control contract
- Convert `components/P5Button.qml` to a `FocusScope` or focusable root with one
activation path shared by `TapHandler`, Enter, Space, and
`Accessible.onPressAction`. Add `description`, visible focus state, and a real
disabled state.
- Replace `P5Slider.enabledControl` with root `enabled` and update its call sites.
Gate tap, drag, hover, accessible increase/decrease, and left/right keys through
the same enabled root.
- Add semantic names and roles to `RailIcon`, `StatusIcon`, notification actions,
media transport, calendar navigation, and tray-menu rows. Mark split-text ghosts,
masks, texture layers, and duplicate glyphs ignored.
### Verification
- Focus every base control with Tab and confirm a visible non-color-only state.
- Trigger each through pointer, Enter/Space, and its accessible action; assert one
signal per activation.
- Disable each control and assert no `clicked`/`moved` signal and no hover animation.
- Recompute all foreground/background contrast pairs; normal text must be ≥4.5:1.
- Restart Quickshell and confirm preferences persist.
### Guards
- Do not treat `Accessible` metadata as keyboard support; implement both.
- Do not attach semantics to every decorative child.
- Do not use `MouseArea.enabled` as the component-wide disabled gate.
- Do not spread raw state colors through callers; state treatment belongs in base controls.
---
## Phase 2 — Quiet and clarify permanent chrome
### 2.1 Bar
Update `bar/Bar.qml` and `bar/BarPill.qml` to use `ChromeSurface` and remove all
always-on texture. Keep the single slab and hairline dividers.
- `bar/Clock.qml`: remove permanent seconds; reveal them only inside Calendar.
- `bar/Resources.qml`: render tiny `CPU`, `MEM`, and `GPU` labels beside values so
`2.6G` is not contextless; remove the currently dead `Metric.label` path only after
its visible replacement exists.
- `bar/MediaPill.qml`: keep title/artist but replace the three continuously animated
equalizer bars with one static play-state mark in normal mode. Allow the loop only
when high-intensity chrome is explicitly enabled and reduced motion is off.
- Preserve active-window elision and current collision calculation in
`bar/Bar.qml:90-96`.
### 2.2 Rail interaction grammar
Make left click mean **open/pin details** for network, Bluetooth, audio, battery,
notifications, and power. Remove undisclosed right/middle-click state changes from
`bar/Rail.qml:76-132,151-171`; move those actions into their corresponding pop-outs.
Retain scrolling only for volume, microphone, and brightness because it directly
maps to a continuous readout. Mention that gesture in the tooltip.
### 2.3 Persona tooltip
Add `components/P5Tooltip.qml` as a small anchored `PopupWindow`, not a stock Qt
tooltip. Copy anchor/delay/hide behavior from `components/Popout.qml`, but use:
- calm `ChromeSurface`
- 350450ms delay
- one-line name + current state
- optional second micro-line such as `SCROLL TO ADJUST`
- no focus grab, no halftone, no scale loop
- `PopupAdjustment.Flip | Slide`
Add `label` and `hint` to `RailIcon`; tooltips honor `Preferences.tooltips` and never
become the only source of an accessible name.
### 2.4 Pinned keyboard opening
Add explicit `openHover()` and `openPinned()` paths to `components/Popout.qml`.
`openPinned()` must set its focus mode before mapping; if a hover popup is already
visible, close and remap rather than assuming a live `grabFocus` change works. Start
with Audio as the vertical slice. Choose either `PopupWindow.grabFocus` or the current
`HyprlandFocusGrab` for pinned dismissal after runtime comparison; never leave both
owning the same close event.
### Verification
- Screenshot clean desktop against both a quiet and high-contrast wallpaper.
- First-time test: identify every rail icon and predict left click from tooltip.
- Verify no secondary pointer action remains unless it is printed in the tooltip.
- Open Audio by hover and click: hover must remain passive; pinned mode must accept
keyboard focus, Esc-close, and outside-click close exactly once.
- Move/rescale the rail while a popup is open and call `anchor.updateAnchor()` from
the actual geometry-change path if it drifts.
### Guards
- Do not reintroduce separate floating cards into bar or rail.
- Do not increase bar/rail footprint.
- Do not put stripes, halftone, or large display text in permanent chrome.
- Do not make a tooltip interactive.
---
## Phase 3 — Give transient surfaces distinct macro-compositions
Retain shared popup lifecycle, width discipline, coordinator, header baseline, and
body controls. Redesign one family at a time; capture and approve each family before
moving on so a bad motif is not copied eight times.
### 3.1 Control pop-outs
- **Audio (`AudioPopout.qml`)** — make current volume the headline number; output and
microphone form two clear bands; applications become a restrained mixer list.
Put Mute and `Open audio mixer` inside the panel.
- **Network (`NetPopout.qml`)** — dossier composition: current connection/IP at top,
download/upload fully named, network list as clipped paper rows. Use action labels
`Turn Wi-Fi on/off` and `Network settings`.
- **Bluetooth (`BtPopout.qml`)** — share list-row mechanics with Network but not its
exact silhouette. Use action copy `Turn on/off`, `Start/stop scanning`; pair/connect
status must not rely on color alone.
- **Battery (`BatteryPopout.qml`)** — oversized percentage crossing one edge, with
`Plugged in`/`On battery`, remaining time, draw, and health ordered underneath.
Keep Suspend as the one explicit action.
### 3.2 Information pop-outs
- **Media (`MediaPopout.qml`)** — let album art define one side of the silhouette;
keep metadata in a solid legibility zone; label transport actions accessibly.
- **Calendar (`CalendarPopout.qml`)** — large selected day/month as the composition,
grid subordinate; move seconds here from the permanent bar; label previous/next.
- **Notifications (`NotifPopout.qml`, `NotifCard.qml`)** — overlapping-paper rhythm
only at the list level, not on every inner action. Rename `n HELD` to `n IN HISTORY`;
make DND copy an action (`Silence notifications` / `Resume notifications`).
- **System (`SysPopout.qml`)** — treat it as a diagnostic poster: four primary metrics
first, hardware detail second. Expand `PROCS`, `MEM`, `VRAM`, `Root`, and label load
averages `1 / 5 / 15 min`.
### 3.3 Shared transient rules
- `PopoutHeader` supplies alignment and focus traversal, not the full silhouette.
- Only one dominant numeral/image/headline per panel.
- Maximum one halftone region and one red slash per panel.
- Lists use consistent 3240px targets and explicit selected/focused/disabled states.
- Empty and loading layouts preserve the panel silhouette instead of collapsing.
- Fix muted OSD honesty: show stored percentage beside `MUTED`, or omit percentage;
never display a fabricated 0%.
### Verification
- Capture all eight pop-outs at rest plus empty/loading/error/disabled states where applicable.
- Test with longest realistic SSID, device, track, app, and notification strings.
- Verify list focus order, Enter/Space activation, Esc close, and disabled-row skipping.
- Ensure only one transient window stays open through a rapid rail sweep.
- Test popup bounds at every screen edge and both target resolutions.
### Guards
- Do not create eight copies of the same card grid.
- Do not vary edge angle randomly inside one surface.
- Do not use display italic for body copy or dense device lists.
- Do not put action wording in state form (`Wi-Fi On`) when clicking performs the inverse.
---
## Phase 4 — Focus the cinematic scenes
### 4.1 Power menu
In `overlays/PowerMenu.qml`:
- Keep the left headline/right action-stack asymmetry.
- Increase backdrop opacity enough that application text no longer competes.
- Use one stripe band or wedge, not a full-screen stripe field plus every other motif.
- Separate Lock/Suspend/Hibernate from Log Out/Reboot/Shut Down with a labeled break
and greater spacing.
- Keep white fill/black text for normal selection. Use solid crimson/black text for
destructive selection.
- Add `pendingAction`: the first activation of Log Out/Reboot/Shut Down replaces the
action region with `CONFIRM LOG OUT`, `CONFIRM REBOOT`, or `CONFIRM SHUT DOWN`
plus Cancel; second activation executes.
Escape clears confirmation first and closes the menu only when no action is pending.
- Keep roving Up/Down/J/K navigation and update focus order for confirmation.
### 4.2 Lock screen
Refactor `lock/LockFace.qml` into explicit visual states while retaining the single
PAM context and `WlSessionLock` shell:
- **Idle:** clock is dominant; status is quiet; auth field is present but subordinate.
- **Editing:** first user edit shifts emphasis to auth and dims/moves clock/status.
- **Busy:** `CHECKING…`, disabled input, bounded sweep.
- **Failure:** `PASSWORD INCORRECT` or PAM message, solid error treatment, one shake.
- **Success:** paint a short `ACCESS GRANTED` state before unlocking; skip delay under
reduced motion.
Replace `WHO GOES THERE`, `WRONG`, and `PAM ERROR` with explicit copy. Resolve the
username safely from `USER` then `LOGNAME`; if both are empty, do not call PAM with a
literal placeholder—show `AUTHENTICATION USER UNAVAILABLE`.
Keep responsive scaling, per-monitor wallpaper, and `lockpreview.qml`. Ensure the
password input exposes `Accessible.EditableText`, name `Password`, `editable: true`,
and `passwordEdit: true`.
### Verification
- Power: pointer and keyboard through safe action, destructive confirmation, cancel,
outside click, and repeated open/close.
- Lock preview: capture all five states at 1920×1080 and 1366×768.
- Real lock test only after preview and PAM-path review; test wrong password, correct
password, empty username environment simulation, and Quickshell reload behavior.
- Confirm `WlSessionLock.secure` path remains unchanged.
### Guards
- Never execute destructive action on the first activation.
- Never fake authentication success in preview or bypass PAM.
- Never replace session lock with a focus-exclusive overlay.
- Do not let theatrical copy obscure what the user must do.
---
## Phase 5 — Motion, contrast, and runtime calm
### 5.1 Motion policy
In `Theme.qml`, derive `motionScale` from `Preferences.reducedMotion`. Apply it to:
- every `Behavior`
- popup enter/exit sequences
- media equalizer and marquee loops
- icon pulses
- toast countdown/entry
- OSD, power, and lock scene transitions
When reduced motion is on, set final state directly or disable `Behavior`; do not merely
shorten loops to 1ms. Essential progress indicators may remain static with clear text.
### 5.2 High contrast
Read `Application.styleHints.accessibility.contrastPreference`. In high contrast:
- increase keyline/border separation
- disable low-opacity texture behind text
- retain shape/icon differences among selection, active, danger, and urgency
- do not change the three-color identity
### 5.3 Polling cleanup
After visual behavior is stable, move `sysReader` from the 2-second batch to the
20-second batch in `services/Sys.qml:78-102`; uptime/load/process count do not drive
permanent animation. Keep CPU/MEM/GPU at 2 seconds because the bar renders them.
Measure before making further demand-driven polling changes; do not introduce a
consumer-count abstraction without evidence that it materially reduces work.
### Verification
- Restart with reduced motion and verify zero infinite animations via source grep and
live observation; all final states must still appear.
- Toggle high contrast and re-run contrast checks.
- Compare Fluxo process-launch frequency before/after for 60 seconds.
- Confirm notification timing, popup dismissal, and lock success still complete when
animation is disabled.
### Guards
- Do not claim to follow an OS reduced-motion setting.
- Do not gate functional timers such as notification lifetime or polling behind motion.
- Do not add blur, gradients, or extra texture to compensate for removed motion.
---
## Phase 6 — Integrated verification and cutover
### Automated checks
1. `qmllint -I quickshell` on every changed QML file individually; the current tool can
exit 255 without diagnostics on some recursive/singleton batches, so record files
and results rather than treating one giant invocation as authoritative.
2. `git diff --check`.
3. Grep for:
- white small text on solid accent
- `enabledControl`
- pointer handlers without an accessible name on shared controls
- infinite animations not gated by reduced motion
- deprecated popup properties
- old copy: `WHO GOES THERE|PAM error|HELD|PROCS|VRAM`
4. Reload through `qs ipc call shell reloadConfig` and inspect `qs log`.
### Interaction matrix
Test each bar/rail/pop-out action with pointer, keyboard where focusable, outside-click,
Esc, disabled state, and service unavailable. Specifically test tray menus with nested
submenus, Wi-Fi off, no Bluetooth adapter, no battery, no player, no notifications,
and missing album art.
### Visual matrix
Capture:
- 1920×1080 and 1366×768
- calm and high-contrast wallpaper
- default, reduced-motion, and high-contrast preferences
- desktop, every pop-out, tray menu, OSD, toast, power normal/confirm, and all lock states
Compare against these invariants:
- Chrome has no texture and never dominates content.
- Transient panels each have one dominant composition.
- Cinematic scenes retain the strongest typography and atmosphere.
- Nothing important is encoded by color alone.
- No text clips, crosses a cut edge, or falls below contrast requirements.
### Cutover sequence
Implement in six reviewable commits matching Phases 16. Do not delete the old visual
path until its phase passes screenshots and interaction tests. Remove migration aliases
(`enabledControl`, high-intensity escape hatch) only in the final cleanup commit after
all call sites and preferences have migrated.
## Expected file map
### New
- `quickshell/config/Preferences.qml`
- `quickshell/components/ChromeSurface.qml`
- `quickshell/components/P5Tooltip.qml`
### Foundation edits
- `quickshell/config/Theme.qml`
- `quickshell/components/P5Panel.qml`
- `quickshell/components/P5Button.qml`
- `quickshell/components/P5Slider.qml`
- `quickshell/components/Popout.qml`
- `quickshell/components/Icon.qml`
- `quickshell/components/Marquee.qml`
- `quickshell/services/Actions.qml`
### Surface edits
- `quickshell/bar/{Bar,BarPill,Rail,RailIcon,StatusIcon,Clock,Resources,MediaPill,Tray}.qml`
- `quickshell/popouts/{PopoutSurface,PopoutHeader,AudioPopout,NetPopout,BtPopout,BatteryPopout,MediaPopout,CalendarPopout,NotifPopout,SysPopout,TrayMenuList}.qml`
- `quickshell/components/NotifCard.qml`
- `quickshell/overlays/{Osd,Toasts,PowerMenu}.qml`
- `quickshell/lock/LockFace.qml`
- `quickshell/services/Sys.qml`
## Recommended execution order
Execute Phase 1 first and review its state matrix before any visual restyling. Then use
Audio as the full vertical slice through Phases 23. Once its tooltip, pinned focus,
semantic states, reduced motion, and transient composition pass, repeat that proven
pattern across the remaining surfaces. Leave real session-lock testing until the lock
preview and power confirmation work are complete.
+7 -1
View File
@@ -80,7 +80,13 @@ contentPad * 2`), which keeps the shape maths out of binding loops.
| CPU/MEM/GPU | bar | btop | | | | resource dashboard | | CPU/MEM/GPU | bar | btop | | | | resource dashboard |
Popouts open on a short dwell and stay while the pointer is over the trigger or Popouts open on a short dwell and stay while the pointer is over the trigger or
the panel. Clicking pins one open. the panel. Clicking pins one open. A shell-wide coordinator keeps only one
transient surface mapped at a time, including tray context menus, so sweeping
across the rail cannot stack several animated panels over one another.
The power menu is fully keyboard-driven as well as clickable: `Up`/`Down` or
`J`/`K` moves the active slab, `Enter`/`Space` confirms it, and `Escape`
dismisses the screen.
## Keybinds ## Keybinds
+2 -1
View File
@@ -11,13 +11,14 @@ Item {
property color accent: Theme.primary property color accent: Theme.primary
property color hoverFill: Theme.accent property color hoverFill: Theme.accent
property bool destructive: false property bool destructive: false
property bool selected: false
property real lean: Theme.skew property real lean: Theme.skew
property real lunge: 6 property real lunge: 6
property bool wide: false property bool wide: false
signal clicked() signal clicked()
readonly property bool hovered: hover.hovered readonly property bool hovered: hover.hovered || root.selected
readonly property bool pressed: tap.pressed readonly property bool pressed: tap.pressed
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2) implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
+2
View File
@@ -103,6 +103,7 @@ PopupWindow {
} }
function open() { function open() {
PopupCoordinator.claim(root);
exitAnim.stop(); exitAnim.stop();
shown = true; shown = true;
enterAnim.restart(); enterAnim.restart();
@@ -183,6 +184,7 @@ PopupWindow {
root.pinned = false; root.pinned = false;
contentHolder.x = root.bleed; contentHolder.x = root.bleed;
contentHolder.y = root.bleed; contentHolder.y = root.bleed;
PopupCoordinator.release(root);
root.dismissedFully(); root.dismissedFully();
} }
} }
+27
View File
@@ -0,0 +1,27 @@
pragma Singleton
import QtQuick
import Quickshell
// One transient surface at a time. Without a shared owner, a quick sweep over
// the bar/rail can leave several independently animated windows mapped on top
// of each other. Every coordinated surface exposes dismiss().
Singleton {
property var active: null
function claim(surface: var) {
if (active && active !== surface)
active.dismiss();
active = surface;
}
function release(surface: var) {
if (active === surface)
active = null;
}
function dismissAll() {
if (active)
active.dismiss();
}
}
+11 -5
View File
@@ -20,6 +20,8 @@ Item {
property string status: "" property string status: ""
property bool failed: false property bool failed: false
property bool busy: pam.active property bool busy: pam.active
readonly property real uiScale: Math.max(0.72, Math.min(1.15,
Math.min(width / 1920, height / 1080)))
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -137,7 +139,9 @@ Item {
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: parent.height * 0.2 anchors.topMargin: parent.height * 0.2
spacing: Theme.padM spacing: Theme.padM
width: 300 width: Math.min(300, parent.width * 0.28)
scale: surface.uiScale
transformOrigin: Item.TopRight
RailRow { RailRow {
width: parent.width width: parent.width
@@ -181,8 +185,10 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: parent.height * 0.14 anchors.bottomMargin: parent.height * 0.14
width: 460 width: Math.min(460, parent.width - Theme.padXL * 2)
height: 150 height: 150
scale: surface.uiScale
transformOrigin: Item.Bottom
transform: Translate { id: shakeShift } transform: Translate { id: shakeShift }
@@ -206,7 +212,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prompt.bottom anchors.top: prompt.bottom
anchors.topMargin: Theme.padL anchors.topMargin: Theme.padL
width: 380 width: Math.min(380, authBlock.width - Theme.padL * 2)
height: 56 height: 56
lean: 0.12 lean: 0.12
chop: 16 chop: 16
@@ -320,7 +326,7 @@ Item {
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Quickshell.env("USER") || "narl" text: Quickshell.env("USER") || "USER"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsSmall
@@ -349,7 +355,7 @@ Item {
id: pam id: pam
config: "hyprlock" config: "hyprlock"
user: Quickshell.env("USER") || "narl" user: Quickshell.env("USER") || "USER"
onPamMessage: { onPamMessage: {
if (pam.responseRequired) pam.respond(surface.entry); if (pam.responseRequired) pam.respond(surface.entry);
+31 -9
View File
@@ -26,9 +26,21 @@ PanelWindow {
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen readonly property bool open: Actions.powerMenuOpen
property int selectedAction: 0
readonly property var actionModel: [
{ label: "Lock", icon: "󰌾", danger: false, act: "lock" },
{ label: "Suspend", icon: "󰤄", danger: false, act: "suspend" },
{ label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" },
{ label: "Log Out", icon: "󰗽", danger: true, act: "logout" },
{ label: "Reboot", icon: "󰜉", danger: true, act: "reboot" },
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
]
onOpenChanged: { onOpenChanged: {
if (open) { if (open) {
PopupCoordinator.dismissAll();
selectedAction = 0;
openAnim.restart(); openAnim.restart();
} else { } else {
closeAnim.restart(); closeAnim.restart();
@@ -122,14 +134,7 @@ PanelWindow {
Repeater { Repeater {
id: buttonRepeater id: buttonRepeater
model: [ model: root.actionModel
{ label: "Lock", icon: "󰌾", danger: false, act: "lock" },
{ label: "Suspend", icon: "󰤄", danger: false, act: "suspend" },
{ label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" },
{ label: "Log Out", icon: "󰗽", danger: true, act: "logout" },
{ label: "Reboot", icon: "󰜉", danger: true, act: "reboot" },
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
]
// A carrier item owns the staggered slide so the button keeps its // A carrier item owns the staggered slide so the button keeps its
// own hover transform. // own hover transform.
@@ -159,10 +164,12 @@ PanelWindow {
text: entry.modelData.label text: entry.modelData.label
icon: entry.modelData.icon icon: entry.modelData.icon
destructive: entry.modelData.danger destructive: entry.modelData.danger
selected: root.selectedAction === entry.index
accent: entry.modelData.danger ? Theme.accent : Theme.primary accent: entry.modelData.danger ? Theme.accent : Theme.primary
lean: Theme.skew lean: Theme.skew
lunge: -10 lunge: -10
onClicked: root.invoke(entry.modelData.act) onClicked: root.invoke(entry.modelData.act)
onHoveredChanged: if (hovered) root.selectedAction = entry.index
} }
// Staggered entry, one button after another. // Staggered entry, one button after another.
@@ -211,7 +218,7 @@ PanelWindow {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 40 anchors.bottomMargin: 40
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: "ESC TO CANCEL" text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
@@ -237,6 +244,21 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
focus: root.open focus: root.open
Keys.onEscapePressed: Actions.closePowerMenu() Keys.onEscapePressed: Actions.closePowerMenu()
Keys.onPressed: event => {
if (!root.open) return;
if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
root.selectedAction = (root.selectedAction + root.actionModel.length - 1)
% root.actionModel.length;
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
root.selectedAction = (root.selectedAction + 1) % root.actionModel.length;
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|| event.key === Qt.Key_Space) {
root.invoke(root.actionModel[root.selectedAction].act);
event.accepted = true;
}
}
} }
ParallelAnimation { ParallelAnimation {
+7
View File
@@ -53,6 +53,7 @@ PopupWindow {
function open() { function open() {
if (!menuHandle) return; if (!menuHandle) return;
PopupCoordinator.claim(root);
closeAnim.stop(); closeAnim.stop();
shown = true; shown = true;
openAnim.restart(); openAnim.restart();
@@ -68,6 +69,11 @@ PopupWindow {
if (shown) close(); else open(); if (shown) close(); else open();
} }
// Shared name used by PopupCoordinator for every transient surface.
function dismiss() {
close();
}
// The grab can only take hold once the popup is actually mapped. Asked for // The grab can only take hold once the popup is actually mapped. Asked for
// in the same tick as `shown`, Hyprland has no surface to hand it and // in the same tick as `shown`, Hyprland has no surface to hand it and
// clears it immediately — which closed the menu the instant it opened. // clears it immediately — which closed the menu the instant it opened.
@@ -145,6 +151,7 @@ PopupWindow {
onFinished: { onFinished: {
root.shown = false; root.shown = false;
holder.x = root.bleed; holder.x = root.bleed;
PopupCoordinator.release(root);
} }
} }
} }
+1 -1
View File
@@ -188,7 +188,7 @@ Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: row.entry.hasChildren ? 18 : 0 width: row.entry.hasChildren ? 18 : 0
visible: row.entry.hasChildren visible: row.entry.hasChildren
text: row.expanded ? "" : "" text: row.expanded ? "󰅀" : "󰅂"
color: row.hovered ? Theme.ink : Theme.subtext color: row.hovered ? Theme.ink : Theme.subtext
font.family: Theme.fontIcon font.family: Theme.fontIcon
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro