Open History to see the last commit
1import type {
2 CSSProperties,
3 KeyboardEvent as ReactKeyboardEvent,
4 PointerEvent as ReactPointerEvent,
5} from "react";
6
7import { cn } from "~/lib/utils";
8
9import {
10 BROWSER_VIEWPORT_RESIZE_RAIL_SIZE,
11 type BrowserViewportLayout,
12 type BrowserViewportResizeDirection,
13} from "./browserViewportLayout";
14
15interface Props {
16 readonly layout: BrowserViewportLayout;
17 readonly activeDirection: BrowserViewportResizeDirection | null;
18 readonly onPointerDown: (
19 direction: BrowserViewportResizeDirection,
20 event: ReactPointerEvent<HTMLButtonElement>,
21 ) => void;
22 readonly onKeyDown: (
23 direction: BrowserViewportResizeDirection,
24 event: ReactKeyboardEvent<HTMLButtonElement>,
25 ) => void;
26}
27
28type HandleKind = "horizontal" | "vertical" | "corner";
29
30const EDGE_BUTTON_CLASS =
31 "group absolute z-20 touch-none border-0 bg-transparent p-0 outline-none before:absolute before:-inset-1 before:content-[''] focus-visible:bg-foreground/[0.04]";
32const EDGE_GRIP_CLASS =
33 "pointer-events-none absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center text-muted-foreground/55 transition-colors duration-150 group-hover:text-foreground/85 group-focus-visible:text-foreground group-active:text-foreground";
34
35function ResizeHandle(props: {
36 readonly direction: BrowserViewportResizeDirection;
37 readonly label: string;
38 readonly kind: HandleKind;
39 readonly cursorClassName: string;
40 readonly style: CSSProperties;
41 readonly active: boolean;
42 readonly mirrorCorner?: boolean;
43 readonly onPointerDown: Props["onPointerDown"];
44 readonly onKeyDown: Props["onKeyDown"];
45}) {
46 const {
47 direction,
48 label,
49 kind,
50 cursorClassName,
51 style,
52 active,
53 mirrorCorner = false,
54 onPointerDown,
55 onKeyDown,
56 } = props;
57 return (
58 <button
59 type="button"
60 aria-label={`${label}. Use arrow keys to resize.`}
61 className={cn(EDGE_BUTTON_CLASS, kind === "corner" && "z-30", cursorClassName)}
62 style={style}
63 onPointerDown={(event) => onPointerDown(direction, event)}
64 onKeyDown={(event) => onKeyDown(direction, event)}
65 >
66 <span
67 className={cn(
68 EDGE_GRIP_CLASS,
69 kind === "vertical" && "h-8 w-1.5",
70 kind === "horizontal" && "h-1.5 w-8",
71 kind === "corner" && "size-3",
72 active && "text-foreground",
73 )}
74 >
75 {kind === "vertical" ? (
76 <span className="flex gap-px" aria-hidden="true">
77 <span className="h-6 w-px rounded-full bg-current" />
78 <span className="h-6 w-px rounded-full bg-current" />
79 </span>
80 ) : kind === "horizontal" ? (
81 <span className="flex flex-col gap-px" aria-hidden="true">
82 <span className="h-px w-6 rounded-full bg-current" />
83 <span className="h-px w-6 rounded-full bg-current" />
84 </span>
85 ) : (
86 <span
87 className={cn("relative block size-3", mirrorCorner && "-scale-x-100")}
88 aria-hidden="true"
89 >
90 <span className="absolute bottom-[3px] left-0 h-px w-3 -rotate-45 rounded-full bg-current" />
91 <span className="absolute bottom-0 left-[5px] h-px w-2 -rotate-45 rounded-full bg-current" />
92 </span>
93 )}
94 </span>
95 </button>
96 );
97}
98
99export function BrowserViewportResizeHandles({
100 layout,
101 activeDirection,
102 onPointerDown,
103 onKeyDown,
104}: Props) {
105 const left = layout.viewportX;
106 const top = layout.viewportY;
107 const right = left + layout.viewportWidth;
108 const bottom = top + layout.viewportHeight;
109 const railSize = BROWSER_VIEWPORT_RESIZE_RAIL_SIZE;
110
111 const shared = { activeDirection, onPointerDown, onKeyDown };
112 return (
113 <>
114 <ResizeHandle
115 direction="west"
116 label="Resize browser viewport from left edge"
117 kind="vertical"
118 cursorClassName="cursor-ew-resize"
119 style={{ left: left - railSize, top, width: railSize, height: layout.viewportHeight }}
120 active={shared.activeDirection === "west"}
121 onPointerDown={shared.onPointerDown}
122 onKeyDown={shared.onKeyDown}
123 />
124 <ResizeHandle
125 direction="east"
126 label="Resize browser viewport from right edge"
127 kind="vertical"
128 cursorClassName="cursor-ew-resize"
129 style={{ left: right, top, width: railSize, height: layout.viewportHeight }}
130 active={shared.activeDirection === "east"}
131 onPointerDown={shared.onPointerDown}
132 onKeyDown={shared.onKeyDown}
133 />
134 <ResizeHandle
135 direction="south"
136 label="Resize browser viewport from bottom edge"
137 kind="horizontal"
138 cursorClassName="cursor-ns-resize"
139 style={{ left, top: bottom, width: layout.viewportWidth, height: railSize }}
140 active={shared.activeDirection === "south"}
141 onPointerDown={shared.onPointerDown}
142 onKeyDown={shared.onKeyDown}
143 />
144 <ResizeHandle
145 direction="southwest"
146 label="Resize browser viewport from bottom-left corner"
147 kind="corner"
148 cursorClassName="cursor-nesw-resize"
149 style={{ left: left - railSize, top: bottom, width: railSize, height: railSize }}
150 active={shared.activeDirection === "southwest"}
151 mirrorCorner
152 onPointerDown={shared.onPointerDown}
153 onKeyDown={shared.onKeyDown}
154 />
155 <ResizeHandle
156 direction="southeast"
157 label="Resize browser viewport from bottom-right corner"
158 kind="corner"
159 cursorClassName="cursor-nwse-resize"
160 style={{ left: right, top: bottom, width: railSize, height: railSize }}
161 active={shared.activeDirection === "southeast"}
162 onPointerDown={shared.onPointerDown}
163 onKeyDown={shared.onKeyDown}
164 />
165 </>
166 );
167}