Commit Graph
74 Commits
Author SHA1 Message Date
David Heinemeier Hansson 3eda4948b2 Incorporate outfoxxed feedback 2026-05-16 13:28:37 +02:00
David Heinemeier Hansson 2aa3cae7f7 More menu tweaks 2026-05-16 13:07:31 +02:00
David Heinemeier Hansson 80ab790879 Similar fade on all menus 2026-05-16 12:58:39 +02:00
David Heinemeier Hansson 372f582b3d Loop menu 2026-05-16 12:49:53 +02:00
David Heinemeier Hansson 43a8406d44 We don't need action icons 2026-05-16 12:48:43 +02:00
David Heinemeier Hansson 5de0c40a7e Try smaller toggles 2026-05-16 12:45:35 +02:00
David Heinemeier Hansson c751d0cac9 Pull theme and background into quick settings 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson 0e446f70f2 Use pointer hand for click target 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson 7e9528b7a3 No need for this either 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson 525d2855ca No need for this tool tip 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson ecd9dc6ffe Furter notification tweaks 2026-05-16 10:05:30 +02:00
Ryan HughesandGitHub 1513adb339 Merge pull request #5860 from jweaver60/settings-widget-styling
Restyle Quickshell settings inputs to match Omarchy aesthetic
2026-05-16 00:45:47 -04:00
Ryan Hughes fd36836eec Narrow weather flyout and center forecast row 2026-05-16 00:41:41 -04:00
Ryan Hughes f404a81c6a Consolidate omarchy-shell colors into a single shell.toml surface
Five plugins each owned their own colors.toml watcher with subtly
different parsers, plus two ad-hoc per-theme override files
(notifications.json, image-picker-colors.json). This collapses all of
that into one source of truth: Commons/Color.qml watches colors.toml +
shell.toml and exposes Color.bar.*, Color.popups.*, Color.notifications.*,
Color.menu.*, Color.imagePicker.* for every surface to bind to.

shell.toml is generated by the existing template pipeline from
default/themed/shell.toml.tpl. Themes can ship their own shell.toml to
override individual keys; everything missing falls back to the
foundational palette via root.pick(). Settings panel is intentionally
not themable beyond the foundational tokens.

last-horizon and solitude ship a minimal shell.toml to preserve their
historical 'notification border matches Hyprland active border'
behavior, which previously came from parsing the theme's hyprland.conf
(now removed).
2026-05-16 00:30:27 -04:00
Ryan Hughes 227d32544f Fix battery indicator scale on omarchy-shell bar
Quickshell's UPowerDevice.percentage is a 0.0-1.0 real (energy /
energyCapacity), not 0-100. The bar was treating it as 0-100, so a 99%
charge picked the lowest icon, the tooltip read "1%", and the
low-battery active state was always true.
2026-05-16 00:27:18 -04:00
Ryan HughesandGitHub 620ab05e9d Merge pull request #5838 from jweaver60/weather-flyout-forecast
Add multi-day forecast to weather widget; click-only popup
2026-05-16 00:20:56 -04:00
Jake Weaver ae70db3c53 Restyle Quickshell settings inputs to match Omarchy aesthetic
Settings dialogs were rendering text/spin/combo controls with Qt's
default light style, which clashed with the dark Omarchy theme — black
text on dark fills, white dropdown popups, rounded corners, and a
pill-style toggle that didn't match the native StyleToggleRow switch.

- Style.qml: collapse all radius tokens to 0 so Noctalia-compat widgets
  inherit Omarchy's sharp-corner convention
- NToggle: rewrite to mirror the native 48x22 switch with accent fill
  and a sliding 16x16 thumb
- NComboBox: skin contentItem, indicator, popup, and delegate so the
  dropdown reads on dark themes
- NSpinBox: add a styled background, contentItem, and up/down indicators
- SettingsPanel calendar form + DynamicSettingsForm string field: drop
  default TextField chrome for the same translucent foreground fill used
  by WidgetCard, accent border on focus
