Skip to content

Commit 686528f

Browse files
committed
serial WIP
1 parent 9b92954 commit 686528f

3 files changed

Lines changed: 100 additions & 15 deletions

File tree

src/components/connections/EphemeralSerialConfigOverlay.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Icon } from "@iconify/react";
33
import type { SerialConnectParams } from "@/types";
44
import { serialListPorts } from "@/services/serial";
55
import { Pills } from "@/components/shared/Pills";
6+
import { FormSelect } from "@/components/shared/FormSelect";
67

78
const BAUD_RATE_PRESETS = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];
89

@@ -62,11 +63,11 @@ export function EphemeralSerialConfigOverlay({
6263
</div>
6364
<div>
6465
<label className="text-xs text-[var(--t-text-dim)] mb-1 block">Baud Rate</label>
65-
<select className={sel} value={baud} onChange={(e) => setBaud(Number(e.target.value))}>
66-
{BAUD_RATE_PRESETS.map((r) => (
67-
<option key={r} value={r}>{r.toLocaleString()}</option>
68-
))}
69-
</select>
66+
<FormSelect
67+
value={String(baud)}
68+
options={BAUD_RATE_PRESETS.map((r) => ({ value: String(r), label: r.toLocaleString() }))}
69+
onChange={(v) => setBaud(Number(v))}
70+
/>
7071
</div>
7172
<button
7273
type="button"

src/components/connections/SerialConnectionForm.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
formLabelStyle,
2020
} from "@/components/shared/Panel";
2121
import { Pills } from "@/components/shared/Pills";
22+
import { FormSelect } from "@/components/shared/FormSelect";
2223
import EncodingSelector from "./EncodingSelector";
2324
import type { ConnectionFormHandle } from "./ConnectionForm";
2425

@@ -292,16 +293,12 @@ const SerialConnectionForm = forwardRef<ConnectionFormHandle, Props>(function Se
292293
<label className={formLabelClass} style={formLabelStyle}>Baud Rate</label>
293294
{!useCustomBaud ? (
294295
<div className="flex gap-2">
295-
<select
296-
className={`${formInputClass} flex-1`}
297-
style={{ ...formInputStyle, cursor: "pointer" }}
298-
value={baud}
299-
onChange={(e) => { markDirty(); setBaud(Number(e.target.value)); }}
300-
>
301-
{BAUD_RATES.map((r) => (
302-
<option key={r} value={r}>{r.toLocaleString()}</option>
303-
))}
304-
</select>
296+
<FormSelect
297+
className="flex-1"
298+
value={String(baud)}
299+
options={BAUD_RATES.map((r) => ({ value: String(r), label: r.toLocaleString() }))}
300+
onChange={(v) => { markDirty(); setBaud(Number(v)); }}
301+
/>
305302
<button
306303
type="button"
307304
className="text-xs text-[var(--t-text-dim)] hover:text-[var(--t-text-primary)] px-2 transition-colors whitespace-nowrap"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import { createPortal } from "react-dom";
3+
import { Icon } from "@iconify/react";
4+
import { DropdownMenuItem } from "./DropdownMenuItem";
5+
import { formInputStyle } from "./Panel";
6+
7+
interface Option {
8+
value: string;
9+
label: string;
10+
}
11+
12+
interface Props {
13+
value: string;
14+
options: Option[];
15+
onChange: (value: string) => void;
16+
className?: string;
17+
}
18+
19+
export function FormSelect({ value, options, onChange, className = "" }: Props) {
20+
const [open, setOpen] = useState(false);
21+
const [menuRect, setMenuRect] = useState({ top: 0, left: 0, width: 0 });
22+
const triggerRef = useRef<HTMLButtonElement>(null);
23+
const menuRef = useRef<HTMLDivElement>(null);
24+
25+
const selectedLabel = options.find((o) => o.value === value)?.label ?? value;
26+
27+
useEffect(() => {
28+
if (!open) return;
29+
const handler = (e: MouseEvent) => {
30+
const target = e.target as Node;
31+
if (!triggerRef.current?.contains(target) && !menuRef.current?.contains(target)) {
32+
setOpen(false);
33+
}
34+
};
35+
document.addEventListener("mousedown", handler);
36+
return () => document.removeEventListener("mousedown", handler);
37+
}, [open]);
38+
39+
const handleToggle = () => {
40+
if (!open && triggerRef.current) {
41+
const r = triggerRef.current.getBoundingClientRect();
42+
setMenuRect({ top: r.bottom + 4, left: r.left, width: r.width });
43+
}
44+
setOpen((o) => !o);
45+
};
46+
47+
return (
48+
<div className={className}>
49+
<button
50+
ref={triggerRef}
51+
type="button"
52+
onClick={handleToggle}
53+
className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm transition-colors"
54+
style={formInputStyle}
55+
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--t-accent)")}
56+
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--t-border)")}
57+
>
58+
<span className="text-[var(--t-text-primary)]">{selectedLabel}</span>
59+
<Icon
60+
icon="lucide:chevron-down"
61+
width={14}
62+
className="text-[var(--t-text-dim)] shrink-0"
63+
style={{ transition: "transform 150ms", transform: open ? "rotate(180deg)" : "rotate(0deg)" }}
64+
/>
65+
</button>
66+
67+
{open && createPortal(
68+
<div
69+
ref={menuRef}
70+
className="fixed p-1.5 rounded-xl z-[9999] flex flex-col bg-[var(--t-bg-card)] border border-[var(--t-bg-card-hover)] max-h-[240px] overflow-y-auto"
71+
style={{ top: menuRect.top, left: menuRect.left, width: menuRect.width, boxShadow: "0 8px 24px rgba(0,0,0,0.4)" }}
72+
>
73+
{options.map((opt) => (
74+
<DropdownMenuItem
75+
key={opt.value}
76+
label={opt.label}
77+
iconSize={15}
78+
checked={value === opt.value}
79+
onClick={() => { onChange(opt.value); setOpen(false); }}
80+
/>
81+
))}
82+
</div>,
83+
document.body,
84+
)}
85+
</div>
86+
);
87+
}

0 commit comments

Comments
 (0)