diff --git a/.gitignore b/.gitignore index 9514bef..6d9a453 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ .ruff_cache/ dist/ build/ +node_modules/ .coverage htmlcov/ .env diff --git a/CHANGELOG.md b/CHANGELOG.md index 38d83b8..c78af85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ HaloCLI uses semantic-ish versioning while it is young: patch releases are small fixes and packaging polish, minor releases may add commands or change operator workflows, and major releases are reserved for breaking CLI behavior. +## 0.5.0 - 2026-04-27 + +- Added `halocli todo web`, a local-first FastAPI/Vite React Todo web UI over + Halo appointment-backed tasks. +- Added normalized Todo API routes for listing, creating, updating, completing, + and noting tasks, preserving the HaloCLI metadata marker in `note_html`. +- Added client/ticket picker APIs and 0-duration Halo time-entry-backed work + logs for Todo updates. +- Added Todo work-log history reads from Halo time entries. +- Added the optional `web` package extra for FastAPI and Uvicorn. + ## 0.4.0 - 2026-04-26 - Added a central Halo resource registry. diff --git a/MANIFEST.in b/MANIFEST.in index 7813919..90ad964 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include FRIDAY_PRESENT.md include THIRD_PARTY_NOTICES.md include CHANGELOG.md include RELEASE.md +recursive-include src/halocli/web_static * diff --git a/README.md b/README.md index 5bc86c3..4b19eb0 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ the same command surface. From the latest GitHub release tag: ```powershell -pipx install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.4.0 +pipx install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.5.0 ``` If you use `uv`: ```powershell -uv tool install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.4.0 +uv tool install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.5.0 ``` From a local checkout: @@ -187,6 +187,24 @@ Create a lightweight Halo Todo backed by Halo's `Appointment` API: halocli todo add "Independent todo list front end for HaloPSA" --owner 37 --due 2026-04-26 --tag microsoft-todo --tag halo-todo ``` +Run the local-first Todo web UI from the same HaloCLI profile: + +```powershell +python -m pip install -e ".[web]" +halocli todo web --profile midtown --host 127.0.0.1 --port 8766 +``` + +The web UI serves a compact three-pane task triage surface over Halo appointment +tasks: quick capture, Inbox/Today/Upcoming/Blocked/Completed views, search, +keyboard selection, completion, detail editing, customer/ticket pickers, work +logs backed by Halo time entries, and source metadata for +imported Microsoft To Do items. HaloPSA remains the system of record; the UI API +returns normalized Todo JSON and does not create a local database. + +Todo priority is currently stored as HaloCLI metadata in the backing +appointment `note_html`; it is not mapped to Halo ticket priority or a native +Halo appointment priority field. + ## Bifrost Compatibility This package does not import Bifrost. Bifrost workflows can shell out to diff --git a/RELEASE.md b/RELEASE.md index a0ebf25..651e893 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -33,8 +33,8 @@ The raw write command should exit nonzero and refuse the request unless ## Tag A Release ```powershell -git tag v0.4.0 -git push origin v0.4.0 +git tag v0.5.0 +git push origin v0.5.0 ``` Pushing a `v*.*.*` tag runs the release workflow, builds the wheel and source @@ -44,8 +44,8 @@ artifacts to a GitHub Release. ## Install From A Release Tag ```powershell -pipx install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.4.0 -uv tool install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.4.0 +pipx install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.5.0 +uv tool install git+https://github.com/Midtown-Technology-Group/halocli.git@v0.5.0 ``` Use the tagged install form for demos and managed rollout scripts so everyone diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 0000000..194b707 --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,188 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test } from "vitest"; +import { App } from "./App"; +import type { TodoApi } from "./api"; + +const todos = [ + { + id: 10038, + title: "Independent todo list front end for HaloPSA", + description: "Build the first UI slice.", + status: "open", + priority: "high", + due_date: "2026-04-27", + owner: 37, + client_id: 12, + ticket_id: 12345, + tags: ["microsoft-todo", "Tasks"], + notes: [], + time_entries: [], + source_metadata: { source: "microsoft.todo" } + }, + { + id: 10039, + title: "Caller verification", + description: "", + status: "open", + priority: "normal", + due_date: null, + owner: 37, + client_id: null, + ticket_id: null, + tags: [], + notes: [], + time_entries: [], + source_metadata: { source: "halocli" } + } +]; + +function fakeApi(): TodoApi { + const state = [...todos]; + return { + async listTodos() { + return { + count: state.length, + items: state.map((item) => ({ + ...item, + notes: [...item.notes], + tags: [...item.tags], + time_entries: [...item.time_entries] + })) + }; + }, + async createTodo(payload) { + const created = { + id: 20000, + description: "", + status: "open", + priority: "normal", + due_date: null, + owner: null, + client_id: null, + ticket_id: null, + tags: [], + notes: [], + time_entries: [], + source_metadata: { source: "halocli" }, + ...payload + }; + state.unshift(created); + return { todo: created }; + }, + async updateTodo(id, payload) { + const item = state.find((todo) => todo.id === id)!; + Object.assign(item, payload); + return { todo: item }; + }, + async completeTodo(id) { + const item = state.find((todo) => todo.id === id)!; + item.status = "done"; + return { todo: item }; + }, + async addNote(id, note) { + const item = state.find((todo) => todo.id === id)!; + item.notes.push({ body: note }); + return { todo: item }; + }, + async logTime(id, payload) { + const item = state.find((todo) => todo.id === id)!; + const entry = { id: 9001, todo_id: id, duration_minutes: payload.minutes ?? 0, note: payload.note }; + item.time_entries.push(entry); + return { time_entry: entry, todo: item }; + }, + async listTimeEntries(id) { + const item = state.find((todo) => todo.id === id)!; + return { count: item.time_entries.length, items: item.time_entries }; + }, + async searchClients() { + return { items: [{ id: 12, name: "Midtown Technology Group" }] }; + }, + async searchTickets(_query, clientId) { + return { items: [{ id: 12345, summary: "Backup alert", client_id: clientId ?? 12, status: "Open" }] }; + }, + async me() { + return { id: 37, name: "Thomas Bray", client_id: 12, client_name: "Midtown Technology Group" }; + } + }; +} + +describe("Halo Todo app", () => { + afterEach(() => cleanup()); + + test("renders the triage layout and selected task detail", async () => { + render(); + + expect(await screen.findByText("Inbox")).toBeInTheDocument(); + expect(screen.getByText("Independent todo list front end for HaloPSA")).toBeInTheDocument(); + expect(screen.getByText("Build the first UI slice.")).toBeInTheDocument(); + expect(screen.getByText("Ticket #12345")).toBeInTheDocument(); + }); + + test("quick-add creates a task and keyboard completion completes the selected task", async () => { + const user = userEvent.setup(); + render(); + + await user.type(await screen.findByLabelText("Quick add title"), "Review imported tasks"); + await user.click(screen.getByRole("button", { name: "Add task" })); + + expect(await screen.findByText("Review imported tasks")).toBeInTheDocument(); + + await user.keyboard("x"); + + await waitFor(() => { + expect(screen.getByText("Done")).toBeInTheDocument(); + }); + }); + + test("quick-add can select a customer and related ticket", async () => { + const user = userEvent.setup(); + render(); + + await user.click(await screen.findByRole("button", { name: "Choose customer" })); + await user.click(screen.getAllByRole("option", { name: "Midtown Technology Group" })[0]); + await user.click(screen.getByRole("button", { name: "Choose ticket" })); + await user.click(screen.getAllByRole("option", { name: "Backup alert" })[0]); + await user.type(screen.getByLabelText("Quick add title"), "Customer-linked task"); + await user.click(screen.getByRole("button", { name: "Add task" })); + + expect(await screen.findByText("Customer-linked task")).toBeInTheDocument(); + expect(screen.getByText("Client #12")).toBeInTheDocument(); + expect(screen.getAllByText("Ticket #12345").length).toBeGreaterThan(0); + }); + + test("work log submits a zero-duration time entry", async () => { + const user = userEvent.setup(); + render(); + + await user.type(await screen.findByLabelText("Work log note"), "Reviewed alert context."); + await user.clear(screen.getByLabelText("Minutes")); + await user.type(screen.getByLabelText("Minutes"), "0"); + await user.click(screen.getByRole("button", { name: "Log work" })); + + expect(await screen.findByText("Reviewed alert context.")).toBeInTheDocument(); + expect(screen.getByText("0 min")).toBeInTheDocument(); + }); + + test("loads work log history for the selected task", async () => { + const api = fakeApi(); + const first = todos[0]; + first.time_entries = [{ id: 9003, todo_id: first.id, duration_minutes: 0, note: "Historical work log" }]; + + render(); + + expect(await screen.findByText("Historical work log")).toBeInTheDocument(); + }); + + test("slash focuses search and filters task list", async () => { + const user = userEvent.setup(); + render(); + + await screen.findByText("Caller verification"); + await user.keyboard("/"); + await user.type(screen.getByLabelText("Search tasks"), "caller"); + + expect(screen.getByText("Caller verification")).toBeInTheDocument(); + expect(screen.queryByText("Independent todo list front end for HaloPSA")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..cce03da --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,408 @@ +import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; +import type { ClientOption, TicketOption, Todo, TodoApi } from "./api"; +import { httpTodoApi } from "./api"; +import "./styles.css"; + +type ViewKey = "open" | "today" | "upcoming" | "blocked" | "done" | "tagged"; + +const views: { key: ViewKey; label: string }[] = [ + { key: "open", label: "Inbox" }, + { key: "today", label: "Today" }, + { key: "upcoming", label: "Upcoming" }, + { key: "blocked", label: "Blocked" }, + { key: "done", label: "Completed" }, + { key: "tagged", label: "Tagged" } +]; + +export function App({ api = httpTodoApi }: { api?: TodoApi }) { + const [todos, setTodos] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [view, setView] = useState("open"); + const [query, setQuery] = useState(""); + const [quickTitle, setQuickTitle] = useState(""); + const [clients, setClients] = useState([]); + const [tickets, setTickets] = useState([]); + const [quickClientId, setQuickClientId] = useState(null); + const [quickTicketId, setQuickTicketId] = useState(null); + const [showClients, setShowClients] = useState(false); + const [showTickets, setShowTickets] = useState(false); + const [busy, setBusy] = useState(false); + const searchRef = useRef(null); + const quickRef = useRef(null); + + async function refresh() { + setBusy(true); + try { + const result = await api.listTodos({ status: view === "done" ? "done" : view === "blocked" ? "blocked" : "open" }); + setTodos(result.items); + setSelectedId((current) => current ?? result.items[0]?.id ?? null); + } finally { + setBusy(false); + } + } + + useEffect(() => { + refresh(); + }, [view]); + + useEffect(() => { + async function loadPickers() { + const [clientResult, me] = await Promise.all([api.searchClients(""), api.me()]); + setClients(clientResult.items); + setQuickClientId(me.client_id ?? clientResult.items[0]?.id ?? null); + } + loadPickers(); + }, []); + + useEffect(() => { + async function loadTickets() { + const result = await api.searchTickets("", quickClientId); + setTickets(result.items); + } + loadTickets(); + }, [quickClientId]); + + const visibleTodos = useMemo(() => { + const today = new Date().toISOString().slice(0, 10); + const needle = query.trim().toLowerCase(); + return todos.filter((todo) => { + if (view === "today" && todo.due_date !== today) return false; + if (view === "upcoming" && (!todo.due_date || todo.due_date <= today)) return false; + if (view === "tagged" && todo.tags.length === 0) return false; + if (!needle) return true; + return ( + todo.title.toLowerCase().includes(needle) || + todo.description.toLowerCase().includes(needle) || + todo.tags.some((tag) => tag.toLowerCase().includes(needle)) + ); + }); + }, [query, todos, view]); + + const selected = visibleTodos.find((todo) => todo.id === selectedId) ?? visibleTodos[0] ?? null; + + useEffect(() => { + if (selected && selected.id !== selectedId) setSelectedId(selected.id); + }, [selected, selectedId]); + + useEffect(() => { + async function loadTimeEntries() { + if (!selected) return; + const result = await api.listTimeEntries(selected.id); + setTodos((items) => + items.map((todo) => (todo.id === selected.id ? { ...todo, time_entries: result.items } : todo)) + ); + } + loadTimeEntries(); + }, [selected?.id]); + + useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + const target = event.target as HTMLElement | null; + const isTyping = target?.tagName === "INPUT" || target?.tagName === "TEXTAREA"; + if (event.key === "/" && !isTyping) { + event.preventDefault(); + searchRef.current?.focus(); + } + if (event.key === "n" && !isTyping) { + event.preventDefault(); + quickRef.current?.focus(); + } + if (event.key === "x" && !isTyping && selected) { + event.preventDefault(); + completeSelected(selected.id); + } + if ((event.key === "j" || event.key === "k") && !isTyping) { + event.preventDefault(); + moveSelection(event.key === "j" ? 1 : -1); + } + } + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [selected, visibleTodos]); + + async function addTask(event: FormEvent) { + event.preventDefault(); + const title = quickTitle.trim(); + if (!title) return; + const result = await api.createTodo({ title, client_id: quickClientId, ticket_id: quickTicketId }); + setTodos((items) => [result.todo, ...items]); + setSelectedId(result.todo.id); + setQuickTitle(""); + quickRef.current?.blur(); + } + + async function completeSelected(id: number) { + const result = await api.completeTodo(id); + setTodos((items) => items.map((todo) => (todo.id === id ? result.todo : todo))); + } + + async function saveSelected(patch: Partial) { + if (!selected) return; + const result = await api.updateTodo(selected.id, patch); + setTodos((items) => items.map((todo) => (todo.id === selected.id ? result.todo : todo))); + } + + function moveSelection(delta: number) { + if (visibleTodos.length === 0) return; + const index = Math.max(0, visibleTodos.findIndex((todo) => todo.id === selected?.id)); + const next = visibleTodos[Math.min(visibleTodos.length - 1, Math.max(0, index + delta))]; + setSelectedId(next.id); + } + + return ( + + + + + + setQuickTitle(event.target.value)} + placeholder="Capture a task" + /> + client.id === quickClientId)?.name ?? "Customer"} + open={showClients} + onToggle={() => setShowClients((value) => !value)} + options={clients.map((client) => ({ id: client.id, label: client.name }))} + onSelect={(id) => { + setQuickClientId(id); + setQuickTicketId(null); + setShowClients(false); + }} + /> + ticket.id === quickTicketId)?.summary ?? "Ticket"} + open={showTickets} + onToggle={() => setShowTickets((value) => !value)} + options={tickets.map((ticket) => ({ id: ticket.id, label: ticket.summary }))} + onSelect={(id) => { + setQuickTicketId(id); + setShowTickets(false); + }} + /> + Add task + + setQuery(event.target.value)} + placeholder="Search" + /> + {busy ? "Loading" : `${visibleTodos.length} tasks`} + + {visibleTodos.map((todo) => ( + setSelectedId(todo.id)} + > + {todo.priority} + {todo.title} + + {todo.due_date ?? "No due date"} {todo.ticket_id ? `Ticket #${todo.ticket_id}` : ""} + + + ))} + + + + { + const result = await api.logTime(id, payload); + setTodos((items) => items.map((todo) => (todo.id === id ? result.todo : todo))); + }} + /> + + ); +} + +function Picker({ + label, + value, + open, + options, + onToggle, + onSelect +}: { + label: string; + value: string; + open: boolean; + options: { id: number; label: string }[]; + onToggle: () => void; + onSelect: (id: number) => void; +}) { + return ( + + + {value} + + {open ? ( + + {options.map((option) => ( + onSelect(option.id)}> + {option.label} + + ))} + + ) : null} + + ); +} + +function TaskDetail({ + todo, + clients, + tickets, + onComplete, + onSave, + onLogTime +}: { + todo: Todo | null; + clients: ClientOption[]; + tickets: TicketOption[]; + onComplete: (id: number) => void; + onSave: (patch: Partial) => void; + onLogTime: (id: number, payload: { note: string; minutes: number; client_id?: number | null; ticket_id?: number | null }) => void; +}) { + const [workNote, setWorkNote] = useState(""); + const [minutes, setMinutes] = useState("0"); + if (!todo) return ; + + function submitWorkLog(event: FormEvent) { + event.preventDefault(); + if (!workNote.trim()) return; + onLogTime(todo.id, { + note: workNote, + minutes: Number(minutes || 0), + client_id: todo.client_id, + ticket_id: todo.ticket_id + }); + setWorkNote(""); + setMinutes("0"); + } + + return ( + + ); +} diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..268d6c6 --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,93 @@ +export type TodoStatus = "open" | "blocked" | "done" | "cancelled" | string; + +export type Todo = { + id: number; + title: string; + description: string; + status: TodoStatus; + priority: string; + due_date: string | null; + owner: number | null; + client_id: number | null; + ticket_id: number | null; + tags: string[]; + notes: { body: string; created_at?: string }[]; + time_entries: TimeEntry[]; + source_metadata: Record; +}; + +export type ClientOption = { id: number; name: string }; +export type TicketOption = { id: number; summary: string; client_id: number | null; status: string | null }; +export type Me = { id: number; name: string; client_id: number | null; client_name: string | null }; +export type TimeEntry = { + id: number; + todo_id: number; + client_id: number | null; + ticket_id: number | null; + note: string; + duration_minutes: number; +}; + +export type TodoApi = { + listTodos(params?: Record): Promise<{ count: number; items: Todo[] }>; + createTodo(payload: Partial & { title: string }): Promise<{ todo: Todo }>; + updateTodo(id: number, payload: Partial): Promise<{ todo: Todo }>; + completeTodo(id: number): Promise<{ todo: Todo }>; + addNote(id: number, note: string): Promise<{ todo: Todo }>; + logTime(id: number, payload: { note: string; minutes?: number; client_id?: number | null; ticket_id?: number | null }): Promise<{ time_entry: TimeEntry; todo: Todo }>; + listTimeEntries(id: number): Promise<{ count: number; items: TimeEntry[] }>; + searchClients(query?: string): Promise<{ count?: number; items: ClientOption[] }>; + searchTickets(query?: string, clientId?: number | null): Promise<{ count?: number; items: TicketOption[] }>; + me(): Promise; +}; + +export const httpTodoApi: TodoApi = { + async listTodos(params = {}) { + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== null && value !== undefined && value !== "") search.set(key, String(value)); + } + return request(`/api/todos?${search.toString()}`); + }, + async createTodo(payload) { + return request("/api/todos", { method: "POST", body: JSON.stringify(payload) }); + }, + async updateTodo(id, payload) { + return request(`/api/todos/${id}`, { method: "PATCH", body: JSON.stringify(payload) }); + }, + async completeTodo(id) { + return request(`/api/todos/${id}/complete`, { method: "POST" }); + }, + async addNote(id, note) { + return request(`/api/todos/${id}/notes`, { method: "POST", body: JSON.stringify({ note }) }); + }, + async logTime(id, payload) { + return request(`/api/todos/${id}/time-entries`, { method: "POST", body: JSON.stringify(payload) }); + }, + async listTimeEntries(id) { + return request(`/api/todos/${id}/time-entries`); + }, + async searchClients(query = "") { + const search = new URLSearchParams(); + if (query) search.set("q", query); + return request(`/api/clients?${search.toString()}`); + }, + async searchTickets(query = "", clientId = null) { + const search = new URLSearchParams(); + if (query) search.set("q", query); + if (clientId !== null && clientId !== undefined) search.set("client_id", String(clientId)); + return request(`/api/tickets?${search.toString()}`); + }, + async me() { + return request("/api/me"); + } +}; + +async function request(path: string, init: RequestInit = {}) { + const response = await fetch(path, { + headers: { "content-type": "application/json", ...(init.headers ?? {}) }, + ...init + }); + if (!response.ok) throw new Error(await response.text()); + return response.json(); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..f258e49 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..e8b03be --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,328 @@ +:root { + color: #17202a; + background: #f5f7f8; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +.app-shell { + display: grid; + grid-template-columns: 216px minmax(360px, 1fr) minmax(360px, 440px); + min-height: 100vh; + background: #f5f7f8; +} + +.rail { + background: #18202a; + color: #f8fafc; + padding: 18px 14px; +} + +.brand { + font-size: 18px; + font-weight: 700; + margin-bottom: 18px; +} + +.rail nav { + display: grid; + gap: 4px; +} + +.rail button { + border: 0; + border-radius: 6px; + background: transparent; + color: #cbd5e1; + padding: 9px 10px; + text-align: left; +} + +.rail button.active, +.rail button:hover { + background: #2b3a4a; + color: #fff; +} + +.list-pane { + border-right: 1px solid #d9e0e7; + padding: 16px; + min-width: 0; +} + +.quick-add { + display: grid; + grid-template-columns: minmax(180px, 1fr) 150px 150px auto; + gap: 8px; + margin-bottom: 10px; +} + +.quick-add input, +.search, +.title-editor, +textarea, +select, +.fields input { + width: 100%; + border: 1px solid #c9d3dd; + border-radius: 6px; + background: #fff; + color: #17202a; + padding: 9px 10px; +} + +.quick-add button, +.detail-actions button { + border: 0; + border-radius: 6px; + background: #1264a3; + color: #fff; + padding: 9px 12px; + font-weight: 650; +} + +.picker { + position: relative; +} + +.picker > button { + width: 100%; + min-height: 37px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.picker-menu { + position: absolute; + z-index: 5; + display: grid; + gap: 2px; + width: 260px; + max-height: 240px; + overflow: auto; + border: 1px solid #c9d3dd; + border-radius: 6px; + background: #fff; + box-shadow: 0 12px 32px #17202a22; + padding: 4px; +} + +.picker-menu button { + border: 0; + border-radius: 4px; + background: transparent; + color: #17202a; + padding: 7px 8px; + text-align: left; +} + +.picker-menu button:hover { + background: #edf2f7; +} + +.search { + margin-bottom: 8px; +} + +.list-meta { + color: #66758a; + font-size: 12px; + margin: 6px 0 10px; +} + +.task-list { + display: grid; + gap: 6px; +} + +.task-row { + display: grid; + grid-template-columns: 64px 1fr; + gap: 4px 10px; + min-height: 70px; + border: 1px solid #dbe3eb; + border-radius: 7px; + background: #fff; + padding: 10px; + text-align: left; +} + +.task-row.selected { + border-color: #1264a3; + box-shadow: inset 3px 0 0 #1264a3; +} + +.priority { + grid-row: 1 / span 2; + align-self: start; + border-radius: 999px; + background: #edf2f7; + color: #4a5568; + padding: 3px 7px; + font-size: 11px; + text-align: center; +} + +.priority.high { + background: #ffe8e8; + color: #b42318; +} + +.task-title { + font-weight: 650; + overflow-wrap: anywhere; +} + +.task-subline { + color: #66758a; + font-size: 12px; +} + +.detail-pane { + padding: 18px; + background: #ffffff; + min-width: 0; +} + +.detail-pane.empty { + color: #66758a; +} + +.detail-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.status { + border-radius: 999px; + background: #e9f5ee; + color: #166534; + padding: 4px 9px; + text-transform: capitalize; + font-size: 12px; + font-weight: 700; +} + +.status.done { + background: #e4eefc; + color: #1d4ed8; +} + +.title-editor { + border: 0; + border-bottom: 1px solid #d9e0e7; + border-radius: 0; + padding-left: 0; + font-size: 22px; + font-weight: 750; + margin-bottom: 12px; +} + +textarea { + min-height: 110px; + resize: vertical; + margin-bottom: 12px; +} + +.fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.fields label { + display: grid; + gap: 4px; + color: #66758a; + font-size: 12px; + font-weight: 650; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 14px 0; +} + +.chips span { + border-radius: 999px; + background: #edf2f7; + color: #324154; + padding: 4px 8px; + font-size: 12px; +} + +.metadata, +.notes { + border-top: 1px solid #e3e9ef; + padding-top: 12px; + margin-top: 12px; +} + +.metadata h2, +.notes h2 { + font-size: 12px; + text-transform: uppercase; + color: #66758a; + margin: 0 0 8px; +} + +.work-log-form { + display: grid; + gap: 8px; +} + +.work-log-form textarea { + min-height: 72px; + margin-bottom: 0; +} + +.work-log-form button { + justify-self: end; + border: 0; + border-radius: 6px; + background: #1264a3; + color: #fff; + padding: 8px 11px; + font-weight: 650; +} + +@media (max-width: 920px) { + .app-shell { + grid-template-columns: 1fr; + } + + .rail { + position: sticky; + top: 0; + z-index: 1; + } + + .rail nav { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/frontend/src/test-setup.ts b/frontend/src/test-setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/frontend/src/test-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/index.html b/index.html new file mode 100644 index 0000000..4547111 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Halo Todo + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..201c35d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2950 @@ +{ + "name": "halocli-todo-web", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "halocli-todo-web", + "dependencies": { + "@vitejs/plugin-react": "^5.1.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "typescript": "^5.9.3", + "vite": "^7.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.1", + "@testing-library/user-event": "^14.6.1", + "jsdom": "^27.3.0", + "vitest": "^4.0.16" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.28" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..39ffe80 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "halocli-todo-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5174", + "build": "vite build", + "test": "vitest run" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.1.2", + "vite": "^7.3.0", + "typescript": "^5.9.3", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.1", + "@testing-library/user-event": "^14.6.1", + "jsdom": "^27.3.0", + "vitest": "^4.0.16" + } +} diff --git a/pyproject.toml b/pyproject.toml index 7da31cc..a6c22ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "halocli" -version = "0.4.0" +version = "0.5.0" description = "Standalone HaloPSA CLI for safe operator and automation workflows" readme = "README.md" license = "GPL-3.0-only" @@ -31,9 +31,15 @@ classifiers = [ [project.optional-dependencies] dev = [ "build>=1.0.0", + "fastapi>=0.111.0", "pytest>=7.0.0", "pytest-asyncio>=0.23.0", "ruff>=0.1.0", + "uvicorn>=0.30.0", +] +web = [ + "fastapi>=0.111.0", + "uvicorn>=0.30.0", ] release = [ "build>=1.0.0", @@ -57,6 +63,9 @@ Changelog = "https://github.com/Midtown-Technology-Group/halocli/blob/main/CHANG [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.package-data] +halocli = ["web_static/*", "web_static/assets/*"] + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] diff --git a/src/halocli/__init__.py b/src/halocli/__init__.py index 059b5d1..02ca830 100644 --- a/src/halocli/__init__.py +++ b/src/halocli/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.4.0" +__version__ = "0.5.0" diff --git a/src/halocli/cli.py b/src/halocli/cli.py index 1003413..1486499 100644 --- a/src/halocli/cli.py +++ b/src/halocli/cli.py @@ -291,6 +291,24 @@ def todo_add( ) +@todo_app.command("web") +def todo_web( + profile: Annotated[str, typer.Option("--profile")] = "default", + host: Annotated[str, typer.Option("--host")] = "127.0.0.1", + port: Annotated[int, typer.Option("--port")] = 8766, + reload: Annotated[bool, typer.Option("--reload")] = False, +) -> None: + try: + import uvicorn + except ModuleNotFoundError as exc: + raise typer.BadParameter("Install halocli with the web extra: pip install 'halocli[web]'.") from exc + + from halocli.todo_web import create_halo_todo_api + + typer.echo(f"Starting Halo Todo web UI at http://{host}:{port}") + uvicorn.run(create_halo_todo_api(profile=profile), host=host, port=port, reload=reload) + + async def _auth_test(*, profile: str, output: str) -> None: halo_profile = load_profile(profile) async with HaloClient(halo_profile, profile_name=profile) as client: diff --git a/src/halocli/todo.py b/src/halocli/todo.py index 9218f0a..a0d4eb2 100644 --- a/src/halocli/todo.py +++ b/src/halocli/todo.py @@ -150,6 +150,10 @@ async def create( description: str = "", owner: int | None = None, due: date | None = None, + priority: str = "normal", + client_id: int | None = None, + site_id: int | None = None, + ticket_id: int | None = None, tags: list[str] | None = None, source_metadata: dict[str, Any] | None = None, ) -> dict[str, Any]: @@ -161,6 +165,10 @@ async def create( description=description, owner=resolved_owner, due=due, + priority=priority, + client_id=client_id, + site_id=site_id, + ticket_id=ticket_id, tags=tags or [], source_metadata=source_metadata or {"source": "halocli"}, ) @@ -168,12 +176,217 @@ async def create( item = first_result(result) or {**payload} return todo_from_appointment(item) + async def list( + self, + *, + status: str | None = "open", + mine: bool = False, + client_id: int | None = None, + ticket_id: int | None = None, + tag: str | None = None, + q: str | None = None, + max_records: int = 200, + ) -> list[dict[str, Any]]: + params: dict[str, Any] = {"page_size": max_records} + if client_id is not None: + params["client_id"] = client_id + if ticket_id is not None: + params["ticket_id"] = ticket_id + if q: + params["search"] = q + if mine: + params["agent_id"] = await self._current_agent_id() + result = await self.halo_client.raw("GET", "/Appointment", params=params) + rows = result_rows(result) + todos = [todo_from_appointment(row) for row in rows if row.get("is_task") is True] + if status: + todos = [todo for todo in todos if todo.get("status") == status] + if tag: + todos = [todo for todo in todos if tag in todo.get("tags", [])] + if q: + needle = q.lower() + todos = [ + todo + for todo in todos + if needle in str(todo.get("title") or "").lower() + or needle in str(todo.get("description") or "").lower() + ] + return todos[:max_records] + + async def get(self, todo_id: int | str) -> dict[str, Any]: + item = await self.halo_client.raw("GET", f"/Appointment/{todo_id}") + return todo_from_appointment(first_result(item) or item) + + async def update( + self, + todo_id: int | str, + *, + title: str | None = None, + description: str | None = None, + status: str | None = None, + priority: str | None = None, + due: date | None = None, + owner: int | None = None, + client_id: int | None = None, + site_id: int | None = None, + ticket_id: int | None = None, + tags: list[str] | None = None, + ) -> dict[str, Any]: + existing = first_result(await self.halo_client.raw("GET", f"/Appointment/{todo_id}")) or {} + metadata = extract_metadata(str(existing.get("note_html") or "")) + merged_tags = tags if tags is not None else list(metadata.get("tags", [])) + merged_metadata = { + **metadata, + "kind": "halocli.todo", + "priority": priority or metadata.get("priority") or "normal", + "status": status or metadata.get("status") or todo_from_appointment(existing).get("status"), + "tags": merged_tags, + } + payload = { + **existing, + "id": int(todo_id), + "subject": title if title is not None else existing.get("subject"), + "note_html": note_html( + description if description is not None else extract_description(str(existing.get("note_html") or "")), + merged_metadata, + ), + "is_task": True, + } + if due is not None: + payload.update(due_fields(due)) + if owner is not None: + payload["agent_id"] = owner + payload["agents"] = [{"id": owner, "use": "agent"}] + for key, value in { + "client_id": client_id, + "site_id": site_id, + "ticket_id": ticket_id, + }.items(): + if value is not None: + payload[key] = value + result = await self.halo_client.raw("POST", "/Appointment", body=[payload]) + return todo_from_appointment(first_result(result) or payload) + + async def complete(self, todo_id: int | str) -> dict[str, Any]: + existing = first_result(await self.halo_client.raw("GET", f"/Appointment/{todo_id}")) or {} + payload = { + **existing, + "id": int(todo_id), + "is_task": True, + "complete_status": 0, + "complete_date": datetime.now().isoformat(timespec="seconds"), + } + result = await self.halo_client.raw("POST", "/Appointment", body=[payload]) + return todo_from_appointment(first_result(result) or payload) + + async def add_note(self, todo_id: int | str, note: str) -> dict[str, Any]: + existing = first_result(await self.halo_client.raw("GET", f"/Appointment/{todo_id}")) or {} + metadata = extract_metadata(str(existing.get("note_html") or "")) + notes = list(metadata.get("notes") or []) + notes.append({"body": note, "created_at": datetime.now().isoformat(timespec="seconds")}) + metadata["notes"] = notes + payload = { + **existing, + "id": int(todo_id), + "is_task": True, + "note_html": note_html(extract_description(str(existing.get("note_html") or "")), metadata), + } + result = await self.halo_client.raw("POST", "/Appointment", body=[payload]) + return todo_from_appointment(first_result(result) or payload) + + async def log_time( + self, + todo_id: int | str, + *, + note: str, + minutes: float | None = None, + hours: float | None = None, + start: datetime | None = None, + end: datetime | None = None, + client_id: int | None = None, + ticket_id: int | None = None, + ) -> dict[str, Any]: + existing = first_result(await self.halo_client.raw("GET", f"/Appointment/{todo_id}")) or {} + resolved_client_id = client_id if client_id is not None else existing.get("client_id") + resolved_ticket_id = ticket_id if ticket_id is not None else existing.get("ticket_id") + agent = await self._current_agent() + if client_id is not None and client_id != existing.get("client_id"): + await self.update(todo_id, client_id=client_id) + duration_minutes = duration_as_minutes(minutes=minutes, hours=hours, start=start, end=end) + payload: dict[str, Any] = { + "subject": f"[Todo #{todo_id}] {existing.get('subject') or 'Todo work'}", + "note": note, + "timetaken": round(duration_minutes / 60, 4), + "lognewticket": False, + "todo_id": int(todo_id), + } + if start is not None: + payload["start_date"] = start.isoformat(timespec="seconds") + if end is not None: + payload["end_date"] = end.isoformat(timespec="seconds") + if resolved_client_id is not None: + payload["client_id"] = int(resolved_client_id) + if resolved_ticket_id is not None: + payload["ticket_id"] = int(resolved_ticket_id) + if agent.get("id") is not None: + payload["agent_id"] = int(agent["id"]) + result = await self.halo_client.raw("POST", "/TimesheetEvent", body=[payload]) + item = first_result(result) or payload + return time_entry_from_halo(item, todo_id=int(todo_id), duration_minutes=duration_minutes) + + async def list_time_entries(self, todo_id: int | str) -> list[dict[str, Any]]: + existing = first_result(await self.halo_client.raw("GET", f"/Appointment/{todo_id}")) or {} + params: dict[str, Any] = {"todo_id": int(todo_id), "page_size": 50} + if existing.get("client_id") is not None: + params["client_id"] = int(existing["client_id"]) + if existing.get("ticket_id") is not None: + params["ticket_id"] = int(existing["ticket_id"]) + rows = result_rows(await self.halo_client.raw("GET", "/TimesheetEvent", params=params)) + marker = f"[Todo #{todo_id}]" + entries = [ + time_entry_from_halo(row, todo_id=int(todo_id), duration_minutes=minutes_from_timetaken(row.get("timetaken"))) + for row in rows + if str(row.get("todo_id") or "") == str(todo_id) or marker in str(row.get("subject") or "") + ] + return entries + + async def search_clients(self, q: str | None = None) -> list[dict[str, Any]]: + params: dict[str, Any] = {"page_size": 25} + if q: + params["search"] = q + rows = result_rows(await self.halo_client.raw("GET", "/Client", params=params)) + return [compact_client(row) for row in rows] + + async def search_tickets( + self, + *, + q: str | None = None, + client_id: int | None = None, + open_only: bool = True, + ) -> list[dict[str, Any]]: + params: dict[str, Any] = {"page_size": 25} + if q: + params["search"] = q + if client_id is not None: + params["client_id"] = client_id + if open_only: + params["open_only"] = True + rows = result_rows(await self.halo_client.raw("GET", "/Tickets", params=params)) + return [compact_ticket(row) for row in rows] + + async def me(self) -> dict[str, Any]: + return compact_agent(await self._current_agent()) + async def _current_agent_id(self) -> int: - agent = await self.halo_client.raw("GET", "/Agent/me") + agent = await self._current_agent() if isinstance(agent, dict) and agent.get("id") is not None: return int(agent["id"]) raise RuntimeError("Could not resolve current Halo agent id.") + async def _current_agent(self) -> dict[str, Any]: + agent = await self.halo_client.raw("GET", "/Agent/me") + return agent if isinstance(agent, dict) else {} + def preview_import( repository: MicrosoftTodoRepository, @@ -289,6 +502,10 @@ def appointment_payload( description: str, owner: int | None, due: date | None, + priority: str, + client_id: int | None, + site_id: int | None, + ticket_id: int | None, tags: list[str], source_metadata: dict[str, Any], ) -> dict[str, Any]: @@ -298,6 +515,8 @@ def appointment_payload( metadata = { **source_metadata, "kind": "halocli.todo", + "priority": priority, + "status": "open", "tags": tags, } payload: dict[str, Any] = { @@ -317,6 +536,13 @@ def appointment_payload( if owner is not None: payload["agent_id"] = owner payload["agents"] = [{"id": owner, "use": "agent"}] + for key, value in { + "client_id": client_id, + "site_id": site_id, + "ticket_id": ticket_id, + }.items(): + if value is not None: + payload[key] = value if due: payload.update(due_fields(due)) else: @@ -326,13 +552,21 @@ def appointment_payload( def todo_from_appointment(item: dict[str, Any]) -> dict[str, Any]: metadata = extract_metadata(str(item.get("note_html") or "")) + status = "done" if str(item.get("complete_status")) == "0" else metadata.get("status", "open") return { "id": item.get("id"), "title": item.get("subject"), - "status": "done" if str(item.get("complete_status")) == "0" else "open", + "description": extract_description(str(item.get("note_html") or "")), + "status": status, + "priority": metadata.get("priority", "normal"), "owner": item.get("agent_id"), + "client_id": item.get("client_id"), + "site_id": item.get("site_id"), + "ticket_id": item.get("ticket_id"), "due_date": parse_halo_date(item.get("start_date")), "tags": metadata.get("tags", []), + "notes": metadata.get("notes", []), + "time_entries": metadata.get("time_entries", []), "source_metadata": metadata, } @@ -429,11 +663,16 @@ def clean_optional(value: Any) -> str | None: def note_html(description: str, metadata: dict[str, Any]) -> str: - body = f"{html.escape(description)}" if description else "" + body = f"{html.escape(description).replace(chr(10), '')}" if description else "" encoded = json.dumps(metadata, sort_keys=True, separators=(",", ":")) return f"{body}" +def extract_description(value: str) -> str: + without_marker = re.sub(r"", "", value or "", flags=re.DOTALL) + return clean_body(without_marker) + + def extract_metadata(value: str) -> dict[str, Any]: match = re.search(r"", value or "", flags=re.DOTALL) if not match: @@ -477,3 +716,75 @@ def first_result(result: Any) -> dict[str, Any] | None: return result[key][0] if result[key] else None return result return None + + +def result_rows(result: Any) -> list[dict[str, Any]]: + if isinstance(result, list): + return [row for row in result if isinstance(row, dict)] + if isinstance(result, dict): + for key in ("items", "appointments", "value", "results"): + rows = result.get(key) + if isinstance(rows, list): + return [row for row in rows if isinstance(row, dict)] + return [result] + return [] + + +def duration_as_minutes( + *, + minutes: float | None = None, + hours: float | None = None, + start: datetime | None = None, + end: datetime | None = None, +) -> float: + if minutes is not None: + return float(minutes) + if hours is not None: + return float(hours) * 60 + if start is not None and end is not None: + return max(0.0, (end - start).total_seconds() / 60) + return 0.0 + + +def time_entry_from_halo(item: dict[str, Any], *, todo_id: int, duration_minutes: float) -> dict[str, Any]: + return { + "id": item.get("id"), + "todo_id": todo_id, + "client_id": item.get("client_id"), + "ticket_id": item.get("ticket_id"), + "agent_id": item.get("agent_id"), + "note": item.get("note") or item.get("note_html") or "", + "duration_minutes": duration_minutes, + "timetaken": item.get("timetaken"), + "start": item.get("start_date"), + "end": item.get("end_date"), + } + + +def minutes_from_timetaken(value: Any) -> float: + try: + return float(value or 0) * 60 + except (TypeError, ValueError): + return 0.0 + + +def compact_client(row: dict[str, Any]) -> dict[str, Any]: + return {"id": row.get("id"), "name": row.get("name") or row.get("client_name") or row.get("display_name")} + + +def compact_ticket(row: dict[str, Any]) -> dict[str, Any]: + return { + "id": row.get("id"), + "summary": row.get("summary") or row.get("subject") or row.get("title"), + "client_id": row.get("client_id"), + "status": row.get("status") or row.get("status_name"), + } + + +def compact_agent(row: dict[str, Any]) -> dict[str, Any]: + return { + "id": row.get("id"), + "name": row.get("name") or row.get("display_name"), + "client_id": row.get("client_id") or row.get("company_id"), + "client_name": row.get("client_name") or row.get("company_name"), + } diff --git a/src/halocli/todo_web.py b/src/halocli/todo_web.py new file mode 100644 index 0000000..30c75e1 --- /dev/null +++ b/src/halocli/todo_web.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +from collections.abc import Callable +from datetime import date, datetime +from importlib import resources +from pathlib import Path +from typing import Any + +from halocli.client import HaloClient +from halocli.config import load_profile +from halocli.todo import HaloTodoRepository + +try: + from fastapi import Depends, FastAPI, HTTPException + from fastapi.responses import FileResponse + from fastapi.staticfiles import StaticFiles + from pydantic import BaseModel +except ModuleNotFoundError: # pragma: no cover + Depends = FastAPI = HTTPException = FileResponse = StaticFiles = BaseModel = None # type: ignore[assignment] + + +if BaseModel is not None: + + class TodoCreate(BaseModel): + title: str + description: str = "" + due_date: date | None = None + priority: str = "normal" + owner: int | None = None + client_id: int | None = None + site_id: int | None = None + ticket_id: int | None = None + tags: list[str] = [] + + class TodoPatch(BaseModel): + title: str | None = None + description: str | None = None + status: str | None = None + due_date: date | None = None + priority: str | None = None + owner: int | None = None + client_id: int | None = None + site_id: int | None = None + ticket_id: int | None = None + tags: list[str] | None = None + + class TodoNote(BaseModel): + note: str + + class TodoTimeEntry(BaseModel): + note: str + minutes: float | None = None + hours: float | None = None + start: datetime | None = None + end: datetime | None = None + client_id: int | None = None + ticket_id: int | None = None + + +def create_todo_api(repository_factory: Callable[[], Any]): + if FastAPI is None or Depends is None: + raise RuntimeError("Install halocli with the web extra: pip install 'halocli[web]'.") + + async def repository(): + return repository_factory() + + app = FastAPI(title="Halo Todo", version="0.1.0") + + @app.get("/api/todos") + async def list_todos( + repo: Any = Depends(repository), + status: str | None = "open", + mine: bool = False, + client_id: int | None = None, + ticket_id: int | None = None, + tag: str | None = None, + q: str | None = None, + max_records: int = 200, + ): + items = await repo.list( + status=status, + mine=mine, + client_id=client_id, + ticket_id=ticket_id, + tag=tag, + q=q, + max_records=max_records, + ) + return {"count": len(items), "items": items} + + @app.get("/api/clients") + async def search_clients(repo: Any = Depends(repository), q: str | None = None): + items = await repo.search_clients(q=q) + return {"count": len(items), "items": items} + + @app.get("/api/tickets") + async def search_tickets( + repo: Any = Depends(repository), + q: str | None = None, + client_id: int | None = None, + open: bool = True, # noqa: A002 + ): + items = await repo.search_tickets(q=q, client_id=client_id, open_only=open) + return {"count": len(items), "items": items} + + @app.get("/api/me") + async def me(repo: Any = Depends(repository)): + return await repo.me() + + @app.post("/api/todos") + async def create_todo(payload: TodoCreate, repo: Any = Depends(repository)): + if not payload.title.strip(): + raise HTTPException(status_code=400, detail="Todo title is required.") + todo = await repo.create( + title=payload.title, + description=payload.description, + owner=payload.owner, + due=payload.due_date, + priority=payload.priority, + client_id=payload.client_id, + site_id=payload.site_id, + ticket_id=payload.ticket_id, + tags=payload.tags, + ) + return {"todo": todo} + + @app.get("/api/todos/{todo_id}") + async def get_todo(todo_id: int, repo: Any = Depends(repository)): + return {"todo": await repo.get(todo_id)} + + @app.patch("/api/todos/{todo_id}") + async def update_todo(todo_id: int, payload: TodoPatch, repo: Any = Depends(repository)): + todo = await repo.update( + todo_id, + title=payload.title, + description=payload.description, + status=payload.status, + due=payload.due_date, + priority=payload.priority, + owner=payload.owner, + client_id=payload.client_id, + site_id=payload.site_id, + ticket_id=payload.ticket_id, + tags=payload.tags, + ) + return {"todo": todo} + + @app.post("/api/todos/{todo_id}/complete") + async def complete_todo(todo_id: int, repo: Any = Depends(repository)): + return {"todo": await repo.complete(todo_id)} + + @app.post("/api/todos/{todo_id}/notes") + async def add_todo_note(todo_id: int, payload: TodoNote, repo: Any = Depends(repository)): + if not payload.note.strip(): + raise HTTPException(status_code=400, detail="Note is required.") + return {"todo": await repo.add_note(todo_id, payload.note)} + + @app.post("/api/todos/{todo_id}/time-entries") + async def add_time_entry(todo_id: int, payload: TodoTimeEntry, repo: Any = Depends(repository)): + if not payload.note.strip(): + raise HTTPException(status_code=400, detail="Work log note is required.") + entry = await repo.log_time( + todo_id, + note=payload.note, + minutes=payload.minutes, + hours=payload.hours, + start=payload.start, + end=payload.end, + client_id=payload.client_id, + ticket_id=payload.ticket_id, + ) + todo = await repo.get(todo_id) + todo["time_entries"] = [entry, *list(todo.get("time_entries") or [])] + return {"time_entry": entry, "todo": todo} + + @app.get("/api/todos/{todo_id}/time-entries") + async def list_time_entries(todo_id: int, repo: Any = Depends(repository)): + items = await repo.list_time_entries(todo_id) + return {"count": len(items), "items": items} + + static_dir = web_static_dir() + if static_dir.exists(): + assets = static_dir / "assets" + if assets.exists(): + app.mount("/assets", StaticFiles(directory=assets), name="assets") + + @app.get("/{path:path}", include_in_schema=False) + async def index(path: str = ""): + return FileResponse(static_dir / "index.html") + + return app + + +def create_halo_todo_api(*, profile: str): + halo_profile = load_profile(profile) + + def repository_factory() -> HaloTodoRepository: + client = HaloClient(halo_profile, profile_name=profile) + return ManagedHaloTodoRepository(client) + + return create_todo_api(repository_factory) + + +class ManagedHaloTodoRepository(HaloTodoRepository): + async def _run(self, name: str, *args: Any, **kwargs: Any) -> Any: + async with self.halo_client: + method = getattr(super(), name) + return await method(*args, **kwargs) + + async def list(self, **filters: Any) -> list[dict[str, Any]]: + return await self._run("list", **filters) + + async def get(self, todo_id: int | str) -> dict[str, Any]: + return await self._run("get", todo_id) + + async def create(self, **data: Any) -> dict[str, Any]: + return await self._run("create", **data) + + async def update(self, todo_id: int | str, **patch: Any) -> dict[str, Any]: + return await self._run("update", todo_id, **patch) + + async def complete(self, todo_id: int | str) -> dict[str, Any]: + return await self._run("complete", todo_id) + + async def add_note(self, todo_id: int | str, note: str) -> dict[str, Any]: + return await self._run("add_note", todo_id, note) + + async def log_time(self, todo_id: int | str, **payload: Any) -> dict[str, Any]: + return await self._run("log_time", todo_id, **payload) + + async def list_time_entries(self, todo_id: int | str) -> list[dict[str, Any]]: + return await self._run("list_time_entries", todo_id) + + async def search_clients(self, q: str | None = None) -> list[dict[str, Any]]: + return await self._run("search_clients", q) + + async def search_tickets( + self, + *, + q: str | None = None, + client_id: int | None = None, + open_only: bool = True, + ) -> list[dict[str, Any]]: + return await self._run("search_tickets", q=q, client_id=client_id, open_only=open_only) + + async def me(self) -> dict[str, Any]: + return await self._run("me") + + +def web_static_dir() -> Path: + return Path(str(resources.files("halocli") / "web_static")) diff --git a/src/halocli/web_static/assets/index-0elXQOnn.css b/src/halocli/web_static/assets/index-0elXQOnn.css new file mode 100644 index 0000000..3f7db48 --- /dev/null +++ b/src/halocli/web_static/assets/index-0elXQOnn.css @@ -0,0 +1 @@ +:root{color:#17202a;background:#f5f7f8;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px}*{box-sizing:border-box}body{margin:0}button,input,textarea,select{font:inherit}button{cursor:pointer}.app-shell{display:grid;grid-template-columns:216px minmax(360px,1fr) minmax(360px,440px);min-height:100vh;background:#f5f7f8}.rail{background:#18202a;color:#f8fafc;padding:18px 14px}.brand{font-size:18px;font-weight:700;margin-bottom:18px}.rail nav{display:grid;gap:4px}.rail button{border:0;border-radius:6px;background:transparent;color:#cbd5e1;padding:9px 10px;text-align:left}.rail button.active,.rail button:hover{background:#2b3a4a;color:#fff}.list-pane{border-right:1px solid #d9e0e7;padding:16px;min-width:0}.quick-add{display:grid;grid-template-columns:minmax(180px,1fr) 150px 150px auto;gap:8px;margin-bottom:10px}.quick-add input,.search,.title-editor,textarea,select,.fields input{width:100%;border:1px solid #c9d3dd;border-radius:6px;background:#fff;color:#17202a;padding:9px 10px}.quick-add button,.detail-actions button{border:0;border-radius:6px;background:#1264a3;color:#fff;padding:9px 12px;font-weight:650}.picker{position:relative}.picker>button{width:100%;min-height:37px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picker-menu{position:absolute;z-index:5;display:grid;gap:2px;width:260px;max-height:240px;overflow:auto;border:1px solid #c9d3dd;border-radius:6px;background:#fff;box-shadow:0 12px 32px #17202a22;padding:4px}.picker-menu button{border:0;border-radius:4px;background:transparent;color:#17202a;padding:7px 8px;text-align:left}.picker-menu button:hover{background:#edf2f7}.search{margin-bottom:8px}.list-meta{color:#66758a;font-size:12px;margin:6px 0 10px}.task-list{display:grid;gap:6px}.task-row{display:grid;grid-template-columns:64px 1fr;gap:4px 10px;min-height:70px;border:1px solid #dbe3eb;border-radius:7px;background:#fff;padding:10px;text-align:left}.task-row.selected{border-color:#1264a3;box-shadow:inset 3px 0 #1264a3}.priority{grid-row:1 / span 2;align-self:start;border-radius:999px;background:#edf2f7;color:#4a5568;padding:3px 7px;font-size:11px;text-align:center}.priority.high{background:#ffe8e8;color:#b42318}.task-title{font-weight:650;overflow-wrap:anywhere}.task-subline{color:#66758a;font-size:12px}.detail-pane{padding:18px;background:#fff;min-width:0}.detail-pane.empty{color:#66758a}.detail-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.status{border-radius:999px;background:#e9f5ee;color:#166534;padding:4px 9px;text-transform:capitalize;font-size:12px;font-weight:700}.status.done{background:#e4eefc;color:#1d4ed8}.title-editor{border:0;border-bottom:1px solid #d9e0e7;border-radius:0;padding-left:0;font-size:22px;font-weight:750;margin-bottom:12px}textarea{min-height:110px;resize:vertical;margin-bottom:12px}.fields{display:grid;grid-template-columns:1fr 1fr;gap:10px}.fields label{display:grid;gap:4px;color:#66758a;font-size:12px;font-weight:650}.chips{display:flex;flex-wrap:wrap;gap:6px;margin:14px 0}.chips span{border-radius:999px;background:#edf2f7;color:#324154;padding:4px 8px;font-size:12px}.metadata,.notes{border-top:1px solid #e3e9ef;padding-top:12px;margin-top:12px}.metadata h2,.notes h2{font-size:12px;text-transform:uppercase;color:#66758a;margin:0 0 8px}.work-log-form{display:grid;gap:8px}.work-log-form textarea{min-height:72px;margin-bottom:0}.work-log-form button{justify-self:end;border:0;border-radius:6px;background:#1264a3;color:#fff;padding:8px 11px;font-weight:650}@media(max-width:920px){.app-shell{grid-template-columns:1fr}.rail{position:sticky;top:0;z-index:1}.rail nav{grid-template-columns:repeat(3,1fr)}} diff --git a/src/halocli/web_static/assets/index-BtClEaws.js b/src/halocli/web_static/assets/index-BtClEaws.js new file mode 100644 index 0000000..2baa93f --- /dev/null +++ b/src/halocli/web_static/assets/index-BtClEaws.js @@ -0,0 +1,9 @@ +(function(){const G=document.createElement("link").relList;if(G&&G.supports&&G.supports("modulepreload"))return;for(const B of document.querySelectorAll('link[rel="modulepreload"]'))o(B);new MutationObserver(B=>{for(const Q of B)if(Q.type==="childList")for(const tl of Q.addedNodes)tl.tagName==="LINK"&&tl.rel==="modulepreload"&&o(tl)}).observe(document,{childList:!0,subtree:!0});function x(B){const Q={};return B.integrity&&(Q.integrity=B.integrity),B.referrerPolicy&&(Q.referrerPolicy=B.referrerPolicy),B.crossOrigin==="use-credentials"?Q.credentials="include":B.crossOrigin==="anonymous"?Q.credentials="omit":Q.credentials="same-origin",Q}function o(B){if(B.ep)return;B.ep=!0;const Q=x(B);fetch(B.href,Q)}})();var ic={exports:{}},Te={};var oy;function Wv(){if(oy)return Te;oy=1;var T=Symbol.for("react.transitional.element"),G=Symbol.for("react.fragment");function x(o,B,Q){var tl=null;if(Q!==void 0&&(tl=""+Q),B.key!==void 0&&(tl=""+B.key),"key"in B){Q={};for(var gl in B)gl!=="key"&&(Q[gl]=B[gl])}else Q=B;return B=Q.ref,{$$typeof:T,type:o,key:tl,ref:B!==void 0?B:null,props:Q}}return Te.Fragment=G,Te.jsx=x,Te.jsxs=x,Te}var hy;function kv(){return hy||(hy=1,ic.exports=Wv()),ic.exports}var N=kv(),fc={exports:{}},X={};var Sy;function $v(){if(Sy)return X;Sy=1;var T=Symbol.for("react.transitional.element"),G=Symbol.for("react.portal"),x=Symbol.for("react.fragment"),o=Symbol.for("react.strict_mode"),B=Symbol.for("react.profiler"),Q=Symbol.for("react.consumer"),tl=Symbol.for("react.context"),gl=Symbol.for("react.forward_ref"),R=Symbol.for("react.suspense"),_=Symbol.for("react.memo"),F=Symbol.for("react.lazy"),O=Symbol.for("react.activity"),il=Symbol.iterator;function Cl(s){return s===null||typeof s!="object"?null:(s=il&&s[il]||s["@@iterator"],typeof s=="function"?s:null)}var Ql={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Al=Object.assign,yt={};function Xl(s,A,p){this.props=s,this.context=A,this.refs=yt,this.updater=p||Ql}Xl.prototype.isReactComponent={},Xl.prototype.setState=function(s,A){if(typeof s!="object"&&typeof s!="function"&&s!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,s,A,"setState")},Xl.prototype.forceUpdate=function(s){this.updater.enqueueForceUpdate(this,s,"forceUpdate")};function _t(){}_t.prototype=Xl.prototype;function Ul(s,A,p){this.props=s,this.context=A,this.refs=yt,this.updater=p||Ql}var Ll=Ul.prototype=new _t;Ll.constructor=Ul,Al(Ll,Xl.prototype),Ll.isPureReactComponent=!0;var mt=Array.isArray;function Rl(){}var K={H:null,A:null,T:null,S:null},ql=Object.prototype.hasOwnProperty;function lt(s,A,p){var H=p.ref;return{$$typeof:T,type:s,key:A,ref:H!==void 0?H:null,props:p}}function Ut(s,A){return lt(s.type,A,s.props)}function vt(s){return typeof s=="object"&&s!==null&&s.$$typeof===T}function ol(s){var A={"=":"=0",":":"=2"};return"$"+s.replace(/[=:]/g,function(p){return A[p]})}var yl=/\/+/g;function Ot(s,A){return typeof s=="object"&&s!==null&&s.key!=null?ol(""+s.key):A.toString(36)}function Jl(s){switch(s.status){case"fulfilled":return s.value;case"rejected":throw s.reason;default:switch(typeof s.status=="string"?s.then(Rl,Rl):(s.status="pending",s.then(function(A){s.status==="pending"&&(s.status="fulfilled",s.value=A)},function(A){s.status==="pending"&&(s.status="rejected",s.reason=A)})),s.status){case"fulfilled":return s.value;case"rejected":throw s.reason}}throw s}function r(s,A,p,H,Z){var J=typeof s;(J==="undefined"||J==="boolean")&&(s=null);var ul=!1;if(s===null)ul=!0;else switch(J){case"bigint":case"string":case"number":ul=!0;break;case"object":switch(s.$$typeof){case T:case G:ul=!0;break;case F:return ul=s._init,r(ul(s._payload),A,p,H,Z)}}if(ul)return Z=Z(s),ul=H===""?"."+Ot(s,0):H,mt(Z)?(p="",ul!=null&&(p=ul.replace(yl,"$&/")+"/"),r(Z,A,p,"",function(pu){return pu})):Z!=null&&(vt(Z)&&(Z=Ut(Z,p+(Z.key==null||s&&s.key===Z.key?"":(""+Z.key).replace(yl,"$&/")+"/")+ul)),A.push(Z)),1;ul=0;var Vl=H===""?".":H+":";if(mt(s))for(var Tl=0;Tl>>1,j=r[D];if(0>>1;DB(p,E))HB(Z,p)?(r[D]=Z,r[H]=E,D=H):(r[D]=p,r[A]=E,D=A);else if(HB(Z,E))r[D]=Z,r[H]=E,D=H;else break l}}return M}function B(r,M){var E=r.sortIndex-M.sortIndex;return E!==0?E:r.id-M.id}if(T.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var Q=performance;T.unstable_now=function(){return Q.now()}}else{var tl=Date,gl=tl.now();T.unstable_now=function(){return tl.now()-gl}}var R=[],_=[],F=1,O=null,il=3,Cl=!1,Ql=!1,Al=!1,yt=!1,Xl=typeof setTimeout=="function"?setTimeout:null,_t=typeof clearTimeout=="function"?clearTimeout:null,Ul=typeof setImmediate<"u"?setImmediate:null;function Ll(r){for(var M=x(_);M!==null;){if(M.callback===null)o(_);else if(M.startTime<=r)o(_),M.sortIndex=M.expirationTime,G(R,M);else break;M=x(_)}}function mt(r){if(Al=!1,Ll(r),!Ql)if(x(R)!==null)Ql=!0,Rl||(Rl=!0,ol());else{var M=x(_);M!==null&&Jl(mt,M.startTime-r)}}var Rl=!1,K=-1,ql=5,lt=-1;function Ut(){return yt?!0:!(T.unstable_now()-ltr&&Ut());){var D=O.callback;if(typeof D=="function"){O.callback=null,il=O.priorityLevel;var j=D(O.expirationTime<=r);if(r=T.unstable_now(),typeof j=="function"){O.callback=j,Ll(r),M=!0;break t}O===x(R)&&o(R),Ll(r)}else o(R);O=x(R)}if(O!==null)M=!0;else{var s=x(_);s!==null&&Jl(mt,s.startTime-r),M=!1}}break l}finally{O=null,il=E,Cl=!1}M=void 0}}finally{M?ol():Rl=!1}}}var ol;if(typeof Ul=="function")ol=function(){Ul(vt)};else if(typeof MessageChannel<"u"){var yl=new MessageChannel,Ot=yl.port2;yl.port1.onmessage=vt,ol=function(){Ot.postMessage(null)}}else ol=function(){Xl(vt,0)};function Jl(r,M){K=Xl(function(){r(T.unstable_now())},M)}T.unstable_IdlePriority=5,T.unstable_ImmediatePriority=1,T.unstable_LowPriority=4,T.unstable_NormalPriority=3,T.unstable_Profiling=null,T.unstable_UserBlockingPriority=2,T.unstable_cancelCallback=function(r){r.callback=null},T.unstable_forceFrameRate=function(r){0>r||125D?(r.sortIndex=E,G(_,r),x(R)===null&&r===x(_)&&(Al?(_t(K),K=-1):Al=!0,Jl(mt,E-D))):(r.sortIndex=j,G(R,r),Ql||Cl||(Ql=!0,Rl||(Rl=!0,ol()))),r},T.unstable_shouldYield=Ut,T.unstable_wrapCallback=function(r){var M=il;return function(){var E=il;il=M;try{return r.apply(this,arguments)}finally{il=E}}}})(dc)),dc}var by;function Iv(){return by||(by=1,sc.exports=Fv()),sc.exports}var yc={exports:{}},Zl={};var Ty;function Pv(){if(Ty)return Zl;Ty=1;var T=mc();function G(R){var _="https://react.dev/errors/"+R;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(T)}catch(G){console.error(G)}}return T(),yc.exports=Pv(),yc.exports}var Ey;function to(){if(Ey)return ze;Ey=1;var T=Iv(),G=mc(),x=lo();function o(l){var t="https://react.dev/errors/"+l;if(1j||(l.current=D[j],D[j]=null,j--)}function p(l,t){j++,D[j]=l.current,l.current=t}var H=s(null),Z=s(null),J=s(null),ul=s(null);function Vl(l,t){switch(p(J,t),p(Z,l),p(H,null),t.nodeType){case 9:case 11:l=(l=t.documentElement)&&(l=l.namespaceURI)?Yd(l):0;break;default:if(l=t.tagName,t=t.namespaceURI)t=Yd(t),l=xd(t,l);else switch(l){case"svg":l=1;break;case"math":l=2;break;default:l=0}}A(H),p(H,l)}function Tl(){A(H),A(Z),A(J)}function pu(l){l.memoizedState!==null&&p(ul,l);var t=H.current,a=xd(t,l.type);t!==a&&(p(Z,l),p(H,a))}function Ee(l){Z.current===l&&(A(H),A(Z)),ul.current===l&&(A(ul),Se._currentValue=E)}var Xn,vc;function _a(l){if(Xn===void 0)try{throw Error()}catch(a){var t=a.stack.trim().match(/\n( *(at )?)/);Xn=t&&t[1]||"",vc=-1)":-1e||c[u]!==v[e]){var g=` +`+c[u].replace(" at new "," at ");return l.displayName&&g.includes("")&&(g=g.replace("",l.displayName)),g}while(1<=u&&0<=e);break}}}finally{Zn=!1,Error.prepareStackTrace=a}return(a=l?l.displayName||l.name:"")?_a(a):""}function Oy(l,t){switch(l.tag){case 26:case 27:case 5:return _a(l.type);case 16:return _a("Lazy");case 13:return l.child!==t&&t!==null?_a("Suspense Fallback"):_a("Suspense");case 19:return _a("SuspenseList");case 0:case 15:return Ln(l.type,!1);case 11:return Ln(l.type.render,!1);case 1:return Ln(l.type,!0);case 31:return _a("Activity");default:return""}}function oc(l){try{var t="",a=null;do t+=Oy(l,a),a=l,l=l.return;while(l);return t}catch(u){return` +Error generating stack: `+u.message+` +`+u.stack}}var Vn=Object.prototype.hasOwnProperty,Kn=T.unstable_scheduleCallback,Jn=T.unstable_cancelCallback,My=T.unstable_shouldYield,py=T.unstable_requestPaint,tt=T.unstable_now,Dy=T.unstable_getCurrentPriorityLevel,hc=T.unstable_ImmediatePriority,Sc=T.unstable_UserBlockingPriority,Ae=T.unstable_NormalPriority,Uy=T.unstable_LowPriority,gc=T.unstable_IdlePriority,Ny=T.log,Hy=T.unstable_setDisableYieldValue,Du=null,at=null;function Pt(l){if(typeof Ny=="function"&&Hy(l),at&&typeof at.setStrictMode=="function")try{at.setStrictMode(Du,l)}catch{}}var ut=Math.clz32?Math.clz32:qy,Cy=Math.log,Ry=Math.LN2;function qy(l){return l>>>=0,l===0?32:31-(Cy(l)/Ry|0)|0}var _e=256,Oe=262144,Me=4194304;function Oa(l){var t=l&42;if(t!==0)return t;switch(l&-l){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return l&261888;case 262144:case 524288:case 1048576:case 2097152:return l&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return l&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return l}}function pe(l,t,a){var u=l.pendingLanes;if(u===0)return 0;var e=0,n=l.suspendedLanes,i=l.pingedLanes;l=l.warmLanes;var f=u&134217727;return f!==0?(u=f&~n,u!==0?e=Oa(u):(i&=f,i!==0?e=Oa(i):a||(a=f&~l,a!==0&&(e=Oa(a))))):(f=u&~n,f!==0?e=Oa(f):i!==0?e=Oa(i):a||(a=u&~l,a!==0&&(e=Oa(a)))),e===0?0:t!==0&&t!==e&&(t&n)===0&&(n=e&-e,a=t&-t,n>=a||n===32&&(a&4194048)!==0)?t:e}function Uu(l,t){return(l.pendingLanes&~(l.suspendedLanes&~l.pingedLanes)&t)===0}function jy(l,t){switch(l){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function rc(){var l=Me;return Me<<=1,(Me&62914560)===0&&(Me=4194304),l}function wn(l){for(var t=[],a=0;31>a;a++)t.push(l);return t}function Nu(l,t){l.pendingLanes|=t,t!==268435456&&(l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0)}function By(l,t,a,u,e,n){var i=l.pendingLanes;l.pendingLanes=a,l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0,l.expiredLanes&=a,l.entangledLanes&=a,l.errorRecoveryDisabledLanes&=a,l.shellSuspendCounter=0;var f=l.entanglements,c=l.expirationTimes,v=l.hiddenUpdates;for(a=i&~a;0"u")return null;try{return l.activeElement||l.body}catch{return l.body}}var Zy=/[\n"\\]/g;function ht(l){return l.replace(Zy,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Pn(l,t,a,u,e,n,i,f){l.name="",i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"?l.type=i:l.removeAttribute("type"),t!=null?i==="number"?(t===0&&l.value===""||l.value!=t)&&(l.value=""+ot(t)):l.value!==""+ot(t)&&(l.value=""+ot(t)):i!=="submit"&&i!=="reset"||l.removeAttribute("value"),t!=null?li(l,i,ot(t)):a!=null?li(l,i,ot(a)):u!=null&&l.removeAttribute("value"),e==null&&n!=null&&(l.defaultChecked=!!n),e!=null&&(l.checked=e&&typeof e!="function"&&typeof e!="symbol"),f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?l.name=""+ot(f):l.removeAttribute("name")}function Hc(l,t,a,u,e,n,i,f){if(n!=null&&typeof n!="function"&&typeof n!="symbol"&&typeof n!="boolean"&&(l.type=n),t!=null||a!=null){if(!(n!=="submit"&&n!=="reset"||t!=null)){In(l);return}a=a!=null?""+ot(a):"",t=t!=null?""+ot(t):a,f||t===l.value||(l.value=t),l.defaultValue=t}u=u??e,u=typeof u!="function"&&typeof u!="symbol"&&!!u,l.checked=f?l.checked:!!u,l.defaultChecked=!!u,i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(l.name=i),In(l)}function li(l,t,a){t==="number"&&Ne(l.ownerDocument)===l||l.defaultValue===""+a||(l.defaultValue=""+a)}function Wa(l,t,a,u){if(l=l.options,t){t={};for(var e=0;e"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ni=!1;if(Yt)try{var qu={};Object.defineProperty(qu,"passive",{get:function(){ni=!0}}),window.addEventListener("test",qu,qu),window.removeEventListener("test",qu,qu)}catch{ni=!1}var ta=null,ii=null,Ce=null;function xc(){if(Ce)return Ce;var l,t=ii,a=t.length,u,e="value"in ta?ta.value:ta.textContent,n=e.length;for(l=0;l=Yu),Vc=" ",Kc=!1;function Jc(l,t){switch(l){case"keyup":return Sm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function wc(l){return l=l.detail,typeof l=="object"&&"data"in l?l.data:null}var Ia=!1;function rm(l,t){switch(l){case"compositionend":return wc(t);case"keypress":return t.which!==32?null:(Kc=!0,Vc);case"textInput":return l=t.data,l===Vc&&Kc?null:l;default:return null}}function bm(l,t){if(Ia)return l==="compositionend"||!yi&&Jc(l,t)?(l=xc(),Ce=ii=ta=null,Ia=!1,l):null;switch(l){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:a,offset:t-l};l=u}l:{for(;a;){if(a.nextSibling){a=a.nextSibling;break l}a=a.parentNode}a=void 0}a=ts(a)}}function us(l,t){return l&&t?l===t?!0:l&&l.nodeType===3?!1:t&&t.nodeType===3?us(l,t.parentNode):"contains"in l?l.contains(t):l.compareDocumentPosition?!!(l.compareDocumentPosition(t)&16):!1:!1}function es(l){l=l!=null&&l.ownerDocument!=null&&l.ownerDocument.defaultView!=null?l.ownerDocument.defaultView:window;for(var t=Ne(l.document);t instanceof l.HTMLIFrameElement;){try{var a=typeof t.contentWindow.location.href=="string"}catch{a=!1}if(a)l=t.contentWindow;else break;t=Ne(l.document)}return t}function oi(l){var t=l&&l.nodeName&&l.nodeName.toLowerCase();return t&&(t==="input"&&(l.type==="text"||l.type==="search"||l.type==="tel"||l.type==="url"||l.type==="password")||t==="textarea"||l.contentEditable==="true")}var pm=Yt&&"documentMode"in document&&11>=document.documentMode,Pa=null,hi=null,Xu=null,Si=!1;function ns(l,t,a){var u=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;Si||Pa==null||Pa!==Ne(u)||(u=Pa,"selectionStart"in u&&oi(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),Xu&&Qu(Xu,u)||(Xu=u,u=Mn(hi,"onSelect"),0>=i,e-=i,Nt=1<<32-ut(t)+e|a<V?($=C,C=null):$=C.sibling;var ll=h(y,C,m[V],b);if(ll===null){C===null&&(C=$);break}l&&C&&ll.alternate===null&&t(y,C),d=n(ll,d,V),P===null?q=ll:P.sibling=ll,P=ll,C=$}if(V===m.length)return a(y,C),I&&Gt(y,V),q;if(C===null){for(;VV?($=C,C=null):$=C.sibling;var Aa=h(y,C,ll.value,b);if(Aa===null){C===null&&(C=$);break}l&&C&&Aa.alternate===null&&t(y,C),d=n(Aa,d,V),P===null?q=Aa:P.sibling=Aa,P=Aa,C=$}if(ll.done)return a(y,C),I&&Gt(y,V),q;if(C===null){for(;!ll.done;V++,ll=m.next())ll=z(y,ll.value,b),ll!==null&&(d=n(ll,d,V),P===null?q=ll:P.sibling=ll,P=ll);return I&&Gt(y,V),q}for(C=u(C);!ll.done;V++,ll=m.next())ll=S(C,y,V,ll.value,b),ll!==null&&(l&&ll.alternate!==null&&C.delete(ll.key===null?V:ll.key),d=n(ll,d,V),P===null?q=ll:P.sibling=ll,P=ll);return l&&C.forEach(function(wv){return t(y,wv)}),I&&Gt(y,V),q}function sl(y,d,m,b){if(typeof m=="object"&&m!==null&&m.type===Al&&m.key===null&&(m=m.props.children),typeof m=="object"&&m!==null){switch(m.$$typeof){case Cl:l:{for(var q=m.key;d!==null;){if(d.key===q){if(q=m.type,q===Al){if(d.tag===7){a(y,d.sibling),b=e(d,m.props.children),b.return=y,y=b;break l}}else if(d.elementType===q||typeof q=="object"&&q!==null&&q.$$typeof===ql&&Ba(q)===d.type){a(y,d.sibling),b=e(d,m.props),wu(b,m),b.return=y,y=b;break l}a(y,d);break}else t(y,d);d=d.sibling}m.type===Al?(b=Ha(m.props.children,y.mode,b,m.key),b.return=y,y=b):(b=Ze(m.type,m.key,m.props,null,y.mode,b),wu(b,m),b.return=y,y=b)}return i(y);case Ql:l:{for(q=m.key;d!==null;){if(d.key===q)if(d.tag===4&&d.stateNode.containerInfo===m.containerInfo&&d.stateNode.implementation===m.implementation){a(y,d.sibling),b=e(d,m.children||[]),b.return=y,y=b;break l}else{a(y,d);break}else t(y,d);d=d.sibling}b=Ai(m,y.mode,b),b.return=y,y=b}return i(y);case ql:return m=Ba(m),sl(y,d,m,b)}if(Jl(m))return U(y,d,m,b);if(ol(m)){if(q=ol(m),typeof q!="function")throw Error(o(150));return m=q.call(m),Y(y,d,m,b)}if(typeof m.then=="function")return sl(y,d,ke(m),b);if(m.$$typeof===Ul)return sl(y,d,Ke(y,m),b);$e(y,m)}return typeof m=="string"&&m!==""||typeof m=="number"||typeof m=="bigint"?(m=""+m,d!==null&&d.tag===6?(a(y,d.sibling),b=e(d,m),b.return=y,y=b):(a(y,d),b=Ei(m,y.mode,b),b.return=y,y=b),i(y)):a(y,d)}return function(y,d,m,b){try{Ju=0;var q=sl(y,d,m,b);return du=null,q}catch(C){if(C===su||C===we)throw C;var P=nt(29,C,null,y.mode);return P.lanes=b,P.return=y,P}}}var xa=Ds(!0),Us=Ds(!1),ia=!1;function ji(l){l.updateQueue={baseState:l.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Bi(l,t){l=l.updateQueue,t.updateQueue===l&&(t.updateQueue={baseState:l.baseState,firstBaseUpdate:l.firstBaseUpdate,lastBaseUpdate:l.lastBaseUpdate,shared:l.shared,callbacks:null})}function fa(l){return{lane:l,tag:0,payload:null,callback:null,next:null}}function ca(l,t,a){var u=l.updateQueue;if(u===null)return null;if(u=u.shared,(al&2)!==0){var e=u.pending;return e===null?t.next=t:(t.next=e.next,e.next=t),u.pending=t,t=Xe(l),ms(l,null,a),t}return Qe(l,u,t,a),Xe(l)}function Wu(l,t,a){if(t=t.updateQueue,t!==null&&(t=t.shared,(a&4194048)!==0)){var u=t.lanes;u&=l.pendingLanes,a|=u,t.lanes=a,Tc(l,a)}}function Yi(l,t){var a=l.updateQueue,u=l.alternate;if(u!==null&&(u=u.updateQueue,a===u)){var e=null,n=null;if(a=a.firstBaseUpdate,a!==null){do{var i={lane:a.lane,tag:a.tag,payload:a.payload,callback:null,next:null};n===null?e=n=i:n=n.next=i,a=a.next}while(a!==null);n===null?e=n=t:n=n.next=t}else e=n=t;a={baseState:u.baseState,firstBaseUpdate:e,lastBaseUpdate:n,shared:u.shared,callbacks:u.callbacks},l.updateQueue=a;return}l=a.lastBaseUpdate,l===null?a.firstBaseUpdate=t:l.next=t,a.lastBaseUpdate=t}var xi=!1;function ku(){if(xi){var l=cu;if(l!==null)throw l}}function $u(l,t,a,u){xi=!1;var e=l.updateQueue;ia=!1;var n=e.firstBaseUpdate,i=e.lastBaseUpdate,f=e.shared.pending;if(f!==null){e.shared.pending=null;var c=f,v=c.next;c.next=null,i===null?n=v:i.next=v,i=c;var g=l.alternate;g!==null&&(g=g.updateQueue,f=g.lastBaseUpdate,f!==i&&(f===null?g.firstBaseUpdate=v:f.next=v,g.lastBaseUpdate=c))}if(n!==null){var z=e.baseState;i=0,g=v=c=null,f=n;do{var h=f.lane&-536870913,S=h!==f.lane;if(S?(k&h)===h:(u&h)===h){h!==0&&h===fu&&(xi=!0),g!==null&&(g=g.next={lane:0,tag:f.tag,payload:f.payload,callback:null,next:null});l:{var U=l,Y=f;h=t;var sl=a;switch(Y.tag){case 1:if(U=Y.payload,typeof U=="function"){z=U.call(sl,z,h);break l}z=U;break l;case 3:U.flags=U.flags&-65537|128;case 0:if(U=Y.payload,h=typeof U=="function"?U.call(sl,z,h):U,h==null)break l;z=O({},z,h);break l;case 2:ia=!0}}h=f.callback,h!==null&&(l.flags|=64,S&&(l.flags|=8192),S=e.callbacks,S===null?e.callbacks=[h]:S.push(h))}else S={lane:h,tag:f.tag,payload:f.payload,callback:f.callback,next:null},g===null?(v=g=S,c=z):g=g.next=S,i|=h;if(f=f.next,f===null){if(f=e.shared.pending,f===null)break;S=f,f=S.next,S.next=null,e.lastBaseUpdate=S,e.shared.pending=null}}while(!0);g===null&&(c=z),e.baseState=c,e.firstBaseUpdate=v,e.lastBaseUpdate=g,n===null&&(e.shared.lanes=0),va|=i,l.lanes=i,l.memoizedState=z}}function Ns(l,t){if(typeof l!="function")throw Error(o(191,l));l.call(t)}function Hs(l,t){var a=l.callbacks;if(a!==null)for(l.callbacks=null,l=0;ln?n:8;var i=r.T,f={};r.T=f,uf(l,!1,t,a);try{var c=e(),v=r.S;if(v!==null&&v(f,c),c!==null&&typeof c=="object"&&typeof c.then=="function"){var g=Bm(c,u);Pu(l,t,g,dt(l))}else Pu(l,t,u,dt(l))}catch(z){Pu(l,t,{then:function(){},status:"rejected",reason:z},dt())}finally{M.p=n,i!==null&&f.types!==null&&(i.types=f.types),r.T=i}}function Zm(){}function tf(l,t,a,u){if(l.tag!==5)throw Error(o(476));var e=s0(l).queue;c0(l,e,t,E,a===null?Zm:function(){return d0(l),a(u)})}function s0(l){var t=l.memoizedState;if(t!==null)return t;t={memoizedState:E,baseState:E,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Lt,lastRenderedState:E},next:null};var a={};return t.next={memoizedState:a,baseState:a,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Lt,lastRenderedState:a},next:null},l.memoizedState=t,l=l.alternate,l!==null&&(l.memoizedState=t),t}function d0(l){var t=s0(l);t.next===null&&(t=l.alternate.memoizedState),Pu(l,t.next.queue,{},dt())}function af(){return Yl(Se)}function y0(){return El().memoizedState}function m0(){return El().memoizedState}function Lm(l){for(var t=l.return;t!==null;){switch(t.tag){case 24:case 3:var a=dt();l=fa(a);var u=ca(t,l,a);u!==null&&(Pl(u,t,a),Wu(u,t,a)),t={cache:Hi()},l.payload=t;return}t=t.return}}function Vm(l,t,a){var u=dt();a={lane:u,revertLane:0,gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},fn(l)?o0(t,a):(a=Ti(l,t,a,u),a!==null&&(Pl(a,l,u),h0(a,t,u)))}function v0(l,t,a){var u=dt();Pu(l,t,a,u)}function Pu(l,t,a,u){var e={lane:u,revertLane:0,gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null};if(fn(l))o0(t,e);else{var n=l.alternate;if(l.lanes===0&&(n===null||n.lanes===0)&&(n=t.lastRenderedReducer,n!==null))try{var i=t.lastRenderedState,f=n(i,a);if(e.hasEagerState=!0,e.eagerState=f,et(f,i))return Qe(l,t,e,0),dl===null&&Ge(),!1}catch{}if(a=Ti(l,t,e,u),a!==null)return Pl(a,l,u),h0(a,t,u),!0}return!1}function uf(l,t,a,u){if(u={lane:2,revertLane:Bf(),gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},fn(l)){if(t)throw Error(o(479))}else t=Ti(l,a,u,2),t!==null&&Pl(t,l,2)}function fn(l){var t=l.alternate;return l===L||t!==null&&t===L}function o0(l,t){mu=Pe=!0;var a=l.pending;a===null?t.next=t:(t.next=a.next,a.next=t),l.pending=t}function h0(l,t,a){if((a&4194048)!==0){var u=t.lanes;u&=l.pendingLanes,a|=u,t.lanes=a,Tc(l,a)}}var le={readContext:Yl,use:an,useCallback:rl,useContext:rl,useEffect:rl,useImperativeHandle:rl,useLayoutEffect:rl,useInsertionEffect:rl,useMemo:rl,useReducer:rl,useRef:rl,useState:rl,useDebugValue:rl,useDeferredValue:rl,useTransition:rl,useSyncExternalStore:rl,useId:rl,useHostTransitionStatus:rl,useFormState:rl,useActionState:rl,useOptimistic:rl,useMemoCache:rl,useCacheRefresh:rl};le.useEffectEvent=rl;var S0={readContext:Yl,use:an,useCallback:function(l,t){return Kl().memoizedState=[l,t===void 0?null:t],l},useContext:Yl,useEffect:Ps,useImperativeHandle:function(l,t,a){a=a!=null?a.concat([l]):null,en(4194308,4,u0.bind(null,t,l),a)},useLayoutEffect:function(l,t){return en(4194308,4,l,t)},useInsertionEffect:function(l,t){en(4,2,l,t)},useMemo:function(l,t){var a=Kl();t=t===void 0?null:t;var u=l();if(Ga){Pt(!0);try{l()}finally{Pt(!1)}}return a.memoizedState=[u,t],u},useReducer:function(l,t,a){var u=Kl();if(a!==void 0){var e=a(t);if(Ga){Pt(!0);try{a(t)}finally{Pt(!1)}}}else e=t;return u.memoizedState=u.baseState=e,l={pending:null,lanes:0,dispatch:null,lastRenderedReducer:l,lastRenderedState:e},u.queue=l,l=l.dispatch=Vm.bind(null,L,l),[u.memoizedState,l]},useRef:function(l){var t=Kl();return l={current:l},t.memoizedState=l},useState:function(l){l=$i(l);var t=l.queue,a=v0.bind(null,L,t);return t.dispatch=a,[l.memoizedState,a]},useDebugValue:Pi,useDeferredValue:function(l,t){var a=Kl();return lf(a,l,t)},useTransition:function(){var l=$i(!1);return l=c0.bind(null,L,l.queue,!0,!1),Kl().memoizedState=l,[!1,l]},useSyncExternalStore:function(l,t,a){var u=L,e=Kl();if(I){if(a===void 0)throw Error(o(407));a=a()}else{if(a=t(),dl===null)throw Error(o(349));(k&127)!==0||Ys(u,t,a)}e.memoizedState=a;var n={value:a,getSnapshot:t};return e.queue=n,Ps(Gs.bind(null,u,n,l),[l]),u.flags|=2048,ou(9,{destroy:void 0},xs.bind(null,u,n,a,t),null),a},useId:function(){var l=Kl(),t=dl.identifierPrefix;if(I){var a=Ht,u=Nt;a=(u&~(1<<32-ut(u)-1)).toString(32)+a,t="_"+t+"R_"+a,a=ln++,0<\/script>",n=n.removeChild(n.firstChild);break;case"select":n=typeof u.is=="string"?i.createElement("select",{is:u.is}):i.createElement("select"),u.multiple?n.multiple=!0:u.size&&(n.size=u.size);break;default:n=typeof u.is=="string"?i.createElement(e,{is:u.is}):i.createElement(e)}}n[jl]=t,n[wl]=u;l:for(i=t.child;i!==null;){if(i.tag===5||i.tag===6)n.appendChild(i.stateNode);else if(i.tag!==4&&i.tag!==27&&i.child!==null){i.child.return=i,i=i.child;continue}if(i===t)break l;for(;i.sibling===null;){if(i.return===null||i.return===t)break l;i=i.return}i.sibling.return=i.return,i=i.sibling}t.stateNode=n;l:switch(Gl(n,e,u),e){case"button":case"input":case"select":case"textarea":u=!!u.autoFocus;break l;case"img":u=!0;break l;default:u=!1}u&&Kt(t)}}return vl(t),rf(t,t.type,l===null?null:l.memoizedProps,t.pendingProps,a),null;case 6:if(l&&t.stateNode!=null)l.memoizedProps!==u&&Kt(t);else{if(typeof u!="string"&&t.stateNode===null)throw Error(o(166));if(l=J.current,nu(t)){if(l=t.stateNode,a=t.memoizedProps,u=null,e=Bl,e!==null)switch(e.tag){case 27:case 5:u=e.memoizedProps}l[jl]=t,l=!!(l.nodeValue===a||u!==null&&u.suppressHydrationWarning===!0||jd(l.nodeValue,a)),l||ea(t,!0)}else l=pn(l).createTextNode(u),l[jl]=t,t.stateNode=l}return vl(t),null;case 31:if(a=t.memoizedState,l===null||l.memoizedState!==null){if(u=nu(t),a!==null){if(l===null){if(!u)throw Error(o(318));if(l=t.memoizedState,l=l!==null?l.dehydrated:null,!l)throw Error(o(557));l[jl]=t}else Ca(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;vl(t),l=!1}else a=pi(),l!==null&&l.memoizedState!==null&&(l.memoizedState.hydrationErrors=a),l=!0;if(!l)return t.flags&256?(ft(t),t):(ft(t),null);if((t.flags&128)!==0)throw Error(o(558))}return vl(t),null;case 13:if(u=t.memoizedState,l===null||l.memoizedState!==null&&l.memoizedState.dehydrated!==null){if(e=nu(t),u!==null&&u.dehydrated!==null){if(l===null){if(!e)throw Error(o(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(o(317));e[jl]=t}else Ca(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;vl(t),e=!1}else e=pi(),l!==null&&l.memoizedState!==null&&(l.memoizedState.hydrationErrors=e),e=!0;if(!e)return t.flags&256?(ft(t),t):(ft(t),null)}return ft(t),(t.flags&128)!==0?(t.lanes=a,t):(a=u!==null,l=l!==null&&l.memoizedState!==null,a&&(u=t.child,e=null,u.alternate!==null&&u.alternate.memoizedState!==null&&u.alternate.memoizedState.cachePool!==null&&(e=u.alternate.memoizedState.cachePool.pool),n=null,u.memoizedState!==null&&u.memoizedState.cachePool!==null&&(n=u.memoizedState.cachePool.pool),n!==e&&(u.flags|=2048)),a!==l&&a&&(t.child.flags|=8192),mn(t,t.updateQueue),vl(t),null);case 4:return Tl(),l===null&&Qf(t.stateNode.containerInfo),vl(t),null;case 10:return Xt(t.type),vl(t),null;case 19:if(A(zl),u=t.memoizedState,u===null)return vl(t),null;if(e=(t.flags&128)!==0,n=u.rendering,n===null)if(e)ae(u,!1);else{if(bl!==0||l!==null&&(l.flags&128)!==0)for(l=t.child;l!==null;){if(n=Ie(l),n!==null){for(t.flags|=128,ae(u,!1),l=n.updateQueue,t.updateQueue=l,mn(t,l),t.subtreeFlags=0,l=a,a=t.child;a!==null;)vs(a,l),a=a.sibling;return p(zl,zl.current&1|2),I&&Gt(t,u.treeForkCount),t.child}l=l.sibling}u.tail!==null&&tt()>gn&&(t.flags|=128,e=!0,ae(u,!1),t.lanes=4194304)}else{if(!e)if(l=Ie(n),l!==null){if(t.flags|=128,e=!0,l=l.updateQueue,t.updateQueue=l,mn(t,l),ae(u,!0),u.tail===null&&u.tailMode==="hidden"&&!n.alternate&&!I)return vl(t),null}else 2*tt()-u.renderingStartTime>gn&&a!==536870912&&(t.flags|=128,e=!0,ae(u,!1),t.lanes=4194304);u.isBackwards?(n.sibling=t.child,t.child=n):(l=u.last,l!==null?l.sibling=n:t.child=n,u.last=n)}return u.tail!==null?(l=u.tail,u.rendering=l,u.tail=l.sibling,u.renderingStartTime=tt(),l.sibling=null,a=zl.current,p(zl,e?a&1|2:a&1),I&&Gt(t,u.treeForkCount),l):(vl(t),null);case 22:case 23:return ft(t),Qi(),u=t.memoizedState!==null,l!==null?l.memoizedState!==null!==u&&(t.flags|=8192):u&&(t.flags|=8192),u?(a&536870912)!==0&&(t.flags&128)===0&&(vl(t),t.subtreeFlags&6&&(t.flags|=8192)):vl(t),a=t.updateQueue,a!==null&&mn(t,a.retryQueue),a=null,l!==null&&l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(a=l.memoizedState.cachePool.pool),u=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(u=t.memoizedState.cachePool.pool),u!==a&&(t.flags|=2048),l!==null&&A(ja),null;case 24:return a=null,l!==null&&(a=l.memoizedState.cache),t.memoizedState.cache!==a&&(t.flags|=2048),Xt(_l),vl(t),null;case 25:return null;case 30:return null}throw Error(o(156,t.tag))}function km(l,t){switch(Oi(t),t.tag){case 1:return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 3:return Xt(_l),Tl(),l=t.flags,(l&65536)!==0&&(l&128)===0?(t.flags=l&-65537|128,t):null;case 26:case 27:case 5:return Ee(t),null;case 31:if(t.memoizedState!==null){if(ft(t),t.alternate===null)throw Error(o(340));Ca()}return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 13:if(ft(t),l=t.memoizedState,l!==null&&l.dehydrated!==null){if(t.alternate===null)throw Error(o(340));Ca()}return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 19:return A(zl),null;case 4:return Tl(),null;case 10:return Xt(t.type),null;case 22:case 23:return ft(t),Qi(),l!==null&&A(ja),l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 24:return Xt(_l),null;case 25:return null;default:return null}}function Q0(l,t){switch(Oi(t),t.tag){case 3:Xt(_l),Tl();break;case 26:case 27:case 5:Ee(t);break;case 4:Tl();break;case 31:t.memoizedState!==null&&ft(t);break;case 13:ft(t);break;case 19:A(zl);break;case 10:Xt(t.type);break;case 22:case 23:ft(t),Qi(),l!==null&&A(ja);break;case 24:Xt(_l)}}function ue(l,t){try{var a=t.updateQueue,u=a!==null?a.lastEffect:null;if(u!==null){var e=u.next;a=e;do{if((a.tag&l)===l){u=void 0;var n=a.create,i=a.inst;u=n(),i.destroy=u}a=a.next}while(a!==e)}}catch(f){nl(t,t.return,f)}}function ya(l,t,a){try{var u=t.updateQueue,e=u!==null?u.lastEffect:null;if(e!==null){var n=e.next;u=n;do{if((u.tag&l)===l){var i=u.inst,f=i.destroy;if(f!==void 0){i.destroy=void 0,e=t;var c=a,v=f;try{v()}catch(g){nl(e,c,g)}}}u=u.next}while(u!==n)}}catch(g){nl(t,t.return,g)}}function X0(l){var t=l.updateQueue;if(t!==null){var a=l.stateNode;try{Hs(t,a)}catch(u){nl(l,l.return,u)}}}function Z0(l,t,a){a.props=Qa(l.type,l.memoizedProps),a.state=l.memoizedState;try{a.componentWillUnmount()}catch(u){nl(l,t,u)}}function ee(l,t){try{var a=l.ref;if(a!==null){switch(l.tag){case 26:case 27:case 5:var u=l.stateNode;break;case 30:u=l.stateNode;break;default:u=l.stateNode}typeof a=="function"?l.refCleanup=a(u):a.current=u}}catch(e){nl(l,t,e)}}function Ct(l,t){var a=l.ref,u=l.refCleanup;if(a!==null)if(typeof u=="function")try{u()}catch(e){nl(l,t,e)}finally{l.refCleanup=null,l=l.alternate,l!=null&&(l.refCleanup=null)}else if(typeof a=="function")try{a(null)}catch(e){nl(l,t,e)}else a.current=null}function L0(l){var t=l.type,a=l.memoizedProps,u=l.stateNode;try{l:switch(t){case"button":case"input":case"select":case"textarea":a.autoFocus&&u.focus();break l;case"img":a.src?u.src=a.src:a.srcSet&&(u.srcset=a.srcSet)}}catch(e){nl(l,l.return,e)}}function bf(l,t,a){try{var u=l.stateNode;gv(u,l.type,a,t),u[wl]=t}catch(e){nl(l,l.return,e)}}function V0(l){return l.tag===5||l.tag===3||l.tag===26||l.tag===27&&ra(l.type)||l.tag===4}function Tf(l){l:for(;;){for(;l.sibling===null;){if(l.return===null||V0(l.return))return null;l=l.return}for(l.sibling.return=l.return,l=l.sibling;l.tag!==5&&l.tag!==6&&l.tag!==18;){if(l.tag===27&&ra(l.type)||l.flags&2||l.child===null||l.tag===4)continue l;l.child.return=l,l=l.child}if(!(l.flags&2))return l.stateNode}}function zf(l,t,a){var u=l.tag;if(u===5||u===6)l=l.stateNode,t?(a.nodeType===9?a.body:a.nodeName==="HTML"?a.ownerDocument.body:a).insertBefore(l,t):(t=a.nodeType===9?a.body:a.nodeName==="HTML"?a.ownerDocument.body:a,t.appendChild(l),a=a._reactRootContainer,a!=null||t.onclick!==null||(t.onclick=Bt));else if(u!==4&&(u===27&&ra(l.type)&&(a=l.stateNode,t=null),l=l.child,l!==null))for(zf(l,t,a),l=l.sibling;l!==null;)zf(l,t,a),l=l.sibling}function vn(l,t,a){var u=l.tag;if(u===5||u===6)l=l.stateNode,t?a.insertBefore(l,t):a.appendChild(l);else if(u!==4&&(u===27&&ra(l.type)&&(a=l.stateNode),l=l.child,l!==null))for(vn(l,t,a),l=l.sibling;l!==null;)vn(l,t,a),l=l.sibling}function K0(l){var t=l.stateNode,a=l.memoizedProps;try{for(var u=l.type,e=t.attributes;e.length;)t.removeAttributeNode(e[0]);Gl(t,u,a),t[jl]=l,t[wl]=a}catch(n){nl(l,l.return,n)}}var Jt=!1,pl=!1,Ef=!1,J0=typeof WeakSet=="function"?WeakSet:Set,Hl=null;function $m(l,t){if(l=l.containerInfo,Lf=qn,l=es(l),oi(l)){if("selectionStart"in l)var a={start:l.selectionStart,end:l.selectionEnd};else l:{a=(a=l.ownerDocument)&&a.defaultView||window;var u=a.getSelection&&a.getSelection();if(u&&u.rangeCount!==0){a=u.anchorNode;var e=u.anchorOffset,n=u.focusNode;u=u.focusOffset;try{a.nodeType,n.nodeType}catch{a=null;break l}var i=0,f=-1,c=-1,v=0,g=0,z=l,h=null;t:for(;;){for(var S;z!==a||e!==0&&z.nodeType!==3||(f=i+e),z!==n||u!==0&&z.nodeType!==3||(c=i+u),z.nodeType===3&&(i+=z.nodeValue.length),(S=z.firstChild)!==null;)h=z,z=S;for(;;){if(z===l)break t;if(h===a&&++v===e&&(f=i),h===n&&++g===u&&(c=i),(S=z.nextSibling)!==null)break;z=h,h=z.parentNode}z=S}a=f===-1||c===-1?null:{start:f,end:c}}else a=null}a=a||{start:0,end:0}}else a=null;for(Vf={focusedElem:l,selectionRange:a},qn=!1,Hl=t;Hl!==null;)if(t=Hl,l=t.child,(t.subtreeFlags&1028)!==0&&l!==null)l.return=t,Hl=l;else for(;Hl!==null;){switch(t=Hl,n=t.alternate,l=t.flags,t.tag){case 0:if((l&4)!==0&&(l=t.updateQueue,l=l!==null?l.events:null,l!==null))for(a=0;a title"))),Gl(n,u,a),n[jl]=l,Nl(n),u=n;break l;case"link":var i=Id("link","href",e).get(u+(a.href||""));if(i){for(var f=0;fsl&&(i=sl,sl=Y,Y=i);var y=as(f,Y),d=as(f,sl);if(y&&d&&(S.rangeCount!==1||S.anchorNode!==y.node||S.anchorOffset!==y.offset||S.focusNode!==d.node||S.focusOffset!==d.offset)){var m=z.createRange();m.setStart(y.node,y.offset),S.removeAllRanges(),Y>sl?(S.addRange(m),S.extend(d.node,d.offset)):(m.setEnd(d.node,d.offset),S.addRange(m))}}}}for(z=[],S=f;S=S.parentNode;)S.nodeType===1&&z.push({element:S,left:S.scrollLeft,top:S.scrollTop});for(typeof f.focus=="function"&&f.focus(),f=0;fa?32:a,r.T=null,a=Uf,Uf=null;var n=ha,i=Ft;if(Dl=0,bu=ha=null,Ft=0,(al&6)!==0)throw Error(o(331));var f=al;if(al|=4,ud(n.current),ld(n,n.current,i,a),al=f,de(0,!1),at&&typeof at.onPostCommitFiberRoot=="function")try{at.onPostCommitFiberRoot(Du,n)}catch{}return!0}finally{M.p=e,r.T=u,zd(l,t)}}function Ad(l,t,a){t=gt(a,t),t=cf(l.stateNode,t,2),l=ca(l,t,2),l!==null&&(Nu(l,2),Rt(l))}function nl(l,t,a){if(l.tag===3)Ad(l,l,a);else for(;t!==null;){if(t.tag===3){Ad(t,l,a);break}else if(t.tag===1){var u=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof u.componentDidCatch=="function"&&(oa===null||!oa.has(u))){l=gt(a,l),a=_0(2),u=ca(t,a,2),u!==null&&(O0(a,u,t,l),Nu(u,2),Rt(u));break}}t=t.return}}function Rf(l,t,a){var u=l.pingCache;if(u===null){u=l.pingCache=new Pm;var e=new Set;u.set(t,e)}else e=u.get(t),e===void 0&&(e=new Set,u.set(t,e));e.has(a)||(Of=!0,e.add(a),l=ev.bind(null,l,t,a),t.then(l,l))}function ev(l,t,a){var u=l.pingCache;u!==null&&u.delete(t),l.pingedLanes|=l.suspendedLanes&a,l.warmLanes&=~a,dl===l&&(k&a)===a&&(bl===4||bl===3&&(k&62914560)===k&&300>tt()-Sn?(al&2)===0&&Tu(l,0):Mf|=a,ru===k&&(ru=0)),Rt(l)}function _d(l,t){t===0&&(t=rc()),l=Na(l,t),l!==null&&(Nu(l,t),Rt(l))}function nv(l){var t=l.memoizedState,a=0;t!==null&&(a=t.retryLane),_d(l,a)}function iv(l,t){var a=0;switch(l.tag){case 31:case 13:var u=l.stateNode,e=l.memoizedState;e!==null&&(a=e.retryLane);break;case 19:u=l.stateNode;break;case 22:u=l.stateNode._retryCache;break;default:throw Error(o(314))}u!==null&&u.delete(t),_d(l,a)}function fv(l,t){return Kn(l,t)}var An=null,Eu=null,qf=!1,_n=!1,jf=!1,ga=0;function Rt(l){l!==Eu&&l.next===null&&(Eu===null?An=Eu=l:Eu=Eu.next=l),_n=!0,qf||(qf=!0,sv())}function de(l,t){if(!jf&&_n){jf=!0;do for(var a=!1,u=An;u!==null;){if(l!==0){var e=u.pendingLanes;if(e===0)var n=0;else{var i=u.suspendedLanes,f=u.pingedLanes;n=(1<<31-ut(42|l)+1)-1,n&=e&~(i&~f),n=n&201326741?n&201326741|1:n?n|2:0}n!==0&&(a=!0,Dd(u,n))}else n=k,n=pe(u,u===dl?n:0,u.cancelPendingCommit!==null||u.timeoutHandle!==-1),(n&3)===0||Uu(u,n)||(a=!0,Dd(u,n));u=u.next}while(a);jf=!1}}function cv(){Od()}function Od(){_n=qf=!1;var l=0;ga!==0&&bv()&&(l=ga);for(var t=tt(),a=null,u=An;u!==null;){var e=u.next,n=Md(u,t);n===0?(u.next=null,a===null?An=e:a.next=e,e===null&&(Eu=a)):(a=u,(l!==0||(n&3)!==0)&&(_n=!0)),u=e}Dl!==0&&Dl!==5||de(l),ga!==0&&(ga=0)}function Md(l,t){for(var a=l.suspendedLanes,u=l.pingedLanes,e=l.expirationTimes,n=l.pendingLanes&-62914561;0f)break;var g=c.transferSize,z=c.initiatorType;g&&Bd(z)&&(c=c.responseEnd,i+=g*(c"u"?null:document;function Wd(l,t,a){var u=Au;if(u&&typeof t=="string"&&t){var e=ht(t);e='link[rel="'+l+'"][href="'+e+'"]',typeof a=="string"&&(e+='[crossorigin="'+a+'"]'),wd.has(e)||(wd.add(e),l={rel:l,crossOrigin:a,href:t},u.querySelector(e)===null&&(t=u.createElement("link"),Gl(t,"link",l),Nl(t),u.head.appendChild(t)))}}function Dv(l){It.D(l),Wd("dns-prefetch",l,null)}function Uv(l,t){It.C(l,t),Wd("preconnect",l,t)}function Nv(l,t,a){It.L(l,t,a);var u=Au;if(u&&l&&t){var e='link[rel="preload"][as="'+ht(t)+'"]';t==="image"&&a&&a.imageSrcSet?(e+='[imagesrcset="'+ht(a.imageSrcSet)+'"]',typeof a.imageSizes=="string"&&(e+='[imagesizes="'+ht(a.imageSizes)+'"]')):e+='[href="'+ht(l)+'"]';var n=e;switch(t){case"style":n=_u(l);break;case"script":n=Ou(l)}At.has(n)||(l=O({rel:"preload",href:t==="image"&&a&&a.imageSrcSet?void 0:l,as:t},a),At.set(n,l),u.querySelector(e)!==null||t==="style"&&u.querySelector(oe(n))||t==="script"&&u.querySelector(he(n))||(t=u.createElement("link"),Gl(t,"link",l),Nl(t),u.head.appendChild(t)))}}function Hv(l,t){It.m(l,t);var a=Au;if(a&&l){var u=t&&typeof t.as=="string"?t.as:"script",e='link[rel="modulepreload"][as="'+ht(u)+'"][href="'+ht(l)+'"]',n=e;switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":n=Ou(l)}if(!At.has(n)&&(l=O({rel:"modulepreload",href:l},t),At.set(n,l),a.querySelector(e)===null)){switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(a.querySelector(he(n)))return}u=a.createElement("link"),Gl(u,"link",l),Nl(u),a.head.appendChild(u)}}}function Cv(l,t,a){It.S(l,t,a);var u=Au;if(u&&l){var e=Ja(u).hoistableStyles,n=_u(l);t=t||"default";var i=e.get(n);if(!i){var f={loading:0,preload:null};if(i=u.querySelector(oe(n)))f.loading=5;else{l=O({rel:"stylesheet",href:l,"data-precedence":t},a),(a=At.get(n))&&Ff(l,a);var c=i=u.createElement("link");Nl(c),Gl(c,"link",l),c._p=new Promise(function(v,g){c.onload=v,c.onerror=g}),c.addEventListener("load",function(){f.loading|=1}),c.addEventListener("error",function(){f.loading|=2}),f.loading|=4,Un(i,t,u)}i={type:"stylesheet",instance:i,count:1,state:f},e.set(n,i)}}}function Rv(l,t){It.X(l,t);var a=Au;if(a&&l){var u=Ja(a).hoistableScripts,e=Ou(l),n=u.get(e);n||(n=a.querySelector(he(e)),n||(l=O({src:l,async:!0},t),(t=At.get(e))&&If(l,t),n=a.createElement("script"),Nl(n),Gl(n,"link",l),a.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},u.set(e,n))}}function qv(l,t){It.M(l,t);var a=Au;if(a&&l){var u=Ja(a).hoistableScripts,e=Ou(l),n=u.get(e);n||(n=a.querySelector(he(e)),n||(l=O({src:l,async:!0,type:"module"},t),(t=At.get(e))&&If(l,t),n=a.createElement("script"),Nl(n),Gl(n,"link",l),a.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},u.set(e,n))}}function kd(l,t,a,u){var e=(e=J.current)?Dn(e):null;if(!e)throw Error(o(446));switch(l){case"meta":case"title":return null;case"style":return typeof a.precedence=="string"&&typeof a.href=="string"?(t=_u(a.href),a=Ja(e).hoistableStyles,u=a.get(t),u||(u={type:"style",instance:null,count:0,state:null},a.set(t,u)),u):{type:"void",instance:null,count:0,state:null};case"link":if(a.rel==="stylesheet"&&typeof a.href=="string"&&typeof a.precedence=="string"){l=_u(a.href);var n=Ja(e).hoistableStyles,i=n.get(l);if(i||(e=e.ownerDocument||e,i={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},n.set(l,i),(n=e.querySelector(oe(l)))&&!n._p&&(i.instance=n,i.state.loading=5),At.has(l)||(a={rel:"preload",as:"style",href:a.href,crossOrigin:a.crossOrigin,integrity:a.integrity,media:a.media,hrefLang:a.hrefLang,referrerPolicy:a.referrerPolicy},At.set(l,a),n||jv(e,l,a,i.state))),t&&u===null)throw Error(o(528,""));return i}if(t&&u!==null)throw Error(o(529,""));return null;case"script":return t=a.async,a=a.src,typeof a=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Ou(a),a=Ja(e).hoistableScripts,u=a.get(t),u||(u={type:"script",instance:null,count:0,state:null},a.set(t,u)),u):{type:"void",instance:null,count:0,state:null};default:throw Error(o(444,l))}}function _u(l){return'href="'+ht(l)+'"'}function oe(l){return'link[rel="stylesheet"]['+l+"]"}function $d(l){return O({},l,{"data-precedence":l.precedence,precedence:null})}function jv(l,t,a,u){l.querySelector('link[rel="preload"][as="style"]['+t+"]")?u.loading=1:(t=l.createElement("link"),u.preload=t,t.addEventListener("load",function(){return u.loading|=1}),t.addEventListener("error",function(){return u.loading|=2}),Gl(t,"link",a),Nl(t),l.head.appendChild(t))}function Ou(l){return'[src="'+ht(l)+'"]'}function he(l){return"script[async]"+l}function Fd(l,t,a){if(t.count++,t.instance===null)switch(t.type){case"style":var u=l.querySelector('style[data-href~="'+ht(a.href)+'"]');if(u)return t.instance=u,Nl(u),u;var e=O({},a,{"data-href":a.href,"data-precedence":a.precedence,href:null,precedence:null});return u=(l.ownerDocument||l).createElement("style"),Nl(u),Gl(u,"style",e),Un(u,a.precedence,l),t.instance=u;case"stylesheet":e=_u(a.href);var n=l.querySelector(oe(e));if(n)return t.state.loading|=4,t.instance=n,Nl(n),n;u=$d(a),(e=At.get(e))&&Ff(u,e),n=(l.ownerDocument||l).createElement("link"),Nl(n);var i=n;return i._p=new Promise(function(f,c){i.onload=f,i.onerror=c}),Gl(n,"link",u),t.state.loading|=4,Un(n,a.precedence,l),t.instance=n;case"script":return n=Ou(a.src),(e=l.querySelector(he(n)))?(t.instance=e,Nl(e),e):(u=a,(e=At.get(n))&&(u=O({},a),If(u,e)),l=l.ownerDocument||l,e=l.createElement("script"),Nl(e),Gl(e,"link",u),l.head.appendChild(e),t.instance=e);case"void":return null;default:throw Error(o(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(u=t.instance,t.state.loading|=4,Un(u,a.precedence,l));return t.instance}function Un(l,t,a){for(var u=a.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),e=u.length?u[u.length-1]:null,n=e,i=0;i title"):null)}function Bv(l,t,a){if(a===1||t.itemProp!=null)return!1;switch(l){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;return t.rel==="stylesheet"?(l=t.disabled,typeof t.precedence=="string"&&l==null):!0;case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function ly(l){return!(l.type==="stylesheet"&&(l.state.loading&3)===0)}function Yv(l,t,a,u){if(a.type==="stylesheet"&&(typeof u.media!="string"||matchMedia(u.media).matches!==!1)&&(a.state.loading&4)===0){if(a.instance===null){var e=_u(u.href),n=t.querySelector(oe(e));if(n){t=n._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(l.count++,l=Hn.bind(l),t.then(l,l)),a.state.loading|=4,a.instance=n,Nl(n);return}n=t.ownerDocument||t,u=$d(u),(e=At.get(e))&&Ff(u,e),n=n.createElement("link"),Nl(n);var i=n;i._p=new Promise(function(f,c){i.onload=f,i.onerror=c}),Gl(n,"link",u),a.instance=n}l.stylesheets===null&&(l.stylesheets=new Map),l.stylesheets.set(a,t),(t=a.state.preload)&&(a.state.loading&3)===0&&(l.count++,a=Hn.bind(l),t.addEventListener("load",a),t.addEventListener("error",a))}}var Pf=0;function xv(l,t){return l.stylesheets&&l.count===0&&Rn(l,l.stylesheets),0Pf?50:800)+t);return l.unsuspend=a,function(){l.unsuspend=null,clearTimeout(u),clearTimeout(e)}}:null}function Hn(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Rn(this,this.stylesheets);else if(this.unsuspend){var l=this.unsuspend;this.unsuspend=null,l()}}}var Cn=null;function Rn(l,t){l.stylesheets=null,l.unsuspend!==null&&(l.count++,Cn=new Map,t.forEach(Gv,l),Cn=null,Hn.call(l))}function Gv(l,t){if(!(t.state.loading&4)){var a=Cn.get(l);if(a)var u=a.get(null);else{a=new Map,Cn.set(l,a);for(var e=l.querySelectorAll("link[data-precedence],style[data-precedence]"),n=0;n"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(T)}catch(G){console.error(G)}}return T(),cc.exports=to(),cc.exports}var uo=ao();const eo={async listTodos(T={}){const G=new URLSearchParams;for(const[x,o]of Object.entries(T))o!=null&&o!==""&&G.set(x,String(o));return qt(`/api/todos?${G.toString()}`)},async createTodo(T){return qt("/api/todos",{method:"POST",body:JSON.stringify(T)})},async updateTodo(T,G){return qt(`/api/todos/${T}`,{method:"PATCH",body:JSON.stringify(G)})},async completeTodo(T){return qt(`/api/todos/${T}/complete`,{method:"POST"})},async addNote(T,G){return qt(`/api/todos/${T}/notes`,{method:"POST",body:JSON.stringify({note:G})})},async logTime(T,G){return qt(`/api/todos/${T}/time-entries`,{method:"POST",body:JSON.stringify(G)})},async listTimeEntries(T){return qt(`/api/todos/${T}/time-entries`)},async searchClients(T=""){const G=new URLSearchParams;return T&&G.set("q",T),qt(`/api/clients?${G.toString()}`)},async searchTickets(T="",G=null){const x=new URLSearchParams;return T&&x.set("q",T),G!=null&&x.set("client_id",String(G)),qt(`/api/tickets?${x.toString()}`)},async me(){return qt("/api/me")}};async function qt(T,G={}){const x=await fetch(T,{headers:{"content-type":"application/json",...G.headers??{}},...G});if(!x.ok)throw new Error(await x.text());return x.json()}const no=[{key:"open",label:"Inbox"},{key:"today",label:"Today"},{key:"upcoming",label:"Upcoming"},{key:"blocked",label:"Blocked"},{key:"done",label:"Completed"},{key:"tagged",label:"Tagged"}];function io({api:T=eo}){const[G,x]=Sl.useState([]),[o,B]=Sl.useState(null),[Q,tl]=Sl.useState("open"),[gl,R]=Sl.useState(""),[_,F]=Sl.useState(""),[O,il]=Sl.useState([]),[Cl,Ql]=Sl.useState([]),[Al,yt]=Sl.useState(null),[Xl,_t]=Sl.useState(null),[Ul,Ll]=Sl.useState(!1),[mt,Rl]=Sl.useState(!1),[K,ql]=Sl.useState(!1),lt=Sl.useRef(null),Ut=Sl.useRef(null);async function vt(){ql(!0);try{const E=await T.listTodos({status:Q==="done"?"done":Q==="blocked"?"blocked":"open"});x(E.items),B(D=>D??E.items[0]?.id??null)}finally{ql(!1)}}Sl.useEffect(()=>{vt()},[Q]),Sl.useEffect(()=>{async function E(){const[D,j]=await Promise.all([T.searchClients(""),T.me()]);il(D.items),yt(j.client_id??D.items[0]?.id??null)}E()},[]),Sl.useEffect(()=>{async function E(){const D=await T.searchTickets("",Al);Ql(D.items)}E()},[Al]);const ol=Sl.useMemo(()=>{const E=new Date().toISOString().slice(0,10),D=gl.trim().toLowerCase();return G.filter(j=>Q==="today"&&j.due_date!==E||Q==="upcoming"&&(!j.due_date||j.due_date<=E)||Q==="tagged"&&j.tags.length===0?!1:D?j.title.toLowerCase().includes(D)||j.description.toLowerCase().includes(D)||j.tags.some(s=>s.toLowerCase().includes(D)):!0)},[gl,G,Q]),yl=ol.find(E=>E.id===o)??ol[0]??null;Sl.useEffect(()=>{yl&&yl.id!==o&&B(yl.id)},[yl,o]),Sl.useEffect(()=>{async function E(){if(!yl)return;const D=await T.listTimeEntries(yl.id);x(j=>j.map(s=>s.id===yl.id?{...s,time_entries:D.items}:s))}E()},[yl?.id]),Sl.useEffect(()=>{function E(D){const j=D.target,s=j?.tagName==="INPUT"||j?.tagName==="TEXTAREA";D.key==="/"&&!s&&(D.preventDefault(),lt.current?.focus()),D.key==="n"&&!s&&(D.preventDefault(),Ut.current?.focus()),D.key==="x"&&!s&&yl&&(D.preventDefault(),Jl(yl.id)),(D.key==="j"||D.key==="k")&&!s&&(D.preventDefault(),M(D.key==="j"?1:-1))}return window.addEventListener("keydown",E),()=>window.removeEventListener("keydown",E)},[yl,ol]);async function Ot(E){E.preventDefault();const D=_.trim();if(!D)return;const j=await T.createTodo({title:D,client_id:Al,ticket_id:Xl});x(s=>[j.todo,...s]),B(j.todo.id),F(""),Ut.current?.blur()}async function Jl(E){const D=await T.completeTodo(E);x(j=>j.map(s=>s.id===E?D.todo:s))}async function r(E){if(!yl)return;const D=await T.updateTodo(yl.id,E);x(j=>j.map(s=>s.id===yl.id?D.todo:s))}function M(E){if(ol.length===0)return;const D=Math.max(0,ol.findIndex(s=>s.id===yl?.id)),j=ol[Math.min(ol.length-1,Math.max(0,D+E))];B(j.id)}return N.jsxs("main",{className:"app-shell",children:[N.jsxs("aside",{className:"rail",children:[N.jsx("div",{className:"brand",children:"Halo Todo"}),N.jsx("nav",{"aria-label":"Todo views",children:no.map(E=>N.jsx("button",{className:Q===E.key?"active":"",onClick:()=>tl(E.key),children:E.label},E.key))})]}),N.jsxs("section",{className:"list-pane",children:[N.jsxs("form",{className:"quick-add",onSubmit:Ot,children:[N.jsx("input",{ref:Ut,"aria-label":"Quick add title",value:_,onChange:E=>F(E.target.value),placeholder:"Capture a task"}),N.jsx(_y,{label:"Choose customer",value:O.find(E=>E.id===Al)?.name??"Customer",open:Ul,onToggle:()=>Ll(E=>!E),options:O.map(E=>({id:E.id,label:E.name})),onSelect:E=>{yt(E),_t(null),Ll(!1)}}),N.jsx(_y,{label:"Choose ticket",value:Cl.find(E=>E.id===Xl)?.summary??"Ticket",open:mt,onToggle:()=>Rl(E=>!E),options:Cl.map(E=>({id:E.id,label:E.summary})),onSelect:E=>{_t(E),Rl(!1)}}),N.jsx("button",{type:"submit",children:"Add task"})]}),N.jsx("input",{ref:lt,"aria-label":"Search tasks",className:"search",value:gl,onChange:E=>R(E.target.value),placeholder:"Search"}),N.jsx("div",{className:"list-meta",children:K?"Loading":`${ol.length} tasks`}),N.jsx("div",{className:"task-list",children:ol.map(E=>N.jsxs("button",{className:`task-row ${E.id===yl?.id?"selected":""}`,onClick:()=>B(E.id),children:[N.jsx("span",{className:`priority ${E.priority}`,children:E.priority}),N.jsx("span",{className:"task-title",children:E.title}),N.jsxs("span",{className:"task-subline",children:[E.due_date??"No due date"," ",E.ticket_id?`Ticket #${E.ticket_id}`:""]})]},E.id))})]}),N.jsx(fo,{todo:yl,clients:O,tickets:Cl,onComplete:Jl,onSave:r,onLogTime:async(E,D)=>{const j=await T.logTime(E,D);x(s=>s.map(A=>A.id===E?j.todo:A))}})]})}function _y({label:T,value:G,open:x,options:o,onToggle:B,onSelect:Q}){return N.jsxs("div",{className:"picker",children:[N.jsx("button",{type:"button","aria-label":T,onClick:B,children:G}),x?N.jsx("div",{role:"listbox",className:"picker-menu",children:o.map(tl=>N.jsx("button",{type:"button",role:"option",onClick:()=>Q(tl.id),children:tl.label},tl.id))}):null]})}function fo({todo:T,clients:G,tickets:x,onComplete:o,onSave:B,onLogTime:Q}){const[tl,gl]=Sl.useState(""),[R,_]=Sl.useState("0");if(!T)return N.jsx("aside",{className:"detail-pane empty",children:"No task selected"});function F(O){O.preventDefault(),tl.trim()&&(Q(T.id,{note:tl,minutes:Number(R||0),client_id:T.client_id,ticket_id:T.ticket_id}),gl(""),_("0"))}return N.jsxs("aside",{className:"detail-pane",children:[N.jsxs("div",{className:"detail-actions",children:[N.jsx("span",{className:`status ${T.status}`,children:T.status==="done"?"Done":T.status}),N.jsx("button",{onClick:()=>o(T.id),children:"Complete"})]}),N.jsx("input",{className:"title-editor",value:T.title,onChange:O=>B({title:O.target.value}),"aria-label":"Task title"}),N.jsx("textarea",{value:T.description,onChange:O=>B({description:O.target.value}),"aria-label":"Task description"}),N.jsxs("div",{className:"fields",children:[N.jsxs("label",{children:["Priority",N.jsxs("select",{value:T.priority,onChange:O=>B({priority:O.target.value}),children:[N.jsx("option",{value:"normal",children:"normal"}),N.jsx("option",{value:"high",children:"high"}),N.jsx("option",{value:"low",children:"low"})]})]}),N.jsxs("label",{children:["Due",N.jsx("input",{value:T.due_date??"",onChange:O=>B({due_date:O.target.value})})]}),N.jsxs("label",{children:["Customer",N.jsxs("select",{value:T.client_id??"",onChange:O=>B({client_id:O.target.value?Number(O.target.value):null}),children:[N.jsx("option",{value:"",children:"None"}),G.map(O=>N.jsx("option",{value:O.id,children:O.name},O.id))]})]}),N.jsxs("label",{children:["Ticket",N.jsxs("select",{value:T.ticket_id??"",onChange:O=>B({ticket_id:O.target.value?Number(O.target.value):null}),children:[N.jsx("option",{value:"",children:"None"}),x.filter(O=>!T.client_id||O.client_id===T.client_id).map(O=>N.jsx("option",{value:O.id,children:O.summary},O.id))]})]})]}),N.jsxs("div",{className:"chips",children:[T.client_id?N.jsxs("span",{children:["Client #",T.client_id]}):null,T.ticket_id?N.jsxs("span",{children:["Ticket #",T.ticket_id]}):null,T.tags.map(O=>N.jsx("span",{children:O},O))]}),N.jsxs("section",{className:"metadata",children:[N.jsx("h2",{children:"Source"}),N.jsx("code",{children:String(T.source_metadata.source??"halocli")})]}),N.jsxs("section",{className:"notes",children:[N.jsx("h2",{children:"Work Log"}),N.jsxs("form",{className:"work-log-form",onSubmit:F,children:[N.jsx("textarea",{"aria-label":"Work log note",value:tl,onChange:O=>gl(O.target.value),placeholder:"Add a Halo time entry"}),N.jsxs("label",{children:["Minutes",N.jsx("input",{"aria-label":"Minutes",value:R,onChange:O=>_(O.target.value)})]}),N.jsx("button",{type:"submit",children:"Log work"})]}),T.time_entries.length===0?N.jsx("p",{children:"No work logged yet."}):T.time_entries.map((O,il)=>N.jsxs("p",{children:[O.note," ",N.jsxs("span",{children:[O.duration_minutes," min"]})]},O.id??il))]})]})}uo.createRoot(document.getElementById("root")).render(N.jsx(Sl.StrictMode,{children:N.jsx(io,{})})); diff --git a/src/halocli/web_static/index.html b/src/halocli/web_static/index.html new file mode 100644 index 0000000..11081a3 --- /dev/null +++ b/src/halocli/web_static/index.html @@ -0,0 +1,13 @@ + + + + + + Halo Todo + + + + + + + diff --git a/tests/test_todo.py b/tests/test_todo.py index a3a349c..df6bb1d 100644 --- a/tests/test_todo.py +++ b/tests/test_todo.py @@ -8,7 +8,9 @@ GraphMicrosoftTodoRepository, HaloTodoRepository, MicrosoftTodoTask, + extract_description, import_tasks, + note_html, task_from_graph, ) @@ -162,3 +164,203 @@ async def create(self, **kwargs): assert results[0]["imported"] is True assert results[0]["halo_todo"]["id"] == 321 assert completed == ["ms-1"] + + +@pytest.mark.asyncio +async def test_halo_repository_update_preserves_metadata_and_sets_links() -> None: + calls = [] + existing_note = note_html( + "Existing body", + {"kind": "halocli.todo", "tags": ["microsoft-todo"], "microsoft_todo_id": "ms-1"}, + ) + + class FakeClient: + async def raw(self, method, path, *, params=None, body=None): + calls.append((method, path, body)) + if method == "GET": + return { + "id": 123, + "subject": "Old", + "note_html": existing_note, + "is_task": True, + "complete_status": -1, + "client_id": 1, + } + return [{**body[0], "id": 123}] + + repository = HaloTodoRepository(FakeClient()) + result = await repository.update( + 123, + title="New title", + description="Updated body", + priority="high", + client_id=99, + ticket_id=456, + tags=["microsoft-todo", "triage"], + ) + + assert result["title"] == "New title" + assert result["description"] == "Updated body" + assert result["priority"] == "high" + assert result["client_id"] == 99 + assert result["ticket_id"] == 456 + assert result["source_metadata"]["microsoft_todo_id"] == "ms-1" + post_body = calls[-1][2][0] + assert "microsoft_todo_id" in post_body["note_html"] + assert "triage" in post_body["note_html"] + + +def test_extract_description_ignores_metadata_marker() -> None: + value = note_html("Plain text body", {"kind": "halocli.todo", "tags": ["x"]}) + + assert extract_description(value) == "Plain text body" + + +@pytest.mark.asyncio +async def test_halo_repository_searches_clients_and_tickets() -> None: + calls = [] + + class FakeClient: + async def raw(self, method, path, *, params=None, body=None): + calls.append((method, path, params)) + if path == "/Client": + return [{"id": 12, "name": "Midtown Technology Group"}] + if path == "/Tickets": + return [{"id": 12345, "summary": "Backup alert", "client_id": 12, "status": "Open"}] + return {} + + repository = HaloTodoRepository(FakeClient()) + clients = await repository.search_clients(q="Midtown") + tickets = await repository.search_tickets(q="backup", client_id=12, open_only=True) + + assert clients == [{"id": 12, "name": "Midtown Technology Group"}] + assert tickets == [{"id": 12345, "summary": "Backup alert", "client_id": 12, "status": "Open"}] + assert calls[0] == ("GET", "/Client", {"search": "Midtown", "page_size": 25}) + assert calls[1] == ( + "GET", + "/Tickets", + {"search": "backup", "client_id": 12, "open_only": True, "page_size": 25}, + ) + + +@pytest.mark.asyncio +async def test_halo_repository_logs_zero_duration_time_entry_with_context() -> None: + calls = [] + existing_note = note_html("Existing body", {"kind": "halocli.todo", "tags": []}) + + class FakeClient: + async def raw(self, method, path, *, params=None, body=None): + calls.append((method, path, body)) + if method == "GET" and path == "/Appointment/123": + return { + "id": 123, + "subject": "Investigate backup alert", + "note_html": existing_note, + "is_task": True, + "complete_status": -1, + "client_id": 12, + "ticket_id": 12345, + "agent_id": 37, + } + if method == "GET" and path == "/Agent/me": + return {"id": 37, "name": "Thomas Bray", "client_id": 12, "client_name": "Midtown"} + if method == "POST" and path == "/TimesheetEvent": + return [{"id": 9001, **body[0]}] + return [{**body[0], "id": 123}] + + repository = HaloTodoRepository(FakeClient()) + result = await repository.log_time(123, note="Reviewed alert context.", minutes=0) + + assert result["id"] == 9001 + assert result["todo_id"] == 123 + assert result["duration_minutes"] == 0 + assert result["client_id"] == 12 + assert result["ticket_id"] == 12345 + time_payload = calls[-1][2][0] + assert calls[-1][1] == "/TimesheetEvent" + assert time_payload["timetaken"] == 0 + assert time_payload["client_id"] == 12 + assert time_payload["ticket_id"] == 12345 + assert time_payload["note"] == "Reviewed alert context." + + +@pytest.mark.asyncio +async def test_halo_repository_time_entry_client_override_updates_todo() -> None: + calls = [] + + class FakeClient: + async def raw(self, method, path, *, params=None, body=None): + calls.append((method, path, body)) + if method == "GET" and path == "/Appointment/123": + return { + "id": 123, + "subject": "Investigate", + "note_html": note_html("", {"kind": "halocli.todo", "tags": []}), + "is_task": True, + "complete_status": -1, + "client_id": 12, + } + if method == "GET" and path == "/Agent/me": + return {"id": 37} + if method == "POST" and path == "/TimesheetEvent": + return [{"id": 9002, **body[0]}] + return [{**body[0], "id": 123}] + + repository = HaloTodoRepository(FakeClient()) + result = await repository.log_time(123, note="Moved to customer context.", minutes=0, client_id=99) + + appointment_updates = [call for call in calls if call[1] == "/Appointment"] + assert appointment_updates[0][2][0]["client_id"] == 99 + assert result["client_id"] == 99 + + +@pytest.mark.asyncio +async def test_halo_repository_reads_time_entry_history_for_todo() -> None: + calls = [] + + class FakeClient: + async def raw(self, method, path, *, params=None, body=None): + calls.append((method, path, params)) + if method == "GET" and path == "/Appointment/123": + return { + "id": 123, + "subject": "Investigate backup alert", + "note_html": note_html("", {"kind": "halocli.todo", "tags": []}), + "is_task": True, + "complete_status": -1, + "client_id": 12, + "ticket_id": 12345, + } + if method == "GET" and path == "/TimesheetEvent": + return [ + { + "id": 9001, + "todo_id": 123, + "subject": "[Todo #123] Investigate backup alert", + "note": "Reviewed alert context.", + "timetaken": 0, + "client_id": 12, + "ticket_id": 12345, + }, + { + "id": 9002, + "subject": "Unrelated", + "note": "Ignore me", + "timetaken": 1, + "client_id": 12, + }, + ] + return {} + + repository = HaloTodoRepository(FakeClient()) + entries = await repository.list_time_entries(123) + + assert len(entries) == 1 + assert entries[0]["id"] == 9001 + assert entries[0]["note"] == "Reviewed alert context." + assert entries[0]["duration_minutes"] == 0 + assert calls[-1] == ( + "GET", + "/TimesheetEvent", + {"todo_id": 123, "client_id": 12, "ticket_id": 12345, "page_size": 50}, + ) diff --git a/tests/test_todo_web.py b/tests/test_todo_web.py new file mode 100644 index 0000000..1cce002 --- /dev/null +++ b/tests/test_todo_web.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from fastapi.testclient import TestClient + +from halocli.todo_web import create_todo_api + + +class FakeTodoRepository: + def __init__(self) -> None: + self.todos = { + 1: { + "id": 1, + "title": "Independent todo list front end for HaloPSA", + "description": "Build the first UI slice.", + "status": "open", + "priority": "normal", + "due_date": "2026-04-27", + "owner": 37, + "client_id": 12, + "ticket_id": 12345, + "tags": ["microsoft-todo", "Tasks"], + "notes": [], + "time_entries": [], + "source_metadata": {"source": "microsoft.todo"}, + } + } + self.clients = [{"id": 12, "name": "Midtown Technology Group"}] + self.tickets = [{"id": 12345, "summary": "Backup alert", "client_id": 12, "status": "Open"}] + + async def list(self, **filters): + items = list(self.todos.values()) + if filters.get("status"): + items = [item for item in items if item["status"] == filters["status"]] + if filters.get("tag"): + items = [item for item in items if filters["tag"] in item["tags"]] + if filters.get("q"): + q = filters["q"].lower() + items = [item for item in items if q in item["title"].lower()] + return items + + async def get(self, todo_id): + return self.todos[int(todo_id)] + + async def create(self, **data): + todo_id = 2 + self.todos[todo_id] = { + "id": todo_id, + "status": "open", + "priority": "normal", + "tags": data.get("tags") or [], + "notes": [], + "time_entries": [], + "source_metadata": {"source": "halocli"}, + **data, + } + return self.todos[todo_id] + + async def update(self, todo_id, **patch): + self.todos[int(todo_id)].update({k: v for k, v in patch.items() if v is not None}) + return self.todos[int(todo_id)] + + async def complete(self, todo_id): + self.todos[int(todo_id)]["status"] = "done" + return self.todos[int(todo_id)] + + async def add_note(self, todo_id, note): + self.todos[int(todo_id)]["notes"].append({"body": note}) + return self.todos[int(todo_id)] + + async def log_time(self, todo_id, **payload): + entry = {"id": 9001, "todo_id": int(todo_id), "duration_minutes": payload.get("minutes", 0), **payload} + self.todos[int(todo_id)]["time_entries"].append(entry) + return entry + + async def list_time_entries(self, todo_id): + return self.todos[int(todo_id)]["time_entries"] + + async def search_clients(self, q=None): + return self.clients + + async def search_tickets(self, q=None, client_id=None, open_only=True): + return [ticket for ticket in self.tickets if client_id is None or ticket["client_id"] == client_id] + + async def me(self): + return {"id": 37, "name": "Thomas Bray", "client_id": 12, "client_name": "Midtown Technology Group"} + + +def test_todo_api_lists_filtered_items() -> None: + client = TestClient(create_todo_api(lambda: FakeTodoRepository())) + + response = client.get("/api/todos", params={"status": "open", "tag": "Tasks", "q": "front end"}) + + assert response.status_code == 200 + payload = response.json() + assert payload["count"] == 1 + assert payload["items"][0]["title"] == "Independent todo list front end for HaloPSA" + + +def test_todo_api_create_update_complete_and_note() -> None: + repository = FakeTodoRepository() + client = TestClient(create_todo_api(lambda: repository)) + + created = client.post( + "/api/todos", + json={"title": "Review migrated tasks", "tags": ["triage"], "ticket_id": 456}, + ) + assert created.status_code == 200 + assert created.json()["todo"]["id"] == 2 + + updated = client.patch("/api/todos/2", json={"priority": "high", "client_id": 99}) + assert updated.status_code == 200 + assert updated.json()["todo"]["priority"] == "high" + assert updated.json()["todo"]["client_id"] == 99 + + noted = client.post("/api/todos/2/notes", json={"note": "Waiting on vendor."}) + assert noted.status_code == 200 + assert noted.json()["todo"]["notes"][0]["body"] == "Waiting on vendor." + + completed = client.post("/api/todos/2/complete") + assert completed.status_code == 200 + assert completed.json()["todo"]["status"] == "done" + + +def test_todo_api_searches_clients_and_tickets() -> None: + client = TestClient(create_todo_api(lambda: FakeTodoRepository())) + + clients = client.get("/api/clients", params={"q": "Midtown"}) + tickets = client.get("/api/tickets", params={"q": "backup", "client_id": 12, "open": True}) + me = client.get("/api/me") + + assert clients.status_code == 200 + assert clients.json()["items"] == [{"id": 12, "name": "Midtown Technology Group"}] + assert tickets.status_code == 200 + assert tickets.json()["items"][0]["id"] == 12345 + assert me.status_code == 200 + assert me.json()["client_id"] == 12 + + +def test_todo_api_logs_zero_duration_work() -> None: + repository = FakeTodoRepository() + client = TestClient(create_todo_api(lambda: repository)) + + response = client.post( + "/api/todos/1/time-entries", + json={"note": "Reviewed alert context.", "minutes": 0, "client_id": 12, "ticket_id": 12345}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["time_entry"]["duration_minutes"] == 0 + assert payload["time_entry"]["client_id"] == 12 + assert payload["time_entry"]["ticket_id"] == 12345 + + +def test_todo_api_reads_time_entry_history() -> None: + repository = FakeTodoRepository() + repository.todos[1]["time_entries"].append( + {"id": 9001, "todo_id": 1, "duration_minutes": 0, "note": "Reviewed alert context."} + ) + client = TestClient(create_todo_api(lambda: repository)) + + response = client.get("/api/todos/1/time-entries") + + assert response.status_code == 200 + payload = response.json() + assert payload["count"] == 1 + assert payload["items"][0]["note"] == "Reviewed alert context." diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6c9f035 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["frontend/src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4f95d19 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: "src/halocli/web_static", + emptyOutDir: true + }, + test: { + environment: "jsdom", + setupFiles: ["frontend/src/test-setup.ts"] + } +});
{html.escape(description)}
{html.escape(description).replace(chr(10), '')}