2026-05-15 16:46:56 -04:00
Jake Weaver 66f902e50c Widen and enlarge weather popup right-side details
Bump location and stat label/value font sizes a couple pixels and add
breathing room between the FEELS/WIND/HUMIDITY columns.
2026-05-15 16:02:38 -04:00
Jake Weaver 9c859a4c9b Reorganize weather popup hero layout
Move the weather icon next to the big temperature on the left and stack
the location and stats columns on the right, matching the new mockup.
2026-05-15 15:55:14 -04:00
David Heinemeier Hansson b98d3833f8 No zoom effect on clicks 2026-05-15 21:23:35 +02:00
David Heinemeier Hansson 692b291678 Workspaces without windows are greyed out 2026-05-15 21:17:43 +02:00
Jake Weaver 8eb91cd06b Enlarge hero icon and tighten popup spacing 2026-05-15 15:14:21 -04:00
David Heinemeier Hansson e8fb83de11 Improve notification look 2026-05-15 20:58:09 +02:00
Jake Weaver 15c7cbfc91 Redesign weather popup and auto-refresh forecast
Replace the inline detail flyout with a two-column hero: location chip
and large temperature on the left, big condition icon on the right.
Stats (Feels / Wind / Humidity) move below as caps-labeled columns,
separated from the forecast by a thin divider. Forecast cells now lead
with the day icon and put the day name above hi/lo temps.

