diff --git a/.windsurfrules b/.windsurfrules deleted file mode 100644 index 2117545..0000000 --- a/.windsurfrules +++ /dev/null @@ -1,13 +0,0 @@ -You are a Staff Engineer and an Expert in ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS, NodeJS, Prisma, Postgres, and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix). You are also great at scalling things. You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning. - -- Follow the user’s requirements carefully & to the letter. -- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail. -- Always write correct, best practice, bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines . -- Focus on easy and readability code, over being performant. -- Fully implement all requested functionality. -- Leave NO todo’s, placeholders or missing pieces. -- Ensure code is complete! Verify thoroughly finalised. -- Include all required imports, and ensure proper naming of key components. -- Be concise Minimize any other prose. -- If you think there might not be a correct answer, you say so. -- If you do not know the answer, say so, instead of guessing. diff --git a/apps/marketing/public/hero-dark.png b/apps/marketing/public/hero-dark.png new file mode 100644 index 0000000..cc31395 Binary files /dev/null and b/apps/marketing/public/hero-dark.png differ diff --git a/apps/marketing/public/hero-light.png b/apps/marketing/public/hero-light.png new file mode 100644 index 0000000..04be634 Binary files /dev/null and b/apps/marketing/public/hero-light.png differ diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx index ef38178..78f98ac 100644 --- a/apps/marketing/src/app/layout.tsx +++ b/apps/marketing/src/app/layout.tsx @@ -16,9 +16,35 @@ const jetbrainsMono = JetBrains_Mono({ }); export const metadata: Metadata = { - title: "useSend - Open source email platform", - description: "Open source email platform for everyone", + title: "useSend – Open source email platform", + description: + "Open source email platform for everyone: SMTP, API, editor, analytics.", icons: [{ rel: "icon", url: "/favicon.ico" }], + metadataBase: new URL("https://usesend.com"), + openGraph: { + title: "useSend – Open source email platform", + description: + "Open source email platform for everyone: SMTP, API, editor, analytics.", + url: "https://usesend.com", + siteName: "useSend", + images: [ + { + url: "/logo-squircle.png", + width: 512, + height: 512, + alt: "useSend", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "useSend – Open source email platform", + description: + "Open source email platform for everyone: SMTP, API, editor, analytics.", + images: ["/logo-squircle.png"], + }, }; export default function RootLayout({ @@ -27,11 +53,17 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - + {/* System theme with isolated storage to avoid stale overrides */} + {children} diff --git a/apps/marketing/src/app/page.tsx b/apps/marketing/src/app/page.tsx index c15ca94..21bc427 100644 --- a/apps/marketing/src/app/page.tsx +++ b/apps/marketing/src/app/page.tsx @@ -1,27 +1,532 @@ import Image from "next/image"; +import Link from "next/link"; import { GitHubStarsButton } from "~/components/GitHubStarsButton"; +import { Button } from "@usesend/ui/src/button"; +import { TopNav } from "~/components/TopNav"; +import { FeatureCard } from "~/components/FeatureCard"; +import { FeatureCardPlain } from "~/components/FeatureCardPlain"; +import { CodeBlock } from "@usesend/ui/src/code-block"; + +const REPO = "unsend-dev/unsend"; +const REPO_URL = `https://github.com/${REPO}`; +const APP_URL = "https://app.usesend.com"; export default function Page() { return ( -
-
-
- useSend logo -
-

useSend

-

- Open source email platform for everyone -

-
- -
-
+
+ + + + + + + +
); } + +// (Removed unused SectionHeading component) + +function Hero() { + return ( +
+
+

+ The open source email platform for everyone +

+

+ Send product, transactional and marketing emails.{" "} + + Pay only for what you send + {" "} + and not for storing contacts. Open source and self-hostable. +

+ +
+ + + +
+ +

+ Open source • Self-host in minutes • Free tier +

+ +
+
+
+ useSend product hero + useSend product hero +
+
+
+
+
+ ); +} + +// TopNav moved to a dedicated client component in ~/components/TopNav + +function TrustedBy() { + const featured = [ + { + quote: + "Transitioned recently to open source email sender useSend for our 30k and growing newsletter. It's such a great product and amazing oss experience.", + author: "Marc Seitz", + company: "papermark.com", + image: + "https://pbs.twimg.com/profile_images/1176854646343852032/iYnUXJ-m_400x400.jpg", + }, + { + quote: + "useSend was extremely easy to set up, and I love that it's open source. Koushik has been an absolute awesome person to deal with and helps us with any issues or feedback.", + author: "Tommerty", + company: "doras.to", + image: + "https://cdn.doras.to/doras/user/83bda65b-8d42-4011-9bf0-ab23402776f2-0.890688178917765.webp", + }, + ]; + + const quick = [ + { + quote: "don't sleep on useSend", + author: "shellscape", + company: "jsx.email", + image: + "https://pbs.twimg.com/profile_images/1698447401781022720/b0DZSc_D_400x400.jpg", + }, + { + quote: "Thank you for making useSend!", + author: "Andras Bacsai", + company: "coolify.io", + image: + "https://pbs.twimg.com/profile_images/1884210412524027905/jW4NB4rx_400x400.jpg", + }, + { + quote: "I KNOW WHAT TO DO", + author: "VicVijayakumar", + company: "onetimefax.com", + image: + "https://pbs.twimg.com/profile_images/1665351804685524995/W4BpDx5Z_400x400.jpg", + }, + ]; + + return ( +
+
+
+ Builders and open source teams love + useSend +
+ + {/* Top: 2 larger testimonials */} +
+ {featured.map((t) => ( +
+
+ {t.quote} +
+
+ {`${t.author} +
+ {t.author} + + {" "} + — {t.company} + {" "} +
+
+
+ ))} +
+ + {/* Bottom: 3 multi-line testimonials (same style as top) */} +
+ {quick.map((t) => ( +
+
+ {t.quote} +
+
+ {`${t.author} +
+ {t.author} + + {" "} + — {t.company} + +
+
+
+ ))} +
+
+
+ ); +} + +function Features() { + // Top: 2 cards (with image area) — Analytics, Editor + const top = [ + { + key: "feature-analytics", + title: "Analytics", + content: + "Track deliveries, opens, clicks, bounces and unsubscribes in real time with a simple, searchable log. Filter by domain, status, api key and export them. Track which campaigns perform best.", + imageSrc: "", // add an image like "/analytics.png" + }, + { + key: "feature-editor", + title: "Marketing Email Editor", + content: + "Design beautiful campaigns without code using a visual, notion like WYSIWYG editor that works in major email clients. Reuse templates and brand styles, and personalize with variables.", + imageSrc: "", // add an image like "/editor.png" + }, + ]; + + // Bottom: 3 cards (no images) — Contact Management, Suppression List, SMTP Relay Service + const bottom = [ + { + key: "feature-contacts", + title: "Contact Management", + content: + "Manage contacts, lists, and consent in one place. Import and export easily, keep per-list subscription status. Contacts are automatically updated from bounces and complaints.", + }, + { + key: "feature-suppression", + title: "Suppression List", + content: + "Prevent accidental sends. Automatically populated from bounces and complaints, and manage via import/export or API. Works with transactional and marketing emails.", + }, + { + key: "feature-smtp", + title: "SMTP Relay", + content: + "Drop-in SMTP relay that works with any app or framework. Do not get vendor lock-in. Comes in handy with services like Supabase", + }, + ]; + + return ( +
+
+
+
+ Features +
+
+ + {/* Top row: 2 side-by-side cards with images */} +
+ {top.map((f) => ( + + ))} +
+ + {/* Bottom row: 3 cards without images */} +
+ {bottom.map((f) => ( + + ))} +
+
+
+ ); +} + +function CodeExample() { + const code = `import { Unsend } from "@unsend/sdk"; + +const unsend = new Unsend({ apiKey: process.env.UNSEND_API_KEY! }); + +await unsend.emails.send({ + from: "hi@example.com", + to: "you@company.com", + subject: "Welcome to useSend", + template: "welcome", // or html/text + data: { name: "Ada" }, +});`; + + return ( +
+
+
+
+ Developers +
+

+ Typed SDKs and simple APIs, so you can focus on product not + plumbing. +

+
+ +
+
TypeScript
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+ ); +} + +function Pricing() { + const freePerks = [ + "Send up to 3000 emails per month", + "Send up to 100 emails per day", + "Can have 1 contact book", + "Can have 1 domain", + "Can have 1 team member", + ]; + + const paidPerks = [ + "$10 monthly usage credits", + "Send transactional emails at $0.0004 per email", + "Send marketing emails at $0.001 per email", + "Can have unlimited contact books", + "Can have unlimited domains", + "Can have unlimited team members", + ]; + + return ( +
+
+
+
+ PRICING +
+

+ pay for what you use, not for storing contacts +

+
+ +
+ + +
+
+
+ ); +} + +type PricingCardProps = { + title: string; + price: string; + note: string; + perks: string[]; +}; + +function PricingCard({ title, price, note, perks }: PricingCardProps) { + return ( +
+
+
+

{title}

+
{price}
+
{note}
+
    + {perks.map((perk) => ( +
  • + + {perk} +
  • + ))} +
+ +
+
+
+ ); +} + +function About() { + return ( +
+
+
+
+ About +
+
+ +
+

+ As most of email products out there, useSend also uses Amazon SES + under the hood to send emails. We provide an open and alternative + way to send emails reliably and cheaply with a great dashboard. +

+

+ useSend is bootstrapped and funded by the cloud offering and + sponsors. If you self host useSend, please consider{" "} + + sponsoring us + + . +

+
+
+
+ ); +} + +// FAQ section removed per request + +function Footer() { + return ( + + ); +} + +// Minimal inline icons (stroke-based, sleek) +function CheckIcon({ className = "" }: { className?: string }) { + return ( + + ); +} diff --git a/apps/marketing/src/app/privacy/page.tsx b/apps/marketing/src/app/privacy/page.tsx new file mode 100644 index 0000000..e411680 --- /dev/null +++ b/apps/marketing/src/app/privacy/page.tsx @@ -0,0 +1,197 @@ +import type { Metadata } from "next"; +import { TopNav } from "~/components/TopNav"; + +export const metadata: Metadata = { + title: "Privacy Policy – useSend", + description: "Simple privacy policy for the useSend marketing site.", +}; + +export default function PrivacyPage() { + return ( +
+ +
+

