Give Input keycaps, a shortcut search, and the missing pointer basics

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 15:21:01 -04:00
parent b8f88a91f3
commit aba2d16ffa
25 changed files with 2346 additions and 191 deletions
@@ -0,0 +1,147 @@
# Notifications & Focus redesign — two tabs, nothing unbounded
Approved mock: `home-mocks/notifications.html` (scratchpad, :8642). This spec is the
implementation contract; where mock and spec disagree, the spec wins.
## Goals
1. **Bound the app list.** Recent senders + customized apps render up top; everything else sits
behind a collapsed, searchable "All apps" expander. Rows expand in place to their controls.
2. **Real per-app rules**: sound on/off, banners-vs-history, urgency override, forget.
3. **A real focus-mode editor**: create/rename/delete/reorder (order IS priority), trigger-kind
editing for all five kinds, schedules with day pills, chip-based interrupt lists.
4. **Two tabs**: Notifications | Focus, as category tabs like Shell.
Non-goals: lock-screen privacy (hyprlock cannot render notifications; contract-banned),
per-app counters (no data), time-based history retention, merging duplicate app identities
(show the raw id honestly instead).
## Rule shape (pinned — service and UI program against this)
`normalizedAppRule` in `Notifs.qml` grows from `{ enabled }` to:
| Field | Type | Default | Meaning |
|---|---|---|---|
| `enabled` | bool | true | Off = rejected before tracking/history/unread/toast (unchanged) |
| `sound` | bool | true | false = `playBell` skips this app |
| `display` | string | `"banners"` | `"history"` = file in history + unread, no popup, no bell |
| `urgency` | string | `"auto"` | `"low"` / `"critical"` override what the app claims |
| `lastSeenMs` | int | 0 | stamped in `rememberApplication` on every notification |
| `name` | string | "" | display name cached at remember time (resolution stays live-first) |
| `icon` | string | "" | icon cached at remember time (DesktopEntries lookup, else `appIcon`) |
Unknown/stale fields (incl. the old lock-screen pair) keep being dropped on read. Old
`{enabled}`-only blobs stay valid — every new field is optional with the defaults above.
New API: `forgetApp(appId)` deletes the rule key outright.
`effectiveUrgency(notification)` — the app's `urgency` override applied over
`notification.urgency`; consumed by `playBell` (low = silent), the popup timeout choice
(critical duration), the DND breakthrough gate, and `NotificationCard`'s critical edge.
**Critical breakthrough**: new schema key `criticalBreaksThrough` (bool, def **false**, group
`notifications`, label "Critical alerts break through"). Popup gate becomes: show when
`!doNotDisturb || FocusModes.allows(appId) || (Settings.criticalBreaksThrough &&
effectiveUrgency(n) === critical)`.
## FocusModes API additions (pinned)
- `createMode(name)` → new mode `{ id: unique slug, name, enabled: true, triggers: [{kind:
"manual"}], silence: true, keepAwake: false, allow: [] }`, appended (lowest priority).
- `removeMode(id)`, `renameMode(id, name)` (non-empty, trimmed).
- `moveMode(id, delta)` — reorder; order is priority and the UI says so.
- `setTriggerKind(id, kind, fields)` — replaces the mode's `triggers` with one trigger of the
new kind. Kind-specific seeds: schedule → `{ start: "22:00", end: "07:00", days: [0..6] }`;
workspace → `{ id: 1 }`; fullscreen/game/manual → no fields. (Shipped modes each carry one
trigger; a hand-edited multi-trigger mode collapses to one on first kind change — the editor
edits `triggers[0]` and that is documented in a comment.)
- **Manual-mode semantics**: investigate how a manual-trigger mode activates today and PRESERVE
it exactly; if manual modes currently have no activation path besides `enabled`, the header
toggle keeps meaning `enabled` and the editor's "Turns on: Manually" detail explains that a
manual mode quiets things whenever it is switched on. Do not invent new activation machinery.
- Existing `setEnabled`/`update`/`reschedule`-style schedule + day editing semantics stay;
`withinWindow`, single-DND-ownership, and the gaming report-don't-silence rule are
contract-pinned and untouched.
## Routing (pinned)
`SettingsRoutes` category `notifications` gains tabs:
`[{ page: "notifications", label: "Notifications" }, { page: "focus", label: "Focus" }]`.
New leaf `focus` → new `FocusPage.qml`; `SettingsShell` case + Component. `groupPages`
`"focus"` moves `"notifications"` → `"focus"` (the focusModes entry renders there now).
`GamingPage`'s "Open Focus" action retargets `openSettings("focus")`.
`NotificationCard`'s "Notification settings" jump stays `"notifications"`.
## Notifications tab (NotificationsPage.qml rebuilt)
Lede unchanged. Cards:
1. **Quiet** — Do Not Disturb toggle; "Critical alerts break through" ToggleRow
(`criticalBreaksThrough`); "Quiet hours" ActionRow whose detail states the Sleep mode's
live schedule (or "not scheduled" when Sleep lacks/disabled) and whose button opens the
Focus tab (`openSettings("focus")`).
2. **Banners & history** — the four existing schema sliders (names contract-pinned; critical
zero renders "Never"); history count + Clear folded into the history row.
3. **Applications** — subtitle per mock. Sections:
- *Recent*: apps with `lastSeenMs` within 7 days, newest first.
- *Customized*: any app with a non-default field (and not already in Recent).
- *All apps (N)*: collapsed expander with an inline search field; alphabetical.
Rows: cached icon (fallback letter tile), name, subtitle (relative last-seen when known ·
state summary like "sound off"/"History only", else the raw appId), enabled toggle, chevron.
Expanded body: Play sound toggle · "Show as" dropdown (Banners & history / History only) ·
"Urgency" dropdown (App decides / Treat as low / Treat as critical) · "Forget this app"
ActionRow (detail: "Remove its rule; it returns on its next notification").
4. **Active-mode banner** at top (ok-tinted) when a focus mode is active: "<name> is quieting
notifications · because <reason> · <allow summary>", button "Open Focus".
## Focus tab (FocusPage.qml, new)
Lede: "Modes quiet this machine on their own terms — first matching mode wins, and the order
below is the priority."
1. **Focus modes** card — accordion (one open at a time): drag grip (reorder = priority; also
keyboard up/down on the grip), mode glyph, name, summary line ("Turns on <trigger summary> ·
silences everything except N apps" / "On now — <reason>"), enabled toggle, chevron.
Expanded: "Turns on" dropdown (five kinds) + kind fields (schedule start/end `TimeOfDayRow`-
style or validated HH:MM inputs + seven day pills; workspace id picker); "Silence
notifications" toggle; "May interrupt" chip row (chips with ×, "+ Add app" opens a searchable
picker over known apps — reuse the rules list's app universe); "Keep the screen awake"
toggle; Rename + Delete mode buttons. "+ New focus mode" dashed row at the bottom.
2. **Focus sessions** card — default duration segmented chips (25/45/60/90 →
`focusDurationMinutes`), Caffeine toggle, session status row + Start focus/Show controls
(existing behaviors move over unchanged).
Reuse existing row widgets and the Displays/Sound phase components (OptionPickerRow, etc.)
before inventing new ones. No continuously repainting animations. All new components get
qmldir lines in the same wave as first reference.
## Search & docs
Hand-written entries (page per target): Do Not Disturb, Quiet hours, Critical alerts break
through, Application notification rules, Forget an app's notifications, Per-app notification
sound → `notifications`; Focus modes detail already schema-indexed (now routes to `focus`),
plus Focus session duration → `focus` if not covered by the workspaces group move. Docs and
launcher commands regenerate after the schema lands (orchestrator's audit pass).
## Contracts (write, do NOT run — cite in the backlog for the next sweep)
- `notification-app-rules-contract`: extend the pinned rule shape to the table above (defaults,
optional back-compat, stale-field dropping incl. lock-screen pair), pin `forgetApp`, the
display="history" no-popup-no-bell path, sound=false no-bell, `effectiveUrgency` consumers,
and the breakthrough gate literal.
- `focus-modes-contract`: pin the new CRUD/reorder/trigger APIs, keep every existing pin
(conditions-not-alarms, DND ownership, gaming reports, exception list consulted + editable —
the editable needle moves to FocusPage).
- `settings-pages-contract` (page id list + component list), `settings-jump-contract`
(GamingPage → focus), `search-routing-contract` expectations, `settings-window-contract` if
it enumerates tabs.
- Backlog spec gains a Phase 7 section listing all of it.
## Agent ownership (parallel)
- **A — services**: `services/Notifs.qml`, `services/FocusModes.qml`,
`config/PreferenceSchema.qml` (one new key; comments above braces).
- **B — UI**: `modules/settings/NotificationsPage.qml`, new `modules/settings/FocusPage.qml` +
new components + `modules/settings/qmldir`, `services/SettingsRoutes.qml`,
`modules/settings/SettingsShell.qml`, `modules/notifications/NotificationCard.qml`
(effectiveUrgency), `modules/settings/GamingPage.qml` (Open Focus target).
- **C — periphery**: `services/SettingsSearch.qml`, the contracts above + harness fixtures,
test-backlog spec, README count line only if the contract count changes.
B programs against the pinned shapes; A must not change them without updating this spec.