Drop the Refresh and wttr.in pill buttons; a Timer refreshes the
forecast every refreshMinutes (default 15). The interval is exposed
through the Settings panel — Bar -> Weather has a gear that opens a
SpinBox-backed form, persisted as "settings": { "refreshMinutes": N }
in shell.json.
2026-05-15 14:09:26 -04:00
Jake Weaver 1855eeca47 Show multi-day forecast in weather widget and drop hover popup
Replace the hover-driven detail flyout with a click-toggled popup that
includes a Noctalia-style row of upcoming days. Switching the wttr.in
fetch to the j1 JSON gives us structured current conditions plus a
3-day forecast; each day's representative icon comes from the hourly
entry nearest noon, mapped through the same code → glyph table as the
bar pill.
2026-05-15 13:40:53 -04:00
David Heinemeier Hansson ce44f7b3f8 Fix tooltip format 2026-05-15 19:28:42 +02:00
David Heinemeier Hansson 3b523edeca Fix indicator spacing 2026-05-15 19:22:58 +02:00
David Heinemeier Hansson 36edee945c Closer on the weather 2026-05-15 19:20:31 +02:00
David Heinemeier Hansson 6cb880908a More spacing tweaks 2026-05-15 19:14:52 +02:00
David Heinemeier Hansson 52817ec465 Fix spacing 2026-05-15 19:06:14 +02:00
David Heinemeier Hansson d6e8149580 Slim down the quick controls 2026-05-15 18:56:39 +02:00
David Heinemeier Hansson 470503bed3 Prevent flickering on theme/backgrounds selector 2026-05-15 18:48:20 +02:00
David Heinemeier Hansson 421bbb3e79 Improve font rendering 2026-05-15 18:41:25 +02:00
David Heinemeier Hansson 78de3c76d2 Merge branch 'dev' into omarchy-shell 2026-05-15 18:33:09 +02:00
Jake Weaver 182f71a61b Ceil tooltip popup size so right/bottom borders render
Text.implicitWidth/Height can be fractional. Passing those values
straight to the PopupWindow rounded the Wayland surface down, clipping
the 1px right/bottom border of the bubble.
2026-05-15 10:13:39 -04:00
Ryan Hughes 96c5e048cb Drive Wi-Fi widget from iwctl with scrollable network list 2026-05-15 02:40:26 -04:00
Ryan Hughes 679e14c37f Restore menu toggle behavior on keybinds and bar clicks
The old bash bin short-circuited with close_visible_quickshell_menu at
the top of open_quickshell_menu, so pressing any menu keybind while the
menu was open just closed it. The IPC path I introduced summoned every
time, so keyboard shortcuts couldn't dismiss \u2014 only outside clicks did
(which is why the bar icon appeared to toggle: clicking it while the
menu was open hit the menu's outer-click MouseArea, not the icon).

Add a menu.toggle IPC method:
  - if root.opened: cancel() and return "closed"
  - otherwise: behaves exactly like summon

Switch every Hyprland binding and the two bar-icon click handlers
(omarchy logo, battery right-click \u2192 power menu) to toggle. summon stays
as the always-open primitive for callers that want explicit summon
semantics (rare \u2014 right now nothing internal uses it).
2026-05-14 18:04:25 -04:00
Ryan Hughes edfd68d48e Strip the bash-bin compatibility layer from Menu.qml
With bin/omarchy-menu gone there's no caller that summons the menu by
writing a JSON tempfile and polling a done file. Drop everything that
existed only to support that handshake:

  - menuJsonFile / menuJsonFileView / loadMenuJson / openMenu(json,...)
  - pendingSelectionFile / pendingDoneFile / selectionFile / doneFile
  - mergeProviderJson (the JSON-wrapper for provider output)
  - applyProc / releaseProc / doneFilesToRelease / finishDoneFile /
    releaseNextDoneFile / resetRequest / requestActive / closeMenu
  - the menuBin property pointing at the deleted bin

applySelected is now four lines (close + execDetached); cancel is two
(close + clear filter). open() takes either {menu: id} or a bare initial
menu name, and hands off to openExistingMenu which uses the cached items
from the JSONC FileViews.

Net: 91 lines smaller, no perf change.
2026-05-14 17:45:53 -04:00
Ryan Hughes 4a665af581 Drop bin/omarchy-menu; sweep JSONC actions to bare commands
The shell now owns the menu — both data and dispatch. The 821-line bash
bin was carrying the open path (cold spawn + jq pipeline + tempfile dance
+ IPC + poll) and a grab-bag of helpers that only existed inside it. Both
go away in this commit.

New bins for the few composite helpers that were genuinely worth keeping:

  - omarchy-install-app NAME PKG
  - omarchy-install-and-launch NAME PKG DESKTOP_ID
  - omarchy-install-font LABEL PKG FAMILY
  - omarchy-launch-config-editor PATH
  - omarchy-reminder-set-interactive
  - omarchy-capture-screenrecording-with-webcam

Sweep through default/omarchy/omarchy-menu.jsonc rewrites:

  - present_terminal X   -> omarchy-launch-floating-terminal-with-presentation X
  - install_terminal X   -> omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal X'
  - install / install_and_launch / install_font / open_in_editor -> new bins above
  - terminal X           -> xdg-terminal-exec --app-id=org.omarchy.terminal X
  - default_browser_is X -> [[ "$(omarchy-default-browser)" == "X" ]]
  - default_terminal_is X / default_editor_is X / haptic_touchpad_is X same shape
  - $(hypr_config_file X) -> ~/.config/hypr/X.lua
  - show_custom_reminder_input -> omarchy-reminder-set-interactive
  - screenrecord_with_webcam   -> omarchy-capture-screenrecording-with-webcam
  - stop_active_screenrecording -> omarchy-capture-screenrecording --stop-recording

Hyprland bindings switch from `omarchy-menu X` to
`omarchy-shell-ipc menu summon X` (the keybind hot path now skips bash
entirely). The Bar.qml omarchy widget and the battery right-click do the
same. ALT+PRINT becomes a one-liner: stop the recording if one is going,
otherwise summon the screenrecord submenu.

Measured: keybind-to-visible is ~30ms (was ~235ms). The shell's plugin
keepLoaded:true stops being theoretical \u2014 the menu data lives in memory
across opens, and the only work between keypress and paint is the IPC
roundtrip and the layer-shell window mount.
2026-05-14 17:43:12 -04:00
Ryan Hughes a960d2b1bf Move omarchy-menu data and dispatch into the shell
omarchy-menu used to spawn an 821-line bash bin on every keybind: 11 jq
invocations against omarchy-menu.jsonc, mktemp dance, summon, poll. The
shell waited ~235ms (keybind to visible) before painting the first row.

The bar plugin host is already a persistent process with the menu plugin
loaded (keepLoaded: true) — it just wasn't allowed to own the menu data.
Now it does:

  - parseMenuJsonc / normalizeItem ports the bash bin's jq pipeline to
    JS: strip // comments + trailing commas, infer parent from dotted
    ids, dedupe keyword tokens across id/aliases/keywords, fill defaults.
  - Two FileViews with watchChanges: true on the default and user
    extension JSONCs. Edit either file and the in-memory item map
    refreshes without a restart.
  - IpcHandler exposes menu.summon(initialMenu) / refresh() / close() /
    ping(). summon resolves aliases the way the old bash route_target
    did, including the leaf-alias-is-an-action shortcut.
  - Action dispatch is now direct: when summoned via IPC (no selection
    or done file), Quickshell.execDetached(['bash','-lc', action]) runs
    the action and the menu closes. The legacy tempfile dance is kept
    only for the duration of the migration window.
  - provider sub-menus (fonts, power-profiles) are inline: tab-delimited
    bash one-liner per provider, parsed by mergeProviderRows.
  - when:/checked: bash expressions are batched into one Process at
    shell startup (and again whenever the JSONCs change). The menu
    opens instantly; visibility/check marks settle within ~50ms.

Measured: keybind-to-visible via the new IPC path is ~30ms (was ~235ms).
The bash bin still works for now and is removed in the next commit.
2026-05-14 17:39:04 -04:00
Ryan Hughes 85199197af Drive the bar font from fontconfig (drop shell.json round-trip)
omarchy-font-set already updates ~/.config/fontconfig/fonts.conf, which
is what fc-match, Qt, and every other XDG app reads from. Storing the
same family in shell.json gives the same value two homes that can drift
apart, and stops the bar from picking up font changes without a shell
restart.

  - omarchy-font-set stops writing bar.fontFamily to shell.json and stops
    poking ~/.config/waybar/style.css. fontconfig + terminals + hyprlock
    + swayosd are still updated as before.
  - omarchy-font-current reads fc-match monospace -f '%{family}' and
    takes the first comma-separated entry; the old jq-on-shell.json
    path is gone.
  - Bar / SettingsPanel / NDropdown.qml default fontFamily to
    "monospace" and stop reading config.fontFamily. Qt resolves the
    family through fontconfig at paint time, so omarchy-font-set updates
    the bar live with no reload.
  - shell-defaults.json drops bar.fontFamily.

User-visible effect: pick a Nerd Font, the bar follows it. Pick a font
without glyphs and you get tofu — same failure mode the terminals and
hyprlock already had.
2026-05-14 16:34:21 -04:00
Ryan Hughes 2ca15dc767 Remove waybar; relocate bar helper scripts into the shell tree
omarchy-shell's bar plugin replaces waybar on every default path, so the
waybar package, configs, indicator scripts, and toggle/restart/refresh
binaries all go away in this commit.

The four small helper scripts that Bar.qml shells out to (weather.sh and
indicators/*.sh) move with the shell rather than disappear \u2014 they live
at default/quickshell/omarchy-shell/scripts/ now, alongside the rest of
the shell code.

Companion script changes:
  - omarchy-shell-ipc gains --if-running so fire-and-forget refresh
    callers (toggle-idle, toggle-notification-silencing,
    update-available-reset, capture-screenrecording) don't accidentally
    spawn the shell when it isn't already up.
  - omarchy-theme-set, omarchy-tz-select, omarchy-voxtype-{install,model},
    omarchy-toggle-nightlight, omarchy-voxtype-config drop their waybar
    branches and comments.
  - omarchy-voxtype-status's summary stops claiming it's a Waybar helper.
  - omarchy-base.packages drops waybar; install/packaging/fonts.sh's
    comment points at the new bar consumer.
  - SKILL.md / AGENTS.md / omarchy-menu.jsonc lose their waybar entries.
  - test/omarchy-cli-test.sh substitutes a remaining toggle command.
2026-05-14 16:33:20 -04:00
Ryan Hughes 020cb537c7 Dedupe notifications by id and persist history to ~/.local/state
Chat apps (Vesktop, etc.) can rapid-fire the same Notify D-Bus call when
they sync, leaving the pending tab with hundreds of byte-identical rows
because the old handler appended unconditionally and never re-entered the
`markSeenByOriginalId` path for follow-up arrivals.

  - addToPending + popup-insert now remove any existing row matching the
    same `originalId` before insertion, honoring the freedesktop
    "replaces_id" semantics that chat apps rely on.
  - loadHistory dedupes by originalId on hydration, so existing on-disk
    files with the old duplicate-laden schema collapse on next start.
  - loadHistory also guards against re-entry: FileView fires onLoaded
    more than once during startup (initial preload + Component.onCompleted
    reload), and the old version would double the model each fire.

While in here, move the persistent notifications.json out of `~/.cache`
to `~/.local/state/omarchy` so a "clear cache" habit no longer drops
DND preference and notification history. The image thumbnail cache stays
in ~/.cache where regeneratable artifacts belong.
2026-05-14 16:31:47 -04:00
Ryan Hughes 806bd034b4 Fix bluetooth panel pair flow, scroll, and X-button click
- Discovered devices now populate via a new `discoveredDevices` filter; the
  Repeater was referencing a property that did not exist.
- Pairing chains into trust + connect at root scope, so freshly-paired
  devices (especially audio headphones) persist in BlueZ and actually
  finish connecting instead of flashing "Connected" and disappearing.
- Wrap the device list in a Flickable capped at 400px with a scroll bar
  so crowded RF environments do not grow the popup off-screen.
- Reorder MouseAreas so the per-row X button receives clicks instead of
  the full-row click handler swallowing them.
2026-05-14 14:40:14 -04:00
Ryan Hughes c101591284 Replace mako with quickshell-backed notification daemon
Adds first-party omarchy.notifications service plugin that hosts a
freedesktop notification server and renders popups + a history popup
inside the shell. Uninstalls mako and retargets every helper, keybind,
indicator, and migration entry to the new daemon.

Plugin (default/quickshell/omarchy-shell/plugins/notifications/):
- Service.qml: NotificationServer, popupModel + pendingModel + pastModel
  (two-tier history, see below), DND via PersistentProperties +
  cache-file backstop, image cache for /tmp screenshots, IpcHandler with
  toggleDnd/setDnd/isDnd/showHistory/clear/clearPending/markAllSeen/
  dismissAll/dismissOne/invokeLast/dismiss, per-theme override file
  ~/.config/omarchy/current/theme/notifications.json honoring
  borderColor/backgroundColor/textColor/countdownColor.
- components/NotificationCard.qml: theme-driven card (Color.foreground/
  background/border tokens from Commons/Color.qml), 32x32 icon slot,
  Nerd Font glyph fallback via omarchy-glyph hint, hero image strip
  for screenshot/image-path notifications, hover-pause progress bar,
  uses bar.fontFamily so all surfaces share one font.

Filtering and DND:
- transient hint and CLI-style senders (app_name in notify-send /
  omarchy-action) bypass history but still pop.
- DND only allows omarchy-action toasts and notify-send -u critical
  through; real-app urgency=critical (Discord, Slack, Vesktop) is
  silenced and lands in pending instead.
- Pending vs past split surfaced via tabs in the bar widget popup;
  past tab is auto-pruned at the 15-minute mark.
- Click-to-jump: notifications without a libnotify default action focus
  the matching Hyprland window via class lookup.

Shell host:
- shell.qml: generic first-party service loader (mirrors the existing
  noctalia-compat path) and an alias for the bar so plugins can read
  barSize / barHidden / position for anchoring.
- Commons/Color.qml: parses the theme's hyprland.conf for
  $activeBorderColor so notifications match Hyprland window borders;
  picks the explicit accent= key over the color4= alias.

Bar widget rebase (plugins/bar/widgets/notificationCenter.qml):
- Drops the chunk-1 stub server, binds count/dnd state to the service,
  hosts the history popup via PopupCard so it drops down from the
  notification glyph the same way Quick Settings does.
- Pending/Past tabs, dismiss-individual close X, mark-all-as-seen and
  clear-recent action buttons, theme-driven palette.

Quick Settings rework (plugins/bar/widgets/controlCenter.qml):
- DND tile binds directly to service.doNotDisturb for instant feedback.
- Drops the volume slider (already in audioPanel) and the no-op Theme
  tile; adds a Bluetooth toggle bound to Quickshell.Bluetooth.
- Bigger 44x44 wallet was scaled back to 32x32 for tighter rows.

Notification scripts (bin/omarchy-*):
- omarchy-notification-send: passes glyph as a custom hint instead of
  prepending to the summary; adds -a omarchy-action and -u urgency
  automatically; supports -e/--transient passthrough.
- User-action toasts in the capture / toggle / hyprland / default-*
  scripts and bindings/utilities.lua now tag themselves
  -a omarchy-action so DND treats them as intent-based bypass.
- omarchy-toggle-notification-silencing, omarchy-notification-dismiss,
  default/waybar/indicators/notification-silencing.sh, and the Hyprland
  comma-keybinds all route through omarchy-shell-ipc notifications.
- omarchy-capture-screenshot / -screenrecording set the image-path
  hint properly so the hero-image rendering kicks in.

Mako removal (migrations/1778743515.sh):
- pkill -x mako, systemctl --user stop mako.service, pacman -Rns mako
  (uninstalling deletes /usr/lib/systemd/user/mako.service so D-Bus
  activation can't respawn it). Removes ~/.config/mako/ and the legacy
  toggle file. Restarts quickshell so it claims the bus name.
- Drops mako from install/omarchy-base.packages, autostart.lua,
  install/config/theme.sh + toggles.sh, default/themed/mako.ini.tpl,
  default/mako/, the omarchy-menu Mako restart row, bin/omarchy
  GROUP_DESCRIPTIONS, the settings panel catalogue, and the
  default/omarchy-skill paths table.
- Removed scripts: bin/omarchy-restart-mako,
  bin/omarchy-style-corners-mako.
- bin/omarchy-style-corners summary updated; corner radius for the
  notification card reads ~/.local/state/omarchy/toggles/quickshell-menu.json
  alongside the rest of the shell.
2026-05-14 14:21:07 -04:00
Ryan Hughes b37a8cbaca Anchor tray to section's inner edge with pin/hide support
The tray drawer reserves space for its slide-in animation; placed
mid-section that gap was always visible. Now:

- normalizeLayout moves any tray entry to the section's inner edge
  (start of right, end of left/center) so the reserved gap sits next
  to the bar center instead of stranded mid-section.
- TrayModule reads its own entry settings to support pinned[] and
  hidden[] arrays of SNI ids. Pinned icons render to the right of the
  chevron (always visible); hidden icons never render; the rest fall
  into the drawer.
- Right-click the chevron opens a management popup listing every
  reporting tray item with Pin/Unpin and Hide/Show pills. Toggles
  persist via shell.updateEntryInline.
- Bar imports common/ as BarCommon so the tray can reach PopupCard
  and PillButton for the popup.
2026-05-14 03:24:53 -04:00
Ryan Hughes 5047c4c66e Collapse systemStats widget to a single chip icon
Bar now shows just the chip glyph; hover opens the existing CPU+Memory
graphs popup, left-click launches btop. Removes the inline sparklines,
the right-click→alacritty binding, and the redundant Open btop pill.
2026-05-14 03:24:22 -04:00
Ryan Hughes 1c559d2231 Default settings panel to Defaults category on open 2026-05-14 02:25:05 -04:00
Ryan Hughes 3595279dce Park focus on a sink Item when leaving body
The previous clearBodyFocus() set focus = false on navRoot.activeFocusItem
and called navRoot.forceActiveFocus(), which works for plain Rectangles
but not for Controls 2 widgets like ComboBox: the combo keeps an internal
focused child that the surrounding FocusScope happily restores, so the
accent focus border on the Position/Center anchor dropdowns lingered
after backing out to the sidebar.

Add an invisible 1x1 navFocusSink Item inside navRoot and force focus
onto it whenever the user leaves the body (h/Esc/Shift-Tab, category
changes from sidebar nav, or panel reopens). The sink reliably owns
activeFocus so every body control \u2014 ComboBox included \u2014 falls back to
its unfocused style.
2026-05-14 02:25:05 -04:00
Ryan Hughes 9bb35dd7c1 Differentiate focus from selected; clear focus when leaving body
Two related fixes from user feedback on the keyboard nav:

1. Focused and selected controls were drawing the same 2px accent border
   so it was impossible to tell where the keyboard cursor was on a row
   that happened to also be the current value. Focused now uses a 3px
   accent border plus a translucent accent fill; selected keeps its 2px
   border with no fill. Centralized into focusBorderColor /
   focusBorderWidth / focusFillColor so all components share the look.

2. Backing out of the body to the sidebar (h/Left/Esc) left the focus
   ring lingering on the body item. FocusScope was restoring activeFocus
   to its remembered descendant when navRoot reclaimed focus.
   exitBodyZone (and onActiveCategoryChanged when not in body mode) now
   clears focus from the previously active descendant before forcing
   focus back onto navRoot.
2026-05-14 02:25:05 -04:00