+ Privacy Policy +

+

+ This Privacy Policy explains how we collect, use, and share + information when you visit or interact with the useSend marketing + website at usesend.com. It also summarizes the limited information we + process when you sign up for our product and receive transactional or + occasional marketing emails. +

+ +
+

Who We Are

+

+ useSend ("we", "us") operates the marketing website at + usesend.com. The marketing + site is hosted on Vercel. Our application is hosted on Railway. We + are the controller of the information described in this policy for + the marketing site. If you have questions about this policy or your + data, contact us at + + hey@usesend.com + + . +

+
+ +
+

What We Collect

+
    +
  • + + Usage and device data (marketing site): + {" "} + We use Simple Analytics to understand overall traffic and usage + patterns (e.g., pages visited, referrers, device type). Simple + Analytics is a privacy‑friendly analytics provider and does not + use cookies for tracking. Data is aggregated and not used to + identify you. +
  • +
  • + Server and security logs:{" "} + Our hosting providers (Vercel for the marketing site; Railway for + the app) may process IP addresses and basic request metadata + transiently for security, reliability, and debugging. +
  • +
  • + + Account and email data (product): + {" "} + If you sign up for useSend, we process your account information + and send transactional emails. If you opt in, we may also send + occasional marketing emails. You can unsubscribe at any time via + the link in those emails. +
  • +
+
+ +
+

How We Use Information

+
    +
  • Operate, secure, and maintain the marketing site and app.
  • +
  • + Understand aggregated usage to improve performance and content. +
  • +
  • + Deliver transactional emails related to your account or use. +
  • +
  • Send occasional marketing emails to subscribers who opt in.
  • +
  • Comply with legal obligations and enforce our terms.
  • +
+
+ +
+

Legal Bases

+

+ Where applicable (e.g., in the EEA/UK), we rely on legitimate + interests to operate and secure our services and to measure + aggregated site usage, and on your consent for marketing emails. We + may rely on contract and legal obligation where relevant. +

+
+ +
+

Sharing and Processors

+

+ We share information with service providers who process data on our + behalf, including: +

+
    +
  • + Hosting: Vercel + (marketing site) and Railway (application) for serving content, + networking, and security. +
  • +
  • + Analytics: Simple + Analytics for aggregated, privacy‑friendly usage metrics on the + marketing site. +
  • +
  • + Email delivery: We send + transactional emails and, for subscribers who opt in, occasional + marketing emails. +
  • +
+

+ We do not sell your personal information. We may disclose + information if required by law or to protect our rights, users, or + the public. +

+
+ +
+

Retention

+

+ We retain information only for as long as necessary to fulfill the + purposes described in this policy, including security, analytics, + and legal compliance. Aggregated analytics do not identify + individuals. +

+
+ +
+

International Transfers

+

+ Our providers may process data in locations outside of your country + of residence. Where required, we implement appropriate safeguards + for cross‑border transfers. +

+
+ +
+

Your Rights

+

+ Depending on your location, you may have rights to access, correct, + delete, or export your information; to object to or restrict certain + processing; and to withdraw consent where processing is based on + consent. To exercise these rights, contact us using the details on + our website. We may ask you to verify your identity before acting on + a request. +

+
+ +
+

Contact

+

+ For privacy requests or questions, email us at + + hey@usesend.com + + . +

+
+ +
+

Children

+

+ Our services are not directed to children, and we do not knowingly + collect personal information from children. +

+
+ +
+

