From 65d74cf57aa462c718501ff55af28f0094630ecc Mon Sep 17 00:00:00 2001 From: xSuneth <44576805+xSuneth@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:12:04 +0530 Subject: [PATCH] docs: add Next.js documentation and landing page --- docs/.gitignore | 41 +++ docs/README.md | 36 +++ docs/components.json | 23 ++ docs/eslint.config.mjs | 18 ++ docs/next.config.ts | 7 + docs/package.json | 39 +++ docs/postcss.config.mjs | 8 + docs/public/file.svg | 1 + docs/public/globe.svg | 1 + docs/public/next.svg | 1 + docs/public/vercel.svg | 1 + docs/public/window.svg | 1 + docs/src/app/docs/how-it-works/page.tsx | 92 +++++++ docs/src/app/docs/installation/page.tsx | 69 +++++ docs/src/app/docs/layout.tsx | 68 +++++ docs/src/app/docs/page.tsx | 32 +++ docs/src/app/docs/react/page.tsx | 146 ++++++++++ docs/src/app/docs/server/page.tsx | 101 +++++++ docs/src/app/docs/vanilla/page.tsx | 91 +++++++ docs/src/app/favicon.ico | Bin 0 -> 25931 bytes docs/src/app/globals.css | 203 ++++++++++++++ docs/src/app/layout.tsx | 36 +++ docs/src/app/page.tsx | 300 +++++++++++++++++++++ docs/src/components/CodeBlock.tsx | 43 +++ docs/src/components/CodeCopyButton.tsx | 28 ++ docs/src/components/DocsPagination.tsx | 48 ++++ docs/src/components/DocsSidebar.tsx | 47 ++++ docs/src/components/PackageManagerCode.tsx | 68 +++++ docs/src/components/ui/accordion.tsx | 66 +++++ docs/src/components/ui/badge.tsx | 48 ++++ docs/src/components/ui/button.tsx | 64 +++++ docs/src/components/ui/card.tsx | 92 +++++++ docs/src/components/ui/scroll-area.tsx | 58 ++++ docs/src/components/ui/separator.tsx | 28 ++ docs/src/components/ui/sheet.tsx | 143 ++++++++++ docs/src/components/ui/tabs.tsx | 91 +++++++ docs/src/lib/utils.ts | 6 + docs/tailwind.config.ts | 84 ++++++ docs/tsconfig.json | 34 +++ 39 files changed, 2263 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/README.md create mode 100644 docs/components.json create mode 100644 docs/eslint.config.mjs create mode 100644 docs/next.config.ts create mode 100644 docs/package.json create mode 100644 docs/postcss.config.mjs create mode 100644 docs/public/file.svg create mode 100644 docs/public/globe.svg create mode 100644 docs/public/next.svg create mode 100644 docs/public/vercel.svg create mode 100644 docs/public/window.svg create mode 100644 docs/src/app/docs/how-it-works/page.tsx create mode 100644 docs/src/app/docs/installation/page.tsx create mode 100644 docs/src/app/docs/layout.tsx create mode 100644 docs/src/app/docs/page.tsx create mode 100644 docs/src/app/docs/react/page.tsx create mode 100644 docs/src/app/docs/server/page.tsx create mode 100644 docs/src/app/docs/vanilla/page.tsx create mode 100644 docs/src/app/favicon.ico create mode 100644 docs/src/app/globals.css create mode 100644 docs/src/app/layout.tsx create mode 100644 docs/src/app/page.tsx create mode 100644 docs/src/components/CodeBlock.tsx create mode 100644 docs/src/components/CodeCopyButton.tsx create mode 100644 docs/src/components/DocsPagination.tsx create mode 100644 docs/src/components/DocsSidebar.tsx create mode 100644 docs/src/components/PackageManagerCode.tsx create mode 100644 docs/src/components/ui/accordion.tsx create mode 100644 docs/src/components/ui/badge.tsx create mode 100644 docs/src/components/ui/button.tsx create mode 100644 docs/src/components/ui/card.tsx create mode 100644 docs/src/components/ui/scroll-area.tsx create mode 100644 docs/src/components/ui/separator.tsx create mode 100644 docs/src/components/ui/sheet.tsx create mode 100644 docs/src/components/ui/tabs.tsx create mode 100644 docs/src/lib/utils.ts create mode 100644 docs/tailwind.config.ts create mode 100644 docs/tsconfig.json diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/docs/components.json b/docs/components.json new file mode 100644 index 0000000..03909d9 --- /dev/null +++ b/docs/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/docs/eslint.config.mjs b/docs/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/docs/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/docs/next.config.ts b/docs/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/docs/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..94df3fa --- /dev/null +++ b/docs/package.json @@ -0,0 +1,39 @@ +{ + "name": "docs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@tailwindcss/typography": "^0.5.19", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.34.3", + "lucide-react": "^0.562.0", + "next": "16.1.6", + "radix-ui": "^1.4.3", + "react": "19.2.3", + "react-dom": "19.2.3", + "shiki": "^4.0.0", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "autoprefixer": "^10.4.27", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "postcss": "^8.5.6", + "shadcn": "^3.6.3", + "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } +} diff --git a/docs/postcss.config.mjs b/docs/postcss.config.mjs new file mode 100644 index 0000000..0241738 --- /dev/null +++ b/docs/postcss.config.mjs @@ -0,0 +1,8 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + "autoprefixer": {}, + }, +}; + +export default config; diff --git a/docs/public/file.svg b/docs/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/docs/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/globe.svg b/docs/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/docs/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/next.svg b/docs/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/docs/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/vercel.svg b/docs/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/docs/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/window.svg b/docs/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/docs/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/src/app/docs/how-it-works/page.tsx b/docs/src/app/docs/how-it-works/page.tsx new file mode 100644 index 0000000..848360d --- /dev/null +++ b/docs/src/app/docs/how-it-works/page.tsx @@ -0,0 +1,92 @@ +import { DocsPagination } from "@/components/DocsPagination"; +import { Cpu, Shield, FileCode2 } from "lucide-react"; +import { Card } from "@/components/ui/card"; + +export default function HowItWorksPage() { + return ( +
+

How It Works

+

+ Secure Input changes the paradigm of form handling by ensuring the raw text value never exists in the main thread's React state or the DOM in a way that is easily scannable by external scripts. +

+ +
+ +

The Architecture Flow

+ +
    +
  1. +
    1
    +
    + Keystroke Capture + As the user types, each keystroke is intercepted immediately at the input level before typical React or Vanilla JS state updates occur. +
    +
  2. +
  3. +
    2
    +
    + Worker Isolation + The plain text is sent to an isolated Web Worker. This means the sensitive processing occurs completely off the main thread, making it invisible to extensions inspecting the standard DOM or window object. +
    +
  4. +
  5. +
    3
    +
    + WASM Encryption + Inside the worker, a highly optimized, Rust-compiled WebAssembly module uses ChaCha20Poly1305 to instantly encrypt the payload. +
    +
  6. +
  7. +
    4
    +
    + Safe Output + The main thread only ever receives and holds the encrypted ciphertext. The plain text never touches the form's `value` attribute or the generic component state. +
    +
  8. +
+ +

The Three Packages

+

+ The project is broken down into three modular packages to keep bundle sizes incredibly small: +

+ +
+ + +

@secure-input/core

+

+ The framework-agnostic engine that manages the Web Worker and encryption lifecycle. Use this in Vanilla JS or other frameworks. +

+
+ + + +

@secure-input/react

+

+ Lightweight hooks and components wrapping the core for seamless React and Next.js integration. +

+
+ + + +

@secure-input/wasm

+

+ The raw ChaCha20 encryption module. (You typically do not interact with this directly, it's used internally). +

+
+
+ +
+

Security Notice

+

+ This library provides obfuscation, not absolute security. It raises the bar significantly against automated scrapers and basic extensions, making it annoying enough that standard bots fail. However, determined attackers with reverse-engineering skills, network traffic inspectors, or low-level OS keyloggers can still extract data. Always implement server-side validation and rate limiting. +

+
+ + +
+ ); +} \ No newline at end of file diff --git a/docs/src/app/docs/installation/page.tsx b/docs/src/app/docs/installation/page.tsx new file mode 100644 index 0000000..ec6e4ad --- /dev/null +++ b/docs/src/app/docs/installation/page.tsx @@ -0,0 +1,69 @@ +import { CodeBlock } from "@/components/CodeBlock"; +import { PackageManagerCode } from "@/components/PackageManagerCode"; +import { DocsPagination } from "@/components/DocsPagination"; + +export default function InstallationPage() { + return ( +
+

Installation

+

+ Choose the package that fits your technology stack. Secure Input is built to be modular so you only ship what you need. +

+ +
+ +

React / Next.js

+

+ If you are using React, you should install the @secure-input/react package. This package automatically includes the core logic and provides easy-to-use hooks and drop-in components. +

+ + + +

Vanilla JavaScript / Other Frameworks

+

+ If you are using Vue, Svelte, Angular, or plain HTML/JS, you should install the framework-agnostic @secure-input/core package. +

+ + + +

Bundle Sizes

+

+ The library is highly optimized for performance and minimal network impact. WebAssembly and Web Worker files are lazy-loaded dynamically. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PackageEnvironmentGzipped Size
@secure-input/reactClient (React)~5KB
@secure-input/coreClient (Vanilla JS)~15KB
@secure-input/wasmWorker Thread~10KB
+
+ + +
+ ); +} \ No newline at end of file diff --git a/docs/src/app/docs/layout.tsx b/docs/src/app/docs/layout.tsx new file mode 100644 index 0000000..38710d0 --- /dev/null +++ b/docs/src/app/docs/layout.tsx @@ -0,0 +1,68 @@ +import { DocsSidebar } from "@/components/DocsSidebar"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { Menu, Lock, GitBranch, BookText } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {/* Navigation */} + + +
+ +
+
+ {children} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/docs/src/app/docs/page.tsx b/docs/src/app/docs/page.tsx new file mode 100644 index 0000000..31243ff --- /dev/null +++ b/docs/src/app/docs/page.tsx @@ -0,0 +1,32 @@ +import { CodeBlock } from "@/components/CodeBlock"; +import { DocsPagination } from "@/components/DocsPagination"; + +export default function DocsPage() { + return ( +
+

Introduction

+

+ Secure Input is a lightweight, framework-agnostic library that uses WebAssembly encryption and Web Workers to protect sensitive input data from browser extensions and client-side scrapers. +

+ +
+ +

The Problem

+

+ Modern browsers are filled with extensions that help users by scraping the DOM. While tools like Honey or Capital One Shopping are great for consumers, they act as automated bots that can scrape and leak exclusive discount codes, single-use coupons, or referral links right out of your checkout flow. +

+

+ Standard input fields leave this data completely exposed in the DOM as plain text, allowing any extension with activeTab permissions to read it. +

+ +

The Solution

+

+ Secure Input changes this by moving the sensitive data off the main thread. It intercepts keystrokes, sends them to a dedicated Web Worker, and uses a Rust-compiled WebAssembly module to encrypt the data instantly. Only the encrypted ciphertext is ever exposed to the React state or the DOM. +

+ + +
+ ); +} \ No newline at end of file diff --git a/docs/src/app/docs/react/page.tsx b/docs/src/app/docs/react/page.tsx new file mode 100644 index 0000000..bc3a014 --- /dev/null +++ b/docs/src/app/docs/react/page.tsx @@ -0,0 +1,146 @@ +import { CodeBlock } from "@/components/CodeBlock"; +import { DocsPagination } from "@/components/DocsPagination"; + +export default function ReactPage() { + return ( +
+

React Implementation

+

+ The @secure-input/react package provides both a high-level drop-in component and a low-level hook to give you maximum flexibility. +

+ +
+ +

Option 1: The SecureInput Component

+

+ The easiest way to get started is using the pre-built <SecureInput /> wrapper. It handles the Web Worker initialization and state management for you automatically. +

+ + { + // 1. You receive the encrypted payload + // 2. Send it securely to your backend + + const response = await fetch("/api/validate-coupon", { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: encryptedValue, + }); + + const result = await response.json(); + console.log("Coupon valid:", result.valid); + }; + + return ( + + ); +}`} + lang="tsx" + /> + +

Component Props

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropTypeDescription
onEncryptedSubmit(encrypted: string) => voidCalled on Enter key or internal form submit. Passes the cipher text.
showStatusbooleanOptional. Defaults to true. Shows visual indicator of WASM loading state.
inputPropsReact.InputHTMLAttributesOptional. Standard HTML input attributes to spread onto the internal input field.
+
+ +

Option 2: The useSecureInput Hook

+

+ If you need to build a custom UI (like integrating into a complex form library like React Hook Form), use the useSecureInput hook. This gives you manual control over exactly when encryption happens. +

+ + { + e.preventDefault(); + + if (!isReady) return; + + // Trigger encryption right before network request + const encryptedPayload = await encrypt(plainTextInput); + + // Clear the plain text state immediately for safety + setPlainTextInput(""); + + await fetch("/api/checkout", { + method: "POST", + body: JSON.stringify({ coupon: encryptedPayload }) + }); + }; + + return ( +
+ setPlainTextInput(e.target.value)} + disabled={!isReady} + placeholder="Discount code" + /> + + + {error &&

Encryption failed to load.

} +
+ ); +}`} + lang="tsx" + /> + +

+ Best Practice: When using the hook, manually clear your React state (e.g. setPlainTextInput("")) immediately after you have generated the encrypted payload. This minimizes the window of time the plain text exists in memory. +

+ + +
+ ); +} \ No newline at end of file diff --git a/docs/src/app/docs/server/page.tsx b/docs/src/app/docs/server/page.tsx new file mode 100644 index 0000000..cf946fa --- /dev/null +++ b/docs/src/app/docs/server/page.tsx @@ -0,0 +1,101 @@ +import { CodeBlock } from "@/components/CodeBlock"; +import { Shield } from "lucide-react"; +import { DocsPagination } from "@/components/DocsPagination"; + +export default function ServerPage() { + return ( +
+

Server-Side Decryption

+

+ The client-side WASM module uses standard ChaCha20Poly1305 authenticated encryption. To process the submitted payload, you must decrypt it on your backend. +

+ +
+ +

Node.js Implementation

+

+ If your backend runs on Node.js (Express, Fastify, Next.js API routes), we recommend using the highly audited @noble/ciphers library. +

+ + + +

+ The ciphertext you receive from the client is a base64-encoded string. The first 12 bytes represent the randomly generated initialization vector (nonce), and the remaining bytes are the actual encrypted payload. +

+ + + +
+
+
+ +

Crucial Security Requirement

+
+

+ If a browser extension or bot extracts your hardcoded 32-byte encryption key from your client-side JavaScript bundle, the obfuscation is broken. They can simply recreate the ChaCha cipher and decrypt the payload themselves. +

+

+ For production systems, you should generate a unique, cryptographically random 32-byte key on the server per user session, inject it into the initial HTML page load, and store it in Redis/Memcached. When the user submits the form, you look up the specific key for their session to decrypt the data. +

+
+ + +
+ ); +} \ No newline at end of file diff --git a/docs/src/app/docs/vanilla/page.tsx b/docs/src/app/docs/vanilla/page.tsx new file mode 100644 index 0000000..753268f --- /dev/null +++ b/docs/src/app/docs/vanilla/page.tsx @@ -0,0 +1,91 @@ +import { CodeBlock } from "@/components/CodeBlock"; +import { DocsPagination } from "@/components/DocsPagination"; + +export default function VanillaPage() { + return ( +
+

Vanilla JavaScript Core

+

+ If you are using Vue, Svelte, Angular, or building a traditional server-rendered application without a heavy frontend framework, you can use the barebones @secure-input/core class. +

+ +
+ +

Basic Implementation

+

+ The SecureInput class initializes the Web Worker and compiles the WebAssembly module behind the scenes. It handles all thread communication securely. +

+ + console.error("Encryption failed:", err) + }); + + try { + // 2. Wait for the Web Worker and WASM module to initialize + await secureInput.initialize(); + + const checkoutBtn = document.getElementById("checkout-btn"); + const inputField = document.getElementById("coupon-input"); + + checkoutBtn.addEventListener("click", async (e) => { + e.preventDefault(); + + const plainText = inputField.value; + + // 3. Encrypt the value. This happens off the main thread. + const encryptedPayload = await secureInput.encrypt(plainText); + + // Clear the input immediately + inputField.value = ""; + + // Send ciphertext to server + await fetch("/api/checkout", { + method: "POST", + body: JSON.stringify({ coupon: encryptedPayload }) + }); + }); + + } catch (error) { + // Fallback if WASM is not supported by the browser + console.error("Secure Input unavailable:", error); + } +} + +setupSecureCheckout();`} + lang="javascript" + /> + +

Cleanup & Memory Management

+

+ Because this library spans up a dedicated Web Worker, it consumes memory. If you are building a Single Page Application (SPA), it is critical to destroy the instance when the component unmounts or the user navigates away to prevent memory leaks. +

+ + { + secureInput.destroy(); +});`} + lang="javascript" + /> + +
+

Note on Vite / Webpack

+

+ Modern bundlers like Vite and Webpack 5 handle Web Worker files automatically. The @secure-input/core package utilizes standard new Worker(new URL('...', import.meta.url)) syntax so it should work out-of-the-box with zero custom bundler configuration required. +

+
+ + +
+ ); +} \ No newline at end of file diff --git a/docs/src/app/favicon.ico b/docs/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css new file mode 100644 index 0000000..de2ea6d --- /dev/null +++ b/docs/src/app/globals.css @@ -0,0 +1,203 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@plugin "@tailwindcss/typography"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-body); + --font-mono: var(--font-display); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +:root { + --radius: 0rem; + --background: oklch(0.98 0 0); + --foreground: oklch(0.1 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.1 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.1 0 0); + --primary: oklch(0.2 0 0); + --primary-foreground: oklch(0.98 0 0); + --secondary: oklch(0.95 0 0); + --secondary-foreground: oklch(0.2 0 0); + --muted: oklch(0.95 0 0); + --muted-foreground: oklch(0.5 0 0); + --accent: oklch(0.95 0 0); + --accent-foreground: oklch(0.2 0 0); + --destructive: oklch(0.6 0.2 27); + --border: oklch(0.9 0 0); + --input: oklch(0.9 0 0); + --ring: oklch(0.7 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.05 0 0); + --foreground: oklch(0.98 0 0); + --card: oklch(0.08 0 0); + --card-foreground: oklch(0.98 0 0); + --popover: oklch(0.08 0 0); + --popover-foreground: oklch(0.98 0 0); + --primary: oklch(0.98 0 0); + --primary-foreground: oklch(0.05 0 0); + --secondary: oklch(0.12 0 0); + --secondary-foreground: oklch(0.98 0 0); + --muted: oklch(0.12 0 0); + --muted-foreground: oklch(0.7 0 0); + --accent: oklch(0.85 0.15 150); /* Cyan/Neon accent for that secure/WASM vibe */ + --accent-foreground: oklch(0.05 0 0); + --destructive: oklch(0.4 0.15 22); + --border: oklch(0.15 0 0); + --input: oklch(0.15 0 0); + --ring: oklch(0.85 0.15 150); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.05 0 0); + --sidebar-foreground: oklch(0.98 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.98 0 0); + --sidebar-accent: oklch(0.12 0 0); + --sidebar-accent-foreground: oklch(0.98 0 0); + --sidebar-border: oklch(0.15 0 0); + --sidebar-ring: oklch(0.85 0.15 150); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground selection:bg-accent selection:text-accent-foreground; + } + + .hero-glow-bg { + position: absolute; + inset: 0; + overflow: hidden; + mask-image: radial-gradient(ellipse 100% 100% at 50% 0%, black 50%, transparent 100%); + } + + .hero-glow-bg::before, + .hero-glow-bg::after { + content: ''; + position: absolute; + border-radius: 50%; + filter: blur(120px); + animation: float-glow 20s infinite ease-in-out alternate; + } + + .hero-glow-bg::before { + width: 60vw; + height: 60vw; + background: var(--color-accent); + top: -20vw; + left: -10vw; + animation-delay: -5s; + } + + .hero-glow-bg::after { + width: 50vw; + height: 50vw; + background: oklch(0.98 0 0); + top: 10vw; + right: -10vw; + animation-duration: 25s; + } + + @keyframes float-glow { + 0% { transform: translate(0, 0) scale(1); opacity: 0.04; } + 50% { opacity: 0.08; } + 100% { transform: translate(-5%, 10%) scale(1.1); opacity: 0.04; } + } + + @keyframes shine { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } + } + + .btn-shine { + position: relative; + overflow: hidden; + } + + .btn-shine::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 150%; + height: 100%; + background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + animation: shine 3s infinite; + } + + .tech-border { + @apply border border-white/10 relative; + } + + .tech-border::before { + content: ''; + position: absolute; + top: -1px; + left: -1px; + width: 4px; + height: 4px; + background-color: var(--color-accent); + } +} \ No newline at end of file diff --git a/docs/src/app/layout.tsx b/docs/src/app/layout.tsx new file mode 100644 index 0000000..3df0c32 --- /dev/null +++ b/docs/src/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import { Spline_Sans_Mono, Hanken_Grotesk } from "next/font/google"; +import "./globals.css"; + +const displayFont = Spline_Sans_Mono({ + subsets: ["latin"], + variable: "--font-display", + weight: ["400", "700"], +}); + +const bodyFont = Hanken_Grotesk({ + subsets: ["latin"], + variable: "--font-body", + weight: ["300", "400", "500", "700"], +}); + +export const metadata: Metadata = { + title: "Secure Input Documentation", + description: "WASM-powered input obfuscation library for preventing client-side scraping", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/docs/src/app/page.tsx b/docs/src/app/page.tsx new file mode 100644 index 0000000..070c8eb --- /dev/null +++ b/docs/src/app/page.tsx @@ -0,0 +1,300 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { Shield, Cpu, FileCode2, Lock, GitBranch, ArrowRight, Check, Copy, BookText } from "lucide-react"; +import Link from "next/link"; +import { motion } from "framer-motion"; +import { useState } from "react"; + +const FADE_UP_ANIMATION_VARIANTS = { + hidden: { opacity: 0, y: 20 }, + show: { opacity: 1, y: 0, transition: { type: "spring" as const, damping: 20, stiffness: 100 } }, +}; + +const STAGGER_CONTAINER_VARIANTS = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { staggerChildren: 0.1 }, + }, +}; + +export default function Home() { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText("npm install @secure-input/react"); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Navigation */} + + + {/* Hero Section */} +
+
+
+ + + + WASM-Powered Protection + + + + + Protect your checkout
+ from coupon-stealing extensions. +
+ + + Stop extensions like Honey from scraping and leaking your discount codes. A lightweight WebAssembly library that obfuscates sensitive e-commerce inputs from client-side bots. + + + + + + + + +
~30KB Gzipped
+
Zero Dependencies
+
Type Safe
+
+
+
+
+ + + + {/* Features Grid */} +
+ + +

Core Architecture

+

Designed for performance and isolation. Plain text never exists in the main thread or DOM.

+
+ +
+ + + +

WASM Encryption

+

+ Utilizes ChaCha20Poly1305 authenticated encryption compiled from Rust to WebAssembly for maximum performance and a higher barrier to reverse-engineering. +

+
+
+ + + + +

Worker Isolation

+

+ Sensitive processing occurs entirely in a separate Web Worker thread. Only the encrypted payload is accessible to browser extensions inspecting the DOM. +

+
+
+ + + + +

Framework Agnostic

+

+ Usable anywhere. Core library works in Vanilla JS, with first-class React hooks and components provided out of the box. +

+
+
+
+
+
+ + {/* Packages Section */} +
+ +
+ +

Packages

+

Modular design allows you to import exactly what you need.

+
+ +
+ +
+

+ @secure-input/core + ~15KB +

+

Framework-agnostic core library + Web Worker.

+
+ npm i @secure-input/core +
+ + +
+

+ @secure-input/react + ~5KB +

+

React hooks and wrapper components.

+
+ npm i @secure-input/react +
+ + +
+

+ @secure-input/wasm + ~10KB +

+

Underlying ChaCha20 encryption module.

+
+ Internal Dep +
+
+
+
+
+ + {/* Security Notice / Warning */} +
+
+ +
+
+ +
+
+

Security Notice

+

+ This library provides obfuscation, not absolute security. + It defeats basic extensions, DOM inspection, and simple JS injection. + It does not protect against low-level keyloggers, network traffic inspection, + or determined reverse-engineering. Always pair with robust server-side validation and rate limiting. +

+
+
+
+
+ + {/* Quick Start / Code */} +
+ + Implementation is minimal. + + +
+
+
+
+ CouponForm.tsx +
+
+
+                 
+import {"{ SecureInput }"} from "@secure-input/react";{"\n\n"}
+export function CouponForm() {"{\n"}
+{"  "}const handleSubmit = async (encryptedValue: string) ={">"} {"{\n"}
+{"    "}// Plain text is never exposed here.{"\n"}
+{"    "}await fetch("/api/validate", {"{\n"}
+{"      "}method: "POST",{"\n"}
+{"      "}body: encryptedValue,{"\n"}
+{"    });\n"}
+{"  };\n\n"}
+{"  "}return ({"\n"}
+{"    <"}SecureInput{"\n"}
+{"      "}placeholder="Enter coupon code"{"\n"}
+{"      "}onEncryptedSubmit={"{handleSubmit}"}{"\n"}
+{"    />\n"}
+{"  );\n"}
+{"}"}
+                 
+               
+
+
+
+
+ + {/* Footer */} +
+
+
+ + Secure Input +
+

+ MIT License © {new Date().getFullYear()} +

+
+
+
+ ); +} diff --git a/docs/src/components/CodeBlock.tsx b/docs/src/components/CodeBlock.tsx new file mode 100644 index 0000000..8097e46 --- /dev/null +++ b/docs/src/components/CodeBlock.tsx @@ -0,0 +1,43 @@ +import { codeToHtml } from "shiki"; +import { Copy } from "lucide-react"; +import { CodeCopyButton } from "./CodeCopyButton"; + +interface CodeBlockProps { + code: string; + lang?: string; + filename?: string; +} + +export async function CodeBlock({ code, lang = "typescript", filename }: CodeBlockProps) { + const html = await codeToHtml(code, { + lang, + theme: "vitesse-dark", + }); + + return ( +
+ {filename && ( +
+
+
+
+
+ {filename} +
+ +
+ )} +
+ {!filename && ( +
+ +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/docs/src/components/CodeCopyButton.tsx b/docs/src/components/CodeCopyButton.tsx new file mode 100644 index 0000000..5523a5b --- /dev/null +++ b/docs/src/components/CodeCopyButton.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; + +export function CodeCopyButton({ code }: { code: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/docs/src/components/DocsPagination.tsx b/docs/src/components/DocsPagination.tsx new file mode 100644 index 0000000..51c485f --- /dev/null +++ b/docs/src/components/DocsPagination.tsx @@ -0,0 +1,48 @@ +import Link from "next/link"; +import { ArrowLeft, ArrowRight } from "lucide-react"; + +interface PaginationLink { + href: string; + title: string; +} + +interface DocsPaginationProps { + prev?: PaginationLink; + next?: PaginationLink; +} + +export function DocsPagination({ prev, next }: DocsPaginationProps) { + return ( +
+ {prev ? ( + + + + Previous + + {prev.title} + + ) : ( +
+ )} + + {next ? ( + + + Next + + + {next.title} + + ) : ( +
+ )} +
+ ); +} \ No newline at end of file diff --git a/docs/src/components/DocsSidebar.tsx b/docs/src/components/DocsSidebar.tsx new file mode 100644 index 0000000..0bf4598 --- /dev/null +++ b/docs/src/components/DocsSidebar.tsx @@ -0,0 +1,47 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; + +const links = [ + { href: "/docs", label: "Introduction" }, + { href: "/docs/how-it-works", label: "How It Works" }, + { href: "/docs/installation", label: "Installation" }, + { href: "/docs/react", label: "React Implementation" }, + { href: "/docs/vanilla", label: "Vanilla JS Implementation" }, + { href: "/docs/server", label: "Server-Side Decryption" }, +]; + +export function DocsSidebar() { + const pathname = usePathname(); + + return ( +
+
+

+ Getting Started +

+
+ {links.map((link) => { + const isActive = pathname === link.href; + return ( + + {link.label} + + ); + })} +
+
+
+ ); +} \ No newline at end of file diff --git a/docs/src/components/PackageManagerCode.tsx b/docs/src/components/PackageManagerCode.tsx new file mode 100644 index 0000000..9b3ffe2 --- /dev/null +++ b/docs/src/components/PackageManagerCode.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; + +interface PackageManagerCodeProps { + packageName: string; +} + +export function PackageManagerCode({ packageName }: PackageManagerCodeProps) { + const [manager, setManager] = useState<"npm" | "pnpm" | "yarn" | "bun">("npm"); + const [copied, setCopied] = useState(false); + + const commands = { + npm: `npm install ${packageName}`, + pnpm: `pnpm add ${packageName}`, + yarn: `yarn add ${packageName}`, + bun: `bun add ${packageName}`, + }; + + const currentCommand = commands[manager]; + + const handleCopy = async () => { + await navigator.clipboard.writeText(currentCommand); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+ {(["npm", "pnpm", "yarn", "bun"] as const).map((m) => ( + + ))} +
+ +
+
+
+          
+            {manager} {manager === "npm" ? "install" : "add"} {packageName}
+          
+        
+
+
+ ); +} \ No newline at end of file diff --git a/docs/src/components/ui/accordion.tsx b/docs/src/components/ui/accordion.tsx new file mode 100644 index 0000000..aa972da --- /dev/null +++ b/docs/src/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import { ChevronDownIcon } from "lucide-react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/docs/src/components/ui/badge.tsx b/docs/src/components/ui/badge.tsx new file mode 100644 index 0000000..beb56ed --- /dev/null +++ b/docs/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/docs/src/components/ui/button.tsx b/docs/src/components/ui/button.tsx new file mode 100644 index 0000000..de91c8f --- /dev/null +++ b/docs/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-white/10 hover:text-accent transition-colors", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/docs/src/components/ui/card.tsx b/docs/src/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/docs/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/docs/src/components/ui/scroll-area.tsx b/docs/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0f873dc --- /dev/null +++ b/docs/src/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/docs/src/components/ui/separator.tsx b/docs/src/components/ui/separator.tsx new file mode 100644 index 0000000..4c24b2a --- /dev/null +++ b/docs/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import { Separator as SeparatorPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/docs/src/components/ui/sheet.tsx b/docs/src/components/ui/sheet.tsx new file mode 100644 index 0000000..5963090 --- /dev/null +++ b/docs/src/components/ui/sheet.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as SheetPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/docs/src/components/ui/tabs.tsx b/docs/src/components/ui/tabs.tsx new file mode 100644 index 0000000..7f73dcd --- /dev/null +++ b/docs/src/components/ui/tabs.tsx @@ -0,0 +1,91 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/docs/src/lib/utils.ts b/docs/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/docs/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/docs/tailwind.config.ts b/docs/tailwind.config.ts new file mode 100644 index 0000000..80e6f88 --- /dev/null +++ b/docs/tailwind.config.ts @@ -0,0 +1,84 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class", "dark"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + display: ["var(--font-display)"], + body: ["var(--font-body)"], + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; + +export default config; diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +}