diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2cd7175..e1ddaa6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -37,7 +37,8 @@ "Bash(mv components/DemandasFilters.tsx components/TasksFilters.tsx)", "Bash(npx --yes tsc --noEmit --project \"c:/Users/dinam/Documents/Projetos/run-way/tsconfig.json\")", "Bash(npm show:*)", - "Bash(ls c:/Users/dinam/Documents/Projetos/run-way/node_modules/boneyard-js/react*)" + "Bash(ls c:/Users/dinam/Documents/Projetos/run-way/node_modules/boneyard-js/react*)", + "Bash(npm list *)" ] }, "hooks": { diff --git a/CLAUDE.md b/CLAUDE.md index c5dec3e..dbca1ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,8 @@ npm run lint # ESLint **Member:** `id, name, role ('Designer'|'Developer'), avatar (iniciais), avatar_url?, email?, auth_user_id?, access_role ('admin'|'user'), is_active?, created_at?, deactivated_at?` +**UserPreferences:** `id, user_id (fk → members.id), theme ('light'|'dark'|'system'), language ('pt-BR'|'en'), notifications_enabled, default_view ('home'|'calendar'|'timeline'|'list'), client_order (text[]), notification_step_overdue (bool, def true), notification_task_stalled (bool, def true), notification_member_overloaded (bool, def true), stalled_days_threshold (int 1–30, def 5), overload_threshold (int 1–20, def 3), created_at, updated_at` + **Fases:** Design (5d, violeta) → Approval (3d, laranja) → Dev (7d, azul) → QA (3d, esmeralda). Cascata automática. ## Branding @@ -42,6 +44,7 @@ npm run lint # ESLint - **Co-location:** componentes e hooks **privados** de uma view ficam dentro dela (`views//components/`, `views//hooks/`). Só sobem para `src/components/` ou `src/hooks/` quando usados por 2+ views - `.env` nunca commitado - **Testes:** toda nova feature ou bugfix deve ter testes unitários com Vitest. Rodar com `npm run test:run`. Arquivos de teste ficam em `src/**/__tests__/` co-localizados com o módulo testado +- **Migrations:** toda alteração de schema no Supabase requer um arquivo SQL em `supabase/migrations/`. Nomenclatura: `YYYYMMDD_.sql` — seq começa em `000000`; se houver mais de uma migration no mesmo dia, incrementar a seq (`000001`, `000002`, …). Nunca alterar migrations já aplicadas. ## CI/CD (.github/workflows/) @@ -63,6 +66,7 @@ npm run lint # ESLint | TasksView, filtros, badge de prazo, ActionMenu | [docs/views/tasks.md](docs/views/tasks.md) | | MembersView, capacidade | [docs/views/members.md](docs/views/members.md) | | AdminView, UsersPanel, useAdminData, useAdminStore | [docs/views/admin.md](docs/views/admin.md) | +| ProfileView, useProfile, user_preferences | [docs/views/profile.md](docs/views/profile.md) | | TaskModal, cascata de fases | [docs/components/task-modal.md](docs/components/task-modal.md) | | Design system (Button, Input, Label, Badge) | [docs/components/ui.md](docs/components/ui.md) | | useSupabase, CRUD, steps | [docs/hooks/supabase.md](docs/hooks/supabase.md) | diff --git a/docs/architecture.md b/docs/architecture.md index dc3257e..d7b56cc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,13 +4,17 @@ ``` src/ -├── App.tsx # Root: providers, roteamento, inicialização do cliente +├── App.tsx # Root: gates de auth + composição declarativa — ~80 linhas ├── main.tsx # Entry point ├── components/ -│ ├── TaskModal.tsx # Modal criar/editar demanda -│ ├── AppHeader.tsx # Header: logo, hamburger mobile, NotificationBell, theme toggle (desktop) -│ ├── AppSidebar.tsx # Sidebar de navegação; theme toggle no footer mobile -│ └── ui/ # Design system (Button, Input, Label, Badge) +│ ├── AppRouter.tsx # Mapeia view → componente; guards de cliente +│ ├── AppLayout.tsx # Shell do layout: AppHeader + AppSidebar + main/AppRouter +│ ├── AppModals.tsx # TaskModal + ConfirmModal + ClientTransitionOverlay agrupados +│ ├── TaskModal.tsx # Modal criar/editar demanda +│ ├── AppHeader.tsx # Header: logo, hamburger mobile, NotificationBell, theme toggle (desktop) +│ ├── AppSidebar.tsx # Sidebar de navegação; theme toggle no footer mobile +│ ├── ClientTransitionOverlay.tsx # Overlay animado exibido ao trocar de cliente +│ └── ui/ # Design system (Button, Input, Label, Badge) ├── views/ │ ├── home/ # HomeView — saudação, SearchLauncher, QuickAccess │ ├── dashboard/ # DashboardView → Calendar/Timeline @@ -22,16 +26,22 @@ src/ │ ├── admin/ # AdminView (apenas admins) │ ├── login/ # LoginView │ ├── onboarding/ # OnboardingView — usuário autenticado sem cliente associado -│ └── user/ # UserClientsView + useUserClients +│ ├── user/ # UserClientsView + useUserClients +│ └── profile/ # ProfileView — perfil do usuário + preferências ├── store/ │ ├── useUIStore.ts # Estado de UI: view ativa, modal aberto/fechado │ ├── useClientStore.ts # Cliente selecionado (persist localStorage) -│ ├── useDataStore.ts # Cache de tasks/members, fetchData, invalidate -│ └── appStore.ts # Re-export de compatibilidade: useAppStore = useDataStore +│ ├── useTaskStore.ts # Cache de tasks, fetchTasks, invalidate +│ └── useMemberStore.ts # Cache de members por cliente, fetchMembers, invalidate ├── hooks/ +│ ├── useAppOrchestrator.ts # Agrega toda a lógica de orquestração do App (cliente, views, notificações, task actions) │ ├── useSupabase.ts # Mutations CRUD (createTask, updateTask, deleteTask) │ ├── useHolidays.ts # Feriados -│ └── useFormState.ts # Estado do formulário TaskModal +│ ├── useFormState.ts # Estado do formulário TaskModal +│ ├── useAppTheme.ts # Dark mode: estado + sync com localStorage e +│ ├── useAppSidebar.ts # Sidebar desktop (persist) e mobile open/close +│ ├── useTaskActions.ts # Estado e handlers de create/update/delete de tasks +│ └── useClientTransition.ts # Fluxo animado de troca de cliente (overlay + stores) ├── contexts/ │ └── AuthContext.tsx # Sessão, member, clients, isAdmin, refreshProfile ├── lib/ @@ -50,12 +60,13 @@ src/ ``` AuthContext (AuthProvider) ↓ session, member, clients (filtrado por access_role), isAdmin, refreshProfile -App.tsx (inicialização, roteamento, clientMembers) - ├── useClientStore → selectedClientId (persist) - ├── useDataStore → tasks, members, fetchData, invalidate - ├── useUIStore → view, isTaskModalOpen - ├── useSupabase({ memberId, clientId, isAdmin }) → mutations CRUD - ├── clientMembers (useMemo) → membros filtrados pelo cliente ativo +App.tsx (gates de auth + composição) + └── useAppOrchestrator (toda a lógica de orquestração) + ├── useClientStore → selectedClientId (persist) + ├── useTaskStore → tasks, loading (lido pelas views diretamente) + ├── useMemberStore → members, loading (lido pelas views diretamente) + ├── useUIStore → view, isTaskModalOpen + ├── useSupabase({ memberId, clientId, isAdmin }) → mutations CRUD ├── view="home" → HomeView ├── view="clients" → UserClientsView ├── view="overview" → DashboardView (subview="overview") — métricas e resumo @@ -67,7 +78,10 @@ App.tsx (inicialização, roteamento, clientMembers) ├── view="tools" → ToolsView (grid de ferramentas) ├── view="tools-briefing-analyzer" → ToolsView com subview (BriefingAnalyzerView) ├── view="tools-import/export/integrations" → ToolsView com subview (em breve) - └── TaskModal → criar/editar (useFormState → cascata de fases) + ├── view="profile" → ProfileView — perfil + preferências + └── TaskModal → criar/editar (useFormState → cascata de fases) + ├── AppLayout → shell do layout (AppHeader + AppSidebar + main/AppRouter) + └── AppModals → TaskModal + ConfirmModal + ClientTransitionOverlay ``` ## Stores (`src/store/`) @@ -79,6 +93,7 @@ view: ViewType // 'home' | 'overview' | 'calendar' | 'timeline' | 'list' // | 'members' | 'reports' | 'admin' | 'clients' // | 'tools' | 'tools-briefing-analyzer' | 'tools-import' | 'tools-export' | 'tools-integrations' +// | 'profile' setView(view) isTaskModalOpen: boolean openTaskModal() / closeTaskModal() @@ -96,23 +111,33 @@ setClient(id) | `null` | Admin vê todos os clientes (sem filtro no fetch) | | `string` | Cliente específico selecionado | -### `useDataStore` -Cache de dados. Guard: `fetchData` retorna imediatamente se `clientId === undefined`. +### `useTaskStore` +Cache de tasks. Fetch lazy — nenhum dado é buscado no boot. Guard: retorna imediatamente se `clientId === undefined` ou se já há dados para o mesmo `cacheKey`. ```ts tasks: Task[] -members: Member[] loading: boolean error: string | null -fetchData(clientId, isAdmin) // guard: não fetcha se clientId === undefined -invalidate() // limpa cache (tasks, members, cachedClientId) +cacheKey: string | undefined // `${clientId ?? 'all'}:${isAdmin}` +fetchTasks(clientId, isAdmin) // idempotente; guard se loading=true ou cacheKey inalterado +invalidate() // reseta tasks, cacheKey, error e loading=false ``` -### `appStore.ts` -Re-export de compatibilidade: `useAppStore = useDataStore`. Consumers existentes (views, `useSupabase`) não precisam de alteração. +> **Importante:** `invalidate()` reseta `loading: false`. Sem isso, um fetch em andamento no momento da troca de cliente deixaria `loading` preso em `true`, bloqueando todos os fetches seguintes via o guard `if (state.loading) return`. + +### `useMemberStore` +Cache de members filtrados por cliente. Fetch lazy por view. +```ts +members: Member[] +loading: boolean +error: string | null +cachedClientId: string | null | undefined +fetchMembers(clientId) // idempotente; guard se loading=true ou clientId inalterado +invalidate() // reseta members, cachedClientId, error e loading=false +``` -## Fluxo de Autenticação — Retornos Condicionais em App.tsx +## Fluxo de Autenticação — Gates em App.tsx -Antes do layout principal (sidebar + header), `App.tsx` aplica retornos condicionais em ordem: +`App.tsx` delega toda a lógica ao `useAppOrchestrator` e aplica retornos condicionais em ordem antes de montar o layout: | Condição | View renderizada | |---|---| @@ -143,26 +168,57 @@ Relê o perfil do usuário atual (member + clients) sem reiniciar o ciclo de aut ``` 1. AuthContext resolve → loading=false, clients=[...] (já filtrado por access_role) -2. App.tsx useEffect: selectedClientId===undefined && hasClients - → setClient(clients[0].id) ← só corre depois de auth estar pronto +2. useAppOrchestrator useEffect: selectedClientId===undefined && hasClients + → setClient(clients[0].id) + fetchTasks/fetchMembers disparados imediatamente + ← fetch eager: dados começam a carregar antes da view montar 3. useClientStore persiste clientId no localStorage -4. useEffect separado: effectiveClientId !== undefined - → fetchData(clientId, isAdmin) ← nunca dispara com clientId=undefined -5. useDataStore popula tasks/members → views renderizam +4. Cada view ao montar lê tasks/members dos stores (já em loading ou com cache) ``` **Chave:** `App.tsx` usa `AuthContext.clients` diretamente (não `useUserClients`). `useUserClients` existe apenas em `UserClientsView` para `linkToClient`/`unlinkFromClient`. ## Troca de Cliente +A troca de cliente exibe um overlay de transição animado antes de efetivar a mudança: + ``` handleSelectClient(newId) - → invalidate() ← limpa tasks/members imediatamente (sem vazamento) - → setClient(newId) ← persiste no localStorage - → setView("home") - → useEffect dispara fetchData(newId) + → setTransitionClient({ id, name }) ← exibe ClientTransitionOverlay (~3.2s) + → após 650ms: setClient(newId) ← persiste no localStorage + invalidateTasks() ← reseta tasks + loading=false + invalidateMembers() ← reseta members + loading=false + setView("home") + ↓ onComplete (após fade-out do overlay) + → toast cinza "Trocado para " (sonner, 3s) + → setTransitionClient(null) ``` +> `invalidate` dos dois stores é chamado com `loading: false` para evitar o bug de loading eterno: se um fetch estava em andamento no momento da troca, o guard `if (state.loading) return` bloquearia todos os fetches seguintes sem o reset. + +> A store e o fetch são disparados **imediatamente** ao clicar, antes do overlay fechar. Quando a animação termina, os dados já chegaram ou estão a caminho — sem espera visível após a transição. + +### `ClientTransitionOverlay` (`src/components/ClientTransitionOverlay.tsx`) + +Overlay fullscreen (`z-[9999]`) exibido ao trocar de cliente. Duração total ~3.8s. + +**Camadas visuais (profundidade):** +- Gradiente radial fixo no fundo (`--muted`) simulando luminosidade de céu — não anima +- 8 nuvens SVG com posição, escala e opacidade fixas no `style`; cada nuvem anima **apenas `translateX`** via keyframe único gerado dinamicamente por valor de `dx` (`ct-cloud-8`, `ct-cloud-10`, etc.) — isso evita que o `scale` do style seja sobrescrito e mantém o drift orgânico e independente +- Linha de horizonte + pista SVG posicionadas em `bottom: 24-26%`, criando perspectiva de chão + +**Avião lateral (`PlaneSide`) + rastro:** +- SVG de vista lateral: fuselagem em perspectiva, asa principal, cauda vertical e horizontal, janelas +- Percorre da esquerda para a direita em arco de decolagem (`ct-takeoff`): táxi → inclinação suave → saída diagonal em ~25° +- Rastro de condensação duplo com gradiente linear atrás do avião + +**Conteúdo central (sem card):** +- Label "DESTINO" em uppercase + tracking largo +- Nome do cliente em `text-5xl` bold, fonte Syne +- Separador decorativo `✦` +- Mensagens rotativas com fade + `translateY(3px)` suave (350ms): `🛂 Verificando passaporte` → `🎫 Confirmando embarque` → `🛫 Decolando agora` → `✈️ Em rota de cruzeiro` + +Ao fechar, dispara `onComplete` → efetiva a troca no store e exibe toast de confirmação. + ## `ClientOption` (tipo em `src/contexts/AuthContext.ts`) ```ts @@ -177,22 +233,28 @@ interface ClientOption { - **admin** → todos os clientes - **user** → apenas os associados via `user_clients` -## Membros filtrados por cliente (`clientMembers` em `App.tsx`) - -`clientMembers` é derivado via `useMemo` a partir de `tasks` e `members`: -- Filtra `members` para mostrar apenas quem tem steps atribuídos nas tarefas do cliente ativo. -- Quando `effectiveClientId === null` (admin vê todos), retorna `members` completo. -- Passado para `MembersView` e `ReportsView`. O `TaskModal` recebe `members` completo. - ## Views lendo da store -`DashboardView`, `MembersView` e `ReportsView` leem `tasks` e `members` diretamente de `useAppStore()` (alias de `useDataStore`). Não recebem essas props via `App.tsx`. +`DashboardView`, `MembersView`, `ReportsView` e `TasksView` leem `tasks` e `members` diretamente de `useTaskStore()` e `useMemberStore()`. Não recebem essas props via `App.tsx`. As views ainda chamam `fetchTasks`/`fetchMembers` no mount, mas o `App.tsx` já dispara esses fetchs ao resolver o `effectiveClientId` — reduzindo o delay de loading nas views. ## Papéis de Acesso - **admin** (`access_role = 'admin'`): vê todos os clientes; pode selecionar cliente específico ou `null` (todos) - **user**: vê apenas os clientes associados via `user_clients` +### Permissões disponíveis (`AppPermission` em `src/lib/accessControl.ts`) + +| Permissão | admin | user | +|---|---|---| +| `view:home` | ✓ | ✓ | +| `view:clients` | ✓ | ✓ | +| `view:dashboard` | ✓ | ✓ | +| `view:members` | ✓ | ✓ | +| `view:reports` | ✓ | ✓ | +| `view:tools` | ✓ | ✓ | +| `view:profile` | ✓ | ✓ | +| `view:admin` | ✓ | — | + ### RLS — Visibilidade de Members A policy de `members` é `members_authenticated_read` (`USING (true)` para autenticados) — qualquer usuário autenticado vê todos os members. @@ -221,5 +283,5 @@ A segunda policy é necessária para que `fetchMembersFromDb` consiga buscar tod ## Decisões - Sem router — navegação via `useUIStore.view` (poucas views) -- State manager: Zustand (3 stores separadas por responsabilidade) +- State manager: Zustand (4 stores separadas por responsabilidade: UI, Client, Tasks, Members) - `any` intencional em dados do DB sem schema fixo em runtime diff --git a/docs/hooks/notifications.md b/docs/hooks/notifications.md index 33aee8f..3f2f7eb 100644 --- a/docs/hooks/notifications.md +++ b/docs/hooks/notifications.md @@ -100,6 +100,22 @@ Componente de sino com dropdown. Recebe notificações já carregadas via props - **Todas** — todas as notificações do usuário - **Cliente atual** — filtra por `client_id === selectedClientId` +--- + +# createNotificationForAll + +```ts +createNotificationForAll(clientIds: string[], title: string, message: string, type?: string): Promise +``` + +Insere um broadcast (`user_id = null`) para cada `clientId` em uma única operação `insert` em lote. Usado pelo `NotificationsPanel` na opção de destino **Todos**. + +--- + +## Formatação da mensagem + +O campo `message` suporta **Markdown** — negrito, itálico, quebras de linha etc. Renderizado via `react-markdown` com classes `prose-xs` do Tailwind. + ## Audiência Cada item exibe ícone e label indicando o destinatário: @@ -109,3 +125,27 @@ Cada item exibe ícone e label indicando o destinatário: ## Comportamento de clique `onNotificationClick` deve navegar dentro do **cliente atual** sem trocar de cliente. A rota é resolvida por `resolveNotificationRoute` em `src/lib/notifications.ts`. + +--- + +# Triggers automáticos (pg_cron) + +Migration: `supabase/migrations/20260420000002_notification_triggers.sql` + +Jobs agendados às **9h, 12h e 15h UTC**. Só executam se houve mudança de `status` em `audit_logs` nas últimas 3 horas (`has_recent_audit_activity`). + +## Tipos gerados automaticamente + +| `type` | Destinatário | Condição | +|---|---|---| +| `step_overdue` | assignee do step | `task_steps.end_date < hoje` e step não concluído | +| `task_stalled` | assignee da task | sem entrada em `audit_logs` há mais de `stalled_days_threshold` dias | +| `member_overloaded` | admins do cliente | membro com tasks `em andamento` ≥ `overload_threshold` | + +## Deduplicação + +Cada função verifica `NOT EXISTS` antes de inserir — nunca gera a mesma notificação duas vezes no mesmo dia para o mesmo par `(user_id, entity_id)`. + +## Preferências respeitadas + +As funções consultam `user_preferences` antes de inserir. Se o switch correspondente for `false`, o usuário não recebe aquele tipo. diff --git a/docs/views/admin.md b/docs/views/admin.md index 6ad8106..8de6a1e 100644 --- a/docs/views/admin.md +++ b/docs/views/admin.md @@ -15,7 +15,15 @@ src/views/admin/ └── components/ ├── ClientsPanel.tsx # Gestão de clientes ├── UsersPanel.tsx # Gestão de membros e vínculos - └── AuditLogsPanel.tsx # Log de auditoria com filtros + ├── AuditLogsPanel.tsx # Log de auditoria com filtros + └── NotificationsPanel/ # Envio e histórico de notificações manuais + ├── index.ts + ├── NotificationsPanel.tsx # Orquestrador + ├── NotificationForm.tsx # Formulário de envio (destino, título, mensagem) + ├── NotificationHistory.tsx # Histórico agrupado por período + ├── NotificationList.tsx # Lista de itens individuais + ├── useNotificationsPanel.ts # Estado, handlers e fetch do histórico + └── utils.ts # formatDate, groupNotifications, getIcon/Label src/store/ └── useAdminStore.ts # Estado e fetches admin (Zustand) @@ -107,6 +115,28 @@ No tab **Membros**, os filtros disponíveis são: **Todos / Ativos / Sem acesso - **Sem acesso** filtra members ativos sem `auth_user_id` - **Desativados** filtra members com `is_active === false` +## NotificationsPanel + +Aba de envio e histórico de notificações manuais. Estrutura modular em `components/NotificationsPanel/` — o componente raiz `NotificationsPanel.tsx` consome `useNotificationsPanel` e renderiza `NotificationForm` + `NotificationHistory`. + +### Destinos + +| Opção | Comportamento | +|---|---| +| **Usuário** | Notificação direta a um membro ativo (`user_id` preenchido, `manual`) | +| **Cliente** | Broadcast para todos os membros de um cliente (`user_id = null`, `client_id`) | +| **Todos** | Broadcast para todos os clientes cadastrados — chama `createNotificationForAll` | + +### Editor de mensagem + +O campo **Mensagem** tem tabs **Editar / Preview**: +- **Editar** — textarea mono; suporta Markdown (`**negrito**`, `_itálico_`, quebras de linha) +- **Preview** — renderiza via `react-markdown` antes do envio + +### Histórico + +Scroll independente, contido em `100vh`. Mensagens renderizadas como Markdown via `react-markdown`. + ## Impersonation Admin pode visualizar a aplicação como um cliente específico via `impersonatedClientId` (estado no `AuthContext`). Um banner amarelo aparece na AdminView quando ativo, com botão para sair da visão. diff --git a/docs/views/profile.md b/docs/views/profile.md new file mode 100644 index 0000000..0fea369 --- /dev/null +++ b/docs/views/profile.md @@ -0,0 +1,117 @@ +# ProfileView + +**Localização:** `src/views/profile/` + +View de perfil do usuário autenticado. Acessível a todos os papéis (`admin` e `user`). Não requer cliente selecionado. + +## Acesso + +- Sidebar → dropdown do usuário (rodapé) → "Meu Perfil" +- Notificação com rota `/profile` navega para esta view automaticamente + +## Estrutura + +``` +src/views/profile/ +├── ProfileView.tsx # View principal com sidebar interna +├── index.ts +├── components/ +│ ├── AccountSection.tsx # Dados da conta (nome, avatar, campos somente leitura) +│ └── PreferencesSection.tsx # Preferências (tema, idioma, notificações, ordem de clientes, view inicial) +└── hooks/ + └── useProfile.ts # Fetch/upsert de preferências, update de perfil +``` + +## Layout + +A view usa um layout de dois painéis: sidebar de navegação à esquerda (abas Conta / Preferências) e conteúdo à direita dentro de um card com borda. Largura máxima `max-w-4xl`. + +## Componentes + +### `AccountSection` + +Exibe e edita os dados do membro autenticado. + +**Campos editáveis:** `name`, `avatar_url` + +**Campos somente leitura:** `email`, `role`, `access_role`, `created_at` + +**Validação:** +- Nome: obrigatório, mínimo 2 caracteres +- Avatar URL: deve ser URL válida se preenchida + +O botão "Salvar alterações" fica desabilitado enquanto não há alterações (`dirty = false`). O componente usa `key={member.id}` no pai para recriar o estado local quando o membro muda — sem `useEffect` para reset de form. + +### `PreferencesSection` + +Exibe e edita as preferências do usuário. Persistência automática (sem botão salvar). Recebe `clients: Client[]` do store de admin para montar a lista de ordem de clientes. + +**Campos:** `theme` (`light | dark | system`), `language` (`pt-BR | en`), `default_view` (`home | calendar | timeline | list`), `client_order` (array de IDs arrastável), `notifications_enabled` + +**Ordem de clientes:** Drag-and-drop nativo (HTML5). Os IDs são salvos em `user_preferences.client_order`. Clientes não presentes no array aparecem no final na ordem de cadastro. + +## Hook `useProfile` + +```ts +useProfile(): { + member: Member | null + preferences: UserPreferences | null + loading: boolean + saving: boolean + savingPrefs: boolean + error: string | null + successMessage: string | null + updateProfile(data: ProfileFormData): Promise + updatePreferences(prefs: Partial): Promise +} +``` + +### Fluxo de carregamento + +1. Busca `user_preferences` onde `user_id = member.id` via `.maybeSingle()` +2. Se não encontrar → cria registro com defaults (`theme: 'system'`, `language: 'pt-BR'`, `notifications_enabled: true`) +3. Seta `loading = false` ao final + +### `updateProfile` + +- Atualiza `name` e opcionalmente `avatar_url` na tabela `members` +- Chama `refreshProfile()` do `AuthContext` para recarregar o estado global +- Exibe `successMessage` por 3 segundos + +### `updatePreferences` + +- Faz update na tabela `user_preferences` com `updated_at` atual +- Atualiza estado local imediatamente após sucesso + +## Modelo de Dados — `user_preferences` + +Migrations: `20260420000000_user_preferences.sql`, `20260420000001_notification_preferences.sql` + +| Coluna | Tipo | Default | Descrição | +|---|---|---|---| +| `id` | uuid PK | gen_random_uuid() | | +| `user_id` | uuid FK → members | — | unique, cascade delete | +| `theme` | text | `'system'` | `light \| dark \| system` | +| `language` | text | `'pt-BR'` | `pt-BR \| en` | +| `notifications_enabled` | boolean | `true` | Liga/desliga todas as notificações | +| `default_view` | text | `'home'` | `home \| calendar \| timeline \| list` | +| `client_order` | text[] | `{}` | IDs ordenados dos clientes | +| `notification_step_overdue` | boolean | `true` | Receber alerta de etapa em atraso | +| `notification_task_stalled` | boolean | `true` | Receber alerta de demanda parada | +| `notification_member_overloaded` | boolean | `true` | Receber alerta de membro sobrecarregado (relevante para admins) | +| `stalled_days_threshold` | integer 1–30 | `5` | Dias sem atividade para considerar demanda parada (configurável pelo manager) | +| `overload_threshold` | integer 1–20 | `3` | Nº de tasks simultâneas para disparar alerta de sobrecarga (configurável pelo manager) | +| `created_at` | timestamptz | now() | | +| `updated_at` | timestamptz | now() | | + +## Restrições de Acesso + +- Usuário só pode editar seus próprios dados (`members.id` derivado do `auth.uid()` via AuthContext) +- `access_role` e `is_active` nunca são editáveis pela view +- A permissão `view:profile` está disponível para `admin` e `user` em `src/lib/accessControl.ts` + +## Testes + +`src/views/profile/__tests__/useProfile.test.ts` — 5 testes do hook (carregamento, upsert de prefs, update de perfil, erro) + +`src/views/profile/__tests__/AccountSection.test.tsx` — 6 testes de componente (render, validação, disabled state, mensagens) diff --git a/package-lock.json b/package-lock.json index de0bc63..a12aec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,8 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", + "remark-breaks": "^4.0.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "vaul": "^1.1.2", @@ -363,9 +365,9 @@ } }, "node_modules/@chenglou/pretext": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@chenglou/pretext/-/pretext-0.0.3.tgz", - "integrity": "sha512-RQmqMqUAPRCyv4R3LlRi/ao6KbNWYclqLA+V1HS7sWgyUUbjn3JmmlfXZSY/BjM4rbmIaMSyIVisYocYGYftiQ==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@chenglou/pretext/-/pretext-0.0.5.tgz", + "integrity": "sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==", "license": "MIT", "optional": true }, @@ -513,7 +515,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -525,7 +526,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -536,7 +536,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -859,10 +858,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", - "dev": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "license": "MIT", "optional": true, "dependencies": { @@ -878,10 +876,10 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", - "dev": true, + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2622,13 +2620,12 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2639,13 +2636,12 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2656,13 +2652,12 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2673,13 +2668,12 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2690,13 +2684,12 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2707,13 +2700,12 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], - "dev": true, "libc": [ "glibc" ], @@ -2727,13 +2719,12 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -2747,13 +2738,12 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], - "dev": true, "libc": [ "glibc" ], @@ -2767,13 +2757,12 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], - "dev": true, "libc": [ "glibc" ], @@ -2787,13 +2776,12 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -2807,13 +2795,12 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -2827,13 +2814,12 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2844,30 +2830,46 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], "license": "MIT", "optional": true, "os": [ @@ -3355,7 +3357,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3381,6 +3382,15 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3392,9 +3402,26 @@ "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/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3402,6 +3429,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", @@ -3415,7 +3457,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3431,6 +3472,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -3735,6 +3782,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -3980,6 +4033,16 @@ "node": ">=12" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4011,9 +4074,9 @@ } }, "node_modules/boneyard-js": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/boneyard-js/-/boneyard-js-1.6.7.tgz", - "integrity": "sha512-ClzI7uF4cAaSBQOtwChjNQhIzOUIWofYBBN8Q0bY/b7+i3Ik6o/ykd0SZp4XaRMt7z9BDwE7Ir/U6IxDV9cGqw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/boneyard-js/-/boneyard-js-1.7.7.tgz", + "integrity": "sha512-/xWx9Okv73pfdFLHS8oQw4SkPBDJN1OxDSIb3bMY2LI/t50a5qHkJNLm6/GeFpaahTSVJQKG7p6kgisheDsGFg==", "license": "MIT", "dependencies": { "playwright": "^1.58.2" @@ -4022,19 +4085,24 @@ "boneyard-js": "bin/cli.js" }, "optionalDependencies": { - "@chenglou/pretext": "^0.0.3" + "@chenglou/pretext": "^0.0.5" }, "peerDependencies": { "@angular/core": ">=14", + "preact": ">=10", "react": ">=18", "react-native": ">=0.71", "svelte": ">=5.29", + "vite": ">=5", "vue": ">=3" }, "peerDependenciesMeta": { "@angular/core": { "optional": true }, + "preact": { + "optional": true + }, "react": { "optional": true }, @@ -4044,6 +4112,9 @@ "svelte": { "optional": true }, + "vite": { + "optional": true + }, "vue": { "optional": true } @@ -4125,6 +4196,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -4152,6 +4233,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4193,6 +4314,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4247,7 +4378,6 @@ "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": { @@ -4268,7 +4398,6 @@ "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" @@ -4289,6 +4418,19 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4300,7 +4442,6 @@ "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" @@ -4310,7 +4451,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4322,6 +4463,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -4568,6 +4722,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4598,6 +4762,12 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4623,7 +4793,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4692,7 +4862,6 @@ "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, @@ -4765,6 +4934,46 @@ "node": ">=8" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -4795,6 +5004,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -4851,6 +5070,46 @@ "node": ">=8" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4874,6 +5133,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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", @@ -4892,7 +5173,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -5044,7 +5325,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -5077,7 +5358,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5098,7 +5378,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5119,7 +5398,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5140,7 +5418,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5161,7 +5438,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5182,7 +5458,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "glibc" ], @@ -5206,7 +5481,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -5230,7 +5504,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -5254,7 +5527,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -5278,7 +5550,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5299,7 +5570,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5336,6 +5606,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5376,48 +5656,684 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "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, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "devOptional": true, "funding": [ { "type": "github", @@ -5520,6 +6436,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -5564,14 +6505,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -5628,7 +6569,7 @@ "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -5693,6 +6634,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5873,6 +6824,33 @@ "license": "MIT", "peer": true }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -5956,6 +6934,54 @@ "node": ">=8" } }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5977,14 +7003,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", - "dev": true, + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "devOptional": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5993,45 +7019,28 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" - } - }, - "node_modules/rolldown/node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", - "dev": true, + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "devOptional": true, "license": "MIT" }, "node_modules/saxes": { @@ -6107,12 +7116,22 @@ "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, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6127,6 +7146,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -6153,6 +7186,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6225,7 +7276,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -6294,6 +7345,26 @@ "node": ">=20" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6380,6 +7451,93 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6486,17 +7644,45 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", - "dev": true, + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "devOptional": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -6514,7 +7700,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -6854,6 +8040,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 71c49b1..5a8872f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", + "remark-breaks": "^4.0.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "vaul": "^1.1.2", diff --git a/src/App.tsx b/src/App.tsx index e32cded..4f7c77f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,164 +1,14 @@ -import { useState, useEffect, useCallback } from "react"; -import { cn } from "@/lib/utils"; -import { useClientStore } from "@/store/useClientStore"; -import { useUIStore } from "@/store/useUIStore"; -import { useMemberStore } from "@/store/useMemberStore"; -import TaskModal from "./components/TaskModal"; -import { AppHeader } from "./components/AppHeader"; -import { AppSidebar } from "./components/AppSidebar"; -import { DashboardView } from "./views/dashboard"; -import MembersView from "./views/MembersView"; -import ReportsView from "./views/reports"; -import { AdminView } from "./views/admin"; -import { RequireAdmin } from "./components/RequireAdmin"; -import { LoginView } from "./views/login"; -import { OnboardingView } from "./views/onboarding"; -import { UserClientsView } from "./views/user/UserClientsView"; -import { NoClientView } from "./components/NoClientView"; -import { HomeView } from "./views/home"; -import { ToolsView } from "./views/tools"; -import TasksView from "./views/tasks"; -import { useAuthContext } from "@/contexts/AuthContext"; -import { useSupabase } from "./hooks/useSupabase"; -import { useHolidays } from "./hooks/useHolidays"; -import { useNotifications } from "./hooks/useNotifications"; -import { resolveNotificationRoute } from "./lib/notifications"; -import { Toaster, toast } from "sonner"; -import type { Task } from "./lib/steps"; -import type { Notification } from "./types/notification"; -import type { ViewType } from "@/store/useUIStore"; -import { canAccessView, resolveAccessRole } from "@/lib/accessControl"; -import { ConfirmModal, TooltipProvider } from "@/components/ui"; +import { Toaster } from "sonner"; +import { useAppOrchestrator } from "@/hooks/useAppOrchestrator"; +import { AppLayout } from "@/components/AppLayout"; +import { AppModals } from "@/components/AppModals"; +import { LoginView } from "@/views/login"; +import { OnboardingView } from "@/views/onboarding"; export default function App() { - const { session, user, signIn, signOut, authError, loading: authLoading, isAdmin, member, clients, refreshProfile } = useAuthContext(); - const [isLoggingOut, setIsLoggingOut] = useState(false); - const accessRole = resolveAccessRole(member); + const app = useAppOrchestrator(); - const hasClients = clients.length > 0; - - const { selectedClientId, setClient } = useClientStore(); - const { members } = useMemberStore(); - - const effectiveClientId = selectedClientId === undefined - ? (hasClients ? clients[0].id : null) - : (isAdmin ? selectedClientId : (selectedClientId ?? (hasClients ? clients[0].id : null))); - - const handleSignOut = () => { - setIsLoggingOut(true); - signOut(); - }; - - useEffect(() => { - if (authLoading || !session) return; - - if (selectedClientId === undefined && hasClients) { - setClient(clients[0].id); - } else if (!hasClients) { - setClient(undefined); - } else if (typeof selectedClientId === 'string' && !clients.find(c => c.id === selectedClientId)) { - setClient(clients[0]?.id ?? undefined); - } - }, [authLoading, session, hasClients, clients, selectedClientId, setClient]); - - const { createTask, updateTask, deleteTask } = useSupabase({ - memberId: member?.id, - clientId: effectiveClientId, - isAdmin, - }); - - const { holidays } = useHolidays(); - - const allClientIds = clients.map((c) => c.id) - - const { - notifications, - unreadCount, - markAsRead: markNotificationAsRead, - markAllAsRead: markAllNotificationsAsRead, - reload: reloadNotifications, - } = useNotifications(member?.id, allClientIds) - - const view = useUIStore((s) => s.view) - const setView = useUIStore((s) => s.setView) - - const handleNotificationClick = useCallback((notification: Notification) => { - // Navega dentro do cliente atual — não troca de cliente - const route = resolveNotificationRoute(notification) - if (!route) return - - if (route.startsWith('/dashboard')) { - setView('calendar') - } else if (route === '/profile') { - // TODO: open profile - } else if (route === '/clients') { - setView('clients') - } - }, [setView]) - - const [darkMode, setDarkMode] = useState(() => { - return ( - localStorage.getItem("theme") === "dark" || - (!localStorage.getItem("theme") && - window.matchMedia("(prefers-color-scheme: dark)").matches) - ) - }) - - useEffect(() => { - document.documentElement.classList.toggle("dark", darkMode) - localStorage.setItem("theme", darkMode ? "dark" : "light") - }, [darkMode]) - - const [sidebarOpen, setSidebarOpen] = useState(() => { - return localStorage.getItem("sidebarOpen") !== "false" - }) - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) - - const handleToggleSidebar = () => { - setSidebarOpen((prev) => { - localStorage.setItem("sidebarOpen", String(!prev)) - return !prev - }) - } - - const isModalOpen = useUIStore((s) => s.isTaskModalOpen) - const openTaskModal = useUIStore((s) => s.openTaskModal) - const closeTaskModal = useUIStore((s) => s.closeTaskModal) - const [editingTask, setEditingTask] = useState(null) - const [pendingDeleteTaskId, setPendingDeleteTaskId] = useState(null) - const [closeTaskModalOnDelete, setCloseTaskModalOnDelete] = useState(false) - - const handleSelectClient = useCallback((clientId: string | null | undefined) => { - setClient(clientId); - setView("home"); - }, [setClient, setView]); - - const handleViewChange = useCallback((newView: ViewType) => { - setMobileSidebarOpen(false); - const hasClientSelected = effectiveClientId !== null; - const canAccess = canAccessView(newView, accessRole, hasClientSelected); - - if (!canAccess) { - if (newView !== "clients") { - toast.error("Selecione um cliente para acessar esta funcionalidade."); - } - setView("clients"); - } else { - setView(newView); - } - }, [effectiveClientId, accessRole, setView]); - - useEffect(() => { - if (!authLoading && session) { - const hasClientSelected = effectiveClientId !== null; - const canAccess = canAccessView(view, accessRole, hasClientSelected); - if (!canAccess) { - setView("clients"); - } - } - }, [authLoading, session, effectiveClientId, accessRole, view, setView]); - - if (authLoading) { + if (app.auth.loading) { return (
@@ -166,185 +16,83 @@ export default function App() { ); } - if (!session) { - return ; + if (!app.auth.session) { + return ; } - if (!hasClients) { + if (!app.hasClients) { return ( - ) + ); } - const requestDeleteTask = (id: string, closeModalAfterDelete = false) => { - setPendingDeleteTaskId(id); - setCloseTaskModalOnDelete(closeModalAfterDelete); - }; - - const handleConfirmDeleteTask = async () => { - if (!pendingDeleteTaskId) return; - - const taskId = pendingDeleteTaskId; - const shouldCloseModal = closeTaskModalOnDelete; - - setPendingDeleteTaskId(null); - setCloseTaskModalOnDelete(false); - - const ok = await deleteTask(taskId); - if (!ok) { - toast.error("Erro ao eliminar tarefa"); - return; - } - - if (shouldCloseModal) { - closeTaskModal(); - } - }; - - const showNoClientView = !hasClients && view !== "clients"; - const showSelectClient = hasClients && !effectiveClientId && view !== "clients"; - const selectedClient = effectiveClientId ? clients.find(c => c.id === effectiveClientId) : null; - return ( -
+ <> - setMobileSidebarOpen(true)} - notifications={notifications} - unreadCount={unreadCount} - onMarkNotificationAsRead={markNotificationAsRead} - onMarkAllNotificationsAsRead={markAllNotificationsAsRead} - onNotificationClick={handleNotificationClick} - reloadNotifications={reloadNotifications} - selectedClientId={effectiveClientId ?? null} - darkMode={darkMode} - onToggleDark={() => setDarkMode((d) => !d)} - /> - -
- setMobileSidebarOpen(false)} - view={view} - onViewChange={handleViewChange} - hasClient={hasClients} - role={accessRole} - darkMode={darkMode} - onToggleDark={() => setDarkMode((d) => !d)} - userEmail={user?.email} - userAvatarUrl={member?.avatar_url} - onSignOut={handleSignOut} - selectedClient={selectedClient ?? null} - availableClients={clients} - onSelectClient={handleSelectClient} - isAdmin={isAdmin} - /> -
- - {showNoClientView ? ( - setView("clients")} /> - ) : showSelectClient ? ( - setView("clients")} /> - ) : view === "home" ? ( - - ) : view === "admin" ? ( - - - - ) : view === "clients" ? ( - - ) : view === "calendar" || view === "timeline" || view === "list" ? ( - { setEditingTask(task); openTaskModal(); }} - onDelete={(id: string) => requestDeleteTask(id)} - onUpdateTask={updateTask} - onOpenNew={() => { setEditingTask(null); openTaskModal(); }} - onExport={() => window.print()} - holidays={holidays} - /> - ) : view === "demandas" ? ( - { setEditingTask(task); openTaskModal(); }} - onOpenNew={() => { setEditingTask(null); openTaskModal(); }} - /> - ) : view === "members" ? ( - - ) : view === "tools" || view === "tools-briefing-analyzer" || view === "tools-import" || view === "tools-export" || view === "tools-integrations" ? ( - - ) : view === "reports" || view === "reports-fluxo" || view === "reports-timeline" || view === "reports-membros" || view === "reports-alertas" ? ( - - ) : ( - - )} - -
-
- - {isModalOpen && ( - { - requestDeleteTask(id, true); - }} - onSave={async (taskData) => { - let ok: boolean; - if (editingTask) { - ok = await updateTask({ - ...editingTask, - ...taskData, - clientId: effectiveClientId ?? editingTask.clientId, - }); - } else { - ok = await createTask({ - title: taskData.title, - clickupLink: taskData.clickupLink, - clientId: effectiveClientId ?? undefined, - status: taskData.status, - steps: taskData.steps, - }); - } - if (!ok) { toast.error("Erro ao guardar tarefa"); return; } - closeTaskModal(); - }} - holidays={holidays} - /> - )} + - {pendingDeleteTaskId && ( - { - setPendingDeleteTaskId(null); - setCloseTaskModalOnDelete(false); - }} - /> - )} -
- ); + app.taskActions.requestDeleteTask(id, true), + }} + deleteModal={{ + pendingId: app.taskActions.pendingDeleteTaskId, + onConfirm: app.taskActions.confirmDeleteTask, + onCancel: app.taskActions.cancelDeleteTask, + }} + transition={{ + target: app.transitionTarget, + onComplete: app.onTransitionComplete, + }} + members={app.members} + holidays={app.holidays} + /> + + ); } diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx new file mode 100644 index 0000000..e533026 --- /dev/null +++ b/src/components/AppLayout.tsx @@ -0,0 +1,128 @@ +import { cn } from "@/lib/utils"; +import { AppHeader } from "@/components/AppHeader"; +import { AppSidebar } from "@/components/AppSidebar"; +import { AppRouter } from "@/components/AppRouter"; +import { TooltipProvider } from "@/components/ui"; +import type { ClientOption } from "@/contexts/AuthContext"; +import type { Holiday } from "@/utils/holidayUtils"; +import type { Task } from "@/types/task"; +import type { ViewType } from "@/store/useUIStore"; +import type { AccessRole } from "@/lib/accessControl"; +import type { Notification } from "@/types/notification"; + +interface AppLayoutProps { + // header + darkMode: boolean; + onToggleDark: () => void; + notifications: Notification[]; + unreadCount: number; + selectedClientId: string | null; + onToggleMobileSidebar: () => void; + onMarkNotificationAsRead: (id: string) => Promise; + onMarkAllNotificationsAsRead: () => Promise; + onNotificationClick: (notification: Notification) => void; + onReloadNotifications: () => Promise; + + // sidebar + sidebarOpen: boolean; + mobileSidebarOpen: boolean; + onToggleSidebar: () => void; + onCloseMobileSidebar: () => void; + view: ViewType; + onViewChange: (view: ViewType) => void; + hasClients: boolean; + role: AccessRole | null; + userEmail?: string; + userAvatarUrl?: string | null; + onSignOut: () => void; + selectedClient: ClientOption | null; + availableClients: ClientOption[]; + onSelectClient: (clientId: string | null | undefined) => void; + isAdmin: boolean; + + // main / router + effectiveClientId: string | null | undefined; + userName: string; + holidays: Holiday[]; + onEditTask: (task: Task) => void; + onOpenNewTask: () => void; + onDeleteTask: (id: string) => void; + onUpdateTask: (task: Task) => Promise; +} + +export function AppLayout({ + darkMode, onToggleDark, + notifications, unreadCount, selectedClientId, + onToggleMobileSidebar, onMarkNotificationAsRead, onMarkAllNotificationsAsRead, + onNotificationClick, onReloadNotifications, + sidebarOpen, mobileSidebarOpen, onToggleSidebar, onCloseMobileSidebar, + view, onViewChange, hasClients, role, + userEmail, userAvatarUrl, onSignOut, + selectedClient, availableClients, onSelectClient, isAdmin, + effectiveClientId, userName, holidays, + onEditTask, onOpenNewTask, onDeleteTask, onUpdateTask, +}: AppLayoutProps) { + return ( +
+ + +
+ + +
+ + + +
+
+
+ ); +} diff --git a/src/components/AppModals.tsx b/src/components/AppModals.tsx new file mode 100644 index 0000000..2b435d0 --- /dev/null +++ b/src/components/AppModals.tsx @@ -0,0 +1,68 @@ +import { ConfirmModal } from "@/components/ui"; +import TaskModal from "@/components/TaskModal"; +import { ClientTransitionOverlay } from "@/components/ClientTransitionOverlay"; +import type { Task } from "@/types/task"; +import type { Member } from "@/types/member"; +import type { Holiday } from "@/utils/holidayUtils"; + +interface TaskModalState { + isOpen: boolean; + editingTask: Task | null; + onClose: () => void; + onSave: (taskData: Omit) => Promise; + onDelete: (id: string) => void; +} + +interface DeleteModalState { + pendingId: string | null; + onConfirm: () => Promise; + onCancel: () => void; +} + +interface TransitionState { + target: { id: string | null | undefined; name: string } | null; + onComplete: () => void; +} + +interface AppModalsProps { + taskModal: TaskModalState; + deleteModal: DeleteModalState; + transition: TransitionState; + members: Member[]; + holidays: Holiday[]; +} + +export function AppModals({ taskModal, deleteModal, transition, members, holidays }: AppModalsProps) { + return ( + <> + {taskModal.isOpen && ( + taskModal.onDelete(id)} + /> + )} + + {transition.target && ( + + )} + + {deleteModal.pendingId && ( + + )} + + ); +} diff --git a/src/components/AppRouter.tsx b/src/components/AppRouter.tsx new file mode 100644 index 0000000..6f91d24 --- /dev/null +++ b/src/components/AppRouter.tsx @@ -0,0 +1,138 @@ +import { DashboardView } from "@/views/dashboard"; +import MembersView from "@/views/MembersView"; +import ReportsView from "@/views/reports"; +import { AdminView } from "@/views/admin"; +import { RequireAdmin } from "@/components/RequireAdmin"; +import { UserClientsView } from "@/views/user/UserClientsView"; +import { NoClientView } from "@/components/NoClientView"; +import { HomeView } from "@/views/home"; +import { ToolsView } from "@/views/tools"; +import TasksView from "@/views/tasks"; +import { ProfileView } from "@/views/profile"; +import type { ViewType } from "@/store/useUIStore"; +import type { Task } from "@/lib/steps"; +import type { Holiday } from "@/utils/holidayUtils"; +import type { ClientOption } from "@/contexts/AuthContext"; +import type { ReportsSubview } from "@/views/reports/ReportsView"; + +type DashboardSubview = "calendar" | "timeline" | "list"; +type ToolsSubview = Extract; + +const DASHBOARD_VIEWS = new Set(["calendar", "timeline", "list"]); +const TOOLS_VIEWS = new Set(["tools", "tools-briefing-analyzer", "tools-import", "tools-export", "tools-integrations"]); +const REPORTS_VIEWS = new Set(["reports", "reports-fluxo", "reports-timeline", "reports-membros", "reports-alertas"]); + +const REPORTS_SUBVIEW_MAP: Partial> = { + "reports": "geral", + "reports-fluxo": "fluxo", + "reports-timeline": "timeline", + "reports-membros": "membros", + "reports-alertas": "alertas", +}; + +interface AppRouterProps { + view: ViewType; + hasClients: boolean; + effectiveClientId: string | null | undefined; + selectedClient: ClientOption | null; + userName: string; + userEmail: string | undefined; + holidays: Holiday[]; + onViewChange: (view: ViewType) => void; + onEditTask: (task: Task) => void; + onOpenNewTask: () => void; + onDeleteTask: (id: string) => void; + onUpdateTask: (task: Task) => Promise; +} + +/** + * Maps the active `view` to the corresponding view component. + * Handles guard conditions (no client selected) before delegating to the view. + */ +export function AppRouter({ + view, + hasClients, + effectiveClientId, + selectedClient, + userName, + userEmail, + holidays, + onViewChange, + onEditTask, + onOpenNewTask, + onDeleteTask, + onUpdateTask, +}: AppRouterProps) { + const goToClients = () => onViewChange("clients"); + const displayName = userName || userEmail || ""; + + if (!hasClients && view !== "clients") { + return ; + } + + if (hasClients && !effectiveClientId && view !== "clients") { + return ; + } + + if (view === "home") { + return ( + + ); + } + + if (view === "admin") { + return ; + } + + if (view === "clients") { + return ; + } + + if (DASHBOARD_VIEWS.has(view)) { + return ( + window.print()} + holidays={holidays} + /> + ); + } + + if (view === "demandas") { + return ; + } + + if (view === "profile") { + return ; + } + + if (view === "members") { + return ; + } + + if (TOOLS_VIEWS.has(view)) { + return ; + } + + if (REPORTS_VIEWS.has(view)) { + return ; + } + + return ( + + ); +} diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 5fc9556..cd3c1aa 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -1,6 +1,5 @@ import { useState } from "react" import { - CalendarDays, Users, BarChart2, ChevronLeft, @@ -14,6 +13,7 @@ import { LogOut, Wrench, ListChecks, + UserCircle, } from "lucide-react" import { cn } from "@/lib/utils" @@ -70,16 +70,14 @@ interface AppSidebarProps { const NAV_ITEMS: NavItem[] = [ { label: "Início", Icon: Home, view: "home" }, - { label: "Demandas", Icon: ListChecks, view: "demandas", requiresClient: true }, - { - label: "Calendário", - Icon: CalendarDays, + label: "Demandas", + Icon: ListChecks, requiresClient: true, children: [ + { view: "demandas", label: "Todas Demandas" }, { view: "calendar", label: "Calendário" }, { view: "timeline", label: "Linha do Tempo" }, - { view: "list", label: "Lista" }, ], }, @@ -301,11 +299,13 @@ export function AppSidebar({ - {isAdmin && ( + {/* {isAdmin && ( <> - )} + )} */} + + {availableClients.map((client) => ( {userEmail} + onViewChange("profile")}> + + Meu Perfil + + Sair @@ -386,6 +391,11 @@ export function AppSidebar({ {userEmail} + onViewChange("profile")}> + + Meu Perfil + + Sair diff --git a/src/components/ClientTransitionOverlay.tsx b/src/components/ClientTransitionOverlay.tsx new file mode 100644 index 0000000..73fb5c6 --- /dev/null +++ b/src/components/ClientTransitionOverlay.tsx @@ -0,0 +1,438 @@ +import { useEffect, useRef, useState } from "react"; + +// --- TIPO DAS PROPS --- +interface ClientTransitionOverlayProps { + clientName: string; + onComplete: () => void; +} + +const MESSAGES = [ + "🛂 Verificando credenciais", + "🎫 Confirmando embarque", + "🛫 Autorização de torre", + "✈️ Em rota de cruzeiro", +]; + +const TOTAL_MS = 3200; // Transição mais rápida (reduzida de 4600ms para 3200ms) +const MSG_INTERVAL = TOTAL_MS / MESSAGES.length; + +// Nuvens com posições e velocidades otimizadas para fluir da direita para a esquerda +const CLOUDS = [ + { top: 5, size: 200, opacity: 0.45, duration: 45, delay: -10 }, + { top: 15, size: 300, opacity: 0.35, duration: 35, delay: -5 }, + { top: 30, size: 250, opacity: 0.25, duration: 50, delay: -20 }, + { top: 45, size: 400, opacity: 0.3, duration: 40, delay: -15 }, + { top: 10, size: 150, opacity: 0.5, duration: 25, delay: -2 }, + { top: 60, size: 350, opacity: 0.2, duration: 60, delay: -30 }, +]; + +// --- COMPONENTES AUXILIARES --- + +function NoiseOverlay() { + return ( +
+ + + + + + +
+ ); +} + +function Cloud({ top, size, opacity, duration, delay }: (typeof CLOUDS)[0]) { + return ( +
+ ); +} + +function PlaneIcon() { + return ( + + + {/* Fuselagem */} + + {/* Asa Principal */} + + + {/* Asa Traseira */} + + + {/* Cockpit Window */} + + + + ); +} + +const BARCODE_BARS = Array.from({ length: 32 }).map((_, i) => { + // deterministic pseudo-random based on index to avoid impure render + const seed = Math.sin(i * 9301 + 49297) * 233280; + const r1 = seed - Math.floor(seed); + const seed2 = Math.sin(i * 4321 + 12345) * 233280; + const r2 = seed2 - Math.floor(seed2); + return { + width: r1 > 0.6 ? "3px" : "1.5px", + height: `${40 + r2 * 60}%`, + }; +}); + +function Barcode() { + const bars = BARCODE_BARS; + + return ( +
+ {bars.map((bar, i) => ( +
+ ))} +
+ ); +} + +// --- COMPONENTE PRINCIPAL --- + +export function ClientTransitionOverlay({ + clientName, + onComplete, +}: ClientTransitionOverlayProps) { + const [messageIndex, setMessageIndex] = useState(0); + const [msgOut, setMsgOut] = useState(false); + const [visible, setVisible] = useState(false); + + const onCompleteRef = useRef(onComplete); + useEffect(() => { + onCompleteRef.current = onComplete; + }); + + useEffect(() => { + // Pequeno delay para garantir que a animação de entrada seja vista + const showTimer = setTimeout(() => setVisible(true), 50); + + let current = 0; + const msgInterval = setInterval(() => { + setMsgOut(true); + setTimeout(() => { + current = (current + 1) % MESSAGES.length; + setMessageIndex(current); + setMsgOut(false); + }, 300); // Fade out do texto mais rápido + }, MSG_INTERVAL); + + const fadeOutTimer = setTimeout(() => { + setVisible(false); + setTimeout(() => onCompleteRef.current(), 600); // Fade global mais ágil + }, TOTAL_MS); + + return () => { + clearTimeout(showTimer); + clearInterval(msgInterval); + clearTimeout(fadeOutTimer); + }; + }, []); + + return ( +
+ + + {/* CÉU GRADIENTE SUAVE - Adaptável ao tema via pseudo-elemento / var CSS local */} +
+ + {/* NUVENS ATMOSFÉRICAS */} + {CLOUDS.map((c, i) => ( + + ))} + + {/* CHÃO / PISTA - Adaptável */} +
+ {/* Linha Central da Pista Animada */} +
+
+ {Array.from({ length: 30 }).map((_, i) => ( +
+ ))} +
+
+
+ + {/* AVIÃO ANIMADO - Movimento contínuo e mais rápido */} +
+ +
+ + {/* TICKET DE EMBARQUE PREMIUM - Tema Adaptável */} +
+ {/* Topo do Ticket */} +
+
+ {/* Símbolo decorativo */} + + + + +
+ +
+ + Boarding Pass + + + Priority + +
+ +

+ Passenger +

+

+ {clientName} +

+ +
+
+

+ Flight +

+

+ NX-012 +

+
+
+

+ Gate +

+

+ A-42 +

+
+
+
+ + {/* Linha Picotada */} +
+
+
+
+
+ + {/* Canhoto Inferior */} +
+

+ {MESSAGES[messageIndex]} +

+ +
+ +

+ 0010110 0110001 +

+
+
+
+ + {/* --- ESTILOS INJETADOS (KEYFRAMES) --- */} + +
+ ); +} + +// === COMPONENTE APP DE DEMONSTRAÇÃO (Apenas para o Preview) === +export default function App() { + const [showOverlay, setShowOverlay] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(false); + + // Injeta a classe dark no HTML para o Tailwind aplicar os estilos adaptáveis + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [isDarkMode]); + + return ( +
+
+ +
+ +
+

Dashboard Principal

+

+ Clique no botão abaixo para ver a transição 100% fluida e adaptável. +

+ +
+ + {showOverlay && ( + setShowOverlay(false)} + /> + )} +
+ ); +} diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx index eaeba2d..8a6a1a0 100644 --- a/src/components/NotificationBell.tsx +++ b/src/components/NotificationBell.tsx @@ -1,5 +1,7 @@ import { useState, useRef, useEffect, useMemo, useCallback } from 'react' -import { Bell, CheckCheck, User, Users } from 'lucide-react' +import ReactMarkdown from 'react-markdown' +import remarkBreaks from 'remark-breaks' +import { Bell, CheckCheck, UserCheck, UserX, ShieldCheck, ShieldOff, Megaphone, MessageSquare, ClipboardList } from 'lucide-react' import { DropdownMenu, DropdownMenuTrigger, @@ -74,6 +76,19 @@ function groupNotificationsByDate(notifications: Notification[]): Map + case 'step_unassigned': return + case 'role_changed': return + case 'client_access_granted': return + case 'client_access_revoked': return + case 'admin_broadcast': return + case 'manual': return + default: return + } +} + function playNotificationSound() { const audio = new Audio('/notification.mp3') audio.volume = 0.3 @@ -102,17 +117,14 @@ function NotificationItem({ e.preventDefault() handleClick() }} - className={`group flex items-start gap-3 px-3 py-3 cursor-pointer rounded-md mx-1 transition-colors ${ - notification.read ? 'hover:bg-muted/40' : 'bg-primary/5 hover:bg-primary/10' - }`} + className={`group flex items-start gap-3 px-3 py-3 cursor-pointer rounded-md mx-1 border border-black/5 dark:border-white/10 transition-colors ${ + notification.read + ? 'bg-background hover:bg-muted/40' + : 'bg-muted/30 hover:bg-muted/50' +}`} > - {/* Ícone de audiência */} -
- {isPersonal ? : } +
+ {getNotificationTypeIcon(notification.type)}
@@ -125,9 +137,9 @@ function NotificationItem({
-

- {notification.message} -

+
+ {notification.message} +
- + {/* Header */}
@@ -246,7 +258,7 @@ export function NotificationBell({ e.preventDefault() onMarkAllAsRead() }} - className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors" + className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors cursor-pointer" > Marcar como lidas @@ -294,6 +306,7 @@ export function NotificationBell({
{GROUP_LABELS[groupKey]}
+
{items.map((notification) => ( ))} +
) })} diff --git a/src/hooks/useAppOrchestrator.ts b/src/hooks/useAppOrchestrator.ts new file mode 100644 index 0000000..ed26256 --- /dev/null +++ b/src/hooks/useAppOrchestrator.ts @@ -0,0 +1,163 @@ +import { useEffect, useCallback } from "react"; +import { toast } from "sonner"; +import { useClientStore } from "@/store/useClientStore"; +import { useUIStore } from "@/store/useUIStore"; +import { useMemberStore } from "@/store/useMemberStore"; +import { useTaskStore } from "@/store/useTaskStore"; +import { useAuthContext } from "@/contexts/AuthContext"; +import { useSupabase } from "@/hooks/useSupabase"; +import { useHolidays } from "@/hooks/useHolidays"; +import { useNotifications } from "@/hooks/useNotifications"; +import { useAppTheme } from "@/hooks/useAppTheme"; +import { useAppSidebar } from "@/hooks/useAppSidebar"; +import { useTaskActions } from "@/hooks/useTaskActions"; +import { useClientTransition } from "@/hooks/useClientTransition"; +import { resolveNotificationRoute } from "@/lib/notifications"; +import { canAccessView, resolveAccessRole } from "@/lib/accessControl"; +import type { Notification } from "@/types/notification"; +import type { ViewType } from "@/store/useUIStore"; + +export function useAppOrchestrator() { + const auth = useAuthContext(); + const { darkMode, toggleDark } = useAppTheme(); + const sidebar = useAppSidebar(); + + const accessRole = resolveAccessRole(auth.member); + const hasClients = auth.clients.length > 0; + + const { selectedClientId, setClient } = useClientStore(); + const { members, fetchMembers } = useMemberStore(); + const { fetchTasks } = useTaskStore(); + + const effectiveClientId = + selectedClientId === undefined + ? hasClients ? auth.clients[0].id : null + : auth.isAdmin + ? selectedClientId + : selectedClientId ?? (hasClients ? auth.clients[0].id : null); + + useEffect(() => { + if (auth.loading || !auth.session) return; + + let resolvedClientId: string | null | undefined = selectedClientId; + + if (selectedClientId === undefined && hasClients) { + setClient(auth.clients[0].id); + resolvedClientId = auth.clients[0].id; + } else if (!hasClients) { + setClient(undefined); + resolvedClientId = null; + } else if ( + typeof selectedClientId === "string" && + !auth.clients.find((c) => c.id === selectedClientId) + ) { + setClient(auth.clients[0]?.id ?? undefined); + resolvedClientId = auth.clients[0]?.id ?? null; + } + + if (resolvedClientId !== undefined) { + fetchTasks(resolvedClientId, auth.isAdmin); + fetchMembers(resolvedClientId); + } + }, [auth.loading, auth.session, hasClients, auth.clients, selectedClientId, setClient, fetchTasks, fetchMembers, auth.isAdmin]); + + const { createTask, updateTask, deleteTask } = useSupabase({ + memberId: auth.member?.id, + clientId: effectiveClientId, + isAdmin: auth.isAdmin, + }); + + const { holidays } = useHolidays(); + + const allClientIds = auth.clients.map((c) => c.id); + const notifications = useNotifications(auth.member?.id, allClientIds); + + const view = useUIStore((s) => s.view); + const setView = useUIStore((s) => s.setView); + const isModalOpen = useUIStore((s) => s.isTaskModalOpen); + const closeTaskModal = useUIStore((s) => s.closeTaskModal); + + const { transitionTarget, selectClient, onTransitionComplete } = useClientTransition( + auth.clients, + effectiveClientId + ); + + const taskActions = useTaskActions({ createTask, updateTask, deleteTask, effectiveClientId }); + + const handleNotificationClick = useCallback( + (notification: Notification) => { + const route = resolveNotificationRoute(notification); + if (!route) return; + if (route.startsWith("/dashboard")) setView("calendar"); + else if (route === "/profile") setView("profile"); + else if (route === "/clients") setView("clients"); + }, + [setView] + ); + + const handleViewChange = useCallback( + (newView: ViewType) => { + sidebar.closeMobileSidebar(); + const canAccess = canAccessView(newView, accessRole, effectiveClientId !== null); + if (!canAccess) { + if (newView !== "clients") toast.error("Selecione um cliente para acessar esta funcionalidade."); + setView("clients"); + } else { + setView(newView); + } + }, + [effectiveClientId, accessRole, setView, sidebar] + ); + + useEffect(() => { + if (!auth.loading && auth.session) { + const canAccess = canAccessView(view, accessRole, effectiveClientId !== null); + if (!canAccess) setView("clients"); + } + }, [auth.loading, auth.session, effectiveClientId, accessRole, view, setView]); + + const selectedClient = effectiveClientId + ? auth.clients.find((c) => c.id === effectiveClientId) ?? null + : null; + + return { + // auth + auth, + hasClients, + accessRole, + + // theme + darkMode, + toggleDark, + + // sidebar + sidebar, + + // client + effectiveClientId, + selectedClient, + selectClient, + transitionTarget, + onTransitionComplete, + + // data + members, + holidays, + + // notifications + notifications, + handleNotificationClick, + + // view + view, + handleViewChange, + + // modal + isModalOpen, + closeTaskModal, + + // task actions + taskActions, + updateTask, + }; +} diff --git a/src/hooks/useAppSidebar.ts b/src/hooks/useAppSidebar.ts new file mode 100644 index 0000000..55e6259 --- /dev/null +++ b/src/hooks/useAppSidebar.ts @@ -0,0 +1,32 @@ +import { useState } from "react"; + +function resolveInitialOpen(): boolean { + return localStorage.getItem("sidebarOpen") !== "false"; +} + +/** + * Manages desktop and mobile sidebar visibility. + * Desktop open state is persisted in `localStorage`. + */ +export function useAppSidebar() { + const [sidebarOpen, setSidebarOpen] = useState(resolveInitialOpen); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + + const toggleSidebar = () => { + setSidebarOpen((prev) => { + localStorage.setItem("sidebarOpen", String(!prev)); + return !prev; + }); + }; + + const openMobileSidebar = () => setMobileSidebarOpen(true); + const closeMobileSidebar = () => setMobileSidebarOpen(false); + + return { + sidebarOpen, + mobileSidebarOpen, + toggleSidebar, + openMobileSidebar, + closeMobileSidebar, + }; +} diff --git a/src/hooks/useAppTheme.ts b/src/hooks/useAppTheme.ts new file mode 100644 index 0000000..f3f8588 --- /dev/null +++ b/src/hooks/useAppTheme.ts @@ -0,0 +1,25 @@ +import { useState, useEffect } from "react"; + +function resolveInitialDarkMode(): boolean { + const stored = localStorage.getItem("theme"); + if (stored === "dark") return true; + if (stored === "light") return false; + return window.matchMedia("(prefers-color-scheme: dark)").matches; +} + +/** + * Manages the app-wide dark mode state. + * Syncs with `localStorage` and applies the `dark` class to ``. + */ +export function useAppTheme() { + const [darkMode, setDarkMode] = useState(resolveInitialDarkMode); + + useEffect(() => { + document.documentElement.classList.toggle("dark", darkMode); + localStorage.setItem("theme", darkMode ? "dark" : "light"); + }, [darkMode]); + + const toggleDark = () => setDarkMode((prev) => !prev); + + return { darkMode, toggleDark }; +} diff --git a/src/hooks/useClientTransition.ts b/src/hooks/useClientTransition.ts new file mode 100644 index 0000000..bdb31cc --- /dev/null +++ b/src/hooks/useClientTransition.ts @@ -0,0 +1,58 @@ +import { useState, useCallback } from "react"; +import { toast } from "sonner"; +import { useClientStore } from "@/store/useClientStore"; +import { useTaskStore } from "@/store/useTaskStore"; +import { useMemberStore } from "@/store/useMemberStore"; +import { useUIStore } from "@/store/useUIStore"; +import type { ClientOption } from "@/contexts/AuthContext"; + +interface TransitionTarget { + id: string | null | undefined; + name: string; +} + +/** + * Handles the animated client-switch flow: + * shows `ClientTransitionOverlay`, then swaps the active client in stores. + */ +export function useClientTransition( + clients: ClientOption[], + effectiveClientId: string | null | undefined +) { + const setClient = useClientStore((s) => s.setClient); + const invalidateTasks = useTaskStore((s) => s.invalidate); + const invalidateMembers = useMemberStore((s) => s.invalidate); + const setView = useUIStore((s) => s.setView); + + const [transitionTarget, setTransitionTarget] = useState(null); + + const selectClient = useCallback((clientId: string | null | undefined) => { + const target = clients.find((c) => c.id === clientId); + if (!target || clientId === effectiveClientId) return; + + // Show overlay first; after fade-in (~650ms) trigger the swap so data fetches happen in background + setTransitionTarget({ id: clientId, name: target.name }); + setTimeout(() => { + setClient(clientId); + invalidateTasks(); + invalidateMembers(); + setView("home"); + }, 650); + }, [clients, effectiveClientId, setClient, invalidateTasks, invalidateMembers, setView]); + + const onTransitionComplete = useCallback(() => { + if (!transitionTarget) return; + toast("Visualização alterada", { + description: `Trocado para ${transitionTarget.name}`, + style: { + background: "var(--muted)", + color: "var(--foreground)", + border: "1px solid var(--border)", + }, + duration: 3000, + }); + setTransitionTarget(null); + }, [transitionTarget]); + + return { transitionTarget, selectClient, onTransitionComplete }; +} diff --git a/src/hooks/useTaskActions.ts b/src/hooks/useTaskActions.ts new file mode 100644 index 0000000..452586e --- /dev/null +++ b/src/hooks/useTaskActions.ts @@ -0,0 +1,102 @@ +import { useState, useCallback } from "react"; +import { toast } from "sonner"; +import { useUIStore } from "@/store/useUIStore"; +import type { Task } from "@/lib/steps"; + +interface UseTaskActionsParams { + createTask: (data: Omit) => Promise; + updateTask: (task: Task) => Promise; + deleteTask: (id: string) => Promise; + effectiveClientId: string | null | undefined; +} + +/** + * Encapsulates task modal state and CRUD handlers (create, update, delete). + * Keeps App.tsx free of task-specific imperative logic. + */ +export function useTaskActions({ + createTask, + updateTask, + deleteTask, + effectiveClientId, +}: UseTaskActionsParams) { + const openTaskModal = useUIStore((s) => s.openTaskModal); + const closeTaskModal = useUIStore((s) => s.closeTaskModal); + + const [editingTask, setEditingTask] = useState(null); + const [pendingDeleteTaskId, setPendingDeleteTaskId] = useState(null); + const [closeModalOnDelete, setCloseModalOnDelete] = useState(false); + + const openNewTask = useCallback(() => { + setEditingTask(null); + openTaskModal(); + }, [openTaskModal]); + + const openEditTask = useCallback((task: Task) => { + setEditingTask(task); + openTaskModal(); + }, [openTaskModal]); + + const requestDeleteTask = useCallback((id: string, closeModalAfterDelete = false) => { + setPendingDeleteTaskId(id); + setCloseModalOnDelete(closeModalAfterDelete); + }, []); + + const cancelDeleteTask = useCallback(() => { + setPendingDeleteTaskId(null); + setCloseModalOnDelete(false); + }, []); + + const confirmDeleteTask = useCallback(async () => { + if (!pendingDeleteTaskId) return; + + const taskId = pendingDeleteTaskId; + const shouldCloseModal = closeModalOnDelete; + + setPendingDeleteTaskId(null); + setCloseModalOnDelete(false); + + const ok = await deleteTask(taskId); + if (!ok) { + toast.error("Erro ao eliminar tarefa"); + return; + } + + if (shouldCloseModal) closeTaskModal(); + }, [pendingDeleteTaskId, closeModalOnDelete, deleteTask, closeTaskModal]); + + const saveTask = useCallback(async (taskData: Omit) => { + let ok: boolean; + + if (editingTask) { + ok = await updateTask({ + ...editingTask, + ...taskData, + clientId: effectiveClientId ?? editingTask.clientId, + }); + } else { + ok = await createTask({ + ...taskData, + clientId: effectiveClientId ?? taskData.clientId, + }); + } + + if (!ok) { + toast.error("Erro ao guardar tarefa"); + return; + } + + closeTaskModal(); + }, [editingTask, effectiveClientId, updateTask, createTask, closeTaskModal]); + + return { + editingTask, + pendingDeleteTaskId, + openNewTask, + openEditTask, + requestDeleteTask, + cancelDeleteTask, + confirmDeleteTask, + saveTask, + }; +} diff --git a/src/lib/accessControl.ts b/src/lib/accessControl.ts index f1dd284..3888736 100644 --- a/src/lib/accessControl.ts +++ b/src/lib/accessControl.ts @@ -11,6 +11,7 @@ export type AppPermission = | 'view:reports' | 'view:admin' | 'view:tools' + | 'view:profile' interface ViewRule { requiresClient: boolean @@ -27,6 +28,7 @@ const ROLE_PERMISSIONS: Record> = { 'view:reports', 'view:admin', 'view:tools', + 'view:profile', ]), user: new Set([ 'view:home', @@ -35,6 +37,7 @@ const ROLE_PERMISSIONS: Record> = { 'view:members', 'view:reports', 'view:tools', + 'view:profile', ]), } @@ -57,6 +60,7 @@ const VIEW_RULES: Record = { 'tools-import': { requiresClient: false, roles: ['admin', 'user'], permission: 'view:tools' }, 'tools-export': { requiresClient: false, roles: ['admin', 'user'], permission: 'view:tools' }, 'tools-integrations': { requiresClient: false, roles: ['admin', 'user'], permission: 'view:tools' }, + profile: { requiresClient: false, roles: ['admin', 'user'], permission: 'view:profile' }, } export function resolveAccessRole(member: Member | null): AccessRole | null { diff --git a/src/lib/notifications.ts b/src/lib/notifications.ts index a36b994..8e8f885 100644 --- a/src/lib/notifications.ts +++ b/src/lib/notifications.ts @@ -118,6 +118,26 @@ export async function createNotificationForClient( if (error) throw error } +export async function createNotificationForAll( + clientIds: string[], + title: string, + message: string, + type: string = 'admin_broadcast' +) { + if (!supabaseAdmin) throw new Error('Admin não configurado') + + const rows = clientIds.map((clientId) => ({ + client_id: clientId, + user_id: null, + title, + message, + type, + })) + + const { error } = await supabaseAdmin.from('notifications').insert(rows) + if (error) throw error +} + export function resolveNotificationRoute(notification: Notification): string | null { if (!notification.metadata) return null diff --git a/src/store/useMemberStore.ts b/src/store/useMemberStore.ts index 785df40..e933694 100644 --- a/src/store/useMemberStore.ts +++ b/src/store/useMemberStore.ts @@ -84,7 +84,7 @@ export const useMemberStore = create()( }, invalidate: () => { - set({ members: [], cachedClientId: undefined, error: null }) + set({ members: [], cachedClientId: undefined, error: null, loading: false }) }, }), { name: 'app/members', enabled: true } diff --git a/src/store/useTaskStore.ts b/src/store/useTaskStore.ts index dbaf5b5..f1fecab 100644 --- a/src/store/useTaskStore.ts +++ b/src/store/useTaskStore.ts @@ -113,7 +113,7 @@ export const useTaskStore = create()( }, invalidate: () => { - set({ tasks: [], cacheKey: undefined, error: null }) + set({ tasks: [], cacheKey: undefined, error: null, loading: false }) }, }), { name: 'app/tasks', enabled: true } diff --git a/src/store/useUIStore.ts b/src/store/useUIStore.ts index cd9b2ec..1996f94 100644 --- a/src/store/useUIStore.ts +++ b/src/store/useUIStore.ts @@ -20,6 +20,7 @@ export type ViewType = | 'tools-export' | 'tools-integrations' | 'demandas' + | 'profile' export type DashboardMode = 'calendar' | 'timeline' interface DashboardRedirect { diff --git a/src/views/admin/components/NotificationsPanel.tsx b/src/views/admin/components/NotificationsPanel.tsx deleted file mode 100644 index 72315b6..0000000 --- a/src/views/admin/components/NotificationsPanel.tsx +++ /dev/null @@ -1,440 +0,0 @@ -import { useState, useCallback, useEffect, useMemo } from 'react' -import { Send, Users, Building2, Clock, AlertTriangle, RefreshCw, Eye, EyeOff } from 'lucide-react' -import { Button, Input, Label } from '@/components/ui' -import { toast } from 'sonner' -import { supabaseAdmin } from '@/lib/supabase' -import { createNotification, createNotificationForClient, fetchAllNotifications, markAsRead as markAsReadApi } from '@/lib/notifications' -import type { Member } from '@/hooks/useSupabase' -import type { Client } from '@/types/db' -import type { Notification } from '@/types/notification' - -interface NotificationsPanelProps { - clients: Client[] - users: Member[] -} - -type TargetType = 'user' | 'client' - -function formatDate(dateString: string): string { - try { - const date = new Date(dateString) - return date.toLocaleString('pt-BR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) - } catch { - return '' - } -} - -function getNotificationTypeLabel(type: string): string { - const labels: Record = { - step_assigned: 'Etapa atribuída', - step_unassigned: 'Etapa removida', - role_changed: 'Cargo alterado', - client_access_granted: 'Acesso concedido', - client_access_revoked: 'Acesso revogado', - admin_broadcast: 'Broadcast', - manual: 'Manual', - } - return labels[type] || type -} - -function isToday(dateString: string): boolean { - const date = new Date(dateString) - const today = new Date() - return date.toDateString() === today.toDateString() -} - -function isYesterday(dateString: string): boolean { - const date = new Date(dateString) - const yesterday = new Date() - yesterday.setDate(yesterday.getDate() - 1) - return date.toDateString() === yesterday.toDateString() -} - -function isLast7Days(dateString: string): boolean { - const date = new Date(dateString) - const today = new Date() - const diffDays = Math.floor((today.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)) - return diffDays >= 1 && diffDays <= 7 -} - -interface GroupedNotifications { - today: Notification[] - yesterday: Notification[] - last7Days: Notification[] - older: Notification[] -} - -function groupNotifications(notifications: Notification[]): GroupedNotifications { - const grouped: GroupedNotifications = { - today: [], - yesterday: [], - last7Days: [], - older: [], - } - - for (const notif of notifications) { - if (isToday(notif.created_at)) { - grouped.today.push(notif) - } else if (isYesterday(notif.created_at)) { - grouped.yesterday.push(notif) - } else if (isLast7Days(notif.created_at)) { - grouped.last7Days.push(notif) - } else { - grouped.older.push(notif) - } - } - - return grouped -} - -function NotificationList({ - notifications, - onMarkAsRead, - onToggleRead, -}: { - notifications: Notification[] - onMarkAsRead: (id: string) => void - onToggleRead?: (id: string) => void -}) { - if (notifications.length === 0) return null - - return ( -
- {notifications.map((notif) => ( -
-
-
- {!notif.read && ( - - )} - - {notif.title} - - - {getNotificationTypeLabel(notif.type)} - - {notif.read && ( - - lida - - )} -
-

{notif.message}

- - {formatDate(notif.created_at)} - -
-
- {!notif.read ? ( - - ) : onToggleRead ? ( - - ) : null} -
-
- ))} -
- ) -} - -export function NotificationsPanel({ clients, users }: NotificationsPanelProps) { - const [title, setTitle] = useState('') - const [message, setMessage] = useState('') - const [targetType, setTargetType] = useState('user') - const [selectedUserId, setSelectedUserId] = useState('') - const [selectedClientId, setSelectedClientId] = useState('') - const [sending, setSending] = useState(false) - - const [history, setHistory] = useState([]) - const [loadingHistory, setLoadingHistory] = useState(true) - const [error, setError] = useState(null) - - const loadHistory = useCallback(async () => { - if (!supabaseAdmin) return - setLoadingHistory(true) - setError(null) - try { - const data = await fetchAllNotifications() - setHistory(data) - } catch (e) { - setError(e instanceof Error ? e.message : 'Erro ao carregar') - } finally { - setLoadingHistory(false) - } - }, []) - - useEffect(() => { - loadHistory() - }, [loadHistory]) - - const grouped = useMemo(() => groupNotifications(history), [history]) - - const handleSend = async () => { - if (!title.trim() || !message.trim()) { - toast.error('Preencha o título e a mensagem') - return - } - - if (!supabaseAdmin) { - toast.error('Admin não configurado') - return - } - - setSending(true) - - try { - if (targetType === 'user' && selectedUserId) { - await createNotification(title, message, selectedUserId, undefined, 'manual') - } else if (targetType === 'client' && selectedClientId) { - await createNotificationForClient(selectedClientId, title, message) - } - - toast.success('Notificação enviada') - setTitle('') - setMessage('') - await loadHistory() - } catch { - toast.error('Erro ao enviar notificação') - } finally { - setSending(false) - } - } - - const handleMarkAsRead = async (id: string) => { - try { - await markAsReadApi(id) - setHistory((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n))) - } catch { - toast.error('Erro ao marcar como lida') - } - } - - const handleToggleRead = async (id: string) => { - const notification = history.find((n) => n.id === id) - if (!notification) return - - try { - await markAsReadApi(id) - setHistory((prev) => - prev.map((n) => (n.id === id ? { ...n, read: !n.read } : n)) - ) - } catch { - toast.error('Erro ao atualizar') - } - } - - const activeUsers = users.filter((u) => u.is_active !== false) - - const hasNotifications = history.length > 0 - - return ( -
-
-
- -

Enviar Notificação

-
- -
-
- -
- - -
-
- - {targetType === 'user' && ( -
- - -
- )} - - {targetType === 'client' && ( -
- - -
- )} - -
- - setTitle(e.target.value)} - placeholder="Ex: Atualização importante" - /> -
- -
- -