Changes

+

+ We may update this policy from time to time. The "Last updated" date + below reflects the most recent changes. +

+
+ +

+ Last updated: {new Date().toLocaleDateString()} +

+
+
+ ); +} diff --git a/apps/marketing/src/app/terms/page.tsx b/apps/marketing/src/app/terms/page.tsx new file mode 100644 index 0000000..39828f0 --- /dev/null +++ b/apps/marketing/src/app/terms/page.tsx @@ -0,0 +1,124 @@ +import type { Metadata } from "next"; +import { TopNav } from "~/components/TopNav"; + +export const metadata: Metadata = { + title: "Terms of Service – useSend", + description: "Terms governing use of the useSend website and product.", +}; + +export default function TermsPage() { + return ( +
+ +
+

Terms of Service

+

+ These Terms of Service ("Terms") govern your access to and use of the + useSend marketing website at usesend.com and the useSend application. + By accessing or using our site or product, you agree to be bound by + these Terms. +

+ +
+

Eligibility & Accounts

+

+ You may use the site and product only if you can form a binding + contract with useSend and are not barred from doing so under any + applicable laws. You are responsible for maintaining the security of + your account credentials and for all activity under your account. +

+
+ +
+

Acceptable Use

+

+ You agree not to misuse the site or product. Prohibited conduct + includes, without limitation: +

+
    +
  • Violating any applicable laws or regulations.
  • +
  • Infringing the rights of others or violating their privacy.
  • +
  • Attempting to interfere with or disrupt the services.
  • +
  • + Uploading or transmitting malicious code, spam, or prohibited + content. +
  • +
+
+ +
+

Intellectual Property

+

+ Content on the site, including trademarks, logos, text, and + graphics, is owned by or licensed to useSend and protected by + intellectual property laws. You may not use our marks without our + prior written permission. +

+
+ +
+

Third‑Party Links

+

+ The site may contain links to third‑party websites or services we do + not control. We are not responsible for their content or practices. +

+
+ +
+

Disclaimer

+

+ The site is provided on an "as is" and "as available" basis without + warranties of any kind, express or implied. +

+
+ +
+

Limitation of Liability

+

+ To the fullest extent permitted by law, useSend shall not be liable + for any indirect, incidental, special, consequential or punitive + damages, or any loss of profits or revenues. +

+
+ +
+

Indemnification

+

+ You agree to indemnify and hold harmless useSend from any claims, + damages, liabilities, and expenses arising out of your use of the + site or product or your violation of these Terms. +

+
+ +
+

Changes & Availability

+

+ We may modify these Terms and update the site or product at any + time. Changes are effective when posted. We may suspend or + discontinue the site or product in whole or in part. +

+
+ +
+

Governing Law

+

+ These Terms are governed by applicable laws without regard to + conflict‑of‑law principles. Where required, disputes will be subject + to the jurisdiction of competent courts in your place of residence + or as otherwise mandated by law. +

+
+ +
+

Contact

+

+ Questions about these Terms? Contact us at + hey@usesend.com. +

+
+ +

Last updated: {new Date().toLocaleDateString()}

+
+
+ ); +} diff --git a/apps/marketing/src/components/FeatureCard.tsx b/apps/marketing/src/components/FeatureCard.tsx new file mode 100644 index 0000000..b6271a4 --- /dev/null +++ b/apps/marketing/src/components/FeatureCard.tsx @@ -0,0 +1,61 @@ +"use client"; + +import Image from "next/image"; + +export function FeatureCard({ + title, + content, + imageSrc, +}: { + title?: string; + content?: string; + imageSrc?: string; +}) { + return ( +
+
+
+
+ {imageSrc ? ( + {title + ) : ( + <> + Feature image + Feature image + + )} +
+ +
+

+ {title || ""} +

+ {content ? ( +

{content}

+ ) : ( +
+ )} +
+
+
+
+ ); +} diff --git a/apps/marketing/src/components/FeatureCardPlain.tsx b/apps/marketing/src/components/FeatureCardPlain.tsx new file mode 100644 index 0000000..b62d638 --- /dev/null +++ b/apps/marketing/src/components/FeatureCardPlain.tsx @@ -0,0 +1,28 @@ +"use client"; + +export function FeatureCardPlain({ + title, + content, +}: { + title?: string; + content?: string; +}) { + return ( +
+
+
+
+

+ {title || ""} +

+ {content ? ( +

{content}

+ ) : ( +
+ )} +
+
+
+
+ ); +} diff --git a/apps/marketing/src/components/GitHubStarsButton.tsx b/apps/marketing/src/components/GitHubStarsButton.tsx index 6239f03..4f1dba0 100644 --- a/apps/marketing/src/components/GitHubStarsButton.tsx +++ b/apps/marketing/src/components/GitHubStarsButton.tsx @@ -1,42 +1,53 @@ -"use client"; - -import { useEffect, useState } from "react"; import { Button } from "@usesend/ui/src/button"; const REPO = "unsend-dev/unsend"; const REPO_URL = `https://github.com/${REPO}`; const API_URL = `https://api.github.com/repos/${REPO}`; +const REVALIDATE_SECONDS = 60 * 60 * 24 * 7; // 7 days -export function GitHubStarsButton() { - const [stars, setStars] = useState(null); - - useEffect(() => { - let cancelled = false; - async function load() { - try { - const res = await fetch(API_URL, { - headers: { - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - cache: "no-store", - }); - if (!res.ok) return; - const json = await res.json(); - if (!cancelled && typeof json.stargazers_count === "number") { - setStars(json.stargazers_count); - } - } catch (err) { - // ignore network errors; keep placeholder - } +function formatCompact(n: number): string { + if (n < 1000) return n.toLocaleString(); + const units = [ + { v: 1_000_000_000, s: " B" }, + { v: 1_000_000, s: " M" }, + { v: 1_000, s: " K" }, + ]; + for (const u of units) { + if (n >= u.v) { + const num = n / u.v; + const rounded = Math.round(num * 10) / 10; // 1 decimal + const str = rounded.toFixed(1).replace(/\.0$/, ""); + return str + u.s; } - load(); - return () => { - cancelled = true; - }; - }, []); + } + return n.toLocaleString(); +} - const formatted = stars?.toLocaleString() ?? "—"; +export async function GitHubStarsButton() { + const headers: Record = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "usesend-marketing", + }; + if (process.env.GITHUB_TOKEN) + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + + let stars: number | null = null; + try { + const res = await fetch(API_URL, { + headers, + next: { revalidate: REVALIDATE_SECONDS }, + }); + if (res.ok) { + const json = (await res.json()) as { stargazers_count?: number }; + if (typeof json.stargazers_count === "number") + stars = json.stargazers_count; + } + } catch { + // ignore network errors; show placeholder + } + + const formatted = stars == null ? "—" : formatCompact(stars); return ( ); diff --git a/apps/marketing/src/components/TopNav.tsx b/apps/marketing/src/components/TopNav.tsx new file mode 100644 index 0000000..1e1d1fa --- /dev/null +++ b/apps/marketing/src/components/TopNav.tsx @@ -0,0 +1,108 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState } from "react"; +import { Button } from "@usesend/ui/src/button"; + +const REPO = "unsend-dev/unsend"; +const REPO_URL = `https://github.com/${REPO}`; +const APP_URL = "https://app.usesend.com"; + +export function TopNav() { + const [open, setOpen] = useState(false); + const pathname = usePathname(); + const isHome = pathname === "/"; + const pricingHref = isHome ? "#pricing" : "/#pricing"; + + return ( +
+
+ + useSend + useSend + + + {/* Desktop nav */} + + + {/* Mobile hamburger */} + +
+ + {/* Mobile menu panel */} + {open ? ( + + ) : null} +
+ ); +} diff --git a/apps/web/prisma/seed_dashboard.sql b/apps/web/prisma/seed_dashboard.sql new file mode 100644 index 0000000..113c66e --- /dev/null +++ b/apps/web/prisma/seed_dashboard.sql @@ -0,0 +1,253 @@ +-- Seed data for a photogenic dashboard screenshot +-- Postgres SQL compatible with the Prisma schema in apps/web/prisma/schema.prisma +-- Usage: +-- psql "$DATABASE_URL" -f apps/web/prisma/seed_dashboard.sql + +BEGIN; + +-- 1) Create a team (allow duplicates for demo runs) +INSERT INTO "Team" ("name", "plan", "isActive", "apiRateLimit", "createdAt", "updatedAt") +VALUES ('Acme Inc', 'BASIC'::"Plan", TRUE, 10, NOW(), NOW()); + +-- 2) Ensure a verified domain for that team (upsert by unique name) +INSERT INTO "Domain" ( + "name", "teamId", "status", "region", "clickTracking", "openTracking", + "publicKey", "dkimSelector", "dmarcAdded", "createdAt", "updatedAt" +) +SELECT + 'mail.acme.test', id, 'SUCCESS'::"DomainStatus", 'us-east-1', TRUE, TRUE, + 'PUBLIC_KEY_SAMPLE', 'usesend', TRUE, NOW(), NOW() +FROM "Team" +WHERE "name" = 'Acme Inc' +ORDER BY id DESC +LIMIT 1 +ON CONFLICT ("name") DO UPDATE +SET "updatedAt" = NOW(); + +-- 3) Cumulated totals to power headline KPIs (idempotent) +INSERT INTO "CumulatedMetrics" ("teamId", "domainId", "delivered", "hardBounced", "complained") +SELECT d."teamId", d.id, 125000, 750, 180 +FROM "Domain" d +WHERE d."name" = 'mail.acme.test' +ON CONFLICT ("teamId", "domainId") DO UPDATE +SET "delivered" = EXCLUDED."delivered", + "hardBounced" = EXCLUDED."hardBounced", + "complained" = EXCLUDED."complained"; + +-- 4) 14 days of daily usage (TRANSACTIONAL) with internally consistent metrics +INSERT INTO "DailyEmailUsage" ( + "teamId", "domainId", "date", "type", + "sent", "delivered", "opened", "clicked", + "bounced", "complained", "hardBounced", + "createdAt", "updatedAt" +) +SELECT + d."teamId", + d.id AS "domainId", + TO_CHAR((CURRENT_DATE - i)::date, 'YYYY-MM-DD') AS date, + 'TRANSACTIONAL'::"EmailUsageType" AS type, + s.sent, + s.delivered, + s.opened, + s.clicked, + s.bounced, + s.complained, + s.hardBounced, + NOW() - (i || ' days')::interval AS createdAt, + NOW() - (i || ' days')::interval AS updatedAt +FROM ( + SELECT id, "teamId" FROM "Domain" WHERE "name" = 'mail.acme.test' LIMIT 1 +) d, +generate_series(0, 13) AS i, +LATERAL ( + WITH season AS ( + SELECT + CASE EXTRACT(DOW FROM (CURRENT_DATE - i)) + WHEN 0 THEN 0.80 -- Sunday + WHEN 6 THEN 0.85 -- Saturday + ELSE 1.00 -- Weekday + END AS dow_factor, + (1.0 + 0.06 * sin(2*pi() * ((13 - i)::float / 7.0))) AS wave + ), base AS ( + SELECT + GREATEST(700, round((900 * dow_factor * wave) + 30*random()))::int AS sent_base + FROM season + ), parts AS ( + SELECT + sent_base AS sent, + (0.006 + random()*0.007) AS bounce_frac + FROM base + ) + SELECT + sent, + (sent - floor(sent*bounce_frac)::int) AS delivered, + floor((sent - floor(sent*bounce_frac)::int) * (0.60 + random()*0.15))::int AS opened, + LEAST( + floor((sent - floor(sent*bounce_frac)::int) * (0.18 + random()*0.12))::int, + floor((sent - floor(sent*bounce_frac)::int) * 0.95)::int + ) AS clicked, + floor(sent*bounce_frac)::int AS bounced, + CASE WHEN random() < ((sent - floor(sent*bounce_frac)::int) * (0.0001 + random()*0.0002)) + THEN 1 ELSE 0 END AS complained, + floor(floor(sent*bounce_frac)::int * (0.60 + random()*0.25))::int AS hardBounced + FROM parts +) s +ON CONFLICT ("teamId", "domainId", "date", "type") DO NOTHING; + +-- 5) 14 days of daily usage (MARKETING) with consistent metrics +INSERT INTO "DailyEmailUsage" ( + "teamId", "domainId", "date", "type", + "sent", "delivered", "opened", "clicked", + "bounced", "complained", "hardBounced", + "createdAt", "updatedAt" +) +SELECT + d."teamId", + d.id AS "domainId", + TO_CHAR((CURRENT_DATE - i)::date, 'YYYY-MM-DD') AS date, + 'MARKETING'::"EmailUsageType" AS type, + s.sent, + s.delivered, + s.opened, + s.clicked, + s.bounced, + s.complained, + s.hardBounced, + NOW() - (i || ' days')::interval AS createdAt, + NOW() - (i || ' days')::interval AS updatedAt +FROM ( + SELECT id, "teamId" FROM "Domain" WHERE "name" = 'mail.acme.test' LIMIT 1 +) d, +generate_series(0, 13) AS i, +LATERAL ( + WITH season AS ( + SELECT + CASE EXTRACT(DOW FROM (CURRENT_DATE - i)) + WHEN 0 THEN 0.75 -- Sunday + WHEN 6 THEN 0.85 -- Saturday + ELSE 1.00 + END AS dow_factor, + (1.0 + 0.08 * sin(2*pi() * ((13 - i)::float / 7.0) + 0.6)) AS wave + ), base AS ( + SELECT + GREATEST(500, round((700 * dow_factor * wave) + 40*random()))::int AS sent_base + FROM season + ), parts AS ( + SELECT + sent_base AS sent, + (0.008 + random()*0.010) AS bounce_frac + FROM base + ) + SELECT + sent, + (sent - floor(sent*bounce_frac)::int) AS delivered, + floor((sent - floor(sent*bounce_frac)::int) * (0.47 + random()*0.18))::int AS opened, + LEAST( + floor((sent - floor(sent*bounce_frac)::int) * (0.14 + random()*0.10))::int, + floor((sent - floor(sent*bounce_frac)::int) * 0.90)::int + ) AS clicked, + floor(sent*bounce_frac)::int AS bounced, + CASE WHEN random() < ((sent - floor(sent*bounce_frac)::int) * (0.00005 + random()*0.00020)) + THEN 1 ELSE 0 END AS complained, + floor(floor(sent*bounce_frac)::int * (0.60 + random()*0.25))::int AS hardBounced + FROM parts +) s +ON CONFLICT ("teamId", "domainId", "date", "type") DO NOTHING; + +-- 6) A recent campaign with healthy open/click, realistic bounce/complaint rates +INSERT INTO "Campaign" ( + id, "name", "teamId", "from", cc, bcc, "replyTo", "domainId", subject, "previewText", + html, content, "contactBookId", total, sent, delivered, opened, clicked, unsubscribed, + bounced, "hardBounced", complained, status, "createdAt", "updatedAt" +) +SELECT + ('cmp_' || substr(md5(random()::text), 1, 12)) AS id, + 'August Promo – Back to Business' AS name, + d."teamId" AS teamId, + 'Acme ' AS "from", + ARRAY[]::text[] AS cc, + ARRAY[]::text[] AS bcc, + ARRAY['support@acme.test']::text[] AS "replyTo", + d.id AS "domainId", + 'Save 30% on Pro' AS subject, + 'Limited-time offer for power users' AS "previewText", + '

