-
Notifications
You must be signed in to change notification settings - Fork 10
feat: Custom Status-Dot Tooltip / Hover-Card #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c8e3ed2
feat: replace status-dot title tooltip with StatusDotTip floating hov…
sahil87 76a4e3f
Update ship status and record PR URL
sahil87 249339b
fix: address review feedback from @Copilot
sahil87 8be6b98
Update review-pr status
sahil87 51505cf
refactor: replace docs sentence with a quiet circled-(i) icon in stat…
sahil87 5f4fe6f
docs: make docs/site images + cross-set links absolute (shll.ai closu…
sahil87 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { DotShape, StatusDotState } from "@/components/pr-status-line"; | ||
| import type { WindowInfo } from "@/types"; | ||
|
|
||
| /** | ||
| * Shared label resolver for the status dot, extracted into its own module so | ||
| * both `status-dot.tsx` (the dot) and `status-dot-tip.tsx` (the hover-card) | ||
| * can import it without forming an import cycle between the two components. | ||
| * `status-dot.tsx` re-exports `dotLabel` to keep its public surface stable. | ||
| */ | ||
|
|
||
| /** Human word for the SHAPE/status axis used in the accessible label. */ | ||
| const SHAPE_LABEL: Record<DotShape, string> = { | ||
| ring: "pending", | ||
| solid: "active", | ||
| failed: "failed", | ||
| done: "done", | ||
| skipped: "skipped", | ||
| }; | ||
|
|
||
| // PR-phase status words. The shared SHAPE_LABEL vocabulary is fab-stage language | ||
| // ("active"/"done"); for a PR those read unnaturally, so the PR branch maps the | ||
| // same shapes onto PR-native words — a PR is "open", "merged", "failing", not | ||
| // "active"/"done"/"failed". | ||
| const PR_SHAPE_LABEL: Record<DotShape, string> = { | ||
| ring: "checks running", | ||
| solid: "open", | ||
| failed: "failing", | ||
| done: "merged", | ||
| skipped: "closed", | ||
| }; | ||
|
|
||
| /** | ||
| * Compose the accessible label. The fab branch uses the real stage word | ||
| * ("apply — active"); the PR branch uses PR-native words ("PR — merged"); the | ||
| * tmux fallback uses the bare activity word ("active"/"idle"), no journey. | ||
| * | ||
| * The tmux fallback is gated on `!win.fabChange`, NOT on `phase === "none"`: a | ||
| * fab-bound window whose `fabStage` is unknown/absent maps to `phase: "none"` | ||
| * via `fabPhase` (and may carry a `failed`/`done` shape), yet it still | ||
| * represents fab state — so it gets a `{stage} — {status}` label (the raw | ||
| * `fabStage`, or the literal "fab" when the stage word is absent), never the | ||
| * bare tmux activity word. Only a window with no `fabChange` is a true tmux | ||
| * fallback. | ||
| */ | ||
| export function dotLabel(win: WindowInfo, state: StatusDotState): string { | ||
| if (state.phase === "pr") return `PR — ${PR_SHAPE_LABEL[state.shape]}`; | ||
| if (!win.fabChange) return win.activity; // tmux fallback: "active" | "idle" | ||
| return `${win.fabStage ?? "fab"} — ${SHAPE_LABEL[state.shape]}`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| import { useState, type ReactNode } from "react"; | ||
| import { | ||
| useFloating, | ||
| offset, | ||
| flip, | ||
| shift, | ||
| useHover, | ||
| useFocus, | ||
| useDismiss, | ||
| useInteractions, | ||
| FloatingPortal, | ||
| safePolygon, | ||
| autoUpdate, | ||
| } from "@floating-ui/react"; | ||
| import { dotLabel } from "@/components/status-dot-label"; | ||
| import type { StatusDotState } from "@/components/pr-status-line"; | ||
| import type { WindowInfo } from "@/types"; | ||
|
|
||
| /** | ||
| * Status-dot docs page (rendered by GitHub). Opens in a new tab from the | ||
| * hover-card's docs icon — the canonical "what does this dot mean" reference. | ||
| * docs/site is NOT served by the backend, so we link the GitHub blob (no | ||
| * anchor → lands at the top of the doc), matching the convention the only | ||
| * other in-app docs link uses (top-bar.tsx NOTIFICATIONS_HELP_URL). | ||
| */ | ||
| const STATUS_DOT_DOCS_URL = | ||
| "https://github.com/sahil87/run-kit/blob/main/docs/site/status-dot.md"; | ||
|
|
||
| /** A single interactive link rendered inside the hover-card. */ | ||
| export type DotLink = { label: string; href: string; testid: string }; | ||
|
|
||
| /** Resolved hover-card content for a window+state. */ | ||
| export type DotTipContent = { label: string; links: DotLink[] }; | ||
|
|
||
| /** | ||
| * Pure content resolver: maps a window + its derived `StatusDotState` to the | ||
| * hover-card's text + interactive links. The label REUSES `dotLabel()` so the | ||
| * card text is the single source of truth shared with the dot's `aria-label`. | ||
| * | ||
| * Only PR-phase dots that actually carry a `prUrl` get a link ("Open PR #N"). | ||
| * Fab-phase and tmux-fallback dots get text only. The docs-link icon is NOT in | ||
| * `links[]` — it is a fixed element the card always renders (constant href), so | ||
| * it does not flow through per-state logic. | ||
| */ | ||
| export function dotTipContent(win: WindowInfo, state: StatusDotState): DotTipContent { | ||
| const label = dotLabel(win, state); | ||
| const links: DotLink[] = []; | ||
| if (state.phase === "pr" && win.prUrl) { | ||
| links.push({ | ||
| label: `Open PR #${win.prNumber}`, | ||
| href: win.prUrl, | ||
| testid: "dot-tip-pr-link", | ||
| }); | ||
| } | ||
| return { label, links }; | ||
| } | ||
|
|
||
| /** | ||
| * Circled-"i" info glyph for the docs affordance — an inline SVG (matching the | ||
| * codebase's hand-built SVG icons, e.g. window-row's pin) rather than a Nerd | ||
| * Font glyph, so it renders crisply at any size, themes via `currentColor`, and | ||
| * doesn't depend on the user's terminal font being patched. "info" intent reads | ||
| * as "know more" without competing with the "Open PR" external-link affordance. | ||
| */ | ||
| function InfoIcon() { | ||
| return ( | ||
| <svg | ||
| aria-hidden="true" | ||
| width="12" | ||
| height="12" | ||
| viewBox="0 0 16 16" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="1.4" | ||
| className="shrink-0" | ||
| > | ||
| <circle cx="8" cy="8" r="6.5" /> | ||
| <line x1="8" y1="7.25" x2="8" y2="11" strokeLinecap="round" /> | ||
| <circle cx="8" cy="4.75" r="0.5" fill="currentColor" stroke="none" /> | ||
| </svg> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Shared link styling for the card's interactive rows. Click is stopped from | ||
| * bubbling so activating a link never selects/navigates the underlying window | ||
| * row (the dot sits inside a clickable sidebar row) — mirrors the proven | ||
| * PrStatusLine link pattern (pr-status-line.tsx). | ||
| */ | ||
| const LINK_CLASS = | ||
| "text-text-secondary hover:text-text-primary hover:underline whitespace-nowrap coarse:py-1"; | ||
|
|
||
| type StatusDotTipProps = { | ||
| win: WindowInfo; | ||
| state: StatusDotState; | ||
| /** | ||
| * Renders the dot itself. Receives the floating reference setter and the | ||
| * reference interaction props (which carry hover/focus/aria wiring) to spread | ||
| * onto the dot element — keeping the dot the floating anchor while StatusDot | ||
| * owns the dot's shape/color markup. | ||
| */ | ||
| renderDot: ( | ||
| setReference: (node: HTMLElement | null) => void, | ||
| referenceProps: Record<string, unknown>, | ||
| ) => ReactNode; | ||
| }; | ||
|
|
||
| /** | ||
| * Custom hover-card wrapping a `StatusDot`. Replaces the native HTML `title` | ||
| * tooltip: a headless `@floating-ui/react` floating element gives full styling | ||
| * control (terminal aesthetic), portals out of the sidebar's `overflow:hidden` | ||
| * clip, flips/shifts at viewport edges, and uses a `safePolygon` bridge so the | ||
| * pointer can travel dot → card to click a link. | ||
| * | ||
| * Opens on hover (snappy delay) AND on keyboard focus (Constitution V — | ||
| * keyboard-first), dismisses on Escape / blur / pointer-leave. Always shows the | ||
| * dot's label text + a docs-link icon; PR-phase dots additionally show an | ||
| * "Open PR #N" link (from `dotTipContent`). | ||
| */ | ||
| export function StatusDotTip({ win, state, renderDot }: StatusDotTipProps) { | ||
| const [open, setOpen] = useState(false); | ||
| const { label, links } = dotTipContent(win, state); | ||
|
|
||
| const { refs, floatingStyles, context } = useFloating({ | ||
| open, | ||
| onOpenChange: setOpen, | ||
| placement: "top", | ||
| middleware: [offset(6), flip(), shift({ padding: 8 })], | ||
| whileElementsMounted: autoUpdate, | ||
| }); | ||
|
|
||
| const hover = useHover(context, { | ||
| delay: { open: 150, close: 100 }, | ||
| handleClose: safePolygon(), | ||
| }); | ||
| const focus = useFocus(context); | ||
| const dismiss = useDismiss(context); | ||
| // No `useRole({ role: "tooltip" })`: the W3C tooltip pattern requires a | ||
| // tooltip to contain NO interactive content, but this card holds real `<a>` | ||
| // links (PR + docs). Advertising `role="tooltip"` would (a) lie to AT about | ||
| // there being nothing actionable and (b) wire a misleading `aria-describedby` | ||
| // from the dot to a "tooltip" that is actually a hover-card. The dot already | ||
| // carries its own accessible name via `aria-label`, and the links are | ||
| // real Tab-reachable controls — so we add no extra ARIA role here. | ||
| const { getReferenceProps, getFloatingProps } = useInteractions([ | ||
| hover, | ||
| focus, | ||
| dismiss, | ||
| ]); | ||
|
|
||
| return ( | ||
| <> | ||
| {renderDot(refs.setReference, getReferenceProps())} | ||
| {open && ( | ||
| <FloatingPortal> | ||
| <div | ||
| ref={refs.setFloating} | ||
| style={floatingStyles} | ||
| {...getFloatingProps()} | ||
| // Don't let clicks inside the card bubble to the underlying row. | ||
| onClick={(e) => e.stopPropagation()} | ||
| data-testid="status-dot-tip" | ||
| className="z-50 flex flex-col gap-1 bg-bg-primary border border-border rounded-md shadow-lg px-2 py-1.5 text-xs font-mono w-max max-w-xs" | ||
| > | ||
| {/* Label row: status text on the left, a quiet circled-(i) docs | ||
| affordance pinned top-right. No visible copy — the icon's | ||
| "info / know more" convention plus its aria-label/title carry | ||
| the meaning, keeping the card terse and matching its register. */} | ||
| <div className="flex items-start gap-3"> | ||
| <span className="text-text-primary whitespace-nowrap">{label}</span> | ||
| <a | ||
| href={STATUS_DOT_DOCS_URL} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| onClick={(e) => e.stopPropagation()} | ||
| className="ml-auto mt-px text-text-secondary hover:text-text-primary coarse:p-1" | ||
| aria-label="What do status dots mean? (opens docs)" | ||
| title="What do status dots mean?" | ||
| data-testid="dot-tip-docs-link" | ||
| > | ||
| <InfoIcon /> | ||
| </a> | ||
| </div> | ||
| {links.map((link) => ( | ||
| <a | ||
| key={link.testid} | ||
| href={link.href} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| onClick={(e) => e.stopPropagation()} | ||
| className={LINK_CLASS} | ||
| data-testid={link.testid} | ||
| > | ||
| {link.label} | ||
| </a> | ||
| ))} | ||
| </div> | ||
| </FloatingPortal> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Skipped — the asserted breakage doesn't occur:
tsc --noEmitpasses (strict mode) both before and after this PR.getReferenceProps()from @floating-ui/react returnsRecord<string, unknown>, so this type matches the source exactly; spreading an index-signature object into JSX props is valid under strict TS (it only rejects when a named prop's type conflicts). Narrowing to a DOM-props type would actually misrepresent floating-ui's contract here.