diff --git a/app/.vitepress/config.ts b/app/.vitepress/config.ts index 8c4d4a9..af50647 100644 --- a/app/.vitepress/config.ts +++ b/app/.vitepress/config.ts @@ -1,23 +1,25 @@ -import { defineConfig, type DefaultTheme, type UserConfig } from "vitepress"; import { transformerTwoslash } from "@shikijs/vitepress-twoslash"; +import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { ModuleDetectionKind, ModuleKind, ModuleResolutionKind } from "typescript"; +import { defineConfig, type DefaultTheme } from "vitepress"; import { groupIconMdPlugin, groupIconVitePlugin } from "vitepress-plugin-group-icons"; -import { Path, pipe } from "@duplojs/utils"; import { withMermaid } from "vitepress-plugin-mermaid"; -import tailwindcss from "@tailwindcss/vite"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const hostname = "https://duplojs.dev"; const ogImage = new URL("/images/ogImage.png", hostname).toString(); -export default pipe( - { +export default withMermaid( + defineConfig({ title: "DuploJS", base: "/", appearance: false, cleanUrls: true, - sitemap: { - hostname, - }, + + sitemap: { hostname }, + head: [ [ "link", @@ -26,6 +28,28 @@ export default pipe( href: "/images/logo.ico", }, ], + [ + "link", + { + rel: "preconnect", + href: "https://fonts.googleapis.com", + }, + ], + [ + "link", + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossorigin: "", + }, + ], + [ + "link", + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@700;800&family=Fragment+Mono:ital,wght@0,400;1,400&display=swap", + }, + ], [ "meta", { @@ -55,34 +79,22 @@ export default pipe( }, ], ], + themeConfig: { logo: "/images/logo.png", - wip: { - title: "WIP", - button: "Request this page", - }, socialLinks: [ { icon: "github", link: "https://github.com/duplojs/discover", }, - { - icon: "npm", - link: "https://www.npmjs.com/package/@duplojs/discover", - }, - { - icon: "linkedin", - link: "https://linkedin.com/company/duplojs", - }, { icon: "discord", link: "https://discord.gg/5d6Ze5Wuqm", }, ], - search: { - provider: "local", - }, - }, + search: { provider: "local" }, + } satisfies DefaultTheme.Config, + markdown: { config: (md) => { md.use(groupIconMdPlugin); @@ -112,6 +124,7 @@ export default pipe( }), ], }, + vite: { plugins: [ groupIconVitePlugin(), @@ -119,60 +132,53 @@ export default pipe( ], resolve: { alias: { - "@": Path.resolveRelative([import.meta.dirname, ".."]), + "@": path.resolve(__dirname, ".."), }, }, server: { host: "0.0.0.0", }, }, + locales: { - fr: { - label: "Français", - lang: "fr", - link: "/fr/", + root: { + label: "English", + lang: "en-US", themeConfig: { nav: [], sidebar: {}, docFooter: { - prev: "Page précédente", - next: "Page suivante", - }, - outline: { - label: "Sur cette page", + prev: "Previous page", + next: "Next page", }, - returnToTopLabel: "Retour en haut", - darkModeSwitchLabel: "Mode sombre", + outline: { label: "On this page" }, + returnToTopLabel: "Return to top", + darkModeSwitchLabel: "Dark mode", footer: { - copyright: "Copyright © 2025-présent Contributeurs de DuploJS", - message: "Diffusé sous licence MIT.", + copyright: "Copyright © 2025-present DuploJS contributors", + message: "Released under the MIT License.", }, }, }, - root: { - label: "English", - lang: "en", - link: "/en/", + fr: { + label: "Français", + lang: "fr-FR", themeConfig: { nav: [], sidebar: {}, docFooter: { - prev: "Previous page", - next: "Next page", - }, - outline: { - label: "On this page", + prev: "Page précédente", + next: "Page suivante", }, - returnToTopLabel: "Return to top", - darkModeSwitchLabel: "Dark mode", + outline: { label: "Sur cette page" }, + returnToTopLabel: "Retour en haut", + darkModeSwitchLabel: "Mode sombre", footer: { - copyright: "Copyright © 2025-present DuploJS contributors", - message: "Released under the MIT License.", + copyright: "Copyright © 2025-présent Contributeurs de DuploJS", + message: "Diffusé sous licence MIT.", }, }, }, }, - } satisfies UserConfig, - defineConfig, - withMermaid, + }), ); diff --git a/app/.vitepress/theme/Layout.vue b/app/.vitepress/theme/Layout.vue new file mode 100644 index 0000000..eeaaec9 --- /dev/null +++ b/app/.vitepress/theme/Layout.vue @@ -0,0 +1,122 @@ + + + diff --git a/app/.vitepress/theme/components/AppFooter.vue b/app/.vitepress/theme/components/AppFooter.vue new file mode 100644 index 0000000..f9f0d51 --- /dev/null +++ b/app/.vitepress/theme/components/AppFooter.vue @@ -0,0 +1,235 @@ + + + diff --git a/app/.vitepress/theme/components/AppNav.vue b/app/.vitepress/theme/components/AppNav.vue new file mode 100644 index 0000000..51fde2e --- /dev/null +++ b/app/.vitepress/theme/components/AppNav.vue @@ -0,0 +1,446 @@ + + + + + diff --git a/app/.vitepress/theme/components/LargeBrick.vue b/app/.vitepress/theme/components/LargeBrick.vue deleted file mode 100644 index 3335146..0000000 --- a/app/.vitepress/theme/components/LargeBrick.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - - - diff --git a/app/.vitepress/theme/components/TerminalBlock.vue b/app/.vitepress/theme/components/TerminalBlock.vue deleted file mode 100644 index 22313f2..0000000 --- a/app/.vitepress/theme/components/TerminalBlock.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/app/.vitepress/theme/components/TheBrick.vue b/app/.vitepress/theme/components/TheBrick.vue deleted file mode 100644 index 742a374..0000000 --- a/app/.vitepress/theme/components/TheBrick.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/app/.vitepress/theme/components/TheLogo.vue b/app/.vitepress/theme/components/TheLogo.vue deleted file mode 100644 index 24ab372..0000000 --- a/app/.vitepress/theme/components/TheLogo.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/app/.vitepress/theme/components/TheVersus.vue b/app/.vitepress/theme/components/TheVersus.vue deleted file mode 100644 index c1be15a..0000000 --- a/app/.vitepress/theme/components/TheVersus.vue +++ /dev/null @@ -1,330 +0,0 @@ - - - - - diff --git a/app/.vitepress/theme/components/WipPage/TheComponent.vue b/app/.vitepress/theme/components/WipPage/TheComponent.vue deleted file mode 100644 index fdbff27..0000000 --- a/app/.vitepress/theme/components/WipPage/TheComponent.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - diff --git a/app/.vitepress/theme/components/WipPage/index.ts b/app/.vitepress/theme/components/WipPage/index.ts deleted file mode 100644 index d26a904..0000000 --- a/app/.vitepress/theme/components/WipPage/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import TheComponent from "./TheComponent.vue"; - -declare module "vitepress" { - namespace DefaultTheme { - interface Config { - wip?: { - title?: string; - subtitle?: string; - button?: string; - }; - } - } -} - -export const WipPage = TheComponent; diff --git a/app/.vitepress/theme/components/sections/BenchmarkSection.vue b/app/.vitepress/theme/components/sections/BenchmarkSection.vue new file mode 100644 index 0000000..6169450 --- /dev/null +++ b/app/.vitepress/theme/components/sections/BenchmarkSection.vue @@ -0,0 +1,138 @@ + + + diff --git a/app/.vitepress/theme/components/sections/CheckersSection.vue b/app/.vitepress/theme/components/sections/CheckersSection.vue new file mode 100644 index 0000000..26aee31 --- /dev/null +++ b/app/.vitepress/theme/components/sections/CheckersSection.vue @@ -0,0 +1,298 @@ + + + diff --git a/app/.vitepress/theme/components/sections/CommunitySection.vue b/app/.vitepress/theme/components/sections/CommunitySection.vue new file mode 100644 index 0000000..daa32ca --- /dev/null +++ b/app/.vitepress/theme/components/sections/CommunitySection.vue @@ -0,0 +1,99 @@ + + + diff --git a/app/.vitepress/theme/components/sections/ComparisonSection.vue b/app/.vitepress/theme/components/sections/ComparisonSection.vue new file mode 100644 index 0000000..ba33b2f --- /dev/null +++ b/app/.vitepress/theme/components/sections/ComparisonSection.vue @@ -0,0 +1,99 @@ + + + diff --git a/app/.vitepress/theme/components/sections/ConvictionSection.vue b/app/.vitepress/theme/components/sections/ConvictionSection.vue new file mode 100644 index 0000000..12a5208 --- /dev/null +++ b/app/.vitepress/theme/components/sections/ConvictionSection.vue @@ -0,0 +1,204 @@ + + + diff --git a/app/.vitepress/theme/components/sections/CtaSection.vue b/app/.vitepress/theme/components/sections/CtaSection.vue new file mode 100644 index 0000000..212815b --- /dev/null +++ b/app/.vitepress/theme/components/sections/CtaSection.vue @@ -0,0 +1,141 @@ + + + diff --git a/app/.vitepress/theme/components/sections/EcosystemSection.vue b/app/.vitepress/theme/components/sections/EcosystemSection.vue new file mode 100644 index 0000000..8c669c3 --- /dev/null +++ b/app/.vitepress/theme/components/sections/EcosystemSection.vue @@ -0,0 +1,501 @@ + + + diff --git a/app/.vitepress/theme/components/sections/GettingStartedSection.vue b/app/.vitepress/theme/components/sections/GettingStartedSection.vue new file mode 100644 index 0000000..b487233 --- /dev/null +++ b/app/.vitepress/theme/components/sections/GettingStartedSection.vue @@ -0,0 +1,192 @@ + + + diff --git a/app/.vitepress/theme/components/sections/HeroSection.vue b/app/.vitepress/theme/components/sections/HeroSection.vue new file mode 100644 index 0000000..b583837 --- /dev/null +++ b/app/.vitepress/theme/components/sections/HeroSection.vue @@ -0,0 +1,596 @@ + + + diff --git a/app/.vitepress/theme/components/sections/PipelineSection.vue b/app/.vitepress/theme/components/sections/PipelineSection.vue new file mode 100644 index 0000000..cb11101 --- /dev/null +++ b/app/.vitepress/theme/components/sections/PipelineSection.vue @@ -0,0 +1,456 @@ + + + diff --git a/app/.vitepress/theme/components/sections/TestimonialsSection.vue b/app/.vitepress/theme/components/sections/TestimonialsSection.vue new file mode 100644 index 0000000..120b748 --- /dev/null +++ b/app/.vitepress/theme/components/sections/TestimonialsSection.vue @@ -0,0 +1,201 @@ + + + diff --git a/app/.vitepress/theme/components/sections/TypeFlowSection.vue b/app/.vitepress/theme/components/sections/TypeFlowSection.vue new file mode 100644 index 0000000..2df5979 --- /dev/null +++ b/app/.vitepress/theme/components/sections/TypeFlowSection.vue @@ -0,0 +1,213 @@ + + + diff --git a/app/.vitepress/theme/composables/useCounter.ts b/app/.vitepress/theme/composables/useCounter.ts new file mode 100644 index 0000000..cd13dcc --- /dev/null +++ b/app/.vitepress/theme/composables/useCounter.ts @@ -0,0 +1,66 @@ +import { onMounted, onUnmounted, ref } from "vue"; + +function easeOutExpo(t: number) { + return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); +} + +function animateCount( + setter: (v: number) => void, + from: number, + to: number, + duration: number, +) { + const start = performance.now(); + const range = to - from; + function tick(now: number) { + const progress = Math.min((now - start) / duration, 1); + setter(Math.round(from + (easeOutExpo(progress) * range))); + if (progress < 1) { + requestAnimationFrame(tick); + } else { + setter(to); + } + } + requestAnimationFrame(tick); +} + +export function useCounterOnReveal(targets: { + value: number; + key: string; +}[]) { + const counts = ref>({}); + targets.forEach((t) => { + counts.value[t.key] = t.value; + }); + + let observer: IntersectionObserver | null = null; + + onMounted(() => { + const container = document.getElementById("conviction"); + if (!container) { + return; + } + + observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + targets.forEach((t) => { + if (typeof t.value === "number") { + counts.value[t.key] = 0; + animateCount((v) => { + counts.value[t.key] = v; + }, 0, t.value, 1200); + } + }); + observer?.disconnect(); + } + }); + }, { threshold: 0.3 }); + + observer.observe(container); + }); + + onUnmounted(() => observer?.disconnect()); + + return { counts }; +} diff --git a/app/.vitepress/theme/composables/useI18n.ts b/app/.vitepress/theme/composables/useI18n.ts new file mode 100644 index 0000000..26eff47 --- /dev/null +++ b/app/.vitepress/theme/composables/useI18n.ts @@ -0,0 +1,10 @@ +import { useData } from "vitepress"; +import { computed } from "vue"; +import en from "../i18n/en"; +import fr from "../i18n/fr"; + +export function useI18n() { + const { lang } = useData(); + const t = computed(() => lang.value.startsWith("fr") ? fr : en); + return { t }; +} diff --git a/app/.vitepress/theme/composables/useMagneticCard.ts b/app/.vitepress/theme/composables/useMagneticCard.ts new file mode 100644 index 0000000..b3b8ce6 --- /dev/null +++ b/app/.vitepress/theme/composables/useMagneticCard.ts @@ -0,0 +1,39 @@ +import { onMounted, onUnmounted } from "vue"; + +export function useMagneticCard(selector = ".eco-card") { + if (typeof window === "undefined") { + return; + } + + function onMouseMove(e: MouseEvent) { + const card = (e.currentTarget as HTMLElement); + const rect = card.getBoundingClientRect(); + const dx = (((e.clientX - rect.left) / rect.width) - 0.5) * 8; + const dy = (((e.clientY - rect.top) / rect.height) - 0.5) * 8; + card.style.setProperty("--mx", `${dx.toFixed(1)}px`); + card.style.setProperty("--my", `${dy.toFixed(1)}px`); + } + + function onMouseLeave(e: MouseEvent) { + const card = e.currentTarget as HTMLElement; + card.style.setProperty("--mx", "0px"); + card.style.setProperty("--my", "0px"); + } + + onMounted(() => { + if (window.matchMedia("(hover: none)").matches) { + return; + } + document.querySelectorAll(selector).forEach((card) => { + card.addEventListener("mousemove", onMouseMove); + card.addEventListener("mouseleave", onMouseLeave); + }); + }); + + onUnmounted(() => { + document.querySelectorAll(selector).forEach((card) => { + card.removeEventListener("mousemove", onMouseMove); + card.removeEventListener("mouseleave", onMouseLeave); + }); + }); +} diff --git a/app/.vitepress/theme/composables/useScrollReveal.ts b/app/.vitepress/theme/composables/useScrollReveal.ts new file mode 100644 index 0000000..eede1ba --- /dev/null +++ b/app/.vitepress/theme/composables/useScrollReveal.ts @@ -0,0 +1,23 @@ +import { onMounted, onUnmounted } from "vue"; + +export function useScrollReveal(selector = ".reveal") { + let observer: IntersectionObserver | null = null; + + onMounted(() => { + observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("visible"); + observer?.unobserve(entry.target); + } + }); + }, { + threshold: 0.08, + rootMargin: "0px 0px -40px 0px", + }); + + document.querySelectorAll(selector).forEach((el) => void observer?.observe(el)); + }); + + onUnmounted(() => observer?.disconnect()); +} diff --git a/app/.vitepress/theme/composables/useTypewriter.ts b/app/.vitepress/theme/composables/useTypewriter.ts new file mode 100644 index 0000000..563abd9 --- /dev/null +++ b/app/.vitepress/theme/composables/useTypewriter.ts @@ -0,0 +1,46 @@ +import { onMounted, onUnmounted, ref } from "vue"; + +export function useTypewriter(commands: string[] = [ + "npm install @duplojs/http", + "bun add @duplojs/http", + "deno add @duplojs/http", +]) { + const text = ref(""); + let timer: ReturnType | null = null; + let ci = 0; + let len = 0; + let deleting = false; + const SPEED_TYPE = 55; + const SPEED_DEL = 28; + const PAUSE = 1800; + + function tick() { + const cmd = commands[ci % commands.length]!; + if (!deleting) { + text.value = cmd.slice(0, ++len); + if (len === cmd.length) { + deleting = true; + timer = setTimeout(tick, PAUSE); + return; + } + } else { + text.value = cmd.slice(0, --len); + if (len === 0) { + deleting = false; + ci++; + timer = setTimeout(tick, 300); + return; + } + } + timer = setTimeout(tick, deleting ? SPEED_DEL : SPEED_TYPE); + } + + onMounted(() => void tick()); + onUnmounted(() => { + if (timer) { + clearTimeout(timer); + } + }); + + return { text }; +} diff --git a/app/.vitepress/theme/i18n/en.ts b/app/.vitepress/theme/i18n/en.ts new file mode 100644 index 0000000..5855a12 --- /dev/null +++ b/app/.vitepress/theme/i18n/en.ts @@ -0,0 +1,228 @@ +type DeepStringify = { + [K in keyof T]: T[K] extends object ? DeepStringify : string; +}; + +const en = { + nav: { + packages: "Packages", + docs: "Docs", + github: "GitHub", + getStarted: "Get started →", + toggleTheme: "Toggle color theme", + toggleMenu: "Toggle menu", + core: "Core", + tooling: "Tooling", + pkgs: { + http: { + name: "@duplojs/http", + desc: "Server & client, 100% typesafe", + }, + utils: { + name: "@duplojs/utils", + desc: "TypeScript utility toolbox", + }, + serverUtils: { + name: "@duplojs/server-utils", + desc: "Runtime-independent server helpers", + }, + zodAccelerator: { + name: "@duplojs/zod-accelerator", + desc: "Accelerated Zod validation ★ 100", + }, + playwright: { + name: "@duplojs/playwright", + desc: "Playwright testing reimagined", + }, + jwt: { + name: "@duplojs/json-web-token", + desc: "Typed JWT handler", + }, + }, + }, + hero: { + label: "@duplojs/http", + headline1: "BUILD HTTP APIS", + headline2: "THAT CANNOT", + headline3: "LIE.", + tagline1: "A 100% typesafe HTTP framework for TypeScript.", + tagline2: "Define once, consume everywhere.", + installLabel: "Install command", + getStarted: "Get started →", + viewGithub: "View on GitHub", + scroll: "scroll", + }, + conviction: { + eyebrow: "Why DuploJS", + stat0: "0", + stat0unit: "ms", + stat0label: "Runtime overhead", + stat1: "9", + stat1unit: "+", + stat1label: "Packages", + quote: "\"Types that cannot drift.\nCheckers that cannot skip.\nAPIs that cannot lie.\"", + quoteEm1: "drift", + quoteEm2: "skip", + quoteEm3: "lie", + quoteCite: "DuploJS design philosophy", + stat2: "3", + stat2label: "Runtimes", + statInf: "∞", + statInflabel: "Composability", + compatWith: "Compatible with", + mit: "MIT License", + }, + pipeline: { + label: "request.lifecycle()", + title: "Built around a contract.", + subtitle: "Every request flows through a typed pipeline. The server definition becomes the client type. Automatically. No drift possible.", + scenarioHappy: ".send({ status: 200 })", + scenarioBadBody: "bodyChecker() → 400", + scenarioNoAuth: "authChecker() → 401", + runBtn: "await .send()", + lifecycleLabel: "[ REQUEST LIFECYCLE ]", + contractDivider: "[ contract ]", + serverLabel: "Server · Route definition", + clientLabel: "Client · Inferred types", + }, + typeflow: { + label: "type Safety =", + title: "Types that cannot drift.", + subtitle: "Define once on the server. Consume everywhere on the client — no manual sync, no cast hacks. The contract is the code.", + tabGet: "GET route", + tabPost: "POST with body", + tabAuth: "Auth protected", + serverLabel: "Server · Route definition", + clientLabel: "Client · Inferred types", + contractLabel: "[ contract ]", + }, + checkers: { + label: ".useChecker()", + title: "Checkers compose.\nLogic stays clean.", + subtitle: "Define reusable validation guards and stack them on any route. Each checker runs in sequence. The handler only executes if all pass.", + bodyName: "bodyChecker", + bodySub: "zod schema · email + name", + authName: "authChecker", + authSub: "JWT · Bearer token", + roleName: "roleChecker", + roleSub: "requires: admin", + handlerName: "handler", + handlerSub: "awaiting checkers", + badgePass: "✓ pass", + badgeChecking: "→ checking", + badgePending: "pending", + }, + comparison: { + label: "framework.compare()", + title: "The honest comparison.", + subtitle: "DuploJS vs the frameworks you already know.", + featureCol: "Feature", + f1: "End-to-end type safety", + f2: "Runtime agnostic", + f3: "Checker composition", + f4: "Built-in schema validation", + f5: "Auto client type inference", + f6: "Zero-config TypeScript", + note: "Comparison based on default setups as of 2025. ~ = partial support.", + }, + benchmark: { + label: "perf.measure()", + title1: "Built for speed.", + title2: "Measured in requests.", + subtitle: "HTTP throughput benchmark. autocannon · 10 connections · 10s · Node.js 20 LTS · hello-world handler.", + footnote: "Relative comparison. Actual results vary by hardware, workload, and configuration.", + viewBenchmarks: "View benchmarks →", + }, + ecosystem: { + label: "import { * }", + title: "Everything you need.", + subtitle: "A complete suite of TypeScript packages built around DuploJS.", + coreLabel: "Core", + toolingLabel: "Tooling", + copyLabel: "Copy install command", + httpDesc: "100% typesafe HTTP interface for Server and Client. Build and consume HTTP APIs with full confidence.", + utilsDesc: "TypeScript utility toolbox used across the DuploJS ecosystem. Strong typing and composable primitives.", + serverUtilsDesc: "Runtime-independent server helpers as part of the DuploJS ecosystem.", + zodAcceleratorDesc: "Accelerated Zod schema validation for high-performance TypeScript applications.", + zodToTsDesc: "Converts Zod schemas to TypeScript types. Bridge between runtime validation and static typing.", + playwrightDesc: "Rethinks Playwright testing through website, page, and component building blocks.", + jwtDesc: "Typed, opinionated token handler for signing, encrypting, and validating JWT tokens.", + formDesc: "Structure-first, strongly typed Vue form library for building complex forms with confidence.", + dataParserDesc: "Converts dataParser schemas to TypeScript types and JSON Schema formats.", + }, + gettingStarted: { + label: ".install().run()", + title1: "Up and running", + title2: "in 3 steps.", + subtitle: "No boilerplate. No ceremony. Just a typed, running API.", + step1num: "01", + step1title: ".install", + step1desc: "Add the core package to your project.", + step2num: "02", + step2title: ".createRoute", + step2desc: "Declare your first typesafe route.", + step3num: "03", + step3title: ".getClient", + step3desc: "Consume fully-typed on the client — automatically.", + readDocs: "Read the docs →", + viewExamples: "View examples", + }, + testimonials: { + label: "/* community */", + title1: "Built for those who", + title2: "care about correctness.", + starsLabel: "GitHub Stars", + packagesLabel: "Packages", + runtimesLabel: "Runtimes", + typescriptLabel: "TypeScript", + t1quote: "\"Finally a framework where the types actually mean something. No more `as any` to make the compiler happy.\"", + t1handle: "@devuser1", + t1role: "Senior Backend Engineer", + t2quote: "\"The checker system is genuinely clever. Reusable, composable, and the type inference just works.\"", + t2handle: "@tsdev", + t2role: "TypeScript Enthusiast", + t3quote: "\"Switched from Express + Zod + manual types. DuploJS eliminates an entire category of bugs.\"", + t3handle: "@apiengineer", + t3role: "Full-stack Developer", + t4quote: "\"The contract between server and client is automatic. I cannot overstate how much cognitive load this removes.\"", + t4handle: "@contractfirst", + t4role: "API Architect", + }, + cta: { + label: ".getStarted()", + title1: "Build APIs that", + title2: "cannot lie.", + subtitle1: "Join developers who build with confidence.", + subtitle2: "Zero surprises at runtime. Full types everywhere.", + readDocs: "Read the docs →", + viewGithub: "View on GitHub", + }, + community: { + label: "@community", + title1: "Built by developers,", + title2: "for developers.", + discordTitle: "Discord", + discordSub: "Join the community · Get help · Share your work", + githubTitle: "GitHub", + githubSub: "Explore the source · Open issues · Contribute", + linkedinTitle: "LinkedIn", + linkedinSub: "Follow updates · News · Announcements", + }, + footer: { + tagline: "Build HTTP APIs that cannot lie.", + desc: "A 100% typesafe HTTP framework for TypeScript.", + colPackages: "Packages", + colTooling: "Tooling", + colCommunity: "Community", + colLearn: "Learn", + documentation: "Documentation", + gettingStarted: "Getting Started", + apiReference: "API Reference", + changelog: "Changelog", + copyright: "© 2025-present DuploJS Contributors", + langSwitch: "Français", + langSwitchHref: "/fr/", + }, +}; + +export type Translations = DeepStringify; +export default en; diff --git a/app/.vitepress/theme/i18n/fr.ts b/app/.vitepress/theme/i18n/fr.ts new file mode 100644 index 0000000..284cb46 --- /dev/null +++ b/app/.vitepress/theme/i18n/fr.ts @@ -0,0 +1,226 @@ + +import type { Translations } from "./en"; + +const fr: Translations = { + nav: { + packages: "Packages", + docs: "Docs", + github: "GitHub", + getStarted: "Démarrer →", + toggleTheme: "Changer le thème", + toggleMenu: "Ouvrir le menu", + core: "Core", + tooling: "Outils", + pkgs: { + http: { + name: "@duplojs/http", + desc: "Serveur & client, 100% typesafe", + }, + utils: { + name: "@duplojs/utils", + desc: "Boîte à outils TypeScript", + }, + serverUtils: { + name: "@duplojs/server-utils", + desc: "Helpers serveur indépendants du runtime", + }, + zodAccelerator: { + name: "@duplojs/zod-accelerator", + desc: "Validation Zod accélérée ★ 100", + }, + playwright: { + name: "@duplojs/playwright", + desc: "Tests Playwright repensés", + }, + jwt: { + name: "@duplojs/json-web-token", + desc: "Gestionnaire JWT typé", + }, + }, + }, + hero: { + label: "@duplojs/http", + headline1: "CONSTRUISEZ DES APIs HTTP", + headline2: "QUI NE PEUVENT PAS", + headline3: "MENTIR.", + tagline1: "Un framework HTTP 100% typesafe pour TypeScript.", + tagline2: "Définissez une fois, consommez partout.", + installLabel: "Commande d'installation", + getStarted: "Démarrer →", + viewGithub: "Voir sur GitHub", + scroll: "défiler", + }, + conviction: { + eyebrow: "Pourquoi DuploJS", + stat0: "0", + stat0unit: "ms", + stat0label: "Overhead d'exécution", + stat1: "9", + stat1unit: "+", + stat1label: "Packages", + quote: "\"Des types qui ne peuvent pas dériver.\nDes checkers qui ne peuvent pas sauter.\nDes APIs qui ne peuvent pas mentir.\"", + quoteEm1: "dériver", + quoteEm2: "sauter", + quoteEm3: "mentir", + quoteCite: "Philosophie de conception DuploJS", + stat2: "3", + stat2label: "Runtimes", + statInf: "∞", + statInflabel: "Composabilité", + compatWith: "Compatible avec", + mit: "Licence MIT", + }, + pipeline: { + label: "request.lifecycle()", + title: "Construit autour d'un contrat.", + subtitle: "Chaque requête traverse un pipeline typé. La définition serveur devient le type client. Automatiquement. Aucune dérive possible.", + scenarioHappy: ".send({ status: 200 })", + scenarioBadBody: "bodyChecker() → 400", + scenarioNoAuth: "authChecker() → 401", + runBtn: "await .send()", + lifecycleLabel: "[ CYCLE DE VIE DE LA REQUÊTE ]", + contractDivider: "[ contrat ]", + serverLabel: "Serveur · Définition de route", + clientLabel: "Client · Types inférés", + }, + typeflow: { + label: "type Safety =", + title: "Des types qui ne dérivent pas.", + subtitle: "Définissez une fois côté serveur. Consommez partout côté client — pas de synchronisation manuelle, pas de cast hacks. Le contrat, c'est le code.", + tabGet: "Route GET", + tabPost: "POST avec body", + tabAuth: "Protégée par auth", + serverLabel: "Serveur · Définition de route", + clientLabel: "Client · Types inférés", + contractLabel: "[ contrat ]", + }, + checkers: { + label: ".useChecker()", + title: "Les checkers se composent.\nLa logique reste claire.", + subtitle: "Définissez des gardes de validation réutilisables et empilez-les sur n'importe quelle route. Chaque checker s'exécute en séquence. Le handler ne s'exécute que si tous passent.", + bodyName: "bodyChecker", + bodySub: "schéma zod · email + nom", + authName: "authChecker", + authSub: "JWT · Bearer token", + roleName: "roleChecker", + roleSub: "requiert : admin", + handlerName: "handler", + handlerSub: "en attente des checkers", + badgePass: "✓ pass", + badgeChecking: "→ vérification", + badgePending: "en attente", + }, + comparison: { + label: "framework.compare()", + title: "La comparaison honnête.", + subtitle: "DuploJS face aux frameworks que vous connaissez déjà.", + featureCol: "Fonctionnalité", + f1: "Sécurité des types end-to-end", + f2: "Agnostique au runtime", + f3: "Composition de checkers", + f4: "Validation de schéma intégrée", + f5: "Inférence de types client auto", + f6: "TypeScript zéro-config", + note: "Comparaison basée sur les configurations par défaut en 2025. ~ = support partiel.", + }, + benchmark: { + label: "perf.measure()", + title1: "Conçu pour la vitesse.", + title2: "Mesuré en requêtes.", + subtitle: "Benchmark de débit HTTP. autocannon · 10 connexions · 10s · Node.js 20 LTS · handler hello-world.", + footnote: "Comparaison relative. Les résultats réels varient selon le matériel, la charge et la configuration.", + viewBenchmarks: "Voir les benchmarks →", + }, + ecosystem: { + label: "import { * }", + title: "Tout ce dont vous avez besoin.", + subtitle: "Une suite complète de packages TypeScript construits autour de DuploJS.", + coreLabel: "Core", + toolingLabel: "Outils", + copyLabel: "Copier la commande d'installation", + httpDesc: "Interface HTTP 100% typesafe pour serveur et client. Construisez et consommez des APIs HTTP en toute confiance.", + utilsDesc: "Boîte à outils TypeScript utilisée dans l'écosystème DuploJS. Typage fort et primitives composables.", + serverUtilsDesc: "Helpers serveur indépendants du runtime, dans l'écosystème DuploJS.", + zodAcceleratorDesc: "Validation de schéma Zod accélérée pour les applications TypeScript haute performance.", + zodToTsDesc: "Convertit les schémas Zod en types TypeScript. Pont entre validation runtime et typage statique.", + playwrightDesc: "Repense les tests Playwright via des blocs de construction site, page et composant.", + jwtDesc: "Gestionnaire de tokens typé et opinioné pour signer, chiffrer et valider les JWT.", + formDesc: "Bibliothèque de formulaires Vue fortement typée, structure-first, pour construire des formulaires complexes.", + dataParserDesc: "Convertit les schémas dataParser en types TypeScript et en formats JSON Schema.", + }, + gettingStarted: { + label: ".install().run()", + title1: "Opérationnel", + title2: "en 3 étapes.", + subtitle: "Pas de boilerplate. Pas de cérémonie. Juste une API typée qui tourne.", + step1num: "01", + step1title: ".install", + step1desc: "Ajoutez le package core à votre projet.", + step2num: "02", + step2title: ".createRoute", + step2desc: "Déclarez votre première route typesafe.", + step3num: "03", + step3title: ".getClient", + step3desc: "Consommez côté client, entièrement typé — automatiquement.", + readDocs: "Lire la documentation →", + viewExamples: "Voir les exemples", + }, + testimonials: { + label: "/* communauté */", + title1: "Conçu pour ceux qui", + title2: "se soucient de la correction.", + starsLabel: "Étoiles GitHub", + packagesLabel: "Packages", + runtimesLabel: "Runtimes", + typescriptLabel: "TypeScript", + t1quote: "\"Enfin un framework où les types veulent vraiment dire quelque chose. Plus de `as any` pour satisfaire le compilateur.\"", + t1handle: "@devuser1", + t1role: "Ingénieur Backend Senior", + t2quote: "\"Le système de checkers est vraiment ingénieux. Réutilisable, composable, et l'inférence de types fonctionne tout simplement.\"", + t2handle: "@tsdev", + t2role: "Passionné TypeScript", + t3quote: "\"Passé d'Express + Zod + types manuels. DuploJS élimine toute une catégorie de bugs.\"", + t3handle: "@apiengineer", + t3role: "Développeur Full-stack", + t4quote: "\"Le contrat entre serveur et client est automatique. Je ne peux pas assez souligner à quel point cela réduit la charge cognitive.\"", + t4handle: "@contractfirst", + t4role: "Architecte API", + }, + cta: { + label: ".getStarted()", + title1: "Construisez des APIs qui", + title2: "ne mentent pas.", + subtitle1: "Rejoignez les développeurs qui construisent avec confiance.", + subtitle2: "Zéro surprise à l'exécution. Types partout.", + readDocs: "Lire la documentation →", + viewGithub: "Voir sur GitHub", + }, + community: { + label: "@communauté", + title1: "Construit par des développeurs,", + title2: "pour des développeurs.", + discordTitle: "Discord", + discordSub: "Rejoindre la communauté · Obtenir de l'aide · Partager votre travail", + githubTitle: "GitHub", + githubSub: "Explorer le code · Ouvrir des issues · Contribuer", + linkedinTitle: "LinkedIn", + linkedinSub: "Suivre les mises à jour · Actualités · Annonces", + }, + footer: { + tagline: "Construisez des APIs HTTP qui ne mentent pas.", + desc: "Un framework HTTP 100% typesafe pour TypeScript.", + colPackages: "Packages", + colTooling: "Outils", + colCommunity: "Communauté", + colLearn: "Apprendre", + documentation: "Documentation", + gettingStarted: "Démarrage rapide", + apiReference: "Référence API", + changelog: "Journal des modifications", + copyright: "© 2025-present Contributeurs DuploJS", + langSwitch: "English", + langSwitchHref: "/", + }, +}; + +export default fr; diff --git a/app/.vitepress/theme/index.ts b/app/.vitepress/theme/index.ts index d7858f7..be66a5d 100644 --- a/app/.vitepress/theme/index.ts +++ b/app/.vitepress/theme/index.ts @@ -1,21 +1,7 @@ import type { Theme } from "vitepress"; -import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client"; -import DefaultTheme from "vitepress/theme"; -import MainLayout from "./layouts/MainLayout.vue"; -import { WipPage } from "./components/WipPage"; -import TerminalBlock from "./components/TerminalBlock.vue"; -import TheVersus from "./components/TheVersus.vue"; -import "@shikijs/vitepress-twoslash/style.css"; -import "virtual:group-icons.css"; +import Layout from "./Layout.vue"; import "./style.css"; export default { - extends: DefaultTheme, - Layout: MainLayout, - enhanceApp({ app }) { - app.use(TwoslashFloatingVue); - app.component("WipPage", WipPage); - app.component("TerminalBlock", TerminalBlock); - app.component("TheVersus", TheVersus); - }, + Layout, } satisfies Theme; diff --git a/app/.vitepress/theme/layouts/MainLayout.vue b/app/.vitepress/theme/layouts/MainLayout.vue deleted file mode 100644 index 3b6947b..0000000 --- a/app/.vitepress/theme/layouts/MainLayout.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/app/.vitepress/theme/layouts/home/HomeLayout.vue b/app/.vitepress/theme/layouts/home/HomeLayout.vue deleted file mode 100644 index de78706..0000000 --- a/app/.vitepress/theme/layouts/home/HomeLayout.vue +++ /dev/null @@ -1,261 +0,0 @@ - - - diff --git a/app/.vitepress/theme/layouts/home/components/HomePackageLink.vue b/app/.vitepress/theme/layouts/home/components/HomePackageLink.vue deleted file mode 100644 index e198ed2..0000000 --- a/app/.vitepress/theme/layouts/home/components/HomePackageLink.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/app/.vitepress/theme/layouts/home/components/HomeSection.vue b/app/.vitepress/theme/layouts/home/components/HomeSection.vue deleted file mode 100644 index 47f1172..0000000 --- a/app/.vitepress/theme/layouts/home/components/HomeSection.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/app/.vitepress/theme/layouts/home/components/HomeVersusCarousel.vue b/app/.vitepress/theme/layouts/home/components/HomeVersusCarousel.vue deleted file mode 100644 index 0b78644..0000000 --- a/app/.vitepress/theme/layouts/home/components/HomeVersusCarousel.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/app/.vitepress/theme/style.css b/app/.vitepress/theme/style.css index 1a06442..ae8708e 100644 --- a/app/.vitepress/theme/style.css +++ b/app/.vitepress/theme/style.css @@ -1,244 +1,3920 @@ -/** - * Customize default theme styling by overriding CSS variables: - * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css - */ - -/** - * Colors - * - * Each colors have exact same color scale system with 3 levels of solid - * colors with different brightness, and 1 soft color. - * - * - `XXX-1`: The most solid color used mainly for colored text. It must - * satisfy the contrast ratio against when used on top of `XXX-soft`. - * - * - `XXX-2`: The color used mainly for hover state of the button. - * - * - `XXX-3`: The color for solid background, such as bg color of the button. - * It must satisfy the contrast ratio with pure white (#ffffff) text on - * top of it. - * - * - `XXX-soft`: The color used for subtle background such as custom container - * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors - * on top of it. - * - * The soft color must be semi transparent alpha channel. This is crucial - * because it allows adding multiple "soft" colors on top of each other - * to create a accent, such as when having inline code block inside - * custom containers. - * - * - `default`: The color used purely for subtle indication without any - * special meanings attached to it such as bg color for menu hover state. - * - * - `brand`: Used for primary brand colors, such as link text, button with - * brand theme, etc. - * - * - `tip`: Used to indicate useful information. The default theme uses the - * brand color for this by default. - * - * - `warning`: Used to indicate warning to the users. Used in custom - * container, badges, etc. - * - * - `danger`: Used to show error, or dangerous message to the users. Used - * in custom container, badges, etc. - * -------------------------------------------------------------------------- */ - -@import "tailwindcss"; +::selection { + background: rgba(250, 204, 21, 0.28); + color: #050505; +} +[data-theme="light"] ::selection { + background: rgba(180, 83, 9, 0.22); + color: #1a1a1a; +} + +:root { + --canvas: #050505; + --canvas-elevated: #0e0e10; + --surface: #111113; + --surface-soft: #18181b; + --surface-hover: #1e1e22; + --border: #27272a; + --border-soft: #1c1c20; + + --yellow: #FACC15; + --yellow-glow: #FDE047; + --yellow-dim: rgba(250, 204, 21, 0.15); + --amber: #F59E0B; + --amber-dark: #D97706; + + --ink: #fafafa; + --body: #d4d4d8; + --muted: #a1a1aa; + --muted-soft: #71717a; + --code: #e4e4e7; + --terminal-green: #4ade80; + --terminal-red: #f87171; + --kw-blue: #93c5fd; + + --radius-xs: 0px; + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 0px; + + --max-w: 1440px; + --section-py: 120px; + --gutter: 64px; + --content-px: 96px; + + --transition: 0.2s ease; +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; +} + +body { + background: var(--canvas); + color: var(--ink); + font-family: 'Inter', system-ui, sans-serif; + font-size: 16px; + font-weight: 300; + line-height: 1.65; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; +} + +a { + text-decoration: none; + color: inherit; +} + +img, svg { + display: block; + max-width: 100%; +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; + color: inherit; +} + +.rails { + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: var(--max-w); + height: 100%; + pointer-events: none; + z-index: 1; + padding: 0 var(--gutter); +} + +.rails::before, +.rails::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 1px; + background: rgba(255, 255, 255, 0.055); +} + +.rails::before { left: var(--gutter); } +.rails::after { right: var(--gutter); } + +[data-section] { + position: relative; +} + +[data-section]::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: var(--max-w); + height: 1px; + background: rgba(255, 255, 255, 0.055); + pointer-events: none; +} + +[data-section]::after { + content: attr(data-section); + position: absolute; + top: -1px; + left: calc(50% - var(--max-w) / 2 + var(--gutter) + 12px); + font-family: 'Fragment Mono', monospace; + font-size: 9px; + font-weight: 300; + color: var(--muted-soft); + letter-spacing: 0.1em; + line-height: 1; + padding: 3px 0; + pointer-events: none; +} + +@media (max-width: 1300px) { + [data-section]::after { + left: calc(var(--gutter) + 12px); + } +} + +.container { + max-width: var(--max-w); + margin: 0 auto; + padding: 0 var(--content-px); +} + +.section { + padding: var(--section-py) 0; +} + +.section__header { + margin-bottom: 56px; +} + +.section__title { + font-family: 'Space Grotesk', sans-serif; + font-size: clamp(28px, 4vw, 52px); + font-weight: 800; + line-height: 1.05; + letter-spacing: -1.5px; + color: var(--ink); +} + +.section__subtitle { + margin-top: 14px; + font-size: 16px; + font-weight: 300; + color: var(--muted); + line-height: 1.7; + max-width: 560px; +} + +.label { + font-family: 'Inter', system-ui, sans-serif; + font-size: 13px; + font-weight: 400; + letter-spacing: -0.01em; + text-transform: none; + color: var(--yellow); + opacity: 0.7; + display: inline-block; + margin-bottom: 18px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 44px; + padding: 0 22px; + border-radius: var(--radius-sm); + font-family: 'Inter', sans-serif; + font-size: 14px; + font-weight: 600; + line-height: 1; + transition: background var(--transition), border-color var(--transition), + color var(--transition), transform var(--transition), + box-shadow var(--transition); + white-space: nowrap; + cursor: pointer; +} + +.btn--primary { + background: var(--yellow); + color: #0a0a0a; + border: 1px solid transparent; +} + +.btn--primary:hover { + background: var(--yellow-glow); + transform: translateY(-1px); + box-shadow: 0 0 28px rgba(250, 204, 21, 0.28); +} + +.btn--secondary { + background: transparent; + color: var(--ink); + border: 1px solid var(--border); +} + +.btn--secondary:hover { + border-color: var(--muted-soft); + background: var(--surface); +} + +.btn--sm { + height: 36px; + padding: 0 16px; + font-size: 13px; +} + +.nav { + position: sticky; + top: 0; + z-index: 200; + height: 56px; + background: rgba(5, 5, 5, 0.75); + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border-bottom: 1px solid transparent; + transition: border-color 0.3s ease, background 0.3s ease; +} + +.nav.scrolled { + border-bottom-color: rgba(255, 255, 255, 0.06); + background: rgba(5, 5, 5, 0.92); +} + +.nav__inner { + max-width: var(--max-w); + margin: 0 auto; + padding: 0 var(--content-px); + height: 100%; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 0; +} + +.nav__logo { + display: flex; + align-items: center; + gap: 9px; + flex-shrink: 0; +} + +.nav__logo-img { + width: 28px; + height: 28px; + object-fit: contain; + flex-shrink: 0; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.nav__logo:hover .nav__logo-img { + transform: translateY(-3px) scale(1.08); +} + +.nav__logo-text { + font-size: 14px; + font-weight: 600; + color: var(--ink); + letter-spacing: -0.1px; +} + +.nav__center { + display: flex; + align-items: center; + justify-content: center; + gap: 2px; +} + +.nav__link { + font-size: 13px; + font-weight: 300; + color: var(--muted); + padding: 6px 13px; + border-radius: var(--radius-xs); + transition: color var(--transition); + letter-spacing: 0.01em; + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; +} + +.nav__link:hover { + color: var(--ink); +} + +.nav__chevron { + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.nav__dropdown-trigger[aria-expanded="true"] .nav__chevron { + transform: rotate(180deg); +} + +.nav__actions { + display: flex; + align-items: center; + gap: 4px; + justify-content: flex-end; +} + +.nav__icon-link { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: var(--radius-xs); + color: var(--muted-soft); + transition: color var(--transition), background var(--transition); +} + +.nav__icon-link:hover { + color: var(--ink); + background: var(--surface); +} + +.nav__cta { + margin-left: 8px; + flex-shrink: 0; +} + +.nav__dropdown { + position: relative; +} + +.nav__dropdown-panel { + position: absolute; + top: calc(100% + 12px); + left: 50%; + transform: translateX(-50%); + width: 560px; + background: var(--canvas-elevated); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-md); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0,0,0,0.3); + padding: 20px; + display: none; + opacity: 0; + transform: translateX(-50%) translateY(-6px); + transition: opacity 0.18s ease, transform 0.18s ease; + pointer-events: none; +} + +.nav__dropdown-panel.open { + display: block; + opacity: 1; + transform: translateX(-50%) translateY(0); + pointer-events: auto; +} + +.nav__dropdown-inner { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.nav__dropdown-section { + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav__dropdown-label { + font-family: 'Fragment Mono', monospace; + font-size: 9.5px; + font-weight: 300; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--muted-soft); + padding: 0 10px 8px; +} + +.nav__pkg-card { + display: flex; + flex-direction: column; + gap: 2px; + padding: 9px 10px; + border-radius: var(--radius-xs); + transition: background var(--transition); +} + +.nav__pkg-card:hover { + background: var(--surface-hover); +} + +.nav__pkg-name { + font-family: 'Fragment Mono', monospace; + font-size: 11.5px; + font-weight: 400; + color: var(--yellow); + letter-spacing: 0.01em; +} + +.nav__pkg-desc { + font-size: 11.5px; + font-weight: 300; + color: var(--muted-soft); + line-height: 1.4; +} + +.nav__hamburger { + display: none; + flex-direction: column; + gap: 5px; + padding: 6px; +} + +.nav__hamburger span { + display: block; + width: 20px; + height: 1px; + background: var(--muted); + border-radius: 1px; + transition: all 0.3s ease; +} + +.nav__hamburger.open span:nth-child(1) { + transform: translateY(6px) rotate(45deg); +} + +.nav__hamburger.open span:nth-child(2) { + opacity: 0; +} + +.nav__hamburger.open span:nth-child(3) { + transform: translateY(-6px) rotate(-45deg); +} + +.hero { + position: relative; + min-height: 95vh; + display: flex; + align-items: center; + overflow: hidden; +} + +.hero__bg-grid { + position: absolute; + inset: 0; + background-image: radial-gradient( + circle at 1px 1px, + rgba(255, 255, 255, 0.025) 1px, + transparent 0 + ); + background-size: 40px 40px; + pointer-events: none; +} + +.hero__orb { + position: absolute; + top: -15%; + right: -8%; + width: 750px; + height: 750px; + background: radial-gradient( + circle at center, + rgba(250, 204, 21, 0.12) 0%, + rgba(245, 158, 11, 0.06) 40%, + transparent 70% + ); + border-radius: 50%; + pointer-events: none; + animation: orb-drift 10s ease-in-out infinite alternate; +} + +@keyframes orb-drift { + from { transform: translate(0, 0) scale(1); opacity: 0.8; } + to { transform: translate(-40px, 50px) scale(1.08); opacity: 1; } +} + +.hero__inner { + position: relative; + z-index: 1; + max-width: var(--max-w); + margin: 0 auto; + padding: 120px var(--content-px) 100px; + width: 100%; + display: grid; + grid-template-columns: 55% 1fr; + align-items: center; + gap: 48px; +} + +.hero__content { + max-width: 580px; +} + +.hero__headline { + font-family: 'Space Grotesk', sans-serif; + font-size: clamp(46px, 6.5vw, 84px); + font-weight: 800; + line-height: 0.95; + letter-spacing: -2.5px; + color: var(--ink); + margin-bottom: 28px; +} + +.hero__accent { + background: linear-gradient(135deg, var(--yellow) 0%, var(--amber) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero__tagline { + font-size: 17px; + font-weight: 300; + color: var(--muted); + line-height: 1.7; + margin-bottom: 32px; +} + +.hero__ctas { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 32px; +} + +.hero__meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.hero__badge { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + font-weight: 500; + color: var(--muted); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xs); + padding: 3px 8px; + letter-spacing: 0.2px; +} + +.hero__sep { + color: var(--border); +} + +.hero__version { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--muted-soft); + letter-spacing: 0.2px; +} + +.hero__wall { + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; +} + +.iso-svg { + width: 100%; + height: auto; + overflow: visible; + filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.6)) + drop-shadow(0 0 80px rgba(250, 204, 21, 0.07)); +} + +.iso-brick { + cursor: pointer; + animation: brick-drop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +.iso-brick:hover .iso-top { + filter: brightness(1.25); +} + +.iso-brick:hover .iso-east { + filter: brightness(1.15); +} + +.iso-brick:hover .iso-south { + filter: brightness(1.1); +} + +@keyframes brick-drop { + from { + opacity: 0; + transform: translateY(-50px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.iso-tooltip { + position: absolute; + top: 0; + left: 0; + background: var(--surface-soft); + color: var(--ink); + border: 1px solid var(--border); + border-radius: var(--radius-xs); + padding: 4px 10px; + font-family: 'Fragment Mono', monospace; + font-size: 11px; + pointer-events: none; + z-index: 20; + white-space: nowrap; + opacity: 0; + transform: translateY(-2px); + transition: opacity 0.15s ease, transform 0.15s ease; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); +} + +.iso-tooltip.visible { + opacity: 1; + transform: translateY(0); +} + +.hero__scroll-hint { + position: absolute; + bottom: 36px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + z-index: 1; +} + +.hero__scroll-label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--muted-soft); +} + +.hero__scroll-line { + width: 1px; + height: 36px; + background: linear-gradient(to bottom, var(--muted-soft), transparent); + animation: scroll-line 2.2s ease-in-out infinite; +} + +@keyframes scroll-line { + 0% { opacity: 0; transform: scaleY(0); transform-origin: top; } + 40% { opacity: 1; } + 80% { opacity: 0; transform: scaleY(1); transform-origin: top; } + 100% { opacity: 0; } +} + +pre, code { + font-family: 'Fragment Mono', monospace; + font-size: 13px; + line-height: 1.75; + color: var(--code); +} + +pre { + overflow-x: auto; + tab-size: 2; +} + +.ck { color: var(--kw-blue); } +.cp { color: var(--muted); } +.cs { color: var(--terminal-green); } +.cf { color: var(--yellow); } +.ct { color: #fca5a5; } +.cc { color: var(--muted-soft); font-style: italic; } + +.features__grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + padding-bottom: 16px; + padding-right: 16px; +} + +.feature-card { + background: var(--surface); + padding: 40px 36px 36px; + border-radius: var(--radius-md); + position: relative; + cursor: default; + + box-shadow: + 6px 0 0 0 #0e0e12, + 6px 6px 0 0 #0b0b0f, + 0 6px 0 0 #0b0b0f, + inset 0 1px 0 rgba(255,255,255,0.04); + + transition: + transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.28s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.feature-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 3px; + border-radius: var(--radius-md) 0 0 var(--radius-md); + background: linear-gradient(to bottom, var(--yellow), var(--amber)); + opacity: 0.7; + transition: opacity var(--transition); +} + +.feature-card:hover { + transform: translate(-4px, -8px); + box-shadow: + 10px 0 0 0 #0e0e12, + 10px 10px 0 0 #0b0b0f, + 0 10px 0 0 #0b0b0f, + 0 0 40px rgba(250, 204, 21, 0.06), + inset 0 1px 0 rgba(255,255,255,0.07); +} + +.feature-card:hover::before { + opacity: 1; +} + +.feature-card__num { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + font-weight: 500; + letter-spacing: 0.4px; + color: var(--yellow); + display: block; + margin-bottom: 20px; +} + +.feature-card__title { + font-size: 17px; + font-weight: 600; + color: var(--ink); + margin-bottom: 10px; + letter-spacing: -0.2px; +} + +.feature-card__desc { + font-size: 14px; + font-weight: 300; + color: var(--muted); + line-height: 1.7; +} + +.stats-bar, +.logo-cloud { + background: #0a0a0c; +} + +#features { + background: #0d0d10; +} + +#demo { + background: var(--canvas); +} + +#ecosystem { + background: #0d0d10; +} + +#cta-finale { + background: var(--canvas); +} + +.community { + background: #0a0a0c; +} + +.demo { + border-top: 1px solid rgba(255, 255, 255, 0.05); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.terminal { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; +} + +.terminal__header { + background: var(--surface-soft); + border-bottom: 1px solid var(--border); + padding: 0 20px; + height: 46px; + display: flex; + align-items: center; + gap: 16px; +} + +.terminal__dots { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +.terminal__dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.terminal__dot--red { background: #ff5f57; } +.terminal__dot--yellow { background: var(--yellow); } +.terminal__dot--green { background: #28c840; } + +.terminal__tabs { + display: flex; + gap: 2px; +} + +.terminal__tab { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--muted-soft); + padding: 4px 12px; + border-radius: var(--radius-xs); + cursor: pointer; + transition: color var(--transition), background var(--transition); + letter-spacing: 0.1px; +} + +.terminal__tab:hover { + color: var(--muted); +} + +.terminal__tab--active { + color: var(--ink); + background: var(--border); +} + +.terminal__body { + padding: 36px 40px; +} + +.terminal__pane--hidden { + display: none; +} + +.eco-group { + margin-bottom: 40px; +} + +.eco-group:last-child { + margin-bottom: 0; +} + +.eco-group__label { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + font-weight: 500; + letter-spacing: 0.5px; + text-transform: uppercase; + color: var(--muted-soft); + margin-bottom: 16px; + padding-bottom: 14px; + border-bottom: 1px solid var(--border-soft); +} + +.eco-grid { + display: grid; + gap: 1px; + background: var(--border-soft); + border: 1px solid var(--border-soft); + border-radius: var(--radius-md); + overflow: hidden; +} + +.eco-grid--3 { + grid-template-columns: repeat(3, 1fr); +} + +.eco-card { + background: var(--surface); + padding: 28px 28px 24px; + transition: background var(--transition); + position: relative; +} + +.eco-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, var(--yellow), var(--amber)); + opacity: 0; + transition: opacity var(--transition); +} + +.eco-card:hover { + background: var(--surface-hover); +} + +.eco-card:hover::before { + opacity: 1; +} + +.eco-card__top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} + +.eco-card__pkg { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + font-weight: 500; + color: var(--yellow); + line-height: 1.5; + transition: color var(--transition); +} + +.eco-card__pkg:hover { + color: var(--yellow-glow); +} + +.eco-card__actions { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.eco-card__stars { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--amber); + letter-spacing: 0.2px; +} + +.eco-icon-link { + color: var(--muted-soft); + display: flex; + align-items: center; + padding: 3px; + border-radius: 0; + transition: color var(--transition); +} + +.eco-icon-link:hover { + color: var(--yellow); +} + +.eco-card__desc { + font-size: 13px; + font-weight: 300; + color: var(--muted); + line-height: 1.65; + margin-bottom: 12px; +} + +.eco-grid--featured { + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; +} + +.eco-card--featured { + grid-column: 1 / -1; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 32px; + border-top: 3px solid var(--yellow); +} + +.eco-card--featured .eco-card__top { + grid-column: 1 / -1; +} + +.eco-card--featured .eco-card__desc { + font-size: 14px; + color: var(--body); +} + +.eco-card__tags { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-bottom: 14px; +} + +.eco-tag { + font-family: 'Inter', system-ui, sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: -0.01em; + text-transform: none; + padding: 2px 7px; + border: 1px solid; + border-radius: 0; + line-height: 1.6; +} + +.eco-tag--yellow { color: var(--yellow); border-color: rgba(250,204,21,0.3); background: rgba(250,204,21,0.05); } +.eco-tag--green { color: #4ade80; border-color: rgba(74,222,128,0.3); background: rgba(74,222,128,0.05); } +.eco-tag--cyan { color: #22d3ee; border-color: rgba(34,211,238,0.3); background: rgba(34,211,238,0.05); } +.eco-tag--violet { color: #8b5cf6; border-color: rgba(139,92,246,0.3); background: rgba(139,92,246,0.05); } + +.eco-card__install { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 7px 10px; + background: var(--canvas); + border: 1px solid var(--border-soft); + max-height: 0; + overflow: hidden; + opacity: 0; + transition: max-height 0.25s cubic-bezier(0.23,1,0.32,1), + opacity 0.2s ease, + padding 0.25s ease; + padding-top: 0; + padding-bottom: 0; +} + +.eco-card:hover .eco-card__install { + max-height: 40px; + opacity: 1; + padding-top: 7px; + padding-bottom: 7px; +} + +.eco-card__install-cmd { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--body); + letter-spacing: 0.2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.eco-card__copy { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: transparent; + border: 1px solid var(--border); + color: var(--muted-soft); + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; + padding: 0; +} + +.eco-card__copy:hover { + border-color: var(--yellow); + color: var(--yellow); + background: rgba(250,204,21,0.06); +} + +.eco-card__copy-check { + display: none; + color: #4ade80; +} + +.eco-card__copy--copied .eco-card__copy-icon { display: none; } +.eco-card__copy--copied .eco-card__copy-check { display: block; } +.eco-card__copy--copied { border-color: #4ade80 !important; color: #4ade80 !important; } + +.eco-grid .eco-card.reveal:nth-child(1) { transition-delay: 0ms; } +.eco-grid .eco-card.reveal:nth-child(2) { transition-delay: 60ms; } +.eco-grid .eco-card.reveal:nth-child(3) { transition-delay: 120ms; } +.eco-grid .eco-card.reveal:nth-child(4) { transition-delay: 180ms; } +.eco-grid .eco-card.reveal:nth-child(5) { transition-delay: 240ms; } +.eco-grid .eco-card.reveal:nth-child(6) { transition-delay: 300ms; } + +@media (max-width: 768px) { + .eco-grid--featured, + .eco-grid--3 { + grid-template-columns: 1fr; + } + .eco-card--featured { + grid-template-columns: 1fr; + } +} + +.community { + padding: var(--section-py) 0; + background: var(--canvas-elevated); + border-top: 1px solid var(--border-soft); + border-bottom: 1px solid var(--border-soft); +} + +.community__row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + background: var(--border-soft); + border: 1px solid var(--border-soft); + border-radius: var(--radius-md); + overflow: hidden; +} + +.community__card { + display: flex; + align-items: center; + gap: 18px; + padding: 32px 28px; + background: var(--surface); + color: var(--muted); + transition: background var(--transition), color var(--transition); +} + +.community__card:hover { + background: var(--surface-hover); + color: var(--ink); +} + +.community__icon { + flex-shrink: 0; + color: var(--muted-soft); + transition: color var(--transition); +} + +.community__card:hover .community__icon { + color: var(--yellow); +} + +.community__text { + flex: 1; + min-width: 0; +} + +.community__title { + display: block; + font-weight: 600; + font-size: 15px; + color: var(--ink); + margin-bottom: 3px; +} + +.community__sub { + display: block; + font-size: 12px; + color: var(--muted-soft); + line-height: 1.5; +} + +.community__arrow { + font-size: 16px; + color: var(--muted-soft); + flex-shrink: 0; + transition: transform 0.2s ease, color var(--transition); +} + +.community__card:hover .community__arrow { + transform: translateX(4px); + color: var(--yellow); +} + +.footer { + padding: 80px 0 40px; + border-top: 1px solid var(--border-soft); + background: var(--canvas); +} + +.footer__brand-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + margin-bottom: 56px; + padding-bottom: 48px; + border-bottom: 1px solid var(--border-soft); +} + +.footer__brand { display: flex; flex-direction: column; gap: 10px; } + +.footer__logo { + display: inline-flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; +} +.footer__logo img { flex-shrink: 0; } +.footer__logo-name { + font-size: 15px; + font-weight: 700; + color: var(--ink); + letter-spacing: -0.2px; +} + +.footer__tagline { font-size: 15px; font-weight: 600; color: var(--ink); } +.footer__desc { font-size: 13px; color: var(--muted-soft); line-height: 1.5; } + +.footer__social { display: flex; gap: 4px; align-items: center; flex-shrink: 0; } +.footer__social-link { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + color: var(--muted-soft); + border: 1px solid var(--border-soft); + transition: color var(--transition), border-color var(--transition), background var(--transition); +} +.footer__social-link:hover { + color: var(--ink); + border-color: var(--border); + background: var(--surface); +} + +.footer__grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 40px; + margin-bottom: 48px; +} + +.footer__col { display: flex; flex-direction: column; gap: 8px; } + +.footer__col-label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + font-weight: 400; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--muted-soft); + margin-bottom: 6px; +} + +.footer__link { + font-size: 13px; + color: var(--muted); + transition: color var(--transition); + padding: 2px 0; + line-height: 1.4; +} +.footer__link:hover { color: var(--ink); } + +.footer__bottom { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding-top: 24px; + border-top: 1px solid var(--border-soft); +} + +.footer__status-pills { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + +.footer__pill { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--muted-soft); + border: 1px solid var(--border-soft); + padding: 4px 10px; +} +.footer__pill-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.footer__pill--node .footer__pill-dot { background: #4ade80; box-shadow: 0 0 6px rgba(74,222,128,0.5); } +.footer__pill--bun .footer__pill-dot { background: #fb923c; box-shadow: 0 0 6px rgba(251,146,60,0.5); } +.footer__pill--deno .footer__pill-dot { background: #60a5fa; box-shadow: 0 0 6px rgba(96,165,250,0.5); } + +.footer__badge { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--muted-soft); +} + +.footer__sep { color: var(--border); font-size: 13px; } + +.footer__copy { font-size: 12px; color: var(--muted-soft); flex-shrink: 0; } + +.reveal { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.55s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.55s cubic-bezier(0.23, 1, 0.32, 1); +} + +.reveal.visible { + opacity: 1; + transform: none; +} + +.features__grid .feature-card.reveal { + opacity: 0; + transform: translateY(-70px) scale(0.96); + transition: + opacity 0.5s ease, + transform 0.55s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.features__grid .feature-card.reveal.visible { + opacity: 1; + transform: none; +} + +.features__grid .feature-card.reveal:nth-child(1) { transition-delay: 0ms; } +.features__grid .feature-card.reveal:nth-child(2) { transition-delay: 100ms; } +.features__grid .feature-card.reveal:nth-child(3) { transition-delay: 200ms; } +.features__grid .feature-card.reveal:nth-child(4) { transition-delay: 300ms; } + +.eco-grid .reveal:nth-child(1) { transition-delay: 0ms; } +.eco-grid .reveal:nth-child(2) { transition-delay: 60ms; } +.eco-grid .reveal:nth-child(3) { transition-delay: 120ms; } +.eco-grid .reveal:nth-child(4) { transition-delay: 180ms; } +.eco-grid .reveal:nth-child(5) { transition-delay: 240ms; } +.eco-grid .reveal:nth-child(6) { transition-delay: 300ms; } + +.community__row .reveal:nth-child(1) { transition-delay: 0ms; } +.community__row .reveal:nth-child(2) { transition-delay: 80ms; } +.community__row .reveal:nth-child(3) { transition-delay: 160ms; } + +@media (max-width: 1024px) { + :root { + --section-py: 80px; + } + + .hero__inner { + grid-template-columns: 1fr; + padding: 100px var(--content-px) 80px; + text-align: center; + } + + .hero__content { + max-width: 100%; + } + + .hero__tagline { + margin-left: auto; + margin-right: auto; + } + + .hero__ctas { + justify-content: center; + } + + .hero__meta { + justify-content: center; + } + + .hero__wall { + display: none; + } + + .eco-grid--3 { + grid-template-columns: repeat(2, 1fr); + } + + .community__row { + grid-template-columns: 1fr; + } + + .footer__grid { grid-template-columns: repeat(2, 1fr); gap: 32px; } +} + +@media (max-width: 768px) { + :root { + --section-py: 64px; + --gutter: 20px; + --content-px: 24px; + } + + .nav__inner { + grid-template-columns: auto 1fr auto; + } + + .nav__center { + display: none; + } + + .nav__center.open { + display: flex; + flex-direction: column; + align-items: flex-start; + position: fixed; + top: 56px; + left: 0; + right: 0; + bottom: 0; + background: rgba(5, 5, 5, 0.97); + backdrop-filter: blur(20px); + border-top: 1px solid rgba(255,255,255,0.06); + padding: 24px var(--content-px); + gap: 4px; + z-index: 99; + overflow-y: auto; + } + + .nav__center.open .nav__link { + font-size: 16px; + padding: 14px 4px; + color: var(--body); + width: 100%; + } + + .nav__hamburger { + display: flex; + } + + .nav__actions .nav__cta { + display: none; + } + + .nav__icon-link { + display: none; + } + + .hero__headline { + letter-spacing: -1.5px; + } + + .hero__inner { + padding: 80px var(--content-px) 60px; + } + + .features__grid { + grid-template-columns: 1fr; + } + + .eco-grid--3 { + grid-template-columns: 1fr; + } + .terminal__body { + padding: 24px 20px; + } + + .terminal__body pre { + font-size: 12px; + } + + .section__header { + margin-bottom: 36px; + } + + .footer__brand-row { flex-direction: column; } + .footer__grid { grid-template-columns: repeat(2, 1fr); gap: 24px; } + .footer__bottom { flex-direction: column; align-items: flex-start; gap: 12px; } +} + +@media (max-width: 480px) { + .hero__ctas { + flex-direction: column; + } + + .btn { + width: 100%; + } + + .hero__headline { + letter-spacing: -1px; + } +} + +.iso-svg.iso-ready .iso-brick { + animation: none; + transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.iso-svg.iso-ready .iso-brick:hover { + transform: translateY(-16px); +} + +.hero__install { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 16px; + font-family: 'Fragment Mono', monospace; + font-size: 0.85rem; + margin-bottom: 28px; + color: var(--code); +} + +.hero__install-prompt { + color: var(--yellow); + user-select: none; +} + +.hero__install-cursor { + display: inline-block; + width: 2px; + height: 1em; + background: var(--yellow); + animation: blink 1s step-end infinite; + vertical-align: text-bottom; + margin-left: 1px; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +.stats-bar { + border-top: 1px solid var(--border-soft); + border-bottom: 1px solid var(--border-soft); + background: var(--canvas-elevated); + padding: 32px 0; +} + +.stats-bar__inner { + display: flex; + align-items: center; + justify-content: center; + gap: 0; +} + +.stats-bar__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 0 48px; + text-align: center; +} + +.stats-bar__num { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.75rem; + font-weight: 800; + color: var(--yellow); +} + +.stats-bar__label { + font-size: 0.75rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.08em; + font-family: 'Fragment Mono', monospace; +} + +.stats-bar__sep { + width: 1px; + height: 40px; + background: var(--border); + flex-shrink: 0; +} + +@media (max-width: 600px) { + .stats-bar__inner { flex-wrap: wrap; gap: 24px; } + .stats-bar__sep { display: none; } + .stats-bar__item { padding: 0 24px; } +} + +.logo-cloud { + padding: 40px 0; + text-align: center; +} + +.logo-cloud__label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + font-weight: 300; + color: var(--muted-soft); + text-transform: uppercase; + letter-spacing: 0.15em; + margin-bottom: 20px; +} + +.logo-cloud__row { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 12px 32px; +} + +.logo-cloud__item { + font-family: 'Fragment Mono', monospace; + font-size: 0.9rem; + font-weight: 500; + color: var(--muted); + border: 1px solid var(--border); + border-radius: 0; + padding: 6px 16px; + transition: color 0.2s, border-color 0.2s; +} + +.logo-cloud__item:hover { + color: var(--yellow); + border-color: var(--yellow); +} + +.feature-card__code { + margin-top: 16px; + background: var(--canvas); + border: 1px solid var(--border-soft); + border-radius: 0; + padding: 12px 14px; + font-family: 'Fragment Mono', monospace; + font-size: 0.75rem; + color: var(--code); + overflow-x: auto; + line-height: 1.65; + white-space: pre; +} + +.demo__steps { + display: flex; + gap: 0; + border-bottom: 1px solid var(--border); +} + +.demo__step { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 20px; + background: none; + border: none; + border-right: 1px solid var(--border); + cursor: pointer; + text-align: left; + color: var(--muted); + transition: color var(--transition), background var(--transition); +} + +.demo__step:last-child { + border-right: none; +} + +.demo__step:hover { + background: var(--surface-hover); +} + +.demo__step--active { + color: var(--yellow); + background: var(--surface-hover); +} + +.demo__step-num { + font-family: 'Fragment Mono', monospace; + font-size: 0.7rem; + letter-spacing: 0.05em; +} + +.demo__step-label { + font-size: 0.85rem; + font-weight: 600; +} + +.demo__step-bar { + height: 2px; + background: var(--border); + border-radius: 0; + overflow: hidden; + margin-top: 8px; +} + +.demo__step-fill { + height: 100%; + width: 0%; + background: var(--yellow); + border-radius: 0; +} + +.demo__step--active .demo__step-fill { + animation: step-progress 5s linear forwards; +} + +@keyframes step-progress { + from { width: 0%; } + to { width: 100%; } +} + +.terminal__pane { + transition: opacity 0.25s ease; +} + +.terminal__pane--hidden { + display: none; +} + +.cta-finale { + padding: var(--section-py) 0; + text-align: center; + position: relative; + overflow: hidden; +} + +.cta-finale::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient( + ellipse 60% 80% at 50% 50%, + rgba(250, 204, 21, 0.06), + transparent 70% + ); + pointer-events: none; +} + +.cta-finale__inner { + position: relative; + z-index: 1; +} + +.cta-finale__title { + font-family: 'Space Grotesk', sans-serif; + font-size: clamp(2.5rem, 5vw, 4rem); + font-weight: 800; + line-height: 1.1; + margin: 16px 0 20px; + letter-spacing: -0.02em; + color: var(--ink); +} + +.cta-finale__sub { + color: var(--muted); + max-width: 480px; + margin: 0 auto 40px; + line-height: 1.65; + font-size: 1.05rem; +} + +.cta-finale__btns { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + flex-wrap: wrap; +} + +.btn--lg { + height: 52px; + padding: 0 28px; + font-size: 1rem; +} + +.easter-brick { + position: fixed; + pointer-events: none; + z-index: 9999; + width: 36px; + height: 36px; + object-fit: contain; + top: -50px; + animation: brick-rain linear forwards; + filter: drop-shadow(0 4px 8px rgba(0,0,0,0.5)); +} + +@keyframes brick-rain { + 0% { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); } + 15% { transform: translateY(30px) rotate(15deg) scale(1.05); } + 85% { opacity: 1; } + 100% { opacity: 0; transform: translateY(105vh) rotate(var(--spin)) scale(0.8); } +} + +.conviction { + background: var(--canvas); + position: relative; + background-image: + linear-gradient(rgba(255,255,255,0.012) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.012) 1px, transparent 1px); + background-size: 48px 48px; +} + +.conviction__inner { + max-width: var(--max-w); + margin: 0 auto; + padding: 72px var(--content-px); +} + +.conviction__eyebrow { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 56px; +} +.conviction__eyebrow-line { + flex: 1; + height: 1px; + background: var(--border-soft); +} +.conviction__eyebrow-text { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--muted-soft); + flex-shrink: 0; +} + +.conviction__stats { + display: grid; + grid-template-columns: 1fr 1px 1fr 1px 2fr 1px 1fr 1px 1fr; + align-items: center; + gap: 0; + margin-bottom: 48px; +} + +.conviction__divider { + height: 80px; + background: var(--border-soft); + width: 1px; +} + +.conviction__stat { + padding: 0 48px; +} +.conviction__stat:first-child { padding-left: 0; } +.conviction__stat:last-child { padding-right: 0; } + +.conviction__num { + font-size: 60px; + font-weight: 800; + line-height: 1; + letter-spacing: -2px; + color: var(--ink); + font-family: Inter, sans-serif; +} +.conviction__stat:first-child .conviction__num { color: var(--yellow); } +.conviction__unit { + font-size: 22px; + font-weight: 400; + color: var(--muted-soft); + letter-spacing: 0; +} + +.conviction__label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--muted-soft); + margin-top: 8px; +} + +.conviction__quote { + padding: 0 40px; + text-align: center; + border: none; + margin: 0; +} +.conviction__quote p { + font-size: 15px; + color: var(--body); + line-height: 1.75; + font-style: italic; +} +.conviction__quote em { + color: var(--yellow); + font-style: normal; + font-weight: 600; +} +.conviction__quote cite { + display: block; + font-family: 'Fragment Mono', monospace; + font-size: 10px; + color: var(--muted-soft); + margin-top: 10px; + text-transform: uppercase; + letter-spacing: 1px; + font-style: normal; +} + +.conviction__runtime-row { + display: flex; + align-items: center; + gap: 16px; + border-top: 1px solid var(--border-soft); + padding-top: 28px; +} +.conviction__runtime-label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--muted-soft); + flex-shrink: 0; +} +.conviction__pills { + display: flex; + gap: 8px; + flex: 1; +} +.conviction__pill { + display: inline-flex; + align-items: center; + gap: 7px; + border: 1px solid var(--border); + padding: 6px 14px; + font-size: 13px; + font-weight: 500; + color: var(--body); +} +.conviction__pill-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.conviction__pill--node .conviction__pill-dot { + background: #4ade80; + box-shadow: 0 0 8px rgba(74,222,128,0.6); +} +.conviction__pill--bun .conviction__pill-dot { + background: #fb923c; + box-shadow: 0 0 8px rgba(251,146,60,0.6); +} +.conviction__pill--deno .conviction__pill-dot { + background: #60a5fa; + box-shadow: 0 0 8px rgba(96,165,250,0.6); +} +.conviction__mit { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--muted-soft); + border: 1px solid var(--border-soft); + padding: 6px 12px; + margin-left: auto; + flex-shrink: 0; +} + +.pipeline-section { background: var(--surface); } + +.pipeline-diagram { + margin-bottom: 48px; + padding: 40px 0; + border: 1px solid var(--border-soft); + background: var(--canvas); + position: relative; + overflow: hidden; +} + +.pipeline__lifecycle-label { + text-align: center; + font-family: 'Fragment Mono', monospace; + font-size: 10px; + color: var(--muted-soft); + text-transform: uppercase; + letter-spacing: 2px; + margin-bottom: 28px; +} + +.pipeline__nodes { + display: flex; + align-items: center; + justify-content: center; + padding: 0 48px; + gap: 0; +} + +.pipe-node { + display: flex; + flex-direction: column; + align-items: center; + flex-shrink: 0; +} + +.pipe-box { + width: 80px; + height: 52px; + border: 1px solid var(--border); + background: var(--surface); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: 'Inter', system-ui, sans-serif; + font-size: 8px; + font-weight: 500; + color: var(--muted); + text-align: center; + line-height: 1.4; + letter-spacing: -0.01em; + transition: border-color 0.4s ease, box-shadow 0.4s ease, color 0.35s ease, background 0.4s ease; +} +.pipe-box__sub { + font-size: 8px; + color: var(--muted-soft); + display: block; +} +.pipe-box--checker { + border-color: var(--yellow); + color: var(--yellow); + background: rgba(250,204,21,0.05); + box-shadow: 0 0 16px rgba(250,204,21,0.08); +} +.pipe-box--checker .pipe-box__sub { color: rgba(250,204,21,0.5); } +.pipe-box--handler { + border-color: #4ade80; + color: #4ade80; + background: rgba(74,222,128,0.05); + box-shadow: 0 0 16px rgba(74,222,128,0.08); +} +.pipe-box--handler .pipe-box__sub { color: rgba(74,222,128,0.5); } +.pipe-box--response { + border-color: #4ade80; + color: #4ade80; + background: rgba(74,222,128,0.05); + box-shadow: 0 0 16px rgba(74,222,128,0.08); +} +.pipe-box--response .pipe-box__sub { color: rgba(74,222,128,0.5); } + +.pipe-connector { + flex: 1; + position: relative; + height: 1px; + min-width: 28px; + max-width: 72px; + overflow: visible; +} +.pipe-connector__line { + width: 100%; + height: 1px; + background: var(--border); +} +.pipe-connector__line--yellow { + background: linear-gradient(90deg, var(--border), var(--yellow), var(--border)); +} +.pipe-connector__line--green { + background: linear-gradient(90deg, var(--border), #4ade80, var(--border)); +} + +.pipe-dot { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + width: 6px; + height: 6px; + border-radius: 50%; + animation: pipeDot 2.4s linear infinite; +} +.pipe-dot--yellow { + background: var(--yellow); + box-shadow: 0 0 8px var(--yellow); +} +.pipe-dot--green { + background: #4ade80; + box-shadow: 0 0 8px #4ade80; +} + +@keyframes pipeDot { + 0% { left: 0; opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { left: 100%; opacity: 0; } +} + +.contract-split { + display: grid; + grid-template-columns: 1fr auto 1fr; + border: 1px solid var(--border-soft); + background: var(--surface); + overflow: hidden; +} + +.contract-pane__label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--muted-soft); + padding: 14px 24px; + border-bottom: 1px solid var(--border-soft); + background: var(--canvas-elevated); +} +.contract-pane__code { + margin: 0; + padding: 24px; + font-family: 'Fragment Mono', monospace; + font-size: 13px; + line-height: 1.65; + overflow-x: auto; + background: transparent; + border: none; +} +.contract-ok { + display: block; + background: rgba(74,222,128,0.06); + margin: 0 -24px; + padding: 0 24px; +} + +.contract-divider { + display: flex; + flex-direction: column; + align-items: center; + width: 1px; + background: var(--border-soft); +} +.contract-divider__line { + flex: 1; + width: 1px; + background: var(--border-soft); +} +.contract-divider__label { + font-family: 'Fragment Mono', monospace; + font-size: 9px; + color: var(--yellow); + writing-mode: vertical-rl; + text-orientation: mixed; + letter-spacing: 1px; + padding: 12px 0; + white-space: nowrap; + background: var(--surface); +} + +.checkers-section { background: var(--canvas-elevated); } + +.checkers-layout { + display: grid; + grid-template-columns: 45% 55%; + border: 1px solid var(--border-soft); +} + +.checker-stack { + padding: 24px; + border-right: 1px solid var(--border-soft); + display: flex; + flex-direction: column; + background: var(--canvas-elevated); +} + +.checker-block { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border: 1px solid var(--border); + background: var(--surface); + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} +.checker-block:hover:not(.checker-block--pending) { + border-color: var(--muted-soft); + transition: border-color 0.15s, background 0.15s, + transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); +} +.checker-block--active { + border-color: var(--yellow); + background: rgba(250,204,21,0.05); +} +.checker-block--active .checker-block__name { color: var(--yellow); } +.checker-block--active .checker-block__icon { border-color: var(--yellow); color: var(--yellow); } +.checker-block--pending { opacity: 0.4; cursor: default; } + +.checker-block__icon { + width: 32px; + height: 32px; + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + flex-shrink: 0; + color: var(--muted-soft); + font-style: normal; +} + +.checker-block__info { flex: 1; min-width: 0; } +.checker-block__name { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--code); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.checker-block__sub { + font-size: 11px; + color: var(--muted-soft); + margin-top: 2px; +} + +.checker-block__badge { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + border: 1px solid var(--border-soft); + color: var(--muted-soft); + padding: 3px 8px; + flex-shrink: 0; +} +.checker-block__badge--pass { + border-color: rgba(74,222,128,0.4); + color: #4ade80; +} +.checker-block__badge--checking { + border-color: rgba(250,204,21,0.4); + color: var(--yellow); +} + +.checker-connector { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-left: 32px; + height: 20px; +} +.checker-connector__line { + flex: 1; + width: 1px; + background: var(--border-soft); +} +.checker-connector__dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--border); + flex-shrink: 0; + margin-left: -2px; +} + +.checker-code-panel { + display: flex; + flex-direction: column; + background: var(--surface); +} + +.checker-tabs { + display: flex; + border-bottom: 1px solid var(--border-soft); + background: var(--canvas-elevated); + overflow-x: auto; +} +.checker-tab { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--muted-soft); + padding: 12px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: color 0.15s, border-color 0.15s; +} +.checker-tab:hover { color: var(--body); } +.checker-tab--active { + color: var(--yellow); + border-bottom-color: var(--yellow); +} + +.checker-code { + flex: 1; + padding: 24px; +} +.checker-code pre { + margin: 0; + font-family: 'Fragment Mono', monospace; + font-size: 13px; + line-height: 1.7; +} +.checker-code--hidden { display: none; } + +.comparison-section { background: var(--canvas); } +.benchmark-section { background: var(--surface); } +.testimonials-section { background: var(--canvas-elevated); } + +.comparison-wrap { overflow-x: auto; } + +.comparison-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.comparison-table thead tr { + border-bottom: 1px solid var(--border); +} +.comparison-table th { + padding: 16px 20px; + text-align: center; + font-weight: 400; + vertical-align: bottom; +} +.comparison-table th:first-child { text-align: left; } + +.comparison-table__feature-col { min-width: 220px; } + +.comparison-table__framework-name { + font-family: 'Fragment Mono', monospace; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--muted-soft); +} +.comparison-table__framework-name--duplo { color: var(--yellow); } + +.comparison-table__duplo-col { + background: rgba(250,204,21,0.03); + border-top: 2px solid var(--yellow); +} +thead .comparison-table__duplo-col { padding-top: 18px; } + +.comparison-table tbody tr { + border-bottom: 1px solid var(--border-soft); + transition: background 0.12s; +} +.comparison-table tbody tr:hover { background: var(--surface-hover); } +.comparison-table tbody tr:last-child { border-bottom: none; } + +.comparison-table td { + padding: 16px 20px; + text-align: center; + vertical-align: middle; +} +.comparison-table__feature { + text-align: left !important; + color: var(--body); + font-size: 14px; +} + +.cell-yes { color: #4ade80; font-size: 16px; font-weight: 600; } +.cell-no { color: var(--muted-soft); font-size: 16px; } +.cell-partial { color: var(--amber); font-size: 16px; font-weight: 600; } + +.comparison-table__note { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + color: var(--muted-soft); + margin-top: 16px; + letter-spacing: 0.3px; +} + +.contract-pane__code .ck, .checker-code .ck { color: var(--kw-blue); } +.contract-pane__code .cf, .checker-code .cf { color: var(--yellow); } +.contract-pane__code .cs, .checker-code .cs { color: var(--terminal-green); } +.contract-pane__code .ct, .checker-code .ct { color: #c4b5fd; } +.contract-pane__code .cc, .checker-code .cc { color: var(--muted-soft); } +.contract-pane__code .cp, .checker-code .cp { color: var(--muted); } + +@media (max-width: 1024px) { + .conviction__stats { + grid-template-columns: 1fr 1px 1fr 1px 1fr 1px 1fr; + } + .conviction__quote { display: none; } + .conviction__stat { padding: 0 24px; } + .conviction__num { font-size: 48px; } + + .pipeline__nodes { padding: 0 16px; } + .pipe-box { width: 64px; height: 46px; font-size: 8px; } + .pipe-connector { min-width: 16px; max-width: 40px; } + + .checkers-layout { grid-template-columns: 1fr; } + .checker-stack { border-right: none; border-bottom: 1px solid var(--border-soft); } + + .contract-split { grid-template-columns: 1fr; } + .contract-divider { + flex-direction: row; + width: 100%; + height: auto; + padding: 10px 24px; + border-top: 1px solid var(--border-soft); + border-bottom: 1px solid var(--border-soft); + background: var(--canvas-elevated); + } + .contract-divider__label { + writing-mode: horizontal-tb; + padding: 0 12px; + } +} + +@media (max-width: 768px) { + .conviction__stats { + grid-template-columns: 1fr 1fr; + gap: 24px; + } + .conviction__divider { display: none; } + .conviction__stat { padding: 0; } + .conviction__num { font-size: 40px; } + .conviction__runtime-row { flex-wrap: wrap; gap: 12px; } + .conviction__pills { flex-wrap: wrap; } + + .pipeline__nodes { flex-wrap: wrap; justify-content: flex-start; gap: 8px; } + .pipe-connector { display: none; } + + .comparison-table th, + .comparison-table td { padding: 12px 10px; font-size: 12px; } + .comparison-table__feature { font-size: 12px; } + .cell-yes, .cell-no, .cell-partial { font-size: 14px; } +} + +@keyframes enter-nav { + from { transform: translateY(-100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +@keyframes enter-fade-up { + from { opacity: 0; transform: translateY(22px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes enter-fade-up-spring { + from { opacity: 0; transform: translateY(32px) scale(0.96); } + 65% { transform: translateY(-5px) scale(1.01); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes enter-slide-left { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes enter-scale-pop { + from { opacity: 0; transform: scale(0.86) translateY(8px); } + 62% { transform: scale(1.05) translateY(-2px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +@keyframes enter-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes accent-shimmer { + 0%, 100% { filter: brightness(1); } + 50% { filter: brightness(1.45) saturate(1.2); } +} + +@keyframes label-prefix-tick { + 0%, 80%, 100% { opacity: 1; } + 40% { opacity: 0.2; } +} + +@keyframes eyebrow-expand { + from { opacity: 0; transform: scaleX(0); } + to { opacity: 1; transform: scaleX(1); } +} + +@keyframes eyebrow-text-in { + from { opacity: 0; letter-spacing: 6px; } + to { opacity: 1; letter-spacing: 2px; } +} + +.nav { + animation: enter-nav 0.6s cubic-bezier(0.23, 1, 0.32, 1) 0.04s both; +} + +.hero .label { + animation: enter-slide-left 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0.22s both; +} + +.hero__headline { + animation: enter-fade-up-spring 0.72s cubic-bezier(0.23, 1, 0.32, 1) 0.34s both; +} + +.hero__accent { + animation: accent-shimmer 2.4s ease 1.6s 2; +} + +.hero__tagline { + animation: enter-fade-up 0.55s cubic-bezier(0.23, 1, 0.32, 1) 0.54s both; +} + +.hero__install { + animation: enter-scale-pop 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) 0.7s both; +} + +.hero__ctas { + animation: enter-fade-up 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0.86s both; +} + +.hero__meta { + animation: enter-fade-in 0.5s ease 1.04s both; +} + +.hero__wall { + animation: enter-fade-in 0.7s ease 0.18s both; +} + +.hero__bg-grid { + animation: enter-fade-in 1s ease 0s both; +} + +.hero__scroll-hint { + animation: enter-fade-in 0.55s ease 1.8s both; +} + +.section__header.reveal { + transform: translate(-10px, 18px); + transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} +.section__header.reveal.visible { + opacity: 1; + transform: none; +} + +.conviction__stats.reveal { + transform: translateY(28px); + transition: opacity 0.65s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.65s cubic-bezier(0.34, 1.4, 0.64, 1); +} +.conviction__stats.reveal.visible { + opacity: 1; + transform: none; +} + +.conviction__runtime-row.reveal { + transform: translateX(18px); + transition: opacity 0.5s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.5s cubic-bezier(0.23, 1, 0.32, 1); +} +.conviction__runtime-row.reveal.visible { + opacity: 1; + transform: none; +} + +.conviction__eyebrow { + opacity: 0; +} +.conviction__eyebrow.eyebrow-visible .conviction__eyebrow-line { + animation: eyebrow-expand 0.6s cubic-bezier(0.34, 1.4, 0.64, 1) both; + transform-origin: center; +} +.conviction__eyebrow.eyebrow-visible .conviction__eyebrow-line:last-child { + animation-delay: 0.08s; +} +.conviction__eyebrow.eyebrow-visible .conviction__eyebrow-text { + animation: eyebrow-text-in 0.55s cubic-bezier(0.23, 1, 0.32, 1) 0.12s both; +} +.conviction__eyebrow.eyebrow-visible { + opacity: 1; +} + + +.pipeline-diagram.reveal { + transform: translateY(24px) scale(0.99); + transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.6s cubic-bezier(0.34, 1.3, 0.64, 1); +} +.pipeline-diagram.reveal.visible { + opacity: 1; + transform: none; +} + +.contract-split.reveal { + transform: translateY(16px); + transition: opacity 0.55s cubic-bezier(0.23, 1, 0.32, 1) 0.15s, + transform 0.55s cubic-bezier(0.23, 1, 0.32, 1) 0.15s; +} +.contract-split.reveal.visible { + opacity: 1; + transform: none; +} + +.checkers-layout.reveal { + transform: translateX(16px); + transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} +.checkers-layout.reveal.visible { + opacity: 1; + transform: none; +} + +.comparison-wrap.reveal { + transform: scale(0.98) translateY(12px); + transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.6s cubic-bezier(0.34, 1.3, 0.64, 1); +} +.comparison-wrap.reveal.visible { + opacity: 1; + transform: none; +} + +.community .section__header.reveal { + transform: translate(10px, 14px); +} +.community .section__header.reveal.visible { + transform: none; +} + +.cta-finale__inner.reveal { + transform: scale(0.96) translateY(14px); + transition: opacity 0.65s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.65s cubic-bezier(0.34, 1.5, 0.64, 1); +} +.cta-finale__inner.reveal.visible { + opacity: 1; + transform: none; +} + + +@media (prefers-reduced-motion: reduce) { + .nav, + .hero .label, .hero__headline, .hero__accent, .hero__tagline, + .hero__install, .hero__ctas, .hero__meta, .hero__wall, .hero__bg-grid, + .hero__scroll-hint { + animation: none !important; + opacity: 1 !important; + transform: none !important; + } + .section__header.reveal, + .conviction__stats.reveal, .conviction__runtime-row.reveal, + .pipeline-diagram.reveal, .contract-split.reveal, + .checkers-layout.reveal, .comparison-wrap.reveal, + .cta-finale__inner.reveal { + transition: opacity 0.2s ease !important; + transform: none !important; + } +} + +[data-theme="light"] { + --canvas: #f5f5f4; + --canvas-elevated: #e4e4e0; + --surface: #ffffff; + --surface-soft: #e8e7e5; + --surface-hover: #dcdcda; + --border: #b0aeab; + --border-soft: #c4c3be; + + --yellow: #b45309; + --yellow-glow: #d97706; + --yellow-dim: rgba(180, 83, 9, 0.1); + --amber: #92400e; + --amber-dark: #78350f; + + --ink: #111110; + --body: #292524; + --muted: #57534e; + --muted-soft: #78716c; + --code: #1c1917; + + --terminal-green: #15803d; + --terminal-red: #b91c1c; + --kw-blue: #1d4ed8; +} + +[data-theme="light"] .nav { + background: rgba(245, 245, 244, 0.82); +} +[data-theme="light"] .nav.scrolled { + background: rgba(245, 245, 244, 0.96); + border-bottom-color: rgba(0, 0, 0, 0.08); +} + +[data-theme="light"] .rails::before, +[data-theme="light"] .rails::after { + background: rgba(0, 0, 0, 0.07); +} + +[data-theme="light"] [data-section]::before { + background: rgba(0, 0, 0, 0.07); +} + +[data-theme="light"] .hero__bg-grid { + background-image: radial-gradient( + circle at 1px 1px, + rgba(0, 0, 0, 0.12) 1px, + transparent 0 + ); +} +[data-theme="light"] .hero__orb { + background: radial-gradient( + circle at center, + rgba(180, 83, 9, 0.09) 0%, + rgba(146, 64, 14, 0.04) 40%, + transparent 70% + ); +} + +[data-theme="light"] .iso-svg { + filter: drop-shadow(0 14px 36px rgba(0, 0, 0, 0.1)) + drop-shadow(0 0 48px rgba(180, 83, 9, 0.06)); +} + +[data-theme="light"] .nav__dropdown-panel { + border-color: rgba(0, 0, 0, 0.09); + box-shadow: 0 20px 48px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0,0,0,0.05); +} + +[data-theme="light"] .nav__center.open { + background: rgba(245, 245, 244, 0.98); + border-top-color: rgba(0, 0, 0, 0.08); +} + +[data-theme="light"] .stats-bar, +[data-theme="light"] .logo-cloud { + background: #eeede9; +} +[data-theme="light"] #features { background: #e8e8e3; } +[data-theme="light"] #ecosystem { background: #eeede9; } +[data-theme="light"] .community { + background: #eeede9; + border-top-color: var(--border-soft); + border-bottom-color: var(--border-soft); +} +[data-theme="light"] .pipeline-section { background: #eeede9; } +[data-theme="light"] .checkers-section { background: #e8e8e3; } +[data-theme="light"] .comparison-section { background: var(--canvas); } +[data-theme="light"] .footer { background: var(--canvas); } -:root { - --vp-c-default-1: var(--vp-c-gray-1); - --vp-c-default-2: var(--vp-c-gray-2); - --vp-c-default-3: var(--vp-c-gray-3); - --vp-c-default-soft: var(--vp-c-gray-soft); +[data-theme="light"] .conviction { + background-image: + linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px); +} + +[data-theme="light"] .feature-card { + box-shadow: + 6px 0 0 0 #ccc9c4, + 6px 6px 0 0 #c0bdb8, + 0 6px 0 0 #c0bdb8, + inset 0 1px 0 rgba(255,255,255,0.6); +} +[data-theme="light"] .feature-card:hover { + box-shadow: + 10px 0 0 0 #ccc9c4, + 10px 10px 0 0 #c0bdb8, + 0 10px 0 0 #c0bdb8, + 0 0 28px rgba(180, 83, 9, 0.05), + inset 0 1px 0 rgba(255,255,255,0.8); +} - --vp-c-brand-1: #EBC32A; - --vp-c-brand-2: #FFEB53; - --vp-c-brand-3: #D4AC24; - --vp-c-brand-soft: rgba(235, 195, 42, 0.14); +[data-theme="light"] .pipeline-diagram { background: var(--surface); } +[data-theme="light"] .pipe-box { + background: var(--surface-soft); + color: var(--body); +} +[data-theme="light"] .pipe-box .pipe-box__sub { color: var(--muted); } +[data-theme="light"] .pipe-box--checker { + color: #b45309; + border-color: #b45309; + background: rgba(180, 83, 9, 0.06); + box-shadow: 0 0 14px rgba(180, 83, 9, 0.06); +} +[data-theme="light"] .pipe-box--checker .pipe-box__sub { color: rgba(180,83,9,0.6); } +[data-theme="light"] .pipe-box--handler, +[data-theme="light"] .pipe-box--response { + color: #15803d; + border-color: #15803d; + background: rgba(21, 128, 61, 0.06); + box-shadow: 0 0 14px rgba(21, 128, 61, 0.06); +} +[data-theme="light"] .pipe-box--handler .pipe-box__sub, +[data-theme="light"] .pipe-box--response .pipe-box__sub { color: rgba(21,128,61,0.65); } +[data-theme="light"] .pipe-connector__line { + background: var(--border); +} +[data-theme="light"] .pipe-connector__line--yellow { + background: linear-gradient(90deg, var(--border), var(--yellow), var(--border)); +} +[data-theme="light"] .pipe-connector__line--green { + background: linear-gradient(90deg, var(--border), #15803d, var(--border)); +} - --vp-c-brand-dark: #B8941F; - --vp-c-brand-darker: #9A7D1A; +[data-theme="light"] .checker-stack { background: var(--canvas-elevated); } +[data-theme="light"] .checker-code-panel { background: var(--surface); } +[data-theme="light"] .checker-tabs { background: var(--canvas-elevated); } +[data-theme="light"] .contract-pane__label { background: var(--canvas-elevated); } +[data-theme="light"] .contract-ok { + background: rgba(21, 128, 61, 0.07); +} - --vp-c-tip-1: var(--vp-c-brand-1); - --vp-c-tip-2: var(--vp-c-brand-2); - --vp-c-tip-3: var(--vp-c-brand-3); - --vp-c-tip-soft: var(--vp-c-brand-soft); +[data-theme="light"] .comparison-table__duplo-col { + background: rgba(180, 83, 9, 0.04); +} +[data-theme="light"] .comparison-table tbody tr:hover { + background: var(--surface-hover); +} - --vp-c-warning-1: #f59e0b; - --vp-c-warning-2: #fbbf24; - --vp-c-warning-3: #d97706; - --vp-c-warning-soft: rgba(245, 158, 11, 0.14); +[data-theme="light"] .ct { color: #be123c; } - --vp-c-danger-1: var(--vp-c-red-1); - --vp-c-danger-2: var(--vp-c-red-2); - --vp-c-danger-3: var(--vp-c-red-3); - --vp-c-danger-soft: var(--vp-c-red-soft); +[data-theme="light"] .cta-finale { + background: #ebe9e4; +} +[data-theme="light"] .cta-finale::before { + background: radial-gradient( + ellipse 60% 80% at 50% 50%, + rgba(180, 83, 9, 0.05), + transparent 70% + ); +} +[data-theme="light"] .cta-block { + background: rgba(20, 20, 20, calc(var(--op, 0.06) * 1.2 + var(--glow, 0) * 0.08)); } -/** - * Component: Button - * -------------------------------------------------------------------------- */ +[data-theme="light"] .iso-tooltip { + box-shadow: 0 4px 16px rgba(0,0,0,0.12); +} -:root { - --vp-button-brand-border: transparent; - --vp-button-brand-text: var(--vp-c-white); - --vp-button-brand-bg: var(--vp-c-brand-3); - --vp-button-brand-hover-border: transparent; - --vp-button-brand-hover-text: var(--vp-c-white); - --vp-button-brand-hover-bg: var(--vp-c-brand-2); - --vp-button-brand-active-border: transparent; - --vp-button-brand-active-text: var(--vp-c-white); - --vp-button-brand-active-bg: var(--vp-c-brand-1); +[data-theme="light"] .contract-pane__code, +[data-theme="light"] .checker-code pre, +[data-theme="light"] pre code { + background: #f0f0ee; + border-color: #b0aeab; } -/** - * Component: Layout - * -------------------------------------------------------------------------- */ +[data-theme="light"] .typeflow-section { background: var(--canvas); } +[data-theme="light"] .steps-section { background: var(--canvas); } -.VPContent.layout-wide, -.layout-wide .VPContent { - max-width: 100% !important; +[data-theme="light"] .benchmark-section, +[data-theme="light"] .testimonials-section { + background: #2a2a2e; + --ink: #fafafa; + --body: #d4d4d8; + --muted: #a1a1aa; + --muted-soft: #71717a; + --border-soft: rgba(255,255,255,0.08); + --border: rgba(255,255,255,0.14); + --surface: #1e1e22; + --surface-soft: #222226; + --surface-hover:#262628; + --yellow: #FACC15; + --yellow-glow: #FDE047; + --yellow-dim: rgba(250,204,21,0.15); +} +[data-theme="light"] .benchmark-section .bench-bar { + background: rgba(255, 255, 255, 0.14); + border-color: rgba(255, 255, 255, 0.08); } -.layout-wide .VPDoc .container, -.layout-wide .VPDoc .content, -.layout-wide .vp-doc { - max-width: 100% !important; +[data-theme="light"] .nav__hamburger span { + background: var(--muted); } -.layout-wide .VPDoc.has-aside .content-container { - max-width: 80% !important; +[data-theme="light"] .label { + opacity: 1; } -.layout-wide .VPDoc:not(.has-sidebar) .content { - max-width: 1200px !important; +[data-theme="light"] .pipe__run-btn { + color: var(--body); + border-color: var(--border); } -.layout-wide .vp-doc .container { - max-width: 1200px !important; +[data-theme="light"] .pipe__run-btn--success { border-color: #15803d; color: #15803d; } +[data-theme="light"] .pipe__run-btn--error { border-color: #b91c1c; color: #b91c1c; } + +[data-theme="light"] .pipe__scenario { color: var(--muted); border-color: var(--border-soft); } +[data-theme="light"] .pipe__scenario--active { color: var(--yellow); border-color: var(--yellow); background: rgba(180,83,9,0.07); } + +[data-theme="light"] .pipe-node__status--ok { color: #15803d; } +[data-theme="light"] .pipe-node__status--running { color: var(--muted); } +[data-theme="light"] .pipe-node__status--error { color: #b91c1c; } + +[data-theme="light"] .pipe-node--lit-error { + border-color: #b91c1c !important; + box-shadow: 0 0 12px rgba(185,28,28,0.22) !important; } -/** - * Component: Home - * -------------------------------------------------------------------------- */ +[data-theme="light"] .pipe-node--response-ok .pipe-box--response { border-color: #15803d; color: #15803d; background: rgba(21,128,61,0.07); } +[data-theme="light"] .pipe-node--response-warn .pipe-box--response { border-color: #c2410c; color: #c2410c; background: rgba(194,65,12,0.07); } +[data-theme="light"] .pipe-node--response-err .pipe-box--response { border-color: #b91c1c; color: #b91c1c; background: rgba(185,28,28,0.07); } -:root { - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: -webkit-linear-gradient( - 120deg, - #B8941F 10%, - #EBC32A 40%, - #FFEB53 90% - ); +[data-theme="light"] .pipe__status-badge--ok { color: #15803d; border-color: rgba(21,128,61,0.4); } +[data-theme="light"] .pipe__status-badge--warn { color: #c2410c; border-color: rgba(194,65,12,0.4); } +[data-theme="light"] .pipe__status-badge--err { color: #b91c1c; border-color: rgba(185,28,28,0.4); } - --vp-home-hero-image-background-image: linear-gradient( - -45deg, - #EBC32A 50%, - #FFEB53 50% - ); - --vp-home-hero-image-filter: blur(44px); +[data-theme="light"] .eco-tag--green { color: #15803d; border-color: rgba(21,128,61,0.35); background: rgba(21,128,61,0.07); } +[data-theme="light"] .eco-tag--cyan { color: #0369a1; border-color: rgba(3,105,161,0.3); background: rgba(3,105,161,0.06); } +[data-theme="light"] .eco-tag--violet { color: #6d28d9; border-color: rgba(109,40,217,0.3); background: rgba(109,40,217,0.07); } + +[data-theme="light"] .eco-card__copy--copied { border-color: #15803d !important; color: #15803d !important; } + +.nav__theme-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border: none; + background: none; + color: var(--muted-soft); + cursor: pointer; + border-radius: var(--radius-xs); + transition: color var(--transition), background var(--transition), transform 0.3s ease; + flex-shrink: 0; +} +.nav__theme-toggle:hover { + color: var(--ink); + background: var(--surface-soft); + transform: rotate(15deg); } +.nav__theme-toggle svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} +.nav__theme-toggle .icon-sun { display: none; } +.nav__theme-toggle .icon-moon { display: block; } +[data-theme="light"] .nav__theme-toggle .icon-sun { display: block; } +[data-theme="light"] .nav__theme-toggle .icon-moon { display: none; } -@media (min-width: 640px) { - :root { - --vp-home-hero-image-filter: blur(56px); - } +.eco-grid { overflow: visible; } +.community__row { overflow: visible; } + +.eco-card { + --mx: 0px; + --my: 0px; + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease, + background var(--transition); + z-index: 0; } +.eco-card:hover { + background: var(--surface-hover); + transform: translateY(calc(-4px + var(--my, 0px) * 0.3)) translateX(calc(-2px + var(--mx, 0px) * 0.3)); + box-shadow: 3px 6px 0 var(--yellow); + z-index: 1; +} +.eco-card:hover::before { opacity: 1; } -@media (min-width: 960px) { - :root { - --vp-home-hero-image-filter: blur(68px); - } +.community__card { + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease, + background var(--transition), + color var(--transition); +} +.community__card:hover { + background: var(--surface-hover); + color: var(--ink); + transform: translateY(-4px) translateX(-2px); + box-shadow: 3px 6px 0 var(--yellow); +} + +.btn--primary { + box-shadow: 0 4px 0 var(--amber-dark); +} +.btn--primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 0 var(--amber-dark), 0 0 24px rgba(250, 204, 21, 0.2); +} +.btn--primary:active { + transform: translateY(3px); + box-shadow: 0 1px 0 var(--amber-dark); + transition-duration: 0.08s; +} + +.checker-block--active { + box-shadow: 2px 3px 0 var(--yellow); } -.VPNavBarTitle .VPImage { - height: 65% !important; - width: auto !important; - object-fit: contain; +.pipe-box--checker { + box-shadow: 2px 3px 0 var(--yellow), 0 0 16px rgba(250,204,21,0.08); +} +.pipe-box--handler, +.pipe-box--response { + box-shadow: 2px 3px 0 #4ade80, 0 0 16px rgba(74,222,128,0.08); } -.VPNavBarSearch button { +.footer { position: relative; - isolation: isolate; + border-top: none; + padding-top: 84px; +} +.footer::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: repeating-linear-gradient( + 90deg, + var(--yellow) 0px, var(--yellow) 10px, + var(--amber) 10px, var(--amber) 20px, + transparent 20px, transparent 24px, + var(--terminal-green) 24px, var(--terminal-green) 34px, + transparent 34px, transparent 40px, + rgba(96,165,250,0.6) 40px, rgba(96,165,250,0.6) 50px, + transparent 50px, transparent 56px + ); + opacity: 0.45; + pointer-events: none; +} + +.iso-brick { + animation: brick-drop 0.5s cubic-bezier(0.34, 1.8, 0.64, 1) both; +} + +@keyframes wall-settle { + 0% { transform: translateY(0); } + 15% { transform: translateY(-4px); } + 30% { transform: translateY(3px); } + 50% { transform: translateY(-1px); } + 70% { transform: translateY(1px); } + 100% { transform: translateY(0); } +} +.hero__wall.wall-settled { + animation: wall-settle 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} + +.conviction__stat { position: relative; } + +.conviction__block-tower { + position: absolute; + right: -16px; + bottom: 8px; + display: flex; + flex-direction: column-reverse; + align-items: center; + gap: 2px; + pointer-events: none; +} + +.conviction__tower-block { + width: 8px; + height: 8px; + opacity: 0; + transform: scaleY(0); + transform-origin: bottom; +} + +.conviction__stat.tower-active .conviction__tower-block { + animation: tower-block-rise 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} +.conviction__stat.tower-active .conviction__tower-block:nth-child(1) { animation-delay: 0ms; } +.conviction__stat.tower-active .conviction__tower-block:nth-child(2) { animation-delay: 80ms; } +.conviction__stat.tower-active .conviction__tower-block:nth-child(3) { animation-delay: 160ms; } +.conviction__stat.tower-active .conviction__tower-block:nth-child(4) { animation-delay: 240ms; } + +@keyframes tower-block-rise { + from { opacity: 0; transform: scaleY(0) translateY(4px); } + to { opacity: 1; transform: scaleY(1) translateY(0); } +} + +.pipeline-section.section-anim .pipe-node:nth-child(1) { + animation: pipe-from-left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0ms both; +} +.pipeline-section.section-anim .pipe-node:nth-child(3) { + animation: pipe-from-right 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 80ms both; +} +.pipeline-section.section-anim .pipe-node:nth-child(5) { + animation: pipe-from-left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 160ms both; +} +.pipeline-section.section-anim .pipe-node:nth-child(7) { + animation: pipe-from-right 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 240ms both; +} +.pipeline-section.section-anim .pipe-node:nth-child(9) { + animation: pipe-from-left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 320ms both; +} + +@keyframes pipe-from-left { + from { opacity: 0; transform: translateX(-32px) scale(0.88); } + to { opacity: 1; transform: translateX(0) scale(1); } +} +@keyframes pipe-from-right { + from { opacity: 0; transform: translateX(32px) scale(0.88); } + to { opacity: 1; transform: translateX(0) scale(1); } +} + +.checkers-section.section-anim .checker-block { + animation: checker-drop 0.48s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} +.checkers-section.section-anim .checker-block:nth-child(1) { animation-delay: 0ms; } +.checkers-section.section-anim .checker-block:nth-child(3) { animation-delay: 130ms; } +.checkers-section.section-anim .checker-block:nth-child(5) { animation-delay: 260ms; } +.checkers-section.section-anim .checker-block:nth-child(7) { animation-delay: 390ms; } + +.checkers-section.section-anim .checker-connector { + animation: connector-draw 0.25s ease both; +} +.checkers-section.section-anim .checker-connector:nth-child(2) { animation-delay: 80ms; } +.checkers-section.section-anim .checker-connector:nth-child(4) { animation-delay: 210ms; } +.checkers-section.section-anim .checker-connector:nth-child(6) { animation-delay: 340ms; } + +@keyframes checker-drop { + from { opacity: 0; transform: translateY(-28px) scale(0.93); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes connector-draw { + from { opacity: 0; transform: scaleY(0); transform-origin: top; } + to { opacity: 1; transform: scaleY(1); } +} + +.comparison-section.section-anim .comparison-table tbody tr { + animation: row-slide-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} +.comparison-section.section-anim .comparison-table tbody tr:nth-child(1) { animation-delay: 0ms; } +.comparison-section.section-anim .comparison-table tbody tr:nth-child(2) { animation-delay: 70ms; } +.comparison-section.section-anim .comparison-table tbody tr:nth-child(3) { animation-delay: 140ms; } +.comparison-section.section-anim .comparison-table tbody tr:nth-child(4) { animation-delay: 210ms; } +.comparison-section.section-anim .comparison-table tbody tr:nth-child(5) { animation-delay: 280ms; } +.comparison-section.section-anim .comparison-table tbody tr:nth-child(6) { animation-delay: 350ms; } + +@keyframes row-slide-in { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} + +.comparison-section.section-anim .cell-yes { + animation: yes-flash 0.5s ease 500ms both; +} +@keyframes yes-flash { + 0% { text-shadow: 0 0 14px #4ade80; } + 60% { text-shadow: 0 0 0 transparent; } + 100% { text-shadow: none; } +} + +.eco-grid .eco-card.reveal { + opacity: 0; + transform: translateY(-36px) scale(0.93); + transition: + opacity 0.45s ease, + transform 0.52s cubic-bezier(0.34, 1.56, 0.64, 1); +} +.eco-grid .eco-card.reveal.visible { + opacity: 1; + transform: translateY(0) scale(1); +} + +.cta-blocks { + position: absolute; + inset: 0; + display: grid; + grid-template-columns: repeat(24, 1fr); + grid-template-rows: repeat(6, 1fr); + pointer-events: none; + z-index: 0; + overflow: hidden; +} +.cta-block { + opacity: 0; + background: rgba(250, 204, 21, calc(var(--op, 0.03) + var(--glow, 0) * 0.12)); +} +.cta-blocks.anim-active .cta-block { + animation: block-light 0.35s ease forwards; + animation-delay: var(--d, 0ms); +} +@keyframes block-light { + 0% { opacity: 0; transform: scale(0.6); } + 55% { opacity: 1; transform: scale(1.08); } + 100% { opacity: var(--op, 0.10); transform: scale(1); } +} + +.pipe__controls { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-bottom: 24px; +} +.pipe__controls .pipe__run-row { margin-bottom: 0; } + +.pipe__scenarios { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 4px; +} +.pipe__scenario { + font-family: 'Inter', system-ui, sans-serif; + font-size: 10px; + padding: 5px 10px; + border: 1px solid var(--border); + color: var(--muted-soft); + background: transparent; + cursor: pointer; + letter-spacing: -0.01em; + white-space: nowrap; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} +.pipe__scenario:hover { border-color: var(--muted); color: var(--muted); } +.pipe__scenario--active { + border-color: var(--yellow); + color: var(--yellow); + background: rgba(250, 204, 21, 0.06); +} + +.pipe__run-row { + display: flex; + justify-content: center; + margin-bottom: 24px; +} +.pipe__run-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--muted); + font-family: 'Inter', system-ui, sans-serif; + font-size: 12px; + padding: 8px 24px; + cursor: pointer; + transition: border-color 0.2s ease, color 0.2s ease; + letter-spacing: -0.01em; +} +.pipe__run-btn:hover { border-color: var(--yellow); color: var(--yellow); } +.pipe__run-btn { transition: border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease; } + +.pipe__run-btn--running { + border-color: var(--muted); + color: var(--muted); + cursor: not-allowed; +} +.pipe__run-btn--running::before { + content: ''; + display: inline-block; + width: 6px; height: 6px; + background: var(--muted); + border-radius: 50%; + margin-right: 6px; + vertical-align: middle; + animation: btn-pulse 0.8s ease-in-out infinite; +} +@keyframes btn-pulse { 0%,100%{opacity:1} 50%{opacity:0.3} } +.pipe__run-btn--success { border-color: #4ade80; color: #4ade80; } +.pipe__run-btn--error { border-color: #f87171; color: #f87171; } + +.pipe--running .pipe-dot { animation-duration: 0.5s; } + +.pipe--running .pipe-node:not(.pipe-node--lit):not(.pipe-node--lit-error) { + opacity: 0.35; + transition: opacity 0.2s ease; +} + +.pipe-node--lit { + border-color: var(--yellow) !important; + box-shadow: 0 0 12px rgba(250, 204, 21, 0.35) !important; + transform: scale(1.05) !important; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) !important; +} +.pipe-node--lit-error { + border-color: #f87171 !important; + box-shadow: 0 0 12px rgba(248, 113, 113, 0.35) !important; + transform: scale(1.05) !important; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) !important; +} + +.pipe-node--response-ok .pipe-box--response { border-color: #4ade80; color: #4ade80; } +.pipe-node--response-warn .pipe-box--response { border-color: #fb923c; color: #fb923c; background: rgba(251,146,60,0.05); } +.pipe-node--response-err .pipe-box--response { border-color: #f87171; color: #f87171; background: rgba(248,113,113,0.05); } + +.pipe-node__status { + font-family: 'Inter', system-ui, sans-serif; + font-size: 8px; + margin-top: 5px; + color: var(--muted-soft); + height: 12px; + text-align: center; + white-space: nowrap; + letter-spacing: -0.01em; + transition: color 0.15s, opacity 0.2s ease; +} +.pipe-node__status--ok { color: #4ade80; } +.pipe-node__status--running { color: var(--muted); animation: btn-pulse 0.8s ease-in-out infinite; } +.pipe-node__status--error { color: #f87171; } + +.pipe__status-badge { + font-family: 'Inter', system-ui, sans-serif; + font-size: 11px; + letter-spacing: -0.01em; + text-align: center; + padding: 8px 16px; border: 1px solid transparent; - transition: border-color 160ms ease; + margin-top: 8px; + opacity: 0; + transform: translateY(6px); + transition: opacity 0.3s ease, transform 0.3s ease, border-color 0.3s ease, color 0.3s ease; } +.pipe__status-badge.visible { opacity: 1; transform: none; } +.pipe__status-badge--ok { border-color: rgba(74,222,128,0.3); color: #4ade80; } +.pipe__status-badge--warn { border-color: rgba(251,146,60,0.3); color: #fb923c; } +.pipe__status-badge--err { border-color: rgba(248,113,113,0.3); color: #f87171; } -@media (min-width: 768px) { - .VPNavBarSearch { - position: fixed; - top: 0; - left: 50vw; - z-index: calc(var(--vp-z-index-nav) + 1); - height: var(--vp-nav-height); - padding-left: 0 !important; - transform: translateX(-50%); +.checker-block:active:not(.checker-block--pending) { + transform: scale(0.95); + transition: transform 0.1s ease; +} + +.pixel-trail { + position: fixed; + top: 0; + left: 0; + width: 8px; + height: 8px; + background: var(--yellow); + pointer-events: none; + z-index: 9999; + will-change: transform, opacity; +} +@media (prefers-reduced-motion: reduce) { + .pixel-trail { display: none; } +} + +.eco-grid .eco-card.reveal.visible:hover { + transform: translateY(calc(-4px + var(--my, 0px) * 0.3)) translateX(calc(-2px + var(--mx, 0px) * 0.3)); + box-shadow: 3px 6px 0 var(--yellow); +} +.community__row .community__card.reveal.visible:hover { + transform: translateY(-4px) translateX(-2px); + box-shadow: 3px 6px 0 var(--yellow); +} + +.community__row .community__card.reveal { + opacity: 0; + transition: opacity 0.45s cubic-bezier(0.34, 1.56, 0.64, 1), + transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); +} +.community__row .community__card.reveal:nth-child(1) { + transform: translateX(-28px) rotate(-1.5deg); +} +.community__row .community__card.reveal:nth-child(2) { + transform: translateY(24px) scale(0.95); + transition-delay: 100ms; +} +.community__row .community__card.reveal:nth-child(3) { + transform: translateX(28px) rotate(1.5deg); + transition-delay: 200ms; +} +.community__row .community__card.reveal.visible { + opacity: 1; + transform: none; +} + +.typeflow-tabs { + display: flex; + gap: 4px; + margin-bottom: 32px; + border-bottom: 1px solid var(--border); + padding-bottom: 0; +} + +.typeflow-tab { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--muted); + background: transparent; + border: none; + padding: 8px 16px; + cursor: pointer; + position: relative; + transition: color 0.2s ease; + letter-spacing: 0.3px; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} +.typeflow-tab:hover { color: var(--body); } +.typeflow-tab--active { + color: var(--yellow); + border-bottom-color: var(--yellow); +} + +.typeflow { + display: grid; + grid-template-columns: 1fr 80px 1fr; + align-items: start; + gap: 0; +} + +.typeflow__panel { + background: var(--surface); + border: 1px solid var(--border); + overflow: hidden; +} + +.typeflow__panel-label { + display: flex; + align-items: center; + gap: 8px; + font-family: 'Fragment Mono', monospace; + font-size: 11px; + color: var(--muted); + letter-spacing: 0.4px; + text-transform: uppercase; + padding: 10px 16px; + border-bottom: 1px solid var(--border); + background: var(--surface-soft); +} + +.typeflow__panel-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.typeflow__panel-dot--server { background: var(--yellow); } +.typeflow__panel-dot--client { background: var(--terminal-green); } + +.typeflow__code { + padding: 20px; + overflow: auto; + transition: opacity 0.25s ease; +} +.typeflow__code pre { + margin: 0; + font-family: 'Fragment Mono', monospace; + font-size: 13px; + line-height: 1.7; + color: var(--code); + white-space: pre; +} +.typeflow__code--hidden { + display: none; +} + +.typeflow__bridge { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 60px; + gap: 8px; + position: relative; +} + +.typeflow__arrow-svg { + width: 80px; + height: 40px; +} + +.typeflow__arrow-path { + stroke-dashoffset: 60; + animation: dash-flow 2s linear infinite; +} + +.typeflow__bridge-label { + font-family: 'Fragment Mono', monospace; + font-size: 9px; + color: var(--muted-soft); + letter-spacing: 0.3px; + text-align: center; + white-space: nowrap; +} + +.typeflow__particle { + position: absolute; + top: calc(50% + 10px); + left: 0; + width: 6px; + height: 6px; + background: var(--yellow); + border-radius: 50%; + opacity: 0; + animation: particle-flow 2s linear infinite; +} +.typeflow__particle--2 { + animation-delay: 1s; +} + +@keyframes dash-flow { + from { stroke-dashoffset: 60; } + to { stroke-dashoffset: 0; } +} + +@keyframes particle-flow { + 0% { left: 0%; opacity: 0; } + 10% { opacity: 0.8; } + 90% { opacity: 0.8; } + 100% { left: 100%; opacity: 0; } +} + +@media (max-width: 900px) { + .typeflow { + grid-template-columns: 1fr; + } + .typeflow__bridge { + padding-top: 0; + flex-direction: row; justify-content: center; + padding: 16px 0; } + .typeflow__arrow-svg { + transform: none; + } +} - .VPNavBarSearch .DocSearch-Button { - width: min(420px, 42vw); +@media (prefers-reduced-motion: reduce) { + .typeflow__arrow-path, + .typeflow__particle, + .typeflow__particle--2 { + animation: none; + opacity: 1; } } -@media (min-width: 1200px) { - .VPNavBarSearch .DocSearch-Button { - width: min(520px, 38vw); +.bench { + display: flex; + flex-direction: column; + gap: 16px; +} + +.bench-row { + display: grid; + grid-template-columns: 100px 1fr 120px; + align-items: center; + gap: 16px; +} + +.bench-row__name { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--muted); + letter-spacing: 0.3px; + text-align: right; +} +.bench-row__name--duplo { color: var(--yellow); } + +.bench-row__track { + height: 8px; + background: var(--surface-soft); + border: 1px solid var(--border); + overflow: hidden; + position: relative; +} + +.bench-bar { + height: 100%; + width: 0%; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.08); + transition: width 0.8s cubic-bezier(0.23, 1, 0.32, 1); + position: relative; +} +.bench-bar--duplo { + background: var(--yellow); + box-shadow: 0 0 8px rgba(250, 204, 21, 0.3); +} + +.bench-animated .bench-bar { + width: var(--pct); +} + +.bench-row__value { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--muted); + white-space: nowrap; +} +.bench-row:first-child .bench-row__value { color: var(--yellow); } + +.bench-row__unit { + font-size: 10px; + color: var(--muted-soft); + margin-left: 2px; +} + +.bench-footnote { + font-size: 12px; + color: var(--muted-soft); + margin-top: 8px; + font-family: 'Fragment Mono', monospace; +} +.bench-footnote a { + color: var(--muted); + text-decoration: underline; + text-underline-offset: 3px; + transition: color 0.2s ease; +} +.bench-footnote a:hover { color: var(--yellow); } + +.bench-row:nth-child(1) .bench-bar { transition-delay: 0ms; } +.bench-row:nth-child(2) .bench-bar { transition-delay: 120ms; } +.bench-row:nth-child(3) .bench-bar { transition-delay: 240ms; } +.bench-row:nth-child(4) .bench-bar { transition-delay: 360ms; } + +@media (max-width: 600px) { + .bench-row { + grid-template-columns: 72px 1fr 90px; + gap: 10px; } } -.VPNavBarSearch button { - color: #67676C; +.steps { + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr; + align-items: start; + gap: 0; + margin-bottom: 48px; } -.VPNavBarSearch .DocSearch-Button { - justify-content: flex-start; +.step-card { + background: var(--surface); + border: 1px solid var(--border); + padding: 24px; + position: relative; + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease; +} +.step-card:hover { + transform: translateY(-4px) translateX(-2px); + box-shadow: 3px 6px 0 var(--yellow); } -.VPNavBarSearch .DocSearch-Button-Keys { - margin-right: 0; - margin-left: auto; - padding-left: 16px; +.step-card__num { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + color: var(--muted-soft); + letter-spacing: 0.6px; + margin-bottom: 12px; +} + +.step-card__icon { + color: var(--yellow); + margin-bottom: 12px; + display: flex; +} + +.step-card__title { + font-family: 'Space Grotesk', sans-serif; + font-size: 18px; + font-weight: 700; + color: var(--ink); + margin-bottom: 6px; + letter-spacing: -0.3px; +} + +.step-card__parenthesis { + letter-spacing: 2.5px; +} + +.step-card__desc { + font-size: 13px; + color: var(--muted); + margin-bottom: 16px; + line-height: 1.55; } -.VPNavBarSearch .DocSearch-Button-Container { +.step-card__cmd { + background: var(--surface-soft); + border: 1px solid var(--border); + padding: 10px 14px; + display: flex; + align-items: center; gap: 8px; + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--code); +} +.step-card__prompt { color: var(--yellow); flex-shrink: 0; } +.step-card__code { flex: 1; } +.step-card__cursor { + width: 7px; + height: 14px; + background: var(--yellow); + animation: blink 1s step-end infinite; + flex-shrink: 0; +} + +.step-card__pre { + background: var(--surface-soft); + border: 1px solid var(--border); + padding: 12px 14px; + margin: 0; + font-family: 'Fragment Mono', monospace; + font-size: 12px; + line-height: 1.65; + color: var(--code); + overflow: auto; + white-space: pre; } -.dark .VPNavBarSearch button { - background-color: #161618; - color: #98989F; +.step-connector { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding-top: 72px; + gap: 0; + width: 48px; +} +.step-connector__line { + width: 1px; + height: 24px; + background: linear-gradient(180deg, var(--border) 0%, var(--yellow) 50%, var(--border) 100%); + background-size: 100% 200%; + animation: connector-pulse 2s ease-in-out infinite; +} +.step-connector__dot { + width: 6px; + height: 6px; + background: var(--yellow); + border-radius: 50%; + box-shadow: 0 0 8px rgba(250, 204, 21, 0.5); } -.VPNavBarSearch button:hover, -.VPNavBarSearch button:focus-visible { - border-color: var(--vp-c-brand-1) !important; +@keyframes connector-pulse { + 0%, 100% { background-position: 0% 0%; opacity: 0.5; } + 50% { background-position: 0% 100%; opacity: 1; } } +.steps-cta { + display: flex; + align-items: center; + gap: 16px; + justify-content: center; +} -/** - * Component: Custom Block - * -------------------------------------------------------------------------- */ +@media (max-width: 900px) { + .steps { + grid-template-columns: 1fr; + } + .step-connector { + flex-direction: row; + width: 100%; + padding-top: 0; + padding: 8px 0; + justify-content: center; + } + .step-connector__line { + width: 32px; + height: 1px; + background: linear-gradient(90deg, var(--border) 0%, var(--yellow) 50%, var(--border) 100%); + } +} -:root { - --vp-custom-block-tip-border: transparent; - --vp-custom-block-tip-text: var(--vp-c-text-1); - --vp-custom-block-tip-bg: var(--vp-c-brand-soft); - --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); +@media (prefers-reduced-motion: reduce) { + .step-connector__line { animation: none; } + .step-card__cursor { animation: none; opacity: 1; } } -/** - * Component: Algolia - * -------------------------------------------------------------------------- */ +.social-proof { + display: flex; + align-items: center; + gap: 0; + margin-bottom: 48px; + border: 1px solid var(--border); + background: var(--surface); + position: relative; +} +.social-proof::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 0; + height: 2px; + background: var(--yellow); + transition: width 1s cubic-bezier(0.23, 1, 0.32, 1); +} +.social-proof.sp-visible::after { width: 100%; } -.DocSearch { - --docsearch-primary-color: var(--vp-c-brand-1) !important; +.social-proof__live-dot { + display: inline-block; + width: 6px; + height: 6px; + background: #4ade80; + border-radius: 50%; + margin-right: 6px; + vertical-align: middle; + animation: live-pulse 2s ease-in-out infinite; +} +@keyframes live-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(74,222,128,0.5); } + 50% { box-shadow: 0 0 0 5px rgba(74,222,128,0); } } -/** - * Globals Rules - * -------------------------------------------------------------------------- */ +.social-proof__stat { + flex: 1; + padding: 28px 24px; + text-align: center; +} -.toc-hidden { - display: none; -} \ No newline at end of file +.social-proof__num { + font-family: 'Space Grotesk', sans-serif; + font-size: 36px; + font-weight: 800; + color: var(--ink); + letter-spacing: -1px; + line-height: 1; + margin-bottom: 6px; +} + +.social-proof__unit { + font-size: 24px; + color: var(--yellow); +} + +.social-proof__label { + font-family: 'Fragment Mono', monospace; + font-size: 10px; + color: var(--muted-soft); + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.social-proof__divider { + width: 1px; + height: 60px; + background: var(--border); + flex-shrink: 0; +} + +.testimonials { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0; + border: 1px solid var(--border); +} + +.testimonial-card { + padding: 28px 32px; + border-right: 1px solid var(--border); + border-bottom: 1px solid var(--border); + transition: background 0.2s ease, transform 0.25s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.25s ease; +} +.testimonial-card:nth-child(2n) { border-right: none; } +.testimonial-card:nth-child(n+3) { border-bottom: none; } +.testimonial-card:hover { background: var(--surface-hover); transform: translateY(-4px); } + +.testimonial-card[data-accent="yellow"] { border-top: 3px solid var(--yellow); } +.testimonial-card[data-accent="green"] { border-top: 3px solid #4ade80; } +.testimonial-card[data-accent="violet"] { border-top: 3px solid #8b5cf6; } +.testimonial-card[data-accent="cyan"] { border-top: 3px solid #22d3ee; } + +.testimonial-card[data-accent="yellow"]:hover { box-shadow: 0 8px 28px rgba(250,204,21,0.18); } +.testimonial-card[data-accent="green"]:hover { box-shadow: 0 8px 28px rgba(74,222,128,0.18); } +.testimonial-card[data-accent="violet"]:hover { box-shadow: 0 8px 28px rgba(139,92,246,0.18); } +.testimonial-card[data-accent="cyan"]:hover { box-shadow: 0 8px 28px rgba(34,211,238,0.18); } + +.testimonial-card__quote { + font-size: 15px; + color: var(--body); + line-height: 1.65; + font-style: italic; + margin-bottom: 16px; + position: relative; +} +.testimonial-card__quote::before { + content: '\201C'; + font-size: 48px; + color: var(--yellow); + opacity: 0.45; + line-height: 0; + vertical-align: -0.4em; + margin-right: 4px; + font-style: normal; +} +.testimonial-card[data-accent="green"] .testimonial-card__quote::before { color: #4ade80; } +.testimonial-card[data-accent="violet"] .testimonial-card__quote::before { color: #8b5cf6; } +.testimonial-card[data-accent="cyan"] .testimonial-card__quote::before { color: #22d3ee; } + +.testimonial-card__author { + display: flex; + flex-direction: column; + gap: 2px; +} +.testimonial-card__handle { + font-family: 'Fragment Mono', monospace; + font-size: 12px; + color: var(--yellow); +} +.testimonial-card[data-accent="green"] .testimonial-card__handle { color: #4ade80; } +.testimonial-card[data-accent="violet"] .testimonial-card__handle { color: #8b5cf6; } +.testimonial-card[data-accent="cyan"] .testimonial-card__handle { color: #22d3ee; } + +.testimonial-card__role { + font-size: 12px; + color: var(--muted-soft); +} + +.testimonials .testimonial-card.reveal:nth-child(odd) { + transform: translateX(-32px); + transition: opacity 0.5s cubic-bezier(0.23,1,0.32,1), + transform 0.5s cubic-bezier(0.34,1.4,0.64,1); +} +.testimonials .testimonial-card.reveal:nth-child(even) { + transform: translateX(32px); + transition: opacity 0.5s cubic-bezier(0.23,1,0.32,1), + transform 0.5s cubic-bezier(0.34,1.4,0.64,1); +} +.testimonials .testimonial-card.reveal.visible { + opacity: 1; + transform: none; +} + +@media (max-width: 700px) { + .testimonials { + grid-template-columns: 1fr; + } + .testimonial-card { + border-right: none; + border-bottom: 1px solid var(--border); + } + .testimonial-card:last-child { border-bottom: none; } + + .social-proof { + flex-wrap: wrap; + } + .social-proof__stat { min-width: 50%; } + .social-proof__divider { display: none; } +} diff --git a/app/examples/home/clean/application/duplojs.md b/app/examples/home/clean/application/duplojs.md deleted file mode 100644 index cc6bc98..0000000 --- a/app/examples/home/clean/application/duplojs.md +++ /dev/null @@ -1,7 +0,0 @@ -```ts twoslash -// @filename: domain/duplojs.ts - -// @filename: application/duplojs.ts -// ---cut--- - -``` diff --git a/app/examples/home/clean/application/other.md b/app/examples/home/clean/application/other.md deleted file mode 100644 index fa6735b..0000000 --- a/app/examples/home/clean/application/other.md +++ /dev/null @@ -1,7 +0,0 @@ -```ts twoslash -// @filename: domain/other.ts - -// @filename: application/other.ts -// ---cut--- - -``` diff --git a/app/examples/home/clean/domain/duplojs.md b/app/examples/home/clean/domain/duplojs.md deleted file mode 100644 index a2cd0dd..0000000 --- a/app/examples/home/clean/domain/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/clean/domain/other.md b/app/examples/home/clean/domain/other.md deleted file mode 100644 index 83af4dc..0000000 --- a/app/examples/home/clean/domain/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/command/duplojs.md b/app/examples/home/command/duplojs.md deleted file mode 100644 index 6ccee1e..0000000 --- a/app/examples/home/command/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/command/other.md b/app/examples/home/command/other.md deleted file mode 100644 index c67419a..0000000 --- a/app/examples/home/command/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/either/fetch/duplojs.md b/app/examples/home/either/fetch/duplojs.md deleted file mode 100644 index 33838ca..0000000 --- a/app/examples/home/either/fetch/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/either/fetch/other.md b/app/examples/home/either/fetch/other.md deleted file mode 100644 index 7f32ee6..0000000 --- a/app/examples/home/either/fetch/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/either/file/duplojs.md b/app/examples/home/either/file/duplojs.md deleted file mode 100644 index d9882bf..0000000 --- a/app/examples/home/either/file/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/either/file/other.md b/app/examples/home/either/file/other.md deleted file mode 100644 index 3177dbc..0000000 --- a/app/examples/home/either/file/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/either/jwt/duplojs.md b/app/examples/home/either/jwt/duplojs.md deleted file mode 100644 index 10cf274..0000000 --- a/app/examples/home/either/jwt/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/either/jwt/other.md b/app/examples/home/either/jwt/other.md deleted file mode 100644 index 601649f..0000000 --- a/app/examples/home/either/jwt/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/form/duplojs.md b/app/examples/home/form/duplojs.md deleted file mode 100644 index d46efc8..0000000 --- a/app/examples/home/form/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```vue - -``` \ No newline at end of file diff --git a/app/examples/home/form/other.md b/app/examples/home/form/other.md deleted file mode 100644 index c411cd3..0000000 --- a/app/examples/home/form/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```vue - -``` \ No newline at end of file diff --git a/app/examples/home/http/client/duplojs.md b/app/examples/home/http/client/duplojs.md deleted file mode 100644 index 5f7bb18..0000000 --- a/app/examples/home/http/client/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/http/client/other.md b/app/examples/home/http/client/other.md deleted file mode 100644 index a8dde14..0000000 --- a/app/examples/home/http/client/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/http/route/duplojs.md b/app/examples/home/http/route/duplojs.md deleted file mode 100644 index 53d4bce..0000000 --- a/app/examples/home/http/route/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/http/route/other.md b/app/examples/home/http/route/other.md deleted file mode 100644 index 2bce828..0000000 --- a/app/examples/home/http/route/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` diff --git a/app/examples/home/index.ts b/app/examples/home/index.ts deleted file mode 100644 index c43962f..0000000 --- a/app/examples/home/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import CleanApplicationD from "@/examples/home/clean/application/duplojs.md"; -import CleanApplicationO from "@/examples/home/clean/application/other.md"; -import CleanDomainD from "@/examples/home/clean/domain/duplojs.md"; -import CleanDomainO from "@/examples/home/clean/domain/other.md"; -import CommandD from "@/examples/home/command/duplojs.md"; -import CommandO from "@/examples/home/command/other.md"; -import EitherFetchD from "@/examples/home/either/fetch/duplojs.md"; -import EitherFetchO from "@/examples/home/either/fetch/other.md"; -import EitherFileD from "@/examples/home/either/file/duplojs.md"; -import EitherFileO from "@/examples/home/either/file/other.md"; -import EitherJwtD from "@/examples/home/either/jwt/duplojs.md"; -import EitherJwtO from "@/examples/home/either/jwt/other.md"; -import FormD from "@/examples/home/form/duplojs.md"; -import FormO from "@/examples/home/form/other.md"; -import HttpClientD from "@/examples/home/http/client/duplojs.md"; -import HttpClientO from "@/examples/home/http/client/other.md"; -import HttpRouteD from "@/examples/home/http/route/duplojs.md"; -import HttpRouteO from "@/examples/home/http/route/other.md"; -import UFArrayMinElementD from "@/examples/home/utilsFunction/arrayMinElement/duplojs.md"; -import UFArrayMinElementO from "@/examples/home/utilsFunction/arrayMinElement/other.md"; -import UFEntriesD from "@/examples/home/utilsFunction/entries/duplojs.md"; -import UFEntriesO from "@/examples/home/utilsFunction/entries/other.md"; -import UFGroupByD from "@/examples/home/utilsFunction/groupBy/duplojs.md"; -import UFGroupByO from "@/examples/home/utilsFunction/groupBy/other.md"; -import UFMapTupleD from "@/examples/home/utilsFunction/mapTuple/duplojs.md"; -import UFMapTupleO from "@/examples/home/utilsFunction/mapTuple/other.md"; -import UFReduceD from "@/examples/home/utilsFunction/reduce/duplojs.md"; -import UFReduceO from "@/examples/home/utilsFunction/reduce/other.md"; - -export { - CleanApplicationD, - CleanApplicationO, - CleanDomainD, - CleanDomainO, - CommandD, - CommandO, - EitherFetchD, - EitherFetchO, - EitherFileD, - EitherFileO, - EitherJwtD, - EitherJwtO, - FormD, - FormO, - HttpClientD, - HttpClientO, - HttpRouteD, - HttpRouteO, - UFArrayMinElementD, - UFArrayMinElementO, - UFEntriesD, - UFEntriesO, - UFGroupByD, - UFGroupByO, - UFMapTupleD, - UFMapTupleO, - UFReduceD, - UFReduceO, -}; diff --git a/app/examples/home/utilsFunction/arrayMinElement/duplojs.md b/app/examples/home/utilsFunction/arrayMinElement/duplojs.md deleted file mode 100644 index a3df074..0000000 --- a/app/examples/home/utilsFunction/arrayMinElement/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/arrayMinElement/other.md b/app/examples/home/utilsFunction/arrayMinElement/other.md deleted file mode 100644 index 277eb53..0000000 --- a/app/examples/home/utilsFunction/arrayMinElement/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/entries/duplojs.md b/app/examples/home/utilsFunction/entries/duplojs.md deleted file mode 100644 index acaee3e..0000000 --- a/app/examples/home/utilsFunction/entries/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/entries/other.md b/app/examples/home/utilsFunction/entries/other.md deleted file mode 100644 index 4100281..0000000 --- a/app/examples/home/utilsFunction/entries/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/groupBy/duplojs.md b/app/examples/home/utilsFunction/groupBy/duplojs.md deleted file mode 100644 index f6da56e..0000000 --- a/app/examples/home/utilsFunction/groupBy/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/groupBy/other.md b/app/examples/home/utilsFunction/groupBy/other.md deleted file mode 100644 index 1eb3b7b..0000000 --- a/app/examples/home/utilsFunction/groupBy/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/mapTuple/duplojs.md b/app/examples/home/utilsFunction/mapTuple/duplojs.md deleted file mode 100644 index 822bc23..0000000 --- a/app/examples/home/utilsFunction/mapTuple/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/mapTuple/other.md b/app/examples/home/utilsFunction/mapTuple/other.md deleted file mode 100644 index df8fdd4..0000000 --- a/app/examples/home/utilsFunction/mapTuple/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/reduce/duplojs.md b/app/examples/home/utilsFunction/reduce/duplojs.md deleted file mode 100644 index f80cda9..0000000 --- a/app/examples/home/utilsFunction/reduce/duplojs.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/reduce/other.md b/app/examples/home/utilsFunction/reduce/other.md deleted file mode 100644 index 4c95c90..0000000 --- a/app/examples/home/utilsFunction/reduce/other.md +++ /dev/null @@ -1,3 +0,0 @@ -```ts twoslash - -``` \ No newline at end of file diff --git a/app/examples/layers/applications/repositories/book.md b/app/examples/layers/applications/repositories/book.md new file mode 100644 index 0000000..9bb2779 --- /dev/null +++ b/app/examples/layers/applications/repositories/book.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/repositories/book.ts b/app/examples/layers/applications/repositories/book.ts new file mode 100644 index 0000000..b97f1a5 --- /dev/null +++ b/app/examples/layers/applications/repositories/book.ts @@ -0,0 +1,9 @@ +import { type Book } from "@domains/entities"; +import { C } from "@duplojs/utils"; + +export interface BookRepository { + save(entity: T): Promise; + findOne(bookId: Book.Id): Promise>; +} + +export const BookRepository = C.createRepository(); diff --git a/app/examples/layers/applications/repositories/client.md b/app/examples/layers/applications/repositories/client.md new file mode 100644 index 0000000..0034a5f --- /dev/null +++ b/app/examples/layers/applications/repositories/client.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/repositories/client.ts b/app/examples/layers/applications/repositories/client.ts new file mode 100644 index 0000000..e11d76d --- /dev/null +++ b/app/examples/layers/applications/repositories/client.ts @@ -0,0 +1,9 @@ +import { type Client } from "@domains/entities"; +import { C } from "@duplojs/utils"; + +export interface ClientRepository { + save(entity: T): Promise; + findOne(clientId: Client.Id): Promise>; +} + +export const ClientRepository = C.createRepository(); diff --git a/app/examples/layers/applications/repositories/index.ts b/app/examples/layers/applications/repositories/index.ts new file mode 100644 index 0000000..3bcb362 --- /dev/null +++ b/app/examples/layers/applications/repositories/index.ts @@ -0,0 +1,2 @@ +export * from "./book"; +export * from "./client"; diff --git a/app/examples/layers/applications/useCases/bookIsBorrowed.md b/app/examples/layers/applications/useCases/bookIsBorrowed.md new file mode 100644 index 0000000..073db3a --- /dev/null +++ b/app/examples/layers/applications/useCases/bookIsBorrowed.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/useCases/bookIsBorrowed.ts b/app/examples/layers/applications/useCases/bookIsBorrowed.ts new file mode 100644 index 0000000..1cb1e1e --- /dev/null +++ b/app/examples/layers/applications/useCases/bookIsBorrowed.ts @@ -0,0 +1,13 @@ +import { Book } from "@domains/entities"; +import { C } from "@duplojs/utils"; + +interface Input { + book: Generic; +} + +export const BookIsBorrowedUseCase = C.createUseCase( + {}, + (__) => < + Generic extends Book.Entity, + >({ book }: Input) => Book.isBorrowed(book), +); diff --git a/app/examples/layers/applications/useCases/clientBorrowBook.md b/app/examples/layers/applications/useCases/clientBorrowBook.md new file mode 100644 index 0000000..3460419 --- /dev/null +++ b/app/examples/layers/applications/useCases/clientBorrowBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/useCases/clientBorrowBook.ts b/app/examples/layers/applications/useCases/clientBorrowBook.ts new file mode 100644 index 0000000..9edfe9f --- /dev/null +++ b/app/examples/layers/applications/useCases/clientBorrowBook.ts @@ -0,0 +1,30 @@ +import { ClientRepository, BookRepository } from "@applications/repositories"; +import { clientBorrowBook } from "@domains/aggregates"; +import { type Book, type Client } from "@domains/entities"; +import { C, promiseObject } from "@duplojs/utils"; + +interface Input { + client: Client.Entity & Client.CanRent; + book: Book.Entity & Book.Available; +} + +export const ClientBorrowBookUseCase = C.createUseCase( + { + BookRepository, + ClientRepository, + }, + ({ + bookRepository, + clientRepository, + }) => ({ book, client }: Input) => { + const { + book: borrowBook, + client: renterClient, + } = clientBorrowBook(client, book); + + return promiseObject({ + book: bookRepository.save(borrowBook), + client: clientRepository.save(renterClient), + }); + }, +); diff --git a/app/examples/layers/applications/useCases/clientCanRent.md b/app/examples/layers/applications/useCases/clientCanRent.md new file mode 100644 index 0000000..236c938 --- /dev/null +++ b/app/examples/layers/applications/useCases/clientCanRent.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/useCases/clientCanRent.ts b/app/examples/layers/applications/useCases/clientCanRent.ts new file mode 100644 index 0000000..0e54ed0 --- /dev/null +++ b/app/examples/layers/applications/useCases/clientCanRent.ts @@ -0,0 +1,13 @@ +import { Client } from "@domains/entities"; +import { C } from "@duplojs/utils"; + +interface Input { + client: Generic; +} + +export const ClientCanRentUseCase = C.createUseCase( + {}, + (__) => < + Generic extends Client.Entity, + >({ client }: Input) => Client.cantRent(client), +); diff --git a/app/examples/layers/applications/useCases/clientGiveBackBook.md b/app/examples/layers/applications/useCases/clientGiveBackBook.md new file mode 100644 index 0000000..97b6d72 --- /dev/null +++ b/app/examples/layers/applications/useCases/clientGiveBackBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/useCases/clientGiveBackBook.ts b/app/examples/layers/applications/useCases/clientGiveBackBook.ts new file mode 100644 index 0000000..0d2125a --- /dev/null +++ b/app/examples/layers/applications/useCases/clientGiveBackBook.ts @@ -0,0 +1,32 @@ +import { BookRepository, ClientRepository } from "@applications/repositories"; +import { clientGiveBackBook } from "@domains/aggregates"; +import { type Book, type Client } from "@domains/entities"; +import { C, E, pipe, promiseObject, whenNot } from "@duplojs/utils"; + +interface Input { + client: Client.Entity; + book: Book.Entity & Book.Borrow; +} + +export const ClientGiveBackBookUseCase = C.createUseCase( + { + BookRepository, + ClientRepository, + }, + ({ + bookRepository, + clientRepository, + }) => (input: Input) => pipe( + clientGiveBackBook( + input.client, + input.book, + ), + whenNot( + E.isLeft, + ({ book, client }) => promiseObject({ + client: clientRepository.save(client), + book: bookRepository.save(book), + }), + ), + ), +); diff --git a/app/examples/layers/applications/useCases/findOneBook.md b/app/examples/layers/applications/useCases/findOneBook.md new file mode 100644 index 0000000..650b3a7 --- /dev/null +++ b/app/examples/layers/applications/useCases/findOneBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/useCases/findOneBook.ts b/app/examples/layers/applications/useCases/findOneBook.ts new file mode 100644 index 0000000..c7ef281 --- /dev/null +++ b/app/examples/layers/applications/useCases/findOneBook.ts @@ -0,0 +1,14 @@ +import { BookRepository } from "@applications/repositories"; +import { type Book } from "@domains/entities"; +import { C } from "@duplojs/utils"; + +interface Input { + bookId: Book.Id; +} + +export const FindOneBookUseCase = C.createUseCase( + { BookRepository }, + ({ + bookRepository, + }) => ({ bookId }: Input) => bookRepository.findOne(bookId), +); diff --git a/app/examples/layers/applications/useCases/findOneClient.md b/app/examples/layers/applications/useCases/findOneClient.md new file mode 100644 index 0000000..9f5694d --- /dev/null +++ b/app/examples/layers/applications/useCases/findOneClient.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/applications/useCases/findOneClient.ts b/app/examples/layers/applications/useCases/findOneClient.ts new file mode 100644 index 0000000..0a34a76 --- /dev/null +++ b/app/examples/layers/applications/useCases/findOneClient.ts @@ -0,0 +1,14 @@ +import { ClientRepository } from "@applications/repositories"; +import { type Client } from "@domains/entities"; +import { C } from "@duplojs/utils"; + +interface Input { + clientId: Client.Id; +} + +export const FindOneClientUseCase = C.createUseCase( + { ClientRepository }, + ({ + clientRepository, + }) => ({ clientId }: Input) => clientRepository.findOne(clientId), +); diff --git a/app/examples/layers/applications/useCases/index.ts b/app/examples/layers/applications/useCases/index.ts new file mode 100644 index 0000000..965ae84 --- /dev/null +++ b/app/examples/layers/applications/useCases/index.ts @@ -0,0 +1,6 @@ +export * from "./bookIsBorrowed"; +export * from "./clientBorrowBook"; +export * from "./clientCanRent"; +export * from "./clientGiveBackBook"; +export * from "./findOneBook"; +export * from "./findOneClient"; diff --git a/app/examples/layers/client/main.md b/app/examples/layers/client/main.md new file mode 100644 index 0000000..d43c6da --- /dev/null +++ b/app/examples/layers/client/main.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/client/main.ts b/app/examples/layers/client/main.ts new file mode 100644 index 0000000..e4063d7 --- /dev/null +++ b/app/examples/layers/client/main.ts @@ -0,0 +1,23 @@ +import { createHttpClient } from "@duplojs/http/client"; +import type { Routes } from "./types"; + +const httpClient = createHttpClient({ + baseUrl: "https://duplojs.dev", +}); + +const result = await httpClient + .post( + "/clients/{clientId}/borrow-book", + { + params: { clientId: "bookId" }, + body: { bookId: "bookId" }, + }, + ) + .whenInformation( + "client.borrowBook", + () => { + + /* ... */ + }, + ) + .iWantInformation("client.borrowBook"); diff --git a/app/examples/layers/client/swagger.json b/app/examples/layers/client/swagger.json new file mode 100644 index 0000000..f02f03c --- /dev/null +++ b/app/examples/layers/client/swagger.json @@ -0,0 +1,237 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Swagger API", + "version": "0.0.0" + }, + "paths": { + "/clients/{clientId}/borrow-book": { + "post": { + "parameters": [ + { + "name": "clientId", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/NotIdentified0" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotIdentified1" + } + } + } + }, + "responses": { + "204": { + "headers": { + "information": { + "schema": { + "const": "client.borrowBook", + "type": "string" + }, + "description": "client.borrowBook" + } + } + }, + "404": { + "headers": { + "information": { + "schema": { + "anyOf": [ + { + "const": "client.notExist", + "type": "string" + }, + { + "const": "book.notExist", + "type": "string" + } + ] + }, + "description": "client.notExist | book.notExist" + } + } + }, + "409": { + "headers": { + "information": { + "schema": { + "anyOf": [ + { + "const": "book.alreadyBorrowed", + "type": "string" + }, + { + "const": "client.haveNotFreeLocation", + "type": "string" + } + ] + }, + "description": "book.alreadyBorrowed | client.haveNotFreeLocation" + } + } + }, + "422": { + "headers": { + "information": { + "schema": { + "const": "extract-error", + "type": "string" + }, + "description": "extract-error" + } + } + } + } + } + }, + "/clients/{clientId}/give-back-book": { + "post": { + "parameters": [ + { + "name": "clientId", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/NotIdentified8" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotIdentified9" + } + } + } + }, + "responses": { + "204": { + "headers": { + "information": { + "schema": { + "const": "client.giveBackBook", + "type": "string" + }, + "description": "client.giveBackBook" + } + } + }, + "403": { + "headers": { + "information": { + "schema": { + "const": "client.isNotBorrower", + "type": "string" + }, + "description": "client.isNotBorrower" + } + } + }, + "404": { + "headers": { + "information": { + "schema": { + "anyOf": [ + { + "const": "client.notExist", + "type": "string" + }, + { + "const": "book.notExist", + "type": "string" + } + ] + }, + "description": "client.notExist | book.notExist" + } + } + }, + "409": { + "headers": { + "information": { + "schema": { + "anyOf": [ + { + "const": "book.isNotBorrowed", + "type": "string" + }, + { + "const": "client.haveNotBook", + "type": "string" + } + ] + }, + "description": "book.isNotBorrowed | client.haveNotBook" + } + } + }, + "422": { + "headers": { + "information": { + "schema": { + "const": "extract-error", + "type": "string" + }, + "description": "extract-error" + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NotIdentified0": { + "type": "string" + }, + "NotIdentified1": { + "type": "object", + "properties": { + "bookId": { + "type": "string" + } + }, + "required": [ + "bookId" + ] + }, + "NotIdentified2": {}, + "NotIdentified3": {}, + "NotIdentified4": {}, + "NotIdentified5": {}, + "NotIdentified6": {}, + "NotIdentified7": {}, + "NotIdentified8": { + "type": "string" + }, + "NotIdentified9": { + "type": "object", + "properties": { + "bookId": { + "type": "string" + } + }, + "required": [ + "bookId" + ] + }, + "NotIdentified10": {}, + "NotIdentified11": {}, + "NotIdentified12": {}, + "NotIdentified13": {}, + "NotIdentified14": {}, + "NotIdentified15": {}, + "NotIdentified16": {} + } + } +} \ No newline at end of file diff --git a/app/examples/layers/client/swagger.md b/app/examples/layers/client/swagger.md new file mode 100644 index 0000000..a65987c --- /dev/null +++ b/app/examples/layers/client/swagger.md @@ -0,0 +1,3 @@ +```json + +``` diff --git a/app/examples/layers/client/types.d.md b/app/examples/layers/client/types.d.md new file mode 100644 index 0000000..b379087 --- /dev/null +++ b/app/examples/layers/client/types.d.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/client/types.d.ts b/app/examples/layers/client/types.d.ts new file mode 100644 index 0000000..41c7992 --- /dev/null +++ b/app/examples/layers/client/types.d.ts @@ -0,0 +1,73 @@ +export type Routes = { + method: "POST"; + path: "/clients/{clientId}/borrow-book"; + body: { + bookId: string; + }; + params: { + clientId: string; + }; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "404"; + information: "client.notExist"; + body?: undefined; + } | { + code: "404"; + information: "book.notExist"; + body?: undefined; + } | { + code: "409"; + information: "book.alreadyBorrowed"; + body?: undefined; + } | { + code: "409"; + information: "client.haveNotFreeLocation"; + body?: undefined; + } | { + code: "204"; + information: "client.borrowBook"; + body?: undefined; + }; +} | { + method: "POST"; + path: "/clients/{clientId}/give-back-book"; + body: { + bookId: string; + }; + params: { + clientId: string; + }; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "404"; + information: "client.notExist"; + body?: undefined; + } | { + code: "404"; + information: "book.notExist"; + body?: undefined; + } | { + code: "409"; + information: "book.isNotBorrowed"; + body?: undefined; + } | { + code: "403"; + information: "client.isNotBorrower"; + body?: undefined; + } | { + code: "409"; + information: "client.haveNotBook"; + body?: undefined; + } | { + code: "204"; + information: "client.giveBackBook"; + body?: undefined; + }; +}; \ No newline at end of file diff --git a/app/examples/layers/domains/aggregates/clientBorrowBook.md b/app/examples/layers/domains/aggregates/clientBorrowBook.md new file mode 100644 index 0000000..e96199f --- /dev/null +++ b/app/examples/layers/domains/aggregates/clientBorrowBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/domains/aggregates/clientBorrowBook.ts b/app/examples/layers/domains/aggregates/clientBorrowBook.ts new file mode 100644 index 0000000..4a363a6 --- /dev/null +++ b/app/examples/layers/domains/aggregates/clientBorrowBook.ts @@ -0,0 +1,29 @@ +import { Book, Client } from "@domains/entities"; +import { O, pipe } from "@duplojs/utils"; + +export function clientBorrowBook( + client: Client.Entity & Client.CanRent, + book: Book.Entity & Book.Available, +) { + const borrowedBook = pipe( + book, + Book.Entity.update({ currentBorrowerId: client.id }), + Book.Borrow.append({ currentBorrowerId: client.id }), + ); + + const { location } = Client.CanRent.getValue(client); + const updatedClient = pipe( + client, + Client.Entity.update({ + borrowedBooks: O.override( + client.borrowedBooks, + { [location]: book.id }, + ), + }), + ); + + return { + book: borrowedBook, + client: updatedClient, + }; +} diff --git a/app/examples/layers/domains/aggregates/clientGiveBackBook.md b/app/examples/layers/domains/aggregates/clientGiveBackBook.md new file mode 100644 index 0000000..ffb2097 --- /dev/null +++ b/app/examples/layers/domains/aggregates/clientGiveBackBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/domains/aggregates/clientGiveBackBook.ts b/app/examples/layers/domains/aggregates/clientGiveBackBook.ts new file mode 100644 index 0000000..d15edac --- /dev/null +++ b/app/examples/layers/domains/aggregates/clientGiveBackBook.ts @@ -0,0 +1,54 @@ +import { Book, Client } from "@domains/entities"; +import { A, C, E, O, pipe } from "@duplojs/utils"; + +export function clientGiveBackBook( + client: Client.Entity, + book: Book.Entity & Book.Borrow, +) { + const { currentBorrowerId } = Book.Borrow.getValue(book); + + if (!C.equal(client.id, currentBorrowerId)) { + return E.left("client-is-not-borrower", { + client, + book, + }); + } + + const location = pipe( + O.entries(client.borrowedBooks), + A.reduce( + A.reduceFrom(null), + ({ element: [key, bookId], next, exit }) => C.equal(bookId, book.id) + ? exit(key) + : next(null), + ), + ); + + if (location === null) { + return E.left("client-have-not-book", { + client, + book, + }); + } + + const updatedClient = pipe( + client, + Client.Entity.update({ + borrowedBooks: O.override( + client.borrowedBooks, + { [location]: null }, + ), + }), + ); + + const availableBook = pipe( + book, + Book.Entity.update({ currentBorrowerId: null }), + Book.Available.append, + ); + + return { + client: updatedClient, + book: availableBook, + }; +} diff --git a/app/examples/layers/domains/aggregates/index.ts b/app/examples/layers/domains/aggregates/index.ts new file mode 100644 index 0000000..3a80680 --- /dev/null +++ b/app/examples/layers/domains/aggregates/index.ts @@ -0,0 +1,2 @@ +export * from "./clientBorrowBook"; +export * from "./clientGiveBackBook"; diff --git a/app/examples/layers/domains/entities/book.md b/app/examples/layers/domains/entities/book.md new file mode 100644 index 0000000..f94ed5f --- /dev/null +++ b/app/examples/layers/domains/entities/book.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/domains/entities/book.ts b/app/examples/layers/domains/entities/book.ts new file mode 100644 index 0000000..79e67ef --- /dev/null +++ b/app/examples/layers/domains/entities/book.ts @@ -0,0 +1,41 @@ +import { C, DPE } from "@duplojs/utils"; +import { Client } from "./client"; + +export namespace Book { + export const Id = C.createNewType("BookId", DPE.string(), C.Uuid); + export type Id = C.GetNewType; + + export const Name = C.createNewType("BookName", DPE.string()); + export type Name = C.GetNewType; + + export const Entity = C.createEntity( + "Book", + ({ nullable }) => ({ + id: Id, + name: Name, + currentBorrowerId: nullable(Client.Id), + }), + ); + export type Entity = C.GetEntity; + + export const Borrow = C.createFlag< + Entity, + "Borrow", + { currentBorrowerId: Client.Id } + >("Borrow"); + export type Borrow = C.GetFlag; + + export const Available = C.createFlag("Available"); + export type Available = C.GetFlag; + + export function isBorrowed(book: T) { + if (book.currentBorrowerId !== null) { + return Borrow.append( + book, + { currentBorrowerId: book.currentBorrowerId }, + ); + } + + return Available.append(book); + } +} diff --git a/app/examples/layers/domains/entities/client.md b/app/examples/layers/domains/entities/client.md new file mode 100644 index 0000000..2b0548e --- /dev/null +++ b/app/examples/layers/domains/entities/client.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/domains/entities/client.ts b/app/examples/layers/domains/entities/client.ts new file mode 100644 index 0000000..1be1922 --- /dev/null +++ b/app/examples/layers/domains/entities/client.ts @@ -0,0 +1,53 @@ +import { A, C, DPE, O, pipe } from "@duplojs/utils"; +import { Book } from "./book"; + +export namespace Client { + export const Id = C.createNewType("ClientId", DPE.string(), C.Uuid); + export type Id = C.GetNewType; + + export const Entity = C.createEntity( + "Client", + ({ structure, nullable }) => ({ + id: Id, + borrowedBooks: structure({ + one: nullable(Book.Id), + two: nullable(Book.Id), + three: nullable(Book.Id), + four: nullable(Book.Id), + five: nullable(Book.Id), + }), + }), + ); + export type Entity = C.GetEntity; + + export const CanRent = C.createFlag< + Entity, + "CanRent", + { location: keyof Entity["borrowedBooks"] } + >("CanRent"); + export type CanRent = C.GetFlag; + + export const CanNotRent = C.createFlag("CanNotRent"); + export type CanNotRent = C.GetFlag; + + export function cantRent(client: T) { + const availableLocation = pipe( + O.entries(client.borrowedBooks), + A.reduce( + A.reduceFrom(null), + ({ element: [key, bookId], exit, next }) => bookId === null + ? exit(key) + : next(null), + ), + ); + + if (availableLocation) { + return CanRent.append( + client, + { location: availableLocation }, + ); + } + + return CanNotRent.append(client); + } +} diff --git a/app/examples/layers/domains/entities/index.ts b/app/examples/layers/domains/entities/index.ts new file mode 100644 index 0000000..3bcb362 --- /dev/null +++ b/app/examples/layers/domains/entities/index.ts @@ -0,0 +1,2 @@ +export * from "./book"; +export * from "./client"; diff --git a/app/examples/layers/index.ts b/app/examples/layers/index.ts new file mode 100644 index 0000000..ed897a2 --- /dev/null +++ b/app/examples/layers/index.ts @@ -0,0 +1,47 @@ +import ApplicationsRepositoriesBook from "@/examples/layers/applications/repositories/book.md"; +import ApplicationsRepositoriesClient from "@/examples/layers/applications/repositories/client.md"; +import ApplicationsUseCasesBookIsBorrowed from "@/examples/layers/applications/useCases/bookIsBorrowed.md"; +import ApplicationsUseCasesClientBorrowBook from "@/examples/layers/applications/useCases/clientBorrowBook.md"; +import ApplicationsUseCasesClientCanRent from "@/examples/layers/applications/useCases/clientCanRent.md"; +import ApplicationsUseCasesClientGiveBackBook from "@/examples/layers/applications/useCases/clientGiveBackBook.md"; +import ApplicationsUseCasesFindOneBook from "@/examples/layers/applications/useCases/findOneBook.md"; +import ApplicationsUseCasesFindOneClient from "@/examples/layers/applications/useCases/findOneClient.md"; +import ClientMain from "@/examples/layers/client/main.md"; +import ClientSwagger from "@/examples/layers/client/swagger.md"; +import ClientTypesD from "@/examples/layers/client/types.d.md"; +import DomainsAggregatesClientBorrowBook from "@/examples/layers/domains/aggregates/clientBorrowBook.md"; +import DomainsAggregatesClientGiveBackBook from "@/examples/layers/domains/aggregates/clientGiveBackBook.md"; +import DomainsEntitiesBook from "@/examples/layers/domains/entities/book.md"; +import DomainsEntitiesClient from "@/examples/layers/domains/entities/client.md"; +import InfrastructureAdaptersRepositoriesBook from "@/examples/layers/infrastructure/adapters/repositories/book.md"; +import InfrastructureAdaptersRepositoriesClient from "@/examples/layers/infrastructure/adapters/repositories/client.md"; +import InfrastructureCheckersBook from "@/examples/layers/infrastructure/checkers/book.md"; +import InfrastructureCheckersClient from "@/examples/layers/infrastructure/checkers/client.md"; +import InfrastructureMain from "@/examples/layers/infrastructure/main.md"; +import InfrastructureRoutesClientBorrowBook from "@/examples/layers/infrastructure/routes/clientBorrowBook.md"; +import InfrastructureRoutesClientGiveBackBook from "@/examples/layers/infrastructure/routes/clientGiveBackBook.md"; + +export { + ApplicationsRepositoriesBook, + ApplicationsRepositoriesClient, + ApplicationsUseCasesBookIsBorrowed, + ApplicationsUseCasesClientBorrowBook, + ApplicationsUseCasesClientCanRent, + ApplicationsUseCasesClientGiveBackBook, + ApplicationsUseCasesFindOneBook, + ApplicationsUseCasesFindOneClient, + ClientMain, + ClientSwagger, + ClientTypesD, + DomainsAggregatesClientBorrowBook, + DomainsAggregatesClientGiveBackBook, + DomainsEntitiesBook, + DomainsEntitiesClient, + InfrastructureAdaptersRepositoriesBook, + InfrastructureAdaptersRepositoriesClient, + InfrastructureCheckersBook, + InfrastructureCheckersClient, + InfrastructureMain, + InfrastructureRoutesClientBorrowBook, + InfrastructureRoutesClientGiveBackBook, +}; diff --git a/app/examples/layers/infrastructure/adapters/index.ts b/app/examples/layers/infrastructure/adapters/index.ts new file mode 100644 index 0000000..0193817 --- /dev/null +++ b/app/examples/layers/infrastructure/adapters/index.ts @@ -0,0 +1,14 @@ +import { C } from "@duplojs/utils"; +import * as UseCases from "@applications/useCase"; +import { bookRepository } from "./repositories/book"; +import { clientRepository } from "./repositories/client"; + +export const useCases = C.useCaseInstances( + { + ...UseCases, + }, + { + bookRepository, + clientRepository, + }, +); diff --git a/app/examples/layers/infrastructure/adapters/repositories/book.md b/app/examples/layers/infrastructure/adapters/repositories/book.md new file mode 100644 index 0000000..bc13148 --- /dev/null +++ b/app/examples/layers/infrastructure/adapters/repositories/book.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/adapters/repositories/book.ts b/app/examples/layers/infrastructure/adapters/repositories/book.ts new file mode 100644 index 0000000..20f5358 --- /dev/null +++ b/app/examples/layers/infrastructure/adapters/repositories/book.ts @@ -0,0 +1,7 @@ +import { BookRepository } from "@applications/repositories"; +import { C } from "@duplojs/utils"; + +export const bookRepository = BookRepository.createImplementation({ + findOne: (id) => Promise.resolve(C.none("Book")), + save: (entity) => Promise.resolve(entity), +}); diff --git a/app/examples/layers/infrastructure/adapters/repositories/client.md b/app/examples/layers/infrastructure/adapters/repositories/client.md new file mode 100644 index 0000000..2b607cf --- /dev/null +++ b/app/examples/layers/infrastructure/adapters/repositories/client.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/adapters/repositories/client.ts b/app/examples/layers/infrastructure/adapters/repositories/client.ts new file mode 100644 index 0000000..effcb3e --- /dev/null +++ b/app/examples/layers/infrastructure/adapters/repositories/client.ts @@ -0,0 +1,7 @@ +import { ClientRepository } from "@applications/repositories"; +import { C } from "@duplojs/utils"; + +export const clientRepository = ClientRepository.createImplementation({ + findOne: (id) => Promise.resolve(C.none("Client")), + save: (entity) => Promise.resolve(entity), +}); diff --git a/app/examples/layers/infrastructure/checkers/book.md b/app/examples/layers/infrastructure/checkers/book.md new file mode 100644 index 0000000..cc8f3dd --- /dev/null +++ b/app/examples/layers/infrastructure/checkers/book.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/checkers/book.ts b/app/examples/layers/infrastructure/checkers/book.ts new file mode 100644 index 0000000..b1de96a --- /dev/null +++ b/app/examples/layers/infrastructure/checkers/book.ts @@ -0,0 +1,25 @@ +import type { Book } from "@domains/entities"; +import { createPresetChecker, ResponseContract, useCheckerBuilder } from "@duplojs/http"; +import { useCases } from "../adapters"; +import { E, pipe } from "@duplojs/utils"; + +export const checkBookExist = useCheckerBuilder() + .handler( + async(bookId: Book.Id, { output }) => { + const result = await useCases.findOneBookUseCase({ bookId }); + + return pipe( + result, + E.whenIsRight((book) => output("book.exist", book)), + E.whenIsLeft(() => output("book.notExist", null)), + ); + }, + ); + +export const iWantBookExist = createPresetChecker( + checkBookExist, + { + result: "book.exist", + otherwise: ResponseContract.notFound("book.notExist"), + }, +); diff --git a/app/examples/layers/infrastructure/checkers/client.md b/app/examples/layers/infrastructure/checkers/client.md new file mode 100644 index 0000000..afebcf1 --- /dev/null +++ b/app/examples/layers/infrastructure/checkers/client.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/checkers/client.ts b/app/examples/layers/infrastructure/checkers/client.ts new file mode 100644 index 0000000..0e70946 --- /dev/null +++ b/app/examples/layers/infrastructure/checkers/client.ts @@ -0,0 +1,25 @@ +import type { Client } from "@domains/entities"; +import { createPresetChecker, ResponseContract, useCheckerBuilder } from "@duplojs/http"; +import { useCases } from "../adapters"; +import { E, pipe } from "@duplojs/utils"; + +export const checkClientExist = useCheckerBuilder() + .handler( + async(clientId: Client.Id, { output }) => { + const result = await useCases.findOneClientUseCase({ clientId }); + + return pipe( + result, + E.whenIsRight((client) => output("client.exist", client)), + E.whenIsLeft(() => output("client.notExist", null)), + ); + }, + ); + +export const iWantClientExist = createPresetChecker( + checkClientExist, + { + result: "client.exist", + otherwise: ResponseContract.notFound("client.notExist"), + }, +); diff --git a/app/examples/layers/infrastructure/main.md b/app/examples/layers/infrastructure/main.md new file mode 100644 index 0000000..7627d69 --- /dev/null +++ b/app/examples/layers/infrastructure/main.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/main.ts b/app/examples/layers/infrastructure/main.ts new file mode 100644 index 0000000..76c90f7 --- /dev/null +++ b/app/examples/layers/infrastructure/main.ts @@ -0,0 +1,24 @@ +import { createHub, routeStore } from "@duplojs/http"; +import { createHttpServer } from "@duplojs/http/node"; +import { codeGeneratorPlugin } from "@duplojs/http/codeGenerator"; +import { openApiGeneratorPlugin } from "@duplojs/http/openApiGenerator"; + +import "./routes"; + +const hub = createHub({ environment: "BUILD" }) + .register(routeStore.getAll()) + .plug(codeGeneratorPlugin({ outputFile: "types.d.ts" })) + .plug(openApiGeneratorPlugin({ + outputFile: "swagger.json", + routePath: "/swagger-ui", + })); + +await createHttpServer( + hub, + { + host: "localhost", + port: 1506, + }, +); + +console.log("Server is ready !"); diff --git a/app/examples/layers/infrastructure/routes/clientBorrowBook.md b/app/examples/layers/infrastructure/routes/clientBorrowBook.md new file mode 100644 index 0000000..fef52bf --- /dev/null +++ b/app/examples/layers/infrastructure/routes/clientBorrowBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/routes/clientBorrowBook.ts b/app/examples/layers/infrastructure/routes/clientBorrowBook.ts new file mode 100644 index 0000000..141abdc --- /dev/null +++ b/app/examples/layers/infrastructure/routes/clientBorrowBook.ts @@ -0,0 +1,56 @@ +import { Book, Client } from "@domains/entities"; +import { ResponseContract, useRouteBuilder } from "@duplojs/http"; +import { useCases } from "infrastructure/adapters"; +import { iWantBookExist } from "infrastructure/checkers/book"; +import { iWantClientExist } from "infrastructure/checkers/client"; + +useRouteBuilder("POST", "/clients/{clientId}/borrow-book") + .extract({ + params: { + clientId: Client.Id.toExtractParser(), + }, + body: { + bookId: Book.Id.toExtractParser(), + }, + }) + .presetCheck( + iWantClientExist.indexing("client"), + ({ clientId }) => clientId, + ) + .presetCheck( + iWantBookExist.indexing("book"), + ({ bookId }) => bookId, + ) + .cut( + [ + ResponseContract.conflict("book.alreadyBorrowed"), + ResponseContract.conflict("client.haveNotFreeLocation"), + ], + ({ book, client }, { output, response }) => { + const availableBook = useCases.bookIsBorrowedUseCase({ book }); + if (Book.Borrow.has(availableBook)) { + return response("book.alreadyBorrowed"); + } + + const clientWithLocation = useCases.clientCanRentUseCase({ client }); + if (Client.CanNotRent.has(clientWithLocation)) { + return response("client.haveNotFreeLocation"); + } + + return output({ + availableBook, + clientWithLocation, + }); + }, + ) + .handler( + ResponseContract.noContent("client.borrowBook"), + async({ availableBook, clientWithLocation }, { response }) => { + await useCases.clientBorrowBookUseCase({ + book: availableBook, + client: clientWithLocation, + }); + + return response("client.borrowBook"); + }, + ); diff --git a/app/examples/layers/infrastructure/routes/clientGiveBackBook.md b/app/examples/layers/infrastructure/routes/clientGiveBackBook.md new file mode 100644 index 0000000..bbde940 --- /dev/null +++ b/app/examples/layers/infrastructure/routes/clientGiveBackBook.md @@ -0,0 +1,3 @@ +```ts + +``` diff --git a/app/examples/layers/infrastructure/routes/clientGiveBackBook.ts b/app/examples/layers/infrastructure/routes/clientGiveBackBook.ts new file mode 100644 index 0000000..ba3e198 --- /dev/null +++ b/app/examples/layers/infrastructure/routes/clientGiveBackBook.ts @@ -0,0 +1,57 @@ +import { Book, Client } from "@domains/entities"; +import { ResponseContract, useRouteBuilder } from "@duplojs/http"; +import { E, P } from "@duplojs/utils"; +import { useCases } from "infrastructure/adapters"; +import { iWantBookExist } from "infrastructure/checkers/book"; +import { iWantClientExist } from "infrastructure/checkers/client"; + +useRouteBuilder("POST", "/clients/{clientId}/give-back-book") + .extract({ + params: { + clientId: Client.Id.toExtractParser(), + }, + body: { + bookId: Book.Id.toExtractParser(), + }, + }) + .presetCheck( + iWantClientExist.indexing("client"), + ({ clientId }) => clientId, + ) + .presetCheck( + iWantBookExist.indexing("book"), + ({ bookId }) => bookId, + ) + .cut( + [ + ResponseContract.conflict("book.isNotBorrowed"), + ResponseContract.forbidden("client.isNotBorrower"), + ResponseContract.conflict("client.haveNotBook"), + ], + async({ book, client }, { output, response }) => { + const borrowedBook = useCases.bookIsBorrowedUseCase({ book }); + if (Book.Available.has(borrowedBook)) { + return response("book.isNotBorrowed"); + } + + const result = await useCases.clientGiveBackBookUseCase({ + book: borrowedBook, + client, + }); + + return P.match(result) + .when( + E.hasInformation("client-is-not-borrower"), + () => response("client.isNotBorrower"), + ) + .when( + E.hasInformation("client-have-not-book"), + () => response("client.haveNotBook"), + ) + .otherwise(output); + }, + ) + .handler( + ResponseContract.noContent("client.giveBackBook"), + (__, { response }) => response("client.giveBackBook"), + ); diff --git a/app/examples/layers/infrastructure/routes/index.ts b/app/examples/layers/infrastructure/routes/index.ts new file mode 100644 index 0000000..4055ec7 --- /dev/null +++ b/app/examples/layers/infrastructure/routes/index.ts @@ -0,0 +1,2 @@ +import "./clientBorrowBook"; +import "./clientGiveBackBook"; diff --git a/app/examples/layers/tsconfig.json b/app/examples/layers/tsconfig.json new file mode 100644 index 0000000..8d37a62 --- /dev/null +++ b/app/examples/layers/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": ["../../../tsconfig.json"], + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@domains/entities": ["./domains/entities/index.ts"], + "@domains/aggregates": ["./domains/aggregates/index.ts"], + "@applications/repositories": ["./applications/repositories/index.ts"], + "@applications/useCase": ["./applications/useCases/index.ts"], + } + }, + "include": ["**/*.ts"], + "exclude": ["index.ts"], +} \ No newline at end of file diff --git a/app/examples/versus/clean/application/duplojs.md b/app/examples/versus/clean/application/duplojs.md new file mode 100644 index 0000000..eda0426 --- /dev/null +++ b/app/examples/versus/clean/application/duplojs.md @@ -0,0 +1,7 @@ +```ts twoslash +// @filename: domain/duplojs.ts + +// @filename: application/duplojs.ts +// ---cut--- + +``` diff --git a/app/examples/home/clean/application/duplojs.ts b/app/examples/versus/clean/application/duplojs.ts similarity index 100% rename from app/examples/home/clean/application/duplojs.ts rename to app/examples/versus/clean/application/duplojs.ts diff --git a/app/examples/versus/clean/application/other.md b/app/examples/versus/clean/application/other.md new file mode 100644 index 0000000..f132023 --- /dev/null +++ b/app/examples/versus/clean/application/other.md @@ -0,0 +1,7 @@ +```ts twoslash +// @filename: domain/other.ts + +// @filename: application/other.ts +// ---cut--- + +``` diff --git a/app/examples/home/clean/application/other.ts b/app/examples/versus/clean/application/other.ts similarity index 100% rename from app/examples/home/clean/application/other.ts rename to app/examples/versus/clean/application/other.ts diff --git a/app/examples/versus/clean/domain/duplojs.md b/app/examples/versus/clean/domain/duplojs.md new file mode 100644 index 0000000..01d6089 --- /dev/null +++ b/app/examples/versus/clean/domain/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/clean/domain/duplojs.ts b/app/examples/versus/clean/domain/duplojs.ts similarity index 100% rename from app/examples/home/clean/domain/duplojs.ts rename to app/examples/versus/clean/domain/duplojs.ts diff --git a/app/examples/versus/clean/domain/other.md b/app/examples/versus/clean/domain/other.md new file mode 100644 index 0000000..bcc0ed8 --- /dev/null +++ b/app/examples/versus/clean/domain/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/clean/domain/other.ts b/app/examples/versus/clean/domain/other.ts similarity index 100% rename from app/examples/home/clean/domain/other.ts rename to app/examples/versus/clean/domain/other.ts diff --git a/app/examples/versus/command/duplojs.md b/app/examples/versus/command/duplojs.md new file mode 100644 index 0000000..1791858 --- /dev/null +++ b/app/examples/versus/command/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/command/duplojs.ts b/app/examples/versus/command/duplojs.ts similarity index 100% rename from app/examples/home/command/duplojs.ts rename to app/examples/versus/command/duplojs.ts diff --git a/app/examples/versus/command/other.md b/app/examples/versus/command/other.md new file mode 100644 index 0000000..3adcb16 --- /dev/null +++ b/app/examples/versus/command/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/command/other.ts b/app/examples/versus/command/other.ts similarity index 100% rename from app/examples/home/command/other.ts rename to app/examples/versus/command/other.ts diff --git a/app/examples/versus/either/fetch/duplojs.md b/app/examples/versus/either/fetch/duplojs.md new file mode 100644 index 0000000..e7dac7e --- /dev/null +++ b/app/examples/versus/either/fetch/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/either/fetch/duplojs.ts b/app/examples/versus/either/fetch/duplojs.ts similarity index 100% rename from app/examples/home/either/fetch/duplojs.ts rename to app/examples/versus/either/fetch/duplojs.ts diff --git a/app/examples/versus/either/fetch/other.md b/app/examples/versus/either/fetch/other.md new file mode 100644 index 0000000..5e8a932 --- /dev/null +++ b/app/examples/versus/either/fetch/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/either/fetch/other.ts b/app/examples/versus/either/fetch/other.ts similarity index 100% rename from app/examples/home/either/fetch/other.ts rename to app/examples/versus/either/fetch/other.ts diff --git a/app/examples/versus/either/file/duplojs.md b/app/examples/versus/either/file/duplojs.md new file mode 100644 index 0000000..078b302 --- /dev/null +++ b/app/examples/versus/either/file/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/either/file/duplojs.ts b/app/examples/versus/either/file/duplojs.ts similarity index 100% rename from app/examples/home/either/file/duplojs.ts rename to app/examples/versus/either/file/duplojs.ts diff --git a/app/examples/versus/either/file/other.md b/app/examples/versus/either/file/other.md new file mode 100644 index 0000000..cf1511a --- /dev/null +++ b/app/examples/versus/either/file/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/either/file/other.ts b/app/examples/versus/either/file/other.ts similarity index 100% rename from app/examples/home/either/file/other.ts rename to app/examples/versus/either/file/other.ts diff --git a/app/examples/versus/either/jwt/duplojs.md b/app/examples/versus/either/jwt/duplojs.md new file mode 100644 index 0000000..2571590 --- /dev/null +++ b/app/examples/versus/either/jwt/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/either/jwt/duplojs.ts b/app/examples/versus/either/jwt/duplojs.ts similarity index 100% rename from app/examples/home/either/jwt/duplojs.ts rename to app/examples/versus/either/jwt/duplojs.ts diff --git a/app/examples/versus/either/jwt/other.md b/app/examples/versus/either/jwt/other.md new file mode 100644 index 0000000..39c170f --- /dev/null +++ b/app/examples/versus/either/jwt/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/either/jwt/other.ts b/app/examples/versus/either/jwt/other.ts similarity index 100% rename from app/examples/home/either/jwt/other.ts rename to app/examples/versus/either/jwt/other.ts diff --git a/app/examples/home/form/DuploJSForm.vue b/app/examples/versus/form/DuploJSForm.vue similarity index 100% rename from app/examples/home/form/DuploJSForm.vue rename to app/examples/versus/form/DuploJSForm.vue diff --git a/app/examples/home/form/TanstackForm.vue b/app/examples/versus/form/TanstackForm.vue similarity index 100% rename from app/examples/home/form/TanstackForm.vue rename to app/examples/versus/form/TanstackForm.vue diff --git a/app/examples/versus/form/duplojs.md b/app/examples/versus/form/duplojs.md new file mode 100644 index 0000000..7af938b --- /dev/null +++ b/app/examples/versus/form/duplojs.md @@ -0,0 +1,3 @@ +```vue + +``` \ No newline at end of file diff --git a/app/examples/versus/form/other.md b/app/examples/versus/form/other.md new file mode 100644 index 0000000..c5997a3 --- /dev/null +++ b/app/examples/versus/form/other.md @@ -0,0 +1,3 @@ +```vue + +``` \ No newline at end of file diff --git a/app/examples/versus/http/client/duplojs.md b/app/examples/versus/http/client/duplojs.md new file mode 100644 index 0000000..8fc6669 --- /dev/null +++ b/app/examples/versus/http/client/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/http/client/duplojs.ts b/app/examples/versus/http/client/duplojs.ts similarity index 100% rename from app/examples/home/http/client/duplojs.ts rename to app/examples/versus/http/client/duplojs.ts diff --git a/app/examples/versus/http/client/other.md b/app/examples/versus/http/client/other.md new file mode 100644 index 0000000..602495b --- /dev/null +++ b/app/examples/versus/http/client/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/http/client/other.ts b/app/examples/versus/http/client/other.ts similarity index 100% rename from app/examples/home/http/client/other.ts rename to app/examples/versus/http/client/other.ts diff --git a/app/examples/versus/http/route/duplojs.md b/app/examples/versus/http/route/duplojs.md new file mode 100644 index 0000000..eaf9bd1 --- /dev/null +++ b/app/examples/versus/http/route/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/http/route/duplojs.ts b/app/examples/versus/http/route/duplojs.ts similarity index 100% rename from app/examples/home/http/route/duplojs.ts rename to app/examples/versus/http/route/duplojs.ts diff --git a/app/examples/versus/http/route/other.md b/app/examples/versus/http/route/other.md new file mode 100644 index 0000000..8ad232a --- /dev/null +++ b/app/examples/versus/http/route/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` diff --git a/app/examples/home/http/route/other.ts b/app/examples/versus/http/route/other.ts similarity index 100% rename from app/examples/home/http/route/other.ts rename to app/examples/versus/http/route/other.ts diff --git a/app/examples/versus/index.ts b/app/examples/versus/index.ts new file mode 100644 index 0000000..cc74fa8 --- /dev/null +++ b/app/examples/versus/index.ts @@ -0,0 +1,59 @@ +import CleanApplicationD from "@/examples/versus/clean/application/duplojs.md"; +import CleanApplicationO from "@/examples/versus/clean/application/other.md"; +import CleanDomainD from "@/examples/versus/clean/domain/duplojs.md"; +import CleanDomainO from "@/examples/versus/clean/domain/other.md"; +import CommandD from "@/examples/versus/command/duplojs.md"; +import CommandO from "@/examples/versus/command/other.md"; +import EitherFetchD from "@/examples/versus/either/fetch/duplojs.md"; +import EitherFetchO from "@/examples/versus/either/fetch/other.md"; +import EitherFileD from "@/examples/versus/either/file/duplojs.md"; +import EitherFileO from "@/examples/versus/either/file/other.md"; +import EitherJwtD from "@/examples/versus/either/jwt/duplojs.md"; +import EitherJwtO from "@/examples/versus/either/jwt/other.md"; +import FormD from "@/examples/versus/form/duplojs.md"; +import FormO from "@/examples/versus/form/other.md"; +import HttpClientD from "@/examples/versus/http/client/duplojs.md"; +import HttpClientO from "@/examples/versus/http/client/other.md"; +import HttpRouteD from "@/examples/versus/http/route/duplojs.md"; +import HttpRouteO from "@/examples/versus/http/route/other.md"; +import UFArrayMinElementD from "@/examples/versus/utilsFunction/arrayMinElement/duplojs.md"; +import UFArrayMinElementO from "@/examples/versus/utilsFunction/arrayMinElement/other.md"; +import UFEntriesD from "@/examples/versus/utilsFunction/entries/duplojs.md"; +import UFEntriesO from "@/examples/versus/utilsFunction/entries/other.md"; +import UFGroupByD from "@/examples/versus/utilsFunction/groupBy/duplojs.md"; +import UFGroupByO from "@/examples/versus/utilsFunction/groupBy/other.md"; +import UFMapTupleD from "@/examples/versus/utilsFunction/mapTuple/duplojs.md"; +import UFMapTupleO from "@/examples/versus/utilsFunction/mapTuple/other.md"; +import UFReduceD from "@/examples/versus/utilsFunction/reduce/duplojs.md"; +import UFReduceO from "@/examples/versus/utilsFunction/reduce/other.md"; + +export { + CleanApplicationD, + CleanApplicationO, + CleanDomainD, + CleanDomainO, + CommandD, + CommandO, + EitherFetchD, + EitherFetchO, + EitherFileD, + EitherFileO, + EitherJwtD, + EitherJwtO, + FormD, + FormO, + HttpClientD, + HttpClientO, + HttpRouteD, + HttpRouteO, + UFArrayMinElementD, + UFArrayMinElementO, + UFEntriesD, + UFEntriesO, + UFGroupByD, + UFGroupByO, + UFMapTupleD, + UFMapTupleO, + UFReduceD, + UFReduceO, +}; diff --git a/app/examples/versus/utilsFunction/arrayMinElement/duplojs.md b/app/examples/versus/utilsFunction/arrayMinElement/duplojs.md new file mode 100644 index 0000000..4b70518 --- /dev/null +++ b/app/examples/versus/utilsFunction/arrayMinElement/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/arrayMinElement/duplojs.ts b/app/examples/versus/utilsFunction/arrayMinElement/duplojs.ts similarity index 100% rename from app/examples/home/utilsFunction/arrayMinElement/duplojs.ts rename to app/examples/versus/utilsFunction/arrayMinElement/duplojs.ts diff --git a/app/examples/versus/utilsFunction/arrayMinElement/other.md b/app/examples/versus/utilsFunction/arrayMinElement/other.md new file mode 100644 index 0000000..dbbe3cf --- /dev/null +++ b/app/examples/versus/utilsFunction/arrayMinElement/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/arrayMinElement/other.ts b/app/examples/versus/utilsFunction/arrayMinElement/other.ts similarity index 100% rename from app/examples/home/utilsFunction/arrayMinElement/other.ts rename to app/examples/versus/utilsFunction/arrayMinElement/other.ts diff --git a/app/examples/versus/utilsFunction/entries/duplojs.md b/app/examples/versus/utilsFunction/entries/duplojs.md new file mode 100644 index 0000000..d4c42ff --- /dev/null +++ b/app/examples/versus/utilsFunction/entries/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/entries/duplojs.ts b/app/examples/versus/utilsFunction/entries/duplojs.ts similarity index 100% rename from app/examples/home/utilsFunction/entries/duplojs.ts rename to app/examples/versus/utilsFunction/entries/duplojs.ts diff --git a/app/examples/versus/utilsFunction/entries/other.md b/app/examples/versus/utilsFunction/entries/other.md new file mode 100644 index 0000000..a6b186f --- /dev/null +++ b/app/examples/versus/utilsFunction/entries/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/entries/other.ts b/app/examples/versus/utilsFunction/entries/other.ts similarity index 100% rename from app/examples/home/utilsFunction/entries/other.ts rename to app/examples/versus/utilsFunction/entries/other.ts diff --git a/app/examples/versus/utilsFunction/groupBy/duplojs.md b/app/examples/versus/utilsFunction/groupBy/duplojs.md new file mode 100644 index 0000000..d8483d2 --- /dev/null +++ b/app/examples/versus/utilsFunction/groupBy/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/groupBy/duplojs.ts b/app/examples/versus/utilsFunction/groupBy/duplojs.ts similarity index 100% rename from app/examples/home/utilsFunction/groupBy/duplojs.ts rename to app/examples/versus/utilsFunction/groupBy/duplojs.ts diff --git a/app/examples/versus/utilsFunction/groupBy/other.md b/app/examples/versus/utilsFunction/groupBy/other.md new file mode 100644 index 0000000..91c42df --- /dev/null +++ b/app/examples/versus/utilsFunction/groupBy/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/groupBy/other.ts b/app/examples/versus/utilsFunction/groupBy/other.ts similarity index 100% rename from app/examples/home/utilsFunction/groupBy/other.ts rename to app/examples/versus/utilsFunction/groupBy/other.ts diff --git a/app/examples/versus/utilsFunction/mapTuple/duplojs.md b/app/examples/versus/utilsFunction/mapTuple/duplojs.md new file mode 100644 index 0000000..58d3d33 --- /dev/null +++ b/app/examples/versus/utilsFunction/mapTuple/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/mapTuple/duplojs.ts b/app/examples/versus/utilsFunction/mapTuple/duplojs.ts similarity index 100% rename from app/examples/home/utilsFunction/mapTuple/duplojs.ts rename to app/examples/versus/utilsFunction/mapTuple/duplojs.ts diff --git a/app/examples/versus/utilsFunction/mapTuple/other.md b/app/examples/versus/utilsFunction/mapTuple/other.md new file mode 100644 index 0000000..2af16f0 --- /dev/null +++ b/app/examples/versus/utilsFunction/mapTuple/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/mapTuple/other.ts b/app/examples/versus/utilsFunction/mapTuple/other.ts similarity index 100% rename from app/examples/home/utilsFunction/mapTuple/other.ts rename to app/examples/versus/utilsFunction/mapTuple/other.ts diff --git a/app/examples/versus/utilsFunction/reduce/duplojs.md b/app/examples/versus/utilsFunction/reduce/duplojs.md new file mode 100644 index 0000000..6fed27c --- /dev/null +++ b/app/examples/versus/utilsFunction/reduce/duplojs.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/reduce/duplojs.ts b/app/examples/versus/utilsFunction/reduce/duplojs.ts similarity index 100% rename from app/examples/home/utilsFunction/reduce/duplojs.ts rename to app/examples/versus/utilsFunction/reduce/duplojs.ts diff --git a/app/examples/versus/utilsFunction/reduce/other.md b/app/examples/versus/utilsFunction/reduce/other.md new file mode 100644 index 0000000..d41444e --- /dev/null +++ b/app/examples/versus/utilsFunction/reduce/other.md @@ -0,0 +1,3 @@ +```ts twoslash + +``` \ No newline at end of file diff --git a/app/examples/home/utilsFunction/reduce/other.ts b/app/examples/versus/utilsFunction/reduce/other.ts similarity index 100% rename from app/examples/home/utilsFunction/reduce/other.ts rename to app/examples/versus/utilsFunction/reduce/other.ts diff --git a/eslint.config.js b/eslint.config.js index 388ae72..68b8f92 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,6 +45,11 @@ export default [ prev: "*", next: "*", }, + { + blankLine: "never", + prev: "span", + next: "span", + }, ], ], "vue/block-order": [ @@ -77,13 +82,20 @@ export default [ "error", { min: 2, - exceptions: ["t"], + properties: "never", + exceptions: ["t", "e", "i", "v", "x", "y", "z", "c", "r", "h", "p"], }, ], }, files: ["app/**/*.vue", "app/**/*.ts"], ignores: ["**/*.d.ts"], }, + { + rules: { + "vue/multi-word-component-names": "off", + }, + files: ["app/.vitepress/theme/Layout.vue"], + }, { rules: { "@stylistic/js/no-multiple-empty-lines": "off", @@ -95,6 +107,6 @@ export default [ files: ["app/examples/**/*.ts"], }, { - ignores: ["app/public/*", "app/.vitepress/cache/*", "app/.vitepress/dist/*"] + ignores: ["app/public/*", "app/.vitepress/cache/*", "app/.vitepress/dist/*", "**/**.d.ts"] } ]; diff --git a/temp.md b/temp.md new file mode 100644 index 0000000..87f408a --- /dev/null +++ b/temp.md @@ -0,0 +1,30 @@ +# Struct + +## Comments + +- couleurs (plus proches du logo) +- liste des libs: (duplojs/utils (main brick), duplojs/http, duplojs/server-utils, duplojs/json-web-token, duplojs/form, duplojs/playwright). +- nimation "buildIsoBricks" doit pouvoir accepter des mots de plus de 4 lettres. Idealement faire en sorte que l'on puisse avoir des rectangles par exemples. +- pas de diff entre core et tooling (juste 6 package de base). sinon le main est duplojs/utils +- retirer toute mention de performance (dupojs mes en avant la robutesse. TDD first (type driven development)) + +### Sections Orders: + +- Why DuploJS + - 6 packages + - +1000 fonctions + - All runtime (node, bun, deno, browser) + - Robuste code +- Everything you need + - Orders: + 1. @duplojs/utils + 2. @duplojs/http + 3. @duplojs/server-utils + 4. duplojs/json-web-token + 5. duplojs/form + 6. duplojs/playwright +- Top 10 versus +- Exemple couche en couche + - (à fournir) (@ZeRiix, @mathcovax) +- Sequence Diagram + - (à fournir avec du code) (@ZeRiix, @mathcovax) \ No newline at end of file