Upgrade to Pro

Unlock advanced features.

' AS html, + NULL AS content, + NULL AS "contactBookId", + 25000 AS total, + 24800 AS sent, + 24500 AS delivered, + 13600 AS opened, + 6200 AS clicked, + 90 AS unsubscribed, + 300 AS bounced, + 220 AS "hardBounced", + 5 AS complained, + 'SENT'::"CampaignStatus" AS status, + NOW() - interval '2 days' AS "createdAt", + NOW() - interval '1 days' AS "updatedAt" +FROM "Domain" d +JOIN "Team" t2 ON t2.id = d."teamId" +WHERE d."name" = 'mail.acme.test' +ORDER BY d.id DESC +LIMIT 1; + +-- 7) A handful of recent emails (varied statuses) to make lists look alive +DO $$ +DECLARE + v_team_id INT; + v_domain_id INT; +BEGIN + SELECT t.id, d.id INTO v_team_id, v_domain_id + FROM "Team" t + JOIN "Domain" d ON d."teamId" = t.id + WHERE d."name" = 'mail.acme.test' + ORDER BY d.id DESC + LIMIT 1; + + -- delivered/opened + INSERT INTO "Email" ( + id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html, + "latestStatus", "teamId", "domainId", "createdAt", "updatedAt" + ) VALUES ( + ('eml_' || substr(md5(random()::text), 1, 12)), NULL, + 'Acme ', ARRAY['user1@example.com'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[], + 'Welcome to Acme', 'Plaintext welcome', '

