feat: zustand store for overlay state
This commit is contained in:
60
src/lib/store.test.ts
Normal file
60
src/lib/store.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useOverlayStore } from "./store";
|
||||
|
||||
const initial = useOverlayStore.getState();
|
||||
|
||||
describe("useOverlayStore", () => {
|
||||
beforeEach(() => useOverlayStore.setState(initial, true));
|
||||
|
||||
it("starts with sensible defaults", () => {
|
||||
const s = useOverlayStore.getState();
|
||||
expect(s.url).toBe("");
|
||||
expect(s.opacity).toBe(1.0);
|
||||
expect(s.alwaysOnTop).toBe(true);
|
||||
expect(s.clickThrough).toBe(false);
|
||||
expect(s.isOpen).toBe(false);
|
||||
expect(s.urlError).toBe(null);
|
||||
expect(s.hotkeyError).toBe(null);
|
||||
});
|
||||
|
||||
it("setUrl validates and stores both url and urlError", () => {
|
||||
useOverlayStore.getState().setUrl("twitch.tv");
|
||||
expect(useOverlayStore.getState().url).toBe("twitch.tv");
|
||||
expect(useOverlayStore.getState().urlError).toBe(null);
|
||||
|
||||
useOverlayStore.getState().setUrl("javascript:alert(1)");
|
||||
expect(useOverlayStore.getState().urlError).toMatch(/http and https/);
|
||||
});
|
||||
|
||||
it("setOpacity clamps to 0.1..1.0", () => {
|
||||
useOverlayStore.getState().setOpacity(0.05);
|
||||
expect(useOverlayStore.getState().opacity).toBe(0.1);
|
||||
useOverlayStore.getState().setOpacity(2);
|
||||
expect(useOverlayStore.getState().opacity).toBe(1.0);
|
||||
useOverlayStore.getState().setOpacity(0.5);
|
||||
expect(useOverlayStore.getState().opacity).toBe(0.5);
|
||||
});
|
||||
|
||||
it("setAlwaysOnTop and setClickThrough mutate flags", () => {
|
||||
useOverlayStore.getState().setAlwaysOnTop(false);
|
||||
expect(useOverlayStore.getState().alwaysOnTop).toBe(false);
|
||||
useOverlayStore.getState().setClickThrough(true);
|
||||
expect(useOverlayStore.getState().clickThrough).toBe(true);
|
||||
});
|
||||
|
||||
it("hydrate replaces persisted fields and clears errors", () => {
|
||||
useOverlayStore.getState().setUrl("javascript:alert(1)");
|
||||
useOverlayStore.getState().hydrate({
|
||||
url: "https://example.com",
|
||||
opacity: 0.6,
|
||||
alwaysOnTop: false,
|
||||
clickThrough: true,
|
||||
});
|
||||
const s = useOverlayStore.getState();
|
||||
expect(s.url).toBe("https://example.com");
|
||||
expect(s.opacity).toBe(0.6);
|
||||
expect(s.alwaysOnTop).toBe(false);
|
||||
expect(s.clickThrough).toBe(true);
|
||||
expect(s.urlError).toBe(null);
|
||||
});
|
||||
});
|
||||
46
src/lib/store.ts
Normal file
46
src/lib/store.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { create } from "zustand";
|
||||
import type { OverlayActions, OverlayPersistedState, OverlayState } from "../types/overlay";
|
||||
import { validateUrl } from "./url";
|
||||
|
||||
const clamp = (v: number, min: number, max: number): number =>
|
||||
Math.min(max, Math.max(min, v));
|
||||
|
||||
export const useOverlayStore = create<OverlayState & OverlayActions>()((set) => ({
|
||||
url: "",
|
||||
opacity: 1.0,
|
||||
alwaysOnTop: true,
|
||||
clickThrough: false,
|
||||
isOpen: false,
|
||||
urlError: null,
|
||||
hotkeyError: null,
|
||||
|
||||
setUrl: (raw) => {
|
||||
const result = validateUrl(raw);
|
||||
set({
|
||||
url: raw,
|
||||
urlError: result.ok ? null : raw.trim() === "" ? null : result.error,
|
||||
});
|
||||
},
|
||||
|
||||
setOpacity: (v) => set({ opacity: clamp(v, 0.1, 1.0) }),
|
||||
|
||||
setAlwaysOnTop: (v) => set({ alwaysOnTop: v }),
|
||||
|
||||
setClickThrough: (v) => set({ clickThrough: v }),
|
||||
|
||||
_setClickThroughFromBackend: (v) =>
|
||||
set((s) => (s.clickThrough === v ? s : { clickThrough: v })),
|
||||
|
||||
setHotkeyError: (msg) => set({ hotkeyError: msg }),
|
||||
|
||||
setIsOpen: (v) => set({ isOpen: v }),
|
||||
|
||||
hydrate: (p: OverlayPersistedState) =>
|
||||
set({
|
||||
url: p.url,
|
||||
opacity: clamp(p.opacity, 0.1, 1.0),
|
||||
alwaysOnTop: p.alwaysOnTop,
|
||||
clickThrough: p.clickThrough,
|
||||
urlError: null,
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user