docs(desktop): tip only when hover teaches something new

Drop the blanket "every icon* button needs a Tip" rule — it produced
tautological kebab tips like "Actions for <row title>". Menu triggers keep
aria-label; tips stay for unlabeled discovery chrome and keybind hints.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 21:43:52 -05:00
parent 84858d76ba
commit 58714e0e2a
2 changed files with 28 additions and 19 deletions

View File

@ -117,20 +117,28 @@ that sit inside a heading/sentence; replaces `h-auto px-0 py-0`), `micro`
(status-stack/table-footers), and the icon family `icon` / `icon-xs` /
`icon-sm` / `icon-lg` / `icon-titlebar`.
**Icon-only buttons must have a tooltip.** Every button with an `icon*` size
carries no visible text label, so it must be wrapped in `<Tip label={...}>`
with a descriptive label (matching the button's `aria-label`). Never use the
native HTML `title=` attribute — it's unstyled, delayed (~500ms OS default),
and visually inconsistent with the instant themed `Tip`. An enforcement test
(`src/components/ui/__tests__/no-native-title.test.ts`) fails on any `<button>`
or `<Button>` that still carries `title=`.
**Tooltips only when hover teaches something new.** `<Tip>` is for discovery,
not a tax on every icon. Ask: does hover reveal something the user cannot
already see or infer? If not, skip the tip; keep an `aria-label` for a11y.
**Keybind hints in tooltips.** When a button corresponds to a rebindable
hotkey, use `<TipKeybindLabel actionId="..." />` as the `Tip` label — it
auto-reads both the i18n label and the current keybind combo from the store,
so the hint stays live when the user rebinds. Pass `text={...}` only when the
tooltip is context-dependent (e.g. "Show" / "Hide" based on state). Never
hardcode combos in components — always read from the `$bindings` store via
Tip unlabeled chrome when the job (or a keybind / truncated path / host /
other detail) is not already on screen — toolbar / titlebar / statusbar icons,
`TipKeybindLabel` shortcuts, ownership chips, unlabeled icon grids.
Do **not** tip menu triggers (kebabs / ⋯ / `ActionsMenu` /
`DropdownMenuTrigger`). The affordance is "open menu"; the verbs live in the
menu. Never `"Actions for ${row title}"`, `"Project actions"`, `"Actions"`, or
`"Credential actions"` on a ⋯ — the row already names the thing. Same for any
control whose visible label already says what the tip would.
Never use native HTML `title=` on buttons — unstyled, ~500ms OS delay, clashes
with the themed `Tip`. `src/components/ui/__tests__/no-native-title.test.ts`
fails on any `<button>` / `<Button>` that still carries `title=`.
**Keybind hints in tooltips.** On a tipped button bound to a rebindable hotkey,
use `<TipKeybindLabel actionId="..." />` — it reads the i18n label and the
current combo from `$bindings`. Pass `text={...}` only when the label is
context-dependent (e.g. "Show" / "Hide"). Never hardcode combos; always use
`useKeybindHint` or `TipKeybindLabel`.
Notes:
@ -299,9 +307,10 @@ The detailed state contract lives in the scoped
- [ ] Tokens (`--ui-*`, `shadow-nous`, `--stroke-nous`) — zero raw colors /
one-off shadows?
- [ ] No `className` overriding a primitive's padding / size / radius / chrome?
- [ ] Icon-only buttons wrapped in `<Tip>` with a descriptive label?
- [ ] No native `title=` on buttons — use `<Tip>` instead?
- [ ] Keybind hints read from the store via `useKeybindHint` / `TipKeybindLabel`?
- [ ] Tips only where hover teaches something new (no kebab / menu-trigger
tips; unlabeled chrome that needs discovery gets `<Tip>` + `aria-label`)?
- [ ] No native `title=` on buttons?
- [ ] Keybind hints on tipped buttons use `useKeybindHint` / `TipKeybindLabel`?
- [ ] Overlay uses `shadow-nous` + `border-(--stroke-nous)`, no hard border?
- [ ] Flat — no card-in-card, no gratuitous row dividers?
- [ ] No automatic navigation, focus steal, or pane opening from background

View File

@ -5,12 +5,12 @@ import { describe, expect, it } from 'vitest'
// Static-analysis guard: no <button> or <Button> element in the desktop renderer
// may use the native HTML `title=` attribute. Native tooltips are unstyled,
// delayed (~500ms OS default), and visually inconsistent with the app's instant
// themed `Tip` component. Use `<Tip label={...}>` instead.
// delayed (~500ms OS default), and visually inconsistent with the themed `Tip`.
// When a tip is warranted (see DESIGN.md — not every icon, never menu triggers),
// use `<Tip label={...}>` instead of `title=`.
//
// This is a source-text scan, not a behavior test — it's the same category as
// an ESLint rule, expressed as a vitest so it runs with the rest of the suite.
// See DESIGN.md "Buttons — one component" for the rule.
// Recursively walk a directory and collect all .tsx file paths.
function collectTsxFiles(dir: string): string[] {