Welcome!

', 'OPENED'::"EmailStatus", + v_team_id, v_domain_id, NOW() - interval '4 hours', NOW() - interval '3 hours' + ); + + -- clicked + INSERT INTO "Email" ( + id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html, + "latestStatus", "teamId", "domainId", "createdAt", "updatedAt" + ) VALUES ( + ('eml_' || substr(md5(random()::text), 1, 12)), NULL, + 'Acme ', ARRAY['user2@example.com'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[], + 'Get Started Guide', NULL, '

Click to learn more

', 'CLICKED'::"EmailStatus", + v_team_id, v_domain_id, NOW() - interval '2 hours', NOW() - interval '1 hour' + ); + + -- bounced + INSERT INTO "Email" ( + id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html, + "latestStatus", "teamId", "domainId", "createdAt", "updatedAt" + ) VALUES ( + ('eml_' || substr(md5(random()::text), 1, 12)), NULL, + 'Acme ', ARRAY['bad@invalid.test'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[], + 'Delivery failed notice', NULL, '

Delivery failed

', 'BOUNCED'::"EmailStatus", + v_team_id, v_domain_id, NOW() - interval '6 hours', NOW() - interval '5 hours' + ); + + -- complained + INSERT INTO "Email" ( + id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html, + "latestStatus", "teamId", "domainId", "createdAt", "updatedAt" + ) VALUES ( + ('eml_' || substr(md5(random()::text), 1, 12)), NULL, + 'Acme ', ARRAY['too.sensitive@example.com'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[], + 'Feedback request', 'We value your feedback', '

Please reply

