diff --git a/.gitignore b/.gitignore index 425ab4f..4b6a96f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ node_modules/ .env .env.local .env.*.local -.claude \ No newline at end of file +.claude +# Prompts/knowledge (not committed) +docs/ diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 8422c69..53b78c4 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,50 +1,148 @@ -/** - * Mock stand-in for the real atlas `POST /api/chat`. Streams the same SSE event shape - * (`queue` → `reasoning` → `content` → `done`) as the real endpoint would, but replays a - * canned reply instead of calling vLLM — no rate limiting, no budget, no real model. - */ -const REPLY_REASONING = - 'The user is asking about a gap in the wave column display. Argo CD sync waves are integers used purely for ordering; there is no requirement that they be contiguous. The cluster uses 0,1,2,3,5,6,7,8. This is normal and usually happens when a wave is retired or intentionally reserved.' +import { NextRequest } from 'next/server' -const REPLY = - 'Wave 4 is empty. Waves are sort keys, not a sequence — Argo orders by value and skips gaps, so 3 is followed directly by 5. Nothing is missing.' +const LLM_API_URL = 'https://api.riotpiao.com/v1/chat/completions' +const MODEL = 'reasoning' -const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +const SYSTEM_PROMPT = `Poimen. Rock Liang's AI assistant. Answer about technical background, projects, expertise. Specific facts + metrics. -export async function POST() { - const encoder = new TextEncoder() +## Background +6+ years Senior Software Engineer. Infrastructure + Backend + LLM Systems. Homelab K8s + vLLM optimization. - const stream = new ReadableStream({ - async start(controller) { - const send = (type: string, payload: Record = {}) => { - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type, ...payload })}\n\n`)) - } +## Skills +Infra: Talos Linux K8s (4-node), Terraform GitOps, Cilium eBPF CNI, Longhorn 3-replica, MinIO, PostgreSQL +Backend: Go, Java, Python, C++. gRPC. Kafka KRaft. Data systems. +LLM: vLLM 60% latency cut. Model serving. Inference optimization. +DevOps: ArgoCD. cert-manager. SOPS encryption. AWS CDK/CloudFormation. CloudWatch. - for (let position = 3; position > 0; position--) { - send('queue', { position }) - await wait(500) - } +## Achievements +AWS: Distributed-Map 57+ regions, <100ms P99. CDK infrastructure. CF stack mgmt. CloudWatch observability. +RBC: Terraform deploy 2hr→20min. 99.2% automation. +Homelab: 99.2% uptime. Production-grade HA. - send('reasoning', { text: REPLY_REASONING }) - await wait(200) +## Operations (Deep) +Certs: cert-manager + Let's Encrypt. 30d renewal, no downtime. SOPS encrypted secrets. Git audit trail. Prometheus alerts 7d/1d pre-expiry. CertificateTask CRD tracks history. - let i = 0 - while (i < REPLY.length) { - i += 3 - send('content', { text: REPLY.slice(0, i) }) - await wait(40) - } +Queues: SQS FIFO + DLQ. Exactly-once via idempotency keys + PostgreSQL. QueueTask CRD. Multi-region failover (SQS-A→B, ordered). Controller detects stalled tasks, exponential backoff. Inference batching by model/token/SLO. Workers scale 1-100. - send('done') - controller.close() - }, - }) +K8s: CRDs + operators. Reconciliation (leader-election, backoff, finalizers). Go controllers (watch/queue/reconcile). API server internals (etcd, versioning, watch). Pod disruption budgets, PreStop hooks. Talos immutable, atomic updates, no SSH, GitOps state. Cilium eBPF policies. - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }, - }) +Open source: go-flink (distributed DataLakeHouse). + +## Response Style +Caveman ultra. Drop articles/filler. Fragments OK. Short terms. No prose. Facts + metrics. E.g., "Terraform 3-4yr. RBC: 2h→20m, 99.2% auto" not "Several years of experience with approximately..." + +Pre-screen context: Technical depth for platform engineer role. Cert rotation, queue semantics, failure modes, scale. Demonstrate production-grade systems.` + +export async function POST(request: NextRequest) { + const token = process.env.LLM_API_TOKEN + + if (!token) { + return new Response( + JSON.stringify({ error: 'LLM_API_TOKEN not configured' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ) + } + + try { + const { message } = await request.json() + + const response = await fetch(LLM_API_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: MODEL, + messages: [ + { role: 'system', content: SYSTEM_PROMPT }, + { role: 'user', content: message }, + ], + stream: true, + }), + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`LLM API error: ${response.status} - ${error}`) + } + + // Transform the SSE stream + const encoder = new TextEncoder() + const decoder = new TextDecoder() + + const stream = new ReadableStream({ + async start(controller) { + const reader = response.body?.getReader() + if (!reader) { + controller.close() + return + } + + let reasoning = '' + let content = '' + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n') + + for (const line of lines) { + if (!line.startsWith('data: ')) continue + const data = line.slice(6) + if (data === '[DONE]') continue + + try { + const json = JSON.parse(data) + const delta = json.choices?.[0]?.delta + + if (delta?.reasoning_content) { + reasoning += delta.reasoning_content + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'reasoning', text: reasoning })}\n\n`) + ) + } + + if (delta?.content) { + content += delta.content + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'content', text: content })}\n\n`) + ) + } + + if (json.choices?.[0]?.finish_reason === 'stop') { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\n\n`) + ) + } + } catch { + // Skip invalid JSON + } + } + } + } finally { + reader.releaseLock() + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }) + } catch (error) { + console.error('Chat API error:', error) + return new Response( + JSON.stringify({ error: 'Failed to process chat request' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ) + } } diff --git a/components/Header.tsx b/components/Header.tsx index d91b10f..e496314 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -2,15 +2,15 @@ import Link from 'next/link' import Image from 'next/image' -import { Menu, X, Moon, Sun } from 'lucide-react' -import { useState, useEffect } from 'react' +import { Menu, X } from 'lucide-react' +import { useState } from 'react' import { useTerminal } from '@/lib/TerminalContext' import { useLanguage } from '@/lib/LanguageContext' import { CIStatusIndicator } from './CIStatusIndicator' export default function Header() { const [open, setOpen] = useState(false) - const [dark, setDark] = useState(false) + const [skillsOpen, setSkillsOpen] = useState(false) const [contactOpen, setContactOpen] = useState(false) const [educationOpen, setEducationOpen] = useState(false) @@ -28,30 +28,7 @@ export default function Header() { setLang(lang === 'en' ? 'zh' : 'en') } - useEffect(() => { - const stored = localStorage.getItem('theme') - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - const isDark = stored === 'dark' || (!stored && prefersDark) - - if (isDark) { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - setDark(isDark) - }, []) - const toggleDark = () => { - const newDark = !dark - if (newDark) { - document.documentElement.classList.add('dark') - localStorage.setItem('theme', 'dark') - } else { - document.documentElement.classList.remove('dark') - localStorage.setItem('theme', 'light') - } - setDark(newDark) - } return (
@@ -219,9 +196,6 @@ export default function Header() { )} - {/* Language Toggle */} ) : ( -
- Poimen (Agent Terminal) +
+ + Poimen (AI Assistant) + @@ -115,34 +240,65 @@ export function InteractiveTerminal() {
{history.length === 0 && ( -
type 'help' for commands
+
+ Ask me about Rock's experience, projects, or skills! +
+ Type 'help' for commands +
)} {history.map((entry, i) => ( -
-
% {entry.cmd}
-
- {entry.output} +
+
+ {entry.cmd}
+ + {entry.isStreaming && !entry.output && ( +
+
+ + + {entry.thinking ? entry.thinking.slice(-80) : 'Working...'} + +
+
+ (Reasoning on V100 32GB, slight delay expected) +
+
+ )} + + {entry.output && ( +
+ {entry.output} + {entry.isStreaming && ( + + )} +
+ )}
))}
-
-
- - % +
+
+ setInput(e.target.value)} - onKeyPress={(e) => { - if (e.key === 'Enter') handleCommand(input) + onKeyDown={(e) => { + if (e.key === 'Enter' && !isLoading) handleCommand(input) }} - placeholder="" - className="flex-1 bg-transparent text-green-400 font-mono text-sm outline-none ml-1" + placeholder={isLoading ? 'Thinking...' : 'Ask anything...'} + disabled={isLoading} + className={`flex-1 bg-transparent text-sm outline-none ${ + isDark ? 'text-gray-200 placeholder-gray-600' : 'text-gray-800 placeholder-gray-400' + } ${isLoading ? 'opacity-50' : ''}`} autoFocus />
diff --git a/docs/PLAN-atlas.md b/docs/PLAN-atlas.md deleted file mode 100644 index 6e6eb70..0000000 --- a/docs/PLAN-atlas.md +++ /dev/null @@ -1,327 +0,0 @@ -# Implementation Plan: `atlas` — Interactive Cluster Visualization - -**Status**: For review — no code written yet -**Companion ADR**: [ADR-0001](adr/ADR-0001-atlas-cluster-visualization.md) -**Supersedes**: `PLAN.md`, `IMPLEMENTATION.md` (Homarr + Terraform — abandoned) - ---- - -## 1. Scope - -### In scope - -| ID | Surface | Description | -|---|---|---| -| B | Cluster topology | Live node → namespace → workload graph, health-colored, drill-down | -| E | Delivery tree | Argo CD app-of-apps as a sync-wave-ordered DAG, live sync animation | -| C | Terminal | Read-only, enum-dispatched cluster queries in the browser | -| D | Chat | Streaming ChatGPT-style session against the `reasoning` model | - -### Out of scope (v1) - -- Any write operation against the cluster -- LLM tool-calling / agentic loops against live infrastructure -- Forgejo CI half of the pipeline view (`forgejo-gitea` is currently stuck `Init:0/3`) -- Log streaming to the browser (Loki content is unredactable in practice) -- Authenticated / operator-only views — Grafana already serves that need - -### Non-goals - -- Replacing Grafana -- Multi-cluster support -- Historical / time-travel views - ---- - -## 2. Phase 0 — Unblock (blocking; nothing ships until done) - -Verified problems, in the order they must be fixed: - -| # | Problem | Evidence | -|---|---|---| -| 0.1 | `portfolio` namespace is empty; `portfolio` and `auth-infra` Applications do not exist in the cluster | `kubectl get pods -n portfolio` → no resources; neither name appears in `kubectl get app -n argocd` | -| 0.2 | `infra/argocd-apps.yaml` `repoURL` = `forgejo.riotpiao.com` — NXDOMAIN | `dig` | -| 0.3 | Deployment image `forgejo.riotpiao.com/rock/portfolio:latest` — dead host, and `:latest` + `imagePullPolicy: IfNotPresent` means a pushed image will never roll out | `infra/portfolio/base/deployment.yaml` | -| 0.4 | `forgejo-gitea` stuck `Init:0/3` for 3h — no image builds possible | `kubectl get pods -n cicd` | -| 0.5 | `riotpiao.com` returns HTTP 403 from the Cloudflare edge; no origin headers present | `curl -I https://riotpiao.com` | -| 0.6 | No test framework installed — TDD is impossible as the repo stands | `package.json` has no test script or runner | -| 0.7 | `sms` Application Degraded (`macos-bluebubbles` Pending 3h); `longhorn-config` OutOfSync | `kubectl get app -n argocd` | - -**Decisions required from you before 0.1–0.3 can be actioned** — see ADR open questions 1 and 2. - -**Actions** - -1. Resolve which GitOps repo owns the portfolio; delete or correct the losing manifest -2. Point `repoURL` and the image reference at real hostnames -3. Replace `:latest` with a commit-SHA tag; set `imagePullPolicy: IfNotPresent` (correct once tags are immutable) -4. Diagnose `forgejo-gitea` init containers — read the actual init container logs before changing anything -5. Diagnose the apex 403 — check the tunnel's Public Hostnames list and Cloudflare WAF events; the event log names the blocking rule -6. Add Vitest + Testing Library + `msw`; add `test` and `test:watch` scripts -7. Triage 0.7 separately — unrelated to this work, but the delivery tree will render both as red on day one - -**Verify** - -```bash -kubectl get pods -n portfolio # 2/2 Running -curl -sS -o /dev/null -w '%{http_code}\n' https://riotpiao.com # 200 -pnpm test # runner executes, 0 tests, exit 0 -``` - ---- - -## 3. Architecture - -``` -kube API (client-go informers) ─┐ -Prometheus /api/v1/query ├──> atlas (Go, ns: portfolio, RO ServiceAccount) -Argo CD Application CRs ┘ │ - ├─ snapshot (in-memory, redacted at write) - ├─ Redis pub/sub (kmsvc-redis-master.sqs:6379) - └─ HTTP - GET /api/topology - GET /api/delivery - GET /api/stream (SSE) - POST /api/exec - POST /api/chat (SSE) - └──> reasoning-predictor.llm-serving:80 -``` - -Event-driven, per project architectural preference: informers push to a reducer, the reducer publishes deltas to Redis, SSE handlers subscribe. No request-triggered upstream calls anywhere in the read path. - -Snapshot is redacted **at write time**, not at serialization time. A field that never enters the snapshot cannot leak from any surface. - -### Language - -Go, for `client-go` informers and because it matches the rest of the platform. Follow the repo's Go skill set (`go-naming`, `go-concurrency`, `go-error-handling`, `go-context`) — notably: every upstream call carries a context, no naked returns, no `_ =` on errors. - ---- - -## 4. API contract - -Envelope for all non-stream responses: - -```json -{ "data": { }, "meta": { "snapshotAge": 3.2, "generation": 88412 } } -``` - -Errors follow RFC 9457 (`application/problem+json`): - -```json -{ "type": "https://riotpiao.com/errors/rate-limited", - "title": "Rate limit exceeded", - "status": 429, "detail": "12 of 12 messages used", "retryAfter": 3600 } -``` - -| Method | Path | Auth | Limit | Response | -|---|---|---|---|---| -| GET | `/api/topology` | none | 60/min/IP | Nodes, namespaces, workload summaries | -| GET | `/api/delivery` | none | 60/min/IP | Argo apps, wave-grouped; resource children lazy | -| GET | `/api/delivery/{app}/resources` | none | 60/min/IP | Virtualized child list for one app | -| GET | `/api/stream` | session cookie | 2 concurrent/IP | SSE deltas, `topology` + `delivery` event types | -| POST | `/api/exec` | session cookie | 20/min/session | Enum command result | -| POST | `/api/chat` | session + Turnstile | 12/day/session, 6 global concurrent | SSE token stream | - -**Pagination**: `/api/delivery/{app}/resources` is cursor-paginated at 100 items. `prometheus` has 68 resources today, but `homelab-root`'s tree will grow. - -**Payload budget**: topology response capped at 256 KB, delivery at 256 KB. Exceeding the cap truncates and sets `meta.truncated: true` — never a silent drop. - ---- - -## 5. Security - -Mapped against OWASP Top 10 (2021). Every item is a Phase gate, not a wish list. - -### A01 Broken Access Control - -- `atlas` ServiceAccount: one ClusterRole, verbs `get,list,watch` only, explicit resource list. **No `secrets`. No `*`. No wildcards on apiGroups.** -- NetworkPolicy on `atlas`: egress restricted to kube API, `prometheus-operated.monitoring`, `reasoning-predictor.llm-serving`, `kmsvc-redis-master.sqs`. Ingress from `ingress-nginx` only. -- Test: an integration test asserting the SA receives 403 on `get secrets` in every namespace. - -### A02 Cryptographic Failures - -- Session cookie: signed (HMAC), `HttpOnly`, `Secure`, `SameSite=Lax`, 24h expiry. No PII in the payload — a random session ID only. -- Signing key from a Kubernetes Secret via SOPS (`sops-secrets` app already exists), never an env literal in a manifest. - -### A03 Injection - -The primary risk on surface C. Mitigation is structural, not filtering: - -- Input parses to a closed command enum. Anything unmatched is rejected before any lookup. -- Namespace and resource-name arguments are validated by **set membership against the current snapshot**, not by regex or escaping. -- No shell, no `exec`, no `kubectl` binary present in the container image. -- Test: fuzz the parser; assert every input outside the allowlist returns a rejection and performs zero upstream calls. - -### A04 Insecure Design — information disclosure - -The core risk of the whole project. Redaction allowlist, enforced by DTO construction: - -**Emitted**: name, namespace, kind, phase, ready counts, restart count, age, node name, health status, sync status, sync wave, an explicit label subset. - -**Never emitted**: container env, container args, image digests, image tags, `spec.source.repoURL`, full `spec.source.path`, annotations, pod IPs, cluster IPs, Secret names, `status.conditions[].message`, node internal IPs. - -Specific known leaks in current data: -- `reasoning` container args disclose the entire model and quantization strategy -- `spec.source.repoURL` discloses a private GitHub repository -- 21 `Secret` resources appear in Argo trees — render **kind and count only, never names**; `sops-secrets` included -- `status.conditions[].message` echoes raw errors containing internal hostnames — emit condition **type** only - -Test: golden test asserting the serialized snapshot contains none of the denied field names, run against a fixture captured from the real cluster. - -### A05 Security Misconfiguration - -- Container: `runAsNonRoot`, read-only root filesystem, all capabilities dropped, `seccompProfile: RuntimeDefault` -- Security headers on all responses: `Content-Security-Policy` (no `unsafe-inline`), `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `Strict-Transport-Security` -- CORS: same-origin only. No wildcard. - -### A07 Authentication Failures - -- Anonymous by design; Cloudflare Turnstile gates the first chat message -- Session rotation on issue; no session fixation vector since there is no login - -### A08 Software and Data Integrity - -- Image tags are commit SHAs, never `:latest` (fixes Phase 0.3) -- `pnpm audit` and `govulncheck` in CI, failing the build on high severity - -### A09 Logging and Monitoring - -- Structured logs: every rejected `/api/exec` input, every rate-limit trip, every chat queue rejection -- Prometheus metrics from `atlas`: `atlas_chat_concurrent`, `atlas_chat_queue_depth`, `atlas_ratelimit_rejections_total`, `atlas_snapshot_age_seconds` -- Alertmanager rule: chat queue saturated > 5 min, snapshot age > 60s - -### A10 SSRF - -- `atlas` calls a fixed, compile-time list of upstream URLs. No user input reaches any URL construction, on any path. - -### LLM-specific: prompt injection - -- System prompt is a compile-time constant, unreachable by user input -- Cluster snapshot digest is injected in a delimited block explicitly labelled untrusted data -- User message is always last -- **No tool-calling.** The model reads a pre-built digest and cannot query anything. This removes the entire agentic attack surface for v1. -- Output capped at `max_tokens: 1500` — DeepSeek-R1 will otherwise reason for minutes - ---- - -## 6. Rate limiting - -Sized against the verified hard ceiling: **8 concurrent sequences** (`--max-num-seqs=4` × 2 replicas), single GPU node. - -**Tier 1 — Cloudflare edge.** WAF, Bot Fight Mode, per-IP rate rules, Turnstile before first chat message. Free, and stops scripted abuse before it reaches your hardware. - -**Tier 2 — Kong.** `rate-limiting` plugin, `policy: redis` against `kmsvc-redis-master.sqs:6379` so counters are cluster-wide rather than per-pod (Kong runs 2 replicas — a local policy would silently double every limit). Two profiles: generous for topology and delivery, tight for chat. - -**Tier 3 — `atlas`, the tier that actually protects the GPU.** - -| Control | Value | Rationale | -|---|---|---| -| Global chat semaphore | 6 | Leaves 2 of 8 sequence slots as operator headroom | -| Queue depth | 20, then reject with `429` | A visible queue beats a wall; an unbounded queue beats nothing | -| Per-session budget | 12 messages / 24h | Enough to explore, not enough to farm | -| Per-request timeout | 120s hard, server-side | Independent of client behaviour | -| Disconnect handling | cancel upstream immediately | **Critical** — a walked-away tab holding 1 of 8 slots is a real outage | - -Queue position is streamed to the client as SSE `{"type":"queue","position":N}` events, so waiting is legible rather than a hang. - ---- - -## 7. Streaming - -SSE throughout — unidirectional server→client fits every surface, including token streaming. WebSockets are not justified; nothing flows client→server mid-stream. - -- Keepalive comment frame every 30s (idle SSE connections die at proxies) -- `Last-Event-ID` supported on `/api/stream` for resumable topology/delivery deltas; chat is not resumable -- `req.Context()` threaded to the upstream vLLM request so client abort cancels it — this is the mechanism that enforces the Tier 3 disconnect rule -- Backpressure: bounded per-client channel; a slow consumer is dropped rather than allowed to grow memory -- Chat events: `{"type":"reasoning"|"content"|"queue"|"done"|"error"}`. `--reasoning-parser=deepseek_r1` already separates `reasoning_content` from `content` — render thinking in a collapsible block. That block **is** the demo. - -**Context budget** (16384 total): system prompt ~300, snapshot digest capped at 2000, `max_tokens` 1500, leaving ~12500 for history. History is truncated oldest-first to fit. The digest is a compact rendering, never raw JSON. - ---- - -## 8. Frontend - -- **Topology (B)**: React Flow, force layout. Node → namespace → workload. -- **Delivery (E)**: React Flow, wave columns left→right from the existing `sync-wave` annotations (0→8). 31 app nodes — an ideal size for a readable DAG. Click an app → side panel with a `react-arborist` virtualized resource tree, children lazy-loaded. Live sync animation `OutOfSync → Syncing → Synced` driven by the Application watch. Push a commit during a demo and the wave cascades — that is the moment worth engineering for. -- **Terminal (C)**: reuse [components/InteractiveTerminal.tsx](../components/InteractiveTerminal.tsx). Command set: `get nodes`, `get pods `, `get apps`, `top nodes`, `describe pod `, `help`. -- **Chat (D)**: new component. Collapsible reasoning block, queue position, streaming tokens. - -Rendering budget: ~550 resources total across the tree. Lazy expansion plus virtualization is required, not optional. Target: initial delivery view interactive in < 1.5s on a cold load. - -**Also in scope**: delete the fabricated statistics in [app/page.tsx](../app/page.tsx) — "40% CPU reduction", "99.2% uptime", "Mission-critical", "60% latency cut" — and either wire each card to a real number from `/api/topology` or remove the claim. Five of the six cards link to routes that do not exist (`/infrastructure`, `/systems`, `/llm`, `/kafka`, `/opensource`). - ---- - -## 9. Phases, TDD-first - -Each phase is RED → GREEN → REFACTOR. Tests named before implementation exists. - -### Phase 1 — `atlas` core (~4 days) - -RED -- `redact_test.go`: golden test — serialized snapshot contains no denied field, against a real-cluster fixture -- `rbac_test.go`: SA receives 403 on `get secrets` -- `snapshot_test.go`: informer event produces the expected delta - -GREEN: ClusterRole, informers, reducer, DTO construction, Redis publish. -Verify: `kubectl auth can-i get secrets --as=system:serviceaccount:portfolio:atlas` → `no`. - -### Phase 2 — Surface B (~3 days) - -RED -- `stream_test.go`: SSE emits a delta within 5s of a pod state change -- `topology.test.tsx`: graph re-renders on delta without a full reload - -Verify: delete a pod, observe the graph update in < 5s without reloading. - -### Phase 3 — Surface E (~3 days) - -RED -- `delivery_test.go`: apps group correctly by `sync-wave`; Secret names absent from output; `repoURL` absent from output -- `delivery.test.tsx`: 550-node tree renders under the frame budget with virtualization on - -Verify: trigger an Argo sync, observe wave-ordered animation. - -### Phase 4 — Surface C (~2 days) - -RED -- `exec_parse_test.go`: fuzz corpus — every non-allowlisted input rejects and performs zero upstream calls -- `exec_test.go`: unknown namespace rejects on snapshot membership, not regex - -Verify: attempt injection payloads against `/api/exec`; all rejected, all logged. - -### Phase 5 — Surface D + rate limiter, one PR (~5 days) - -RED -- `ratelimit_test.go`: 7th concurrent chat queues rather than reaching vLLM -- `disconnect_test.go`: client abort cancels the upstream request -- `budget_test.go`: 13th message in 24h returns 429 with `Retry-After` -- `injection_test.go`: snapshot content cannot alter system-prompt behaviour -- `context_test.go`: history truncation keeps total tokens under 16384 - -Verify: load test at 20 concurrent clients — GPU sequence usage never exceeds 6, no upstream 5xx, queue drains. - -**Total: ~17 working days.** Rate limiter ships in the same PR as chat, never after. - ---- - -## 10. Risks - -| Risk | Impact | Mitigation | -|---|---|---| -| Redaction miss leaks internal detail | High | Allowlist by DTO construction; golden test; manual review of every new field | -| `worker-1` fails | Chat and all inference down | Out of scope to fix; degrade chat gracefully to "offline", never a hang | -| 8-slot ceiling makes chat feel broken under traffic | Medium | Visible queue position; consider disabling chat and shipping B/E/C only | -| Argo CD CRD schema changes | Low | Allowlist construction means new fields are ignored by default | -| `atlas` compromised | High | Read-only SA, NetworkPolicy egress restriction, no write verbs anywhere | -| Cost of GPU inference for anonymous visitors | Medium | Per-session daily budget; Turnstile; edge WAF | - ---- - -## 11. Decisions needed before Phase 1 - -1. **Which GitOps repo owns the portfolio** — GitHub (`riotpiao.com`) or Forgejo? Blocks 0.1–0.3. -2. **Apex 403 cause** — tunnel route, WAF rule, or no origin? Blocks 0.5. -3. **Is `homarr` still wanted?** Deployed and healthy, but from the abandoned plan. -4. **Does chat stay in v1?** Given the 8-slot ceiling, shipping B + E + C first and treating D as a separate decision is defensible. -5. **Where does `atlas` live** — this repo, or the homelab repo? Follows from decision 1. diff --git a/docs/adr/ADR-0001-atlas-cluster-visualization.md b/docs/adr/ADR-0001-atlas-cluster-visualization.md deleted file mode 100644 index d63f62e..0000000 --- a/docs/adr/ADR-0001-atlas-cluster-visualization.md +++ /dev/null @@ -1,285 +0,0 @@ -# ADR-0001: `atlas` — Single Read-Only Aggregator for Public Cluster Visualization - -**Status**: Proposed - -**Date**: 2026-08-13 - -**Authors**: Rock Liang - -**Supersedes**: `PLAN.md`, `IMPLEMENTATION.md` (Homarr + Terraform approach — abandoned; cluster is now GitOps/Kustomize + Argo CD) - ---- - -## Context - -### Problem Statement - -The homelab cluster is fully operational and runs a non-trivial platform: Talos Kubernetes, GPU-backed LLM inference, event streaming, GitOps delivery, and a full observability stack. None of it is visible to anyone but the operator. The portfolio site meant to showcase it displays **hardcoded, fabricated statistics** and links to pages that do not exist. - -Goal: replace fabricated claims with a live, interactive, public view of the real system. - -### Current Situation (verified 2026-08-13) - -**Cluster** - -| Property | Value | -|---|---| -| Distro | Talos v1.13.3, Kubernetes v1.36.1 | -| Nodes | 3× control-plane (`talos-cp-1/2/3`) + 1× `worker-1` (role `gpu-node`) | -| `worker-1` allocatable | 95950m CPU, 65019644Ki memory, 1× `nvidia.com/gpu` | -| Ingress | nginx, LoadBalancer `192.168.1.160`; Kong `10.105.63.160` for `api.riotpiao.com` | -| Namespaces | 25 | - -**GitOps** - -- `homelab-root` is an app-of-apps: 31 child `Application` resources -- Source: `git@github.com:Riotpiaole/riotpiao.com.git`, path `k8s/argocd/apps` -- Sync waves **0 → 8** already annotated across apps -- ~550 managed resources total (largest: `prometheus` 68, `cert-manager` 47, `kong` 39) - -**LLM serving** (`llm-serving`, KServe, all 5 `InferenceService` Ready) - -| Model | Notes | -|---|---| -| `reasoning` | `unsloth/DeepSeek-R1-Distill-Qwen-32B-bnb-4bit`, vLLM `v0.11.0` | -| `ornith`, `embeddings`, `reranker`, `verifier` | 1 replica each | - -`reasoning` runtime args, verbatim: `--max-num-seqs=4`, `--max-model-len=16384`, `--gpu-memory-utilization=0.90`, `--enable-prefix-caching`, `--reasoning-parser=deepseek_r1`. `minReplicas: 2`, `maxReplicas: 2`, pinned to `worker-1`. - -**Existing building blocks** - -- Prometheus (kube-prometheus-stack) + kube-state-metrics + node-exporter + blackbox + Alertmanager — `monitoring` -- Loki + promtail + Grafana — `logging` -- Kong 3.9 + Kubernetes Ingress Controller 3.5 — `api`, 2 replicas -- Redis (`kmsvc-redis-master.sqs.svc.cluster.local:6379`), 1 master + 3 replicas -- Next.js 15.5 / React 19.2 portfolio source in this repo (not deployed) - -**Public exposure** (verified by DNS + HTTP probe) - -``` -api.riotpiao.com NXDOMAIN -argocd|grafana|vault|longhorn|prometheus|minio|temporal|forgejo|portainer.riotpiao.com - NXDOMAIN -riotpiao.com 172.67.196.33 / 104.21.60.115 (Cloudflare) → HTTP 403 at edge -``` - -Nothing in the cluster is currently reachable from the public internet. The Cloudflare tunnel has no public hostnames wired (cloudflared auto-creates a CNAME per public hostname; no CNAME exists). - -### Requirements - -1. Public, anonymous, interactive visualization of live cluster state -2. Live Argo CD delivery pipeline view, ordered by sync wave -3. Read-only browser terminal for cluster queries -4. Streaming chat against the `reasoning` model, ChatGPT-style -5. No internal service becomes publicly reachable as a side effect -6. No fabricated statistics anywhere on the site - -### Constraints - -- **Hard capacity ceiling: 8 concurrent LLM sequences** (`--max-num-seqs=4` × 2 replicas). Single GPU. Not horizontally scalable without more hardware. -- **Context ceiling: 16384 tokens** (`--max-model-len`) -- Single GPU node — `worker-1` is a single point of failure for all inference -- Solo operator, part-time -- Cluster is GitOps-managed; every change ships through git → Argo CD (per project hard rules) -- No test framework currently installed in the portfolio repo - -### Forces - -- **Impressiveness vs. attack surface** — the most impressive surfaces (terminal, chat) are the most dangerous -- **Live data vs. information disclosure** — real cluster state is the whole point, and real cluster state is exactly what an attacker wants for reconnaissance -- **Anonymous access vs. abuse** — requiring login kills the portfolio demo; not requiring it exposes 8 GPU slots to the open internet -- **Four surfaces vs. one operator** — four independently-built backends is four times the security review - ---- - -## Decision - -**We will build one read-only aggregator service, `atlas`, that is the sole public entry point to all cluster data, serving four presentation surfaces from one shared in-memory snapshot.** - -### Architecture - -``` -kube API (informers) ─┐ -Prometheus /api/v1 ├──> atlas (Go, ns: portfolio, read-only ServiceAccount) -Argo CD Application CRs┘ │ - ├─ snapshot: in-memory, redacted at write time - ├─ Redis pub/sub (kmsvc-redis) — cross-replica fanout - │ - └─ HTTP surfaces - GET /api/topology + /api/stream (B: cluster topology) - GET /api/delivery + /api/stream (E: Argo CD tree) - POST /api/exec (C: terminal) - POST /api/chat (D: chat) ──> reasoning-predictor.llm-serving -``` - -### Core invariants - -These are the load-bearing decisions. Everything else is implementation detail. - -**I1 — One public hostname, forever.** -`riotpiao.com` is the only name that ever gets a public DNS record. `argocd`, `grafana`, `vault`, `longhorn`, `prometheus`, `minio`, `temporal`, `forgejo`, `portainer` stay NXDOMAIN permanently. Every new public record is a new thing to defend, and `atlas` proxying makes all of them unnecessary. - -**I2 — The browser never talks to an internal API.** -No kube API, no Prometheus, no Argo CD API, no vLLM endpoint is reachable from a browser. `atlas` is the only origin. One choke point for rate limiting, redaction, and audit. - -**I3 — Redaction is an allowlist, never a denylist.** -Fields are serialized by explicit construction into DTO structs. A new field appearing in an upstream CRD cannot leak by default, because nothing copies it. - -**I4 — No free-form string ever reaches an internal system.** -The terminal parses to a closed command enum. Resource names are validated by **membership in the current snapshot**, not by regex. The chat model reads a pre-built snapshot digest and has no tool-calling ability. - -**I5 — Global GPU concurrency is capped below physical capacity.** -Hard semaphore at **6** concurrent chat streams, leaving 2 of 8 sequence slots as operator headroom. Client disconnect cancels the upstream vLLM request immediately. - -### Technology - -| Component | Choice | Rationale | -|---|---|---| -| Aggregator | Go, `client-go` informers | Watch-based, not poll-per-request; N visitors ≠ N API calls | -| Fanout | Redis pub/sub (existing `kmsvc-redis`) | Multi-replica `atlas` shares one snapshot stream; no new infrastructure | -| Transport | SSE | Unidirectional server→client fits every surface, including LLM token streaming | -| Topology + delivery graph | React Flow | Both are graphs; one library, one mental model | -| Resource drill-down | `react-arborist` | Virtualized; the 68-resource apps must not jank | -| Edge protection | Cloudflare WAF + Turnstile | Free; stops scripted abuse before it costs a packet | -| Gateway limits | Kong `rate-limiting`, `policy: redis` | Cluster-wide counters, not per-pod; Kong 3.9 OSS ships it | - -### Implementation strategy — phased, in dependency order - -| Phase | Deliverable | Why this position | -|---|---|---| -| 0 | Unblock deployment | Nothing is visible until this lands | -| 1 | `atlas` core: RBAC, informers, redaction | Every surface depends on it | -| 2 | Surface B — cluster topology | Proves the snapshot + SSE pipeline end to end | -| 3 | Surface E — Argo CD delivery tree | Zero new data sources, zero new attack surface, highest signal | -| 4 | Surface C — read-only terminal | First surface accepting user input | -| 5 | Surface D — chat + full rate limiter | Highest risk, highest cost; ships last, ships with its limiter | - -Surface E precedes C and D deliberately: it reuses Phase 1 data wholesale and is the surface that reads as platform engineering rather than hobby. - -**Timeline**: ~3 weeks part-time. **Responsibility**: solo. - ---- - -## Consequences - -### Positive - -- Fabricated portfolio statistics replaced by live, verifiable data -- One service to secure, rate-limit, audit, and operate instead of four -- Sync-wave DAG makes a real dependency-ordering decision legible to a visitor in seconds -- Informer-based design means visitor traffic does not load the kube API -- Invariant I1 leaves the cluster's public footprint at exactly one hostname -- Existing `InteractiveTerminal.tsx` and `LiveIndicator.tsx` get real backing - -### Negative - -- New production service to build, secure, and maintain — currently zero -- `atlas` becomes a high-value target: it holds cluster-wide read access by design -- 8-slot GPU ceiling means chat will queue under real traffic; a "please wait" queue is a worse first impression than no chat at all -- Public chat on a single GPU node has a genuine cost/abuse tail even behind three tiers of limiting -- Redaction is permanent maintenance: every new field surfaced is a new disclosure review -- Test framework must be added to the repo before any of this can be built TDD-first - -### Neutral - -- Grafana remains for operator use; `atlas` is presentation-only and never replaces it -- Argo CD API is deliberately not used in v1 — `Application` CRs are read via the same informer, so no Argo CD token is ever minted -- `worker-1` remains a single point of failure; this ADR does not change that, only exposes it - ---- - -## Alternatives Considered - -### Alternative 1: Grafana dashboards + public iframe embeds - -**Description**: Build dashboards on existing data, expose a read-only Grafana org, embed in the portfolio. - -**Pros** -- Approximately one day of work -- Zero new services, zero new code, zero new attack surface of our own making -- Data already flows - -**Cons** -- Grafana's design, not the portfolio's — reads as a screenshot, not a product -- Requires making Grafana publicly reachable, violating invariant I1 -- Iframes leak internal metric names, job labels, and namespace structure with no redaction layer available -- No path to the terminal or chat surfaces - -**Why not chosen**: The one-day cost is real, but it forces a second public hostname and surrenders all control over what is disclosed. Grafana has no allowlist redaction model. Kept as the fallback if Phase 1 proves too expensive. - -### Alternative 2: Four independent backends, one per surface - -**Description**: Separate services for topology, delivery, terminal, and chat. - -**Pros** -- Blast radius isolation — a terminal compromise does not reach the chat service -- Independent scaling and deployment -- Aligns with the microservices instinct - -**Cons** -- Four RBAC policies, four redaction layers, four rate limiters, four security reviews -- Four independent informer sets hammering the kube API for the same data -- Solo operator; four services will not receive equal maintenance attention -- Cross-surface consistency (terminal and topology disagreeing about pod state) becomes a real bug class - -**Why not chosen**: Blast-radius isolation is genuine, but every surface needs the *same* read-only snapshot. Duplicating the highest-risk component — cluster-wide read access — four times increases total exposure rather than reducing it. Rejected on the specific ground that the shared component is the dangerous one. - -### Alternative 3: Static snapshot generated at build time - -**Description**: CI job dumps cluster state to JSON at build; site renders it statically. No runtime cluster access at all. - -**Pros** -- Zero runtime attack surface — no live credentials anywhere near the public internet -- Trivially cacheable, effectively free to serve, cannot be DoS'd -- No rate limiting needed - -**Cons** -- Not live; "interactive" degrades to "pre-rendered" -- Kills the terminal and chat surfaces entirely -- The sync-wave cascade animation — the single best demo moment — is impossible -- Data staleness makes the fabricated-statistics problem better but not solved - -**Why not chosen**: Fails requirements 1, 3, and 4. Worth revisiting for the topology surface alone if runtime cost becomes a problem. - -### Alternative 4: Authentik-gated access to all surfaces - -**Description**: Put the existing Authentik SSO in front of the whole visualization. - -**Pros** -- Abuse problem largely disappears; rate limiting becomes a formality -- Authentik is already deployed and working -- Redaction requirements relax substantially for authenticated viewers - -**Cons** -- Nobody creates an account to look at a stranger's homelab — the demo dies -- Defeats the entire purpose of a public portfolio -- Still requires a public Authentik hostname, violating I1 - -**Why not chosen**: Directly contradicts requirement 1. Anonymous access plus Cloudflare Turnstile achieves most of the abuse resistance without the conversion cliff. Reconsider only if abuse proves unmanageable in production. - ---- - -## References - -- Cluster state verified 2026-08-13 via `kubectl` against context `admin@homelab-cluster` -- Public exposure verified 2026-08-13 via `dig` + `curl` against `riotpiao.com` and subdomains -- vLLM engine args: `kubectl get inferenceservice reasoning -n llm-serving -o jsonpath='{.spec}'` -- Superseded: [PLAN.md](../../PLAN.md), [IMPLEMENTATION.md](../../IMPLEMENTATION.md) -- Companion implementation plan: [PLAN-atlas.md](../PLAN-atlas.md) - ---- - -## Review Notes (Before Acceptance) - -**Open questions requiring an answer before Phase 1** - -1. **Two GitOps roots exist.** `homelab-root` reads `git@github.com:Riotpiaole/riotpiao.com.git`. This repo's `infra/argocd-apps.yaml` points at `forgejo.riotpiao.com` — a hostname that does not resolve — and its `portfolio` and `auth-infra` Applications **do not exist in the cluster**. Which repository is authoritative for the portfolio? -2. **Apex returns 403 from the Cloudflare edge.** Is this an absent tunnel public-hostname route, a WAF rule, or a proxied record with no origin? The portfolio cannot ship until this is understood. -3. Is `homarr` still wanted? It is deployed and healthy, but the plan it came from is abandoned. -4. Does the chat surface stay in scope given the 8-slot ceiling, or ship topology + delivery + terminal first and treat chat as a separate decision? - -**Approval** - -- [ ] Architecture — invariants I1–I5 accepted -- [ ] Security — redaction allowlist and rate-limit tiers accepted -- [ ] Scope — four surfaces vs. three diff --git a/package.json b/package.json index 6964471..c4f7ff4 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "next": "^15.5.20", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-markdown": "^10.1.0", "tailwindcss": "^3.4.0" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.20", "@types/node": "20.17.6", "@types/react": "19.2.17", "@typescript-eslint/eslint-plugin": "^8.64.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48d8390..21b289d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,10 +23,16 @@ importers: react-dom: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.17)(react@19.2.7) tailwindcss: specifier: ^3.4.0 version: 3.4.19 devDependencies: + '@tailwindcss/typography': + specifier: ^0.5.20 + version: 0.5.20(tailwindcss@3.4.19) '@types/node': specifier: 20.17.6 version: 20.17.6 @@ -426,18 +432,47 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tailwindcss/typography@0.5.20': + resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.17.6': resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.64.0': resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -716,6 +751,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -771,10 +809,25 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -789,6 +842,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -843,6 +899,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -854,10 +913,17 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1034,10 +1100,16 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1206,12 +1278,21 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1235,10 +1316,19 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -1278,6 +1368,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-document.all@1.0.0: resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} engines: {node: '>= 0.4'} @@ -1298,6 +1391,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -1318,6 +1414,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1430,6 +1530,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1446,10 +1549,97 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1581,6 +1771,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1655,6 +1848,10 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.4: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} @@ -1677,6 +1874,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1692,6 +1892,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -1711,6 +1917,12 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1808,6 +2020,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -1838,6 +2053,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1850,6 +2068,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -1899,6 +2123,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -1957,6 +2187,24 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} @@ -1972,6 +2220,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2016,6 +2270,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2349,13 +2606,38 @@ snapshots: dependencies: tslib: 2.8.1 + '@tailwindcss/typography@0.5.20(tailwindcss@3.4.19)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.19 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json5@0.0.29': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@20.17.6': dependencies: undici-types: 6.19.8 @@ -2364,6 +2646,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2647,6 +2933,8 @@ snapshots: axobject-query@4.1.0: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -2699,11 +2987,21 @@ snapshots: caniuse-lite@1.0.30001806: {} + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -2724,6 +3022,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@4.1.1: {} concat-map@0.0.1: {} @@ -2768,6 +3068,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -2782,9 +3086,15 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + dequal@2.0.3: {} + detect-libc@2.1.2: optional: true + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -2927,8 +3237,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.10 eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 7.1.1(eslint@8.57.1) @@ -2950,7 +3260,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -2961,22 +3271,22 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.64.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -2987,7 +3297,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -3125,8 +3435,12 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + esutils@2.0.3: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3300,12 +3614,38 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + html-url-attributes@3.0.1: {} + ignore@5.3.2: {} ignore@7.0.6: {} @@ -3324,12 +3664,21 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.4 side-channel: 1.1.1 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -3378,6 +3727,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-document.all@1.0.0: dependencies: call-bound: 1.0.4 @@ -3400,6 +3751,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -3413,6 +3766,8 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -3519,6 +3874,8 @@ snapshots: lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3533,8 +3890,230 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3679,6 +4258,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -3723,6 +4312,11 @@ snapshots: postcss: 8.5.19 postcss-selector-parser: 6.1.4 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.4: dependencies: cssesc: 3.0.0 @@ -3750,6 +4344,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -3761,6 +4357,24 @@ snapshots: react-is@16.13.1: {} + react-markdown@10.1.0(@types/react@19.2.17)(react@19.2.7): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.2.17 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.7 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react@19.2.7: {} read-cache@1.0.0: @@ -3791,6 +4405,23 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3936,6 +4567,8 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -3994,6 +4627,11 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4002,6 +4640,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): dependencies: client-only: 0.0.1 @@ -4072,6 +4718,10 @@ snapshots: dependencies: is-number: 7.0.0 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.5.0(typescript@5.8.2): dependencies: typescript: 5.8.2 @@ -4148,6 +4798,39 @@ snapshots: undici-types@6.19.8: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unrs-resolver@1.12.2: dependencies: napi-postinstall: 0.3.4 @@ -4187,6 +4870,16 @@ snapshots: util-deprecate@1.0.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -4245,3 +4938,5 @@ snapshots: zod: 4.4.3 zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/public/hire-me.png b/public/hire-me.png index c72aede..4b48ee2 100644 Binary files a/public/hire-me.png and b/public/hire-me.png differ diff --git a/tailwind.config.ts b/tailwind.config.ts index cd2786d..74661cb 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from 'tailwindcss' +import typography from '@tailwindcss/typography' export default { content: [ @@ -29,5 +30,5 @@ export default { }, }, }, - plugins: [], + plugins: [typography], } satisfies Config