Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
636f7c3
fix(ui): keep focus and preview selection borders inside clipped bounds
maria-rcks Oct 7, 2026
19a332c
fix(ui): cover css containing blocks and lint length overrides
maria-rcks Oct 7, 2026
0a5d289
fix(lint): prevent inline offsets from bypassing outline widths
maria-rcks Oct 7, 2026
369e70c
fix(preview): respect css overflow clip margins
maria-rcks Oct 7, 2026
c7a75cf
fix(lint): validate template class fragments together
maria-rcks Oct 7, 2026
9ac3c4e
fix(lint): limit indicator checks to class expressions
maria-rcks Oct 7, 2026
2bf0f87
fix(ui): preserve focus borders across app scroll and preview states
maria-rcks Oct 7, 2026
fe9c6ea
fix(ui): preserve checked focus contrast and lint target scopes
maria-rcks Oct 7, 2026
1ecdd17
fix(lint): match outset overrides to their element target
maria-rcks Oct 7, 2026
d6fc645
fix(lint): evaluate guaranteed joined indicator classes
maria-rcks Oct 7, 2026
dbc94bb
fix(preview): ignore focus markers inside html attributes
maria-rcks Oct 7, 2026
d01f0ae
fix(preview): consume complete custom element tag names
maria-rcks Oct 7, 2026
adc4988
fix(preview): preserve attachment encoding during focus injection
maria-rcks Oct 7, 2026
dc40419
test(server): compare preserved html bytes consistently
maria-rcks Oct 7, 2026
fa9c7c1
perf(preview): avoid encoding html bodies for head requests
maria-rcks Oct 7, 2026
0bd3e18
chore: resolve focus border conflicts with main
maria-rcks Oct 9, 2026
490aa7f
chore: sync focus fixes with current main
maria-rcks Oct 9, 2026
1ad4ea1
refactor(lint): reduce focus indicator rule duplication
maria-rcks Oct 9, 2026
335b819
fix(ui): preserve indicator bounds in remaining edge cases
maria-rcks Oct 9, 2026
11c0eb5
fix(preview): respect viewport overflow propagation
maria-rcks Oct 9, 2026
de943b6
fix(preview): keep propagated clip margins out of local root bounds
maria-rcks Oct 9, 2026
ff90d44
refactor: simplify focus indicator containment
maria-rcks Oct 9, 2026
de7c8c4
fix(lint): ignore conditional branches as indicator companions
maria-rcks Oct 9, 2026
b8a8905
Merge remote-tracking branch 'origin/main' into t3/2751e4dc-focus-bor…
maria-rcks Oct 9, 2026
58c15e0
fix(ui): keep parent thread link focus ring inside
maria-rcks Oct 9, 2026
f96d67f
fix(lint): scope indicator companions to each enclosing class composer
maria-rcks Oct 9, 2026
2397c03
fix: parse outline widths and anchor the editor to visible selections
maria-rcks Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src/permissions/MacPermissionHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ body { margin: 0; background: transparent; color: var(--text); font: 15px/22px -
#panel { position: relative; margin: 2px; padding: 20px; height: 136px; border: 1px solid var(--line); border-radius: 24px; background: var(--base); }
header { font-weight: 600; white-space: nowrap; }
button { font: inherit; color: inherit; }
button:focus-visible { outline: 2px solid #007aff; outline-offset: 3px; }
button:focus-visible { outline: 2px solid #007aff; outline-offset: -2px; }
#close { position: absolute; right: 8px; top: 6px; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 50%; background: var(--base); font-size: 18px; cursor: pointer; opacity: 0; }
#panel:hover #close, #close:focus-visible { opacity: 1; }
#app { display: flex; align-items: center; gap: 12px; width: 100%; height: 52px; margin-top: 20px; padding: 8px 12px; border: 0; border-radius: 10px; background: var(--row); cursor: grab; text-align: left; font-size: 16px; font-weight: 600; }
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/Annotation.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,5 @@ input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent);
outline-offset: 1px;
outline-offset: -2px;
}
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/AnnotationStyles.generated.ts

Large diffs are not rendered by default.

52 changes: 46 additions & 6 deletions apps/desktop/src/preview/PickPreload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,43 @@ function createBox(color: string, fill: string): HTMLDivElement {
}

function positionBox(node: HTMLElement, rect: PreviewAnnotationRect): void {
if (rect.width <= 0 || rect.height <= 0) {
node.style.display = "none";
return;
}
node.style.display = "block";
node.style.transform = `translate(${rect.x}px, ${rect.y}px)`;
node.style.width = `${rect.width}px`;
node.style.height = `${rect.height}px`;
}

/** Clamps a box to the viewport and clipping ancestors, so its border is never painted off-screen. */
function visibleElementRect(element: Element): PreviewAnnotationRect {
const rect = element.getBoundingClientRect();
let left = Math.max(0, rect.left);
let top = Math.max(0, rect.top);
let right = Math.min(document.documentElement.clientWidth, rect.right);
let bottom = Math.min(document.documentElement.clientHeight, rect.bottom);
if (getComputedStyle(element).position !== "fixed") {
for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
if (ancestor === document.body || ancestor === document.documentElement) break;
const style = getComputedStyle(ancestor);
const bounds = ancestor.getBoundingClientRect();
const clipLeft = bounds.left + ancestor.clientLeft;
const clipTop = bounds.top + ancestor.clientTop;
if (style.overflowX !== "visible") {
left = Math.max(left, clipLeft);
right = Math.min(right, clipLeft + ancestor.clientWidth);
}
if (style.overflowY !== "visible") {
top = Math.max(top, clipTop);
bottom = Math.min(bottom, clipTop + ancestor.clientHeight);
}
}
}
return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
}