', 'COMPLAINED'::"EmailStatus", + v_team_id, v_domain_id, NOW() - interval '9 hours', NOW() - interval '8 hours' + ); +END $$; + +COMMIT; diff --git a/apps/web/src/app/(dashboard)/dashboard/email-chart.tsx b/apps/web/src/app/(dashboard)/dashboard/email-chart.tsx index d6c3bbd..1a2f10a 100644 --- a/apps/web/src/app/(dashboard)/dashboard/email-chart.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/email-chart.tsx @@ -2,6 +2,7 @@ import React from "react"; import { BarChart, Bar, + Rectangle, XAxis, YAxis, Tooltip, @@ -22,6 +23,37 @@ interface EmailChartProps { domain: string | null; } +const STACK_ORDER: string[] = [ + "delivered", + "bounced", + "complained", + "opened", + "clicked", +] as const; + +type StackKey = (typeof STACK_ORDER)[number]; + +function createRoundedTopShape(currentKey: StackKey) { + const currentIndex = STACK_ORDER.indexOf(currentKey); + return (props: any) => { + const payload = props.payload as + | Partial> + | undefined; + let hasAbove = false; + for (let i = currentIndex + 1; i < STACK_ORDER.length; i++) { + const key = STACK_ORDER[i]; + const val = key ? (payload?.[key] ?? 0) : 0; + if (val > 0) { + hasAbove = true; + break; + } + } + + const radius = hasAbove ? [0, 0, 0, 0] : [2.5, 2.5, 0, 0]; + return ; + }; +} + export default function EmailChart({ days, domain }: EmailChartProps) { const domainId = domain ? Number(domain) : undefined; const statusQuery = api.dashboard.emailTimeSeries.useQuery({ @@ -197,15 +229,32 @@ export default function EmailChart({ days, domain }: EmailChartProps) { dataKey="delivered" stackId="a" fill={currentColors.delivered} + shape={createRoundedTopShape("delivered")} + /> + - + + - - diff --git a/apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx b/apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx index 143df20..a60d243 100644 --- a/apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx @@ -94,7 +94,7 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
-
Bounce Rate
+
Bounce Rate
@@ -108,7 +108,7 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
-
+
{metrics?.bounceRate.toFixed(2)}%
@@ -242,7 +242,9 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
-
Complaint Rate
+
+ Complaint Rate +
@@ -254,7 +256,7 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
-
+
{metrics?.complaintRate.toFixed(2)}%
diff --git a/apps/web/src/components/AppSideBar.tsx b/apps/web/src/components/AppSideBar.tsx index e643a90..926eded 100644 --- a/apps/web/src/components/AppSideBar.tsx +++ b/apps/web/src/components/AppSideBar.tsx @@ -39,6 +39,7 @@ import { isSelfHosted } from "~/utils/common"; import { usePathname } from "next/navigation"; import { Badge } from "@usesend/ui/src/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@usesend/ui/src/avatar"; +import Image from "next/image"; import { DropdownMenu, DropdownMenuContent, diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts index 2600488..dd03c27 100644 --- a/apps/web/src/server/auth.ts +++ b/apps/web/src/server/auth.ts @@ -55,9 +55,9 @@ function getProviders() { allowDangerousEmailAccountLinking: true, authorization: { params: { - scope: 'read:user user:email' - } - } + scope: "read:user user:email", + }, + }, }) ); } diff --git a/packages/tailwind-config/tailwind.config.ts b/packages/tailwind-config/tailwind.config.ts index ad2ddc9..b313b04 100644 --- a/packages/tailwind-config/tailwind.config.ts +++ b/packages/tailwind-config/tailwind.config.ts @@ -107,6 +107,9 @@ const config = { gray: { DEFAULT: "hsl(var(--gray))", }, + "primary-light": { + DEFAULT: "hsl(var(--primary-light))", + }, }, borderRadius: { lg: "var(--radius)", diff --git a/packages/ui/code-theme.ts b/packages/ui/code-theme.ts deleted file mode 100644 index acc9bf7..0000000 --- a/packages/ui/code-theme.ts +++ /dev/null @@ -1,148 +0,0 @@ -const codeTheme: { - [key: string]: React.CSSProperties; -} = { - hljs: { - display: "block", - overflowX: "auto", - padding: "0.5em", - background: "#070808", - color: "#d6deeb", - }, - "hljs-keyword": { - color: "#c792ea", - fontStyle: "italic", - }, - "hljs-built_in": { - color: "#addb67", - fontStyle: "italic", - }, - "hljs-type": { - color: "#82aaff", - }, - "hljs-literal": { - color: "#ff5874", - }, - "hljs-number": { - color: "#F78C6C", - }, - "hljs-regexp": { - color: "#5ca7e4", - }, - "hljs-string": { - color: "#ecc48d", - }, - "hljs-subst": { - color: "#d3423e", - }, - "hljs-symbol": { - color: "#82aaff", - }, - "hljs-class": { - color: "#ffcb8b", - }, - "hljs-function": { - color: "#82AAFF", - }, - "hljs-title": { - color: "#DCDCAA", - fontStyle: "italic", - }, - "hljs-params": { - color: "#7fdbca", - }, - "hljs-comment": { - color: "#637777", - fontStyle: "italic", - }, - "hljs-doctag": { - color: "#7fdbca", - }, - "hljs-meta": { - color: "#82aaff", - }, - "hljs-meta-keyword": { - color: "#82aaff", - }, - "hljs-meta-string": { - color: "#ecc48d", - }, - "hljs-section": { - color: "#82b1ff", - }, - "hljs-tag": { - color: "#7fdbca", - }, - "hljs-name": { - color: "#7fdbca", - }, - "hljs-builtin-name": { - color: "#7fdbca", - }, - "hljs-attr": { - color: "#7fdbca", - }, - "hljs-attribute": { - color: "#80cbc4", - }, - "hljs-variable": { - color: "#addb67", - }, - "hljs-bullet": { - color: "#d9f5dd", - }, - "hljs-code": { - color: "#80CBC4", - }, - "hljs-emphasis": { - color: "#c792ea", - fontStyle: "italic", - }, - "hljs-strong": { - color: "#addb67", - fontWeight: "bold", - }, - "hljs-formula": { - color: "#c792ea", - }, - "hljs-link": { - color: "#ff869a", - }, - "hljs-quote": { - color: "#697098", - fontStyle: "italic", - }, - "hljs-selector-tag": { - color: "#ff6363", - }, - "hljs-selector-id": { - color: "#fad430", - }, - "hljs-selector-class": { - color: "#addb67", - fontStyle: "italic", - }, - "hljs-selector-attr": { - color: "#c792ea", - fontStyle: "italic", - }, - "hljs-selector-pseudo": { - color: "#c792ea", - fontStyle: "italic", - }, - "hljs-template-tag": { - color: "#c792ea", - }, - "hljs-template-variable": { - color: "#addb67", - }, - "hljs-addition": { - color: "#addb67ff", - fontStyle: "italic", - }, - "hljs-deletion": { - color: "#EF535090", - fontStyle: "italic", - }, -}; - -export default codeTheme; diff --git a/packages/ui/package.json b/packages/ui/package.json index 2249466..067fc3d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,7 +16,6 @@ "@types/node": "^22.15.2", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", - "@types/react-syntax-highlighter": "^15.5.13", "@usesend/eslint-config": "workspace:*", "@usesend/tailwind-config": "workspace:*", "@usesend/typescript-config": "workspace:*", @@ -48,16 +47,17 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "framer-motion": "^12.9.2", + "hast-util-to-jsx-runtime": "^2.3.6", "input-otp": "^1.4.2", "lucide-react": "^0.503.0", "next-themes": "^0.4.6", "pnpm": "^10.9.0", "react-hook-form": "^7.56.1", - "react-syntax-highlighter": "^15.6.1", "recharts": "^2.15.3", + "shiki": "^3.3.0", "sonner": "^2.0.3", "tailwind-merge": "^3.2.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.3" } -} +} \ No newline at end of file diff --git a/packages/ui/src/code-block.tsx b/packages/ui/src/code-block.tsx new file mode 100644 index 0000000..e5ea79f --- /dev/null +++ b/packages/ui/src/code-block.tsx @@ -0,0 +1,36 @@ +import { BundledLanguage, codeToHast } from "shiki"; +import { toJsxRuntime } from "hast-util-to-jsx-runtime"; +import { Fragment } from "react"; +import { jsx, jsxs } from "react/jsx-runtime"; +import { cn } from "../lib/utils"; + +interface Props { + children: string; + lang: BundledLanguage; + className?: string; +} + +export async function CodeBlock(props: Props) { + const out = await codeToHast(props.children, { + lang: props.lang, + themes: { + dark: "catppuccin-mocha", + light: "catppuccin-latte", + }, + }); + + return toJsxRuntime(out, { + Fragment, + jsx, + jsxs, + components: { + // your custom `pre` element + pre: (nodeProps) => ( +
+      ),
+    },
+  }) as React.JSX.Element;
+}
diff --git a/packages/ui/src/code.tsx b/packages/ui/src/code.tsx
deleted file mode 100644
index 701d7be..0000000
--- a/packages/ui/src/code.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
-import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
-import ruby from "react-syntax-highlighter/dist/esm/languages/hljs/ruby";
-import php from "react-syntax-highlighter/dist/esm/languages/hljs/php";
-import python from "react-syntax-highlighter/dist/esm/languages/hljs/python";
-// import { nightOwl } from "react-syntax-highlighter/dist/esm/styles/hljs";
-import codeTheme from "../code-theme";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
-import { Button } from "./button";
-import { ClipboardCopy, Check } from "lucide-react";
-import { useState } from "react";
-import { cn } from "../lib/utils";
-
-export type Language = "js" | "ruby" | "php" | "python" | "curl";
-
-export type CodeBlock = {
-  language: Language;
-  title?: string;
-  code: string;
-};
-
-type CodeProps = {
-  codeBlocks: CodeBlock[];
-  codeClassName?: string;
-};
-
-SyntaxHighlighter.registerLanguage("js", js);
-SyntaxHighlighter.registerLanguage("ruby", ruby);
-SyntaxHighlighter.registerLanguage("php", php);
-SyntaxHighlighter.registerLanguage("python", python);
-
-export const Code: React.FC = ({ codeBlocks, codeClassName }) => {
-  const [selectedTab, setSelectedTab] = useState(
-    codeBlocks[0]?.language ?? "js"
-  );
-  const [isCopied, setIsCopied] = useState(false);
-
-  const copyToClipboard = async (code: string) => {
-    try {
-      await navigator.clipboard.writeText(code);
-      setIsCopied(true);
-      setTimeout(() => setIsCopied(false), 2000); // Reset the icon back to clipboard after 2 seconds
-    } catch (err) {
-      alert("Failed to copy code");
-    }
-  };
-
-  return (
-    
- setSelectedTab(val as Language)} - > -
- -
- {codeBlocks.map((block) => ( - - {block.title || block.language} - - ))} -
-
- -
- {codeBlocks.map((block) => ( - -
- - {block.code} - -
-
- ))} -
-
- ); -}; diff --git a/packages/ui/src/typography.tsx b/packages/ui/src/typography.tsx index e0a96e7..15d5613 100644 --- a/packages/ui/src/typography.tsx +++ b/packages/ui/src/typography.tsx @@ -14,7 +14,7 @@ export const H1 = React.forwardRef( ref={ref} className={cn( // font-mono, larger and a bit bolder - " font-mono text-xl font-medium", + " font-mono text-xl font-medium text-primary", className )} {...props} diff --git a/packages/ui/styles/globals.css b/packages/ui/styles/globals.css index 7dc7218..ac26ca1 100644 --- a/packages/ui/styles/globals.css +++ b/packages/ui/styles/globals.css @@ -14,7 +14,7 @@ --popover: 220 2% 96%; --popover-foreground: 234 16% 35%; - --primary: 200 65% 14%; + --primary: 167 34% 20%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; @@ -63,6 +63,7 @@ --purple: 266 85% 58%; --yellow: 35 77% 49%; --gray: 220 9% 46%; + --primary-light: 183 74% 35%; } .dark { @@ -75,7 +76,7 @@ --popover: 240 21% 15%; --popover-foreground: 226 64% 88%; - --primary: 220 23% 95%; + --primary: 167 64% 94%; --primary-foreground: 240 23% 9%; --secondary: 217.2 32.6% 17.5%; @@ -122,6 +123,7 @@ --purple: 267 84% 81%; --yellow: 41 86% 83%; --gray: 218 11% 65%; + --primary-light: 170 57% 73%; } } @@ -137,6 +139,18 @@ } } +@media (prefers-color-scheme: dark) { + .shiki, + .shiki span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + /* Optional, if you also want font styles */ + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; + } +} + /* .app, ::before, ::after { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dea27f..b6afa2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,7 +283,7 @@ importers: version: 10.0.4(react@19.1.0) usesend: specifier: workspace:* - version: link:../../packages/sdk + version: link:../.. zod: specifier: ^3.24.3 version: 3.24.3 @@ -608,6 +608,9 @@ importers: framer-motion: specifier: ^12.9.2 version: 12.9.2(react-dom@19.1.0)(react@19.1.0) + hast-util-to-jsx-runtime: + specifier: ^2.3.6 + version: 2.3.6 input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.1.0)(react@19.1.0) @@ -623,12 +626,12 @@ importers: react-hook-form: specifier: ^7.56.1 version: 7.56.1(react@19.1.0) - react-syntax-highlighter: - specifier: ^15.6.1 - version: 15.6.1(react@19.1.0) recharts: specifier: ^2.15.3 version: 2.15.3(react-dom@19.1.0)(react@19.1.0) + shiki: + specifier: ^3.3.0 + version: 3.3.0 sonner: specifier: ^2.0.3 version: 2.0.3(react-dom@19.1.0)(react@19.1.0) @@ -654,9 +657,6 @@ importers: '@types/react-dom': specifier: ^19.1.2 version: 19.1.2(@types/react@19.1.2) - '@types/react-syntax-highlighter': - specifier: ^15.5.13 - version: 15.5.13 '@usesend/eslint-config': specifier: workspace:* version: link:../eslint-config @@ -7591,7 +7591,6 @@ packages: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: '@types/ms': 2.1.0 - dev: true /@types/es-aggregate-error@1.0.6: resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -7610,7 +7609,6 @@ packages: resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} dependencies: '@types/estree': 1.0.7 - dev: true /@types/estree@1.0.7: resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -7619,6 +7617,7 @@ packages: resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} dependencies: '@types/unist': 2.0.11 + dev: true /@types/hast@3.0.4: resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -7682,7 +7681,6 @@ packages: /@types/ms@2.1.0: resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - dev: true /@types/nlcst@2.0.3: resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} @@ -7722,12 +7720,6 @@ packages: dependencies: '@types/react': 19.1.2 - /@types/react-syntax-highlighter@15.5.13: - resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - dependencies: - '@types/react': 19.1.2 - dev: true - /@types/react@19.1.2: resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} dependencies: @@ -9023,28 +9015,14 @@ packages: /character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - /character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - dev: false - /character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - /character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - dev: false - /character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - dev: true - - /character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - dev: false /character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - dev: true /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -9232,10 +9210,6 @@ packages: dependencies: delayed-stream: 1.0.0 - /comma-separated-tokens@1.0.8: - resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} - dev: false - /comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -9555,7 +9529,6 @@ packages: resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} dependencies: character-entities: 2.0.2 - dev: true /decode-uri-component@0.4.1: resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} @@ -10706,7 +10679,6 @@ packages: /estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - dev: true /estree-util-scope@1.0.0: resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} @@ -10908,12 +10880,6 @@ packages: dependencies: reusify: 1.1.0 - /fault@1.0.4: - resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} - dependencies: - format: 0.2.2 - dev: false - /fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} dependencies: @@ -11058,6 +11024,7 @@ packages: /format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + dev: true /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -11524,10 +11491,6 @@ packages: hast-util-whitespace: 3.0.0 unist-util-is: 6.0.0 - /hast-util-parse-selector@2.2.5: - resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - dev: false - /hast-util-parse-selector@3.1.1: resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} dependencies: @@ -11627,7 +11590,6 @@ packages: vfile-message: 4.0.2 transitivePeerDependencies: - supports-color - dev: true /hast-util-to-mdast@10.1.2: resolution: {integrity: sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==} @@ -11667,16 +11629,6 @@ packages: dependencies: '@types/hast': 3.0.4 - /hastscript@6.0.0: - resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 1.0.8 - hast-util-parse-selector: 2.2.5 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - dev: false - /hastscript@7.2.0: resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} dependencies: @@ -11705,14 +11657,6 @@ packages: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} dev: false - /highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - dev: false - - /highlightjs-vue@1.0.0: - resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} - dev: false - /hono@4.7.7: resolution: {integrity: sha512-2PCpQRbN87Crty8/L/7akZN3UyZIAopSoRxCwRbJgUuV1+MHNFHzYFxZTg4v/03cXUm+jce/qa2VSBZpKBm3Qw==} engines: {node: '>=16.9.0'} @@ -11956,27 +11900,14 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true - /is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - dev: false - /is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - dev: true - - /is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - dev: false /is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} dependencies: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - dev: true /is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} @@ -12072,13 +12003,8 @@ packages: has-tostringtag: 1.0.2 dev: true - /is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - dev: false - /is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - dev: true /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} @@ -12121,13 +12047,8 @@ packages: dependencies: is-extglob: 2.1.1 - /is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - dev: false - /is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - dev: true /is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} @@ -12682,7 +12603,6 @@ packages: /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - dev: true /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -12695,13 +12615,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true - /lowlight@1.20.0: - resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - dependencies: - fault: 1.0.4 - highlight.js: 10.7.3 - dev: false - /lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -12830,7 +12743,6 @@ packages: unist-util-stringify-position: 4.0.0 transitivePeerDependencies: - supports-color - dev: true /mdast-util-frontmatter@2.0.1: resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} @@ -12939,7 +12851,6 @@ packages: mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: true /mdast-util-mdx-jsx@3.2.0: resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} @@ -12958,7 +12869,6 @@ packages: vfile-message: 4.0.2 transitivePeerDependencies: - supports-color - dev: true /mdast-util-mdx@3.0.0: resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} @@ -12983,14 +12893,12 @@ packages: mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: true /mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} dependencies: '@types/mdast': 4.0.4 unist-util-is: 6.0.0 - dev: true /mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} @@ -13017,13 +12925,11 @@ packages: micromark-util-decode-string: 2.0.1 unist-util-visit: 5.0.0 zwitch: 2.0.4 - dev: true /mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} dependencies: '@types/mdast': 4.0.4 - dev: true /mdast@3.0.0: resolution: {integrity: sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g==} @@ -13071,7 +12977,6 @@ packages: micromark-util-subtokenize: 2.1.0 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-extension-frontmatter@2.0.0: resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} @@ -13233,7 +13138,6 @@ packages: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-factory-label@2.0.1: resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} @@ -13242,7 +13146,6 @@ packages: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-factory-mdx-expression@2.0.3: resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} @@ -13263,7 +13166,6 @@ packages: dependencies: micromark-util-character: 2.1.1 micromark-util-types: 2.0.2 - dev: true /micromark-factory-title@2.0.1: resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} @@ -13272,7 +13174,6 @@ packages: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-factory-whitespace@2.0.1: resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} @@ -13281,7 +13182,6 @@ packages: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -13293,7 +13193,6 @@ packages: resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} dependencies: micromark-util-symbol: 2.0.1 - dev: true /micromark-util-classify-character@2.0.1: resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} @@ -13301,20 +13200,17 @@ packages: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-util-combine-extensions@2.0.1: resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} dependencies: micromark-util-chunked: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-util-decode-numeric-character-reference@2.0.2: resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} dependencies: micromark-util-symbol: 2.0.1 - dev: true /micromark-util-decode-string@2.0.1: resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} @@ -13323,7 +13219,6 @@ packages: micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 - dev: true /micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} @@ -13342,19 +13237,16 @@ packages: /micromark-util-html-tag-name@2.0.1: resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - dev: true /micromark-util-normalize-identifier@2.0.1: resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} dependencies: micromark-util-symbol: 2.0.1 - dev: true /micromark-util-resolve-all@2.0.1: resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} dependencies: micromark-util-types: 2.0.2 - dev: true /micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} @@ -13370,7 +13262,6 @@ packages: micromark-util-chunked: 2.0.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - dev: true /micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} @@ -13400,7 +13291,6 @@ packages: micromark-util-types: 2.0.2 transitivePeerDependencies: - supports-color - dev: true /micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} @@ -14180,17 +14070,6 @@ packages: dependencies: callsites: 3.1.0 - /parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - dev: false - /parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} dependencies: @@ -14201,7 +14080,6 @@ packages: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - dev: true /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -14634,16 +14512,6 @@ packages: - supports-color dev: false - /prismjs@1.27.0: - resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} - engines: {node: '>=6'} - dev: false - - /prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - dev: false - /process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} dev: false @@ -14660,12 +14528,6 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 - /property-information@5.6.0: - resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - dependencies: - xtend: 4.0.2 - dev: false - /property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} dev: true @@ -15125,20 +14987,6 @@ packages: tslib: 2.8.1 dev: false - /react-syntax-highlighter@15.6.1(react@19.1.0): - resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==} - peerDependencies: - react: '>= 0.14.0' - dependencies: - '@babel/runtime': 7.27.0 - highlight.js: 10.7.3 - highlightjs-vue: 1.0.0 - lowlight: 1.20.0 - prismjs: 1.30.0 - react: 19.1.0 - refractor: 3.6.0 - dev: false - /react-transition-group@4.4.5(react-dom@19.1.0)(react@19.1.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -15302,14 +15150,6 @@ packages: which-builtin-type: 1.2.1 dev: true - /refractor@3.6.0: - resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} - dependencies: - hastscript: 6.0.0 - parse-entities: 2.0.0 - prismjs: 1.27.0 - dev: false - /refractor@4.9.0: resolution: {integrity: sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==} dependencies: @@ -16300,10 +16140,6 @@ packages: whatwg-url: 7.1.0 dev: true - /space-separated-tokens@1.1.5: - resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - dev: false - /space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -16533,7 +16369,6 @@ packages: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} dependencies: style-to-object: 1.0.8 - dev: true /style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} @@ -17596,11 +17431,6 @@ packages: engines: {node: '>=4.0'} dev: true - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - dev: false - /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'}