From 083fc0027e457e086ba95ce3658f5881caff0377 Mon Sep 17 00:00:00 2001 From: Lucas Guerra Date: Thu, 19 Mar 2026 14:12:21 -0300 Subject: [PATCH] feat: Implement initial Process Scheduling Simulator application with core logic, algorithms, UI components, and project setup. --- .gitignore | 19 + README.md | 713 +++ index.html | 13 + package-lock.json | 4563 +++++++++++++++++ package.json | 50 + src/App.tsx | 9 + src/components/ControlBar/ControlBar.tsx | 167 + src/components/Layout/AppLayout.tsx | 13 + src/components/Layout/MainArea.tsx | 25 + src/components/Layout/Sidebar.tsx | 27 + src/components/ProcessPanel/ProcessCard.tsx | 73 + src/components/ProcessPanel/ProcessForm.tsx | 256 + src/components/ProcessPanel/ProcessList.tsx | 109 + src/components/ProcessPanel/ProcessPanel.tsx | 42 + .../SchedulerPanel/AlgorithmSelector.tsx | 44 + .../SchedulerPanel/SchedulerConfig.tsx | 140 + .../SchedulerPanel/SchedulerPanel.tsx | 19 + src/components/__tests__/ControlBar.test.tsx | 100 + src/components/__tests__/ProcessForm.test.tsx | 100 + .../__tests__/SchedulerConfig.test.tsx | 123 + src/components/shared/Badge.tsx | 30 + src/components/shared/IconButton.tsx | 51 + src/components/shared/processUtils.ts | 32 + src/core/__tests__/engine.test.ts | 281 + src/core/__tests__/fcfs.test.ts | 54 + src/core/__tests__/hrrn.test.ts | 83 + src/core/__tests__/metrics.test.ts | 163 + src/core/__tests__/priority.test.ts | 118 + src/core/__tests__/rr.test.ts | 116 + src/core/__tests__/sjf.test.ts | 94 + src/core/algorithms/fcfs.ts | 21 + src/core/algorithms/hrrn.ts | 33 + src/core/algorithms/index.ts | 59 + src/core/algorithms/multilevel.ts | 19 + src/core/algorithms/priority.ts | 49 + src/core/algorithms/rr.ts | 22 + src/core/algorithms/sjf.ts | 48 + src/core/algorithms/utils.ts | 11 + src/core/engine.ts | 270 + src/core/metrics.ts | 107 + src/core/presets.ts | 80 + src/core/types.ts | 103 + src/hooks/useLocalStorage.ts | 50 + src/hooks/useSimulation.ts | 50 + src/index.css | 20 + src/main.tsx | 10 + src/store/processStore.ts | 62 + src/store/simulationStore.ts | 69 + src/store/uiStore.ts | 28 + src/test/setup.ts | 1 + tsconfig.app.json | 26 + tsconfig.json | 7 + tsconfig.node.json | 18 + vite.config.ts | 25 + 54 files changed, 8815 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/components/ControlBar/ControlBar.tsx create mode 100644 src/components/Layout/AppLayout.tsx create mode 100644 src/components/Layout/MainArea.tsx create mode 100644 src/components/Layout/Sidebar.tsx create mode 100644 src/components/ProcessPanel/ProcessCard.tsx create mode 100644 src/components/ProcessPanel/ProcessForm.tsx create mode 100644 src/components/ProcessPanel/ProcessList.tsx create mode 100644 src/components/ProcessPanel/ProcessPanel.tsx create mode 100644 src/components/SchedulerPanel/AlgorithmSelector.tsx create mode 100644 src/components/SchedulerPanel/SchedulerConfig.tsx create mode 100644 src/components/SchedulerPanel/SchedulerPanel.tsx create mode 100644 src/components/__tests__/ControlBar.test.tsx create mode 100644 src/components/__tests__/ProcessForm.test.tsx create mode 100644 src/components/__tests__/SchedulerConfig.test.tsx create mode 100644 src/components/shared/Badge.tsx create mode 100644 src/components/shared/IconButton.tsx create mode 100644 src/components/shared/processUtils.ts create mode 100644 src/core/__tests__/engine.test.ts create mode 100644 src/core/__tests__/fcfs.test.ts create mode 100644 src/core/__tests__/hrrn.test.ts create mode 100644 src/core/__tests__/metrics.test.ts create mode 100644 src/core/__tests__/priority.test.ts create mode 100644 src/core/__tests__/rr.test.ts create mode 100644 src/core/__tests__/sjf.test.ts create mode 100644 src/core/algorithms/fcfs.ts create mode 100644 src/core/algorithms/hrrn.ts create mode 100644 src/core/algorithms/index.ts create mode 100644 src/core/algorithms/multilevel.ts create mode 100644 src/core/algorithms/priority.ts create mode 100644 src/core/algorithms/rr.ts create mode 100644 src/core/algorithms/sjf.ts create mode 100644 src/core/algorithms/utils.ts create mode 100644 src/core/engine.ts create mode 100644 src/core/metrics.ts create mode 100644 src/core/presets.ts create mode 100644 src/core/types.ts create mode 100644 src/hooks/useLocalStorage.ts create mode 100644 src/hooks/useSimulation.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/store/processStore.ts create mode 100644 src/store/simulationStore.ts create mode 100644 src/store/uiStore.ts create mode 100644 src/test/setup.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..293fc57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +!**/glob-import/dir/node_modules +.DS_Store +.idea +.pnpm-store +*.cpuprofile +*.local +*.log +/.vscode/ +/docs/.vitepress/cache +/docs/.vitepress/.temp +/packages/vite/LICENSE +dist +dist-ssr +explorations +node_modules +playground-temp +temp +TODOs.md +.eslintcache \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..eae17a8 --- /dev/null +++ b/README.md @@ -0,0 +1,713 @@ +# Process Scheduler Simulator — Especificação Técnica Completa + +> **Versão:** 1.0 +> **Data:** 2026-03-19 +> **Autor:** Lucas Rayan Guerra +> **Stack alvo:** React + Vite (Single Page Application, browser-only, sem backend) + +--- + +## 1. Visão Geral + +O **Process Scheduler Simulator** é uma ferramenta educacional interativa que roda inteiramente no navegador. Ele permite ao usuário criar processos, escolher um algoritmo de escalonamento, executar a simulação passo a passo ou de forma automática, visualizar o diagrama de Gantt em tempo real e analisar métricas detalhadas por processo e globais. + +### 1.1 Objetivos + +| # | Objetivo | +|---|----------| +| O1 | Simular fielmente os algoritmos clássicos de escalonamento de SO | +| O2 | Permitir execução passo a passo para fins didáticos | +| O3 | Exibir diagrama de Gantt animado com estados de CPU e I/O | +| O4 | Calcular e exibir métricas completas por processo e agregadas | +| O5 | Ser completamente autocontido — sem servidor, sem dependências externas de dados | + +### 1.2 Público-Alvo + +- Estudantes de Sistemas Operacionais (graduação/pós) +- Professores que queiram usar em sala de aula +- Profissionais revisando fundamentos de SO + +--- + +## 2. Requisitos Funcionais + +### RF-01 — Gerenciamento de Processos + +| ID | Descrição | +|----|-----------| +| RF-01.1 | O usuário pode **criar** um processo com os atributos listados na seção 3.1 | +| RF-01.2 | O usuário pode **editar** um processo antes de iniciar a simulação | +| RF-01.3 | O usuário pode **remover** um processo individual ou **limpar** todos | +| RF-01.4 | O usuário pode **gerar processos aleatórios** com parâmetros configuráveis (quantidade, faixas de valores) | +| RF-01.5 | A lista de processos exibe uma prévia das propriedades e a classificação CPU/I/O Bound estimada | +| RF-01.6 | Mínimo de 1 processo, máximo de 16 processos por simulação | + +### RF-02 — Configuração do Escalonador + +| ID | Descrição | +|----|-----------| +| RF-02.1 | O usuário seleciona **um** algoritmo de escalonamento por simulação | +| RF-02.2 | Parâmetros específicos de algoritmo são exibidos dinamicamente (ex.: quantum para RR) | +| RF-02.3 | O usuário configura o **tempo de context switch** (0–10 unidades de tempo, padrão 0) | +| RF-02.4 | O usuário configura se o escalonador é **preemptivo ou cooperativo** (onde aplicável) | + +### RF-03 — Simulação + +| ID | Descrição | +|----|-----------| +| RF-03.1 | **Play** inicia/retoma a simulação com velocidade configurável | +| RF-03.2 | **Pause** suspende a execução sem perder o estado | +| RF-03.3 | **Step** avança exatamente 1 unidade de tempo (tick) | +| RF-03.4 | **Reset** retorna ao estado inicial sem perder a configuração de processos | +| RF-03.5 | **Velocidade** ajustável: 0.25×, 0.5×, 1×, 2×, 4× (padrão 1×) | +| RF-03.6 | A simulação termina quando todos os processos atingem estado **Terminated** | +| RF-03.7 | O tick atual é exibido em destaque no painel de controle | + +### RF-04 — Diagrama de Gantt + +| ID | Descrição | +|----|-----------| +| RF-04.1 | Cada processo ocupa uma linha horizontal no diagrama | +| RF-04.2 | Cada tick é representado por uma célula colorida conforme o estado | +| RF-04.3 | O tick atual é destacado com uma linha vertical animada | +| RF-04.4 | Legenda de cores dos estados exibida abaixo do diagrama | +| RF-04.5 | O diagrama faz scroll horizontal automático para acompanhar o tick atual | +| RF-04.6 | Hovering sobre uma célula exibe tooltip: processo, tick, estado, detalhes | + +### RF-05 — Métricas + +| ID | Descrição | +|----|-----------| +| RF-05.1 | Métricas são calculadas por processo ao final da simulação | +| RF-05.2 | Métricas globais (médias, totais) são exibidas em painel separado | +| RF-05.3 | Um gráfico de barras comparativo exibe o turnaround e waiting time por processo | +| RF-05.4 | A classificação CPU Bound / I/O Bound é determinada automaticamente (ver seção 5) | +| RF-05.5 | As métricas podem ser exportadas como CSV | + +### RF-06 — Persistência Local + +| ID | Descrição | +|----|-----------| +| RF-06.1 | O estado atual (processos + configuração) é salvo automaticamente no `localStorage` | +| RF-06.2 | O usuário pode exportar/importar configurações como JSON | +| RF-06.3 | Cenários pré-definidos (presets) estão disponíveis para carregamento rápido | + +--- + +## 3. Modelo de Dados + +### 3.1 Processo (`Process`) + +```typescript +interface BurstSegment { + type: "cpu" | "io"; + duration: number; // unidades de tempo +} + +interface Process { + id: string; // UUID gerado na criação + name: string; // "P1", "P2" ou nome personalizado + arrivalTime: number; // tick de chegada na fila ready (≥ 0) + priority: number; // 1 (maior prioridade) a 10 (menor); padrão 5 + color: string; // cor HEX para o diagrama + bursts: BurstSegment[]; // sequência alternada CPU→I/O→CPU→... + // deve começar e terminar com CPU +} +``` + +**Exemplo de processo:** + +```json +{ + "id": "p1", + "name": "P1", + "arrivalTime": 0, + "priority": 3, + "color": "#4FC3F7", + "bursts": [ + { "type": "cpu", "duration": 4 }, + { "type": "io", "duration": 3 }, + { "type": "cpu", "duration": 2 } + ] +} +``` + +> **Regra:** `bursts` deve ter estrutura CPU → (IO → CPU)\* — sempre começa com CPU. Mínimo: 1 burst CPU. + +### 3.2 Configuração do Escalonador (`SchedulerConfig`) + +```typescript +interface SchedulerConfig { + algorithm: SchedulingAlgorithm; + quantum: number; // apenas para RR (padrão: 2) + contextSwitchTime: number; // overhead entre processos (padrão: 0) + isPreemptive: boolean; // aplicável a SJF e Priority + agingEnabled: boolean; // evitar starvation em Priority + agingInterval: number; // ticks para incremento de prioridade +} +``` + +### 3.3 Estado de Execução do Processo (`ProcessState`) + +```typescript +type StateLabel = + | "New" // antes do arrivalTime + | "Ready" // na fila, aguardando CPU + | "Running" // executando burst CPU + | "Waiting" // executando burst I/O + | "Terminated"; // concluído + +interface ProcessRuntime { + processId: string; + state: StateLabel; + remainingBurst: number; // ticks restantes no burst atual + burstIndex: number; // índice atual em Process.bursts + arrivalTick: number; // tick em que entrou na fila Ready pela 1ª vez + startTick: number | null; // tick da 1ª vez em Running + finishTick: number | null; // tick em que terminou + waitingTime: number; // acumulado em Ready + cpuTime: number; // acumulado em Running + ioTime: number; // acumulado em Waiting + responseTime: number | null; +} +``` + +### 3.4 Tick de Simulação (`SimTick`) + +```typescript +interface SimTick { + tick: number; + cpuProcess: string | null; // PID executando na CPU + ioProcesses: string[]; // PIDs em I/O + readyQueue: string[]; // ordem da fila ready + states: Record; + contextSwitching: boolean; // overhead ativo +} +``` + +--- + +## 4. Algoritmos de Escalonamento + +### 4.1 Lista de Algoritmos Suportados + +| Código | Nome | Preemptivo? | Parâmetro extra | +|--------|------|-------------|-----------------| +| `FCFS` | First Come First Served | Não | — | +| `SJF_NP` | Shortest Job First (não-preemptivo) | Não | — | +| `SJF_P` | Shortest Remaining Time First (preemptivo) | Sim | — | +| `RR` | Round Robin | Sim | Quantum | +| `PRIORITY_NP` | Priority Scheduling (não-preemptivo) | Não | — | +| `PRIORITY_P` | Priority Scheduling (preemptivo) | Sim | — | +| `MULTILEVEL` | Multilevel Queue | Sim | Nº de filas, quantum por fila | +| `HRRN` | Highest Response Ratio Next | Não | — | + +### 4.2 Regras de Desempate + +Para todos os algoritmos, quando dois processos têm o mesmo critério de seleção, o desempate é feito por ordem de chegada (`arrivalTime`) e depois por `id` lexicográfico. + +### 4.3 Especificação de Comportamento por Algoritmo + +#### FCFS +- A fila ready é ordenada por `arrivalTime` ascendente. +- O processo na cabeça da fila ocupa a CPU até terminar seu burst CPU atual. +- Ao término do burst CPU, se houver burst I/O, transita para Waiting. +- Ao terminar o I/O, retorna para a fila Ready (cauda). + +#### SJF Não-Preemptivo +- Na seleção, escolhe o processo com **menor burst CPU atual** (`remainingBurst`). +- Não interrompe o processo em execução. + +#### SRTF (SJF Preemptivo) +- A cada tick, verifica se algum processo na fila Ready tem `remainingBurst` menor que o processo atual. +- Se sim, realiza preempção (com context switch se configurado). + +#### Round Robin +- Usa fila FIFO com quantum `q`. +- Ao expirar o quantum, o processo retorna ao fim da fila Ready se ainda tiver burst CPU restante. +- I/O bursts não consomem quantum. + +#### Priority (Não-Preemptivo / Preemptivo) +- Menor valor numérico = maior prioridade. +- Com aging habilitado: a cada `agingInterval` ticks em Ready, `priority--` (até mínimo 1). + +#### HRRN +- `ratio = (waitingTime + burstTime) / burstTime` +- A cada seleção, escolhe o processo com maior ratio. +- Não-preemptivo. + +--- + +## 5. Definição de CPU Bound vs. I/O Bound + +A classificação é feita **por processo**, baseada na proporção de tempo total gasto: + +``` +cpuRatio = cpuTime / (cpuTime + ioTime) +ioRatio = ioTime / (cpuTime + ioTime) + +if cpuRatio >= 0.65 → "CPU Bound" +if ioRatio >= 0.65 → "I/O Bound" +else → "Balanced" +``` + +A classificação aparece: +- Na lista de processos (badge de preview, calculado sobre os bursts declarados) +- Na tabela de métricas (calculado sobre os tempos reais simulados) + +--- + +## 6. Métricas + +### 6.1 Métricas por Processo + +| Métrica | Fórmula | Descrição | +|---------|---------|-----------| +| **Arrival Time** | `arrivalTime` | Tick de chegada na fila | +| **First CPU Tick** | `startTick` | Primeiro tick em Running | +| **Finish Time** | `finishTick` | Tick de término | +| **Response Time** | `startTick − arrivalTime` | Tempo até 1ª vez na CPU | +| **Turnaround Time** | `finishTick − arrivalTime` | Tempo total de vida | +| **Waiting Time** | `turnaround − cpuTime − ioTime` | Tempo na fila Ready | +| **CPU Time** | Σ Running ticks | Total de CPU consumida | +| **I/O Time** | Σ Waiting ticks | Total de I/O | +| **CPU Utilization** | `cpuTime / turnaround × 100` | % do turnaround em CPU | +| **Bound Type** | ver seção 5 | CPU Bound / I/O Bound / Balanced | + +### 6.2 Métricas Globais + +| Métrica | Fórmula | +|---------|---------| +| **Avg Response Time** | Média dos response times | +| **Avg Turnaround Time** | Média dos turnarounds | +| **Avg Waiting Time** | Média dos waiting times | +| **CPU Throughput** | `processos concluídos / tempo total` | +| **CPU Utilization (global)** | `Σ Running ticks / tempo total × 100` | +| **Total Simulation Time** | Tick final | + +--- + +## 7. Arquitetura da Aplicação + +### 7.1 Stack Tecnológica + +``` +Runtime: React 18 + Vite 5 +Linguagem: TypeScript 5 +Estilos: Tailwind CSS 3 + CSS Modules (para componentes específicos) +Gráficos: Recharts (gráfico de barras de métricas) +Estado: Zustand (store global) +Persistência: localStorage (auto-save) + JSON import/export +Testes: Vitest + @testing-library/react +``` + +### 7.2 Estrutura de Pastas + +``` +src/ +├── core/ # Lógica pura — sem React +│ ├── algorithms/ +│ │ ├── fcfs.ts +│ │ ├── sjf.ts +│ │ ├── rr.ts +│ │ ├── priority.ts +│ │ ├── hrrn.ts +│ │ ├── multilevel.ts +│ │ └── index.ts # dispatch por algoritmo +│ ├── engine.ts # SimulationEngine — step(), runAll() +│ ├── metrics.ts # cálculo de métricas +│ ├── types.ts # todas as interfaces +│ └── presets.ts # cenários pré-definidos +│ +├── store/ +│ ├── processStore.ts # CRUD de processos +│ ├── simulationStore.ts # estado da simulação, ticks +│ └── uiStore.ts # estado da UI (painéis abertos, velocidade) +│ +├── components/ +│ ├── ProcessPanel/ # criação/edição de processos +│ │ ├── ProcessList.tsx +│ │ ├── ProcessForm.tsx +│ │ └── ProcessCard.tsx +│ ├── SchedulerPanel/ # seleção de algoritmo e parâmetros +│ │ ├── AlgorithmSelector.tsx +│ │ └── SchedulerConfig.tsx +│ ├── ControlBar/ # play, pause, step, reset, velocidade +│ │ └── ControlBar.tsx +│ ├── GanttChart/ # diagrama de Gantt +│ │ ├── GanttChart.tsx +│ │ ├── GanttRow.tsx +│ │ ├── GanttCell.tsx +│ │ └── GanttLegend.tsx +│ ├── MetricsPanel/ # tabela + gráficos +│ │ ├── MetricsTable.tsx +│ │ ├── MetricsChart.tsx +│ │ └── GlobalMetrics.tsx +│ └── shared/ # botões, badges, tooltips, modais +│ +├── hooks/ +│ ├── useSimulation.ts # loop de simulação com requestAnimationFrame +│ ├── useLocalStorage.ts +│ └── useExport.ts +│ +├── App.tsx +└── main.tsx +``` + +### 7.3 Fluxo de Dados + +``` +[ProcessForm] ──create/edit──► [processStore] + │ +[AlgorithmSelector] ──config──► [simulationStore] + │ + [SimulationEngine] + ┌──────┴──────┐ + step() runAll() + └──────┬──────┘ + SimTick[] + │ + ┌─────────────────┼────────────────┐ + [GanttChart] [ControlBar] [MetricsPanel] +``` + +--- + +## 8. Motor de Simulação (`SimulationEngine`) + +### 8.1 Interface Pública + +```typescript +class SimulationEngine { + constructor(processes: Process[], config: SchedulerConfig); + + // Avança 1 tick; retorna o snapshot daquele tick + step(): SimTick; + + // Executa todos os ticks até o fim; retorna array completo + runAll(): SimTick[]; + + // Verdadeiro se todos os processos estão Terminated + get isFinished(): boolean; + + // Tick atual + get currentTick(): number; + + // Estado atual de todos os processos + get runtimeStates(): ProcessRuntime[]; +} +``` + +### 8.2 Loop Principal (pseudocódigo) + +``` +function step(): + tick++ + + // 1. Verificar chegadas de novos processos + for each process where arrivalTime == tick: + transitionTo(process, "Ready") + addToReadyQueue(process) + + // 2. Aging (se habilitado) + if agingEnabled and tick % agingInterval == 0: + for each process in readyQueue: + process.priority = max(1, process.priority - 1) + + // 3. Verificar fim de I/O + for each process in ioQueue: + process.remainingBurst-- + if process.remainingBurst == 0: + advanceBurst(process) + if hasMoreBursts(process): + transitionTo(process, "Ready") + addToReadyQueue(process) + else: + transitionTo(process, "Terminated") + process.finishTick = tick + + // 4. Verificar fim do burst CPU atual + if cpuProcess != null: + cpuProcess.remainingBurst-- + cpuProcess.cpuTime++ + if cpuProcess.remainingBurst == 0: + advanceBurst(cpuProcess) + if hasIoBurst(cpuProcess): + transitionTo(cpuProcess, "Waiting") + addToIoQueue(cpuProcess) + cpuProcess = null + else if hasMoreCpuBurst(cpuProcess): + // volta pra ready (ex: após I/O futuro) + else: + transitionTo(cpuProcess, "Terminated") + cpuProcess = null + + // 5. Verificar preempção (algoritmos preemptivos) + if isPreemptive and cpuProcess != null: + candidate = algorithm.selectFromReady(readyQueue) + if shouldPreempt(candidate, cpuProcess): + preempt(cpuProcess) + cpuProcess = candidate + + // 6. Selecionar próximo processo (se CPU livre) + if cpuProcess == null and readyQueue.length > 0: + cpuProcess = algorithm.selectFromReady(readyQueue) + if cpuProcess.startTick == null: + cpuProcess.startTick = tick + transitionTo(cpuProcess, "Running") + + // 7. Acumular waiting time + for each process in readyQueue: + process.waitingTime++ + + return snapshot() +``` + +--- + +## 9. Interface do Usuário + +### 9.1 Layout Geral (Desktop) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ HEADER: título, botão de preset, import/export │ +├──────────────┬──────────────────────────────────────────────────────┤ +│ │ │ +│ ESQUERDA │ CENTRO/DIREITA │ +│ (320px) │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ +│ [Processos] │ │ BARRA DE CONTROLE (play/pause/step/reset/vel.) │ │ +│ + lista │ └─────────────────────────────────────────────────┘ │ +│ + formulário│ │ +│ │ ┌─────────────────────────────────────────────────┐ │ +│ ────────── │ │ GANTT CHART (scroll horizontal) │ │ +│ │ │ Tick: 00000000000001111111111122222222222233... │ │ +│ [Algoritmo] │ │ P1: ██████░░░░░░██████░░░░░█████ │ │ +│ + parâmetros│ │ P2: ░░░░░░██████░░░░░░████░░░░░ │ │ +│ │ └─────────────────────────────────────────────────┘ │ +│ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ +│ │ │ MÉTRICAS (tabela por processo + gráficos) │ │ +│ │ └─────────────────────────────────────────────────┘ │ +└──────────────┴──────────────────────────────────────────────────────┘ +``` + +### 9.2 Cores de Estado no Gantt + +| Estado | Cor | Token CSS | +|--------|-----|-----------| +| New | Cinza escuro | `--state-new` | +| Ready | Amarelo âmbar | `--state-ready` | +| Running | Verde brilhante | `--state-running` | +| Waiting (I/O) | Azul ciano | `--state-waiting` | +| Terminated | Cinza claro | `--state-terminated` | +| Context Switch | Vermelho/laranja | `--state-ctx-switch` | + +### 9.3 Formulário de Processo + +Campos: +- **Nome** (text, max 8 chars) +- **Arrival Time** (number, ≥ 0) +- **Priority** (1–10) +- **Cor** (color picker) +- **Bursts** — editor visual de bursts com botões +/− por segmento + - Tipo: `CPU` | `I/O` + - Duração (number, ≥ 1) + - Validação: deve iniciar com CPU, alternar CPU/IO + +### 9.4 Painel de Algoritmo + +Exibe: +- Dropdown de seleção de algoritmo +- Campos condicionais: + - RR → `quantum` + - Priority → `preemptive toggle`, `aging toggle + interval` + - Multilevel → configuração de filas +- Context switch time (sempre) +- Tooltip explicativo por algoritmo + +### 9.5 Barra de Controle + +``` +[⏮ Reset] [⏭ Step] [▶ Play / ⏸ Pause] [Tick: 0042] [Velocidade: 1×▾] +``` + +### 9.6 Painel de Métricas (após simulação) + +**Tab 1 — Por Processo:** + +Tabela com colunas: +`Processo | Arrival | Start | Finish | Response | Turnaround | Waiting | CPU Time | I/O Time | CPU% | Bound` + +**Tab 2 — Globais:** + +Cards com: +- Avg Response Time +- Avg Turnaround Time +- Avg Waiting Time +- CPU Throughput +- CPU Utilization % +- Total Simulation Time + +**Tab 3 — Gráfico:** + +Gráfico de barras agrupadas por processo: +- Barras: Waiting Time, CPU Time, I/O Time (empilhadas para turnaround) + +--- + +## 10. Cenários Pré-definidos (Presets) + +| Nome | Descrição | Algoritmo sugerido | +|------|-----------|-------------------| +| `classic_fcfs` | 4 processos simples, sem I/O | FCFS | +| `convoy_effect` | Demonstra o efeito de comboio do FCFS | FCFS vs SJF | +| `rr_demo` | 5 processos com quantum 3 | Round Robin | +| `starvation` | Prioridades que causam starvation | Priority NP | +| `aging_fix` | Mesmo cenário, com aging ativo | Priority P + Aging | +| `io_heavy` | Processos I/O Bound vs CPU Bound misturados | SRTF | +| `multilevel_demo` | Demonstra MLQ | Multilevel | + +--- + +## 11. Requisitos Não-Funcionais + +| RNF | Requisito | +|-----|-----------| +| RNF-01 | Desempenho: simulações com até 16 processos e 1000 ticks sem travamento | +| RNF-02 | Responsividade: layout funcional em telas ≥ 768px | +| RNF-03 | Acessibilidade: ARIA labels nos controles principais, contraste WCAG AA | +| RNF-04 | Offline-first: 100% funcional sem internet após build | +| RNF-05 | Build size < 500 KB gzipped | +| RNF-06 | Suporte a Chrome 120+, Firefox 120+, Safari 17+ | + +--- + +## 12. Plano de Implementação (Fases) + +### Fase 1 — Core Engine (sem UI) — *~2 dias* +- [ ] Definir todos os tipos em `core/types.ts` +- [ ] Implementar `SimulationEngine` com suporte a FCFS +- [ ] Testes unitários: FCFS, estados de transição, métricas +- [ ] Implementar SJF NP e RR com testes +- [ ] Implementar Priority NP/P com aging e testes +- [ ] Implementar SRTF e HRRN com testes + +### Fase 2 — Store & State Management — *~1 dia* +- [ ] Configurar Zustand stores +- [ ] Hook `useSimulation` com loop de animação +- [ ] Integração com localStorage + +### Fase 3 — UI Base — *~2 dias* +- [ ] Layout geral (sidebar + área principal) +- [ ] `ProcessForm` e `ProcessList` +- [ ] `AlgorithmSelector` e `SchedulerConfig` +- [ ] `ControlBar` + +### Fase 4 — Gantt Chart — *~2 dias* +- [ ] Renderização de células por tick e processo +- [ ] Auto-scroll horizontal +- [ ] Tooltip por célula +- [ ] Legenda de estados + +### Fase 5 — Métricas e Gráficos — *~1 dia* +- [ ] Tabela de métricas por processo +- [ ] Cards de métricas globais +- [ ] Gráfico de barras com Recharts +- [ ] Export CSV + +### Fase 6 — Polimento e Presets — *~1 dia* +- [ ] Presets prontos para carregar +- [ ] Gerador de processos aleatórios +- [ ] Import/export JSON +- [ ] Responsividade mobile (≥768px) +- [ ] Refinamento visual + +**Total estimado: ~9 dias de trabalho focado** + +--- + +## 13. Casos de Teste Funcionais + +### CT-01: FCFS Básico + +**Entrada:** +``` +P1: arrival=0, bursts=[CPU:4] +P2: arrival=1, bursts=[CPU:3] +P3: arrival=2, bursts=[CPU:1] +``` + +**Esperado:** +``` +Gantt: P1(0-4), P2(4-7), P3(7-8) +P1: response=0, turnaround=4, waiting=0 +P2: response=3, turnaround=6, waiting=3 +P3: response=5, turnaround=6, waiting=5 +``` + +### CT-02: Round Robin (quantum=2) + +**Entrada:** +``` +P1: arrival=0, bursts=[CPU:5] +P2: arrival=0, bursts=[CPU:3] +P3: arrival=0, bursts=[CPU:1] +quantum=2 +``` + +**Esperado:** +``` +Gantt: P1(0-2), P2(2-4), P3(4-5), P1(5-7), P2(7-8), P1(8-9) +P1: response=0, turnaround=9, waiting=4 +P2: response=2, turnaround=8, waiting=5 +P3: response=4, turnaround=5, waiting=4 +``` + +### CT-03: Classificação Bound + +**Entrada:** +``` +P1: bursts=[CPU:8, IO:1, CPU:1] → cpuRatio = 9/10 = 90% → CPU Bound +P2: bursts=[CPU:1, IO:8, CPU:1] → cpuRatio = 2/10 = 20% → I/O Bound +P3: bursts=[CPU:4, IO:2, CPU:4] → cpuRatio = 8/10 = 80% → CPU Bound +``` + +### CT-04: Preempção SRTF + +**Entrada:** +``` +P1: arrival=0, bursts=[CPU:8] +P2: arrival=1, bursts=[CPU:4] +quantum=∞ (SRTF) +``` + +**Esperado:** +``` +P1 executa tick 0-1, P2 preempta no tick 1 (remaining=3<7), P2 termina tick 5, +P1 retoma e termina tick 12 +``` + +--- + +## 14. Glossário + +| Termo | Definição | +|-------|-----------| +| **Tick** | Unidade atômica de tempo na simulação (equivale a 1 unidade de tempo de CPU) | +| **Burst** | Segmento contínuo de execução (CPU ou I/O) | +| **Quantum** | Fatia máxima de CPU no Round Robin | +| **Context Switch** | Overhead ao trocar o processo em execução na CPU | +| **Starvation** | Processo que nunca recebe CPU por processos de maior prioridade sempre chegando | +| **Aging** | Mecanismo que aumenta gradualmente a prioridade de processos esperando há muito tempo | +| **Turnaround Time** | Tempo total desde a chegada até o término do processo | +| **Response Time** | Tempo entre a chegada e o primeiro uso de CPU | +| **Waiting Time** | Tempo total em que o processo ficou na fila Ready | +| **CPU Bound** | Processo que passa a maior parte do tempo usando CPU | +| **I/O Bound** | Processo que passa a maior parte do tempo aguardando I/O | +| **FCFS** | First Come, First Served — ordem de chegada | +| **SJF** | Shortest Job First — menor burst next | +| **SRTF** | Shortest Remaining Time First — SJF preemptivo | +| **RR** | Round Robin — revezamento com quantum | +| **HRRN** | Highest Response Ratio Next — balanceia tempo de espera e burst | diff --git a/index.html b/index.html new file mode 100644 index 0000000..45d3bde --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Process Scheduler Simulator + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b1e5b33 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4563 @@ +{ + "name": "pss", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pss", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.577.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.5.0", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^3.0.9", + "jsdom": "^29.0.0", + "tailwindcss": "^4.2.2", + "typescript": "~5.6.2", + "vite": "^6.2.2", + "vitest": "^3.0.9" + } + }, + "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/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", + "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "peer": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "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.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "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.1.1" + }, + "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", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "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", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "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/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "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" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "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/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "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/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "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": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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": "29.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", + "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.2", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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", + "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/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "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", + "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/pretty-format/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", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/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", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "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.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "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.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "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.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "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": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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", + "engines": { + "node": ">=8" + } + }, + "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/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.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/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.26" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", + "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.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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": "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/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6786a1e --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "pss", + "version": "1.0.0", + "description": "O Process Scheduler Simulator é uma ferramenta educacional interativa que roda inteiramente no navegador", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.577.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.5.0", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^3.0.9", + "jsdom": "^29.0.0", + "tailwindcss": "^4.2.2", + "typescript": "~5.6.2", + "vite": "^6.2.2", + "vitest": "^3.0.9" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lucasrguerra/pss.git" + }, + "keywords": [], + "author": "Lucas Rayan Guerra", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/lucasrguerra/pss/issues" + }, + "homepage": "https://github.com/lucasrguerra/pss#readme" +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..e2a1136 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,9 @@ +import AppLayout from './components/Layout/AppLayout'; + +function App() { + return ( + + ); +} + +export default App; diff --git a/src/components/ControlBar/ControlBar.tsx b/src/components/ControlBar/ControlBar.tsx new file mode 100644 index 0000000..7269ef2 --- /dev/null +++ b/src/components/ControlBar/ControlBar.tsx @@ -0,0 +1,167 @@ +import { + SkipBack, StepForward, Play, Pause, RotateCcw +} from 'lucide-react'; +import { useProcessStore } from '../../store/processStore'; +import { useSimulationStore, type SimStatus } from '../../store/simulationStore'; +import { useUiStore, SPEED_OPTIONS } from '../../store/uiStore'; +import IconButton from '../shared/IconButton'; + +const SPEED_LABELS: Record = { + 0.25: '0.25×', + 0.5: '0.5×', + 1: '1×', + 2: '2×', + 4: '4×', +}; + +const statusLabel: Record = { + idle: 'Pronto', + paused: 'Pausado', + running: 'Executando', + finished: 'Concluído', +}; + +const statusDotClass: Record = { + idle: 'bg-slate-500', + paused: 'bg-amber-400', + running: 'bg-emerald-400 animate-pulse', + finished: 'bg-blue-400', +}; + +const ControlBar = () => { + const processes = useProcessStore(s => s.processes); + const config = useProcessStore(s => s.config); + + const status = useSimulationStore(s => s.status); + const ticks = useSimulationStore(s => s.ticks); + const { init, stepOnce, play, pause, reset } = useSimulationStore(); + + const speed = useUiStore(s => s.speed); + const setSpeed = useUiStore(s => s.setSpeed); + + const currentTick = ticks.length; + const isIdle = status === 'idle'; + const isRunning = status === 'running'; + const isFinished = status === 'finished'; + const canPlay = (status === 'paused' || status === 'idle') && !isFinished && processes.length > 0; + + const handlePlay = () => { + if (isIdle) { + init(processes, config); + } + play(); + }; + + const handleStep = () => { + if (isIdle) { + init(processes, config); + } + stepOnce(); + }; + + const handleReset = () => { + if (isIdle) return; + reset(); + }; + + return ( +
+ {/* Left group: simulation controls */} +
+ {/* Reset */} + + + + + {/* Step */} + + + + + {/* Play / Pause */} + {isRunning ? ( + + + + ) : ( + + + + )} +
+ + {/* Divider */} + + ); +}; + +export default ControlBar; diff --git a/src/components/Layout/AppLayout.tsx b/src/components/Layout/AppLayout.tsx new file mode 100644 index 0000000..20ec488 --- /dev/null +++ b/src/components/Layout/AppLayout.tsx @@ -0,0 +1,13 @@ +import Sidebar from './Sidebar'; +import MainArea from './MainArea'; + +const AppLayout = () => { + return ( +
+ + +
+ ); +}; + +export default AppLayout; diff --git a/src/components/Layout/MainArea.tsx b/src/components/Layout/MainArea.tsx new file mode 100644 index 0000000..4b041f5 --- /dev/null +++ b/src/components/Layout/MainArea.tsx @@ -0,0 +1,25 @@ +import ControlBar from '../ControlBar/ControlBar'; + +const MainArea = () => { + return ( +
+
+ + + {/* Placeholder for GanttChart — Phase 4 */} +
+ [ Gantt Chart — Fase 4 ] +
+ + {/* Placeholder for MetricsPanel — Phase 5 */} +
+
+ [ Metrics Panel — Fase 5 ] +
+
+
+
+ ); +}; + +export default MainArea; diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx new file mode 100644 index 0000000..60b3d12 --- /dev/null +++ b/src/components/Layout/Sidebar.tsx @@ -0,0 +1,27 @@ +import ProcessPanel from '../ProcessPanel/ProcessPanel'; +import SchedulerPanel from '../SchedulerPanel/SchedulerPanel'; + +const Sidebar = () => { + return ( + + ); +}; + +export default Sidebar; diff --git a/src/components/ProcessPanel/ProcessCard.tsx b/src/components/ProcessPanel/ProcessCard.tsx new file mode 100644 index 0000000..85a3bc9 --- /dev/null +++ b/src/components/ProcessPanel/ProcessCard.tsx @@ -0,0 +1,73 @@ +import { Pencil, Trash2 } from 'lucide-react'; +import type { Process } from '@core/types'; +import IconButton from '../shared/IconButton'; +import Badge from '../shared/Badge'; +import { classifyBound } from '../shared/processUtils'; + +interface ProcessCardProps { + process: Process; + onEdit: (process: Process) => void; + onRemove: (id: string) => void; +} + +const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => { + const bound = classifyBound(process.bursts); + const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0); + const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0); + + const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced'; + + return ( +
+ {/* Color indicator */} + + ); +}; + +export default ProcessCard; diff --git a/src/components/ProcessPanel/ProcessForm.tsx b/src/components/ProcessPanel/ProcessForm.tsx new file mode 100644 index 0000000..a33ab59 --- /dev/null +++ b/src/components/ProcessPanel/ProcessForm.tsx @@ -0,0 +1,256 @@ +import { useState, useEffect } from 'react'; +import { Plus, Minus, Check, X } from 'lucide-react'; +import type { Process, BurstSegment } from '@core/types'; +import { useProcessStore } from '../../store/processStore'; +import IconButton from '../shared/IconButton'; +import { generateId, nextColor } from '../shared/processUtils'; + +interface ProcessFormProps { + /** If provided, the form edits this process; otherwise creates a new one. */ + editingProcess?: Process | null; + onClose: () => void; +} + +const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }]; + +const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => { + const addProcess = useProcessStore(s => s.addProcess); + const updateProcess = useProcessStore(s => s.updateProcess); + + const isEditing = !!editingProcess; + + const [name, setName] = useState(''); + const [arrivalTime, setArrivalTime] = useState(0); + const [priority, setPriority] = useState(5); + const [color, setColor] = useState(nextColor); + const [bursts, setBursts] = useState(DEFAULT_BURSTS); + const [errors, setErrors] = useState([]); + + useEffect(() => { + if (editingProcess) { + setName(editingProcess.name); + setArrivalTime(editingProcess.arrivalTime); + setPriority(editingProcess.priority); + setColor(editingProcess.color); + setBursts([...editingProcess.bursts.map(b => ({ ...b }))]); + } else { + setName(''); + setArrivalTime(0); + setPriority(5); + setColor(nextColor()); + setBursts([{ type: 'cpu', duration: 4 }]); + } + setErrors([]); + }, [editingProcess]); + + // ---- Burst helpers ---- + + const updateBurstDuration = (index: number, value: number) => { + setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b)); + }; + + const addBurst = () => { + // enforce alternating cpu/io pattern + const lastType = bursts[bursts.length - 1]?.type ?? 'io'; + const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu'; + setBursts(prev => [...prev, { type: nextType, duration: 2 }]); + }; + + const removeBurst = (index: number) => { + if (bursts.length <= 1) return; + setBursts(prev => { + const next = prev.filter((_, i) => i !== index); + // Safe-guard: ensure it still starts (and ends) with cpu + return next; + }); + }; + + // ---- Validation ---- + + const validate = (): boolean => { + const errs: string[] = []; + if (!name.trim()) errs.push('Nome é obrigatório'); + if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres'); + if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0'); + if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10'); + if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU'); + if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU'); + if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU'); + bursts.forEach((b, i) => { + if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`); + if (i > 0 && bursts[i - 1].type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`); + }); + setErrors(errs); + return errs.length === 0; + }; + + const handleSubmit = () => { + if (!validate()) return; + if (isEditing && editingProcess) { + updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts }); + } else { + const p: Process = { + id: generateId(), + name: name.trim(), + arrivalTime, + priority, + color, + bursts, + }; + addProcess(p); + } + onClose(); + }; + + const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500'; + const labelCls = 'block text-xs text-slate-400 mb-1'; + + return ( +
+
+

+ {isEditing ? 'Editar Processo' : 'Novo Processo'} +

+ + + +
+ + {/* Basic fields */} +
+
+ {/* Name */} +
+ + setName(e.target.value)} + placeholder="P1" + className={inputCls} + aria-label="Nome do processo" + /> +
+ {/* Color */} +
+ + setColor(e.target.value)} + className="h-[34px] w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer" + aria-label="Cor do processo" + /> +
+
+ + {/* Arrival */} +
+ + setArrivalTime(Number(e.target.value))} + className={inputCls} + aria-label="Tempo de chegada" + /> +
+ + {/* Priority */} +
+ + setPriority(Number(e.target.value))} + className={inputCls} + aria-label="Prioridade do processo" + /> +
+
+ + {/* Bursts editor */} +
+
+ + + + +
+
+ {bursts.map((burst, idx) => ( +
+ + {burst.type} + + updateBurstDuration(idx, Number(e.target.value))} + className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500" + aria-label={`Duração do burst ${idx + 1}`} + /> + ticks + removeBurst(idx)} + disabled={bursts.length <= 1} + className="hover:text-red-400" + > + + +
+ ))} +
+
+ + {/* Validation errors */} + {errors.length > 0 && ( +
    + {errors.map((e, i) => ( +
  • {e}
  • + ))} +
+ )} + + {/* Actions */} +
+ + +
+
+ ); +}; + +export default ProcessForm; diff --git a/src/components/ProcessPanel/ProcessList.tsx b/src/components/ProcessPanel/ProcessList.tsx new file mode 100644 index 0000000..b964963 --- /dev/null +++ b/src/components/ProcessPanel/ProcessList.tsx @@ -0,0 +1,109 @@ +import { Plus, Shuffle, Trash2 } from 'lucide-react'; +import type { Process } from '@core/types'; +import { useProcessStore } from '../../store/processStore'; +import ProcessCard from './ProcessCard'; +import IconButton from '../shared/IconButton'; +import { generateId, nextColor } from '../shared/processUtils'; + +interface ProcessListProps { + onEditProcess: (process: Process) => void; + onAddNew: () => void; +} + +const MAX_PROCESSES = 16; + +/** Generates a random process for quick testing */ +function makeRandomProcess(index: number): Process { + const cpuDur = Math.floor(Math.random() * 6) + 1; + const ioDur = Math.floor(Math.random() * 5) + 1; + const hasBothBursts = Math.random() > 0.4; + return { + id: generateId(), + name: `P${index}`, + arrivalTime: Math.floor(Math.random() * 8), + priority: Math.floor(Math.random() * 10) + 1, + color: nextColor(), + bursts: hasBothBursts + ? [{ type: 'cpu', duration: cpuDur }, { type: 'io', duration: ioDur }, { type: 'cpu', duration: Math.floor(Math.random() * 4) + 1 }] + : [{ type: 'cpu', duration: cpuDur }], + }; +} + +const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => { + const processes = useProcessStore(s => s.processes); + const removeProcess = useProcessStore(s => s.removeProcess); + const clearProcesses = useProcessStore(s => s.clearProcesses); + const addProcess = useProcessStore(s => s.addProcess); + + const canAdd = processes.length < MAX_PROCESSES; + + const handleRandomize = () => { + const p = makeRandomProcess(processes.length + 1); + addProcess(p); + }; + + return ( +
+ {/* List header */} +
+ + {processes.length}/{MAX_PROCESSES} processos + +
+ + + + + + +
+
+ + {/* Cards */} +
+ {processes.length === 0 ? ( +
+ Nenhum processo adicionado + Clique em + para criar +
+ ) : ( + processes.map(p => ( + + )) + )} +
+ + {/* Add button */} + {canAdd && ( + + )} +
+ ); +}; + +export default ProcessList; diff --git a/src/components/ProcessPanel/ProcessPanel.tsx b/src/components/ProcessPanel/ProcessPanel.tsx new file mode 100644 index 0000000..920df9a --- /dev/null +++ b/src/components/ProcessPanel/ProcessPanel.tsx @@ -0,0 +1,42 @@ +import { useState } from 'react'; +import { ListTree } from 'lucide-react'; +import type { Process } from '@core/types'; +import ProcessList from './ProcessList'; +import ProcessForm from './ProcessForm'; + +const ProcessPanel = () => { + const [editingProcess, setEditingProcess] = useState(null); + const [showForm, setShowForm] = useState(false); + + const handleEditProcess = (process: Process) => { + setEditingProcess(process); + setShowForm(true); + }; + + const handleAddNew = () => { + setEditingProcess(null); + setShowForm(true); + }; + + const handleClose = () => { + setShowForm(false); + setEditingProcess(null); + }; + + return ( +
+
+ +

Processos

+
+ + {showForm ? ( + + ) : ( + + )} +
+ ); +}; + +export default ProcessPanel; diff --git a/src/components/SchedulerPanel/AlgorithmSelector.tsx b/src/components/SchedulerPanel/AlgorithmSelector.tsx new file mode 100644 index 0000000..3dbb718 --- /dev/null +++ b/src/components/SchedulerPanel/AlgorithmSelector.tsx @@ -0,0 +1,44 @@ +import type { SchedulingAlgorithm } from '@core/types'; +import { useProcessStore } from '../../store/processStore'; + +const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string }[] = [ + { value: 'FCFS', label: 'FCFS — First Come First Served' }, + { value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' }, + { value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' }, + { value: 'RR', label: 'RR — Round Robin' }, + { value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' }, + { value: 'PRIORITY_P', label: 'Priority (Preemptivo)' }, + { value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' }, + { value: 'MULTILEVEL', label: 'Multilevel Queue' }, +]; + +const AlgorithmSelector = () => { + const algorithm = useProcessStore(s => s.config.algorithm); + const setConfig = useProcessStore(s => s.setConfig); + + return ( +
+ + +
+ ); +}; + +export default AlgorithmSelector; diff --git a/src/components/SchedulerPanel/SchedulerConfig.tsx b/src/components/SchedulerPanel/SchedulerConfig.tsx new file mode 100644 index 0000000..1e874a4 --- /dev/null +++ b/src/components/SchedulerPanel/SchedulerConfig.tsx @@ -0,0 +1,140 @@ +import { useProcessStore } from '../../store/processStore'; +import type { SchedulingAlgorithm } from '@core/types'; + +// Algorithms that require a quantum parameter +const QUANTUM_ALGORITHMS = new Set(['RR', 'MULTILEVEL']); + +// Algorithms where aging (anti-starvation) is applicable +const AGING_ALGORITHMS = new Set(['PRIORITY_NP', 'PRIORITY_P']); + +// Algorithms that are preemptive by definition (used for read-only info only) +const PREEMPTIVE_BY_DESIGN = new Set(['SJF_P', 'RR', 'PRIORITY_P', 'MULTILEVEL']); + +const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500'; +const labelCls = 'block text-xs text-slate-400 mb-1'; + +const ToggleRow = ({ + id, + label, + checked, + onChange, +}: { + id: string; + label: string; + checked: boolean; + onChange: (v: boolean) => void; +}) => ( +
+ + +
+); + +const SchedulerConfig = () => { + const config = useProcessStore(s => s.config); + const setConfig = useProcessStore(s => s.setConfig); + + const algo = config.algorithm; + const showQuantum = QUANTUM_ALGORITHMS.has(algo); + const showAging = AGING_ALGORITHMS.has(algo); + const isPreemptive = PREEMPTIVE_BY_DESIGN.has(algo); + + return ( +
+ + {/* Read-only preemptive indicator */} +
+ Tipo de escalonamento + + {isPreemptive ? 'Preemptivo' : 'Não-preemptivo'} + +
+ + {/* Context Switch */} +
+ + setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })} + className={inputCls} + aria-label="Tempo de context switch" + /> +
+ + {/* Quantum — RR and MULTILEVEL */} + {showQuantum && ( +
+ + setConfig({ quantum: Math.max(1, Number(e.target.value)) })} + className={inputCls} + aria-label="Quantum do Round Robin" + /> +
+ )} + + {/* Aging — Priority only */} + {showAging && ( +
+ setConfig({ agingEnabled: v })} + /> + {config.agingEnabled && ( +
+ + setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })} + className={inputCls} + aria-label="Intervalo de aging" + /> +
+ )} +
+ )} +
+ ); +}; + +export default SchedulerConfig; diff --git a/src/components/SchedulerPanel/SchedulerPanel.tsx b/src/components/SchedulerPanel/SchedulerPanel.tsx new file mode 100644 index 0000000..0ff7125 --- /dev/null +++ b/src/components/SchedulerPanel/SchedulerPanel.tsx @@ -0,0 +1,19 @@ +import { Settings } from 'lucide-react'; +import AlgorithmSelector from './AlgorithmSelector'; +import SchedulerConfig from './SchedulerConfig'; + +const SchedulerPanel = () => { + return ( +
+
+ +

Escalonador

+
+ + + +
+ ); +}; + +export default SchedulerPanel; diff --git a/src/components/__tests__/ControlBar.test.tsx b/src/components/__tests__/ControlBar.test.tsx new file mode 100644 index 0000000..5ee17c2 --- /dev/null +++ b/src/components/__tests__/ControlBar.test.tsx @@ -0,0 +1,100 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ControlBar from '../ControlBar/ControlBar'; +import { useProcessStore } from '../../store/processStore'; +import { useSimulationStore } from '../../store/simulationStore'; +import { useUiStore } from '../../store/uiStore'; +import type { Process } from '@core/types'; + +const MOCK_PROCESS: Process = { + id: 'p1', + name: 'P1', + arrivalTime: 0, + priority: 5, + color: '#60a5fa', + bursts: [{ type: 'cpu', duration: 4 }], +}; + +beforeEach(() => { + useProcessStore.setState({ processes: [], config: useProcessStore.getState().config }); + useSimulationStore.setState({ status: 'idle', ticks: [], engine: null, _processes: [], _config: null }); + useUiStore.setState({ speed: 1 }); +}); + +describe('ControlBar', () => { + it('renders the toolbar with all main controls', () => { + render(); + expect(screen.getByRole('toolbar')).toBeInTheDocument(); + expect(screen.getByTestId('btn-play')).toBeInTheDocument(); + expect(screen.getByTestId('btn-step')).toBeInTheDocument(); + expect(screen.getByTestId('btn-reset')).toBeInTheDocument(); + expect(screen.getByTestId('tick-display')).toBeInTheDocument(); + expect(screen.getByTestId('speed-select')).toBeInTheDocument(); + }); + + it('displays tick 0000 on initial render', () => { + render(); + expect(screen.getByTestId('tick-display')).toHaveTextContent('0000'); + }); + + it('shows "Pronto" status when idle', () => { + render(); + expect(screen.getByText('Pronto')).toBeInTheDocument(); + }); + + it('Play button is disabled when no processes are loaded', () => { + render(); + expect(screen.getByTestId('btn-play')).toBeDisabled(); + }); + + it('Step button initialises simulation and advances tick', async () => { + const user = userEvent.setup(); + useProcessStore.setState({ processes: [MOCK_PROCESS] }); + render(); + const stepBtn = screen.getByTestId('btn-step'); + await user.click(stepBtn); + expect(screen.getByTestId('tick-display')).toHaveTextContent('0001'); + }); + + it('Play button triggers init and sets status to running', async () => { + const user = userEvent.setup(); + useProcessStore.setState({ processes: [MOCK_PROCESS] }); + render(); + await user.click(screen.getByTestId('btn-play')); + expect(useSimulationStore.getState().status).toBe('running'); + }); + + it('Pause button is shown (and plays) when simulation is running', async () => { + const user = userEvent.setup(); + useProcessStore.setState({ processes: [MOCK_PROCESS] }); + render(); + await user.click(screen.getByTestId('btn-play')); + expect(screen.getByTestId('btn-pause')).toBeInTheDocument(); + await user.click(screen.getByTestId('btn-pause')); + expect(useSimulationStore.getState().status).toBe('paused'); + }); + + it('changing speed updates uiStore', async () => { + const user = userEvent.setup(); + render(); + const speedSelect = screen.getByTestId('speed-select'); + await user.selectOptions(speedSelect, '2'); + expect(useUiStore.getState().speed).toBe(2); + }); + + it('Reset is disabled when status is idle', () => { + render(); + expect(screen.getByTestId('btn-reset')).toBeDisabled(); + }); + + it('Reset becomes enabled after simulation starts and returns tick to 0', async () => { + const user = userEvent.setup(); + useProcessStore.setState({ processes: [MOCK_PROCESS] }); + render(); + await user.click(screen.getByTestId('btn-step')); + expect(screen.getByTestId('tick-display')).toHaveTextContent('0001'); + await user.click(screen.getByTestId('btn-reset')); + expect(screen.getByTestId('tick-display')).toHaveTextContent('0000'); + }); +}); diff --git a/src/components/__tests__/ProcessForm.test.tsx b/src/components/__tests__/ProcessForm.test.tsx new file mode 100644 index 0000000..455ad0b --- /dev/null +++ b/src/components/__tests__/ProcessForm.test.tsx @@ -0,0 +1,100 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ProcessForm from '../ProcessPanel/ProcessForm'; +import { useProcessStore } from '../../store/processStore'; + +// Reset Zustand processStore between tests +beforeEach(() => { + useProcessStore.setState({ processes: [], config: useProcessStore.getState().config }); + vi.clearAllMocks(); +}); + +describe('ProcessForm', () => { + it('renders the "Novo Processo" form with default fields', () => { + render( {}} />); + expect(screen.getByTestId('process-form')).toBeInTheDocument(); + expect(screen.getByText('Novo Processo')).toBeInTheDocument(); + expect(screen.getByLabelText('Nome do processo')).toBeInTheDocument(); + expect(screen.getByLabelText('Tempo de chegada')).toBeInTheDocument(); + expect(screen.getByLabelText('Prioridade do processo')).toBeInTheDocument(); + }); + + it('shows "Editar Processo" title when editingProcess is provided', () => { + const proc = { + id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff', + bursts: [{ type: 'cpu' as const, duration: 4 }], + }; + render( {}} />); + expect(screen.getByText('Editar Processo')).toBeInTheDocument(); + }); + + it('shows validation error when name is empty', async () => { + const user = userEvent.setup(); + render( {}} />); + const nameInput = screen.getByLabelText('Nome do processo'); + await user.clear(nameInput); + const submitBtn = screen.getByRole('button', { name: /Criar/i }); + await user.click(submitBtn); + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument(); + }); + + it('shows validation error when arrival time is negative', async () => { + const user = userEvent.setup(); + render( {}} />); + const nameInput = screen.getByLabelText('Nome do processo'); + await user.type(nameInput, 'P1'); + const arrivalInput = screen.getByLabelText('Tempo de chegada'); + // fireEvent.change used to reliably set a negative number bypassing HTML min attribute + fireEvent.change(arrivalInput, { target: { value: '-1' } }); + const submitBtn = screen.getByRole('button', { name: /Criar/i }); + await user.click(submitBtn); + expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument(); + }); + + it('shows validation error for name longer than 8 chars', async () => { + const user = userEvent.setup(); + render( {}} />); + const nameInput = screen.getByLabelText('Nome do processo'); + // Bypass the HTML maxLength attribute via fireEvent to test the JS validation + fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } }); + await user.click(screen.getByRole('button', { name: /Criar/i })); + expect(screen.getByText(/Nome deve ter no máximo 8 caracteres/i)).toBeInTheDocument(); + }); + + it('adds a new burst with alternating type when clicking + button', async () => { + const user = userEvent.setup(); + render( {}} />); + const addBurstBtn = screen.getByLabelText('Adicionar burst'); + await user.click(addBurstBtn); + // After one CPU burst at start, clicking + adds IO burst + // getAllByText with exact matches for the type labels + const cpuLabels = screen.getAllByText((_, el) => el?.tagName === 'SPAN' && el.textContent?.trim().toUpperCase() === 'CPU'); + const ioLabels = screen.getAllByText((_, el) => el?.tagName === 'SPAN' && el.textContent?.trim().toUpperCase() === 'IO'); + expect(cpuLabels.length).toBeGreaterThanOrEqual(1); + expect(ioLabels.length).toBeGreaterThanOrEqual(1); + }); + + it('calls onClose after successful form submission', async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + render(); + const nameInput = screen.getByLabelText('Nome do processo'); + await user.type(nameInput, 'P1'); + const submitBtn = screen.getByRole('button', { name: /Criar/i }); + await user.click(submitBtn); + expect(onClose).toHaveBeenCalledOnce(); + }); + + it('adds the process to the store after successful submission', async () => { + const user = userEvent.setup(); + render( {}} />); + const nameInput = screen.getByLabelText('Nome do processo'); + await user.type(nameInput, 'Test1'); + await user.click(screen.getByRole('button', { name: /Criar/i })); + const processes = useProcessStore.getState().processes; + expect(processes).toHaveLength(1); + expect(processes[0]?.name).toBe('Test1'); + }); +}); diff --git a/src/components/__tests__/SchedulerConfig.test.tsx b/src/components/__tests__/SchedulerConfig.test.tsx new file mode 100644 index 0000000..3c6a324 --- /dev/null +++ b/src/components/__tests__/SchedulerConfig.test.tsx @@ -0,0 +1,123 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import SchedulerConfig from '../SchedulerPanel/SchedulerConfig'; +import { useProcessStore } from '../../store/processStore'; +import type { SchedulingAlgorithm } from '@core/types'; + +const setAlgorithm = (algo: SchedulingAlgorithm) => { + useProcessStore.setState(s => ({ config: { ...s.config, algorithm: algo } })); +}; + +beforeEach(() => { + useProcessStore.setState(s => ({ + config: { + ...s.config, + algorithm: 'FCFS', + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, + }, + })); +}); + +describe('SchedulerConfig', () => { + it('always shows Context Switch field regardless of algorithm', () => { + render(); + expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument(); + }); + + it('always shows read-only preemptive badge', () => { + render(); + expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument(); + }); + + it('badge shows "Não-preemptivo" for FCFS', () => { + render(); + expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo'); + }); + + it('badge shows "Preemptivo" for SJF_P (SRTF)', () => { + setAlgorithm('SJF_P'); + render(); + expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo'); + }); + + it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => { + setAlgorithm('SJF_NP'); + render(); + expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo'); + // Confirm the old interactive preemptive switch is gone + expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument(); + }); + + it('does NOT show Quantum field for FCFS', () => { + render(); + expect(screen.queryByTestId('quantum-field')).not.toBeInTheDocument(); + }); + + it('shows Quantum field when algorithm is RR', () => { + setAlgorithm('RR'); + render(); + expect(screen.getByTestId('quantum-field')).toBeInTheDocument(); + expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument(); + }); + + it('shows Quantum field when algorithm is MULTILEVEL', () => { + setAlgorithm('MULTILEVEL'); + render(); + expect(screen.getByTestId('quantum-field')).toBeInTheDocument(); + }); + + it('does NOT show Quantum field for SJF_NP', () => { + setAlgorithm('SJF_NP'); + render(); + expect(screen.queryByTestId('quantum-field')).not.toBeInTheDocument(); + }); + + it('shows Aging toggle for PRIORITY_NP', () => { + setAlgorithm('PRIORITY_NP'); + render(); + expect(screen.getByRole('switch', { name: /Aging/i })).toBeInTheDocument(); + }); + + it('shows Aging toggle for PRIORITY_P', () => { + setAlgorithm('PRIORITY_P'); + render(); + expect(screen.getByRole('switch', { name: /Aging/i })).toBeInTheDocument(); + }); + + it('shows Aging interval field only when aging is enabled', async () => { + const user = userEvent.setup(); + setAlgorithm('PRIORITY_NP'); + render(); + expect(screen.queryByTestId('aging-interval-field')).not.toBeInTheDocument(); + const agingToggle = screen.getByRole('switch', { name: /Aging/i }); + await user.click(agingToggle); + expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument(); + expect(screen.getByLabelText('Intervalo de aging')).toBeInTheDocument(); + }); + + it('updates the store when Quantum field is changed', () => { + setAlgorithm('RR'); + render(); + const quantumInput = screen.getByLabelText('Quantum do Round Robin'); + fireEvent.change(quantumInput, { target: { value: '5' } }); + expect(useProcessStore.getState().config.quantum).toBe(5); + }); + + it('does NOT show Aging toggle for FCFS', () => { + setAlgorithm('FCFS'); + render(); + expect(screen.queryByRole('switch', { name: /Aging/i })).not.toBeInTheDocument(); + }); + + it('does NOT show Aging toggle for HRRN', () => { + setAlgorithm('HRRN'); + render(); + expect(screen.queryByRole('switch', { name: /Aging/i })).not.toBeInTheDocument(); + }); +}); + diff --git a/src/components/shared/Badge.tsx b/src/components/shared/Badge.tsx new file mode 100644 index 0000000..1d7f728 --- /dev/null +++ b/src/components/shared/Badge.tsx @@ -0,0 +1,30 @@ +import { clsx } from 'clsx'; + +type BadgeVariant = 'cpu' | 'io' | 'balanced' | 'slate'; + +interface BadgeProps { + variant?: BadgeVariant; + children: React.ReactNode; + className?: string; +} + +const variantClasses: Record = { + cpu: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30', + io: 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/30', + balanced: 'bg-violet-500/20 text-violet-400 border border-violet-500/30', + slate: 'bg-slate-700 text-slate-400 border border-slate-600', +}; + +const Badge = ({ variant = 'slate', children, className }: BadgeProps) => ( + + {children} + +); + +export default Badge; diff --git a/src/components/shared/IconButton.tsx b/src/components/shared/IconButton.tsx new file mode 100644 index 0000000..39d6e94 --- /dev/null +++ b/src/components/shared/IconButton.tsx @@ -0,0 +1,51 @@ +import { type ButtonHTMLAttributes, type ReactNode } from 'react'; +import { clsx } from 'clsx'; + +interface IconButtonProps extends ButtonHTMLAttributes { + children: ReactNode; + variant?: 'default' | 'primary' | 'danger' | 'ghost'; + size?: 'sm' | 'md'; + label: string; +} + +const variantClasses = { + default: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border-slate-600', + primary: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-500', + danger: 'bg-red-600/80 hover:bg-red-500 text-white border-red-500', + ghost: 'bg-transparent hover:bg-slate-700 text-slate-400 hover:text-slate-200 border-transparent', +}; + +const sizeClasses = { + sm: 'h-7 w-7 text-sm', + md: 'h-9 w-9 text-base', +}; + +const IconButton = ({ + children, + variant = 'default', + size = 'md', + label, + className, + disabled, + ...rest +}: IconButtonProps) => { + return ( + + ); +}; + +export default IconButton; diff --git a/src/components/shared/processUtils.ts b/src/components/shared/processUtils.ts new file mode 100644 index 0000000..24ec26d --- /dev/null +++ b/src/components/shared/processUtils.ts @@ -0,0 +1,32 @@ +import type { BurstSegment } from '@core/types'; + +/** + * Classifies a process as CPU Bound, I/O Bound, or Balanced + * based on declared burst segments (used for preview in ProcessCard). + */ +export function classifyBound(bursts: BurstSegment[]): 'CPU Bound' | 'I/O Bound' | 'Balanced' { + const cpuTotal = bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0); + const ioTotal = bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0); + const total = cpuTotal + ioTotal; + if (total === 0) return 'Balanced'; + const cpuRatio = cpuTotal / total; + const ioRatio = ioTotal / total; + if (cpuRatio >= 0.65) return 'CPU Bound'; + if (ioRatio >= 0.65) return 'I/O Bound'; + return 'Balanced'; +} + +/** Generate a short unique id */ +export function generateId(): string { + return Math.random().toString(36).slice(2, 9); +} + +/** Random vivid HEX color */ +const PALETTE = [ + '#60a5fa','#34d399','#f472b6','#fb923c','#a78bfa', + '#38bdf8','#facc15','#4ade80','#f87171','#c084fc', +]; +let _paletteIndex = 0; +export function nextColor(): string { + return PALETTE[_paletteIndex++ % PALETTE.length]; +} diff --git a/src/core/__tests__/engine.test.ts b/src/core/__tests__/engine.test.ts new file mode 100644 index 0000000..0f1205c --- /dev/null +++ b/src/core/__tests__/engine.test.ts @@ -0,0 +1,281 @@ +import { describe, it, expect } from "vitest"; +import { SimulationEngine } from "../engine"; +import type { Process, SchedulerConfig } from "../types"; + +// ============================================================ +// Helpers +// ============================================================ + +const cfg = (overrides: Partial = {}): SchedulerConfig => ({ + algorithm: "FCFS", + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, + ...overrides, +}); + +const p = ( + id: string, + arrivalTime: number, + bursts: { type: "cpu" | "io"; duration: number }[], + priority = 5, +): Process => ({ + id, + name: id.toUpperCase(), + arrivalTime, + priority, + color: "#fff", + bursts, +}); + +// ============================================================ +// CT-01: FCFS Basic +// Input: P1(arrival=0, CPU:4), P2(arrival=1, CPU:3), P3(arrival=2, CPU:1) +// Expected Gantt: P1(0-4), P2(4-7), P3(7-8) +// P1: response=0, turnaround=4, waiting=0 +// P2: response=3, turnaround=6, waiting=3 +// P3: response=5, turnaround=6, waiting=5 +// ============================================================ + +describe("CT-01: FCFS Basic", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 4 }]), + p("p2", 1, [{ type: "cpu", duration: 3 }]), + p("p3", 2, [{ type: "cpu", duration: 1 }]), + ]; + + it("produces correct Gantt sequence", () => { + const engine = new SimulationEngine(processes, cfg()); + const ticks = engine.runAll(); + + // ticks[i].tick should equal i + expect(ticks.length).toBe(8); + + // P1 runs ticks 0–3 + for (let i = 0; i <= 3; i++) { + expect(ticks[i]?.cpuProcess).toBe("p1"); + } + // P2 runs ticks 4–6 + for (let i = 4; i <= 6; i++) { + expect(ticks[i]?.cpuProcess).toBe("p2"); + } + // P3 runs tick 7 + expect(ticks[7]?.cpuProcess).toBe("p3"); + }); + + it("sets correct finishTick values", () => { + const engine = new SimulationEngine(processes, cfg()); + engine.runAll(); + const rts = engine.runtimeStates; + const rt1 = rts.find((r) => r.processId === "p1")!; + const rt2 = rts.find((r) => r.processId === "p2")!; + const rt3 = rts.find((r) => r.processId === "p3")!; + + expect(rt1.finishTick).toBe(4); + expect(rt2.finishTick).toBe(7); + expect(rt3.finishTick).toBe(8); + }); + + it("computes correct per-process metrics", () => { + const engine = new SimulationEngine(processes, cfg()); + engine.runAll(); + const rts = engine.runtimeStates; + const rt1 = rts.find((r) => r.processId === "p1")!; + const rt2 = rts.find((r) => r.processId === "p2")!; + const rt3 = rts.find((r) => r.processId === "p3")!; + + // P1: arrivalTick=0, startTick=0, finishTick=4 + expect(rt1.startTick).toBe(0); + expect(rt1.waitingTime).toBe(0); + // turnaround = 4 - 0 = 4; waiting = 4 - 4 - 0 = 0 + expect(rt1.finishTick! - rt1.arrivalTick).toBe(4); // turnaround + + // P2: arrivalTick=1, startTick=4, finishTick=7 + expect(rt2.startTick).toBe(4); + expect(rt2.waitingTime).toBe(3); // waited ticks 1,2,3 + // response = 4 - 1 = 3 + expect(rt2.startTick! - rt2.arrivalTick).toBe(3); + + // P3: arrivalTick=2, startTick=7, finishTick=8 + expect(rt3.startTick).toBe(7); + expect(rt3.waitingTime).toBe(5); // waited ticks 2,3,4,5,6 + expect(rt3.startTick! - rt3.arrivalTick).toBe(5); // response + expect(rt3.finishTick! - rt3.arrivalTick).toBe(6); // turnaround + }); +}); + +// ============================================================ +// CT-02: Round Robin (quantum=2) +// Input: P1,P2,P3 all arrival=0; CPU:5, CPU:3, CPU:1 +// Expected: P1(0-2), P2(2-4), P3(4-5), P1(5-7), P2(7-8), P1(8-9) +// ============================================================ + +describe("CT-02: Round Robin (quantum=2)", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 5 }]), + p("p2", 0, [{ type: "cpu", duration: 3 }]), + p("p3", 0, [{ type: "cpu", duration: 1 }]), + ]; + + it("produces correct Gantt sequence", () => { + const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 })); + const ticks = engine.runAll(); + + expect(ticks.length).toBe(9); + + const cpu = ticks.map((t) => t.cpuProcess); + expect(cpu[0]).toBe("p1"); + expect(cpu[1]).toBe("p1"); + expect(cpu[2]).toBe("p2"); + expect(cpu[3]).toBe("p2"); + expect(cpu[4]).toBe("p3"); + expect(cpu[5]).toBe("p1"); + expect(cpu[6]).toBe("p1"); + expect(cpu[7]).toBe("p2"); + expect(cpu[8]).toBe("p1"); + }); + + it("computes waiting times", () => { + const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 })); + engine.runAll(); + const rts = engine.runtimeStates; + const rt1 = rts.find((r) => r.processId === "p1")!; + const rt2 = rts.find((r) => r.processId === "p2")!; + const rt3 = rts.find((r) => r.processId === "p3")!; + + // P1: turnaround=9, cpuTime=5, ioTime=0 → waiting=4 + expect(rt1.waitingTime).toBe(4); + // P2: finishTick=8, arrival=0 → turnaround=8; cpuTime=3 → waiting=5 + expect(rt2.waitingTime).toBe(5); + // P3: finishTick=5, arrival=0 → turnaround=5; cpuTime=1 → waiting=4 + expect(rt3.waitingTime).toBe(4); + }); +}); + +// ============================================================ +// CT-04: SRTF Preemption +// P1(arrival=0, CPU:8), P2(arrival=1, CPU:4) +// P1 runs tick 0, P2 preempts at tick 1 (P2 remaining=4 < P1 remaining=7) +// P2 finishes tick 5 (runs ticks 1-4), P1 resumes ticks 5-11, finishes tick 12 +// ============================================================ + +describe("CT-04: SRTF Preemption", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 8 }]), + p("p2", 1, [{ type: "cpu", duration: 4 }]), + ]; + + it("P2 preempts P1 at tick 1", () => { + const engine = new SimulationEngine(processes, cfg({ algorithm: "SJF_P", isPreemptive: true })); + const ticks = engine.runAll(); + + expect(ticks[0]?.cpuProcess).toBe("p1"); // tick 0: P1 running + expect(ticks[1]?.cpuProcess).toBe("p2"); // tick 1: P2 preempts + expect(ticks[2]?.cpuProcess).toBe("p2"); + expect(ticks[3]?.cpuProcess).toBe("p2"); + expect(ticks[4]?.cpuProcess).toBe("p2"); // tick 4: P2 last tick + }); + + it("P2 finishes at tick 5 and P1 resumes", () => { + const engine = new SimulationEngine(processes, cfg({ algorithm: "SJF_P", isPreemptive: true })); + const ticks = engine.runAll(); + const rts = engine.runtimeStates; + const rt2 = rts.find((r) => r.processId === "p2")!; + const rt1 = rts.find((r) => r.processId === "p1")!; + + expect(rt2.finishTick).toBe(5); + expect(rt1.finishTick).toBe(12); + // P1 resumes at tick 5 + expect(ticks[5]?.cpuProcess).toBe("p1"); + }); +}); + +// ============================================================ +// General engine behaviour +// ============================================================ + +describe("SimulationEngine general", () => { + it("isFinished returns false mid-run, true after runAll", () => { + const engine = new SimulationEngine( + [p("p1", 0, [{ type: "cpu", duration: 3 }])], + cfg(), + ); + expect(engine.isFinished).toBe(false); + engine.step(); + expect(engine.isFinished).toBe(false); + engine.runAll(); + expect(engine.isFinished).toBe(true); + }); + + it("step() after isFinished throws", () => { + const engine = new SimulationEngine( + [p("p1", 0, [{ type: "cpu", duration: 1 }])], + cfg(), + ); + engine.runAll(); + expect(() => engine.step()).toThrow(); + }); + + it("runAll() and repeated step() produce same tick sequence", () => { + const procs = [ + p("p1", 0, [{ type: "cpu", duration: 3 }]), + p("p2", 1, [{ type: "cpu", duration: 2 }]), + ]; + const e1 = new SimulationEngine(procs, cfg()); + const e2 = new SimulationEngine(procs, cfg()); + + const fromRunAll = e1.runAll(); + const fromStep: ReturnType[] = []; + while (!e2.isFinished) fromStep.push(e2.step()); + + expect(fromRunAll).toEqual(fromStep); + }); + + it("process with I/O: transitions through Waiting state", () => { + const engine = new SimulationEngine( + [p("p1", 0, [{ type: "cpu", duration: 2 }, { type: "io", duration: 2 }, { type: "cpu", duration: 1 }])], + cfg(), + ); + const ticks = engine.runAll(); + const states = ticks.map((t) => t.states["p1"]); + // Running for 2, Waiting for 2, Running for 1 + expect(states[0]).toBe("Running"); + expect(states[1]).toBe("Running"); + expect(states[2]).toBe("Waiting"); + expect(states[3]).toBe("Waiting"); + expect(states[4]).toBe("Running"); + }); + + it("process arriving after simulation starts is correctly scheduled", () => { + const engine = new SimulationEngine( + [ + p("p1", 0, [{ type: "cpu", duration: 3 }]), + p("p2", 5, [{ type: "cpu", duration: 2 }]), + ], + cfg(), + ); + const ticks = engine.runAll(); + // P1 finishes at tick 3; CPU idle 3,4; P2 arrives at 5 + expect(ticks[4]?.cpuProcess).toBeNull(); // tick 4: idle + expect(ticks[5]?.cpuProcess).toBe("p2"); + }); + + it("context switch inserts idle ticks between processes", () => { + const engine = new SimulationEngine( + [ + p("p1", 0, [{ type: "cpu", duration: 2 }]), + p("p2", 0, [{ type: "cpu", duration: 2 }]), + ], + cfg({ algorithm: "FCFS", contextSwitchTime: 1 }), + ); + const ticks = engine.runAll(); + // P1 runs ticks 0-1, context switch tick 2, P2 runs ticks 3-4 + expect(ticks[0]?.cpuProcess).toBe("p1"); + expect(ticks[1]?.cpuProcess).toBe("p1"); + expect(ticks[2]?.contextSwitching).toBe(true); + expect(ticks[2]?.cpuProcess).toBeNull(); + expect(ticks[3]?.cpuProcess).toBe("p2"); + }); +}); diff --git a/src/core/__tests__/fcfs.test.ts b/src/core/__tests__/fcfs.test.ts new file mode 100644 index 0000000..7799993 --- /dev/null +++ b/src/core/__tests__/fcfs.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { fcfs } from "../algorithms/fcfs"; +import type { ProcessRuntime } from "../types"; + +function makeRt(id: string, arrivalTick: number): ProcessRuntime { + return { + processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0, + arrivalTick, startTick: null, finishTick: null, + waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null, + quantumRemaining: 2, currentPriority: 5, + }; +} + +const config = { algorithm: "FCFS" as const, quantum: 2, contextSwitchTime: 0, isPreemptive: false, agingEnabled: false, agingInterval: 5 }; + +describe("FCFS select()", () => { + it("returns null for empty queue", () => { + expect(fcfs.select([], config)).toBeNull(); + }); + + it("returns single process", () => { + const rt = makeRt("p1", 0); + expect(fcfs.select([rt], config)).toBe(rt); + }); + + it("returns earliest-arriving process", () => { + const r1 = makeRt("p1", 5); + const r2 = makeRt("p2", 2); + const r3 = makeRt("p3", 8); + expect(fcfs.select([r1, r2, r3], config)?.processId).toBe("p2"); + }); + + it("tiebreaks by processId lexicographic", () => { + const r1 = makeRt("p2", 0); + const r2 = makeRt("p1", 0); + expect(fcfs.select([r1, r2], config)?.processId).toBe("p1"); + }); +}); + +describe("FCFS shouldPreempt()", () => { + it("always returns false", () => { + const a = makeRt("p1", 0); + const b = makeRt("p2", 0); + expect(fcfs.shouldPreempt(a, b, config)).toBe(false); + }); +}); + +describe("FCFS isQuantumExpired()", () => { + it("always returns false", () => { + const rt = makeRt("p1", 0); + rt.quantumRemaining = 0; + expect(fcfs.isQuantumExpired(rt, config)).toBe(false); + }); +}); diff --git a/src/core/__tests__/hrrn.test.ts b/src/core/__tests__/hrrn.test.ts new file mode 100644 index 0000000..bcf221b --- /dev/null +++ b/src/core/__tests__/hrrn.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from "vitest"; +import { hrrn } from "../algorithms/hrrn"; +import { SimulationEngine } from "../engine"; +import type { Process, ProcessRuntime, SchedulerConfig } from "../types"; + +function makeRt(id: string, remainingBurst: number, waitingTime: number, arrivalTick = 0): ProcessRuntime { + return { + processId: id, state: "Ready", remainingBurst, burstIndex: 0, + arrivalTick, startTick: null, finishTick: null, + waitingTime, cpuTime: 0, ioTime: 0, responseTime: null, + quantumRemaining: 2, currentPriority: 5, + }; +} + +const config: SchedulerConfig = { + algorithm: "HRRN", quantum: 2, contextSwitchTime: 0, + isPreemptive: false, agingEnabled: false, agingInterval: 5, +}; + +const p = (id: string, arrival: number, cpuDuration: number): Process => ({ + id, name: id, arrivalTime: arrival, priority: 5, color: "#fff", + bursts: [{ type: "cpu", duration: cpuDuration }], +}); + +describe("HRRN select()", () => { + it("returns null for empty queue", () => { + expect(hrrn.select([], config)).toBeNull(); + }); + + it("picks process with highest response ratio", () => { + // ratio = (waitingTime + burstTime) / burstTime + // P1: (0 + 8) / 8 = 1.0 + // P2: (6 + 3) / 3 = 3.0 ← highest + // P3: (2 + 5) / 5 = 1.4 + const r1 = makeRt("p1", 8, 0); + const r2 = makeRt("p2", 3, 6); + const r3 = makeRt("p3", 5, 2); + expect(hrrn.select([r1, r2, r3], config)?.processId).toBe("p2"); + }); + + it("tiebreaks equal ratio by arrival then id", () => { + // Both ratio = (4 + 4) / 4 = 2.0 + const r1 = makeRt("p2", 4, 4, 1); // arrives at 1 + const r2 = makeRt("p1", 4, 4, 2); // arrives at 2 + expect(hrrn.select([r1, r2], config)?.processId).toBe("p2"); + }); + + it("shouldPreempt always false", () => { + const a = makeRt("p1", 4, 0); + const b = makeRt("p2", 4, 10); + expect(hrrn.shouldPreempt(a, b, config)).toBe(false); + }); +}); + +describe("HRRN integration", () => { + it("long-waiting short process wins over fresh long process", () => { + // P1 arrives at 0 with CPU:8 — occupies CPU first + // P2 arrives at 0 with CPU:4, waits for 8 ticks + // After P1 finishes: P2 has been waiting → ratio for P2 is high + // (No other choice here, but validates engine runs HRRN correctly) + const processes = [p("p1", 0, 4), p("p2", 4, 2)]; + const engine = new SimulationEngine(processes, config); + const ticks = engine.runAll(); + // P1 runs ticks 0-3, P2 arrives at tick 4 and runs immediately + expect(ticks[4]?.cpuProcess).toBe("p2"); + expect(engine.isFinished).toBe(true); + }); + + it("HRRN selects high-ratio process over long process that just arrived", () => { + // P1(arrival=0, CPU:10) starts running + // P2(arrival=1, CPU:2), P3(arrival=1, CPU:8) both arrive at tick 1 + // When P1 finishes, P2 has waited longer relative to its burst → wins HRRN + const processes = [ + p("p1", 0, 3), // short; finishes tick 3 + p("p2", 0, 6), // medium; waits 3 ticks → ratio = (3+6)/6 = 1.5 + p("p3", 0, 2), // short; waits 3 ticks → ratio = (3+2)/2 = 2.5 ← wins + ]; + const engine = new SimulationEngine(processes, config); + const ticks = engine.runAll(); + // After P1 (ticks 0-2), P3 should win (ratio 2.5 > 1.5) + expect(ticks[3]?.cpuProcess).toBe("p3"); + }); +}); diff --git a/src/core/__tests__/metrics.test.ts b/src/core/__tests__/metrics.test.ts new file mode 100644 index 0000000..ef5b61a --- /dev/null +++ b/src/core/__tests__/metrics.test.ts @@ -0,0 +1,163 @@ +import { describe, it, expect } from "vitest"; +import { computeProcessMetrics, computeGlobalMetrics, classifyBound } from "../metrics"; +import { SimulationEngine } from "../engine"; +import type { Process, SchedulerConfig } from "../types"; + +const cfg = (overrides: Partial = {}): SchedulerConfig => ({ + algorithm: "FCFS", + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, + ...overrides, +}); + +const p = (id: string, arrivalTime: number, bursts: { type: "cpu" | "io"; duration: number }[]): Process => ({ + id, name: id, arrivalTime, priority: 5, color: "#fff", bursts, +}); + +// ============================================================ +// CT-03: Bound classification (static, on declared bursts) +// P1: CPU:8, IO:1, CPU:1 → cpuRatio = 9/10 = 90% → CPU Bound +// P2: CPU:1, IO:8, CPU:1 → cpuRatio = 2/10 = 20% → I/O Bound +// P3: CPU:4, IO:2, CPU:4 → cpuRatio = 8/10 = 80% → CPU Bound +// ============================================================ + +describe("CT-03: classifyBound", () => { + it("P1 (CPU:8,IO:1,CPU:1) → CPU Bound", () => { + expect(classifyBound([{ type: "cpu", duration: 8 }, { type: "io", duration: 1 }, { type: "cpu", duration: 1 }])) + .toBe("CPU Bound"); + }); + + it("P2 (CPU:1,IO:8,CPU:1) → I/O Bound", () => { + expect(classifyBound([{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }])) + .toBe("I/O Bound"); + }); + + it("P3 (CPU:4,IO:2,CPU:4) → CPU Bound", () => { + expect(classifyBound([{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }])) + .toBe("CPU Bound"); + }); + + it("balanced process (CPU:5,IO:5) → Balanced", () => { + expect(classifyBound([{ type: "cpu", duration: 5 }, { type: "io", duration: 5 }])) + .toBe("Balanced"); + }); + + it("CPU-only process → CPU Bound", () => { + expect(classifyBound([{ type: "cpu", duration: 10 }])).toBe("CPU Bound"); + }); +}); + +// ============================================================ +// computeProcessMetrics +// ============================================================ + +describe("computeProcessMetrics", () => { + it("throws for non-terminated process", () => { + const engine = new SimulationEngine( + [p("p1", 0, [{ type: "cpu", duration: 5 }])], + cfg(), + ); + engine.step(); // tick 0, not finished + const rt = engine.runtimeStates[0]!; + expect(() => computeProcessMetrics(rt)).toThrow(); + }); + + it("CT-01 P1: response=0, turnaround=4, waiting=0, cpuTime=4", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 4 }]), + p("p2", 1, [{ type: "cpu", duration: 3 }]), + p("p3", 2, [{ type: "cpu", duration: 1 }]), + ]; + const engine = new SimulationEngine(processes, cfg()); + engine.runAll(); + const rt1 = engine.runtimeStates.find((r) => r.processId === "p1")!; + const m = computeProcessMetrics(rt1); + + expect(m.responseTime).toBe(0); + expect(m.turnaroundTime).toBe(4); + expect(m.waitingTime).toBe(0); + expect(m.cpuTime).toBe(4); + expect(m.ioTime).toBe(0); + expect(m.boundType).toBe("CPU Bound"); + }); + + it("CT-01 P2: response=3, turnaround=6, waiting=3", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 4 }]), + p("p2", 1, [{ type: "cpu", duration: 3 }]), + p("p3", 2, [{ type: "cpu", duration: 1 }]), + ]; + const engine = new SimulationEngine(processes, cfg()); + engine.runAll(); + const rt2 = engine.runtimeStates.find((r) => r.processId === "p2")!; + const m = computeProcessMetrics(rt2); + + expect(m.responseTime).toBe(3); + expect(m.turnaroundTime).toBe(6); + expect(m.waitingTime).toBe(3); + }); + + it("process with I/O: correct ioTime, boundType", () => { + const engine = new SimulationEngine( + [p("p1", 0, [{ type: "cpu", duration: 2 }, { type: "io", duration: 8 }, { type: "cpu", duration: 2 }])], + cfg(), + ); + engine.runAll(); + const rt = engine.runtimeStates[0]!; + const m = computeProcessMetrics(rt); + expect(m.cpuTime).toBe(4); + expect(m.ioTime).toBe(8); + expect(m.boundType).toBe("I/O Bound"); + }); + + it("cpuUtilization: CPU-only → 100%", () => { + const engine = new SimulationEngine( + [p("p1", 0, [{ type: "cpu", duration: 4 }])], + cfg(), + ); + engine.runAll(); + const rt = engine.runtimeStates[0]!; + const m = computeProcessMetrics(rt); + expect(m.cpuUtilization).toBe(100); + }); +}); + +// ============================================================ +// computeGlobalMetrics +// ============================================================ + +describe("computeGlobalMetrics", () => { + it("CT-01 global metrics", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 4 }]), + p("p2", 1, [{ type: "cpu", duration: 3 }]), + p("p3", 2, [{ type: "cpu", duration: 1 }]), + ]; + const engine = new SimulationEngine(processes, cfg()); + engine.runAll(); + const rts = engine.runtimeStates; + const pMetrics = rts.map(computeProcessMetrics); + const g = computeGlobalMetrics(pMetrics, engine.currentTick + 1); + + // Avg response: (0 + 3 + 5) / 3 = 8/3 ≈ 2.667 + expect(g.avgResponseTime).toBeCloseTo(8 / 3, 5); + // Avg turnaround: (4 + 6 + 6) / 3 = 16/3 ≈ 5.333 + expect(g.avgTurnaroundTime).toBeCloseTo(16 / 3, 5); + // Avg waiting: (0 + 3 + 5) / 3 = 8/3 ≈ 2.667 + expect(g.avgWaitingTime).toBeCloseTo(8 / 3, 5); + // Throughput: 3 processes / 8 ticks = 0.375 + expect(g.cpuThroughput).toBeCloseTo(3 / 8, 5); + // CPU utilization: (4+3+1)/8 * 100 = 100% + expect(g.cpuUtilization).toBeCloseTo(100, 5); + expect(g.totalSimulationTime).toBe(8); + }); + + it("handles empty metrics array", () => { + const g = computeGlobalMetrics([], 0); + expect(g.avgResponseTime).toBe(0); + expect(g.cpuUtilization).toBe(0); + }); +}); diff --git a/src/core/__tests__/priority.test.ts b/src/core/__tests__/priority.test.ts new file mode 100644 index 0000000..62bd8d7 --- /dev/null +++ b/src/core/__tests__/priority.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from "vitest"; +import { priorityNp, priorityP } from "../algorithms/priority"; +import { SimulationEngine } from "../engine"; +import type { Process, ProcessRuntime, SchedulerConfig } from "../types"; + +function makeRt(id: string, priority: number, arrivalTick = 0): ProcessRuntime { + return { + processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0, + arrivalTick, startTick: null, finishTick: null, + waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null, + quantumRemaining: 2, currentPriority: priority, + }; +} + +const cfgNp: SchedulerConfig = { + algorithm: "PRIORITY_NP", quantum: 2, contextSwitchTime: 0, + isPreemptive: false, agingEnabled: false, agingInterval: 5, +}; +const cfgP: SchedulerConfig = { + algorithm: "PRIORITY_P", quantum: 2, contextSwitchTime: 0, + isPreemptive: true, agingEnabled: false, agingInterval: 5, +}; + +const p = (id: string, arrival: number, priority: number, cpuDuration: number): Process => ({ + id, name: id, arrivalTime: arrival, priority, color: "#fff", + bursts: [{ type: "cpu", duration: cpuDuration }], +}); + +describe("PRIORITY_NP select()", () => { + it("picks lowest priority number (highest priority)", () => { + const r1 = makeRt("p1", 5); + const r2 = makeRt("p2", 2); + const r3 = makeRt("p3", 8); + expect(priorityNp.select([r1, r2, r3], cfgNp)?.processId).toBe("p2"); + }); + + it("tiebreaks by arrival then id", () => { + const r1 = makeRt("p2", 3, 2); + const r2 = makeRt("p1", 3, 5); // p1 arrives later + expect(priorityNp.select([r1, r2], cfgNp)?.processId).toBe("p2"); + }); + + it("shouldPreempt always false", () => { + const a = makeRt("p1", 5); + const b = makeRt("p2", 1); + expect(priorityNp.shouldPreempt(a, b, cfgNp)).toBe(false); + }); +}); + +describe("PRIORITY_P shouldPreempt()", () => { + it("preempts when candidate has higher priority (lower number)", () => { + const current = makeRt("p1", 5); + const candidate = makeRt("p2", 2); + expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(true); + }); + + it("does NOT preempt on equal priority", () => { + const current = makeRt("p1", 3); + const candidate = makeRt("p2", 3); + expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(false); + }); + + it("does NOT preempt when current has higher priority", () => { + const current = makeRt("p1", 2); + const candidate = makeRt("p2", 5); + expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(false); + }); +}); + +describe("Priority NP integration", () => { + it("selects highest-priority process regardless of arrival", () => { + const processes = [ + p("p1", 0, 5, 3), // low priority + p("p2", 0, 1, 3), // high priority + ]; + const engine = new SimulationEngine(processes, cfgNp); + const ticks = engine.runAll(); + // P2 (priority 1) should run first + expect(ticks[0]?.cpuProcess).toBe("p2"); + expect(ticks[1]?.cpuProcess).toBe("p2"); + expect(ticks[2]?.cpuProcess).toBe("p2"); + expect(ticks[3]?.cpuProcess).toBe("p1"); + }); +}); + +describe("Priority P + aging", () => { + it("aging decrements currentPriority for waiting processes", () => { + // P1 priority=1 runs first; P2 priority=10 waits + // After agingInterval=2 ticks, P2's priority should drop + const processes = [ + p("p1", 0, 1, 10), // runs for 10 ticks + p("p2", 0, 10, 5), // waits, aging should help + ]; + const engine = new SimulationEngine(processes, { + ...cfgNp, algorithm: "PRIORITY_NP", agingEnabled: true, agingInterval: 2, + }); + engine.step(); // tick 0 + engine.step(); // tick 1 + engine.step(); // tick 2 — aging fires (tick % 2 === 0), P2 priority 10→9 + const rts = engine.runtimeStates; + const rt2 = rts.find((r) => r.processId === "p2")!; + expect(rt2.currentPriority).toBe(9); + }); + + it("aging does not reduce priority below 1", () => { + const processes = [p("p1", 0, 1, 5)]; + // Construct a runtime with priority already at 1 and test the engine handles it + const engine = new SimulationEngine(processes, { + ...cfgNp, agingEnabled: true, agingInterval: 1, + }); + // Run a few ticks — priority should never go below 1 + for (let i = 0; i < 5; i++) { + if (!engine.isFinished) engine.step(); + } + const rt = engine.runtimeStates[0]!; + expect(rt.currentPriority).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/src/core/__tests__/rr.test.ts b/src/core/__tests__/rr.test.ts new file mode 100644 index 0000000..77e0091 --- /dev/null +++ b/src/core/__tests__/rr.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from "vitest"; +import { rr } from "../algorithms/rr"; +import { SimulationEngine } from "../engine"; +import type { Process, ProcessRuntime, SchedulerConfig } from "../types"; + +function makeRt(id: string, quantumRemaining = 2): ProcessRuntime { + return { + processId: id, state: "Running", remainingBurst: 4, burstIndex: 0, + arrivalTick: 0, startTick: 0, finishTick: null, + waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null, + quantumRemaining, currentPriority: 5, + }; +} + +const config: SchedulerConfig = { + algorithm: "RR", quantum: 2, contextSwitchTime: 0, + isPreemptive: false, agingEnabled: false, agingInterval: 5, +}; + +const p = (id: string, arrival: number, cpuDuration: number): Process => ({ + id, name: id, arrivalTime: arrival, priority: 5, color: "#fff", + bursts: [{ type: "cpu", duration: cpuDuration }], +}); + +describe("RR select()", () => { + it("returns front of queue (FIFO)", () => { + const r1: ProcessRuntime = { ...makeRt("p1"), state: "Ready" }; + const r2: ProcessRuntime = { ...makeRt("p2"), state: "Ready" }; + expect(rr.select([r1, r2], config)?.processId).toBe("p1"); + expect(rr.select([r2, r1], config)?.processId).toBe("p2"); + }); + + it("returns null for empty queue", () => { + expect(rr.select([], config)).toBeNull(); + }); +}); + +describe("RR isQuantumExpired()", () => { + it("returns true when quantumRemaining <= 0", () => { + expect(rr.isQuantumExpired(makeRt("p1", 0), config)).toBe(true); + expect(rr.isQuantumExpired(makeRt("p1", -1), config)).toBe(true); + }); + + it("returns false when quantum not yet exhausted", () => { + expect(rr.isQuantumExpired(makeRt("p1", 1), config)).toBe(false); + expect(rr.isQuantumExpired(makeRt("p1", 2), config)).toBe(false); + }); +}); + +describe("RR shouldPreempt()", () => { + it("always returns false (preemption via quantum expiry only)", () => { + const a = makeRt("p1"); + const b = makeRt("p2"); + expect(rr.shouldPreempt(a, b, config)).toBe(false); + }); +}); + +// CT-02 integration +describe("CT-02: Round Robin integration", () => { + it("produces correct Gantt: P1(0-2),P2(2-4),P3(4-5),P1(5-7),P2(7-8),P1(8-9)", () => { + const processes = [ + p("p1", 0, 5), + p("p2", 0, 3), + p("p3", 0, 1), + ]; + const engine = new SimulationEngine(processes, config); + const ticks = engine.runAll(); + const cpu = ticks.map((t) => t.cpuProcess); + + expect(cpu).toHaveLength(9); + expect(cpu[0]).toBe("p1"); + expect(cpu[1]).toBe("p1"); + expect(cpu[2]).toBe("p2"); + expect(cpu[3]).toBe("p2"); + expect(cpu[4]).toBe("p3"); + expect(cpu[5]).toBe("p1"); + expect(cpu[6]).toBe("p1"); + expect(cpu[7]).toBe("p2"); + expect(cpu[8]).toBe("p1"); + }); + + it("process finishing before quantum exhausted does not rotate", () => { + const engine = new SimulationEngine( + [p("p1", 0, 1), p("p2", 0, 3)], + config, + ); + const ticks = engine.runAll(); + // P1 finishes at tick 1 (1 < quantum 2), P2 gets CPU immediately + expect(ticks[0]?.cpuProcess).toBe("p1"); + expect(ticks[1]?.cpuProcess).toBe("p2"); + expect(ticks[2]?.cpuProcess).toBe("p2"); + expect(ticks[3]?.cpuProcess).toBe("p2"); + }); + + it("process returning from I/O enters tail of queue", () => { + // P1: CPU:1 → IO:1 → CPU:1; P2: CPU:3 + // With quantum=2: P1 runs tick 0, goes to IO; P2 runs ticks 1-2,3 (or until P1 returns) + // P1 IO done at tick 2, enters tail → P2 finishes first if still running + const processes: Process[] = [ + { id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#fff", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 2 }, { type: "cpu", duration: 1 }] }, + { id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#fff", bursts: [{ type: "cpu", duration: 3 }] }, + ]; + const engine = new SimulationEngine(processes, config); + const ticks = engine.runAll(); + const rts = engine.runtimeStates; + // Both should terminate + expect(rts.every((r) => r.state === "Terminated")).toBe(true); + // P1's second CPU burst should happen after P2 gets at least one quantum + const rt2 = rts.find((r) => r.processId === "p2")!; + const rt1 = rts.find((r) => r.processId === "p1")!; + // P2 has 3 CPU ticks and P1 re-enters after IO — verify both complete + expect(rt2.cpuTime).toBe(3); + expect(rt1.cpuTime).toBe(2); + void ticks; // used for debugging if needed + }); +}); diff --git a/src/core/__tests__/sjf.test.ts b/src/core/__tests__/sjf.test.ts new file mode 100644 index 0000000..012f1f9 --- /dev/null +++ b/src/core/__tests__/sjf.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from "vitest"; +import { sjfNp, srtf } from "../algorithms/sjf"; +import { SimulationEngine } from "../engine"; +import type { Process, ProcessRuntime, SchedulerConfig } from "../types"; + +function makeRt(id: string, remainingBurst: number, arrivalTick = 0): ProcessRuntime { + return { + processId: id, state: "Ready", remainingBurst, burstIndex: 0, + arrivalTick, startTick: null, finishTick: null, + waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null, + quantumRemaining: 2, currentPriority: 5, + }; +} + +const config: SchedulerConfig = { + algorithm: "SJF_NP", quantum: 2, contextSwitchTime: 0, + isPreemptive: false, agingEnabled: false, agingInterval: 5, +}; + +const p = (id: string, arrival: number, bursts: { type: "cpu" | "io"; duration: number }[]): Process => ({ + id, name: id, arrivalTime: arrival, priority: 5, color: "#fff", bursts, +}); + +describe("SJF_NP select()", () => { + it("returns null for empty queue", () => { + expect(sjfNp.select([], config)).toBeNull(); + }); + + it("picks shortest remaining burst", () => { + const r1 = makeRt("p1", 8); + const r2 = makeRt("p2", 3); + const r3 = makeRt("p3", 5); + expect(sjfNp.select([r1, r2, r3], config)?.processId).toBe("p2"); + }); + + it("tiebreaks equal burst by arrival then id", () => { + const r1 = makeRt("p2", 4, 1); + const r2 = makeRt("p1", 4, 2); + // p2 arrives earlier + expect(sjfNp.select([r1, r2], config)?.processId).toBe("p2"); + }); + + it("shouldPreempt always false", () => { + const a = makeRt("p1", 4); + const b = makeRt("p2", 2); + expect(sjfNp.shouldPreempt(a, b, config)).toBe(false); + }); +}); + +describe("SRTF shouldPreempt()", () => { + it("preempts when candidate has strictly smaller burst", () => { + const current = makeRt("p1", 7); + const candidate = makeRt("p2", 4); + expect(srtf.shouldPreempt(current, candidate, config)).toBe(true); + }); + + it("does NOT preempt on equal remaining burst", () => { + const current = makeRt("p1", 4); + const candidate = makeRt("p2", 4); + expect(srtf.shouldPreempt(current, candidate, config)).toBe(false); + }); + + it("does NOT preempt when current has smaller burst", () => { + const current = makeRt("p1", 3); + const candidate = makeRt("p2", 6); + expect(srtf.shouldPreempt(current, candidate, config)).toBe(false); + }); +}); + +// CT-04 integration via engine +describe("CT-04: SRTF Preemption (integration)", () => { + it("P2 preempts P1 at tick 1, finishes tick 5, P1 finishes tick 12", () => { + const processes = [ + p("p1", 0, [{ type: "cpu", duration: 8 }]), + p("p2", 1, [{ type: "cpu", duration: 4 }]), + ]; + const engine = new SimulationEngine(processes, { + ...config, algorithm: "SJF_P", + }); + const ticks = engine.runAll(); + const rts = engine.runtimeStates; + + expect(ticks[0]?.cpuProcess).toBe("p1"); + expect(ticks[1]?.cpuProcess).toBe("p2"); + expect(ticks[2]?.cpuProcess).toBe("p2"); + expect(ticks[3]?.cpuProcess).toBe("p2"); + expect(ticks[4]?.cpuProcess).toBe("p2"); + + const rt2 = rts.find((r) => r.processId === "p2")!; + const rt1 = rts.find((r) => r.processId === "p1")!; + expect(rt2.finishTick).toBe(5); + expect(rt1.finishTick).toBe(12); + }); +}); diff --git a/src/core/algorithms/fcfs.ts b/src/core/algorithms/fcfs.ts new file mode 100644 index 0000000..a88e0f7 --- /dev/null +++ b/src/core/algorithms/fcfs.ts @@ -0,0 +1,21 @@ +import type { SchedulerAlgorithm } from "./index"; +import { tiebreak } from "./utils"; + +/** + * FCFS — First Come, First Served + * Non-preemptive. Selects the process with earliest arrival; tiebreak by id. + */ +export const fcfs: SchedulerAlgorithm = { + select(readyQueue) { + if (readyQueue.length === 0) return null; + return [...readyQueue].sort(tiebreak)[0] ?? null; + }, + + shouldPreempt() { + return false; + }, + + isQuantumExpired() { + return false; + }, +}; diff --git a/src/core/algorithms/hrrn.ts b/src/core/algorithms/hrrn.ts new file mode 100644 index 0000000..1a3f30b --- /dev/null +++ b/src/core/algorithms/hrrn.ts @@ -0,0 +1,33 @@ +import type { ProcessRuntime } from "../types"; +import type { SchedulerAlgorithm } from "./index"; +import { tiebreak } from "./utils"; + +function hrrnRatio(rt: ProcessRuntime): number { + // ratio = (waitingTime + burstTime) / burstTime + // remainingBurst > 0 is guaranteed for processes in the ready queue + if (rt.remainingBurst <= 0) return Infinity; + return (rt.waitingTime + rt.remainingBurst) / rt.remainingBurst; +} + +/** + * HRRN — Highest Response Ratio Next + * Non-preemptive. Balances waiting time and burst length to prevent starvation. + */ +export const hrrn: SchedulerAlgorithm = { + select(readyQueue) { + if (readyQueue.length === 0) return null; + return [...readyQueue].sort((a, b) => { + const ratioDiff = hrrnRatio(b) - hrrnRatio(a); // descending + if (ratioDiff !== 0) return ratioDiff; + return tiebreak(a, b); + })[0] ?? null; + }, + + shouldPreempt() { + return false; + }, + + isQuantumExpired() { + return false; + }, +}; diff --git a/src/core/algorithms/index.ts b/src/core/algorithms/index.ts new file mode 100644 index 0000000..a596f29 --- /dev/null +++ b/src/core/algorithms/index.ts @@ -0,0 +1,59 @@ +import type { ProcessRuntime, SchedulerConfig, SchedulingAlgorithm } from "../types"; +import { fcfs } from "./fcfs"; +import { sjfNp, srtf } from "./sjf"; +import { rr } from "./rr"; +import { priorityNp, priorityP } from "./priority"; +import { hrrn } from "./hrrn"; +import { multilevel } from "./multilevel"; + +// ============================================================ +// SchedulerAlgorithm interface +// All implementations must be pure (no mutation of the queue). +// ============================================================ + +export interface SchedulerAlgorithm { + /** + * Select the best process to dispatch from the ready queue. + * Returns null if the queue is empty. + * Must NOT mutate the array; the engine handles removal. + */ + select( + readyQueue: readonly ProcessRuntime[], + config: SchedulerConfig, + ): ProcessRuntime | null; + + /** + * For preemptive algorithms: should the currently-running process + * be replaced by `candidate`? + * Called only when cpuProcess != null and a candidate exists. + */ + shouldPreempt( + current: ProcessRuntime, + candidate: ProcessRuntime, + config: SchedulerConfig, + ): boolean; + + /** + * RR only: has the process exhausted its quantum? + * For all other algorithms this must return false. + */ + isQuantumExpired( + runtime: ProcessRuntime, + config: SchedulerConfig, + ): boolean; +} + +// ============================================================ +// Dispatch table — engine looks up algorithm by name +// ============================================================ + +export const algorithms: Record = { + FCFS: fcfs, + SJF_NP: sjfNp, + SJF_P: srtf, + RR: rr, + PRIORITY_NP: priorityNp, + PRIORITY_P: priorityP, + HRRN: hrrn, + MULTILEVEL: multilevel, +}; diff --git a/src/core/algorithms/multilevel.ts b/src/core/algorithms/multilevel.ts new file mode 100644 index 0000000..a23acc7 --- /dev/null +++ b/src/core/algorithms/multilevel.ts @@ -0,0 +1,19 @@ +import type { SchedulerAlgorithm } from "./index"; + +/** + * MULTILEVEL — Multilevel Queue (stub) + * Not yet implemented. Deferred to a later phase. + */ +export const multilevel: SchedulerAlgorithm = { + select() { + throw new Error("MULTILEVEL scheduling is not yet implemented"); + }, + + shouldPreempt() { + throw new Error("MULTILEVEL scheduling is not yet implemented"); + }, + + isQuantumExpired() { + throw new Error("MULTILEVEL scheduling is not yet implemented"); + }, +}; diff --git a/src/core/algorithms/priority.ts b/src/core/algorithms/priority.ts new file mode 100644 index 0000000..a844b5d --- /dev/null +++ b/src/core/algorithms/priority.ts @@ -0,0 +1,49 @@ +import type { ProcessRuntime } from "../types"; +import type { SchedulerAlgorithm } from "./index"; +import { tiebreak } from "./utils"; + +function prioritySort(a: ProcessRuntime, b: ProcessRuntime): number { + // Lower number = higher priority + if (a.currentPriority !== b.currentPriority) + return a.currentPriority - b.currentPriority; + return tiebreak(a, b); +} + +/** + * Priority Non-Preemptive + * Selects process with lowest priority number; does not interrupt running process. + */ +export const priorityNp: SchedulerAlgorithm = { + select(readyQueue) { + if (readyQueue.length === 0) return null; + return [...readyQueue].sort(prioritySort)[0] ?? null; + }, + + shouldPreempt() { + return false; + }, + + isQuantumExpired() { + return false; + }, +}; + +/** + * Priority Preemptive + * Preempts running process if a higher-priority (lower number) process arrives. + * Aging is applied by the engine — currentPriority decrements over time. + */ +export const priorityP: SchedulerAlgorithm = { + select(readyQueue) { + if (readyQueue.length === 0) return null; + return [...readyQueue].sort(prioritySort)[0] ?? null; + }, + + shouldPreempt(current, candidate) { + return candidate.currentPriority < current.currentPriority; + }, + + isQuantumExpired() { + return false; + }, +}; diff --git a/src/core/algorithms/rr.ts b/src/core/algorithms/rr.ts new file mode 100644 index 0000000..c1b4ac6 --- /dev/null +++ b/src/core/algorithms/rr.ts @@ -0,0 +1,22 @@ +import type { SchedulerAlgorithm } from "./index"; + +/** + * RR — Round Robin + * FIFO queue with a configurable quantum. + * select() returns the front of the queue (insertion-order maintained by engine). + * Preemption is driven by quantum expiry, not by a better candidate. + */ +export const rr: SchedulerAlgorithm = { + select(readyQueue) { + return readyQueue[0] ?? null; + }, + + // RR does not preempt based on candidate priority; quantum expiry handles it. + shouldPreempt() { + return false; + }, + + isQuantumExpired(runtime) { + return runtime.quantumRemaining <= 0; + }, +}; diff --git a/src/core/algorithms/sjf.ts b/src/core/algorithms/sjf.ts new file mode 100644 index 0000000..6578bba --- /dev/null +++ b/src/core/algorithms/sjf.ts @@ -0,0 +1,48 @@ +import type { ProcessRuntime } from "../types"; +import type { SchedulerAlgorithm } from "./index"; +import { tiebreak } from "./utils"; + +function sjfSort(a: ProcessRuntime, b: ProcessRuntime): number { + if (a.remainingBurst !== b.remainingBurst) + return a.remainingBurst - b.remainingBurst; + return tiebreak(a, b); +} + +/** + * SJF Non-Preemptive — Shortest Job First + * Selects the process with the smallest remaining burst; no preemption. + */ +export const sjfNp: SchedulerAlgorithm = { + select(readyQueue) { + if (readyQueue.length === 0) return null; + return [...readyQueue].sort(sjfSort)[0] ?? null; + }, + + shouldPreempt() { + return false; + }, + + isQuantumExpired() { + return false; + }, +}; + +/** + * SRTF — Shortest Remaining Time First (SJF Preemptive) + * At each tick, preempts if a ready process has strictly smaller remainingBurst. + * Equal remaining burst → no preemption (avoids unnecessary context switches). + */ +export const srtf: SchedulerAlgorithm = { + select(readyQueue) { + if (readyQueue.length === 0) return null; + return [...readyQueue].sort(sjfSort)[0] ?? null; + }, + + shouldPreempt(current, candidate) { + return candidate.remainingBurst < current.remainingBurst; + }, + + isQuantumExpired() { + return false; + }, +}; diff --git a/src/core/algorithms/utils.ts b/src/core/algorithms/utils.ts new file mode 100644 index 0000000..bccae3b --- /dev/null +++ b/src/core/algorithms/utils.ts @@ -0,0 +1,11 @@ +import type { ProcessRuntime } from "../types"; + +// ============================================================ +// Shared tiebreak helper (all algorithms use this) +// Primary: arrivalTick ASC Secondary: processId lexicographic ASC +// ============================================================ + +export function tiebreak(a: ProcessRuntime, b: ProcessRuntime): number { + if (a.arrivalTick !== b.arrivalTick) return a.arrivalTick - b.arrivalTick; + return a.processId.localeCompare(b.processId); +} diff --git a/src/core/engine.ts b/src/core/engine.ts new file mode 100644 index 0000000..50f9af3 --- /dev/null +++ b/src/core/engine.ts @@ -0,0 +1,270 @@ +import type { + Process, + ProcessRuntime, + SchedulerConfig, + SimTick, + StateLabel, +} from "./types"; +import { algorithms } from "./algorithms/index"; +import type { SchedulerAlgorithm } from "./algorithms/index"; + +// ============================================================ +// SimulationEngine +// ============================================================ + +export class SimulationEngine { + private readonly _processes: Process[]; + private readonly _config: SchedulerConfig; + private readonly _algorithm: SchedulerAlgorithm; + + private _tick: number; // starts at -1; incremented at top of step() + private _runtimes: Map; + private _cpuProcess: ProcessRuntime | null; + private _readyQueue: ProcessRuntime[]; // ordered array (FIFO for RR) + private _ioQueue: ProcessRuntime[]; + private _contextSwitchRemaining: number; + private _prevCpuProcessId: string | null; // track context switch detection + + constructor(processes: Process[], config: SchedulerConfig) { + if (processes.length === 0) throw new Error("At least one process required"); + this._processes = processes; + this._config = config; + this._algorithm = algorithms[config.algorithm]; + this._tick = -1; + this._cpuProcess = null; + this._readyQueue = []; + this._ioQueue = []; + this._contextSwitchRemaining = 0; + this._prevCpuProcessId = null; + this._runtimes = new Map(processes.map((p) => [p.id, initRuntime(p, config)])); + } + + // ---------------------------------------------------------- + // Public API + // ---------------------------------------------------------- + + /** Advance one tick; return snapshot of that tick. */ + step(): SimTick { + if (this.isFinished) throw new Error("Simulation is already finished"); + + this._tick++; + + // 1. Arrivals + for (const p of this._processes) { + const rt = this._runtimes.get(p.id)!; + if (p.arrivalTime === this._tick && rt.state === "New") { + this._transition(rt, "Ready"); + this._readyQueue.push(rt); + } + } + + // 2. Preemption & Quantum Check (from previous tick's state) + if (this._cpuProcess !== null && this._readyQueue.length > 0) { + const candidate = this._algorithm.select(this._readyQueue, this._config); + if ( + candidate !== null && + this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config) + ) { + const cpu = this._cpuProcess; + this._transition(cpu, "Ready"); + this._readyQueue.push(cpu); + this._cpuProcess = null; + } + } + + // 3. Dispatch / Context Switch + if (this._contextSwitchRemaining > 0) { + // Doing context switch this tick + // We don't dispatch. + } else { + if (this._cpuProcess === null && this._readyQueue.length > 0) { + const next = this._algorithm.select(this._readyQueue, this._config); + if (next !== null) { + const idx = this._readyQueue.indexOf(next); + if (idx !== -1) this._readyQueue.splice(idx, 1); + + if ( + this._config.contextSwitchTime > 0 && + this._prevCpuProcessId !== null && + this._prevCpuProcessId !== next.processId + ) { + this._readyQueue.unshift(next); + this._contextSwitchRemaining = this._config.contextSwitchTime; + this._prevCpuProcessId = next.processId; + } else { + if (next.startTick === null) next.startTick = this._tick; + next.quantumRemaining = this._config.quantum; + this._transition(next, "Running"); + this._prevCpuProcessId = next.processId; + this._cpuProcess = next; + } + } + } + } + + // 4. Take Snapshot representing what happens DURING this tick + const snapshot = this._snapshot(); + + // 5. Passage of Time (Simulate execution for 1 tick) + if (this._contextSwitchRemaining > 0) { + this._contextSwitchRemaining--; + } + + if (this._cpuProcess !== null) { + const cpu = this._cpuProcess; + cpu.remainingBurst--; + cpu.cpuTime++; + cpu.quantumRemaining--; + } + + for (const rt of this._ioQueue) { + rt.remainingBurst--; + rt.ioTime++; + } + + for (const rt of this._readyQueue) { + rt.waitingTime++; + } + + // 6. Aging (Priority) + if ( + this._config.agingEnabled && + this._tick > 0 && + this._tick % this._config.agingInterval === 0 + ) { + for (const rt of this._readyQueue) { + rt.currentPriority = Math.max(1, rt.currentPriority - 1); + } + } + + // 7. Process completions / transition to next state for NEXT tick + if (this._cpuProcess !== null) { + const cpu = this._cpuProcess; + if (cpu.remainingBurst === 0) { + this._advanceBurst(cpu); + const process = this._processes.find((p) => p.id === cpu.processId)!; + const nextBurst = process.bursts[cpu.burstIndex]; + if (nextBurst !== undefined && nextBurst.type === "io") { + this._transition(cpu, "Waiting"); + this._ioQueue.push(cpu); + } else if (nextBurst !== undefined && nextBurst.type === "cpu") { + this._transition(cpu, "Ready"); + this._readyQueue.push(cpu); + } else { + this._transition(cpu, "Terminated"); + cpu.finishTick = this._tick + 1; + } + this._cpuProcess = null; + } else if (this._algorithm.isQuantumExpired(cpu, this._config)) { + this._transition(cpu, "Ready"); + this._readyQueue.push(cpu); + this._cpuProcess = null; + } + } + + // IO completions + const nextIoQueue: ProcessRuntime[] = []; + for (const rt of this._ioQueue) { + if (rt.remainingBurst === 0) { + this._advanceBurst(rt); + const process = this._processes.find((p) => p.id === rt.processId)!; + if (rt.burstIndex < process.bursts.length) { + this._transition(rt, "Ready"); + this._readyQueue.push(rt); + } else { + this._transition(rt, "Terminated"); + rt.finishTick = this._tick + 1; + } + } else { + nextIoQueue.push(rt); + } + } + this._ioQueue = nextIoQueue; + + return snapshot; + } + + /** Run to completion; return all tick snapshots. */ + runAll(): SimTick[] { + const results: SimTick[] = []; + while (!this.isFinished) { + results.push(this.step()); + if (results.length > 10_000) { + throw new Error("Simulation exceeded 10,000 ticks — check for infinite loop"); + } + } + return results; + } + + get isFinished(): boolean { + for (const rt of this._runtimes.values()) { + if (rt.state !== "Terminated") return false; + } + return true; + } + + get currentTick(): number { + return this._tick; + } + + get runtimeStates(): ProcessRuntime[] { + return Array.from(this._runtimes.values()); + } + + // ---------------------------------------------------------- + // Private helpers + // ---------------------------------------------------------- + + private _transition(rt: ProcessRuntime, state: StateLabel): void { + rt.state = state; + } + + private _advanceBurst(rt: ProcessRuntime): void { + rt.burstIndex++; + const process = this._processes.find((p) => p.id === rt.processId)!; + const nextBurst = process.bursts[rt.burstIndex]; + rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0; + } + + private _snapshot(): SimTick { + const states: Record = {}; + for (const [id, rt] of this._runtimes) { + states[id] = rt.state; + } + const contextSwitching = this._contextSwitchRemaining > 0; + return { + tick: this._tick, + cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null), + ioProcesses: this._ioQueue.map((rt) => rt.processId), + readyQueue: this._readyQueue.map((rt) => rt.processId), + states, + contextSwitching, + }; + } +} + +// ============================================================ +// Helper: initialise a ProcessRuntime from a Process +// ============================================================ + +function initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime { + const firstBurst = p.bursts[0]; + if (firstBurst === undefined || firstBurst.type !== "cpu") { + throw new Error(`Process ${p.id}: first burst must be a CPU burst`); + } + return { + processId: p.id, + state: "New", + remainingBurst: firstBurst.duration, + burstIndex: 0, + arrivalTick: p.arrivalTime, + startTick: null, + finishTick: null, + waitingTime: 0, + cpuTime: 0, + ioTime: 0, + responseTime: null, + quantumRemaining: config.quantum, + currentPriority: p.priority, + }; +} diff --git a/src/core/metrics.ts b/src/core/metrics.ts new file mode 100644 index 0000000..5075d2b --- /dev/null +++ b/src/core/metrics.ts @@ -0,0 +1,107 @@ +import type { + GlobalMetrics, + ProcessMetrics, + ProcessRuntime, +} from "./types"; + +// ============================================================ +// Per-process metrics +// ============================================================ + +export function computeProcessMetrics(rt: ProcessRuntime): ProcessMetrics { + if (rt.startTick === null || rt.finishTick === null) { + throw new Error( + `Cannot compute metrics for process ${rt.processId}: not yet terminated`, + ); + } + + const turnaroundTime = rt.finishTick - rt.arrivalTick; + const waitingTime = turnaroundTime - rt.cpuTime - rt.ioTime; + const responseTime = rt.startTick - rt.arrivalTick; + const cpuUtilization = + turnaroundTime > 0 ? (rt.cpuTime / turnaroundTime) * 100 : 0; + + const total = rt.cpuTime + rt.ioTime; + const cpuRatio = total > 0 ? rt.cpuTime / total : 1; + const ioRatio = total > 0 ? rt.ioTime / total : 0; + + let boundType: "CPU Bound" | "I/O Bound" | "Balanced"; + if (cpuRatio >= 0.65) { + boundType = "CPU Bound"; + } else if (ioRatio >= 0.65) { + boundType = "I/O Bound"; + } else { + boundType = "Balanced"; + } + + return { + processId: rt.processId, + arrivalTime: rt.arrivalTick, + startTick: rt.startTick, + finishTick: rt.finishTick, + responseTime, + turnaroundTime, + waitingTime, + cpuTime: rt.cpuTime, + ioTime: rt.ioTime, + cpuUtilization, + boundType, + }; +} + +// ============================================================ +// Global metrics (across all completed processes) +// ============================================================ + +export function computeGlobalMetrics( + metrics: ProcessMetrics[], + totalTicks: number, +): GlobalMetrics { + if (metrics.length === 0) { + return { + avgResponseTime: 0, + avgTurnaroundTime: 0, + avgWaitingTime: 0, + cpuThroughput: 0, + cpuUtilization: 0, + totalSimulationTime: totalTicks, + }; + } + + const mean = (values: number[]) => + values.reduce((a, b) => a + b, 0) / values.length; + + const totalCpuTime = metrics.reduce((s, m) => s + m.cpuTime, 0); + + return { + avgResponseTime: mean(metrics.map((m) => m.responseTime)), + avgTurnaroundTime: mean(metrics.map((m) => m.turnaroundTime)), + avgWaitingTime: mean(metrics.map((m) => m.waitingTime)), + cpuThroughput: metrics.length / totalTicks, + cpuUtilization: totalTicks > 0 ? (totalCpuTime / totalTicks) * 100 : 0, + totalSimulationTime: totalTicks, + }; +} + +// ============================================================ +// Classify process by declared burst ratios (static, pre-simulation) +// Used in the process list preview badge. +// ============================================================ + +export function classifyBound( + bursts: { type: "cpu" | "io"; duration: number }[], +): "CPU Bound" | "I/O Bound" | "Balanced" { + let cpu = 0; + let io = 0; + for (const b of bursts) { + if (b.type === "cpu") cpu += b.duration; + else io += b.duration; + } + const total = cpu + io; + if (total === 0) return "Balanced"; + const cpuRatio = cpu / total; + const ioRatio = io / total; + if (cpuRatio >= 0.65) return "CPU Bound"; + if (ioRatio >= 0.65) return "I/O Bound"; + return "Balanced"; +} diff --git a/src/core/presets.ts b/src/core/presets.ts new file mode 100644 index 0000000..5e52c81 --- /dev/null +++ b/src/core/presets.ts @@ -0,0 +1,80 @@ +import type { Process, SchedulerConfig } from "./types"; + +export interface Preset { + name: string; + description: string; + processes: Process[]; + config: SchedulerConfig; +} + +const defaultConfig = ( + overrides: Partial = {}, +): SchedulerConfig => ({ + algorithm: "FCFS", + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, + ...overrides, +}); + +export const presets: Preset[] = [ + { + name: "classic_fcfs", + description: "4 simple processes, no I/O — demonstrates FCFS order", + processes: [ + { id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 5 }] }, + { id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] }, + { id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 7 }] }, + ], + config: defaultConfig({ algorithm: "FCFS" }), + }, + { + name: "rr_demo", + description: "5 processes with quantum 3 — Round Robin rotation", + processes: [ + { id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }] }, + { id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] }, + { id: "p3", name: "P3", arrivalTime: 1, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 5 }] }, + { id: "p4", name: "P4", arrivalTime: 2, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] }, + { id: "p5", name: "P5", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] }, + ], + config: defaultConfig({ algorithm: "RR", quantum: 3 }), + }, + { + name: "starvation", + description: "High-priority processes starve P5 — demonstrates Priority NP starvation", + processes: [ + { id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] }, + ], + config: defaultConfig({ algorithm: "PRIORITY_NP" }), + }, + { + name: "aging_fix", + description: "Same starvation scenario fixed with aging", + processes: [ + { id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] }, + { id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] }, + ], + config: defaultConfig({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 }), + }, + { + name: "io_heavy", + description: "Mixed CPU-bound and I/O-bound processes", + processes: [ + { id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }, { type: "io", duration: 1 }, { type: "cpu", duration: 2 }] }, + { id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }] }, + { id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }] }, + ], + config: defaultConfig({ algorithm: "SJF_P", isPreemptive: true }), + }, +]; diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..f948320 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,103 @@ +// ============================================================ +// Core types — Process Scheduler Simulator +// ============================================================ + +export type SchedulingAlgorithm = + | "FCFS" + | "SJF_NP" + | "SJF_P" + | "RR" + | "PRIORITY_NP" + | "PRIORITY_P" + | "MULTILEVEL" + | "HRRN"; + +// --------------- Static process definition ------------------ + +export interface BurstSegment { + type: "cpu" | "io"; + duration: number; // must be ≥ 1 +} + +export interface Process { + id: string; + name: string; + arrivalTime: number; // tick of arrival (≥ 0) + priority: number; // 1 (highest) – 10 (lowest); default 5 + color: string; // HEX color for Gantt display + bursts: BurstSegment[]; // must start (and end) with a CPU burst +} + +// --------------- Scheduler configuration -------------------- + +export interface SchedulerConfig { + algorithm: SchedulingAlgorithm; + quantum: number; // for RR (default 2) + contextSwitchTime: number; // overhead between processes (default 0) + isPreemptive: boolean; // applicable to SJF and Priority + agingEnabled: boolean; // prevent starvation in Priority scheduling + agingInterval: number; // ticks in Ready before priority-- (min 1) +} + +// --------------- Runtime state ------------------------------ + +export type StateLabel = + | "New" // before arrivalTime + | "Ready" // in ready queue, waiting for CPU + | "Running" // executing CPU burst + | "Waiting" // executing I/O burst + | "Terminated"; // finished all bursts + +export interface ProcessRuntime { + // ---- spec fields ---- + processId: string; + state: StateLabel; + remainingBurst: number; // ticks left in current burst + burstIndex: number; // index into Process.bursts + arrivalTick: number; // tick when process first entered Ready + startTick: number | null; // tick of first Running state + finishTick: number | null; // tick when process Terminated + waitingTime: number; // accumulated ticks in Ready + cpuTime: number; // accumulated ticks in Running + ioTime: number; // accumulated ticks in Waiting + responseTime: number | null; // startTick - arrivalTick (set once) + // ---- engine-internal fields ---- + quantumRemaining: number; // remaining quantum for RR; reset on dispatch + currentPriority: number; // mutable via aging; starts = Process.priority +} + +// --------------- Tick snapshot ------------------------------ + +export interface SimTick { + tick: number; + cpuProcess: string | null; // processId running on CPU (null if idle) + ioProcesses: string[]; // processIds in I/O + readyQueue: string[]; // processIds in ready queue (in queue order) + states: Record; // all processId → state + contextSwitching: boolean; // true during context-switch overhead +} + +// --------------- Metrics ------------------------------------ + +export interface ProcessMetrics { + processId: string; + arrivalTime: number; + startTick: number; + finishTick: number; + responseTime: number; + turnaroundTime: number; + waitingTime: number; + cpuTime: number; + ioTime: number; + cpuUtilization: number; // cpuTime / turnaroundTime * 100 + boundType: "CPU Bound" | "I/O Bound" | "Balanced"; +} + +export interface GlobalMetrics { + avgResponseTime: number; + avgTurnaroundTime: number; + avgWaitingTime: number; + cpuThroughput: number; // processes completed / total ticks + cpuUtilization: number; // Σ cpuTime / totalTicks * 100 + totalSimulationTime: number; +} diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..ba58ace --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,50 @@ +import { useState, useCallback } from "react"; + +/** + * Generic hook for reading/writing typed values to localStorage. + * + * Returns [value, setValue, removeValue]. + * Read/write errors are silently ignored. + */ +export function useLocalStorage( + key: string, + initialValue: T, +): readonly [T, (value: T | ((prev: T) => T)) => void, () => void] { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item !== null ? (JSON.parse(item) as T) : initialValue; + } catch { + return initialValue; + } + }); + + const setValue = useCallback( + (value: T | ((prev: T) => T)) => { + setStoredValue((prev) => { + const next = + typeof value === "function" + ? (value as (prev: T) => T)(prev) + : value; + try { + window.localStorage.setItem(key, JSON.stringify(next)); + } catch { + // ignore write errors (e.g. private mode quota exceeded) + } + return next; + }); + }, + [key], + ); + + const removeValue = useCallback(() => { + try { + window.localStorage.removeItem(key); + } catch { + // ignore + } + setStoredValue(initialValue); + }, [key, initialValue]); + + return [storedValue, setValue, removeValue] as const; +} diff --git a/src/hooks/useSimulation.ts b/src/hooks/useSimulation.ts new file mode 100644 index 0000000..dede56a --- /dev/null +++ b/src/hooks/useSimulation.ts @@ -0,0 +1,50 @@ +import { useEffect, useRef } from "react"; +import { useSimulationStore } from "../store/simulationStore"; +import { useUiStore } from "../store/uiStore"; + +/** + * Drives the simulation animation loop using requestAnimationFrame. + * Call once at the app root. Returns nothing — side-effect only. + * + * Tick interval = 1000 / speed (ms). + * The loop is active only while status === "running". + */ +export function useSimulation(): void { + const status = useSimulationStore((s) => s.status); + const stepOnce = useSimulationStore((s) => s.stepOnce); + const speed = useUiStore((s) => s.speed); + + const rafRef = useRef(undefined); + const lastTickTime = useRef(0); + + useEffect(() => { + if (status !== "running") { + if (rafRef.current !== undefined) { + cancelAnimationFrame(rafRef.current); + rafRef.current = undefined; + } + return; + } + + const interval = 1000 / speed; + + const loop = (timestamp: number) => { + if (timestamp - lastTickTime.current >= interval) { + lastTickTime.current = timestamp; + stepOnce(); + } + rafRef.current = requestAnimationFrame(loop); + }; + + // Reset timing so the first tick fires immediately after play + lastTickTime.current = 0; + rafRef.current = requestAnimationFrame(loop); + + return () => { + if (rafRef.current !== undefined) { + cancelAnimationFrame(rafRef.current); + rafRef.current = undefined; + } + }; + }, [status, speed, stepOnce]); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..c426e83 --- /dev/null +++ b/src/index.css @@ -0,0 +1,20 @@ +@import "tailwindcss"; + +@theme { + --color-state-new: #475569; /* slate-600 */ + --color-state-ready: #f59e0b; /* amber-500 */ + --color-state-running: #10b981; /* emerald-500 */ + --color-state-waiting: #06b6d4; /* cyan-500 */ + --color-state-terminated: #94a3b8; /* slate-400 */ + --color-state-ctx: #ef4444; /* red-500 */ +} + +@layer utilities { + .scrollbar-hide::-webkit-scrollbar { + display: none; + } + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..340a6e5 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/store/processStore.ts b/src/store/processStore.ts new file mode 100644 index 0000000..7ece227 --- /dev/null +++ b/src/store/processStore.ts @@ -0,0 +1,62 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import type { Process, SchedulerConfig } from "@core/types"; + +// ============================================================ +// Defaults +// ============================================================ + +const DEFAULT_CONFIG: SchedulerConfig = { + algorithm: "FCFS", + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, +}; + +// ============================================================ +// Store +// ============================================================ + +interface ProcessStore { + processes: Process[]; + config: SchedulerConfig; + + addProcess: (p: Process) => void; + updateProcess: (id: string, updates: Partial) => void; + removeProcess: (id: string) => void; + clearProcesses: () => void; + setConfig: (updates: Partial) => void; + loadPreset: (processes: Process[], config: SchedulerConfig) => void; +} + +export const useProcessStore = create()( + persist( + (set) => ({ + processes: [], + config: DEFAULT_CONFIG, + + addProcess: (p) => + set((s) => ({ processes: [...s.processes, p] })), + + updateProcess: (id, updates) => + set((s) => ({ + processes: s.processes.map((p) => + p.id === id ? { ...p, ...updates } : p, + ), + })), + + removeProcess: (id) => + set((s) => ({ processes: s.processes.filter((p) => p.id !== id) })), + + clearProcesses: () => set({ processes: [] }), + + setConfig: (updates) => + set((s) => ({ config: { ...s.config, ...updates } })), + + loadPreset: (processes, config) => set({ processes, config }), + }), + { name: "pss:session" }, + ), +); diff --git a/src/store/simulationStore.ts b/src/store/simulationStore.ts new file mode 100644 index 0000000..049336b --- /dev/null +++ b/src/store/simulationStore.ts @@ -0,0 +1,69 @@ +import { create } from "zustand"; +import { SimulationEngine } from "@core/engine"; +import type { Process, SchedulerConfig, SimTick } from "@core/types"; + +// ============================================================ +// Types +// ============================================================ + +export type SimStatus = "idle" | "running" | "paused" | "finished"; + +interface SimulationStore { + status: SimStatus; + ticks: SimTick[]; + // Mutable engine instance — reactivity comes from ticks[] and status, not engine itself + engine: SimulationEngine | null; + // Saved so reset() can recreate the engine without coupling to processStore + _processes: Process[]; + _config: SchedulerConfig | null; + + init: (processes: Process[], config: SchedulerConfig) => void; + stepOnce: () => void; + play: () => void; + pause: () => void; + reset: () => void; +} + +// ============================================================ +// Store +// ============================================================ + +export const useSimulationStore = create()((set, get) => ({ + status: "idle", + ticks: [], + engine: null, + _processes: [], + _config: null, + + init: (processes, config) => { + const engine = new SimulationEngine(processes, config); + set({ + engine, + ticks: [], + status: "paused", + _processes: processes, + _config: config, + }); + }, + + stepOnce: () => { + const { engine, ticks, status } = get(); + if (!engine || engine.isFinished || status === "finished") return; + const tick = engine.step(); + set({ + ticks: [...ticks, tick], + status: engine.isFinished ? "finished" : status, + }); + }, + + play: () => set({ status: "running" }), + + pause: () => set({ status: "paused" }), + + reset: () => { + const { _processes, _config } = get(); + if (_processes.length === 0 || _config === null) return; + const engine = new SimulationEngine(_processes, _config); + set({ engine, ticks: [], status: "paused" }); + }, +})); diff --git a/src/store/uiStore.ts b/src/store/uiStore.ts new file mode 100644 index 0000000..bf05ade --- /dev/null +++ b/src/store/uiStore.ts @@ -0,0 +1,28 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +// ============================================================ +// Speed options +// ============================================================ + +export const SPEED_OPTIONS = [0.25, 0.5, 1, 2, 4] as const; +export type SpeedOption = (typeof SPEED_OPTIONS)[number]; + +// ============================================================ +// Store +// ============================================================ + +interface UiStore { + speed: number; + setSpeed: (speed: number) => void; +} + +export const useUiStore = create()( + persist( + (set) => ({ + speed: 1, + setSpeed: (speed) => set({ speed }), + }), + { name: "pss:ui" }, + ), +); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..ca97cdd --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "paths": { + "@core/*": ["./src/core/*"], + "@components/*": ["./src/components/*"], + "@store/*": ["./src/store/*"] + } + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..9bc4845 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..93bb192 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +/// +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + test: { + globals: true, + environment: "jsdom", + setupFiles: "./src/test/setup.ts", + coverage: { + provider: "v8", + include: ["src/core/**", "src/components/**"], + }, + }, + resolve: { + alias: { + "@core": path.resolve(__dirname, "./src/core"), + "@components": path.resolve(__dirname, "./src/components"), + "@store": path.resolve(__dirname, "./src/store"), + }, + }, +});