function createLabel(): HTMLDivElement {
const label = document.createElement("div");
label.setAttribute(OVERLAY_ATTRIBUTE, "");
Expand All @@ -343,11 +374,15 @@ function updateSelectedVisual(target: SelectedElement): void {
target.label.style.display = "none";
return;
}
const rect = target.element.getBoundingClientRect();
positionBox(target.outline, rectFromDomRect(rect));
const rect = visibleElementRect(target.element);
positionBox(target.outline, rect);
if (rect.width === 0 || rect.height === 0) {
target.label.style.display = "none";
return;
}
target.label.textContent = describeRawElement(target.element);
target.label.style.display = "block";
target.label.style.transform = `translate(${Math.max(4, rect.left)}px, ${Math.max(4, rect.top - 22)}px)`;
target.label.style.transform = `translate(${Math.max(4, rect.x)}px, ${Math.max(4, rect.y - 22)}px)`;
}

function toStackFrame(frame: {
Expand Down Expand Up @@ -995,8 +1030,9 @@ function startAnnotation(sendEnabled: boolean): void {
const getAnnotationBounds = (): PreviewAnnotationRect | null =>
unionRects(
[
...Array.from(selected.values(), (target) =>
rectFromDomRect(target.element.getBoundingClientRect()),
// Follow the visible outline, so a clipped selection does not anchor the editor off-screen.
...Array.from(selected.values(), (target) => visibleElementRect(target.element)).filter(
(rect) => rect.width > 0 && rect.height > 0,
),
...regions.map((region) => region.rect),
...strokes.map((stroke) => stroke.bounds),
Expand Down Expand Up @@ -1108,6 +1144,8 @@ function startAnnotation(sendEnabled: boolean): void {
for (const target of selected.values()) updateSelectedVisual(target);
queueEditorLayout();
};
// A scrollbar appearing narrows the viewport without a window resize.
const rootResizeObserver = new ResizeObserver(repaint);

const removeTargetAtPoint = (x: number, y: number): boolean => {
for (const target of Array.from(selected.values()).toReversed()) {
Expand Down Expand Up @@ -1192,7 +1230,7 @@ function startAnnotation(sendEnabled: boolean): void {
}
if (tool === "select" && dragStart === null) {
const target = pickFromPoint(event.clientX, event.clientY);
if (target) positionBox(hoverOutline, rectFromDomRect(target.getBoundingClientRect()));
if (target) positionBox(hoverOutline, visibleElementRect(target));
else clearHoverOutline();
return;
}
Expand Down Expand Up @@ -1310,6 +1348,7 @@ function startAnnotation(sendEnabled: boolean): void {
const teardown = (notifyMain: boolean): void => {
if (finished) return;
finished = true;
rootResizeObserver.disconnect();
restoreStyles();
window.removeEventListener("pointermove", onPointerMove, true);
window.removeEventListener("pointerdown", onPointerDown, true);
Expand Down Expand Up @@ -1445,6 +1484,7 @@ function startAnnotation(sendEnabled: boolean): void {
ipcRenderer.on(CANCEL_PICK_CHANNEL, onCancel);
ipcRenderer.on(ANNOTATION_CAPTURED_CHANNEL, onCaptured);
document.documentElement.appendChild(host);
rootResizeObserver.observe(document.documentElement);
refreshToolButtons();
updateStatus();
activeSession = {
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/htmlRender/HtmlRender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,12 @@ describe("HtmlRender", () => {
const htmlRender = yield* HtmlRender.HtmlRender;
const preview = yield* htmlRender.preview({
html: [
'<!doctype html><html><head></head><body><div style="height:300px;background:var(--accent)"></div>',
'<!doctype html><html><head></head><body><div id="page" tabindex="0" style="height:300px;background:var(--accent)"></div>',
'<img src="/nonexistent/t3-missing.png" hidden>',
"<script>",
"const root = getComputedStyle(document.documentElement);",
'const page = document.getElementById("page"); page.focus();',
'const focus = getComputedStyle(page); console.log("focus", focus.outlineWidth, focus.outlineOffset, page.getBoundingClientRect().width, document.documentElement.scrollWidth);',
'console.log("ready", 3); console.info(root.getPropertyValue("--font-sans"));',
'console.warn(root.getPropertyValue("--background")); console.error("boom");',
"</script>",
Expand All @@ -271,6 +273,7 @@ describe("HtmlRender", () => {
expect(preview.consoleMessages).toEqual(
expect.arrayContaining([
{ level: "log", text: "ready 3" },
{ level: "log", text: "focus 2px -2px 400 400" },
{ level: "info", text: HTML_RENDER_MEASURE_FONTS.sans },
{
level: "warning",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/ContextChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { cn } from "~/lib/utils";
* span with tabIndex for a tooltip) gets the focus outline.
*/
const contextChipVariants = cva(
"inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] border px-[0.5em] align-middle font-medium text-[0.86em] leading-none [&_svg]:block [&_svg]:size-[1.17em] [&_svg]:shrink-0 [&_svg]:self-center [button&,a&,[data-popup-open]&]:cursor-pointer [button&,a&]:transition-colors [button&,a&]:motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground disabled:cursor-default",
"inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] border px-[0.5em] align-middle font-medium text-[0.86em] leading-none [&_svg]:block [&_svg]:size-[1.17em] [&_svg]:shrink-0 [&_svg]:self-center [button&,a&,[data-popup-open]&]:cursor-pointer [button&,a&]:transition-colors [button&,a&]:motion-reduce:transition-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-foreground disabled:cursor-default",
{
defaultVariants: { kind: "neutral" },
variants: {
Expand Down Expand Up @@ -117,7 +117,7 @@ function ContextChipLabel({ className, ...props }: React.ComponentProps<"span">)
function ContextChipAction({ className, render, ...props }: useRender.ComponentProps<"button">) {
const defaultProps = {
className: cn(
"ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-(--context-chip-accent,var(--color-foreground))/17 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]",
"ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-(--context-chip-accent,var(--color-foreground))/17 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]",
className,
),
"data-slot": "context-chip-action",
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -793,9 +793,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
value={option.value}
className={cn(
"relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-3 text-left outline-none transition-[background-color,border-color,box-shadow]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
isSelected
? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30"
? "border-primary bg-background shadow-sm ring-2 ring-inset ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30"
: "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent",
)}
>
Expand All @@ -817,7 +817,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
>
Repository
</label>
<div className="flex items-stretch overflow-hidden rounded-md border border-input bg-background focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring">
<div className="flex items-stretch overflow-hidden rounded-md border border-input bg-background focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-ring">
<span className="flex shrink-0 items-center gap-1.5 border-r border-input bg-muted/50 px-2.5 font-mono text-xs text-muted-foreground">
<currentPublishProvider.Icon className="size-3.5" />
{publishHost}/
Expand Down Expand Up @@ -879,9 +879,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
value={option.value}
className={cn(
"relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
isSelected
? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30"
? "border-primary bg-background shadow-sm ring-2 ring-inset ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30"
: "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent",
)}
>
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/LegacySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> =
separate: "Keep separate",
};
const SIDEBAR_ICON_ACTION_BUTTON_CLASS =
"inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-0.75 text-icon-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring";
"inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-0.75 text-icon-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring";

function SidebarThreadDetailPrewarmer({ threadRef }: { readonly threadRef: ScopedThreadRef }) {
useEnvironmentThread(threadRef.environmentId, threadRef.threadId);
Expand Down Expand Up @@ -769,7 +769,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
target="_blank"
rel="noopener noreferrer"
aria-label={prStatus.tooltip}
className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-ring`}
className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring`}
onPointerDown={(event) => event.stopPropagation()}
onClick={handlePrClick}
>
Expand Down Expand Up @@ -835,7 +835,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
<button
type="button"
aria-label={`Open localhost:${discoveredPorts[0]?.port ?? ""}`}
className="inline-flex cursor-pointer items-center justify-center text-success-foreground outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
className="inline-flex cursor-pointer items-center justify-center text-success-foreground outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring"
onClick={handleOpenDiscoveredPort}
/>
}
Expand Down Expand Up @@ -880,7 +880,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
data-thread-selection-safe
data-testid={`thread-archive-confirm-${thread.id}`}
aria-label={`Confirm archive ${thread.title}`}
className="absolute top-1/2 right-1 inline-flex h-5 -translate-y-1/2 cursor-pointer items-center rounded-md bg-destructive/12 px-2 text-3xs font-medium text-destructive transition-colors hover:bg-destructive/18 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-destructive/40"
className="absolute top-1/2 right-1 inline-flex h-5 -translate-y-1/2 cursor-pointer items-center rounded-md bg-destructive/12 px-2 text-3xs font-medium text-destructive transition-colors hover:bg-destructive/18 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-destructive/40"
onPointerDown={stopPropagationOnPointerDown}
onClick={handleConfirmArchiveClick}
>
Expand Down Expand Up @@ -2955,7 +2955,7 @@ function SortableProjectItem({
}}
className={`group/menu-item relative rounded-md ${
isDragging ? "z-20 opacity-80" : ""
} ${isOver && !isDragging ? "ring-1 ring-primary/40" : ""}`}
} ${isOver && !isDragging ? "ring-1 ring-inset ring-primary/40" : ""}`}
data-sidebar="menu-item"
data-slot="sidebar-menu-item"
>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
{renamingDevice === surface.id ? (
<input
aria-label="Device tab name"
className="w-24 min-w-0 rounded-sm bg-background px-1 outline-none ring-1 ring-ring"
className="w-24 min-w-0 rounded-sm bg-background px-1 outline-none ring-1 ring-inset ring-ring"
defaultValue={title}
ref={(element) => {
element?.focus();
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
type="button"
aria-label="Unpin thread"
onClick={handleUnpinClick}
className="group/unpin inline-flex cursor-pointer items-center rounded-sm text-muted-foreground/65 outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
className="group/unpin inline-flex cursor-pointer items-center rounded-sm text-muted-foreground/65 outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
/>
}
>
Expand Down Expand Up @@ -1876,7 +1876,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
type="button"
aria-label="Dismiss Woke notification"
onClick={handleAcknowledgeWokeClick}
className="inline-flex cursor-pointer items-center gap-1 rounded-sm text-xs font-medium text-warning-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
className="inline-flex cursor-pointer items-center gap-1 rounded-sm text-xs font-medium text-warning-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
>
<AlarmClockIcon aria-hidden className="size-3" />
<span role="status">Woke</span>
Expand Down Expand Up @@ -2040,7 +2040,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
aria-label="Dismiss Woke notification"
onClick={handleAcknowledgeWokeClick}
className={cn(
"inline-flex cursor-pointer items-center gap-1 rounded-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring",
"inline-flex cursor-pointer items-center gap-1 rounded-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
topStatus.className,
)}
>
Expand Down
Loading
Loading