diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx
new file mode 100644
index 0000000..70a2780
--- /dev/null
+++ b/src/components/ControlPanel.tsx
@@ -0,0 +1,40 @@
+import { Layers, MousePointerClick } from "lucide-react";
+import { UrlField } from "./UrlField";
+import { OpacitySlider } from "./OpacitySlider";
+import { ToggleRow } from "./ToggleRow";
+import { OpenOverlayButton } from "./OpenOverlayButton";
+import { useOverlayStore } from "../lib/store";
+
+export function ControlPanel(): React.JSX.Element {
+ const alwaysOnTop = useOverlayStore((s) => s.alwaysOnTop);
+ const setAlwaysOnTop = useOverlayStore((s) => s.setAlwaysOnTop);
+ const clickThrough = useOverlayStore((s) => s.clickThrough);
+ const setClickThrough = useOverlayStore((s) => s.setClickThrough);
+
+ return (
+
+
+
+
+
+ }
+ checked={alwaysOnTop}
+ onChange={setAlwaysOnTop}
+ />
+ }
+ checked={clickThrough}
+ onChange={setClickThrough}
+ />
+
+
+
+ );
+}
diff --git a/src/components/OpacitySlider.tsx b/src/components/OpacitySlider.tsx
new file mode 100644
index 0000000..8f9e79d
--- /dev/null
+++ b/src/components/OpacitySlider.tsx
@@ -0,0 +1,28 @@
+import { useOverlayStore } from "../lib/store";
+
+export function OpacitySlider(): React.JSX.Element {
+ const opacity = useOverlayStore((s) => s.opacity);
+ const setOpacity = useOverlayStore((s) => s.setOpacity);
+ const pct = Math.round(opacity * 100);
+
+ return (
+
+
+
+ {pct}%
+
+
setOpacity(Number(e.currentTarget.value))}
+ className="w-full accent-[var(--color-accent)]"
+ />
+
+ );
+}
diff --git a/src/components/OpenOverlayButton.tsx b/src/components/OpenOverlayButton.tsx
new file mode 100644
index 0000000..8d91a35
--- /dev/null
+++ b/src/components/OpenOverlayButton.tsx
@@ -0,0 +1,56 @@
+import { Play, Square } from "lucide-react";
+import { useOverlayStore } from "../lib/store";
+import { closeOverlay, openOverlay } from "../lib/overlay";
+import { validateUrl } from "../lib/url";
+
+export function OpenOverlayButton(): React.JSX.Element {
+ const url = useOverlayStore((s) => s.url);
+ const isOpen = useOverlayStore((s) => s.isOpen);
+ const opacity = useOverlayStore((s) => s.opacity);
+ const alwaysOnTop = useOverlayStore((s) => s.alwaysOnTop);
+ const clickThrough = useOverlayStore((s) => s.clickThrough);
+ const setIsOpen = useOverlayStore((s) => s.setIsOpen);
+
+ const validation = validateUrl(url);
+ const disabled = !isOpen && !validation.ok;
+
+ async function handleClick(): Promise {
+ if (isOpen) {
+ await closeOverlay();
+ // The destroyed listener registered in openOverlay will also flip
+ // isOpen, but call it here too so the UI reflects intent immediately.
+ setIsOpen(false);
+ return;
+ }
+ if (!validation.ok) return;
+ await openOverlay({
+ url: validation.url,
+ opacity,
+ alwaysOnTop,
+ clickThrough,
+ onDestroyed: () => {
+ useOverlayStore.getState().setIsOpen(false);
+ },
+ });
+ setIsOpen(true);
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx
new file mode 100644
index 0000000..8308e5f
--- /dev/null
+++ b/src/components/StatusBar.tsx
@@ -0,0 +1,33 @@
+import { AlertTriangle, Keyboard } from "lucide-react";
+import { useOverlayStore } from "../lib/store";
+
+export type StatusBarProps = { transient: string | null };
+
+export function StatusBar({ transient }: StatusBarProps): React.JSX.Element {
+ const hotkeyError = useOverlayStore((s) => s.hotkeyError);
+ const isOpen = useOverlayStore((s) => s.isOpen);
+
+ return (
+
+ );
+}
diff --git a/src/components/ToggleRow.tsx b/src/components/ToggleRow.tsx
new file mode 100644
index 0000000..0b570a5
--- /dev/null
+++ b/src/components/ToggleRow.tsx
@@ -0,0 +1,47 @@
+import type { ReactNode } from "react";
+
+export type ToggleRowProps = {
+ label: string;
+ description?: string;
+ icon?: ReactNode;
+ checked: boolean;
+ onChange: (v: boolean) => void;
+};
+
+export function ToggleRow({ label, description, icon, checked, onChange }: ToggleRowProps): React.JSX.Element {
+ return (
+
+ );
+}
diff --git a/src/components/UrlField.tsx b/src/components/UrlField.tsx
new file mode 100644
index 0000000..31d4e4e
--- /dev/null
+++ b/src/components/UrlField.tsx
@@ -0,0 +1,37 @@
+import { Globe } from "lucide-react";
+import { useOverlayStore } from "../lib/store";
+
+export function UrlField(): React.JSX.Element {
+ const url = useOverlayStore((s) => s.url);
+ const urlError = useOverlayStore((s) => s.urlError);
+ const setUrl = useOverlayStore((s) => s.setUrl);
+
+ return (
+
+
+
+
+ setUrl(e.currentTarget.value)}
+ className="w-full bg-transparent outline-none text-[var(--color-text)] placeholder:text-[var(--color-muted)]"
+ />
+
+ {urlError && (
+
{urlError}
+ )}
+
+ );
+}