diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..97a1127
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+[*.{ts,tsx,js,jsx,json,yml,yaml}]
+quote_type = single
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..fa12560
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+APP_NAME=hyperwave
+EMAIL_FROM="noreply@example.com"
+HOST=http://localhost:3000
+NODE_ENV=development
+PORT=3000
+RESEND_API_KEY=go_get_an_api_key_from_resend.com
+SECRET_KEY=change_this_to_a_secure_random_key_with_32_chars
+SKIP_AUTH=false
diff --git a/.gitignore b/.gitignore
index 5c005f9..30f9b0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,10 @@ package-lock.json
server
+# SQLite databases
db.sqlite
+app.db
+*.db
+*.db-shm
+*.db-wal
+.env
diff --git a/DEPLOY.md b/DEPLOY.md
new file mode 100644
index 0000000..1d49a65
--- /dev/null
+++ b/DEPLOY.md
@@ -0,0 +1,47 @@
+# Fly.io SQLite Deployment
+
+## 1. Create persistent volume for SQLite database
+
+```bash
+fly volumes create data --region dfw --size 1
+```
+
+This creates a 1GB persistent volume named "data" in the Dallas region.
+
+## 2. Set environment variables
+
+```bash
+fly secrets set RESEND_API_KEY="your_resend_api_key_here"
+fly secrets set HOST="https://hyperwave.fly.dev"
+fly secrets set EMAIL_FROM="noreply@hyperwave.fly.dev"
+```
+
+## 3. Deploy the application
+
+```bash
+fly deploy
+```
+
+## 4. Verify deployment
+
+```bash
+fly status
+fly logs
+```
+
+## Database persistence
+
+The SQLite database will be stored at `/data/app.db` inside the container, which is mounted to the persistent volume. This ensures your data survives deployments and machine restarts.
+
+## Volume management
+
+```bash
+# List volumes
+fly volumes list
+
+# Show volume details
+fly volumes show data
+
+# Backup database (optional)
+fly ssh console -C "cp /data/app.db /tmp/backup.db"
+```
diff --git a/Dockerfile b/Dockerfile
index 64592b2..a7a64c4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,38 +1,21 @@
-# use the official Bun image
-# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
-# install dependencies into temp directory
-# this will cache them and speed up future builds
FROM base AS install
-RUN mkdir -p /temp/dev
-COPY package.json bun.lockb /temp/dev/
-RUN cd /temp/dev && bun install --frozen-lockfile
+COPY package.json bun.lock ./
+RUN bun install --frozen-lockfile
-# install
-RUN mkdir -p /temp/prod
-COPY package.json bun.lockb /temp/prod/
-RUN cd /temp/prod && bun install --frozen-lockfile
-
-# copy node_modules from temp directory
-# then copy all (non-ignored) project files into the image
-FROM base AS prerelease
-COPY --from=install /temp/dev/node_modules node_modules
-COPY . .
+FROM base AS release
+COPY --from=install /usr/src/app/node_modules node_modules
+COPY . ./
+RUN cp .env.example .env
-# Env vars
ENV NODE_ENV=production
+ENV PORT=3000
+ENV DATABASE_PATH=/data/app.db
-# copy production dependencies and source code into final image
-FROM base AS release
-COPY --from=prerelease /usr/src/app .
-COPY --from=install /temp/dev/node_modules node_modules
-COPY --from=prerelease /usr/src/app/public public
+RUN mkdir -p /data
-# run the app
USER bun
EXPOSE 3000/tcp
-WORKDIR /usr/src/app
-ENV PORT=3000
ENTRYPOINT ["bun", "run", "src/server.tsx"]
diff --git a/README.md b/README.md
index 3dec4fc..ed027a3 100644
--- a/README.md
+++ b/README.md
@@ -1,115 +1,388 @@
# hyperwave
-hyperwave combines the benefits of traditional server-rendered applications with the flexibility of modern client-side frameworks.
+A modern, full-stack web application framework combining the benefits of traditional server-rendered applications with the flexibility of modern development practices.
-- **Performance:** Server-side rendering ensures fast, responsive applications, tailored to produce the smallest possible bundles.
-- **Developer experience:** HTMX and Tailwind provide a minimalistic and declarative approach to building user interfaces
-- **Deployment:** bun applications can be easily deployed on any platform as portable binaries
+**๐ [Live Demo](https://hyperwave-demo.fly.dev)** - Try it now to see hyperwave in action!
+
+## ๐ What is hyperwave?
+
+hyperwave represents a return to **simplicity without sacrificing power**. In an era dominated by complex frontend frameworks and overwhelming toolchains, hyperwave offers a different approach:
+
+### **๐ฏ Core Philosophy**
+
+- **Server-First**: HTML is generated on the server and enhanced progressively
+- **Minimal JavaScript**: HTMX provides rich interactions without React/Vue complexity
+- **Type-Safe Everything**: Full TypeScript coverage from database to UI
+- **Single Binary**: Deploy anywhere with zero dependencies
+- **Developer Joy**: Hot reload, excellent DX, minimal configuration
+
+### **๐ Perfect For**
+
+- **Rapid Prototyping**: Go from idea to working app in minutes
+- **Production Apps**: Scales efficiently with built-in authentication and database
+- **Teams**: Clean architecture that's easy to understand and maintain
+- **Indie Hackers**: Everything you need in one lightweight package
+
+### **๐ก Why Not a SPA?**
+
+While single-page applications have their place, they often introduce unnecessary complexity for most web applications. hyperwave delivers:
+
+- **Faster Initial Loads**: Server-rendered HTML appears instantly
+- **Better SEO**: Search engines index your content without gymnastics
+- **Simpler Mental Model**: Request โ Response โ HTML (just like the web was designed)
+- **Progressive Enhancement**: Add interactivity where it matters, not everywhere
## Getting started
Follow these steps to start developing with hyperwave:
-1. Clone the repository:
+1. **Clone the repository:**
```sh
git clone https://github.com/tireymorris/hyperwave.git
cd hyperwave
```
-2. Install dependencies:
+2. **Install dependencies:**
```sh
bun install
```
-3. Start the development server:
+3. **Start the development server:**
```sh
bun dev
```
-4. Visit `http://localhost:1234` in your browser.
+4. **Visit the application:**
+ - Open `http://localhost:3000` in your browser
+ - Try the authentication flow with any email address
+ - Check console logs for magic link URLs in development mode
-5. Start editing `server.tsx` to see your changes live.
+## ๐๏ธ Architecture Overview
-### Example
+### **Authentication Flow**
-This is the endpoint serving our initial landing page:
+```typescript
+// Magic link authentication with JWT tokens
+app.post('/auth/login', async (c) => {
+ const { email } = await c.req.parseBody();
+
+ // Create or find user in SQLite database
+ const user = await authProvider.createUser(email);
+
+ // Generate magic link token
+ const token = await generateToken({ type: 'magic', email, role: 'user' });
+
+ // Send beautiful email with magic link
+ await sendMagicLink(email);
+
+ return c.html( );
+});
+```
+
+### **Database Integration**
```typescript
-app.get("/", ({ html }) =>
- html(
-
-
-
-
- fetch instructions from /instructions
-
-
-
- ,
- ),
+// High-performance SQLite with Bun
+import { Database } from "bun:sqlite";
+
+const db = new Database("app.db");
+db.exec("PRAGMA journal_mode = WAL;");
+db.exec("PRAGMA foreign_keys = ON;");
+
+// Auto-cleanup expired tokens
+setInterval(
+ () => {
+ db.query("DELETE FROM tokens WHERE expires_at < ?").run(Date.now());
+ },
+ 15 * 60 * 1000,
);
```
-- The API serves a full HTML document to the client, which includes Tailwind classes and HTMX attributes
-- The response is wrapped in a ` ` tag, a server-rendered functional component, which takes a `title` prop
-- The button, when clicked, will issue a `GET` request to `/instructions` and replace the content of its parent div with the response.
-- Includes a tiny hyperscript to toggle a class when the button is clicked
+### **Theming with UnoCSS**
----
+hyperwave uses a sophisticated theming system built on UnoCSS with custom design tokens:
+
+```typescript
+// Custom theme configuration in src/styles/uno.config.ts
+export default defineConfig({
+ theme: {
+ colors: {
+ // App backgrounds - Terminal-inspired dark theme
+ "app-background": "#0c0a14", // Primary dark background
+ "app-background-alt": "#161421", // Slightly lighter surfaces
+ "app-surface": "#2a2735", // Interactive surfaces
+
+ // Text hierarchy
+ "text-primary": "#fafafa", // Main text
+ "text-secondary": "#e5e5e5", // Secondary text
+ "text-tertiary": "#a3a3a3", // Muted text
+
+ // Interactive elements - Purple accent system
+ "interactive-primary": "#8b5cf6", // Primary buttons/links
+ "border-primary": "#8b5cf6", // Focus states
+
+ // Status colors
+ "status-success": "#34d399", // Success states
+ "status-error": "#f87171", // Error states
+ "status-warning": "#fbbf24", // Warning states
+ },
+ },
+});
+```
+
+**Using the theme:**
+
+```typescript
+// Components automatically use theme variables
+
+ Sign In
+
+
+// CSS variables are available in custom styles
+.custom-element {
+ background: var(--un-bg-app-background);
+ border: 1px solid var(--un-border-primary);
+}
+```
-### Deployment
+**HTMX Integration:**
-Build an executable for your current architecture with `bun run build`
+```typescript
+// Special variants for HTMX states
+
+ Content that dims during requests and scales during settling
+
+```
-`PORT` environment variable is available if needed (default is 1234)
+### **Middleware Pipeline**
-Note: deploy `public/` with the executable, it contains the generated UnoCSS build.
+```typescript
+// Comprehensive middleware stack
+app.use("*", errorHandler); // Global error handling
+app.use("*", logger); // Request/response logging
+app.use("/dashboard/*", requireAuth); // Protected routes
+app.use("/api/*", requireAuth); // API authentication
+```
----
+## ๐งช Testing
+
+Run the comprehensive test suite:
+
+```sh
+# Run all tests
+bun test
+
+# Type checking
+bun run typecheck
+
+# Linting
+bun run lint
+
+# Pre-commit checks
+bun run precommit
+```
+
+## ๐ Deployment
+
+Build a production executable:
+
+```sh
+bun run build
+```
+
+The build process:
+
+1. **CSS Generation**: UnoCSS processes and optimizes styles
+2. **Binary Compilation**: Creates a single executable file
+3. **Asset Bundling**: Includes all necessary static files
+
+Deploy the `server` binary and `public/` directory to any platform.
+
+## ๐ง Environment Configuration
+
+Create a `.env` file for local development:
+
+```env
+# App Configuration
+APP_NAME="hyperwave"
+HOST="http://localhost:3000"
+PORT=3000
+
+# Email Configuration (Production)
+RESEND_API_KEY="re_..."
+EMAIL_FROM="noreply@yourdomain.com"
+```
+
+## ๐๏ธ Project Structure
+
+```
+src/
+โโโ lib/
+โ โโโ auth/ # Authentication system
+โ โ โโโ tokens.ts # JWT token management
+โ โ โโโ magic.ts # Magic link functionality
+โ โ โโโ session.ts # Session management
+โ โโโ providers/ # Service providers
+โ โ โโโ auth.ts # SQLite auth provider
+โ โ โโโ email.ts # Email service provider
+โ โโโ database.ts # SQLite database management
+โโโ routers/
+โ โโโ auth.tsx # Authentication routes
+โ โโโ dashboard.tsx # Main application
+โ โโโ profile.tsx # User profile management
+โ โโโ settings.tsx # Application settings
+โโโ middleware/
+โ โโโ requireAuth.ts # Authentication middleware
+โ โโโ logger.ts # Request logging
+โ โโโ error-handler.ts# Global error handling
+โโโ components/ # Reusable UI components
+โโโ styles/
+โ โโโ uno.config.ts # UnoCSS theme configuration
+โโโ utils/ # Utility functions
+โโโ __tests__/ # Test suites
+
+public/styles/
+โโโ app.css # Global base styles
+โโโ modal.css # Modal-specific styles
+โโโ uno.css # Generated UnoCSS utilities
+```
+
+## ๐ ๏ธ Technology Stack
+
+### **Core Framework**
+
+- **[Bun](https://bun.sh/)** - Runtime, bundler, test runner, and package manager
+- **[Hono](https://hono.dev)** - Fast, lightweight web framework
+- **[SQLite](https://bun.sh/docs/api/sqlite)** - Built-in database with excellent performance
+
+### **Authentication & Security**
+
+- **[jose](https://github.com/panva/jose)** - JWT token generation and verification
+- **[zod](https://zod.dev/)** - Runtime type validation and schema parsing
+- **Magic Links** - Passwordless authentication system
+
+### **Frontend & Styling**
-### Components
+- **[HTMX](https://htmx.org/)** - Dynamic interactions without JavaScript complexity
+- **[UnoCSS](https://unocss.dev/)** - Atomic CSS engine with Tailwind compatibility
+ - **Custom Theme**: Terminal-inspired dark theme with purple accents
+ - **Design Tokens**: Comprehensive color system and spacing
+ - **HTMX Variants**: Special CSS states for loading/settling animations
+ - **Web Fonts**: Google Fonts integration (Inter + Fira Code/JetBrains Mono)
+- **[Hyperscript](https://hyperscript.org/)** - Lightweight scripting for enhanced UX
-- [bun](https://bun.sh/) provides the bundler, runtime, test runner, and package manager.
-- [SQLite](https://bun.sh/docs/api/sqlite) is production-ready and built into Bun.
-- [hono](https://hono.dev) is a robust web framework with great DX and performance
-- [unoCSS](https://unocss.dev/integrations/cli) is Tailwind-compatible and generates only the styles used in application code.
-- [htmx](https://htmx.org/reference/) gives 99% of the client-side interactivity most apps need.
-- [hyperscript](http://hyperscript.org) is a scripting library for rapid application development.
-- [zod](https://zod.dev/) is a powerful runtime validation library.
+### **Development & Quality**
+
+- **TypeScript** - Full type safety with strict configuration
+- **ESLint** - Code quality and consistency
+- **Prettier** - Code formatting
+- **Bun Test** - Fast, built-in testing framework
+
+## ๐ฏ Key Benefits
+
+### **Performance**
+
+- **Server-Side Rendering**: Fast initial page loads
+- **Minimal JavaScript**: HTMX eliminates most client-side complexity
+- **SQLite**: Single-file database with excellent performance
+- **Optimized Builds**: UnoCSS generates only used styles
+
+### **Developer Experience**
+
+- **Hot Reload**: Instant feedback during development
+- **Type Safety**: Comprehensive TypeScript coverage
+- **Testing**: Built-in test runner with good coverage
+- **Single Binary**: Deploy anywhere with zero dependencies
+
+### **Security**
+
+- **Magic Links**: Eliminate password-related vulnerabilities
+- **JWT Tokens**: Stateless authentication with proper expiration
+- **CSRF Protection**: Built-in request validation
+- **SQL Injection**: Parameterized queries prevent attacks
+
+### **Scalability**
+
+- **Modular Architecture**: Easy to extend and maintain
+- **Provider Pattern**: Swappable authentication and email providers
+- **Middleware System**: Clean separation of concerns
+- **Database**: SQLite scales to millions of rows efficiently
---
-### Benefits and takeways
+## ๐ Example Usage
-**Why bother switching to hyperwave?**
+### **Protected Route**
-- Drastically reduces time from idea to rendered UI
-- Very little cognitive friction to creating something new, after initial learning curve
+```typescript
+// Automatic authentication check
+app.get('/dashboard', requireAuth, async (c) => {
+ const user = c.get('user'); // Injected by middleware
+
+ return c.html(
+
+
+
+
+ );
+});
+```
-**Speed / performance benefit**
+### **Themed Component**
-- hyperwave is designed to generate the smallest possible payloads
-- Deployment is as simple as compiling and running a binary ๐
+```typescript
+// Using the custom theme system
+function StatusCard({ type, message }: { type: 'success' | 'error' | 'warning', message: string }) {
+ const styles = {
+ success: 'bg-status-success text-text-inverse',
+ error: 'bg-status-error text-text-inverse',
+ warning: 'bg-status-warning text-text-inverse'
+ };
+
+ return (
+
+ );
+}
+```
-**Simplicity**
+### **Database Operations**
-- Bun saves us a ton of time and effort fighting tooling issues
-- SPAs are over-prescribed and inherently introduce serious costs
+```typescript
+// Type-safe database operations
+const stats = getDatabaseStats();
+console.log(`Users: ${stats.userCount}, Tokens: ${stats.tokenCount}`);
-**Dev UX benefit**
+// Clean up expired tokens
+const deletedCount = cleanupExpiredTokens();
+console.log(`Cleaned up ${deletedCount} expired tokens`);
+```
-- Better primitives for quickly building UX
-- Uniform interface simplifies writing and reading code
+### **Interactive Component with HTMX**
-**Architectural benefit**
+```typescript
+// Server-rendered components with HTMX enhancements
+function UserProfile({ user }: { user: User }) {
+ return (
+
+
{user.email}
+
Member since {user.createdAt.toLocaleDateString()}
+
+ Delete Account
+
+
+ );
+}
+```
-- Can scale backend and product independently, loosely coupled
+---
+This modern hyperwave setup provides everything needed for building secure, performant web applications with minimal complexity and maximum developer productivity.
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..878e794
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,795 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "app-shell",
+ "dependencies": {
+ "@hono/node-server": "^1.14.0",
+ "@hono/zod-validator": "^0.4.3",
+ "@linear/sdk": "^38.0.0",
+ "@types/uuid": "^10.0.0",
+ "@unocss/preset-web-fonts": "^65.5.0",
+ "canvas": "^3.1.0",
+ "hono": "^4.7.5",
+ "jose": "^5.10.0",
+ "resend": "^4.2.0",
+ "unocss": "^65.5.0",
+ "uuid": "^11.1.0",
+ "zod": "^3.24.2",
+ },
+ "devDependencies": {
+ "@octokit/rest": "^21.1.1",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@types/eslint": "^9.6.1",
+ "@types/node": "^22.14.0",
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
+ "@typescript-eslint/parser": "^7.18.0",
+ "@unocss/cli": "^65.5.0",
+ "bun-types": "^1.2.8",
+ "chalk": "^5.4.1",
+ "cross-env": "^7.0.3",
+ "dotenv": "^16.4.7",
+ "eslint": "^8.57.1",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-jsdoc": "^50.6.9",
+ "husky": "^9.1.7",
+ "nodemon": "^3.1.9",
+ "prettier": "3.5.1",
+ "prettier-plugin-tailwindcss": "^0.6.11",
+ "terser": "^5.39.0",
+ "typescript": "^5.8.3",
+ },
+ },
+ },
+ "packages": {
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.2", "", {}, ""],
+
+ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, ""],
+
+ "@antfu/install-pkg": ["@antfu/install-pkg@1.0.0", "", { "dependencies": { "package-manager-detector": "^0.2.8", "tinyexec": "^0.3.2" } }, ""],
+
+ "@antfu/utils": ["@antfu/utils@8.1.1", "", {}, ""],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, ""],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, ""],
+
+ "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" } }, ""],
+
+ "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, ""],
+
+ "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.49.0", "", { "dependencies": { "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.1.0" } }, ""],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, ""],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.5.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, ""],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, ""],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, ""],
+
+ "@eslint/js": ["@eslint/js@8.57.1", "", {}, ""],
+
+ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, ""],
+
+ "@hono/node-server": ["@hono/node-server@1.14.0", "", { "peerDependencies": { "hono": "^4" } }, ""],
+
+ "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, ""],
+
+ "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, ""],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, ""],
+
+ "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, ""],
+
+ "@iconify/types": ["@iconify/types@2.0.0", "", {}, ""],
+
+ "@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, ""],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, ""],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, ""],
+
+ "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, ""],
+
+ "@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, ""],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, ""],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, ""],
+
+ "@linear/sdk": ["@linear/sdk@38.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.0", "graphql": "^15.4.0", "isomorphic-unfetch": "^3.1.0" } }, ""],
+
+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, ""],
+
+ "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, ""],
+
+ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, ""],
+
+ "@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, ""],
+
+ "@octokit/core": ["@octokit/core@6.1.4", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.1.2", "@octokit/request": "^9.2.1", "@octokit/request-error": "^6.1.7", "@octokit/types": "^13.6.2", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, ""],
+
+ "@octokit/endpoint": ["@octokit/endpoint@10.1.3", "", { "dependencies": { "@octokit/types": "^13.6.2", "universal-user-agent": "^7.0.2" } }, ""],
+
+ "@octokit/graphql": ["@octokit/graphql@8.2.1", "", { "dependencies": { "@octokit/request": "^9.2.2", "@octokit/types": "^13.8.0", "universal-user-agent": "^7.0.0" } }, ""],
+
+ "@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, ""],
+
+ "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@11.6.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, ""],
+
+ "@octokit/plugin-request-log": ["@octokit/plugin-request-log@5.3.1", "", { "peerDependencies": { "@octokit/core": ">=6" } }, ""],
+
+ "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@13.5.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, ""],
+
+ "@octokit/request": ["@octokit/request@9.2.2", "", { "dependencies": { "@octokit/endpoint": "^10.1.3", "@octokit/request-error": "^6.1.7", "@octokit/types": "^13.6.2", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, ""],
+
+ "@octokit/request-error": ["@octokit/request-error@6.1.7", "", { "dependencies": { "@octokit/types": "^13.6.2" } }, ""],
+
+ "@octokit/rest": ["@octokit/rest@21.1.1", "", { "dependencies": { "@octokit/core": "^6.1.4", "@octokit/plugin-paginate-rest": "^11.4.2", "@octokit/plugin-request-log": "^5.3.1", "@octokit/plugin-rest-endpoint-methods": "^13.3.0" } }, ""],
+
+ "@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, ""],
+
+ "@pkgr/core": ["@pkgr/core@0.1.2", "", {}, ""],
+
+ "@polka/url": ["@polka/url@1.0.0-next.28", "", {}, ""],
+
+ "@react-email/render": ["@react-email/render@1.0.5", "", { "dependencies": { "html-to-text": "9.0.5", "prettier": "3.4.2", "react-promise-suspense": "0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, ""],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.39.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
+
+ "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, ""],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.3", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "redent": "^3.0.0" } }, ""],
+
+ "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, ""],
+
+ "@types/estree": ["@types/estree@1.0.7", "", {}, ""],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, ""],
+
+ "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, ""],
+
+ "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
+
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, ""],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.18.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, ""],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@7.18.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, ""],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, ""],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.18.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, ""],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@7.18.0", "", {}, ""],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" } }, ""],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@7.18.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, ""],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" } }, ""],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, ""],
+
+ "@unocss/astro": ["@unocss/astro@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/reset": "65.5.0", "@unocss/vite": "65.5.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" } }, ""],
+
+ "@unocss/cli": ["@unocss/cli@65.5.0", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@unocss/config": "65.5.0", "@unocss/core": "65.5.0", "@unocss/preset-uno": "65.5.0", "cac": "^6.7.14", "chokidar": "^3.6.0", "colorette": "^2.0.20", "consola": "^3.4.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "tinyglobby": "^0.2.10", "unplugin-utils": "^0.2.4" }, "bin": { "unocss": "bin/unocss.mjs" } }, ""],
+
+ "@unocss/config": ["@unocss/config@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "unconfig": "~7.0.0" } }, ""],
+
+ "@unocss/core": ["@unocss/core@65.5.0", "", {}, ""],
+
+ "@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0" } }, ""],
+
+ "@unocss/inspector": ["@unocss/inspector@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/rule-utils": "65.5.0", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.0", "vue-flow-layout": "^0.1.1" } }, ""],
+
+ "@unocss/postcss": ["@unocss/postcss@65.5.0", "", { "dependencies": { "@unocss/config": "65.5.0", "@unocss/core": "65.5.0", "@unocss/rule-utils": "65.5.0", "css-tree": "^3.1.0", "postcss": "^8.5.2", "tinyglobby": "^0.2.10" }, "peerDependencies": { "postcss": "^8.4.21" } }, ""],
+
+ "@unocss/preset-attributify": ["@unocss/preset-attributify@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0" } }, ""],
+
+ "@unocss/preset-icons": ["@unocss/preset-icons@65.5.0", "", { "dependencies": { "@iconify/utils": "^2.3.0", "@unocss/core": "65.5.0", "ofetch": "^1.4.1" } }, ""],
+
+ "@unocss/preset-mini": ["@unocss/preset-mini@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/extractor-arbitrary-variants": "65.5.0", "@unocss/rule-utils": "65.5.0" } }, ""],
+
+ "@unocss/preset-tagify": ["@unocss/preset-tagify@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0" } }, ""],
+
+ "@unocss/preset-typography": ["@unocss/preset-typography@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/preset-mini": "65.5.0", "@unocss/rule-utils": "65.5.0" } }, ""],
+
+ "@unocss/preset-uno": ["@unocss/preset-uno@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/preset-mini": "65.5.0", "@unocss/preset-wind": "65.5.0", "@unocss/rule-utils": "65.5.0" } }, ""],
+
+ "@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "ofetch": "^1.4.1" } }, ""],
+
+ "@unocss/preset-wind": ["@unocss/preset-wind@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/preset-mini": "65.5.0", "@unocss/rule-utils": "65.5.0" } }, ""],
+
+ "@unocss/reset": ["@unocss/reset@65.5.0", "", {}, ""],
+
+ "@unocss/rule-utils": ["@unocss/rule-utils@65.5.0", "", { "dependencies": { "@unocss/core": "^65.5.0", "magic-string": "^0.30.17" } }, ""],
+
+ "@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0" } }, ""],
+
+ "@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0" } }, ""],
+
+ "@unocss/transformer-directives": ["@unocss/transformer-directives@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0", "@unocss/rule-utils": "65.5.0", "css-tree": "^3.1.0" } }, ""],
+
+ "@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@65.5.0", "", { "dependencies": { "@unocss/core": "65.5.0" } }, ""],
+
+ "@unocss/vite": ["@unocss/vite@65.5.0", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@unocss/config": "65.5.0", "@unocss/core": "65.5.0", "@unocss/inspector": "65.5.0", "chokidar": "^3.6.0", "magic-string": "^0.30.17", "tinyglobby": "^0.2.10", "unplugin-utils": "^0.2.4" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" } }, ""],
+
+ "@vue/compiler-core": ["@vue/compiler-core@3.5.13", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, ""],
+
+ "@vue/compiler-dom": ["@vue/compiler-dom@3.5.13", "", { "dependencies": { "@vue/compiler-core": "3.5.13", "@vue/shared": "3.5.13" } }, ""],
+
+ "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.13", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/compiler-core": "3.5.13", "@vue/compiler-dom": "3.5.13", "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, ""],
+
+ "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13" } }, ""],
+
+ "@vue/reactivity": ["@vue/reactivity@3.5.13", "", { "dependencies": { "@vue/shared": "3.5.13" } }, ""],
+
+ "@vue/runtime-core": ["@vue/runtime-core@3.5.13", "", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/shared": "3.5.13" } }, ""],
+
+ "@vue/runtime-dom": ["@vue/runtime-dom@3.5.13", "", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/runtime-core": "3.5.13", "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, ""],
+
+ "@vue/server-renderer": ["@vue/server-renderer@3.5.13", "", { "dependencies": { "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "vue": "3.5.13" } }, ""],
+
+ "@vue/shared": ["@vue/shared@3.5.13", "", {}, ""],
+
+ "acorn": ["acorn@8.14.1", "", { "bin": "bin/acorn" }, ""],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""],
+
+ "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, ""],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
+
+ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, ""],
+
+ "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, ""],
+
+ "argparse": ["argparse@2.0.1", "", {}, ""],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, ""],
+
+ "array-union": ["array-union@2.1.0", "", {}, ""],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, ""],
+
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "before-after-hook": ["before-after-hook@3.0.2", "", {}, ""],
+
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, ""],
+
+ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
+
+ "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""],
+
+ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, ""],
+
+ "bun-types": ["bun-types@1.2.8", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, ""],
+
+ "cac": ["cac@6.7.14", "", {}, ""],
+
+ "callsites": ["callsites@3.1.0", "", {}, ""],
+
+ "canvas": ["canvas@3.1.0", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1" } }, "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg=="],
+
+ "chalk": ["chalk@5.4.1", "", {}, ""],
+
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, ""],
+
+ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""],
+
+ "color-name": ["color-name@1.1.4", "", {}, ""],
+
+ "colorette": ["colorette@2.0.20", "", {}, ""],
+
+ "commander": ["commander@2.20.3", "", {}, ""],
+
+ "comment-parser": ["comment-parser@1.4.1", "", {}, ""],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, ""],
+
+ "confbox": ["confbox@0.2.2", "", {}, ""],
+
+ "consola": ["consola@3.4.2", "", {}, ""],
+
+ "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, ""],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""],
+
+ "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, ""],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, ""],
+
+ "csstype": ["csstype@3.1.3", "", {}, ""],
+
+ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, ""],
+
+ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
+
+ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, ""],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, ""],
+
+ "defu": ["defu@6.1.4", "", {}, ""],
+
+ "destr": ["destr@2.0.5", "", {}, ""],
+
+ "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
+
+ "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, ""],
+
+ "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, ""],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, ""],
+
+ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, ""],
+
+ "domelementtype": ["domelementtype@2.3.0", "", {}, ""],
+
+ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, ""],
+
+ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, ""],
+
+ "dotenv": ["dotenv@16.4.7", "", {}, ""],
+
+ "duplexer": ["duplexer@0.1.2", "", {}, ""],
+
+ "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
+
+ "entities": ["entities@4.5.0", "", {}, ""],
+
+ "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, ""],
+
+ "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/darwin-arm64": "0.25.2" }, "bin": "bin/esbuild" }, ""],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, ""],
+
+ "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": "bin/eslint.js" }, ""],
+
+ "eslint-plugin-eslint-comments": ["eslint-plugin-eslint-comments@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5", "ignore": "^5.0.5" }, "peerDependencies": { "eslint": ">=4.19.1" } }, ""],
+
+ "eslint-plugin-jsdoc": ["eslint-plugin-jsdoc@50.6.9", "", { "dependencies": { "@es-joy/jsdoccomment": "~0.49.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.3.6", "escape-string-regexp": "^4.0.0", "espree": "^10.1.0", "esquery": "^1.6.0", "parse-imports": "^2.1.1", "semver": "^7.6.3", "spdx-expression-parse": "^4.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, ""],
+
+ "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, ""],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, ""],
+
+ "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, ""],
+
+ "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, ""],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, ""],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, ""],
+
+ "estree-walker": ["estree-walker@2.0.2", "", {}, ""],
+
+ "esutils": ["esutils@2.0.3", "", {}, ""],
+
+ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
+
+ "exsolve": ["exsolve@1.0.4", "", {}, ""],
+
+ "fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, ""],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""],
+
+ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, ""],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, ""],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, ""],
+
+ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, ""],
+
+ "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""],
+
+ "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, ""],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""],
+
+ "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, ""],
+
+ "flatted": ["flatted@3.3.3", "", {}, ""],
+
+ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
+
+ "fs.realpath": ["fs.realpath@1.0.0", "", {}, ""],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, ""],
+
+ "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
+
+ "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, ""],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""],
+
+ "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, ""],
+
+ "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, ""],
+
+ "graphemer": ["graphemer@1.4.0", "", {}, ""],
+
+ "graphql": ["graphql@15.10.1", "", {}, ""],
+
+ "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, ""],
+
+ "has-flag": ["has-flag@3.0.0", "", {}, ""],
+
+ "hono": ["hono@4.7.5", "", {}, ""],
+
+ "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, ""],
+
+ "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, ""],
+
+ "husky": ["husky@9.1.7", "", { "bin": "bin.js" }, ""],
+
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, ""],
+
+ "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, ""],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, ""],
+
+ "indent-string": ["indent-string@4.0.0", "", {}, ""],
+
+ "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, ""],
+
+ "inherits": ["inherits@2.0.4", "", {}, ""],
+
+ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
+
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, ""],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, ""],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""],
+
+ "is-number": ["is-number@7.0.0", "", {}, ""],
+
+ "is-path-inside": ["is-path-inside@3.0.3", "", {}, ""],
+
+ "isexe": ["isexe@2.0.0", "", {}, ""],
+
+ "isomorphic-unfetch": ["isomorphic-unfetch@3.1.0", "", { "dependencies": { "node-fetch": "^2.6.1", "unfetch": "^4.2.0" } }, ""],
+
+ "jiti": ["jiti@2.4.2", "", { "bin": "lib/jiti-cli.mjs" }, ""],
+
+ "jose": ["jose@5.10.0", "", {}, ""],
+
+ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, ""],
+
+ "jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@4.1.0", "", {}, ""],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, ""],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, ""],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, ""],
+
+ "kolorist": ["kolorist@1.8.0", "", {}, ""],
+
+ "leac": ["leac@0.6.0", "", {}, ""],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, ""],
+
+ "local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, ""],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, ""],
+
+ "lodash": ["lodash@4.17.21", "", {}, ""],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, ""],
+
+ "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, ""],
+
+ "mdn-data": ["mdn-data@2.12.2", "", {}, ""],
+
+ "merge2": ["merge2@1.4.1", "", {}, ""],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""],
+
+ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
+
+ "min-indent": ["min-indent@1.0.1", "", {}, ""],
+
+ "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""],
+
+ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+
+ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
+
+ "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, ""],
+
+ "mrmime": ["mrmime@2.0.1", "", {}, ""],
+
+ "ms": ["ms@2.1.3", "", {}, ""],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""],
+
+ "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, ""],
+
+ "node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
+
+ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, ""],
+
+ "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, ""],
+
+ "nodemon": ["nodemon@3.1.9", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": "bin/nodemon.js" }, ""],
+
+ "normalize-path": ["normalize-path@3.0.0", "", {}, ""],
+
+ "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, ""],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, ""],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, ""],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, ""],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, ""],
+
+ "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, ""],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, ""],
+
+ "parse-imports": ["parse-imports@2.2.1", "", { "dependencies": { "es-module-lexer": "^1.5.3", "slashes": "^3.0.12" } }, ""],
+
+ "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, ""],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, ""],
+
+ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, ""],
+
+ "path-key": ["path-key@3.1.1", "", {}, ""],
+
+ "path-type": ["path-type@4.0.0", "", {}, ""],
+
+ "pathe": ["pathe@2.0.3", "", {}, ""],
+
+ "peberminta": ["peberminta@0.9.0", "", {}, ""],
+
+ "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, ""],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, ""],
+
+ "picomatch": ["picomatch@4.0.2", "", {}, ""],
+
+ "pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, ""],
+
+ "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""],
+
+ "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, ""],
+
+ "prettier": ["prettier@3.5.1", "", { "bin": "bin/prettier.cjs" }, ""],
+
+ "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, ""],
+
+ "pstree.remy": ["pstree.remy@1.1.8", "", {}, ""],
+
+ "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, ""],
+
+ "quansync": ["quansync@0.2.10", "", {}, ""],
+
+ "queue-microtask": ["queue-microtask@1.2.3", "", {}, ""],
+
+ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
+
+ "react": ["react@19.1.0", "", {}, ""],
+
+ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, ""],
+
+ "react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, ""],
+
+ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, ""],
+
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, ""],
+
+ "resend": ["resend@4.2.0", "", { "dependencies": { "@react-email/render": "1.0.5" } }, ""],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, ""],
+
+ "reusify": ["reusify@1.1.0", "", {}, ""],
+
+ "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, ""],
+
+ "rollup": ["rollup@4.39.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "4.39.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, ""],
+
+ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, ""],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "scheduler": ["scheduler@0.26.0", "", {}, ""],
+
+ "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, ""],
+
+ "semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, ""],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""],
+
+ "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
+
+ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
+
+ "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, ""],
+
+ "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, ""],
+
+ "slash": ["slash@3.0.0", "", {}, ""],
+
+ "slashes": ["slashes@3.0.12", "", {}, ""],
+
+ "source-map": ["source-map@0.6.1", "", {}, ""],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, ""],
+
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, ""],
+
+ "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, ""],
+
+ "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, ""],
+
+ "spdx-license-ids": ["spdx-license-ids@3.0.21", "", {}, ""],
+
+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""],
+
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, ""],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""],
+
+ "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, ""],
+
+ "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, ""],
+
+ "tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
+
+ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
+
+ "terser": ["terser@5.39.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": "bin/terser" }, ""],
+
+ "text-table": ["text-table@0.2.0", "", {}, ""],
+
+ "tinyexec": ["tinyexec@0.3.2", "", {}, ""],
+
+ "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, ""],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""],
+
+ "totalist": ["totalist@3.0.1", "", {}, ""],
+
+ "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, ""],
+
+ "tr46": ["tr46@0.0.3", "", {}, ""],
+
+ "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, ""],
+
+ "tslib": ["tslib@2.8.1", "", {}, ""],
+
+ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""],
+
+ "type-fest": ["type-fest@0.20.2", "", {}, ""],
+
+ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+
+ "ufo": ["ufo@1.5.4", "", {}, ""],
+
+ "unconfig": ["unconfig@7.0.0", "", { "dependencies": { "@antfu/utils": "^8.1.0", "defu": "^6.1.4", "jiti": "^2.4.2" } }, ""],
+
+ "undefsafe": ["undefsafe@2.0.5", "", {}, ""],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, ""],
+
+ "unfetch": ["unfetch@4.2.0", "", {}, ""],
+
+ "universal-user-agent": ["universal-user-agent@7.0.2", "", {}, ""],
+
+ "unocss": ["unocss@65.5.0", "", { "dependencies": { "@unocss/astro": "65.5.0", "@unocss/cli": "65.5.0", "@unocss/core": "65.5.0", "@unocss/postcss": "65.5.0", "@unocss/preset-attributify": "65.5.0", "@unocss/preset-icons": "65.5.0", "@unocss/preset-mini": "65.5.0", "@unocss/preset-tagify": "65.5.0", "@unocss/preset-typography": "65.5.0", "@unocss/preset-uno": "65.5.0", "@unocss/preset-web-fonts": "65.5.0", "@unocss/preset-wind": "65.5.0", "@unocss/transformer-attributify-jsx": "65.5.0", "@unocss/transformer-compile-class": "65.5.0", "@unocss/transformer-directives": "65.5.0", "@unocss/transformer-variant-group": "65.5.0", "@unocss/vite": "65.5.0" }, "peerDependencies": { "@unocss/webpack": "65.5.0", "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" }, "optionalPeers": ["@unocss/webpack"] }, ""],
+
+ "unplugin-utils": ["unplugin-utils@0.2.4", "", { "dependencies": { "pathe": "^2.0.2", "picomatch": "^4.0.2" } }, ""],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "uuid": ["uuid@11.1.0", "", { "bin": "dist/esm/bin/uuid" }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
+
+ "vite": ["vite@6.2.5", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx", "yaml"], "bin": "bin/vite.js" }, ""],
+
+ "vue": ["vue@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" } }, ""],
+
+ "vue-flow-layout": ["vue-flow-layout@0.1.1", "", { "peerDependencies": { "vue": "^3.4.37" } }, ""],
+
+ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, ""],
+
+ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, ""],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, ""],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, ""],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, ""],
+
+ "zod": ["zod@3.24.2", "", {}, ""],
+
+ "@iconify/utils/globals": ["globals@15.15.0", "", {}, ""],
+
+ "@react-email/render/prettier": ["prettier@3.4.2", "", { "bin": "bin/prettier.cjs" }, ""],
+
+ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, ""],
+
+ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
+
+ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""],
+
+ "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
+
+ "eslint-plugin-eslint-comments/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, ""],
+
+ "eslint-plugin-jsdoc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, ""],
+
+ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""],
+
+ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
+
+ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, ""],
+
+ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
+
+ "react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, ""],
+
+ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, ""],
+
+ "@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, ""],
+
+ "eslint-plugin-jsdoc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, ""],
+
+ "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""],
+
+ "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, ""],
+
+ "@testing-library/jest-dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, ""],
+
+ "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, ""],
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..15a4376
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ # Future services can be added here
+
+volumes:
+ # Volume definitions can be added here when needed
+
+networks:
+ default:
+ driver: bridge
diff --git a/eslint-custom-comments-plugin.js b/eslint-custom-comments-plugin.js
new file mode 100644
index 0000000..cd613a1
--- /dev/null
+++ b/eslint-custom-comments-plugin.js
@@ -0,0 +1,35 @@
+const noCommentsRule = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'Disallow all comments',
+ category: 'Stylistic Issues',
+ recommended: false,
+ },
+ schema: [],
+ messages: {
+ unexpected: 'Comments are an anti-pattern. Code should be self-explanatory.',
+ },
+ },
+ create(context) {
+ return {
+ Program(node) {
+ const sourceCode = context.getSourceCode();
+ const comments = sourceCode.getAllComments();
+
+ for (const comment of comments) {
+ context.report({
+ node: comment,
+ messageId: 'unexpected',
+ });
+ }
+ },
+ };
+ },
+};
+
+export default {
+ rules: {
+ 'no-comments': noCommentsRule
+ }
+};
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..ef3a28a
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,80 @@
+import tsParser from '@typescript-eslint/parser';
+import tsPlugin from '@typescript-eslint/eslint-plugin';
+import jsdocPlugin from 'eslint-plugin-jsdoc';
+import eslintCommentsPlugin from 'eslint-plugin-eslint-comments';
+import customCommentsPlugin from './eslint-custom-comments-plugin.js';
+
+export default [
+ {
+ files: ['src/**/*.{ts,tsx}'],
+ languageOptions: {
+ parser: tsParser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ project: './tsconfig.json',
+ },
+ },
+ plugins: {
+ '@typescript-eslint': tsPlugin,
+ jsdoc: jsdocPlugin,
+ 'eslint-comments': eslintCommentsPlugin,
+ 'custom-comments': customCommentsPlugin,
+ },
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: ['.*'],
+ message: 'Use absolute imports with @ prefix instead. Example: @/components/Layout',
+ },
+ ],
+ },
+ ],
+ 'no-console': 'error',
+ 'no-debugger': 'error',
+ '@typescript-eslint/explicit-function-return-type': 'error',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-non-null-assertion': 'error',
+ 'no-duplicate-imports': 'error',
+ 'sort-imports': ['error', { ignoreDeclarationSort: true }],
+ '@typescript-eslint/no-magic-numbers': 'off',
+ 'jsdoc/no-types': 'error',
+ 'jsdoc/require-jsdoc': 'off',
+ 'jsdoc/require-param': 'off',
+ 'jsdoc/require-returns': 'off',
+ 'jsdoc/require-description': 'off',
+ 'no-warning-comments': ['error', { terms: ['todo', 'fixme', 'xxx', 'note'], location: 'anywhere' }],
+ 'spaced-comment': ['error', 'never'],
+ 'multiline-comment-style': ['error', 'starred-block'],
+ 'lines-around-comment': [
+ 'error',
+ { beforeBlockComment: false, afterBlockComment: false, beforeLineComment: false, afterLineComment: false },
+ ],
+ 'no-inline-comments': 'error',
+ 'eslint-comments/no-use': 'error',
+ 'custom-comments/no-comments': 'error',
+ },
+ },
+ {
+ files: ['src/config/env.ts'],
+ rules: {
+ 'no-restricted-syntax': 'off',
+ },
+ },
+ {
+ files: ['src/utils/env.ts'],
+ rules: {
+ 'no-restricted-syntax': 'off',
+ 'eslint-comments/no-use': 'off',
+ 'custom-comments/no-comments': 'off',
+ 'spaced-comment': 'off',
+ },
+ },
+];
diff --git a/fly.toml b/fly.toml
new file mode 100644
index 0000000..4e59e31
--- /dev/null
+++ b/fly.toml
@@ -0,0 +1,21 @@
+app = 'hyperwave-demo'
+primary_region = 'dfw'
+
+[build]
+
+[http_service]
+ internal_port = 3000
+ force_https = true
+ auto_stop_machines = 'stop'
+ auto_start_machines = true
+ min_machines_running = 0
+ processes = ['app']
+
+[mounts]
+ source = "data"
+ destination = "/data"
+
+[[vm]]
+ memory = '1gb'
+ cpu_kind = 'shared'
+ cpus = 1
diff --git a/package.json b/package.json
index 7ff2d4d..b8312ae 100644
--- a/package.json
+++ b/package.json
@@ -1,30 +1,87 @@
{
"name": "hyperwave",
- "version": "0.2.1",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
"scripts": {
"build": "bun build:css && bun build --compile ./src/server.tsx",
- "build:css": "unocss \"src/**/*.tsx\" -o public/styles/uno.css",
- "css": "unocss --watch \"src/**/*.tsx\" -o public/styles/uno.css",
- "db": "bun run src/db.ts",
- "dev": "bun install && concurrently --restart-tries=3 \"bun css\" \"nodemon --watch src --ext ts,tsx --exec 'bun run --hot src/server.tsx'\"",
- "prettier": "bunx prettier --write src/ test/ --plugin prettier-plugin-tailwindcss",
- "server": "bun run --hot src/server.tsx",
- "test": "bun run test",
- "update-deps": "bunx npm-check-updates -u && bun install"
+ "build:css": "mkdir -p public/styles && unocss \"src/**/*.tsx\" -o public/styles/uno.css --config src/styles/uno.config.ts",
+ "check-updates": "bun x npm-check-updates -u && bun install",
+ "ci:local": "bun x act --container-architecture linux/amd64",
+ "ci:local:unit": "bun x act -j unit --container-architecture linux/amd64",
+ "dev": "bun run build:css && bun --hot src/server.tsx",
+ "dev:docker": "docker build -t portal-dev -f Dockerfile . && docker run -it --rm -p 3000:3000 -v $(pwd)/src:/app/src portal-dev",
+ "dev:local": "bun run dev",
+ "favicon": "bun scripts/generate-favicon.js",
+ "format:check": "bun x prettier@3.5.1 --check \"src/**/*.{ts,tsx,js,jsx,css,scss,html,md}\"",
+ "kill": "pkill -9 -f 'bun --watch src/server.tsx' || true",
+ "lint": "bun x eslint \"src/**/*.{ts,tsx}\"",
+ "precommit": "bun run typecheck && bun run format:check && bun run lint && bun run test:unit",
+ "prepare": "husky",
+ "prettier": "bun x prettier@3.5.1 --write \"src/**/*.{ts,tsx,js,jsx,css,scss,html,md}\"",
+ "server": "bun --watch src/server.tsx",
+ "setup": "./setup.sh",
+ "test:unit": "bun test",
+ "typecheck": "bun x tsc --noEmit"
},
"dependencies": {
- "@unocss/preset-web-fonts": "^0.61.9",
- "hono": "^4.7.0",
- "nodemon": "^3.1.9",
- "unocss": "^0.61.9",
+ "@hono/node-server": "^1.14.0",
+ "@hono/zod-validator": "^0.4.3",
+ "@linear/sdk": "^38.0.0",
+ "@types/uuid": "^10.0.0",
+ "@unocss/preset-web-fonts": "^65.5.0",
+ "canvas": "^3.1.0",
+ "hono": "^4.7.5",
+ "jose": "^5.10.0",
+ "resend": "^4.2.0",
+ "unocss": "^65.5.0",
+ "uuid": "^11.1.0",
"zod": "^3.24.2"
},
"devDependencies": {
- "@unocss/cli": "^0.61.9",
- "bun-types": "^1.2.2",
- "concurrently": "^8.2.2",
- "prettier": "^3.5.0",
- "prettier-plugin-tailwindcss": "^0.6.11"
+ "@octokit/rest": "^21.1.1",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@types/eslint": "^9.6.1",
+ "@types/node": "^22.14.0",
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
+ "@typescript-eslint/parser": "^7.18.0",
+ "@unocss/cli": "^65.5.0",
+ "bun-types": "^1.2.8",
+ "chalk": "^5.4.1",
+ "cross-env": "^7.0.3",
+ "dotenv": "^16.4.7",
+ "eslint": "^8.57.1",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-jsdoc": "^50.6.9",
+ "husky": "^9.1.7",
+ "nodemon": "^3.1.9",
+ "prettier": "3.5.1",
+ "prettier-plugin-tailwindcss": "^0.6.11",
+ "terser": "^5.39.0",
+ "typescript": "^5.8.3"
+ },
+ "module": "src/server.tsx",
+ "husky": {
+ "hooks": {
+ "pre-commit": "bun run precommit"
+ }
},
- "module": "src/server.tsx"
-}
+ "bun": {
+ "test": {
+ "coverage": true,
+ "coverageThreshold": {
+ "global": {
+ "lines": 80,
+ "functions": 80,
+ "branches": 70,
+ "statements": 80
+ }
+ },
+ "coverageIgnorePatterns": [
+ "assets/**",
+ "__tests__/**",
+ "node_modules/**"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/favicon.png b/public/favicon.png
new file mode 100644
index 0000000..65a7bf8
Binary files /dev/null and b/public/favicon.png differ
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..39ab8ab
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/styles/app.css b/public/styles/app.css
new file mode 100644
index 0000000..cbbed0f
--- /dev/null
+++ b/public/styles/app.css
@@ -0,0 +1,53 @@
+/* Global CSS Reset and Base Styles */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ outline: none;
+ color: unset;
+}
+
+/* Input, Button, and Link Styles */
+input, button, a {
+ -webkit-tap-highlight-color: transparent !important;
+ -webkit-user-select: none !important;
+ -webkit-touch-callout: none !important;
+ user-select: none !important;
+ outline: none !important;
+ outline-offset: none !important;
+}
+
+button {
+ outline: none !important;
+ outline-offset: none !important;
+ -webkit-tap-highlight-color: transparent !important;
+}
+
+button:focus {
+ outline: none !important;
+ outline-offset: none !important;
+ box-shadow: none !important;
+}
+
+button:focus-visible {
+ outline: none !important;
+ outline-offset: none !important;
+ box-shadow: none !important;
+}
+
+/* Custom Scrollbar */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--un-bg-app-background);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--un-bg-interactive-primary);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--un-bg-interactive-primary-hover);
+}
\ No newline at end of file
diff --git a/public/styles/modal.css b/public/styles/modal.css
new file mode 100644
index 0000000..c2b7dd1
--- /dev/null
+++ b/public/styles/modal.css
@@ -0,0 +1,34 @@
+/* Modal styles to improve interaction */
+
+/* Make sure the modal content itself has proper interaction */
+.modal-content {
+ pointer-events: auto;
+}
+
+/* Ensure all elements within the modal properly get interactions */
+.modal-content * {
+ pointer-events: auto;
+}
+
+/* Improve thumbnails hover state */
+.thumbnail-item {
+ transition: all 0.2s ease-in-out;
+ cursor: pointer;
+}
+
+.thumbnail-item:hover {
+ transform: scale(1.05);
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
+}
+
+/* Fix scrolling container */
+.modal-content>div {
+ max-height: calc(100vh - 12rem);
+ overflow-y: auto;
+}
+
+/* Specifically target the sequence thumbnails to ensure they are clickable */
+.sequence-group .thumbnail-item {
+ cursor: pointer;
+ user-select: none;
+}
\ No newline at end of file
diff --git a/public/styles/uno.css b/public/styles/uno.css
index a32bf4f..b432121 100644
--- a/public/styles/uno.css
+++ b/public/styles/uno.css
@@ -1,80 +1,1679 @@
/* layer: preflights */
*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* symbols2 */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2) format('woff2');
+ unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* symbols2 */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2) format('woff2');
+ unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* symbols2 */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2) format('woff2');
+ unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* symbols2 */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2) format('woff2');
+ unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* symbols2 */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2) format('woff2');
+ unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Fira Code';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/firacode/v26/uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L0UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L9UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L1UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L6UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L2UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L3UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUMbndwV.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L0UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L9UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L1UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L6UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L2UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L3UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUMbndwV.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L0UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L9UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L1UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L6UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L2UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L3UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUMbndwV.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L0UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L9UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L1UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L6UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L2UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L3UUMbndwVgHU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUMbndwV.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD2OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD_OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD4OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD0OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD1OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD7OwGtT0rU.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD2OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD_OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD4OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD0OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD1OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD7OwGtT0rU.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD2OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD_OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD4OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD0OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD1OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD7OwGtT0rU.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD2OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD_OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD4OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD0OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD1OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD7OwGtT0rU.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD2OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD_OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD4OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD0OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD1OwGtT0rU3BE.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v20/tDbV2o-flEEny0FZhsfKu5WU4xD7OwGtT0rU.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
/* latin-ext */
@font-face {
- font-family: 'Lato';
+ font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
- font-family: 'Lato';
+ font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Source Code Pro';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/sourcecodepro/v30/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+/* layer: shortcuts */
+.container{width:100%;}
+@media (min-width: 640px){
+.container{max-width:640px;}
+}
+@media (min-width: 768px){
+.container{max-width:768px;}
+}
+@media (min-width: 1024px){
+.container{max-width:1024px;}
+}
+@media (min-width: 1280px){
+.container{max-width:1280px;}
+}
+@media (min-width: 1536px){
+.container{max-width:1536px;}
+}
/* layer: default */
-.m-0{margin:0;}
-.m-auto{margin:auto;}
+.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;}
+.group:hover .group-hover\:visible{visibility:visible;}
+.hover\:visible:hover{visibility:visible;}
+.invisible{visibility:hidden;}
+.absolute{position:absolute;}
+.fixed{position:fixed;}
+.relative{position:relative;}
+.sticky{position:sticky;}
+.after\:absolute::after{position:absolute;}
+.inset-0{inset:0;}
+.bottom-\[-3px\]{bottom:-3px;}
+.left-\[50\%\]{left:50%;}
+.right-3{right:0.75rem;}
+.right-4{right:1rem;}
+.right-full{right:100%;}
+.top-0{top:0;}
+.top-3{top:0.75rem;}
+.top-4{top:1rem;}
+.after\:left-\[2px\]::after{left:2px;}
+.after\:top-\[2px\]::after{top:2px;}
+.z-\[1000\]{z-index:1000;}
+.z-\[10000\]{z-index:10000;}
+.z-\[9999\]{z-index:9999;}
+.z-10{z-index:10;}
+.z-50{z-index:50;}
+.grid{display:grid;}
+.grid-cols-\[1fr_2fr_1fr\]{grid-template-columns:1fr 2fr 1fr;}
+.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr));}
+.mx-auto{margin-left:auto;margin-right:auto;}
+.mb-1{margin-bottom:0.25rem;}
+.mb-2{margin-bottom:0.5rem;}
+.mb-3{margin-bottom:0.75rem;}
+.mb-4{margin-bottom:1rem;}
+.mb-6{margin-bottom:1.5rem;}
+.mb-8{margin-bottom:2rem;}
+.ml-8{margin-left:2rem;}
+.mr-1{margin-right:0.25rem;}
+.mr-2{margin-right:0.5rem;}
+.mr-3{margin-right:0.75rem;}
+.mt-\[5px\]{margin-top:5px;}
+.mt-1{margin-top:0.25rem;}
+.mt-2{margin-top:0.5rem;}
+.mt-4{margin-top:1rem;}
+.mt-6{margin-top:1.5rem;}
+.block{display:block;}
+.hidden{display:none;}
+[size~="lg"]{width:32rem;height:32rem;}
+[size~="sm"]{width:24rem;height:24rem;}
+.group.expanded .group-\[\.expanded\]\:max-h-\[1000px\]{max-height:1000px;}
+.h-\[1\.5px\]{height:1.5px;}
+.h-0\.5{height:0.125rem;}
+.h-2\.5{height:0.625rem;}
+.h-4{height:1rem;}
+.h-48{height:12rem;}
+.h-6{height:1.5rem;}
.h-8{height:2rem;}
+.h-9{height:2.25rem;}
.h-full{height:100%;}
+.max-h-\[90vh\]{max-height:90vh;}
+.max-h-0{max-height:0;}
+.max-w-6xl{max-width:72rem;}
+.min-h-screen{min-height:100vh;}
+.min-w-32{min-width:8rem;}
+.w-\[105\%\]{width:105%;}
+.w-\[600px\]{width:600px;}
+.w-0{width:0;}
+.w-11{width:2.75rem;}
+.w-4{width:1rem;}
+.w-44{width:11rem;}
+.w-48{width:12rem;}
+.w-5{width:1.25rem;}
+.w-8{width:2rem;}
+.w-9{width:2.25rem;}
+.w-full{width:100%;}
+.after\:h-5::after{height:1.25rem;}
+.after\:w-5::after{width:1.25rem;}
.flex{display:flex;}
+.inline-flex{display:inline-flex;}
.flex-col{flex-direction:column;}
+.flex-wrap{flex-wrap:wrap;}
+.-translate-x-1\/2{--un-translate-x:-50%;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
+.translate-y-0{--un-translate-y:0;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
+.peer:checked~.peer-checked\:after\:translate-x-full::after{--un-translate-x:100%;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
+.group.expanded .group-\[\.expanded\]\:rotate-180{--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-rotate:180deg;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
+.hover\:scale-110:hover{--un-scale-x:1.1;--un-scale-y:1.1;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
+.transform{transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
+.transform-gpu{transform:translate3d(var(--un-translate-x), var(--un-translate-y), var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
.cursor-pointer{cursor:pointer;}
+.disabled\:cursor-not-allowed:disabled{cursor:not-allowed;}
.items-center{align-items:center;}
+.justify-start{justify-content:flex-start;}
.justify-center{justify-content:center;}
-.gap-3{gap:0.75rem;}
-.gap-8{gap:2rem;}
+.justify-between{justify-content:space-between;}
+.gap-2{gap:0.5rem;}
+.gap-4{gap:1rem;}
+.gap-6{gap:1.5rem;}
+.space-x-2>:not([hidden])~:not([hidden]){--un-space-x-reverse:0;margin-left:calc(0.5rem * calc(1 - var(--un-space-x-reverse)));margin-right:calc(0.5rem * var(--un-space-x-reverse));}
+.space-x-8>:not([hidden])~:not([hidden]){--un-space-x-reverse:0;margin-left:calc(2rem * calc(1 - var(--un-space-x-reverse)));margin-right:calc(2rem * var(--un-space-x-reverse));}
+.space-y-2>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(0.5rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(0.5rem * var(--un-space-y-reverse));}
+.space-y-4>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(1rem * var(--un-space-y-reverse));}
+.space-y-6>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(1.5rem * var(--un-space-y-reverse));}
+.space-y-8>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(2rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(2rem * var(--un-space-y-reverse));}
+.overflow-hidden{overflow:hidden;}
+.overflow-x-hidden{overflow-x:hidden;}
+.overflow-y-auto{overflow-y:auto;}
+.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.border{border-width:1px;}
-.border-gray-2{--un-border-opacity:1;border-color:rgb(229 231 235 / var(--un-border-opacity));}
-.focus\:border-blue-200:focus{--un-border-opacity:1;border-color:rgb(191 219 254 / var(--un-border-opacity));}
+.border-0{border-width:0px;}
+.border-2{border-width:2px;}
+.after\:border::after{border-width:1px;}
+.border-b{border-bottom-width:1px;}
+.border-t{border-top-width:1px;}
+.border-border-primary,
+.border-interactive-primary{--un-border-opacity:1;border-color:rgb(139 92 246 / var(--un-border-opacity));}
+.border-border-secondary{--un-border-opacity:1;border-color:rgb(163 163 163 / var(--un-border-opacity));}
+.border-border-subtle{--un-border-opacity:0.1;border-color:rgba(255, 255, 255, var(--un-border-opacity));}
+.border-interactive-danger,
+.border-status-error{--un-border-opacity:1;border-color:rgb(248 113 113 / var(--un-border-opacity));}
+.border-interactive-primary\/50{border-color:rgb(139 92 246 / 0.5);}
+.border-status-info{--un-border-opacity:1;border-color:rgb(56 189 248 / var(--un-border-opacity));}
+.border-status-success{--un-border-opacity:1;border-color:rgb(52 211 153 / var(--un-border-opacity));}
+.border-status-success\/50{border-color:rgb(52 211 153 / 0.5);}
+.border-status-warning{--un-border-opacity:1;border-color:rgb(251 191 36 / var(--un-border-opacity));}
+.border-status-warning\/50{border-color:rgb(251 191 36 / 0.5);}
+.peer:checked~.peer-checked\:after\:border-border-primary::after{--un-border-opacity:1;border-color:rgb(139 92 246 / var(--un-border-opacity));}
+.hover\:border-border-accent:hover{--un-border-opacity:1;border-color:rgb(168 85 247 / var(--un-border-opacity));}
+.hover\:border-border-secondary:hover{--un-border-opacity:1;border-color:rgb(163 163 163 / var(--un-border-opacity));}
+.focus\:border-border-accent:focus{--un-border-opacity:1;border-color:rgb(168 85 247 / var(--un-border-opacity));}
+.after\:border-border-secondary::after{--un-border-opacity:1;border-color:rgb(163 163 163 / var(--un-border-opacity));}
+.border-opacity-30{--un-border-opacity:0.3;}
+.border-opacity-40{--un-border-opacity:0.4;}
+.border-opacity-50{--un-border-opacity:0.5;}
+.border-opacity-60{--un-border-opacity:0.6;}
+.hover\:border-opacity-60:hover{--un-border-opacity:0.6;}
+.hover\:border-opacity-70:hover{--un-border-opacity:0.7;}
+.hover\:border-opacity-80:hover{--un-border-opacity:0.8;}
.rounded{border-radius:0.25rem;}
+.rounded-full{border-radius:9999px;}
+.rounded-lg{border-radius:0.5rem;}
.rounded-md{border-radius:0.375rem;}
-.border-none{border-style:none;}
-.border-solid{border-style:solid;}
-.bg-blue-200{--un-bg-opacity:1;background-color:rgb(191 219 254 / var(--un-bg-opacity)) /* #bfdbfe */;}
-.bg-blue-300{--un-bg-opacity:1;background-color:rgb(147 197 253 / var(--un-bg-opacity)) /* #93c5fd */;}
-.bg-gray-300{--un-bg-opacity:1;background-color:rgb(209 213 219 / var(--un-bg-opacity)) /* #d1d5db */;}
-.bg-indigo-200{--un-bg-opacity:1;background-color:rgb(199 210 254 / var(--un-bg-opacity)) /* #c7d2fe */;}
-.bg-transparent{background-color:transparent /* transparent */;}
-.bg-white{--un-bg-opacity:1;background-color:rgb(255 255 255 / var(--un-bg-opacity)) /* #fff */;}
-.dark .dark\:bg-blue-600{--un-bg-opacity:1;background-color:rgb(37 99 235 / var(--un-bg-opacity)) /* #2563eb */;}
-.dark .dark\:bg-gray-700{--un-bg-opacity:1;background-color:rgb(55 65 81 / var(--un-bg-opacity)) /* #374151 */;}
-.dark .dark\:bg-gray-900{--un-bg-opacity:1;background-color:rgb(17 24 39 / var(--un-bg-opacity)) /* #111827 */;}
-.dark .dark\:bg-indigo-600{--un-bg-opacity:1;background-color:rgb(79 70 229 / var(--un-bg-opacity)) /* #4f46e5 */;}
-.hover\:bg-blue-400:hover{--un-bg-opacity:1;background-color:rgb(96 165 250 / var(--un-bg-opacity)) /* #60a5fa */;}
-.p-0{padding:0;}
-.px-10{padding-left:2.5rem;padding-right:2.5rem;}
+.rounded-none,
+[rounded-none=""]{border-radius:0;}
+.after\:rounded-none::after{border-radius:0;}
+.bg-app-background{--un-bg-opacity:1;background-color:rgb(12 10 20 / var(--un-bg-opacity)) /* #0c0a14 */;}
+.bg-app-background-accent{--un-bg-opacity:1;background-color:rgb(32 30 42 / var(--un-bg-opacity)) /* #201e2a */;}
+.bg-app-background-alt{--un-bg-opacity:1;background-color:rgb(22 20 33 / var(--un-bg-opacity)) /* #161421 */;}
+.bg-app-background-overlay{--un-bg-opacity:0.95;background-color:rgba(12, 10, 20, var(--un-bg-opacity)) /* rgba(12, 10, 20, 0.95) */;}
+.bg-app-surface{--un-bg-opacity:1;background-color:rgb(42 39 53 / var(--un-bg-opacity)) /* #2a2735 */;}
+.bg-app-surface-dim{--un-bg-opacity:0.06;background-color:rgba(255, 255, 255, var(--un-bg-opacity)) /* rgba(255, 255, 255, 0.06) */;}
+.bg-app-surface\/40{background-color:rgb(42 39 53 / 0.4) /* #2a2735 */;}
+.bg-border-primary,
+.bg-interactive-primary{--un-bg-opacity:1;background-color:rgb(139 92 246 / var(--un-bg-opacity)) /* #8b5cf6 */;}
+.bg-interactive-secondary{--un-bg-opacity:1;background-color:rgb(168 85 247 / var(--un-bg-opacity)) /* #a855f7 */;}
+.bg-status-error\/5{background-color:rgb(248 113 113 / 0.05) /* #f87171 */;}
+.bg-status-success{--un-bg-opacity:1;background-color:rgb(52 211 153 / var(--un-bg-opacity)) /* #34d399 */;}
+.bg-status-warning{--un-bg-opacity:1;background-color:rgb(251 191 36 / var(--un-bg-opacity)) /* #fbbf24 */;}
+.bg-text-white{--un-bg-opacity:1;background-color:rgb(255 255 255 / var(--un-bg-opacity)) /* #ffffff */;}
+.peer:checked~.peer-checked\:after\:bg-interactive-primary::after{--un-bg-opacity:1;background-color:rgb(139 92 246 / var(--un-bg-opacity)) /* #8b5cf6 */;}
+.peer:checked~.peer-checked\:bg-interactive-primary\/20{background-color:rgb(139 92 246 / 0.2) /* #8b5cf6 */;}
+.hover\:bg-app-background-accent:hover{--un-bg-opacity:1;background-color:rgb(32 30 42 / var(--un-bg-opacity)) /* #201e2a */;}
+.hover\:bg-app-surface-hover:hover{--un-bg-opacity:0.15;background-color:rgba(139, 92, 246, var(--un-bg-opacity)) /* rgba(139, 92, 246, 0.15) */;}
+.hover\:bg-app-surface:hover{--un-bg-opacity:1;background-color:rgb(42 39 53 / var(--un-bg-opacity)) /* #2a2735 */;}
+.hover\:bg-border-primary\/8:hover{background-color:rgb(139 92 246 / 0.08) /* #8b5cf6 */;}
+.hover\:bg-status-error\/20:hover{background-color:rgb(248 113 113 / 0.2) /* #f87171 */;}
+.hover\:bg-status-error\/8:hover{background-color:rgb(248 113 113 / 0.08) /* #f87171 */;}
+.hover\:bg-status-info\/8:hover{background-color:rgb(56 189 248 / 0.08) /* #38bdf8 */;}
+.focus\:bg-app-background-accent:focus{--un-bg-opacity:1;background-color:rgb(32 30 42 / var(--un-bg-opacity)) /* #201e2a */;}
+.after\:bg-app-background-accent::after{--un-bg-opacity:1;background-color:rgb(32 30 42 / var(--un-bg-opacity)) /* #201e2a */;}
+[stroke-width~="\32 "]{stroke-width:2px;}
+.p-2{padding:0.5rem;}
+.p-3{padding:0.75rem;}
+.p-4,
+[p-4=""]{padding:1rem;}
+.p-6{padding:1.5rem;}
.px-2{padding-left:0.5rem;padding-right:0.5rem;}
+.px-3{padding-left:0.75rem;padding-right:0.75rem;}
.px-4{padding-left:1rem;padding-right:1rem;}
-.px-6{padding-left:1.5rem;padding-right:1.5rem;}
.py-1{padding-top:0.25rem;padding-bottom:0.25rem;}
+.py-1\.5{padding-top:0.375rem;padding-bottom:0.375rem;}
.py-2{padding-top:0.5rem;padding-bottom:0.5rem;}
.py-3{padding-top:0.75rem;padding-bottom:0.75rem;}
-.py-8{padding-top:2rem;padding-bottom:2rem;}
-.pl-3{padding-left:0.75rem;}
-.pr-10{padding-right:2.5rem;}
+.pt-4{padding-top:1rem;}
+.pt-6{padding-top:1.5rem;}
.text-center{text-align:center;}
-.text-5xl{font-size:3rem;line-height:1;}
+.text-right{text-align:right;}
+.text-2xl{font-size:1.5rem;line-height:2rem;}
.text-base{font-size:1rem;line-height:1.5rem;}
+.text-lg{font-size:1.125rem;line-height:1.75rem;}
.text-sm{font-size:0.875rem;line-height:1.25rem;}
-.dark .dark\:text-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity)) /* #fff */;}
-.text-black{--un-text-opacity:1;color:rgb(0 0 0 / var(--un-text-opacity)) /* #000 */;}
-.text-neutral-500{--un-text-opacity:1;color:rgb(115 115 115 / var(--un-text-opacity)) /* #737373 */;}
-.text-slate-900{--un-text-opacity:1;color:rgb(15 23 42 / var(--un-text-opacity)) /* #0f172a */;}
-.dark .dark\:hover\:text-yellow-300:hover{--un-text-opacity:1;color:rgb(253 224 71 / var(--un-text-opacity)) /* #fde047 */;}
-.hover\:text-yellow-600:hover{--un-text-opacity:1;color:rgb(202 138 4 / var(--un-text-opacity)) /* #ca8a04 */;}
+.text-xl{font-size:1.25rem;line-height:1.75rem;}
+.text-xs{font-size:0.75rem;line-height:1rem;}
+.text-status-error{--un-text-opacity:1;color:rgb(248 113 113 / var(--un-text-opacity)) /* #f87171 */;}
+.text-status-info{--un-text-opacity:1;color:rgb(56 189 248 / var(--un-text-opacity)) /* #38bdf8 */;}
+.text-status-success{--un-text-opacity:1;color:rgb(52 211 153 / var(--un-text-opacity)) /* #34d399 */;}
+.text-status-warning{--un-text-opacity:1;color:rgb(251 191 36 / var(--un-text-opacity)) /* #fbbf24 */;}
+.text-text-inverse{--un-text-opacity:1;color:rgb(12 10 20 / var(--un-text-opacity)) /* #0c0a14 */;}
+.text-text-primary{--un-text-opacity:1;color:rgb(250 250 250 / var(--un-text-opacity)) /* #fafafa */;}
+.text-text-secondary{--un-text-opacity:1;color:rgb(229 229 229 / var(--un-text-opacity)) /* #e5e5e5 */;}
+.text-text-tertiary{--un-text-opacity:1;color:rgb(163 163 163 / var(--un-text-opacity)) /* #a3a3a3 */;}
+.text-text-white-dim{--un-text-opacity:0.85;color:rgba(255, 255, 255, var(--un-text-opacity)) /* rgba(255, 255, 255, 0.85) */;}
+.hover\:text-status-error:hover{--un-text-opacity:1;color:rgb(248 113 113 / var(--un-text-opacity)) /* #f87171 */;}
+.hover\:text-text-primary:hover{--un-text-opacity:1;color:rgb(250 250 250 / var(--un-text-opacity)) /* #fafafa */;}
+.hover\:text-text-white:hover{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity)) /* #ffffff */;}
.font-bold{font-weight:700;}
-.font-extrabold{font-weight:800;}
-.font-lato{font-family:"Lato";}
-.underline{text-decoration-line:underline;}
-.shadow-lg{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.font-medium{font-weight:500;}
+.font-semibold{font-weight:600;}
+.font-primary,
+[font-primary=""]{font-family:"Inter";}
+.no-underline{text-decoration:none;}
+.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}
+.group.expanded .group-\[\.expanded\]\:opacity-100,
+.opacity-100{opacity:1;}
+.opacity-0{opacity:0;}
+.opacity-90{opacity:0.9;}
+.disabled\:opacity-50:disabled{opacity:0.5;}
+.shadow-2xl{--un-shadow:var(--un-shadow-inset) 0 25px 50px -12px var(--un-shadow-color, rgb(0 0 0 / 0.25));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.shadow-lg,
+[shadow-lg=""]{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
.shadow-md{--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
-.outline{outline-style:solid;}
-.drop-shadow-lg{--un-drop-shadow:drop-shadow(0 10px 8px var(--un-drop-shadow-color, rgb(0 0 0 / 0.04))) drop-shadow(0 4px 3px var(--un-drop-shadow-color, rgb(0 0 0 / 0.1)));filter:var(--un-blur) var(--un-brightness) var(--un-contrast) var(--un-drop-shadow) var(--un-grayscale) var(--un-hue-rotate) var(--un-invert) var(--un-saturate) var(--un-sepia);}
\ No newline at end of file
+.shadow-sm,
+[shadow-sm=""]{--un-shadow:var(--un-shadow-inset) 0 1px 2px 0 var(--un-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.shadow-xl{--un-shadow:var(--un-shadow-inset) 0 20px 25px -5px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 8px 10px -6px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px;}
+.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px;}
+.ring-0{--un-ring-width:0px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.focus\:ring-1:focus{--un-ring-width:1px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.focus\:ring-2:focus{--un-ring-width:2px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
+.focus\:ring-border-primary\/20:focus{--un-ring-color:rgb(139 92 246 / 0.2) /* #8b5cf6 */;}
+.focus\:ring-border-primary\/30:focus{--un-ring-color:rgb(139 92 246 / 0.3) /* #8b5cf6 */;}
+.focus\:ring-interactive-danger\/30:focus{--un-ring-color:rgb(248 113 113 / 0.3) /* #f87171 */;}
+.focus\:ring-interactive-primary\/30:focus{--un-ring-color:rgb(139 92 246 / 0.3) /* #8b5cf6 */;}
+.backdrop-blur-lg,
+[backdrop-blur-lg=""]{--un-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--un-backdrop-blur) var(--un-backdrop-brightness) var(--un-backdrop-contrast) var(--un-backdrop-grayscale) var(--un-backdrop-hue-rotate) var(--un-backdrop-invert) var(--un-backdrop-opacity) var(--un-backdrop-saturate) var(--un-backdrop-sepia);backdrop-filter:var(--un-backdrop-blur) var(--un-backdrop-brightness) var(--un-backdrop-contrast) var(--un-backdrop-grayscale) var(--un-backdrop-hue-rotate) var(--un-backdrop-invert) var(--un-backdrop-opacity) var(--un-backdrop-saturate) var(--un-backdrop-sepia);}
+.backdrop-blur-sm{--un-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--un-backdrop-blur) var(--un-backdrop-brightness) var(--un-backdrop-contrast) var(--un-backdrop-grayscale) var(--un-backdrop-hue-rotate) var(--un-backdrop-invert) var(--un-backdrop-opacity) var(--un-backdrop-saturate) var(--un-backdrop-sepia);backdrop-filter:var(--un-backdrop-blur) var(--un-backdrop-brightness) var(--un-backdrop-contrast) var(--un-backdrop-grayscale) var(--un-backdrop-hue-rotate) var(--un-backdrop-invert) var(--un-backdrop-opacity) var(--un-backdrop-saturate) var(--un-backdrop-sepia);}
+.backdrop-blur-xl{--un-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--un-backdrop-blur) var(--un-backdrop-brightness) var(--un-backdrop-contrast) var(--un-backdrop-grayscale) var(--un-backdrop-hue-rotate) var(--un-backdrop-invert) var(--un-backdrop-opacity) var(--un-backdrop-saturate) var(--un-backdrop-sepia);backdrop-filter:var(--un-backdrop-blur) var(--un-backdrop-brightness) var(--un-backdrop-contrast) var(--un-backdrop-grayscale) var(--un-backdrop-hue-rotate) var(--un-backdrop-invert) var(--un-backdrop-opacity) var(--un-backdrop-saturate) var(--un-backdrop-sepia);}
+.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
+.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
+.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
+.after\:transition-all::after{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
+.duration-200{transition-duration:200ms;}
+.duration-300{transition-duration:300ms;}
+.ease-in-out{transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);}
+.ease-out{transition-timing-function:cubic-bezier(0, 0, 0.2, 1);}
+.after\:content-\[\'\'\]::after{content:'';}
+.placeholder-text-secondary::placeholder{--un-placeholder-opacity:1;color:rgb(229 229 229 / var(--un-placeholder-opacity)) /* #e5e5e5 */;}
+@media (min-width: 768px){
+.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr));}
+.md\:hidden{display:none;}
+.md\:flex{display:flex;}
+.md\:px-8{padding-left:2rem;padding-right:2rem;}
+}
+@media (min-width: 1024px){
+.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr));}
+}
\ No newline at end of file
diff --git a/setup.sh b/setup.sh
new file mode 100755
index 0000000..cc39b4c
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+set -e
+
+echo "๐ง Setting up the project environment..."
+
+# Function to handle errors
+handle_error() {
+ echo "โ Error: $1"
+ echo "โ ๏ธ Setup failed but you can try again or proceed manually."
+}
+
+# Check for required dependencies
+if ! command -v curl &>/dev/null; then
+ handle_error "curl is not installed. Please install curl first."
+ exit 1
+fi
+
+if ! command -v git &>/dev/null; then
+ handle_error "Git is not installed. Please install Git first."
+ exit 1
+fi
+
+# Install or ensure Bun is available
+if ! command -v bun &>/dev/null; then
+ echo "โ๏ธ Bun is not installed. Installing Bun..."
+ if curl -fsSL https://bun.sh/install | bash; then
+ # Source profile to make bun available in current session
+ export PATH="$HOME/.bun/bin:$PATH"
+ echo "โ
Bun installed successfully!"
+ else
+ handle_error "Failed to install Bun. Please install it manually: https://bun.sh"
+ exit 1
+ fi
+fi
+
+# Install dependencies
+echo "๐ฆ Installing dependencies..."
+if ! bun install; then
+ handle_error "Failed to install dependencies. Please check your network connection or package.json."
+ exit 1
+fi
+
+# Create .env file if it doesn't exist
+if [ ! -f .env ]; then
+ echo "๐ Creating .env file from .env.example..."
+ if [ -f .env.example ]; then
+ cp .env.example .env && echo "โ
.env file created from .env.example."
+ echo "โ ๏ธ Please update the .env file with your actual credentials."
+ else
+ handle_error ".env.example not found. Please create .env file manually."
+ fi
+fi
+
+# Set up husky git hooks
+echo "๐ช Setting up Git hooks..."
+if ! bun husky install 2>/dev/null; then
+ handle_error "Failed to set up Git hooks. This is non-critical, you can continue."
+fi
+
+# Check for Docker (non-critical)
+DOCKER_AVAILABLE=true
+if ! command -v docker &> /dev/null; then
+ echo "โ ๏ธ Docker is not installed. Some features may not work correctly."
+ DOCKER_AVAILABLE=false
+fi
+
+# Check for Docker Compose (non-critical)
+COMPOSE_AVAILABLE=true
+if $DOCKER_AVAILABLE && ! command -v docker-compose &> /dev/null && ! docker compose version &>/dev/null; then
+ echo "โ ๏ธ Docker Compose is not installed. Some features may not work correctly."
+ COMPOSE_AVAILABLE=false
+fi
+
+# Start Docker services if available
+if $DOCKER_AVAILABLE && $COMPOSE_AVAILABLE; then
+ echo "๐ณ Starting Docker services..."
+ if docker compose version &>/dev/null; then
+ if ! docker compose up -d; then
+ handle_error "Failed to start Docker services with docker compose. You may need to run them manually."
+ fi
+ elif command -v docker-compose &>/dev/null; then
+ if ! docker-compose up -d; then
+ handle_error "Failed to start Docker services with docker-compose. You may need to run them manually."
+ fi
+ fi
+fi
+
+# Generate CSS files
+echo "๐จ Generating CSS..."
+if ! bun run build:css; then
+ handle_error "Failed to generate CSS. You can run 'bun run build:css' manually later."
+fi
+
+# Set up scripts to be executable
+echo "๐ Making scripts executable..."
+if [ -d "./scripts" ]; then
+ find ./scripts -name "*.ts" -exec chmod +x {} \; || echo "โ ๏ธ Failed to make some scripts executable."
+ find ./scripts -name "*.sh" -exec chmod +x {} \; || echo "โ ๏ธ Failed to make some scripts executable."
+else
+ mkdir -p ./scripts
+ echo "๐ Created scripts directory."
+fi
+
+# Run any TypeScript setup scripts
+if [ -f scripts/setup.ts ]; then
+ echo "๐ Running TypeScript setup script..."
+ if ! bun scripts/setup.ts "$@"; then
+ handle_error "Failed to run TypeScript setup script. You may need to run it manually."
+ fi
+fi
+
+# Create any required directories
+echo "๐ Creating required directories..."
+mkdir -p public/styles || handle_error "Failed to create public/styles directory."
+
+# Final summary
+echo ""
+echo "โ
Setup complete! Here are some useful commands:"
+echo " โข bun dev - Start the development server"
+echo " โข bun run typecheck - Type check the application"
+echo " โข bun run lint - Lint the code"
+echo " โข bun run prettier - Format the code"
+echo " โข bun run test:unit - Run unit tests"
+echo " โข bun precommit - Run all pre-commit checks"
+echo ""
+echo "Happy coding! ๐"
diff --git a/src/__tests__/lib/auth/tokens.test.ts b/src/__tests__/lib/auth/tokens.test.ts
new file mode 100644
index 0000000..d919053
--- /dev/null
+++ b/src/__tests__/lib/auth/tokens.test.ts
@@ -0,0 +1,865 @@
+import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
+import {
+ blacklistToken,
+ generateCsrfToken,
+ generateToken,
+ resetBlacklist,
+ validateTokenPayload,
+ verifyToken,
+} from '@/lib/auth/tokens';
+import {
+ JOSE_ERROR_CODES,
+ TOKEN_TYPES,
+ type TokenPayload,
+ USER_ROLES,
+} from '@/lib/auth/constants';
+
+let mockEnv = (key: string): string => {
+ if (key === 'SECRET_KEY') {
+ return 'test_secret_key_that_is_at_least_32_bytes';
+ }
+ return '';
+};
+
+mock.module('@/utils/env', () => {
+ return {
+ env: (key: string): string => mockEnv(key),
+ };
+});
+
+mock.module('@/middleware/logger', () => {
+ return {
+ logHandler: {
+ debug: (): void => {},
+ info: (): void => {},
+ warn: (): void => {},
+ error: (): void => {},
+ },
+ };
+});
+
+describe('Token Authentication', () => {
+ const validPayload: TokenPayload = {
+ type: 'access',
+ email: 'test@example.com',
+ role: 'user',
+ };
+
+ beforeEach(() => {
+ resetBlacklist();
+ mockEnv = (key: string): string => {
+ if (key === 'SECRET_KEY') {
+ return 'test_secret_key_that_is_at_least_32_bytes';
+ }
+ return '';
+ };
+ });
+
+ afterEach(() => {
+ resetBlacklist();
+ mock.restore();
+ });
+
+ describe('generateToken', () => {
+ test('should generate a valid access token', async () => {
+ const token = await generateToken(validPayload);
+ expect(token).toBeTruthy();
+ expect(typeof token).toBe('string');
+
+ const tokenParts = token.split('.');
+ expect(tokenParts.length).toBe(3);
+
+ expect(() => Buffer.from(tokenParts[0] || '', 'base64')).not.toThrow();
+ expect(() => Buffer.from(tokenParts[1] || '', 'base64')).not.toThrow();
+ expect(() => Buffer.from(tokenParts[2] || '', 'base64')).not.toThrow();
+
+ const headerJson = Buffer.from(tokenParts[0] || '', 'base64').toString(
+ 'utf-8',
+ );
+ const header = JSON.parse(headerJson);
+
+ expect(header).toHaveProperty('alg', 'HS256');
+ expect(header).toHaveProperty('typ', 'JWT');
+
+ const decoded = await verifyToken(token);
+ expect(decoded).toMatchObject(validPayload);
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('iat');
+ expect(decoded.type).toBe('access');
+ expect(Number(decoded['exp']) > Number(decoded['iat'])).toBe(true);
+ });
+
+ test('should generate a valid refresh token', async () => {
+ const refreshPayload = { ...validPayload, type: 'refresh' as const };
+ const token = await generateToken(refreshPayload);
+ expect(token).toBeTruthy();
+ expect(typeof token).toBe('string');
+
+ const decoded = await verifyToken(token);
+ expect(decoded.type).toBe('refresh');
+ expect(decoded.email).toBe(validPayload.email);
+ expect(decoded.role).toBe(validPayload.role);
+ });
+
+ test('should generate a valid magic token', async () => {
+ const magicPayload = { ...validPayload, type: 'magic' as const };
+ const token = await generateToken(magicPayload);
+ expect(token).toBeTruthy();
+ expect(typeof token).toBe('string');
+
+ const decoded = await verifyToken(token);
+ expect(decoded.type).toBe('magic');
+ expect(decoded.email).toBe(validPayload.email);
+ });
+
+ test('should generate a valid csrf token', async () => {
+ const csrfPayload = { ...validPayload, type: 'csrf' as const };
+ const token = await generateToken(csrfPayload);
+ expect(token).toBeTruthy();
+ expect(typeof token).toBe('string');
+
+ const decoded = await verifyToken(token);
+ expect(decoded.type).toBe('csrf');
+ });
+
+ test('should throw error for invalid token type', async () => {
+ const invalidPayload = { ...validPayload, type: 'invalid' as any };
+ await expect(generateToken(invalidPayload)).rejects.toThrow(
+ 'Non-standard token type',
+ );
+ });
+
+ test('should generate tokens with different signatures for same payload', async () => {
+ const payload1 = { ...validPayload, email: 'test1@example.com' };
+ const payload2 = { ...validPayload, email: 'test2@example.com' };
+
+ const token1 = await generateToken(payload1);
+ const token2 = await generateToken(payload2);
+
+ expect(token1).not.toBe(token2);
+
+ const parts1 = token1.split('.');
+ const parts2 = token2.split('.');
+
+ expect(parts1[1]).not.toBe(parts2[1]);
+ expect(parts1[2]).not.toBe(parts2[2]);
+ });
+ });
+
+ describe('generateCsrfToken', () => {
+ test('should generate a valid CSRF token', async () => {
+ const token = await generateCsrfToken();
+ expect(token).toBeTruthy();
+ expect(typeof token).toBe('string');
+
+ const verified = await verifyToken(token);
+ expect(verified.type).toBe('csrf');
+ expect(verified.email).toBe('csrf@example.com');
+ });
+
+ test('should generate a CSRF token with correct structure', async () => {
+ const token = await generateCsrfToken();
+ const parts = token.split('.');
+
+ expect(parts.length).toBe(3);
+
+ const headerJson = Buffer.from(parts[0] || '', 'base64').toString(
+ 'utf-8',
+ );
+ const header = JSON.parse(headerJson);
+
+ expect(header).toHaveProperty('alg', 'HS256');
+ expect(header).toHaveProperty('typ', 'JWT');
+
+ const payloadJson = Buffer.from(parts[1] || '', 'base64').toString(
+ 'utf-8',
+ );
+ const payload = JSON.parse(payloadJson);
+
+ expect(payload).toHaveProperty('type', 'csrf');
+ expect(payload).toHaveProperty('email', 'csrf@example.com');
+ expect(payload).toHaveProperty('role', 'user');
+ expect(payload).toHaveProperty('exp');
+ expect(payload).toHaveProperty('iat');
+ });
+ });
+
+ describe('verifyToken', () => {
+ test('should verify a valid token', async () => {
+ const token = await generateToken(validPayload);
+ const decoded = await verifyToken(token);
+
+ expect(decoded).toMatchObject(validPayload);
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('iat');
+
+ const now = Math.floor(Date.now() / 1000);
+ expect(Number(decoded['iat'])).toBeLessThanOrEqual(now);
+ expect(Number(decoded['exp'])).toBeGreaterThan(now);
+
+ expect(decoded.email).toBe(validPayload.email);
+ expect(decoded.role).toBe(validPayload.role);
+ expect(decoded.type).toBe(validPayload.type);
+ });
+
+ test('should throw error for empty token', async () => {
+ await expect(verifyToken('')).rejects.toThrow('Token is empty');
+ });
+
+ test('should throw error for blacklisted token', async () => {
+ const token = await generateToken(validPayload);
+ blacklistToken(token);
+
+ await expect(verifyToken(token)).rejects.toThrow();
+ await expect(verifyToken(token)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.REVOKED,
+ );
+ });
+
+ test('should throw error for tampered token', async () => {
+ const token = await generateToken(validPayload);
+
+ const tamperedSignature = token.slice(0, -5) + 'xxxxx';
+ await expect(verifyToken(tamperedSignature)).rejects.toThrow();
+
+ const malformedToken = token.substring(0, token.lastIndexOf('.'));
+ await expect(verifyToken(malformedToken)).rejects.toThrow();
+
+ await expect(verifyToken('not.a.token')).rejects.toThrow();
+
+ const fakeToken =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
+ await expect(verifyToken(fakeToken)).rejects.toThrow();
+ });
+
+ test('should throw error for token with tampered payload', async () => {
+ const token = await generateToken(validPayload);
+ const parts = token.split('.');
+
+ const payloadJson = Buffer.from(parts[1] || '', 'base64').toString(
+ 'utf-8',
+ );
+ const payload = JSON.parse(payloadJson);
+ payload.role = 'admin';
+
+ const tamperedPayload = Buffer.from(JSON.stringify(payload)).toString(
+ 'base64',
+ );
+ const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
+
+ await expect(verifyToken(tamperedToken)).rejects.toThrow();
+ });
+
+ test('should throw error for expired token', async () => {
+ const expiredPayload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) - 3600,
+ iat: Math.floor(Date.now() / 1000) - 7200,
+ };
+
+ await expect(validateTokenPayload(expiredPayload)).rejects.toThrow(
+ 'Token has expired',
+ );
+ await expect(validateTokenPayload(expiredPayload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.EXPIRED,
+ );
+ });
+ });
+
+ describe('validateTokenPayload', () => {
+ test('should validate a correct payload', async () => {
+ const token = await generateToken(validPayload);
+ const decoded = await verifyToken(token);
+
+ await validateTokenPayload(decoded);
+
+ expect(decoded).toHaveProperty('type');
+ expect(decoded).toHaveProperty('email');
+ expect(decoded).toHaveProperty('role');
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('iat');
+ });
+
+ test('should reject payload with invalid token type', async () => {
+ const invalidPayload = { ...validPayload, type: 'invalid' as any };
+
+ await expect(validateTokenPayload(invalidPayload)).rejects.toThrow(
+ 'Invalid token type',
+ );
+ await expect(validateTokenPayload(invalidPayload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should reject payload with invalid role', async () => {
+ const invalidPayload = { ...validPayload, role: 'superuser' as any };
+
+ await expect(validateTokenPayload(invalidPayload)).rejects.toThrow(
+ 'Invalid role',
+ );
+ await expect(validateTokenPayload(invalidPayload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should reject payload with invalid email', async () => {
+ const invalidPayload = { ...validPayload, email: 'not-an-email' };
+
+ await expect(validateTokenPayload(invalidPayload)).rejects.toThrow(
+ 'Invalid email format',
+ );
+ await expect(validateTokenPayload(invalidPayload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should reject payload with various malformed emails', async () => {
+ const malformedEmails = [
+ 'plaintext',
+ '@missinguser.com',
+ 'missing@',
+ '',
+ 'no-at-sign',
+ 'multiple@at@signs.com',
+ 'user@',
+ ];
+
+ for (const email of malformedEmails) {
+ const invalidPayload = {
+ ...validPayload,
+ email,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+ await expect(validateTokenPayload(invalidPayload)).rejects.toThrow(
+ 'Invalid email format',
+ );
+ }
+ });
+
+ test('should allow csrf token with non-email format', async () => {
+ const csrfPayload = {
+ type: 'csrf' as const,
+ email: 'csrf@example.com',
+ role: 'user' as const,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await validateTokenPayload(csrfPayload);
+
+ const csrfToken = await generateCsrfToken();
+ const decoded = await verifyToken(csrfToken);
+
+ expect(decoded.type).toBe('csrf');
+ expect(decoded.email).toBe('csrf@example.com');
+ });
+
+ test('should reject payload with missing required claims', async () => {
+ const incompletePayload = { ...validPayload };
+
+ await expect(validateTokenPayload(incompletePayload)).rejects.toThrow(
+ 'Missing required claims',
+ );
+ await expect(
+ validateTokenPayload(incompletePayload),
+ ).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should reject payload with expired token', async () => {
+ const expiredPayload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) - 3600,
+ iat: Math.floor(Date.now() / 1000) - 7200,
+ };
+
+ await expect(validateTokenPayload(expiredPayload)).rejects.toThrow(
+ 'Token has expired',
+ );
+ await expect(validateTokenPayload(expiredPayload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.EXPIRED,
+ );
+ });
+
+ test('should reject payload with future issuance date', async () => {
+ const futurePayload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) + 7200,
+ iat: Math.floor(Date.now() / 1000) + 3600,
+ };
+
+ await expect(validateTokenPayload(futurePayload)).rejects.toThrow(
+ 'Token not yet valid',
+ );
+ await expect(validateTokenPayload(futurePayload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should handle clock skew between servers gracefully', async () => {
+ const payload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000) + 5,
+ };
+
+ await expect(validateTokenPayload(payload)).rejects.toThrow(
+ 'Token not yet valid',
+ );
+ await expect(validateTokenPayload(payload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test.skip('should implement secure clock skew tolerance', async () => {
+ const minimalSkew = 2;
+ const payload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000) + minimalSkew,
+ };
+
+ await validateTokenPayload(payload);
+
+ const largeSkew = 10 * 60;
+ const badPayload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000) + largeSkew,
+ };
+
+ await expect(validateTokenPayload(badPayload)).rejects.toThrow(
+ 'Token not yet valid',
+ );
+ });
+ });
+
+ describe('blacklistToken and resetBlacklist', () => {
+ test('should blacklist a token and prevent its verification', async () => {
+ const token = await generateToken(validPayload);
+
+ const decoded = await verifyToken(token);
+ expect(decoded).toBeTruthy();
+ expect(decoded.email).toBe(validPayload.email);
+
+ blacklistToken(token);
+
+ await expect(verifyToken(token)).rejects.toThrow();
+ await expect(verifyToken(token)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.REVOKED,
+ );
+
+ const otherToken = await generateToken({
+ ...validPayload,
+ email: 'other@example.com',
+ });
+ const otherDecoded = await verifyToken(otherToken);
+ expect(otherDecoded).toBeTruthy();
+ expect(otherDecoded.email).toBe('other@example.com');
+ });
+
+ test('should reset the blacklist', async () => {
+ const token = await generateToken(validPayload);
+
+ blacklistToken(token);
+
+ await expect(verifyToken(token)).rejects.toThrow();
+
+ resetBlacklist();
+
+ const decoded = await verifyToken(token);
+ expect(decoded).toBeTruthy();
+ });
+
+ test('should correctly blacklist multiple tokens', async () => {
+ const token1 = await generateToken({
+ ...validPayload,
+ email: 'user1@example.com',
+ });
+ const token2 = await generateToken({
+ ...validPayload,
+ email: 'user2@example.com',
+ });
+ const token3 = await generateToken({
+ ...validPayload,
+ email: 'user3@example.com',
+ });
+
+ blacklistToken(token1);
+ blacklistToken(token2);
+
+ await expect(verifyToken(token1)).rejects.toThrow();
+ await expect(verifyToken(token2)).rejects.toThrow();
+
+ const decoded3 = await verifyToken(token3);
+ expect(decoded3.email).toBe('user3@example.com');
+
+ blacklistToken(token3);
+ await expect(verifyToken(token3)).rejects.toThrow();
+
+ resetBlacklist();
+
+ const decoded1 = await verifyToken(token1);
+ const decoded2 = await verifyToken(token2);
+ const decoded3again = await verifyToken(token3);
+
+ expect(decoded1.email).toBe('user1@example.com');
+ expect(decoded2.email).toBe('user2@example.com');
+ expect(decoded3again.email).toBe('user3@example.com');
+ });
+ });
+
+ describe('Token types and roles validation', () => {
+ test('should validate all defined token types', async () => {
+ for (const type of TOKEN_TYPES) {
+ const payload = {
+ ...validPayload,
+ type,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await validateTokenPayload(payload);
+
+ const token = await generateToken({ ...validPayload, type });
+ const decoded = await verifyToken(token);
+
+ expect(decoded.type).toBe(type);
+ expect(decoded.email).toBe(validPayload.email);
+ expect(decoded.role).toBe(validPayload.role);
+ }
+ });
+
+ test('should validate all defined user roles', async () => {
+ for (const role of USER_ROLES) {
+ const payload = {
+ ...validPayload,
+ role,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await validateTokenPayload(payload);
+
+ const token = await generateToken({ ...validPayload, role });
+ const decoded = await verifyToken(token);
+
+ expect(decoded.role).toBe(role);
+ expect(USER_ROLES).toContain(decoded.role);
+ }
+ });
+
+ test('should throw error for invalid role', async () => {
+ const invalidRole = 'super_admin';
+ const payload = {
+ ...validPayload,
+ role: invalidRole as any,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await expect(validateTokenPayload(payload)).rejects.toThrow(
+ 'Invalid role',
+ );
+ await expect(validateTokenPayload(payload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+ });
+
+ describe('Security edge cases', () => {
+ test('should reject token with empty email', async () => {
+ const payload = {
+ ...validPayload,
+ email: '',
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await expect(validateTokenPayload(payload)).rejects.toThrow(
+ 'Invalid email format',
+ );
+ await expect(validateTokenPayload(payload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should reject token with SQL injection attempt in email', async () => {
+ const payload = {
+ ...validPayload,
+ email: "user@example.com' OR 1=1 --",
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await expect(validateTokenPayload(payload)).rejects.toThrow(
+ 'Invalid email format',
+ );
+ await expect(validateTokenPayload(payload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test.skip('should prevent privilege escalation through token manipulation', async () => {
+ const adminPayload = {
+ ...validPayload,
+ role: 'admin' as const,
+ };
+
+ const createAdminToken = async (): Promise => {
+ const adminToken = await generateToken(adminPayload);
+ return await verifyToken(adminToken);
+ };
+
+ await expect(createAdminToken()).rejects.toThrow(
+ 'Unauthorized role escalation',
+ );
+ });
+
+ test('should reject token with null bytes in claims', async () => {
+ const payload = {
+ ...validPayload,
+ email: 'user\0@example.com',
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ await expect(validateTokenPayload(payload)).rejects.toThrow(
+ 'Invalid email format',
+ );
+ await expect(validateTokenPayload(payload)).rejects.toHaveProperty(
+ 'code',
+ JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ );
+ });
+
+ test('should match token expiration with constants definition', async () => {
+ const accessToken = await generateToken({
+ ...validPayload,
+ type: 'access',
+ });
+ const accessDecoded = await verifyToken(accessToken);
+ const now = Math.floor(Date.now() / 1000);
+
+ const fifteenMinutesInSeconds = 15 * 60;
+ const accessExpiry = Number(accessDecoded['exp']) - now;
+ expect(accessExpiry).toBeGreaterThan(fifteenMinutesInSeconds - 10);
+ expect(accessExpiry).toBeLessThan(fifteenMinutesInSeconds + 10);
+
+ const refreshToken = await generateToken({
+ ...validPayload,
+ type: 'refresh',
+ });
+ const refreshDecoded = await verifyToken(refreshToken);
+
+ const oneWeekInSeconds = 7 * 24 * 60 * 60;
+ const refreshExpiry = Number(refreshDecoded['exp']) - now;
+ expect(refreshExpiry).toBeGreaterThan(oneWeekInSeconds - 10);
+ expect(refreshExpiry).toBeLessThan(oneWeekInSeconds + 10);
+
+ const magicToken = await generateToken({
+ ...validPayload,
+ type: 'magic',
+ });
+ const magicDecoded = await verifyToken(magicToken);
+ const magicExpiry = Number(magicDecoded['exp']) - now;
+ expect(magicExpiry).toBeGreaterThan(fifteenMinutesInSeconds - 10);
+ expect(magicExpiry).toBeLessThan(fifteenMinutesInSeconds + 10);
+
+ const csrfToken = await generateToken({ ...validPayload, type: 'csrf' });
+ const csrfDecoded = await verifyToken(csrfToken);
+ const oneHourInSeconds = 60 * 60;
+ const csrfExpiry = Number(csrfDecoded['exp']) - now;
+ expect(csrfExpiry).toBeGreaterThan(oneHourInSeconds - 10);
+ expect(csrfExpiry).toBeLessThan(oneHourInSeconds + 10);
+ });
+
+ test('should ignore custom expiration times in payload', async () => {
+ const oneYearInSeconds = 365 * 24 * 60 * 60;
+ const payload = {
+ ...validPayload,
+ exp: Math.floor(Date.now() / 1000) + oneYearInSeconds,
+ iat: Math.floor(Date.now() / 1000),
+ };
+
+ const token = await generateToken(payload);
+ const decoded = await verifyToken(token);
+
+ const now = Math.floor(Date.now() / 1000);
+
+ const fifteenMinutesInSeconds = 15 * 60;
+ const accessExpiry = Number(decoded['exp']) - now;
+
+ expect(accessExpiry).toBeLessThan(oneYearInSeconds / 100);
+ expect(accessExpiry).toBeCloseTo(fifteenMinutesInSeconds, -1);
+ });
+
+ test('should enforce maximum expiration times for security', async () => {
+ const maxAllowedExpiry = 30 * 24 * 60 * 60;
+
+ const longExpiryPayload = {
+ ...validPayload,
+ type: 'refresh' as const,
+ };
+
+ const token = await generateToken(longExpiryPayload);
+ const decoded = await verifyToken(token);
+ const now = Math.floor(Date.now() / 1000);
+
+ expect(Number(decoded['exp']) - now).toBeLessThan(maxAllowedExpiry);
+ });
+
+ test('should properly validate different token types with correct expiry times', async () => {
+ const testCases = [
+ { type: 'access', expectedExpiry: 15 * 60 },
+ { type: 'refresh', expectedExpiry: 7 * 24 * 60 * 60 },
+ { type: 'magic', expectedExpiry: 15 * 60 },
+ { type: 'csrf', expectedExpiry: 60 * 60 },
+ ];
+
+ for (const { type, expectedExpiry } of testCases) {
+ const payload = { ...validPayload, type: type as any };
+ const token = await generateToken(payload);
+ const decoded = await verifyToken(token);
+
+ const now = Math.floor(Date.now() / 1000);
+ const actualExpiry = Number(decoded['exp']) - now;
+
+ expect(actualExpiry).toBeGreaterThanOrEqual(expectedExpiry - 10);
+ expect(actualExpiry).toBeLessThanOrEqual(expectedExpiry + 10);
+
+ expect(actualExpiry).toBeGreaterThan(expectedExpiry * 0.9);
+
+ expect(actualExpiry).toBeLessThan(expectedExpiry * 1.1);
+ }
+ });
+
+ test.skip('should prevent extending token lifetime via refresh chain', async () => {
+ const refreshPayload = {
+ ...validPayload,
+ type: 'refresh' as const,
+ };
+
+ const refreshToken = await generateToken(refreshPayload);
+
+ const decoded = await verifyToken(refreshToken);
+
+ expect(decoded).toHaveProperty('refreshCount');
+ });
+
+ test.skip('should prevent token replay attacks', async () => {
+ const token = await generateToken(validPayload);
+
+ await verifyToken(token);
+
+ const decoded = await verifyToken(token);
+
+ expect(decoded).toHaveProperty('jti');
+
+ blacklistToken(token);
+ await expect(verifyToken(token)).rejects.toThrow();
+
+ resetBlacklist();
+ });
+
+ test('should validate token structure for all token types', async () => {
+ for (const type of TOKEN_TYPES) {
+ const typePayload = { ...validPayload, type: type as any };
+ const token = await generateToken(typePayload);
+
+ const parts = token.split('.');
+ expect(parts.length).toBe(3);
+
+ const headerJson = Buffer.from(parts[0] || '', 'base64').toString(
+ 'utf-8',
+ );
+ const header = JSON.parse(headerJson);
+
+ expect(header).toHaveProperty('alg', 'HS256');
+ expect(header).toHaveProperty('typ', 'JWT');
+
+ const payloadJson = Buffer.from(parts[1] || '', 'base64').toString(
+ 'utf-8',
+ );
+ const payload = JSON.parse(payloadJson);
+
+ expect(payload).toHaveProperty('type', type);
+ expect(payload).toHaveProperty('email');
+ expect(payload).toHaveProperty('role');
+ expect(payload).toHaveProperty('exp');
+ expect(payload).toHaveProperty('iat');
+
+ expect(parts[2]?.length || 0).toBeGreaterThan(10);
+ }
+ });
+
+ test('should properly enforce maximum token lifetimes', async () => {
+ const tokenLifetimeTests = [
+ { type: 'access', maxLifetime: 60 * 60 },
+ { type: 'refresh', maxLifetime: 30 * 24 * 60 * 60 },
+ { type: 'magic', maxLifetime: 60 * 60 },
+ { type: 'csrf', maxLifetime: 24 * 60 * 60 },
+ ];
+
+ for (const { type, maxLifetime } of tokenLifetimeTests) {
+ const typePayload = { ...validPayload, type: type as any };
+ const token = await generateToken(typePayload);
+ const decoded = await verifyToken(token);
+
+ const now = Math.floor(Date.now() / 1000);
+ const expiration = Number(decoded['exp']);
+ const lifetime = expiration - now;
+
+ expect(lifetime).toBeLessThanOrEqual(maxLifetime);
+ }
+ });
+
+ test('should detect common JWT tampering techniques', async () => {
+ const token = await generateToken(validPayload);
+
+ const parts = token.split('.');
+ expect(parts.length).toBe(3);
+ const [headerB64, payloadB64, signatureB64] = parts;
+
+ await expect(verifyToken('not-a-jwt-token')).rejects.toThrow();
+
+ await expect(verifyToken(`${headerB64}.${payloadB64}`)).rejects.toThrow();
+ await expect(verifyToken(`${headerB64}`)).rejects.toThrow();
+
+ const headerStr = Buffer.from(headerB64 || '', 'base64').toString(
+ 'utf-8',
+ );
+ const headerObj = JSON.parse(headerStr);
+
+ headerObj.alg = 'none';
+ const tamperedHeaderB64 = Buffer.from(JSON.stringify(headerObj)).toString(
+ 'base64',
+ );
+
+ const noneAlgToken = `${tamperedHeaderB64}.${payloadB64}.${signatureB64}`;
+ await expect(verifyToken(noneAlgToken)).rejects.toThrow();
+
+ headerObj.alg = 'HS512';
+ const differentAlgHeaderB64 = Buffer.from(
+ JSON.stringify(headerObj),
+ ).toString('base64');
+ const differentAlgToken = `${differentAlgHeaderB64}.${payloadB64}.${signatureB64}`;
+ await expect(verifyToken(differentAlgToken)).rejects.toThrow();
+ });
+ });
+});
diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx
new file mode 100644
index 0000000..7d40b7e
--- /dev/null
+++ b/src/components/Alert.tsx
@@ -0,0 +1,53 @@
+import { type Child, type FC } from 'hono/jsx';
+
+type AlertType = 'default' | 'success' | 'warning' | 'error' | 'info';
+
+type AlertProps = {
+ title: string;
+ description?: string;
+ type?: AlertType;
+ action?: Child;
+};
+
+const Alert: FC = ({
+ title,
+ description,
+ type = 'default',
+ action,
+}) => {
+ const typeStyles = {
+ default: 'bg-app-background-accent border-border-primary border-opacity-40',
+ success: 'bg-app-background-accent border-status-success border-opacity-40',
+ warning: 'bg-app-background-accent border-status-warning border-opacity-40',
+ error: 'bg-app-background-accent border-status-error border-opacity-40',
+ info: 'bg-app-background-accent border-status-info border-opacity-40',
+ };
+
+ const titleStyles = {
+ default: 'font-medium text-text-primary font-primary',
+ success: 'font-medium text-status-success font-primary',
+ warning: 'font-medium text-status-warning font-primary',
+ error: 'font-medium text-status-error font-primary',
+ info: 'font-medium text-status-info font-primary',
+ };
+
+ return (
+
+
+
+ {'>'} {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {action}
+
+ );
+};
+
+export default Alert;
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index c869de1..ebaa712 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,16 +1,76 @@
-type Props = {
- class?: string;
- children: any;
- [string: string]: any;
-};
+import type { Child, FC } from 'hono/jsx';
+
+interface ButtonProps {
+ children: Child;
+ hxGet?: string;
+ hxPost?: string;
+ hxPut?: string;
+ hxDelete?: string;
+ hxTarget?: string;
+ hxSwap?: string;
+ hxConfirm?: string;
+ hxInclude?: string;
+ hxIndicator?: string;
+ hxTrigger?: string;
+ variant?: 'primary' | 'secondary' | 'danger';
+ size?: 'sm' | 'md' | 'lg';
+ className?: string;
+ disabled?: boolean;
+ type?: 'button' | 'submit' | 'reset';
+}
+
+const Button: FC = ({
+ children,
+ hxGet,
+ hxPost,
+ hxPut,
+ hxDelete,
+ hxTarget,
+ hxSwap,
+ hxConfirm,
+ hxInclude,
+ hxIndicator,
+ hxTrigger,
+ variant = 'primary',
+ className = '',
+ disabled = false,
+ type = 'button',
+}) => {
+ const baseClasses =
+ 'rounded-lg font-medium transition-all duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 px-4 py-2 font-primary border border-opacity-60';
+
+ const variantClasses = {
+ primary:
+ 'border-interactive-primary bg-app-background text-text-primary shadow-md hover:bg-app-surface-hover hover:text-text-primary hover:border-opacity-80 focus:ring-2 focus:ring-interactive-primary/30 focus:outline-none',
+ secondary:
+ 'border-border-subtle bg-app-background text-text-secondary shadow-md hover:bg-app-surface-hover hover:text-text-primary hover:border-border-secondary focus:ring-2 focus:ring-border-primary/20 focus:outline-none',
+ danger:
+ 'border-interactive-danger bg-app-background text-status-error shadow-md hover:bg-status-error/8 hover:text-status-error hover:border-opacity-80 focus:ring-2 focus:ring-interactive-danger/30 focus:outline-none',
+ };
+
+ const hxAttrs = {
+ ...(hxGet && { 'hx-get': hxGet }),
+ ...(hxPost && { 'hx-post': hxPost }),
+ ...(hxPut && { 'hx-put': hxPut }),
+ ...(hxDelete && { 'hx-delete': hxDelete }),
+ ...(hxTarget && { 'hx-target': hxTarget }),
+ ...(hxSwap && { 'hx-swap': hxSwap }),
+ ...(hxConfirm && { 'hx-confirm': hxConfirm }),
+ ...(hxInclude && { 'hx-include': hxInclude }),
+ ...(hxIndicator && { 'hx-indicator': hxIndicator }),
+ ...(hxTrigger && { 'hx-trigger': hxTrigger }),
+ };
-export default function Button({ class: className, children, ...rest }: Props) {
return (
{children}
);
-}
+};
+
+export default Button;
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
new file mode 100644
index 0000000..6658748
--- /dev/null
+++ b/src/components/Card.tsx
@@ -0,0 +1,45 @@
+import type { Child, FC } from 'hono/jsx';
+
+interface CardProps {
+ children: Child;
+ title?: string;
+ titleSize?: string;
+ action?: Child;
+ className?: string;
+ variant?: 'default' | 'section';
+}
+
+const Card: FC = ({
+ children,
+ title,
+ titleSize = 'text-sm',
+ action,
+ className = '',
+ variant = 'default',
+}) => {
+ const baseStyles = 'rounded-lg border border-opacity-50';
+ const variantStyles =
+ variant === 'section'
+ ? 'bg-app-background-accent p-6 border-border-primary'
+ : 'bg-app-background-alt p-4 opacity-90 shadow-md backdrop-blur-lg border-border-subtle';
+
+ return (
+
+ {(title || action) && (
+
+ {title && (
+
+ {'>'} {title}
+
+ )}
+ {action}
+
+ )}
+
{children}
+
+ );
+};
+
+export default Card;
diff --git a/src/components/CheckboxGroup.tsx b/src/components/CheckboxGroup.tsx
new file mode 100644
index 0000000..6fa8cd6
--- /dev/null
+++ b/src/components/CheckboxGroup.tsx
@@ -0,0 +1,42 @@
+import { type FC } from 'hono/jsx';
+
+type Option = {
+ value: string;
+ label: string;
+ checked?: boolean;
+};
+
+type Props = {
+ label: string;
+ name: string;
+ options: Option[];
+ onChange?: (value: string, checked: boolean) => void;
+};
+
+const CheckboxGroup: FC = ({ label, name, options, onChange }) => {
+ return (
+
+
{label}
+
+ {options.map((option) => (
+
+ {
+ const target = e.target as HTMLInputElement;
+ onChange?.(option.value, target.checked);
+ }}
+ />
+ {option.label}
+
+ ))}
+
+
+ );
+};
+
+export default CheckboxGroup;
diff --git a/src/components/DangerZone.tsx b/src/components/DangerZone.tsx
new file mode 100644
index 0000000..377173a
--- /dev/null
+++ b/src/components/DangerZone.tsx
@@ -0,0 +1,32 @@
+import type { Child, FC } from 'hono/jsx';
+
+type DangerZoneProps = {
+ title: string;
+ description: string;
+ action: Child;
+};
+
+const DangerZone: FC = ({ title, description, action }) => {
+ return (
+
+
+ Danger Zone
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+ {action}
+
+
+
+ );
+};
+
+export default DangerZone;
diff --git a/src/components/Expandable.tsx b/src/components/Expandable.tsx
new file mode 100644
index 0000000..3053d8a
--- /dev/null
+++ b/src/components/Expandable.tsx
@@ -0,0 +1,34 @@
+import type { Child, FC } from 'hono/jsx';
+
+type ExpandableProps = {
+ trigger: Child;
+ children: Child;
+ defaultExpanded?: boolean;
+};
+
+const Expandable: FC = ({
+ trigger,
+ children,
+ defaultExpanded = false,
+}) => {
+ const uniqueId = `expandable-${Math.random().toString(36).substring(2, 11)}`;
+
+ return (
+
+
+ {trigger}
+
+
+ {children}
+
+
+ );
+};
+
+export default Expandable;
diff --git a/src/components/ExpandablePanel.tsx b/src/components/ExpandablePanel.tsx
new file mode 100644
index 0000000..cf6713d
--- /dev/null
+++ b/src/components/ExpandablePanel.tsx
@@ -0,0 +1,48 @@
+import type { Child, FC } from 'hono/jsx';
+import Button from '@/components/Button';
+import Expandable from '@/components/Expandable';
+
+type ExpandablePanelProps = {
+ title: string;
+ defaultExpanded?: boolean;
+ children: Child;
+};
+
+const ExpandablePanel: FC = ({
+ title,
+ defaultExpanded = true,
+ children,
+}) => {
+ return (
+
+ {title}
+
+
+
+
+
+
+ }
+ >
+ {children}
+
+ );
+};
+
+export default ExpandablePanel;
diff --git a/src/components/FieldRow.tsx b/src/components/FieldRow.tsx
new file mode 100644
index 0000000..d5fbf32
--- /dev/null
+++ b/src/components/FieldRow.tsx
@@ -0,0 +1,33 @@
+import type { Child, FC } from 'hono/jsx';
+
+type FieldRowProps = {
+ title: string;
+ description?: string;
+ children: Child;
+ className?: string;
+};
+
+const FieldRow: FC = ({
+ title,
+ description,
+ children,
+ className = '',
+}) => {
+ return (
+
+
+
{title}
+ {description && (
+
+ {description}
+
+ )}
+
+ {children}
+
+ );
+};
+
+export default FieldRow;
diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx
new file mode 100644
index 0000000..36e59d3
--- /dev/null
+++ b/src/components/FormField.tsx
@@ -0,0 +1,68 @@
+import { type FC } from 'hono/jsx';
+
+type BaseProps = {
+ label: string;
+ name: string;
+ required?: boolean;
+};
+
+type InputProps = BaseProps & {
+ type: 'text' | 'email' | 'password' | 'number';
+ placeholder?: string;
+ value?: string | number;
+ disabled?: boolean;
+};
+
+type TextareaProps = BaseProps & {
+ type: 'textarea';
+ rows?: number;
+ placeholder?: string;
+ value?: string;
+ disabled?: boolean;
+};
+
+type Props = InputProps | TextareaProps;
+
+const baseClasses =
+ 'w-full rounded-lg border border-border-primary border-opacity-60 bg-app-background px-4 py-3 text-base text-text-primary placeholder-text-secondary backdrop-blur-sm transition-all duration-300 hover:border-border-accent hover:border-opacity-80 focus:border-border-accent focus:bg-app-background-accent focus:outline-none focus:ring-2 focus:ring-border-primary/30 disabled:cursor-not-allowed disabled:opacity-50 font-primary';
+
+const FormField: FC = (props) => {
+ const normalizeValue = (value: string | number | undefined): string => {
+ if (value === undefined || value === null) {
+ return '';
+ }
+ return value.toString();
+ };
+
+ return (
+
+
+ {props.label}
+
+ {props.type === 'textarea' ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default FormField;
diff --git a/src/components/FormattedText.tsx b/src/components/FormattedText.tsx
new file mode 100644
index 0000000..4da86a1
--- /dev/null
+++ b/src/components/FormattedText.tsx
@@ -0,0 +1,12 @@
+import type { FC } from 'hono/jsx';
+
+export const FormattedText: FC<{ text: string; className?: string }> = ({
+ text,
+ className = '',
+}) => {
+ return (
+
+ );
+};
+
+export default FormattedText;
diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx
new file mode 100644
index 0000000..c51d5f1
--- /dev/null
+++ b/src/components/IconButton.tsx
@@ -0,0 +1,62 @@
+import type { FC } from 'hono/jsx';
+
+interface IconButtonProps {
+ onClick?: string;
+ title?: string;
+ className?: string;
+ children: any;
+ hxGet?: string;
+ hxTarget?: string;
+ hxSwap?: string;
+}
+
+const IconButton: FC = ({
+ onClick,
+ title,
+ className = '',
+ children,
+ hxGet,
+ hxTarget,
+ hxSwap,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const FullscreenButton: FC<{ className?: string }> = ({
+ className = '',
+}) => (
+
+
+
+
+
+);
+
+export default IconButton;
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index 7f38a07..7274eb1 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -1,15 +1,40 @@
-export default function Input({ class: className, placeholder, ...rest }) {
+import type { FC } from 'hono/jsx';
+
+type InputProps = {
+ type?: 'text' | 'email' | 'password';
+ name: string;
+ placeholder?: string;
+ value?: string;
+ className?: string;
+ required?: boolean;
+ disabled?: boolean;
+ id?: string;
+};
+
+const Input: FC = ({
+ type = 'text',
+ name,
+ placeholder,
+ value,
+ className = '',
+ required = false,
+ disabled = false,
+ id,
+ ...props
+}) => {
return (
);
-}
+};
+
+export default Input;
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 9a684d8..f44740a 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,38 +1,118 @@
-import EnableDarkMode from "../util/EnableDarkMode";
+import type { Child, FC } from 'hono/jsx';
+import type { Context } from 'hono';
+import Navigation from '@/components/Navigation';
+import Toast from '@/components/Toast';
+import { getUserFromContext, isAuthenticated } from '@/lib/auth/session';
+import { logHandler } from '@/middleware/logger';
+import { raw } from 'hono/html';
+import { createRawInlineScript } from '@/utils/script-loader';
type LayoutProps = {
title: string;
currentPath?: string;
- children: any;
+ children: Child;
+ c: Context;
+ notification?: {
+ type: 'info' | 'success' | 'warning' | 'error';
+ message: string;
+ };
+ userEmail?: string;
+ justLoggedIn?: boolean;
+ spacing?: 'sm' | 'md' | 'lg';
};
-function Layout({ title, children }: LayoutProps) {
+const Layout: FC = ({ title, children, c, spacing = 'lg' }) => {
+ const userEmail = getUserFromContext(c)?.email;
+ const pathname = c.req.path || '/';
+ const isAuthPage = pathname === '/auth';
+ const isLandingPage = false;
+ const showNav = isAuthenticated(c) && !isAuthPage && !isLandingPage;
+ const error = c.req.query('error');
+ const justLoggedIn = c.req.query('justLoggedIn');
+ const successMessage =
+ c.req.query('success') || justLoggedIn
+ ? `Welcome back, ${userEmail}!`
+ : undefined;
+
+ logHandler.debug('ui', 'Rendering layout', {
+ userEmail,
+ error,
+ successMessage,
+ justLoggedIn,
+ });
+
+ const spacingClasses = {
+ sm: 'space-y-4',
+ md: 'space-y-6',
+ lg: 'space-y-8',
+ };
+
return (
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
- {children}
-
-
+ <>
+ {raw('')}
+
+
+
+
+
+
+ {title ? `[${title}] - System` : 'Terminal - System access'}
+
+
+
+
+ {raw(
+ ``,
+ )}
+ {raw(
+ ``,
+ )}
+ {createRawInlineScript(
+ 'htmx-config',
+ 'htmx.config.globalViewTransitions = true',
+ )}
+
+
+ {error ? (
+ Error: Invalid access token - security breach detected'
+ : error === 'expired_token'
+ ? '> Error: Token expired - request new authorization'
+ : error === 'token_revoked'
+ ? '> Error: Token revoked - access denied'
+ : error === 'verification_required'
+ ? '> System: Email verification required'
+ : '> Fatal error: System malfunction detected'
+ }
+ />
+ ) : successMessage ? (
+ Access granted: Welcome ${userEmail}`}
+ />
+ ) : null}
+ {showNav && (
+
+ )}
+
+
+
+
+
+
+ >
);
-}
+};
export default Layout;
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
new file mode 100644
index 0000000..2f0f8a4
--- /dev/null
+++ b/src/components/Link.tsx
@@ -0,0 +1,30 @@
+import type { Child, FC } from 'hono/jsx';
+
+type LinkProps = {
+ href: string;
+ children: Child;
+ external?: boolean;
+ active?: boolean;
+ class?: string;
+};
+
+const Link: FC = ({
+ href,
+ children,
+ external = false,
+ active = false,
+ class: className = '',
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default Link;
diff --git a/src/components/ListGroup.tsx b/src/components/ListGroup.tsx
new file mode 100644
index 0000000..92e8e19
--- /dev/null
+++ b/src/components/ListGroup.tsx
@@ -0,0 +1,18 @@
+import { type Child, type FC } from 'hono/jsx';
+
+type ListGroupProps = {
+ children: Child;
+ spacing?: 'sm' | 'md' | 'lg';
+};
+
+const ListGroup: FC = ({ children, spacing = 'md' }) => {
+ const spacingClasses = {
+ sm: 'space-y-2',
+ md: 'space-y-4',
+ lg: 'space-y-6',
+ };
+
+ return {children}
;
+};
+
+export default ListGroup;
diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx
new file mode 100644
index 0000000..4a9e89e
--- /dev/null
+++ b/src/components/ListItem.tsx
@@ -0,0 +1,21 @@
+import { type Child, type FC } from 'hono/jsx';
+
+type Props = {
+ primaryText: string;
+ secondaryText: string;
+ action?: Child;
+};
+
+const ListItem: FC = ({ primaryText, secondaryText, action }) => {
+ return (
+
+
+
{primaryText}
+
{secondaryText}
+
+ {action &&
{action}
}
+
+ );
+};
+
+export default ListItem;
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
new file mode 100644
index 0000000..e78516c
--- /dev/null
+++ b/src/components/Modal.tsx
@@ -0,0 +1,81 @@
+import type { Child, FC } from 'hono/jsx';
+import ModalHeader from '@/components/ModalHeader';
+import ModalFooter from '@/components/ModalFooter';
+
+export interface Action {
+ label: string;
+ disabled: boolean;
+ 'hx-post'?: string;
+ 'hx-get'?: string;
+ 'hx-target'?: string;
+ 'hx-include'?: string;
+ 'hx-indicator'?: string;
+ 'hx-swap'?: string;
+ 'hx-trigger'?: string;
+ type?: 'button' | 'submit' | 'reset';
+}
+
+export interface ModalProps {
+ title: string;
+ children: Child;
+ onClose?: string;
+ target?: string;
+ showFooter?: boolean;
+ primaryAction?: Action;
+ secondaryAction?: Action;
+ dangerAction?: Action;
+}
+
+const Modal: FC = ({
+ title,
+ children,
+ onClose = '/modal/close',
+ target = '#modal-container',
+ showFooter = true,
+ primaryAction,
+ secondaryAction,
+ dangerAction,
+}) => {
+ const footerProps = {
+ onClose,
+ target,
+ ...(primaryAction ? { primaryAction } : {}),
+ ...(secondaryAction ? { secondaryAction } : {}),
+ ...(dangerAction ? { dangerAction } : {}),
+ };
+
+ return (
+
+
+
+
+
+
{children}
+ {showFooter && (primaryAction || secondaryAction || dangerAction) && (
+
+ )}
+
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/src/components/ModalFooter.tsx b/src/components/ModalFooter.tsx
new file mode 100644
index 0000000..b3bceb2
--- /dev/null
+++ b/src/components/ModalFooter.tsx
@@ -0,0 +1,82 @@
+import type { FC } from 'hono/jsx';
+import Button from '@/components/Button';
+import type { Action } from '@/components/Modal';
+
+interface ModalFooterProps {
+ onClose: string;
+ target: string;
+ primaryAction?: Action;
+ secondaryAction?: Action;
+ dangerAction?: Action;
+}
+
+const ModalFooter: FC = ({
+ onClose,
+ target,
+ primaryAction,
+ secondaryAction,
+ dangerAction,
+}) => {
+ return (
+
+
+ {dangerAction && (
+
+ {dangerAction.label}
+
+ )}
+
+
+ {secondaryAction && (
+
+ {secondaryAction.label}
+
+ )}
+ {primaryAction && (
+
+ {primaryAction.label}
+
+ )}
+
+
+ );
+};
+
+export default ModalFooter;
diff --git a/src/components/ModalHeader.tsx b/src/components/ModalHeader.tsx
new file mode 100644
index 0000000..1f61d44
--- /dev/null
+++ b/src/components/ModalHeader.tsx
@@ -0,0 +1,27 @@
+import type { FC } from 'hono/jsx';
+
+interface ModalHeaderProps {
+ title: string;
+ onClose: string;
+ target: string;
+}
+
+const ModalHeader: FC = ({ title, onClose, target }) => {
+ return (
+
+
+ {'>'} {title}
+
+
+ โ
+
+
+ );
+};
+
+export default ModalHeader;
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
new file mode 100644
index 0000000..47c3e9b
--- /dev/null
+++ b/src/components/Navigation.tsx
@@ -0,0 +1,173 @@
+import type { Child, FC } from 'hono/jsx';
+import Link from '@/components/Link';
+import { logHandler } from '@/middleware/logger';
+
+type NavProps = {
+ userEmail: string | null | undefined;
+ currentPath?: string;
+};
+
+const Navigation: FC = ({ userEmail, currentPath = '/' }) => {
+ logHandler.debug('ui', 'Rendering navigation', { userEmail, currentPath });
+
+ const NavLink = ({
+ href,
+ children,
+ isActive,
+ }: {
+ href: string;
+ children: Child;
+ isActive: boolean;
+ }): ReturnType => {
+ return (
+
+ {children}
+
+
+ );
+ };
+
+ return (
+
+
+
+ {userEmail ? (
+ <>
+
+
+
+
+
+
+
+ ๐
+
+
+ >
+ ) : (
+
+
+ Dashboard
+
+
+ )}
+
+ {userEmail && (
+
+
+ Dashboard
+
+
+ Profile
+
+
+ Settings
+
+
+ )}
+
+
+
+ {userEmail && (
+
+
+ {userEmail[0] ?? 'U'}
+
+
+
+ Logged in as:
+
+
+ {userEmail}
+
+
+
+
{'>'} Profile
+
+
+
{'>'} Settings
+
+
+
{'>'} Logout
+
+
+
+
+ )}
+
+
+
+ {userEmail && (
+
+ )}
+
+ );
+};
+
+export default Navigation;
diff --git a/src/components/PageHeader.tsx b/src/components/PageHeader.tsx
new file mode 100644
index 0000000..1ad0049
--- /dev/null
+++ b/src/components/PageHeader.tsx
@@ -0,0 +1,28 @@
+import { type Child, type FC } from 'hono/jsx';
+
+type PageHeaderProps = {
+ title: string;
+ icon?: string;
+ prefix?: string;
+ action?: Child;
+};
+
+const PageHeader: FC = ({
+ title,
+ icon = '',
+ prefix = '',
+ action,
+}) => {
+ return (
+
+
+ {'>'} {prefix && `[${prefix}] `}
+ {icon && `[${icon}] `}
+ {title}
+
+ {action}
+
+ );
+};
+
+export default PageHeader;
diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx
new file mode 100644
index 0000000..fa6558b
--- /dev/null
+++ b/src/components/ProgressBar.tsx
@@ -0,0 +1,53 @@
+import type { FC } from 'hono/jsx';
+
+const MIN_PERCENTAGE = 0;
+const MAX_PERCENTAGE = 100;
+
+interface ProgressBarProps {
+ label: string;
+ value: string | number;
+ displayValue?: string;
+ color?: 'blue' | 'purple' | 'emerald' | 'amber';
+ labelSize?: string;
+ valueSize?: string;
+ className?: string;
+}
+
+const ProgressBar: FC = ({
+ label,
+ value,
+ color = 'blue',
+ displayValue,
+ labelSize = 'text-sm',
+ valueSize = 'text-sm',
+ className = '',
+}) => {
+ const colorClasses = {
+ blue: 'bg-interactive-primary',
+ purple: 'bg-interactive-secondary',
+ emerald: 'bg-status-success',
+ amber: 'bg-status-warning',
+ };
+
+ const percentage = typeof value === 'number' ? value : parseInt(value) || 0;
+ const width = Math.min(Math.max(percentage, MIN_PERCENTAGE), MAX_PERCENTAGE);
+
+ return (
+
+
{label}
+
+
+ {displayValue || `${percentage}%`}
+
+
+ );
+};
+
+export default ProgressBar;
diff --git a/src/components/Section.tsx b/src/components/Section.tsx
new file mode 100644
index 0000000..fb539d4
--- /dev/null
+++ b/src/components/Section.tsx
@@ -0,0 +1,22 @@
+import type { Child, FC } from 'hono/jsx';
+
+type SectionProps = {
+ title: string;
+ children: Child;
+ className?: string;
+};
+
+const Section: FC = ({ title, children, className = '' }) => {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+};
+
+export default Section;
diff --git a/src/components/Select.tsx b/src/components/Select.tsx
new file mode 100644
index 0000000..229e5c5
--- /dev/null
+++ b/src/components/Select.tsx
@@ -0,0 +1,56 @@
+import type { FC } from 'hono/jsx';
+
+type SelectOption = {
+ value: string;
+ label: string;
+ selected?: boolean;
+};
+
+type SelectProps = {
+ name: string;
+ options: SelectOption[];
+ className?: string;
+ required?: boolean;
+ disabled?: boolean;
+ id?: string;
+ placeholder?: string;
+};
+
+const Select: FC = ({
+ name,
+ options,
+ className = '',
+ required = false,
+ disabled = false,
+ id,
+ placeholder,
+ ...props
+}) => {
+ return (
+
+ {placeholder && (
+
+ {placeholder}
+
+ )}
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+ );
+};
+
+export default Select;
diff --git a/src/components/Stat.tsx b/src/components/Stat.tsx
new file mode 100644
index 0000000..6dd5bab
--- /dev/null
+++ b/src/components/Stat.tsx
@@ -0,0 +1,32 @@
+import type { FC } from 'hono/jsx';
+
+interface StatProps {
+ label: string;
+ value: string | number;
+ color?: 'blue' | 'emerald' | 'amber';
+ className?: string;
+}
+
+const Stat: FC = ({
+ label,
+ value,
+ color = 'blue',
+ className = '',
+}) => {
+ const colorClasses = {
+ blue: 'border-interactive-primary/50 text-status-info',
+ emerald: 'border-status-success/50 text-status-success',
+ amber: 'border-status-warning/50 text-status-warning',
+ };
+
+ return (
+
+ );
+};
+
+export default Stat;
diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx
new file mode 100644
index 0000000..0a21a00
--- /dev/null
+++ b/src/components/Toast.tsx
@@ -0,0 +1,46 @@
+import type { FC } from 'hono/jsx';
+import { createRawToastDismissal } from '@/utils/script-loader';
+import { env } from '@/utils/env';
+
+const TOAST_DELAY = {
+ test: 100,
+ production: 5000,
+};
+
+const ANIMATION_DURATION = 200;
+
+interface ToastProps {
+ type: 'success' | 'error';
+ message: string;
+}
+
+const Toast: FC = ({ type, message }) => {
+ const delay =
+ env('NODE_ENV') === 'test' ? TOAST_DELAY.test : TOAST_DELAY.production;
+ const id = `toast-${Math.random().toString(36).slice(2)}`;
+ const scriptId = `script-${id}`;
+
+ return (
+ <>
+
+
+
+ {type === 'success' ? '[SUCCESS]' : '[ERROR]'}
+
+ {message}
+
+
+ {createRawToastDismissal(id, delay, ANIMATION_DURATION, scriptId)}
+ >
+ );
+};
+
+export default Toast;
diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx
new file mode 100644
index 0000000..1b93944
--- /dev/null
+++ b/src/components/Toggle.tsx
@@ -0,0 +1,59 @@
+import type { FC } from 'hono/jsx';
+
+interface ToggleProps {
+ label: string;
+ description?: string;
+ enabled: boolean;
+ labelSize?: string;
+ descriptionSize?: string;
+ hxPost?: string;
+ hxTarget?: string;
+ hxSwap?: string;
+ className?: string;
+}
+
+const Toggle: FC = ({
+ label,
+ description,
+ enabled,
+ labelSize = 'text-sm',
+ descriptionSize = 'text-xs',
+ hxPost,
+ hxTarget,
+ hxSwap,
+ className = '',
+}) => {
+ const hxAttrs = {
+ ...(hxPost && { 'hx-post': hxPost }),
+ ...(hxTarget && { 'hx-target': hxTarget }),
+ ...(hxSwap && { 'hx-swap': hxSwap }),
+ };
+
+ return (
+
+
+
+ {label}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default Toggle;
diff --git a/src/lib/auth/constants.ts b/src/lib/auth/constants.ts
new file mode 100644
index 0000000..08746aa
--- /dev/null
+++ b/src/lib/auth/constants.ts
@@ -0,0 +1,108 @@
+import { TIME } from '@/utils/constants';
+
+export interface TokenPayload extends Record {
+ type: TokenType;
+ email: string;
+ role: UserRole;
+}
+
+export interface VerifiedToken extends TokenPayload {
+ exp: number;
+ iat: number;
+}
+
+export const TOKEN_TYPES = ['access', 'refresh', 'magic', 'csrf'] as const;
+export type TokenType = (typeof TOKEN_TYPES)[number];
+
+export const USER_ROLES = ['user', 'admin'] as const;
+export type UserRole = (typeof USER_ROLES)[number];
+
+const ACCESS_TOKEN_MINUTES = 15;
+const MAGIC_LINK_MINUTES = 15;
+
+export const TOKEN_EXPIRY_SECONDS = {
+ magic: TIME.SECONDS_IN_MINUTE * MAGIC_LINK_MINUTES,
+ access: TIME.SECONDS_IN_MINUTE * ACCESS_TOKEN_MINUTES,
+ refresh:
+ TIME.SECONDS_IN_MINUTE *
+ TIME.MINUTES_IN_HOUR *
+ TIME.HOURS_IN_DAY *
+ TIME.DAYS_IN_WEEK,
+ csrf: TIME.SECONDS_IN_MINUTE * TIME.MINUTES_IN_HOUR,
+} as const;
+
+type StrictCookieOptions = {
+ httpOnly: boolean;
+ secure: boolean;
+ sameSite: 'Strict';
+ domain?: string;
+};
+
+export const COOKIE_CONFIG = {
+ access: {
+ name: 'access_token',
+ path: '/',
+ maxAge: TIME.SECONDS_IN_MINUTE * ACCESS_TOKEN_MINUTES,
+ },
+ refresh: {
+ name: 'refresh_token',
+ path: '/',
+ maxAge:
+ TIME.SECONDS_IN_MINUTE *
+ TIME.MINUTES_IN_HOUR *
+ TIME.HOURS_IN_DAY *
+ TIME.DAYS_IN_WEEK,
+ },
+} as const;
+
+export const COOKIE_OPTIONS: StrictCookieOptions = {
+ httpOnly: true,
+ secure: true,
+ sameSite: 'Strict',
+} as const;
+
+export const RATE_LIMITS = {
+ login: {
+ max: 30,
+ window: 300000,
+ message: 'Too many page loads. Please try again later.',
+ },
+ verify: {
+ max: 30,
+ window: 300000,
+ message: 'Too many verification attempts. Please request a new magic link.',
+ },
+ refresh: {
+ max: 10,
+ window: 300000,
+ message: 'Too many refresh attempts. Please log in again.',
+ },
+} as const;
+
+export const JOSE_ERROR_CODES = {
+ EXPIRED: 'ERR_JWT_EXPIRED',
+ CLAIM_VALIDATION_FAILED: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
+ INVALID_SIGNATURE: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
+ INVALID_FORMAT: 'ERR_JWS_INVALID',
+ REVOKED: 'ERR_JWT_REVOKED',
+} as const;
+
+export type JoseError = Error & {
+ code?: string;
+};
+
+export const ERROR_CODES = {
+ EXPIRED: 'expired_token',
+ TAMPERED: 'tampered_token',
+ INVALID: 'invalid_token',
+ REVOKED: 'token_revoked',
+ VERIFICATION_REQUIRED: 'verification_required',
+} as const;
+
+export const ERROR_CODE_MAP = {
+ [JOSE_ERROR_CODES.EXPIRED]: ERROR_CODES.EXPIRED,
+ [JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED]: ERROR_CODES.TAMPERED,
+ [JOSE_ERROR_CODES.INVALID_SIGNATURE]: ERROR_CODES.TAMPERED,
+ [JOSE_ERROR_CODES.INVALID_FORMAT]: ERROR_CODES.INVALID,
+ [JOSE_ERROR_CODES.REVOKED]: ERROR_CODES.REVOKED,
+} as const;
diff --git a/src/lib/auth/magic.ts b/src/lib/auth/magic.ts
new file mode 100644
index 0000000..133f861
--- /dev/null
+++ b/src/lib/auth/magic.ts
@@ -0,0 +1,239 @@
+import { generateToken } from '@/lib/auth/tokens';
+import { TOKEN_EXPIRY_SECONDS } from '@/lib/auth/constants';
+import { env } from '@/utils/env';
+import { logHandler } from '@/middleware/logger';
+import { z } from 'zod';
+import { getAuthProvider, getEmailProvider } from '@/lib/providers';
+
+export async function sendMagicLink(
+ email: string,
+): Promise<{ error: string | null }> {
+ if (!email || !z.string().email().safeParse(email).success) {
+ logHandler.warn('auth', 'Invalid email format', { email });
+ return { error: 'Invalid email format' };
+ }
+
+ try {
+ const authProvider = await getAuthProvider();
+ logHandler.debug('auth', 'User verified or created for magic link', {
+ email,
+ });
+
+ const token = await generateToken({ type: 'magic', email, role: 'user' });
+ logHandler.debug('auth', 'Generated magic link token', {
+ email,
+ tokenLength: token.length,
+ });
+
+ await authProvider.storeToken(token, email, TOKEN_EXPIRY_SECONDS.magic);
+ logHandler.debug('auth', 'Stored magic link token', {
+ email,
+ expiry: TOKEN_EXPIRY_SECONDS.magic,
+ });
+
+ const host = env('HOST');
+ const appName = env('APP_NAME');
+ const magicLink = `${host}/auth/verify?token=${token}`;
+ logHandler.debug('auth', 'Created magic link URL', {
+ email,
+ host,
+ appName,
+ url: magicLink.substring(0, magicLink.indexOf('?')),
+ });
+
+ const emailProvider = getEmailProvider();
+ logHandler.debug('auth', 'Initialized email provider', {
+ email,
+ provider: emailProvider.constructor.name,
+ });
+
+ logHandler.info('auth', 'Sending magic link email', { email });
+
+ const mailResponse = await emailProvider.send({
+ from: env('EMAIL_FROM'),
+ to: email,
+ subject: '๐ Your magic link to sign in',
+ html: `
+
+
+
+
+
+ Your Magic Login Link
+
+
+
+
+
+
๐/div>
+
+
Welcome to ${appName}!
+
Click the button below to log in to your account. This link will expire in 15 minutes.
+
+ Log in to ${appName}
+
+
+ If the button doesn't work, copy and paste this link into your browser:
+ ${magicLink}
+
+
+
+
+
+
+
+ `,
+ });
+
+ if (mailResponse.error) {
+ logHandler.error('auth', 'Failed to send magic link email', {
+ email,
+ error: mailResponse.error,
+ errorMessage: mailResponse.error.message,
+ errorStack: mailResponse.error.stack,
+ from: env('EMAIL_FROM'),
+ providerType: emailProvider.constructor.name,
+ });
+ return { error: 'Failed to send magic link email' };
+ }
+
+ logHandler.info('auth', 'Magic link email sent successfully', {
+ email,
+ messageId: mailResponse.data?.id,
+ });
+
+ return { error: null };
+ } catch (error) {
+ logHandler.error('auth', 'Failed to send magic link', {
+ email,
+ error,
+ errorMessage: error instanceof Error ? error.message : String(error),
+ errorStack: error instanceof Error ? error.stack : undefined,
+ });
+ return { error: 'Failed to send magic link' };
+ }
+}
+
+export async function validateMagicLink(
+ token: string,
+ email: string,
+): Promise
{
+ logHandler.debug('auth', 'Starting magic link validation', {
+ email,
+ tokenLength: token.length,
+ });
+
+ try {
+ const authProvider = await getAuthProvider();
+ const storage = await authProvider.validateToken(token, email);
+ logHandler.debug('auth', 'Token validation result', {
+ email,
+ isValid: storage,
+ tokenLength: token.length,
+ });
+
+ if (storage) {
+ const userProvider = await authProvider.getUserByEmail(email);
+
+ if (userProvider) {
+ await authProvider.updateLastLogin(userProvider.id);
+ logHandler.info('user', 'Updated last login time for user', { email });
+ } else {
+ logHandler.warn('user', 'User not found during magic link validation', {
+ email,
+ });
+ }
+
+ await authProvider.invalidateToken(token);
+ logHandler.info('auth', 'Magic link token validated and invalidated', {
+ email,
+ });
+ } else {
+ logHandler.warn('auth', 'Invalid magic link token', {
+ email,
+ tokenLength: token.length,
+ });
+ }
+
+ return storage;
+ } catch (error) {
+ const errorObj = error instanceof Error ? error : new Error(String(error));
+ logHandler.error('auth', 'Error validating magic link', {
+ email,
+ errorMessage: errorObj.message,
+ errorStack: errorObj.stack,
+ errorName: errorObj.name,
+ });
+ return false;
+ }
+}
diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts
new file mode 100644
index 0000000..573e994
--- /dev/null
+++ b/src/lib/auth/session.ts
@@ -0,0 +1,44 @@
+import type { Context } from 'hono';
+import type { TokenPayload } from '@/lib/auth/constants';
+import { logHandler } from '@/middleware/logger';
+
+export function getUserFromContext(c: Context): TokenPayload | null {
+ try {
+ const user = c.get('user');
+ if (!user || typeof user !== 'object') {
+ return null;
+ }
+
+ const userObj = user as TokenPayload;
+ if (!userObj.email || !userObj.role || !userObj.type) {
+ return null;
+ }
+
+ logHandler.debug('auth', 'getUserFromContext completed successfully', {
+ context: 'getUserFromContext',
+ });
+ return userObj;
+ } catch (error) {
+ logHandler.error('auth', 'getUserFromContext failed', {
+ context: 'getUserFromContext',
+ error,
+ });
+ return null;
+ }
+}
+
+export function isAuthenticated(c: Context): boolean {
+ const user = getUserFromContext(c);
+ const isAuth = !!user;
+ if (!isAuth) {
+ logHandler.debug('auth', 'User not authenticated');
+ }
+ return isAuth;
+}
+
+export function getBaseUrl(c: Context): string {
+ const url = new URL(c.req.url);
+ const baseUrl = `${url.protocol}//${url.host}`;
+ logHandler.debug('http', 'Generated base URL', { baseUrl });
+ return baseUrl;
+}
diff --git a/src/lib/auth/tokens.ts b/src/lib/auth/tokens.ts
new file mode 100644
index 0000000..5b08d0b
--- /dev/null
+++ b/src/lib/auth/tokens.ts
@@ -0,0 +1,232 @@
+import { SignJWT, jwtVerify } from 'jose';
+import { logHandler } from '@/middleware/logger';
+import { env } from '@/utils/env';
+import {
+ JOSE_ERROR_CODES,
+ type JoseError,
+ TOKEN_EXPIRY_SECONDS,
+ TOKEN_TYPES,
+ type TokenPayload,
+ USER_ROLES,
+} from '@/lib/auth/constants';
+import { z } from 'zod';
+
+const tokenBlacklist = new Set();
+let signingKey: Uint8Array | null = null;
+
+function getKey(): Uint8Array {
+ if (!signingKey) {
+ try {
+ const key = env('SECRET_KEY');
+
+ const MIN_KEY_LENGTH_BYTES = 32;
+
+ const paddedKey = key.padEnd(MIN_KEY_LENGTH_BYTES, '0');
+ signingKey = new TextEncoder().encode(paddedKey);
+
+ return signingKey;
+ } catch (error) {
+ logHandler.error('token', 'Failed to generate signing key', { error });
+ throw new Error('Failed to generate signing key');
+ }
+ }
+ return signingKey;
+}
+
+export function blacklistToken(token: string): void {
+ tokenBlacklist.add(token);
+ logHandler.debug('token', 'Token added to blacklist');
+}
+
+export async function generateToken(payload: TokenPayload): Promise {
+ try {
+ if (!TOKEN_TYPES.includes(payload.type)) {
+ logHandler.error('token', 'Non-standard token type', { payload });
+ throw new Error('Non-standard token type');
+ }
+
+ const expiry = `${TOKEN_EXPIRY_SECONDS[payload.type]}s`;
+ logHandler.debug('token', 'Generating token', {
+ type: payload.type,
+ expiry,
+ });
+
+ const tokenPayload = {
+ type: payload.type,
+ role: payload.role,
+ email: payload.email,
+ } satisfies TokenPayload;
+
+ const jwt = await new SignJWT(tokenPayload)
+ .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
+ .setIssuedAt()
+ .setExpirationTime(expiry)
+ .sign(getKey());
+
+ logHandler.debug('token', 'Token generated successfully', {
+ type: payload.type,
+ });
+ return jwt;
+ } catch (error) {
+ logHandler.error('token', 'generateToken failed', {
+ type: payload.type,
+ error,
+ });
+ throw error;
+ }
+}
+
+export async function generateCsrfToken(): Promise {
+ const payload: TokenPayload = {
+ type: 'csrf' as const,
+ role: 'user',
+ email: 'csrf@example.com',
+ };
+
+ try {
+ const expiry = `${TOKEN_EXPIRY_SECONDS['csrf']}s`;
+ logHandler.debug('token', 'Generating CSRF token', { expiry });
+
+ const csrfPayload = {
+ ...payload,
+ type: 'csrf' as const,
+ };
+
+ const jwt = await new SignJWT(csrfPayload)
+ .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
+ .setIssuedAt()
+ .setExpirationTime(expiry)
+ .sign(getKey());
+
+ logHandler.debug('token', 'CSRF token generated successfully');
+ return jwt;
+ } catch (error) {
+ logHandler.error('token', 'generateCsrfToken failed', { error });
+ throw error;
+ }
+}
+
+function createTokenError(message: string, code: string): JoseError {
+ const error = new Error(message) as JoseError;
+ error.code = code;
+ return error;
+}
+
+interface ValidationRule {
+ check: (payload: Partial) => boolean;
+ errorCode: (typeof JOSE_ERROR_CODES)[keyof typeof JOSE_ERROR_CODES];
+ message: string;
+}
+
+const tokenValidationRules: ValidationRule[] = [
+ {
+ check: (payload): boolean => {
+ const type = payload.type;
+ return type !== undefined && TOKEN_TYPES.includes(type);
+ },
+ errorCode: JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ message: 'Invalid token type',
+ },
+ {
+ check: (payload): boolean => {
+ const role = payload.role;
+ return role !== undefined && USER_ROLES.includes(role);
+ },
+ errorCode: JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ message: 'Invalid role',
+ },
+ {
+ check: (payload): boolean => {
+ const email = payload.email;
+ const type = payload.type;
+ return (
+ email !== undefined &&
+ (type === 'csrf' || z.string().email().safeParse(email).success)
+ );
+ },
+ errorCode: JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ message: 'Invalid email format',
+ },
+ {
+ check: (payload): boolean =>
+ payload['exp'] !== undefined && payload['iat'] !== undefined,
+ errorCode: JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ message: 'Missing required claims',
+ },
+ {
+ check: (payload): boolean => {
+ const now = Math.floor(Date.now() / 1000);
+ return payload['exp'] !== undefined && Number(payload['exp']) > now;
+ },
+ errorCode: JOSE_ERROR_CODES.EXPIRED,
+ message: 'Token has expired',
+ },
+ {
+ check: (payload): boolean => {
+ const now = Math.floor(Date.now() / 1000);
+ return payload['iat'] !== undefined && Number(payload['iat']) <= now;
+ },
+ errorCode: JOSE_ERROR_CODES.CLAIM_VALIDATION_FAILED,
+ message: 'Token not yet valid',
+ },
+];
+
+export async function validateTokenPayload(
+ tokenPayload: Partial,
+): Promise {
+ try {
+ logHandler.debug('token', 'Validating token payload', {
+ type: tokenPayload.type,
+ });
+
+ for (const rule of tokenValidationRules) {
+ if (!rule.check(tokenPayload)) {
+ logHandler.error('token', rule.message);
+ throw createTokenError(rule.message, rule.errorCode);
+ }
+ }
+
+ logHandler.debug('token', 'Token payload validated successfully');
+ } catch (error) {
+ logHandler.error('token', 'validateTokenPayload failed', {
+ type: tokenPayload.type,
+ error,
+ });
+ throw error;
+ }
+}
+
+export async function verifyToken(token: string): Promise {
+ if (!token) {
+ logHandler.error('token', 'Token is empty');
+ throw createTokenError('Token is empty', JOSE_ERROR_CODES.INVALID_FORMAT);
+ }
+
+ if (tokenBlacklist.has(token)) {
+ logHandler.error('token', 'Token is blacklisted');
+ throw createTokenError('Token is blacklisted', JOSE_ERROR_CODES.REVOKED);
+ }
+
+ try {
+ const key = getKey();
+ const { payload } = await jwtVerify(token, key);
+ const tokenPayload = payload as unknown as TokenPayload;
+
+ await validateTokenPayload(tokenPayload);
+ logHandler.debug('token', 'Token verified successfully', {
+ type: tokenPayload.type,
+ });
+ return tokenPayload;
+ } catch (error: unknown) {
+ logHandler.error('token', 'Token verification failed', {
+ truncatedToken: token.substring(0, 20) + '...',
+ error,
+ });
+ throw error;
+ }
+}
+
+export function resetBlacklist(): void {
+ tokenBlacklist.clear();
+ logHandler.debug('token', 'Token blacklist reset');
+}
diff --git a/src/lib/database.ts b/src/lib/database.ts
new file mode 100644
index 0000000..5c2074d
--- /dev/null
+++ b/src/lib/database.ts
@@ -0,0 +1,118 @@
+import { Database } from 'bun:sqlite';
+import { logHandler } from '@/middleware/logger';
+
+let db: Database | null = null;
+let cleanupInterval: Timer | null = null;
+
+export function getDatabase(): Database {
+ if (!db) {
+ throw new Error(
+ 'Database not initialized. Call initializeDatabase() first.',
+ );
+ }
+ return db;
+}
+
+export function initializeDatabase(path: string = 'app.db'): Database {
+ if (db) {
+ logHandler.debug('db', 'Database already initialized');
+ return db;
+ }
+
+ try {
+ db = new Database(path);
+ db.exec('PRAGMA journal_mode = WAL;');
+ db.exec('PRAGMA synchronous = NORMAL;');
+ db.exec('PRAGMA foreign_keys = ON;');
+
+ createTables();
+ startCleanupScheduler();
+
+ logHandler.info('db', 'SQLite database initialized successfully', { path });
+ return db;
+ } catch (error) {
+ logHandler.error('db', 'Failed to initialize database', {
+ error,
+ errorMessage: error instanceof Error ? error.message : String(error),
+ path,
+ });
+ throw error;
+ }
+}
+
+function createTables(): void {
+ const db = getDatabase();
+
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ email TEXT UNIQUE NOT NULL,
+ created_at INTEGER NOT NULL,
+ last_login_at INTEGER
+ );
+ `);
+
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS tokens (
+ token TEXT PRIMARY KEY,
+ email TEXT NOT NULL,
+ expires_at INTEGER NOT NULL,
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
+ );
+ `);
+
+ db.exec(`
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
+ CREATE INDEX IF NOT EXISTS idx_tokens_email ON tokens(email);
+ CREATE INDEX IF NOT EXISTS idx_tokens_expires_at ON tokens(expires_at);
+ `);
+
+ logHandler.debug('db', 'Database tables created successfully');
+}
+
+function startCleanupScheduler(): void {
+ if (cleanupInterval) {
+ clearInterval(cleanupInterval);
+ }
+
+ cleanupInterval = setInterval(() => {
+ try {
+ cleanupExpiredTokens();
+ } catch (error) {
+ logHandler.error('db', 'Failed to cleanup expired tokens', {
+ error,
+ errorMessage: error instanceof Error ? error.message : String(error),
+ });
+ }
+ }, 60000 * 15);
+
+ logHandler.debug('db', 'Cleanup scheduler started (runs every 15 minutes)');
+}
+
+export function cleanupExpiredTokens(): void {
+ const db = getDatabase();
+ const deleteExpired = db.query(`
+ DELETE FROM tokens WHERE expires_at < $now
+ `);
+
+ const result = deleteExpired.run({ $now: Date.now() });
+
+ if (result.changes > 0) {
+ logHandler.info('db', 'Cleaned up expired tokens', {
+ count: result.changes,
+ });
+ }
+}
+
+export function closeDatabase(): void {
+ if (cleanupInterval) {
+ clearInterval(cleanupInterval);
+ cleanupInterval = null;
+ }
+
+ if (db) {
+ db.close();
+ db = null;
+ logHandler.info('db', 'Database connection closed');
+ }
+}
diff --git a/src/lib/providers/auth.ts b/src/lib/providers/auth.ts
new file mode 100644
index 0000000..fd0e270
--- /dev/null
+++ b/src/lib/providers/auth.ts
@@ -0,0 +1,207 @@
+import { logHandler } from '@/middleware/logger';
+import { v4 as uuid } from 'uuid';
+import { getDatabase, initializeDatabase } from '@/lib/database';
+import { env } from '@/utils/env';
+
+interface User {
+ id: string;
+ email: string;
+ createdAt: Date;
+ lastLoginAt?: Date | undefined;
+}
+
+export interface AuthProvider {
+ createUser(email: string): Promise;
+ getUserByEmail(email: string): Promise;
+ getUserById(id: string): Promise;
+ updateLastLogin(id: string): Promise;
+ storeToken(
+ token: string,
+ email: string,
+ expiresInSeconds: number,
+ ): Promise;
+ validateToken(token: string, email: string): Promise;
+ invalidateToken(token: string): Promise;
+}
+
+class SQLiteAuthProvider implements AuthProvider {
+ private db = getDatabase();
+
+ async createUser(email: string): Promise {
+ const existingUser = await this.getUserByEmail(email);
+ if (existingUser) {
+ return existingUser;
+ }
+
+ const now = Date.now();
+ const user: User = {
+ id: uuid(),
+ email,
+ createdAt: new Date(now),
+ lastLoginAt: new Date(now),
+ };
+
+ const insertUser = this.db.query(`
+ INSERT INTO users (id, email, created_at, last_login_at)
+ VALUES ($id, $email, $createdAt, $lastLoginAt)
+ `);
+
+ insertUser.run({
+ $id: user.id,
+ $email: user.email,
+ $createdAt: now,
+ $lastLoginAt: now,
+ });
+
+ return user;
+ }
+
+ async getUserByEmail(email: string): Promise {
+ const getUser = this.db.query(`
+ SELECT id, email, created_at, last_login_at
+ FROM users
+ WHERE email = $email
+ `);
+
+ const row = getUser.get({ $email: email }) as
+ | {
+ id: string;
+ email: string;
+ created_at: number;
+ last_login_at: number | null;
+ }
+ | undefined;
+
+ if (!row) {
+ return null;
+ }
+
+ return {
+ id: row.id,
+ email: row.email,
+ createdAt: new Date(row.created_at),
+ lastLoginAt: row.last_login_at ? new Date(row.last_login_at) : undefined,
+ };
+ }
+
+ async getUserById(id: string): Promise {
+ const getUser = this.db.query(`
+ SELECT id, email, created_at, last_login_at
+ FROM users
+ WHERE id = $id
+ `);
+
+ const row = getUser.get({ $id: id }) as
+ | {
+ id: string;
+ email: string;
+ created_at: number;
+ last_login_at: number | null;
+ }
+ | undefined;
+
+ if (!row) {
+ return null;
+ }
+
+ return {
+ id: row.id,
+ email: row.email,
+ createdAt: new Date(row.created_at),
+ lastLoginAt: row.last_login_at ? new Date(row.last_login_at) : undefined,
+ };
+ }
+
+ async updateLastLogin(id: string): Promise {
+ const updateQuery = this.db.query(`
+ UPDATE users
+ SET last_login_at = $lastLoginAt
+ WHERE id = $id
+ `);
+
+ const result = updateQuery.run({
+ $id: id,
+ $lastLoginAt: Date.now(),
+ });
+
+ return result.changes > 0;
+ }
+
+ async storeToken(
+ token: string,
+ email: string,
+ expiresInSeconds: number,
+ ): Promise {
+ const expiresAt = Date.now() + expiresInSeconds * 1000;
+
+ const insertToken = this.db.query(`
+ INSERT OR REPLACE INTO tokens (token, email, expires_at)
+ VALUES ($token, $email, $expiresAt)
+ `);
+
+ insertToken.run({
+ $token: token,
+ $email: email,
+ $expiresAt: expiresAt,
+ });
+ }
+
+ async validateToken(token: string, email: string): Promise {
+ const getToken = this.db.query(`
+ SELECT email, expires_at
+ FROM tokens
+ WHERE token = $token
+ `);
+
+ const tokenData = getToken.get({ $token: token }) as
+ | {
+ email: string;
+ expires_at: number;
+ }
+ | undefined;
+
+ if (!tokenData) {
+ return false;
+ }
+
+ if (tokenData.email !== email) {
+ return false;
+ }
+
+ if (tokenData.expires_at < Date.now()) {
+ await this.invalidateToken(token);
+ return false;
+ }
+
+ return true;
+ }
+
+ async invalidateToken(token: string): Promise {
+ const deleteToken = this.db.query(`
+ DELETE FROM tokens WHERE token = $token
+ `);
+
+ deleteToken.run({ $token: token });
+ }
+}
+
+let authProvider: AuthProvider | null = null;
+
+export async function initializeAuthProvider(): Promise {
+ initializeDatabase(env('DATABASE_PATH'));
+ authProvider = new SQLiteAuthProvider();
+ logHandler.info(
+ 'api',
+ `Auth provider initialized: ${authProvider.constructor.name}`,
+ );
+}
+
+export async function getAuthProvider(): Promise {
+ if (!authProvider) {
+ await initializeAuthProvider();
+ }
+
+ if (!authProvider) throw new Error('Auth provider not initialized');
+
+ return authProvider;
+}
diff --git a/src/lib/providers/email.ts b/src/lib/providers/email.ts
new file mode 100644
index 0000000..cde11e9
--- /dev/null
+++ b/src/lib/providers/email.ts
@@ -0,0 +1,158 @@
+import { Resend } from 'resend';
+import { env } from '@/utils/env';
+import { logHandler } from '@/middleware/logger';
+
+interface EmailConfig {
+ from: string;
+ to: string;
+ subject: string;
+ html: string;
+}
+
+interface EmailResponse {
+ data: { id: string } | null;
+ error: Error | null;
+}
+
+interface EmailProvider {
+ send(config: EmailConfig): Promise;
+}
+
+class ResendProvider implements EmailProvider {
+ private client: Resend;
+ private apiKeyConfigured: boolean;
+ private static API_KEY_MIN_LENGTH = 10;
+
+ constructor(apiKey?: string) {
+ const configuredApiKey = apiKey || env('RESEND_API_KEY');
+ this.apiKeyConfigured =
+ !!configuredApiKey &&
+ configuredApiKey.length > ResendProvider.API_KEY_MIN_LENGTH;
+
+ this.client = new Resend(configuredApiKey);
+
+ logHandler.debug('email', 'Resend provider initialized', {
+ apiKeyConfigured: this.apiKeyConfigured,
+ apiKeyLength: configuredApiKey ? configuredApiKey.length : 0,
+ environment: env('NODE_ENV'),
+ });
+
+ if (!this.apiKeyConfigured) {
+ logHandler.warn('email', 'Resend API key not properly configured', {
+ apiKeyPresent: !!configuredApiKey,
+ environment: env('NODE_ENV'),
+ });
+ }
+ }
+
+ async send(config: EmailConfig): Promise {
+ if (!this.apiKeyConfigured) {
+ logHandler.error(
+ 'email',
+ 'Cannot send email - Resend API key not configured',
+ {
+ to: config.to,
+ subject: config.subject,
+ from: config.from,
+ environment: env('NODE_ENV'),
+ },
+ );
+ return {
+ data: null,
+ error: new Error('Resend API key not properly configured'),
+ };
+ }
+
+ logHandler.debug('email', 'Preparing to send email via Resend', {
+ to: config.to,
+ subject: config.subject,
+ from: config.from,
+ contentLength: config.html.length,
+ });
+
+ try {
+ logHandler.debug('email', 'Calling Resend API', {
+ to: config.to,
+ apiKeyConfigured: this.apiKeyConfigured,
+ });
+
+ const { data, error } = await this.client.emails.send(config);
+
+ if (error) {
+ logHandler.error('email', 'Resend API error', {
+ error,
+ errorMessage: error.message,
+ name: error.name,
+ to: config.to,
+ from: config.from,
+ });
+ } else {
+ logHandler.debug('email', 'Email sent successfully via Resend', {
+ id: data?.id,
+ to: config.to,
+ from: config.from,
+ });
+ }
+
+ logHandler.debug('email', 'sendEmailViaResend completed successfully', {
+ to: config.to,
+ subject: config.subject,
+ from: config.from,
+ });
+ return { data, error };
+ } catch (error) {
+ logHandler.error('email', 'sendEmailViaResend failed', {
+ to: config.to,
+ subject: config.subject,
+ from: config.from,
+ error,
+ });
+ return { data: null, error: error as Error };
+ }
+ }
+}
+
+class ConsoleEmailProvider implements EmailProvider {
+ private lastConfig: EmailConfig | null = null;
+
+ async send(config: EmailConfig): Promise {
+ this.lastConfig = config;
+ logHandler.info('email', 'Sending email:', {
+ to: config.to,
+ subject: config.subject,
+ html: config.html,
+ });
+ logHandler.debug('email', 'Email sent successfully via console');
+ return { data: { id: 'test_console' }, error: null };
+ }
+
+ getLastConfig(): EmailConfig | null {
+ logHandler.debug('email', 'Retrieving last email config');
+ return this.lastConfig;
+ }
+}
+
+let emailProvider: EmailProvider;
+
+export function initializeEmailProvider(): void {
+ if (env('NODE_ENV') === 'development') {
+ emailProvider = new ConsoleEmailProvider();
+ logHandler.info(
+ 'email',
+ 'Initialized Console email provider for local development',
+ );
+ } else {
+ emailProvider = new ResendProvider(env('RESEND_API_KEY'));
+ logHandler.info(
+ 'email',
+ 'Initialized Resend email provider for production',
+ );
+ }
+}
+
+export function getEmailProvider(): EmailProvider {
+ if (!emailProvider) {
+ initializeEmailProvider();
+ }
+ return emailProvider;
+}
diff --git a/src/lib/providers/index.ts b/src/lib/providers/index.ts
new file mode 100644
index 0000000..7cc374e
--- /dev/null
+++ b/src/lib/providers/index.ts
@@ -0,0 +1,33 @@
+import { logHandler } from '@/middleware/logger';
+import { getAuthProvider, initializeAuthProvider } from '@/lib/providers/auth';
+import {
+ getEmailProvider,
+ initializeEmailProvider,
+} from '@/lib/providers/email';
+
+let isInitialized = false;
+
+async function initialize(): Promise {
+ if (isInitialized) return;
+
+ logHandler.info('api', 'Initializing providers');
+ await initializeAuthProvider();
+ initializeEmailProvider();
+ isInitialized = true;
+ logHandler.info('api', 'All providers initialized successfully');
+}
+
+export { getAuthProvider, getEmailProvider };
+
+void (async (): Promise => {
+ try {
+ await initialize();
+ logHandler.info('api', 'Auto-initialization completed successfully');
+ } catch (error) {
+ logHandler.error('api', 'Failed to auto-initialize providers', {
+ error,
+ errorMessage: error instanceof Error ? error.message : String(error),
+ });
+ throw error;
+ }
+})();
diff --git a/src/middleware/error-handler.ts b/src/middleware/error-handler.ts
new file mode 100644
index 0000000..3962744
--- /dev/null
+++ b/src/middleware/error-handler.ts
@@ -0,0 +1,167 @@
+import type { Context, MiddlewareHandler, Next } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+import { contextLog } from '@/middleware/logger';
+import { HTTP_STATUS } from '@/utils/constants';
+
+const ERROR_PREFIX_LENGTH = 4;
+
+interface ErrorResponse {
+ status: number;
+ message: string;
+ code?: string | undefined;
+ details?: Record | undefined;
+}
+
+export function errorHandler(): MiddlewareHandler {
+ return async (c: Context, next: Next) => {
+ try {
+ await next();
+ } catch (error) {
+ return handleError(c, error);
+ }
+ return c.res;
+ };
+}
+
+function handleError(c: Context, error: unknown): Response {
+ const response = createErrorResponse(error);
+
+ contextLog(c, 'http', 'error', `Request error: ${response.message}`, {
+ path: c.req.path,
+ method: c.req.method,
+ status: response.status,
+ code: response.code,
+ headers: safeHeaders(c),
+ details: response.details,
+ error:
+ error instanceof Error
+ ? {
+ message: error.message,
+ name: error.name,
+ stack: error.stack || 'No stack trace available',
+ }
+ : String(error),
+ });
+
+ if (response.status === HTTP_STATUS.BAD_REQUEST) {
+ c.status(HTTP_STATUS.BAD_REQUEST);
+ } else if (response.status === HTTP_STATUS.UNAUTHORIZED) {
+ c.status(HTTP_STATUS.UNAUTHORIZED);
+ } else if (response.status === HTTP_STATUS.FORBIDDEN) {
+ c.status(HTTP_STATUS.FORBIDDEN);
+ } else if (response.status === HTTP_STATUS.NOT_FOUND) {
+ c.status(HTTP_STATUS.NOT_FOUND);
+ } else if (response.status === HTTP_STATUS.METHOD_NOT_ALLOWED) {
+ c.status(HTTP_STATUS.METHOD_NOT_ALLOWED);
+ } else if (response.status === HTTP_STATUS.REQUEST_TIMEOUT) {
+ c.status(HTTP_STATUS.REQUEST_TIMEOUT);
+ } else if (response.status === HTTP_STATUS.CONFLICT) {
+ c.status(HTTP_STATUS.CONFLICT);
+ } else if (response.status === HTTP_STATUS.TOO_MANY_REQUESTS) {
+ c.status(HTTP_STATUS.TOO_MANY_REQUESTS);
+ } else if (response.status === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
+ c.status(HTTP_STATUS.INTERNAL_SERVER_ERROR);
+ } else if (response.status === HTTP_STATUS.BAD_GATEWAY) {
+ c.status(HTTP_STATUS.BAD_GATEWAY);
+ } else if (response.status === HTTP_STATUS.SERVICE_UNAVAILABLE) {
+ c.status(HTTP_STATUS.SERVICE_UNAVAILABLE);
+ } else if (response.status === HTTP_STATUS.GATEWAY_TIMEOUT) {
+ c.status(HTTP_STATUS.GATEWAY_TIMEOUT);
+ } else {
+ c.status(HTTP_STATUS.INTERNAL_SERVER_ERROR);
+ }
+
+ if (c.req.path.startsWith('/api')) {
+ return c.json({
+ success: false,
+ error: {
+ message: response.message,
+ status: response.status,
+ code: response.code,
+ },
+ });
+ }
+
+ return c.redirect(
+ `/?error=${encodeURIComponent(response.message || 'An error occurred')}&status=${response.status}`,
+ );
+}
+
+function createErrorResponse(error: unknown): ErrorResponse {
+ if (error instanceof HTTPException) {
+ return {
+ status: error.status,
+ message: error.message || getDefaultMessageForStatus(error.status),
+ details: error.getResponse
+ ? {
+ response: String(error.getResponse()),
+ }
+ : undefined,
+ };
+ }
+
+ if (error instanceof Error) {
+ const statusMatch = error.message.match(/^(\d{3}):/);
+ const status =
+ statusMatch && statusMatch[1]
+ ? parseInt(statusMatch[1], 10)
+ : HTTP_STATUS.INTERNAL_SERVER_ERROR;
+
+ return {
+ status,
+ message: statusMatch
+ ? error.message.substring(ERROR_PREFIX_LENGTH).trim()
+ : error.message,
+ code: error.name,
+ details: { stack: error.stack || 'No stack trace available' },
+ };
+ }
+
+ return {
+ status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
+ message: error ? String(error) : 'Unknown error occurred',
+ };
+}
+
+function getDefaultMessageForStatus(status: number): string {
+ switch (status) {
+ case HTTP_STATUS.BAD_REQUEST:
+ return 'Bad Request';
+ case HTTP_STATUS.UNAUTHORIZED:
+ return 'Unauthorized';
+ case HTTP_STATUS.FORBIDDEN:
+ return 'Forbidden';
+ case HTTP_STATUS.NOT_FOUND:
+ return 'Not Found';
+ case HTTP_STATUS.METHOD_NOT_ALLOWED:
+ return 'Method Not Allowed';
+ case HTTP_STATUS.REQUEST_TIMEOUT:
+ return 'Request Timeout';
+ case HTTP_STATUS.CONFLICT:
+ return 'Conflict';
+ case HTTP_STATUS.TOO_MANY_REQUESTS:
+ return 'Too Many Requests';
+ case HTTP_STATUS.INTERNAL_SERVER_ERROR:
+ return 'Internal Server Error';
+ case HTTP_STATUS.BAD_GATEWAY:
+ return 'Bad Gateway';
+ case HTTP_STATUS.SERVICE_UNAVAILABLE:
+ return 'Service Unavailable';
+ case HTTP_STATUS.GATEWAY_TIMEOUT:
+ return 'Gateway Timeout';
+ default:
+ return 'An unexpected error occurred';
+ }
+}
+
+function safeHeaders(c: Context): Record {
+ const headers: Record = {};
+ c.req.raw.headers.forEach((value, key) => {
+ if (['authorization', 'cookie'].includes(key.toLowerCase())) {
+ headers[key] = '[REDACTED]';
+ } else {
+ headers[key] = value;
+ }
+ });
+ return headers;
+}
diff --git a/src/middleware/logger.ts b/src/middleware/logger.ts
new file mode 100644
index 0000000..24541f4
--- /dev/null
+++ b/src/middleware/logger.ts
@@ -0,0 +1,335 @@
+import type { Context, MiddlewareHandler, Next } from 'hono';
+
+const MAX_RETENTION_DAYS = 7;
+const HOURS_PER_DAY = 24;
+const SECONDS_PER_MINUTE = 60;
+const MINUTES_PER_HOUR = 60;
+const MAX_LOGS_IN_MEMORY = 10000;
+const STORAGE_CLEANUP_FACTOR = 0.2;
+
+const categories = {
+ auth: { icon: '๐', description: 'Authentication' },
+ token: { icon: '๐๏ธ', description: 'Token operations' },
+ email: { icon: '๐ง', description: 'Email operations' },
+ http: { icon: '๐', description: 'HTTP operations' },
+ ui: { icon: '๐จ', description: 'UI operations' },
+ theme: { icon: '๐ญ', description: 'Theming' },
+ user: { icon: '๐ค', description: 'User operations' },
+ db: { icon: '๐๏ธ', description: 'Database operations' },
+ cache: { icon: '๐ฆ', description: 'Caching operations' },
+ perf: { icon: 'โก', description: 'Performance' },
+ storage: { icon: '๐พ', description: 'Storage operations' },
+ system: { icon: '๐ฅ๏ธ', description: 'System operations' },
+ api: { icon: '๐ง', description: 'API operations' },
+} as const;
+
+export type LogCategory =
+ | 'auth'
+ | 'token'
+ | 'email'
+ | 'http'
+ | 'ui'
+ | 'theme'
+ | 'user'
+ | 'db'
+ | 'cache'
+ | 'perf'
+ | 'storage'
+ | 'system'
+ | 'api';
+
+export const LogLevel = {
+ ERROR: 'error',
+ WARN: 'warn',
+ INFO: 'info',
+ DEBUG: 'debug',
+} as const;
+
+export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
+
+const MAX_LOG_RETENTION =
+ MAX_RETENTION_DAYS *
+ HOURS_PER_DAY *
+ MINUTES_PER_HOUR *
+ SECONDS_PER_MINUTE *
+ 1000;
+
+const logStorage: LogEntry[] = [];
+
+interface LogEntry {
+ timestamp: string;
+ level: LogLevel;
+ category: LogCategory;
+ message: string;
+ icon: string;
+ correlationId?: string | undefined;
+ data?: Record | undefined;
+ context?:
+ | {
+ request?:
+ | {
+ method?: string | undefined;
+ path?: string | undefined;
+ ip?: string | undefined;
+ userAgent?: string | undefined;
+ }
+ | undefined;
+ user?:
+ | {
+ email?: string | undefined;
+ }
+ | undefined;
+ }
+ | undefined;
+}
+
+function getIcon(category: LogCategory): string {
+ return categories[category]?.icon ?? '๐ง';
+}
+
+function formatMessage(
+ type: string,
+ message: string,
+ data?: Record,
+): string {
+ const timestamp = new Date().toISOString().split('T')[1]?.slice(0, -1) ?? '';
+ const prefix = `[${timestamp}] ${type.toUpperCase()}:`;
+ const formattedMessage = data
+ ? `${message} ${JSON.stringify(data)}`
+ : message;
+ return `${prefix} ${formattedMessage}`;
+}
+
+export const logger = {
+ error: (msg: string): boolean => process.stderr.write(`${msg}\n`),
+ log: (msg: string): boolean => process.stdout.write(`${msg}\n`),
+};
+
+function log(
+ level: LogLevel,
+ category: LogCategory,
+ message: string,
+ data?: Record,
+ context?: Context,
+): void {
+ const icon = getIcon(category);
+ const timestamp = new Date().toISOString();
+
+ const logEntry: LogEntry = {
+ timestamp,
+ level,
+ category,
+ message,
+ icon,
+ data,
+ };
+
+ if (context) {
+ logEntry.context = {
+ request: {
+ method: context.req.method,
+ path: context.req.path,
+ ip:
+ context.req.header('x-forwarded-for') ||
+ context.req.header('x-real-ip'),
+ userAgent: context.req.header('user-agent'),
+ },
+ };
+
+ try {
+ const user = context.get('user');
+ if (user && typeof user === 'object' && 'email' in user) {
+ if (!logEntry.context) {
+ logEntry.context = {};
+ }
+ logEntry.context.user = {
+ email: String(user.email),
+ };
+ }
+ } catch (e) {}
+ }
+
+ let formattedMessage: string;
+
+ formattedMessage = ` ${formatMessage(category, `${icon} ${message}`, data)}`;
+
+ if (level === LogLevel.ERROR) {
+ logger.error(formattedMessage);
+ } else {
+ logger.log(formattedMessage);
+ }
+
+ storeLog(logEntry);
+}
+
+function storeLog(entry: LogEntry): void {
+ logStorage.push(entry);
+
+ if (logStorage.length > MAX_LOGS_IN_MEMORY) {
+ logStorage.splice(
+ 0,
+ Math.floor(MAX_LOGS_IN_MEMORY * STORAGE_CLEANUP_FACTOR),
+ );
+ }
+
+ const now = Date.now();
+ let i = 0;
+ while (i < logStorage.length) {
+ const timestamp = logStorage[i]?.timestamp;
+ if (timestamp) {
+ const logDate = new Date(timestamp).getTime();
+ if (now - logDate > MAX_LOG_RETENTION) {
+ logStorage.splice(i, 1);
+ } else {
+ i++;
+ }
+ } else {
+ logStorage.splice(i, 1);
+ }
+ }
+}
+
+export const logHandler = {
+ error: (
+ category: LogCategory,
+ message: string,
+ data?: Record,
+ context?: Context,
+ ): void => log(LogLevel.ERROR, category, message, data, context),
+ warn: (
+ category: LogCategory,
+ message: string,
+ data?: Record,
+ context?: Context,
+ ): void => log(LogLevel.WARN, category, message, data, context),
+ info: (
+ category: LogCategory,
+ message: string,
+ data?: Record,
+ context?: Context,
+ ): void => log(LogLevel.INFO, category, message, data, context),
+ debug: (
+ category: LogCategory,
+ message: string,
+ data?: Record,
+ context?: Context,
+ ): void => log(LogLevel.DEBUG, category, message, data, context),
+};
+
+export function getLogs(options?: {
+ level?: LogLevel;
+ category?: LogCategory;
+ limit?: number;
+ since?: Date;
+}): LogEntry[] {
+ const { level, category, limit = MAX_LOGS_IN_MEMORY, since } = options || {};
+
+ let filteredLogs = [...logStorage];
+
+ if (level) {
+ const levels = Object.values(LogLevel);
+ const levelIndex = levels.indexOf(level);
+ const allowedLevels = levels.slice(0, levelIndex + 1);
+
+ filteredLogs = filteredLogs.filter((log) =>
+ allowedLevels.includes(log.level),
+ );
+ }
+
+ if (category) {
+ filteredLogs = filteredLogs.filter((log) => log.category === category);
+ }
+
+ if (since) {
+ filteredLogs = filteredLogs.filter(
+ (log) => new Date(log.timestamp) >= since,
+ );
+ }
+
+ return filteredLogs
+ .sort(
+ (a, b) =>
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
+ )
+ .slice(0, limit);
+}
+
+export interface LoggingContext {
+ c: Context;
+ category: LogCategory;
+ level?: keyof typeof logHandler;
+ message: string;
+ data?: Record | undefined;
+}
+
+export function contextLog(
+ c: Context | null,
+ category: LogCategory,
+ level: keyof typeof logHandler = 'debug',
+ message: string,
+ data?: Record,
+): void {
+ if (c && typeof c.get === 'function') {
+ try {
+ const logFn = c.get('log');
+ if (typeof logFn === 'function') {
+ logFn({ c, category, level, message, data });
+ return;
+ }
+ } catch (error) {}
+ }
+
+ logHandler[level](category, message, data, c || undefined);
+}
+
+export function logging(): MiddlewareHandler {
+ return async (c: Context, next: Next) => {
+ const startTime = Date.now();
+ const method = c.req.method;
+ const path = c.req.path;
+ const userAgent = c.req.header('user-agent') || 'unknown';
+ const ip =
+ c.req.header('x-forwarded-for') ||
+ c.req.header('x-real-ip') ||
+ '127.0.0.1';
+
+ logHandler.info(
+ 'http',
+ `Request received: ${method} ${path}`,
+ {
+ method,
+ path,
+ userAgent,
+ ip,
+ },
+ c,
+ );
+
+ c.set('log', (ctx: LoggingContext) => {
+ const logLevel = ctx.level || 'debug';
+ logHandler[logLevel](ctx.category, ctx.message, ctx.data, ctx.c);
+ });
+
+ await next();
+
+ const duration = Date.now() - startTime;
+
+ logHandler.debug(
+ 'http',
+ `Request complete: ${method} ${path}`,
+ {
+ method,
+ path,
+ duration: `${duration}ms`,
+ status: c.res.status,
+ },
+ c,
+ );
+ };
+}
+
+declare module 'hono' {
+ interface ContextVariableMap {
+ log: (ctx: LoggingContext) => void;
+ }
+}
diff --git a/src/middleware/noAuth.ts b/src/middleware/noAuth.ts
new file mode 100644
index 0000000..ed9f1ef
--- /dev/null
+++ b/src/middleware/noAuth.ts
@@ -0,0 +1,18 @@
+import { type MiddlewareHandler } from 'hono';
+import { logHandler } from '@/middleware/logger';
+
+export const noAuth = (): MiddlewareHandler => {
+ return async (c, next) => {
+ logHandler.info('auth', 'Authentication bypassed - using read-only mode');
+
+ c.set('user', {
+ id: 'guest',
+ email: 'guest@example.com',
+ name: 'Guest User',
+ role: 'user',
+ type: 'access',
+ });
+
+ await next();
+ };
+};
diff --git a/src/middleware/requireAuth.ts b/src/middleware/requireAuth.ts
new file mode 100644
index 0000000..5abc047
--- /dev/null
+++ b/src/middleware/requireAuth.ts
@@ -0,0 +1,80 @@
+import type { Context, MiddlewareHandler, Next } from 'hono';
+import { getCookie } from 'hono/cookie';
+import { getUserFromContext } from '@/lib/auth/session';
+import { verifyToken } from '@/lib/auth/tokens';
+import { COOKIE_CONFIG } from '@/lib/auth/constants';
+import { getAuthProvider } from '@/lib/providers';
+import { contextLog } from '@/middleware/logger';
+
+export function requireAuth(): MiddlewareHandler {
+ return async (c: Context, next: Next) => {
+ try {
+ const existingUser = getUserFromContext(c);
+ if (existingUser) {
+ return next();
+ }
+
+ const accessToken = getCookie(c, COOKIE_CONFIG.access.name);
+
+ if (!accessToken) {
+ contextLog(
+ c,
+ 'auth',
+ 'debug',
+ '๐ No access token, redirecting to refresh endpoint',
+ );
+ return c.redirect(
+ `/auth/refresh?redirect=${encodeURIComponent(c.req.url)}`,
+ );
+ }
+
+ try {
+ const payload = await verifyToken(accessToken);
+ if (!payload) {
+ contextLog(
+ c,
+ 'auth',
+ 'debug',
+ '๐ Invalid access token, redirecting to refresh endpoint',
+ );
+ return c.redirect(
+ `/auth/refresh?redirect=${encodeURIComponent(c.req.url)}`,
+ );
+ }
+
+ const authProvider = await getAuthProvider();
+ const user = await authProvider.getUserByEmail(payload.email);
+ if (!user) {
+ contextLog(
+ c,
+ 'auth',
+ 'debug',
+ '๐ Redirecting to login - user not found',
+ );
+ return c.redirect('/login');
+ }
+
+ c.set('user', payload);
+ return next();
+ } catch (error) {
+ contextLog(
+ c,
+ 'auth',
+ 'debug',
+ '๐ Access token verification failed, redirecting to refresh endpoint',
+ {
+ error,
+ },
+ );
+ return c.redirect(
+ `/auth/refresh?redirect=${encodeURIComponent(c.req.url)}`,
+ );
+ }
+ } catch (error) {
+ contextLog(c, 'auth', 'error', '๐ Error in requireAuth middleware', {
+ error,
+ });
+ return c.redirect('/login');
+ }
+ };
+}
diff --git a/src/routers/auth.tsx b/src/routers/auth.tsx
new file mode 100644
index 0000000..b43fade
--- /dev/null
+++ b/src/routers/auth.tsx
@@ -0,0 +1,427 @@
+import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
+import {
+ COOKIE_CONFIG,
+ COOKIE_OPTIONS,
+ ERROR_CODES,
+ ERROR_CODE_MAP,
+ TOKEN_EXPIRY_SECONDS,
+} from '@/lib/auth/constants';
+import {
+ blacklistToken,
+ generateCsrfToken,
+ generateToken,
+ verifyToken,
+} from '@/lib/auth/tokens';
+import { sendMagicLink, validateMagicLink } from '@/lib/auth/magic';
+import { contextLog, logHandler } from '@/middleware/logger';
+import { env } from '@/utils/env';
+import { getAuthProvider } from '@/lib/providers';
+import { z } from 'zod';
+import { Hono } from 'hono';
+import Layout from '@/components/Layout';
+import Input from '@/components/Input';
+import Button from '@/components/Button';
+import { HTTP_STATUS } from '@/utils/constants';
+
+const BLACKLIST_TIMEOUT_MS = 100;
+
+const authRouter = new Hono();
+
+authRouter.get('/auth/logout', (c) => {
+ const accessToken = getCookie(c, COOKIE_CONFIG.access.name);
+ const refreshToken = getCookie(c, COOKIE_CONFIG.refresh.name);
+
+ if (accessToken) blacklistToken(accessToken);
+ if (refreshToken) blacklistToken(refreshToken);
+
+ deleteCookie(c, COOKIE_CONFIG.access.name, {
+ ...COOKIE_OPTIONS,
+ path: COOKIE_CONFIG.access.path,
+ });
+ deleteCookie(c, COOKIE_CONFIG.refresh.name, {
+ ...COOKIE_OPTIONS,
+ path: COOKIE_CONFIG.refresh.path,
+ });
+
+ if (c.req.header('HX-Request')) {
+ c.header('HX-Redirect', '/login');
+ logHandler.debug('auth', 'User logged out');
+ return c.body(null);
+ }
+ return c.redirect('/login');
+});
+
+authRouter.get('/login', async (c) => {
+ logHandler.debug('http', 'Login page requested');
+ const csrfToken = await generateCsrfToken();
+ logHandler.debug('token', 'Generated CSRF token');
+ return c.html(
+
+
+
+
+ {env('APP_NAME')}
+
+
+
+
+
+
+
+ Your data is encrypted and secure with{' '}
+ {env('APP_NAME')}
+
+
+ ,
+ HTTP_STATUS.OK,
+ );
+});
+
+authRouter.post('/auth/login', async (c) => {
+ const formData = await c.req.parseBody();
+ const email = formData['email']?.toString().trim().toLowerCase();
+ const csrfToken = formData['csrf']?.toString();
+
+ if (!csrfToken) {
+ return c.json({ error: 'Missing CSRF token' }, HTTP_STATUS.FORBIDDEN);
+ }
+
+ try {
+ await verifyToken(csrfToken);
+ logHandler.debug('auth', 'CSRF token verified successfully', {
+ truncatedToken: csrfToken.substring(0, 20) + '...',
+ });
+ } catch (csrfError) {
+ logHandler.error('auth', 'verifyCsrfToken failed', {
+ truncatedToken: csrfToken.substring(0, 20) + '...',
+ error: csrfError,
+ });
+ return c.json({ error: 'Invalid CSRF token' }, HTTP_STATUS.FORBIDDEN);
+ }
+
+ if (!email || !z.string().email().safeParse(email).success) {
+ logHandler.warn('auth', 'Invalid email format', { email });
+ c.status(HTTP_STATUS.BAD_REQUEST);
+ return c.html(
+
+
+
+ {'>'} [Error] Invalid Email Format
+
+
+ Please enter a valid email address.
+
+
+ Back to Login
+
+
+ ,
+ );
+ }
+
+ logHandler.info('auth', 'Login attempt', { email });
+
+ try {
+ const authProvider = await getAuthProvider();
+ const userProvider = await authProvider.createUser(email);
+ logHandler.info('user', 'User created or found', {
+ id: userProvider.id,
+ email,
+ });
+
+ const token = await generateToken({ type: 'magic', email, role: 'user' });
+ await authProvider.storeToken(token, email, TOKEN_EXPIRY_SECONDS.magic);
+ logHandler.info('token', 'Magic link token stored', { email });
+
+ if (env('NODE_ENV') === 'development') {
+ const host = env('HOST');
+ const magicLink = `${host}/auth/verify?token=${token}`;
+ logHandler.info('auth', 'Development magic link', { magicLink });
+
+ c.status(HTTP_STATUS.OK);
+ return c.html(
+
+
+ {'>'} Development Mode
+
+
+ Check the console for the magic link URL.
+
+
+ Back to Login
+
+ ,
+ );
+ }
+
+ const result = await sendMagicLink(email);
+ if (result.error) {
+ logHandler.error('auth', 'Failed to send magic link', {
+ error: result.error,
+ });
+ c.status(HTTP_STATUS.INTERNAL_SERVER_ERROR);
+ return c.html(
+
+
+ {'>'} [Error] Failed to Send Magic Link
+
+
+ There was an issue sending the login link. Please try again later.
+
+
+ Back to Login
+
+ ,
+ );
+ }
+
+ logHandler.info('auth', 'Magic link process completed', { email });
+ c.status(HTTP_STATUS.OK);
+ return c.html(
+
+
+ {'>'} [System] Check Your Email
+
+
+ We've sent a magic link to{' '}
+ {email} . Click the link
+ in the email to log in.
+
+
+ Can't find the email? Check your spam folder or try again.
+
+ ,
+ );
+ } catch (error) {
+ logHandler.error('auth', 'Login process failed', {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ c.status(HTTP_STATUS.INTERNAL_SERVER_ERROR);
+ return c.html(
+
+
+ {'>'} [Fatal] System Error
+
+
+ An unexpected error occurred. Please try again later.
+
+
+ Back to Login
+
+ ,
+ );
+ }
+});
+
+authRouter.get('/auth/verify', async (c) => {
+ const token = c.req.query('token');
+ logHandler.debug('auth', 'Verifying token', { token });
+
+ if (!token) {
+ logHandler.warn('auth', 'Missing verification token');
+ c.status(HTTP_STATUS.BAD_REQUEST);
+ return c.redirect(
+ `/login?error=${ERROR_CODES.VERIFICATION_REQUIRED}`,
+ HTTP_STATUS.REDIRECT,
+ );
+ }
+
+ try {
+ const payload = await verifyToken(token);
+ if (payload.type !== 'magic') {
+ throw new Error('Invalid token type');
+ }
+ if (!payload.email || typeof payload.email !== 'string') {
+ throw new Error('Invalid token payload');
+ }
+
+ logHandler.debug('token', 'Validating magic link token against storage', {
+ email: payload.email,
+ });
+ const isValid = await validateMagicLink(token, payload.email);
+ if (!isValid) {
+ logHandler.warn('token', 'Token not found in storage or invalid', {
+ email: payload.email,
+ });
+ throw new Error('Token not found or invalid');
+ }
+ logHandler.debug('token', 'Token validated successfully against storage', {
+ email: payload.email,
+ });
+
+ logHandler.debug('auth', 'verifyMagicLinkToken completed successfully', {
+ truncatedToken: token.substring(0, 20) + '...',
+ });
+
+ const accessToken = await generateToken({
+ type: 'access',
+ email: payload.email,
+ role: payload.role,
+ });
+ const refreshToken = await generateToken({
+ type: 'refresh',
+ email: payload.email,
+ role: payload.role,
+ });
+ logHandler.debug('auth', 'Generated session tokens');
+
+ setCookie(c, COOKIE_CONFIG.access.name, accessToken, {
+ ...COOKIE_OPTIONS,
+ path: COOKIE_CONFIG.access.path,
+ maxAge: COOKIE_CONFIG.access.maxAge,
+ expires: new Date(Date.now() + COOKIE_CONFIG.access.maxAge * 1000),
+ secure: true,
+ });
+
+ setCookie(c, COOKIE_CONFIG.refresh.name, refreshToken, {
+ ...COOKIE_OPTIONS,
+ path: COOKIE_CONFIG.refresh.path,
+ maxAge: COOKIE_CONFIG.refresh.maxAge,
+ });
+
+ const response = c.redirect(
+ `/dashboard?justLoggedIn=true`,
+ HTTP_STATUS.REDIRECT,
+ );
+ logHandler.debug('auth', 'Generated response');
+
+ setTimeout(() => {
+ blacklistToken(token);
+ logHandler.debug('auth', 'Blacklisted magic token');
+ }, BLACKLIST_TIMEOUT_MS);
+
+ return response;
+ } catch (verifyError) {
+ logHandler.warn('auth', 'Token verification failed', {
+ error: verifyError,
+ truncatedToken: token.substring(0, 20) + '...',
+ });
+ const errorCode =
+ verifyError instanceof Error && 'code' in verifyError && verifyError.code
+ ? (ERROR_CODE_MAP[verifyError.code as keyof typeof ERROR_CODE_MAP] ??
+ ERROR_CODES.INVALID)
+ : ERROR_CODES.INVALID;
+ return c.redirect(`/login?error=${errorCode}`, HTTP_STATUS.REDIRECT);
+ }
+});
+
+authRouter.all('/auth/refresh', async (c) => {
+ const isGet = c.req.method === 'GET';
+ const redirectUrl = isGet ? c.req.query('redirect') : null;
+ const refreshToken = getCookie(c, COOKIE_CONFIG.refresh.name);
+
+ contextLog(c, 'auth', 'debug', 'Refresh token request received', {
+ method: c.req.method,
+ hasToken: Boolean(refreshToken),
+ tokenLength: refreshToken?.length,
+ hasRedirect: Boolean(redirectUrl),
+ });
+
+ if (!refreshToken) {
+ contextLog(c, 'auth', 'warn', 'Missing refresh token');
+ return isGet
+ ? c.redirect('/login')
+ : c.json({ message: 'Unauthorized' }, HTTP_STATUS.UNAUTHORIZED);
+ }
+
+ try {
+ contextLog(c, 'auth', 'debug', 'Attempting to verify refresh token');
+
+ const payload = await verifyToken(refreshToken);
+
+ contextLog(c, 'auth', 'debug', 'Refresh token verified', {
+ email: payload.email,
+ type: payload.type,
+ });
+
+ if (payload.type !== 'refresh') {
+ contextLog(c, 'auth', 'warn', 'Invalid token type', {
+ type: payload.type,
+ });
+ return isGet
+ ? c.redirect('/login')
+ : c.json({ message: 'Invalid token' }, HTTP_STATUS.FORBIDDEN);
+ }
+
+ const authProvider = await getAuthProvider();
+ const userProvider = await authProvider.getUserByEmail(payload.email);
+
+ if (!userProvider) {
+ contextLog(c, 'auth', 'warn', 'User not found', { email: payload.email });
+ return isGet
+ ? c.redirect('/login')
+ : c.json({ message: 'User not found' }, HTTP_STATUS.UNAUTHORIZED);
+ }
+
+ const newAccessToken = await generateToken({
+ type: 'access',
+ email: payload.email,
+ role: payload.role,
+ });
+ contextLog(c, 'auth', 'debug', 'Generated new access token');
+
+ setCookie(c, COOKIE_CONFIG.access.name, newAccessToken, {
+ ...COOKIE_OPTIONS,
+ path: COOKIE_CONFIG.access.path,
+ maxAge: COOKIE_CONFIG.access.maxAge,
+ expires: new Date(Date.now() + COOKIE_CONFIG.access.maxAge * 1000),
+ secure: true,
+ });
+
+ if (isGet) {
+ const targetUrl = redirectUrl || '/dashboard';
+ contextLog(c, 'auth', 'debug', 'Redirecting after token refresh', {
+ targetUrl,
+ });
+ return c.redirect(targetUrl);
+ }
+
+ return c.json({ message: 'Token refreshed' }, HTTP_STATUS.OK);
+ } catch (error) {
+ contextLog(c, 'auth', 'error', 'Failed to refresh token', {
+ error: error instanceof Error ? error.message : String(error),
+ code:
+ error instanceof Error && 'code' in error
+ ? (error as { code: string }).code
+ : 'unknown',
+ });
+
+ return isGet
+ ? c.redirect('/login')
+ : c.json(
+ { message: 'Failed to refresh token' },
+ HTTP_STATUS.INTERNAL_SERVER_ERROR,
+ );
+ }
+});
+
+export default authRouter;
diff --git a/src/routers/dashboard.tsx b/src/routers/dashboard.tsx
new file mode 100644
index 0000000..345c5b3
--- /dev/null
+++ b/src/routers/dashboard.tsx
@@ -0,0 +1,124 @@
+import Layout from '@/components/Layout';
+import { Hono } from 'hono';
+import { contextLog } from '@/middleware/logger';
+import { getUserFromContext } from '@/lib/auth/session';
+import { env } from '@/utils/env';
+import PageHeader from '@/components/PageHeader';
+
+const dashboardRouter = new Hono();
+
+dashboardRouter.get('/', async (c) => {
+ const user = getUserFromContext(c);
+ const userEmail = user?.email;
+ const justLoggedIn = c.req.query('justLoggedIn') === 'true';
+
+ contextLog(c, 'ui', 'debug', 'Viewing dashboard', {
+ userEmail,
+ justLoggedIn,
+ });
+
+ return c.html(
+
+
+
+
+
+
+
+
+
+ Welcome!
+
+
+
+
+ {env('APP_NAME')}
+
+
+ This application provides a base shell with authentication and
+ common UI components. Use it as a starting point for your own
+ applications.
+
+
+
+
+
+
+
+
+ ,
+ );
+});
+
+export default dashboardRouter;
diff --git a/src/routers/modal.tsx b/src/routers/modal.tsx
new file mode 100644
index 0000000..0866c6b
--- /dev/null
+++ b/src/routers/modal.tsx
@@ -0,0 +1,13 @@
+import { Hono } from 'hono';
+import { logHandler } from '@/middleware/logger';
+
+const modalRouter = new Hono();
+
+modalRouter.get('/close', async (c) => {
+ logHandler.debug('ui', 'Closing modal');
+ return c.html(
+
,
+ );
+});
+
+export default modalRouter;
diff --git a/src/routers/profile.tsx b/src/routers/profile.tsx
new file mode 100644
index 0000000..7aa3785
--- /dev/null
+++ b/src/routers/profile.tsx
@@ -0,0 +1,226 @@
+import { Hono } from 'hono';
+import Layout from '@/components/Layout';
+import { contextLog } from '@/middleware/logger';
+import Button from '@/components/Button';
+import { getUserFromContext } from '@/lib/auth/session';
+import FormField from '@/components/FormField';
+import Modal from '@/components/Modal';
+import Section from '@/components/Section';
+import FieldRow from '@/components/FieldRow';
+import PageHeader from '@/components/PageHeader';
+import Select from '@/components/Select';
+import type { TokenPayload } from '@/lib/auth/constants';
+
+const profileRouter = new Hono();
+
+profileRouter.get('/', async (c) => {
+ contextLog(c, 'ui', 'debug', 'Rendering profile page');
+ const user = getUserFromContext(c) as TokenPayload;
+
+ return c.html(
+
+
+
Save Changes}
+ />
+
+
+
+
+
+
+
+
+ Change
+
+
+
+
+
+ Enable
+
+
+
+
+
+
+
+
+ ,
+ );
+});
+
+profileRouter.get('/change-password', async (c) => {
+ return c.html(
+
+
+
+
+
+
+ Password must be at least 8 characters long and include a number and a
+ special character.
+
+
+ ,
+ );
+});
+
+profileRouter.get('/enable-2fa', async (c) => {
+ return c.html(
+
+
+
+
+
+ Scan this QR code with your authenticator app
+
+
+
+
+
QR Code
+
App-Shell-2FA
+
+
+
+
+ Can't scan? Use this code to manually set up:
+
+
+ ABCD-EFGH-IJKL-MNOP
+
+
+
+
+
+
+
+ After scanning the QR code with your authenticator app, enter the
+ 6-digit code displayed in the app to verify setup.
+
+
+ ,
+ );
+});
+
+profileRouter.post('/update-password', async (c) => {
+ contextLog(c, 'ui', 'debug', 'Processing password update');
+
+ const isHtmxRequest = c.req.header('HX-Request') === 'true';
+
+ if (isHtmxRequest) {
+ c.header('HX-Redirect', '/profile?success=Password+successfully+updated!');
+ return c.body(null);
+ } else {
+ return c.redirect('/profile?success=Password+successfully+updated!');
+ }
+});
+
+profileRouter.post('/activate-2fa', async (c) => {
+ contextLog(c, 'ui', 'debug', 'Processing 2FA activation');
+
+ const isHtmxRequest = c.req.header('HX-Request') === 'true';
+
+ if (isHtmxRequest) {
+ c.header(
+ 'HX-Redirect',
+ '/profile?success=Two-factor+authentication+enabled+successfully!',
+ );
+ return c.body(null);
+ } else {
+ return c.redirect(
+ '/profile?success=Two-factor+authentication+enabled+successfully!',
+ );
+ }
+});
+
+export default profileRouter;
diff --git a/src/routers/settings.tsx b/src/routers/settings.tsx
new file mode 100644
index 0000000..56477ea
--- /dev/null
+++ b/src/routers/settings.tsx
@@ -0,0 +1,81 @@
+import { Hono } from 'hono';
+import Layout from '@/components/Layout';
+import { contextLog } from '@/middleware/logger';
+import Button from '@/components/Button';
+import Toggle from '@/components/Toggle';
+import Section from '@/components/Section';
+import FieldRow from '@/components/FieldRow';
+import PageHeader from '@/components/PageHeader';
+import DangerZone from '@/components/DangerZone';
+import Select from '@/components/Select';
+
+const settingsRouter = new Hono();
+
+settingsRouter.get('/', async (c) => {
+ contextLog(c, 'ui', 'debug', 'Rendering settings page');
+
+ return c.html(
+
+
+
Save Preferences}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+ }
+ />
+
+
+
+
+
+
+ ,
+ );
+});
+
+export default settingsRouter;
diff --git a/src/server.tsx b/src/server.tsx
index ad9993e..80b67ed 100644
--- a/src/server.tsx
+++ b/src/server.tsx
@@ -1,42 +1,110 @@
-import { Hono } from "hono";
-import { serveStatic } from "hono/bun";
-import { logger } from "hono/logger";
-import Layout from "./components/Layout.tsx";
+import { Hono } from 'hono';
+import { serve } from '@hono/node-server';
+import { cors } from 'hono/cors';
+import { prettyJSON } from 'hono/pretty-json';
+import { serveStatic } from '@hono/node-server/serve-static';
+import { env } from '@/utils/env';
+import { contextLog, logHandler, logging } from '@/middleware/logger';
+import { noAuth } from '@/middleware/noAuth';
+import { requireAuth } from '@/middleware/requireAuth';
+import { errorHandler } from '@/middleware/error-handler';
+import authRouter from '@/routers/auth';
+import dashboardRouter from '@/routers/dashboard';
+import profileRouter from '@/routers/profile';
+import settingsRouter from '@/routers/settings';
+import modalRouter from '@/routers/modal';
+import Layout from '@/components/Layout';
+import Button from '@/components/Button';
+import { secureHeaders } from 'hono/secure-headers';
+import { HTTP_STATUS } from '@/utils/constants';
const app = new Hono();
+logHandler.info('http', 'Initializing server', {
+ environment: env('NODE_ENV'),
+ skipAuth: env('SKIP_AUTH') === 'true',
+});
-app.use("/styles/*", serveStatic({ root: "./public/" }));
-app.use("*", logger());
-
-app.get("/", (c) =>
- c.html(
-
-
-
- ๐ hyperwave
-
-
- โจ๏ธ edit
-
- src/server.tsx
-
-
-
- ๐ read the
-
- friendly manual
-
- !
-
-
+app.use('*', logging());
+app.use('*', cors());
+app.use('*', prettyJSON());
+app.use('*', errorHandler());
+app.use('*', secureHeaders());
+
+const skipAuth = env('SKIP_AUTH') === 'true';
+if (skipAuth) {
+ logHandler.info(
+ 'auth',
+ 'Using noAuth middleware - authentication is disabled',
+ );
+ app.use('*', noAuth());
+} else {
+ logHandler.info(
+ 'auth',
+ 'Using requireAuth middleware - authentication is enabled',
+ );
+ app.use('/api/*', requireAuth());
+ app.use('/dashboard', requireAuth());
+ app.use('/profile', requireAuth());
+ app.use('/settings', requireAuth());
+}
+
+app.use('/styles/*', serveStatic({ root: './public' }));
+app.use('/favicon.svg', serveStatic({ path: './public/favicon.svg' }));
+
+app.route('/', authRouter);
+app.route('/dashboard', dashboardRouter);
+app.route('/profile', profileRouter);
+app.route('/settings', settingsRouter);
+app.route('/modal', modalRouter);
+
+app.get('/', async (c) => {
+ contextLog(c, 'http', 'info', 'User redirected to dashboard');
+ return c.redirect('/dashboard');
+});
+
+app.notFound((c) => {
+ logHandler.warn('http', 'Not found', { path: c.req.path });
+
+ c.status(HTTP_STATUS.NOT_FOUND);
+
+ const path = c.req.path;
+ if (
+ path.startsWith('/assets/') ||
+ path.startsWith('/styles/') ||
+ path.startsWith('/js/') ||
+ path.startsWith('/images/')
+ ) {
+ return c.json({ error: 'File not found', path }, HTTP_STATUS.NOT_FOUND);
+ }
+
+ return c.html(
+
+ โ Page not found
+
+ The page you're looking for doesn't exist.
+
+
+ Go back home
+
,
- ),
-);
+ );
+});
+
+app.onError((err, _c) => {
+ logHandler.error('system', 'Server error', { error: err });
+ const status =
+ err instanceof Error && 'status' in err && typeof err.status === 'number'
+ ? err.status
+ : HTTP_STATUS.INTERNAL_SERVER_ERROR;
+ return new Response(
+ JSON.stringify({ error: err.message || 'Internal server error' }),
+ { status },
+ );
+});
+
+const port = parseInt(env('PORT'), 10);
+logHandler.info('system', `Server starting on port ${port}...`);
-export default {
- port: process.env.PORT || 1234,
- fetch: app.fetch,
-};
+serve({ fetch: app.fetch, port }, (info) => {
+ logHandler.info('system', `Server started on port ${info.port}`);
+});
diff --git a/src/static/Logo.tsx b/src/static/Logo.tsx
new file mode 100644
index 0000000..0a2874b
--- /dev/null
+++ b/src/static/Logo.tsx
@@ -0,0 +1,15 @@
+import type { FC } from 'hono/jsx';
+
+type LogoProps = {
+ variant?: 'default' | 'compact';
+};
+
+const Logo: FC = ({ variant = 'default' }) => {
+ return (
+
+ ๐
+
+ );
+};
+
+export default Logo;
diff --git a/src/styles/uno.config.ts b/src/styles/uno.config.ts
new file mode 100644
index 0000000..ee25381
--- /dev/null
+++ b/src/styles/uno.config.ts
@@ -0,0 +1,108 @@
+import { defineConfig, presetAttributify, presetWind } from 'unocss';
+import presetWebFonts from '@unocss/preset-web-fonts';
+
+export default defineConfig({
+ presets: [
+ presetAttributify(),
+ presetWind({
+ dark: 'class',
+ }),
+ presetWebFonts({
+ provider: 'google',
+ fonts: {
+ primary: [
+ {
+ name: 'Inter',
+ weights: ['400', '500', '600', '700'],
+ italic: true,
+ },
+ ],
+ mono: [
+ {
+ name: 'Fira Code',
+ weights: ['300', '400', '500', '600', '700'],
+ },
+ {
+ name: 'JetBrains Mono',
+ weights: ['300', '400', '500', '600', '700'],
+ },
+ {
+ name: 'Source Code Pro',
+ weights: ['300', '400', '500', '600', '700'],
+ },
+ ],
+ },
+ }),
+ ],
+ theme: {
+ colors: {
+ 'app-background': '#0c0a14',
+ 'app-background-alt': '#161421',
+ 'app-background-accent': '#201e2a',
+ 'app-background-overlay': 'rgba(12, 10, 20, 0.95)',
+ 'app-surface': '#2a2735',
+ 'app-surface-hover': 'rgba(139, 92, 246, 0.15)',
+ 'app-surface-dim': 'rgba(255, 255, 255, 0.06)',
+ 'app-surface-card': 'rgba(42, 39, 53, 0.9)',
+ 'text-primary': '#fafafa',
+ 'text-secondary': '#e5e5e5',
+ 'text-tertiary': '#a3a3a3',
+ 'text-disabled': '#737373',
+ 'text-inverse': '#0c0a14',
+ 'text-white': '#ffffff',
+ 'text-white-dim': 'rgba(255, 255, 255, 0.85)',
+ 'border-primary': '#8b5cf6',
+ 'border-primary-hover': '#7c3aed',
+ 'border-secondary': '#a3a3a3',
+ 'border-accent': '#a855f7',
+ 'border-danger': '#f87171',
+ 'border-warning': '#fbbf24',
+ 'border-info': '#38bdf8',
+ 'border-subtle': 'rgba(255, 255, 255, 0.1)',
+ 'status-success': '#34d399',
+ 'status-warning': '#fbbf24',
+ 'status-error': '#f87171',
+ 'status-info': '#38bdf8',
+ 'interactive-primary': '#8b5cf6',
+ 'interactive-primary-hover': '#7c3aed',
+ 'interactive-secondary': '#a855f7',
+ 'interactive-secondary-hover': '#9333ea',
+ 'interactive-danger': '#f87171',
+ 'interactive-danger-hover': '#ef4444',
+ },
+ },
+ variants: [
+ (
+ matcher: string,
+ ): { matcher: string; selector: (s: string) => string } | undefined => {
+ if (matcher.startsWith('htmx-request:')) {
+ return {
+ matcher: matcher.slice('htmx-request:'.length),
+ selector: (s: string): string =>
+ `.htmx-request ${s}, ${s}.htmx-request`,
+ };
+ }
+ if (matcher.startsWith('htmx-settling:')) {
+ return {
+ matcher: matcher.slice('htmx-settling:'.length),
+ selector: (s: string): string =>
+ `.htmx-settling ${s}, ${s}.htmx-settling`,
+ };
+ }
+ if (matcher.startsWith('htmx-swapping:')) {
+ return {
+ matcher: matcher.slice('htmx-swapping:'.length),
+ selector: (s: string): string =>
+ `.htmx-swapping ${s}, ${s}.htmx-swapping`,
+ };
+ }
+ if (matcher.startsWith('htmx-added:')) {
+ return {
+ matcher: matcher.slice('htmx-added:'.length),
+ selector: (s: string): string => `.htmx-added ${s}, ${s}.htmx-added`,
+ };
+ }
+ return undefined;
+ },
+ ],
+});
diff --git a/src/types/hono.d.ts b/src/types/hono.d.ts
new file mode 100644
index 0000000..43046d4
--- /dev/null
+++ b/src/types/hono.d.ts
@@ -0,0 +1,7 @@
+import type { TokenPayload } from '@/lib/auth/constants';
+
+declare module 'hono' {
+ interface ContextVariableMap {
+ user: TokenPayload;
+ }
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
new file mode 100644
index 0000000..b8b00e5
--- /dev/null
+++ b/src/utils/constants.ts
@@ -0,0 +1,28 @@
+export const TIME = {
+ DAYS_IN_WEEK: 7,
+ HOURS_IN_DAY: 24,
+ MILLISECONDS_IN_HOUR: 3600000,
+ MINUTES_IN_HOUR: 60,
+ SECONDS_IN_DAY: 86400,
+ SECONDS_IN_HOUR: 3600,
+ SECONDS_IN_MINUTE: 60,
+};
+
+export const HTTP_STATUS = {
+ OK: 200,
+ BAD_REQUEST: 400,
+ UNAUTHORIZED: 401,
+ FORBIDDEN: 403,
+ NOT_FOUND: 404,
+ METHOD_NOT_ALLOWED: 405,
+ REQUEST_TIMEOUT: 408,
+ CONFLICT: 409,
+ TOO_MANY_REQUESTS: 429,
+ INTERNAL_SERVER_ERROR: 500,
+ NOT_IMPLEMENTED: 501,
+ BAD_GATEWAY: 502,
+ SERVICE_UNAVAILABLE: 503,
+ GATEWAY_TIMEOUT: 504,
+ REDIRECT: 302,
+ DUPLICATE_KEY_ERROR: 11000,
+} as const;
diff --git a/src/utils/env.ts b/src/utils/env.ts
new file mode 100644
index 0000000..1a0569a
--- /dev/null
+++ b/src/utils/env.ts
@@ -0,0 +1,41 @@
+import { z } from 'zod';
+import { logHandler } from '@/middleware/logger';
+
+const DEFAULT_SECRET_KEY_LENGTH = 32;
+const DEFAULT_HTTP_PORT = 3000;
+
+const envSchema = z.object({
+ APP_NAME: z.string().default('hyperwave'),
+ DATABASE_PATH: z.string().default('app.db'),
+ EMAIL_FROM: z.string().default('noreply@example.com'),
+ HOST: z.string().default('http://localhost:3000'),
+ NODE_ENV: z.enum(['development', 'test', 'production']),
+ PORT: z.coerce.number().default(DEFAULT_HTTP_PORT),
+ RESEND_API_KEY: z.string().optional().or(z.string()),
+ SECRET_KEY: z.string().min(DEFAULT_SECRET_KEY_LENGTH),
+ SKIP_AUTH: z.string().default('false'),
+});
+
+export function env(key: keyof typeof envSchema.shape): string {
+ try {
+ const value = envSchema.parse(process.env)[key];
+ if (value === undefined && envSchema.shape[key]?.isOptional()) {
+ return '';
+ }
+ if (value === null) {
+ logHandler.error(
+ 'http',
+ `Attempted to access unknown environment variable: ${key}`,
+ );
+ throw new Error(`Environment variable ${key} is not set`);
+ }
+ return String(value);
+ } catch (error) {
+ logHandler.error(
+ 'http',
+ `Failed to retrieve environment variable: ${key}`,
+ { error },
+ );
+ throw new Error(`Failed to retrieve environment variable: ${key}`);
+ }
+}
diff --git a/src/utils/script-loader.ts b/src/utils/script-loader.ts
new file mode 100644
index 0000000..d14c8f5
--- /dev/null
+++ b/src/utils/script-loader.ts
@@ -0,0 +1,100 @@
+import { raw } from 'hono/html';
+
+export const createScriptLoader = (id: string): string => {
+ return `
+ (function() {
+ const script = document.createElement('script');
+ script.id = "${id}";
+ document.head.appendChild(script);
+ return script;
+ })()
+ `;
+};
+
+export const loadExternalScript = (id: string, src?: string): string => {
+ const scriptSrc = src || deriveScriptPath(id);
+
+ return `
+ (function() {
+ const script = document.createElement('script');
+ script.id = "${id}";
+ script.src = "${scriptSrc}";
+ script.async = true;
+ document.head.appendChild(script);
+ return script;
+ })()
+ `;
+};
+
+export const createRawScriptLoader = (
+ id: string,
+ scriptId?: string,
+): ReturnType => {
+ const content = createScriptLoader(id);
+ return raw(
+ ``,
+ );
+};
+
+export const loadRawExternalScript = (
+ id: string,
+ src?: string,
+ scriptId?: string,
+): ReturnType => {
+ const content = loadExternalScript(id, src);
+ return raw(
+ ``,
+ );
+};
+
+export const createRawInlineScript = (
+ id: string,
+ content: string,
+): ReturnType => {
+ return raw(``);
+};
+
+function deriveScriptPath(id: string): string {
+ const baseName = id
+ .replace(/-?(js|script|init)$/, '')
+ .replace(/^(js-|script-)/, '');
+
+ const fileName = baseName
+ .replace(/([A-Z])/g, '-$1')
+ .toLowerCase()
+ .replace(/^-/, '');
+
+ return `/js/${fileName}.js`;
+}
+
+export const createToastDismissal = (
+ id: string,
+ delay: number,
+ animationDuration: number,
+): string => {
+ return `
+ (function() {
+ setTimeout(() => {
+ const toast = document.getElementById('${id}');
+ if (toast) {
+ toast.style.opacity = '0';
+ setTimeout(() => {
+ toast.remove();
+ }, ${animationDuration});
+ }
+ }, ${delay});
+ })()
+ `;
+};
+
+export const createRawToastDismissal = (
+ id: string,
+ delay: number,
+ animationDuration: number,
+ scriptId?: string,
+): ReturnType => {
+ const content = createToastDismissal(id, delay, animationDuration);
+ return raw(
+ ``,
+ );
+};
diff --git a/test/fake.test.ts b/test/fake.test.ts
deleted file mode 100644
index b0c260d..0000000
--- a/test/fake.test.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { describe, expect, it } from "bun:test";
-
-describe("Smoke test", () => {
- it("Should be able to run a test", () => {
- expect(true).toBeTruthy();
- });
-});
diff --git a/tsconfig.json b/tsconfig.json
index 4fb3088..885cac8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,57 @@
{
- "compilerOptions": {
+ "compilerOptions": {
+ "downlevelIteration": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"allowImportingTsExtensions": true,
"noEmit": true,
"types": [
- "bun-types"
- ]
- }
-}
+ "bun-types",
+ "@types/node"
+ ],
+ "skipLibCheck": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "strict": true,
+ "target": "ESNext",
+ "module": "ESNext",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*",
+ "./*"
+ ]
+ },
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "noImplicitOverride": true,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "declaration": true,
+ "incremental": true,
+ "useUnknownInCatchVariables": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "verbatimModuleSyntax": true,
+ "alwaysStrict": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "strictBindCallApply": true,
+ "strictPropertyInitialization": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true
+ },
+ "include": [
+ "src/**/*"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
new file mode 100644
index 0000000..21dd4ef
--- /dev/null
+++ b/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.dom.asynciterable.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/typescript/lib/lib.esnext.full.d.ts","./node_modules/hono/dist/types/utils/html.d.ts","./node_modules/hono/dist/types/jsx/constants.d.ts","./node_modules/hono/dist/types/jsx/children.d.ts","./node_modules/hono/dist/types/jsx/components.d.ts","./node_modules/hono/dist/types/jsx/dom/hooks/index.d.ts","./node_modules/hono/dist/types/jsx/hooks/index.d.ts","./node_modules/hono/dist/types/jsx/streaming.d.ts","./node_modules/hono/dist/types/utils/mime.d.ts","./node_modules/hono/dist/types/utils/types.d.ts","./node_modules/hono/dist/types/jsx/intrinsic-elements.d.ts","./node_modules/hono/dist/types/jsx/types.d.ts","./node_modules/hono/dist/types/jsx/index.d.ts","./node_modules/hono/dist/types/jsx/context.d.ts","./node_modules/hono/dist/types/jsx/base.d.ts","./node_modules/hono/dist/types/jsx/jsx-dev-runtime.d.ts","./node_modules/hono/dist/types/helper/html/index.d.ts","./node_modules/hono/dist/types/jsx/jsx-runtime.d.ts","./node_modules/hono/dist/types/router.d.ts","./node_modules/hono/dist/types/utils/headers.d.ts","./node_modules/hono/dist/types/utils/http-status.d.ts","./node_modules/hono/dist/types/types.d.ts","./node_modules/hono/dist/types/utils/body.d.ts","./node_modules/hono/dist/types/request.d.ts","./node_modules/hono/dist/types/context.d.ts","./node_modules/hono/dist/types/hono-base.d.ts","./node_modules/hono/dist/types/hono.d.ts","./node_modules/hono/dist/types/client/types.d.ts","./node_modules/hono/dist/types/client/client.d.ts","./node_modules/hono/dist/types/client/index.d.ts","./node_modules/hono/dist/types/index.d.ts","./node_modules/@hono/node-server/dist/types.d.ts","./node_modules/@hono/node-server/dist/server.d.ts","./node_modules/@hono/node-server/dist/listener.d.ts","./node_modules/@hono/node-server/dist/request.d.ts","./node_modules/@hono/node-server/dist/index.d.ts","./node_modules/hono/dist/types/middleware/cors/index.d.ts","./node_modules/hono/dist/types/middleware/pretty-json/index.d.ts","./node_modules/@hono/node-server/dist/serve-static.d.ts","./node_modules/zod/lib/helpers/typeAliases.d.ts","./node_modules/zod/lib/helpers/util.d.ts","./node_modules/zod/lib/ZodError.d.ts","./node_modules/zod/lib/locales/en.d.ts","./node_modules/zod/lib/errors.d.ts","./node_modules/zod/lib/helpers/parseUtil.d.ts","./node_modules/zod/lib/helpers/enumUtil.d.ts","./node_modules/zod/lib/helpers/errorUtil.d.ts","./node_modules/zod/lib/helpers/partialUtil.d.ts","./node_modules/zod/lib/standard-schema.d.ts","./node_modules/zod/lib/types.d.ts","./node_modules/zod/lib/external.d.ts","./node_modules/zod/lib/index.d.ts","./node_modules/zod/index.d.ts","./src/middleware/logger.ts","./src/utils/env.ts","./src/middleware/noAuth.ts","./node_modules/hono/dist/types/utils/cookie.d.ts","./node_modules/hono/dist/types/helper/cookie/index.d.ts","./src/utils/constants.ts","./src/lib/auth/constants.ts","./src/lib/auth/session.ts","./node_modules/jose/dist/types/types.d.ts","./node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","./node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","./node_modules/jose/dist/types/jwe/general/decrypt.d.ts","./node_modules/jose/dist/types/jwe/general/encrypt.d.ts","./node_modules/jose/dist/types/jws/compact/verify.d.ts","./node_modules/jose/dist/types/jws/flattened/verify.d.ts","./node_modules/jose/dist/types/jws/general/verify.d.ts","./node_modules/jose/dist/types/jwt/verify.d.ts","./node_modules/jose/dist/types/jwt/decrypt.d.ts","./node_modules/jose/dist/types/jwt/produce.d.ts","./node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","./node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","./node_modules/jose/dist/types/jws/compact/sign.d.ts","./node_modules/jose/dist/types/jws/flattened/sign.d.ts","./node_modules/jose/dist/types/jws/general/sign.d.ts","./node_modules/jose/dist/types/jwt/sign.d.ts","./node_modules/jose/dist/types/jwt/encrypt.d.ts","./node_modules/jose/dist/types/jwk/thumbprint.d.ts","./node_modules/jose/dist/types/jwk/embedded.d.ts","./node_modules/jose/dist/types/jwks/local.d.ts","./node_modules/jose/dist/types/jwks/remote.d.ts","./node_modules/jose/dist/types/jwt/unsecured.d.ts","./node_modules/jose/dist/types/key/export.d.ts","./node_modules/jose/dist/types/key/import.d.ts","./node_modules/jose/dist/types/util/decode_protected_header.d.ts","./node_modules/jose/dist/types/util/decode_jwt.d.ts","./node_modules/jose/dist/types/util/errors.d.ts","./node_modules/jose/dist/types/key/generate_key_pair.d.ts","./node_modules/jose/dist/types/key/generate_secret.d.ts","./node_modules/jose/dist/types/util/base64url.d.ts","./node_modules/jose/dist/types/util/runtime.d.ts","./node_modules/jose/dist/types/index.d.ts","./src/lib/auth/tokens.ts","./node_modules/uuid/dist/cjs/types.d.ts","./node_modules/uuid/dist/cjs/max.d.ts","./node_modules/uuid/dist/cjs/nil.d.ts","./node_modules/uuid/dist/cjs/parse.d.ts","./node_modules/uuid/dist/cjs/stringify.d.ts","./node_modules/uuid/dist/cjs/v1.d.ts","./node_modules/uuid/dist/cjs/v1ToV6.d.ts","./node_modules/uuid/dist/cjs/v35.d.ts","./node_modules/uuid/dist/cjs/v3.d.ts","./node_modules/uuid/dist/cjs/v4.d.ts","./node_modules/uuid/dist/cjs/v5.d.ts","./node_modules/uuid/dist/cjs/v6.d.ts","./node_modules/uuid/dist/cjs/v6ToV1.d.ts","./node_modules/uuid/dist/cjs/v7.d.ts","./node_modules/uuid/dist/cjs/validate.d.ts","./node_modules/uuid/dist/cjs/version.d.ts","./node_modules/uuid/dist/cjs/index.d.ts","./src/lib/database.ts","./src/lib/providers/auth.ts","./node_modules/resend/dist/index.d.ts","./src/lib/providers/email.ts","./src/lib/providers/index.ts","./src/middleware/requireAuth.ts","./node_modules/hono/dist/types/http-exception.d.ts","./src/middleware/error-handler.ts","./src/lib/auth/magic.ts","./src/components/Link.tsx","./src/components/Navigation.tsx","./src/utils/script-loader.ts","./src/components/Toast.tsx","./src/components/Layout.tsx","./src/components/Input.tsx","./src/components/Button.tsx","./src/routers/auth.tsx","./src/components/PageHeader.tsx","./src/routers/dashboard.tsx","./src/components/FormField.tsx","./src/components/ModalHeader.tsx","./src/components/ModalFooter.tsx","./src/components/Modal.tsx","./src/components/Section.tsx","./src/components/FieldRow.tsx","./src/components/Select.tsx","./src/routers/profile.tsx","./src/components/Toggle.tsx","./src/components/DangerZone.tsx","./src/routers/settings.tsx","./src/routers/modal.tsx","./node_modules/hono/dist/types/middleware/secure-headers/permissions-policy.d.ts","./node_modules/hono/dist/types/middleware/secure-headers/secure-headers.d.ts","./node_modules/hono/dist/types/middleware/secure-headers/index.d.ts","./src/server.tsx","./src/__tests__/lib/auth/tokens.test.ts","./src/components/Alert.tsx","./src/components/Card.tsx","./src/components/CheckboxGroup.tsx","./src/components/Expandable.tsx","./src/components/ExpandablePanel.tsx","./src/components/FormattedText.tsx","./src/components/IconButton.tsx","./src/components/ListGroup.tsx","./src/components/ListItem.tsx","./src/components/ProgressBar.tsx","./src/components/Stat.tsx","./src/static/Logo.tsx","./node_modules/magic-string/dist/magic-string.cjs.d.ts","./node_modules/@antfu/utils/dist/index.d.ts","./node_modules/unconfig/dist/shared/unconfig.CCW63Z-Z.d.mts","./node_modules/unconfig/dist/index.d.mts","./node_modules/@unocss/core/dist/index.d.ts","./node_modules/@unocss/preset-mini/dist/shared/preset-mini.BjJC-NnU.d.ts","./node_modules/@unocss/preset-mini/dist/colors.d.ts","./node_modules/@unocss/preset-mini/dist/shared/preset-mini.CoOfBKs_.d.ts","./node_modules/@unocss/rule-utils/dist/index.d.ts","./node_modules/@unocss/preset-mini/dist/shared/preset-mini.CRliz1QB.d.ts","./node_modules/@unocss/preset-mini/dist/index.d.ts","./node_modules/@unocss/preset-uno/dist/index.d.ts","./node_modules/@unocss/preset-attributify/dist/index.d.ts","./node_modules/@iconify/types/types.d.ts","./node_modules/@iconify/utils/lib/customisations/defaults.d.ts","./node_modules/@iconify/utils/lib/customisations/merge.d.ts","./node_modules/@iconify/utils/lib/customisations/bool.d.ts","./node_modules/@iconify/utils/lib/customisations/flip.d.ts","./node_modules/@iconify/utils/lib/customisations/rotate.d.ts","./node_modules/@iconify/utils/lib/icon/name.d.ts","./node_modules/@iconify/utils/lib/icon/defaults.d.ts","./node_modules/@iconify/utils/lib/icon/merge.d.ts","./node_modules/@iconify/utils/lib/icon/transformations.d.ts","./node_modules/@iconify/utils/lib/svg/viewbox.d.ts","./node_modules/@iconify/utils/lib/icon/square.d.ts","./node_modules/@iconify/utils/lib/icon-set/tree.d.ts","./node_modules/@iconify/utils/lib/icon-set/parse.d.ts","./node_modules/@iconify/utils/lib/icon-set/validate.d.ts","./node_modules/@iconify/utils/lib/icon-set/validate-basic.d.ts","./node_modules/@iconify/utils/lib/icon-set/expand.d.ts","./node_modules/@iconify/utils/lib/icon-set/minify.d.ts","./node_modules/@iconify/utils/lib/icon-set/get-icons.d.ts","./node_modules/@iconify/utils/lib/icon-set/get-icon.d.ts","./node_modules/@iconify/utils/lib/icon-set/convert-info.d.ts","./node_modules/@iconify/utils/lib/svg/build.d.ts","./node_modules/@iconify/utils/lib/svg/defs.d.ts","./node_modules/@iconify/utils/lib/svg/id.d.ts","./node_modules/@iconify/utils/lib/svg/size.d.ts","./node_modules/@iconify/utils/lib/svg/encode-svg-for-css.d.ts","./node_modules/@iconify/utils/lib/svg/trim.d.ts","./node_modules/@iconify/utils/lib/svg/pretty.d.ts","./node_modules/@iconify/utils/lib/svg/html.d.ts","./node_modules/@iconify/utils/lib/svg/url.d.ts","./node_modules/@iconify/utils/lib/svg/inner-html.d.ts","./node_modules/@iconify/utils/lib/svg/parse.d.ts","./node_modules/@iconify/utils/lib/colors/types.d.ts","./node_modules/@iconify/utils/lib/colors/keywords.d.ts","./node_modules/@iconify/utils/lib/colors/index.d.ts","./node_modules/@iconify/utils/lib/css/types.d.ts","./node_modules/@iconify/utils/lib/css/icon.d.ts","./node_modules/@iconify/utils/lib/css/icons.d.ts","./node_modules/@iconify/utils/lib/loader/types.d.ts","./node_modules/@iconify/utils/lib/loader/utils.d.ts","./node_modules/@iconify/utils/lib/loader/custom.d.ts","./node_modules/@iconify/utils/lib/loader/modern.d.ts","./node_modules/@iconify/utils/lib/loader/loader.d.ts","./node_modules/@iconify/utils/lib/emoji/cleanup.d.ts","./node_modules/@iconify/utils/lib/emoji/convert.d.ts","./node_modules/@iconify/utils/lib/emoji/format.d.ts","./node_modules/@iconify/utils/lib/emoji/test/parse.d.ts","./node_modules/@iconify/utils/lib/emoji/test/variations.d.ts","./node_modules/@iconify/utils/lib/emoji/data.d.ts","./node_modules/@iconify/utils/lib/emoji/test/components.d.ts","./node_modules/@iconify/utils/lib/emoji/test/name.d.ts","./node_modules/@iconify/utils/lib/emoji/test/similar.d.ts","./node_modules/@iconify/utils/lib/emoji/test/tree.d.ts","./node_modules/@iconify/utils/lib/emoji/test/missing.d.ts","./node_modules/@iconify/utils/lib/emoji/regex/create.d.ts","./node_modules/@iconify/utils/lib/emoji/parse.d.ts","./node_modules/@iconify/utils/lib/emoji/replace/find.d.ts","./node_modules/@iconify/utils/lib/emoji/replace/replace.d.ts","./node_modules/@iconify/utils/lib/misc/strings.d.ts","./node_modules/@iconify/utils/lib/misc/objects.d.ts","./node_modules/@iconify/utils/lib/misc/title.d.ts","./node_modules/@iconify/utils/lib/index.d.ts","./node_modules/@unocss/preset-icons/dist/core.d.ts","./node_modules/@unocss/preset-icons/dist/index.d.ts","./node_modules/@unocss/preset-tagify/dist/index.d.ts","./node_modules/@unocss/preset-typography/dist/index.d.ts","./node_modules/@unocss/preset-web-fonts/dist/shared/preset-web-fonts.TGEYFvVV.d.ts","./node_modules/@unocss/preset-web-fonts/dist/index.d.ts","./node_modules/@unocss/preset-wind/dist/shortcuts.d.ts","./node_modules/@unocss/preset-wind/dist/theme.d.ts","./node_modules/@unocss/preset-wind/dist/shared/preset-wind.DRADYSMV.d.ts","./node_modules/@unocss/preset-wind/dist/index.d.ts","./node_modules/@unocss/transformer-attributify-jsx/dist/index.d.ts","./node_modules/@unocss/transformer-compile-class/dist/index.d.ts","./node_modules/@unocss/transformer-directives/dist/index.d.ts","./node_modules/@unocss/transformer-variant-group/dist/index.d.ts","./node_modules/unocss/dist/index.d.ts","./src/styles/uno.config.ts","./src/types/hono.d.ts","./src/util/EnableDarkMode.tsx","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/dom-events.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/ws/index.d.ts","./node_modules/bun-types/globals.d.ts","./node_modules/bun-types/s3.d.ts","./node_modules/bun-types/fetch.d.ts","./node_modules/bun-types/bun.d.ts","./node_modules/bun-types/extensions.d.ts","./node_modules/bun-types/devserver.d.ts","./node_modules/bun-types/ffi.d.ts","./node_modules/bun-types/html-rewriter.d.ts","./node_modules/bun-types/jsc.d.ts","./node_modules/bun-types/sqlite.d.ts","./node_modules/bun-types/test.d.ts","./node_modules/bun-types/wasm.d.ts","./node_modules/bun-types/overrides.d.ts","./node_modules/bun-types/deprecated.d.ts","./node_modules/bun-types/bun.ns.d.ts","./node_modules/bun-types/index.d.ts"],"fileIdsList":[[342,385,437,438,439,440,442,448,450],[115,116,117,118,342,385,400,401,402,405,437,438,439,440,442,448,450],[115,342,385,400,401,402,437,438,439,440,442,448,450],[342,385,400,401,437,438,439,440,442,448,450],[114,137,229,335,342,385,437,438,439,440,442,448,450],[115,342,385,400,401,402,405,437,438,439,440,442,448,450],[342,385,400,401,402,437,438,439,440,442,448,450],[289,342,385,437,438,439,440,442,448,450],[257,292,342,385,437,438,439,440,442,448,450],[257,342,385,437,438,439,440,442,448,450],[257,258,342,385,437,438,439,440,442,448,450],[313,342,385,437,438,439,440,442,448,450],[303,305,342,385,437,438,439,440,442,448,450],[303,305,306,307,308,309,342,385,437,438,439,440,442,448,450],[303,305,306,342,385,437,438,439,440,442,448,450],[303,305,306,307,342,385,437,438,439,440,442,448,450],[303,305,306,307,308,342,385,437,438,439,440,442,448,450],[257,264,342,385,437,438,439,440,442,448,450],[257,267,342,385,437,438,439,440,442,448,450],[245,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,342,385,437,438,439,440,442,448,450],[245,257,258,295,342,385,437,438,439,440,442,448,450],[245,257,258,342,385,437,438,439,440,442,448,450],[257,258,267,342,385,437,438,439,440,442,448,450],[257,258,267,278,342,385,437,438,439,440,442,448,450],[342,382,385,437,438,439,440,442,448,450],[342,384,385,437,438,439,440,442,448,450],[385,437,438,439,440,442,448,450],[342,385,390,420,437,438,439,440,442,448,450],[342,385,386,391,397,398,405,417,428,437,438,439,440,442,448,450],[342,385,386,387,397,405,437,438,439,440,442,448,450],[337,338,339,342,385,437,438,439,440,442,448,450],[342,385,388,429,437,438,439,440,442,448,450],[342,385,389,390,398,406,437,438,439,440,442,448,450],[342,385,390,417,425,437,438,439,440,442,448,450],[342,385,391,393,397,405,437,438,439,440,442,448,450],[342,384,385,392,437,438,439,440,442,448,450],[342,385,393,394,437,438,439,440,442,448,450],[342,385,397,437,438,439,440,442,448,450],[342,385,395,397,437,438,439,440,442,448,450],[342,384,385,397,437,438,439,440,442,448,450],[342,385,397,398,399,417,428,437,438,439,440,442,448,450],[342,385,397,398,399,412,417,420,437,438,439,440,442,448,449,450],[342,380,385,433,437,438,439,440,442,448,450],[342,380,385,393,397,400,405,417,428,437,438,439,440,442,448,450],[342,385,397,398,400,401,405,417,425,428,437,438,439,440,442,448,450],[342,385,400,402,417,425,428,437,438,439,440,442,448,450],[340,341,342,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,437,438,439,440,442,448,450],[342,385,397,403,437,438,439,440,442,448,450],[342,385,404,428,437,438,439,440,442,448,450],[342,385,393,397,405,417,437,438,439,440,442,448,450],[342,385,406,437,438,439,440,442,448,450],[342,385,407,437,438,439,440,442,448,450],[342,384,385,408,437,438,439,440,442,448,450],[342,382,383,384,385,386,387,388,389,390,391,392,393,394,395,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,437,438,439,440,442,448,449,450],[342,385,410,437,438,439,440,442,448,450],[342,385,411,437,438,439,440,442,448,450],[342,385,397,412,413,437,438,439,440,442,448,450],[342,385,412,414,429,431,437,438,439,440,442,448,450],[342,385,397,417,418,420,437,438,439,440,442,448,450],[342,385,419,420,437,438,439,440,442,448,450],[342,385,417,418,437,438,439,440,442,448,450],[342,385,420,437,438,439,440,442,448,450],[342,385,421,437,438,439,440,442,448,450],[342,382,385,417,437,438,439,440,442,448,450],[342,385,397,423,424,437,438,439,440,442,448,450],[342,385,423,424,437,438,439,440,442,448,450],[342,385,390,405,417,425,437,438,439,440,442,448,449,450],[342,385,426,437,438,439,440,442,448,450],[342,385,405,427,437,438,439,440,442,448,450],[342,385,400,411,428,437,438,439,440,442,448,450],[342,385,390,429,437,438,439,440,442,448,450],[342,385,417,430,437,438,439,440,442,448,450],[342,385,404,431,437,438,439,440,442,448,450],[342,385,432,437,438,439,440,442,448,450],[342,385,390,397,399,408,417,428,431,433,437,438,439,440,442,448,450],[342,385,417,434,437,438,439,440,442,448,450],[342,385,397,400,402,405,417,425,428,434,435,437,438,439,440,442,448,449,450],[244,247,342,385,437,438,439,440,442,448,450],[248,342,385,437,438,439,440,442,448,450],[248,257,282,318,342,385,437,438,439,440,442,448,450],[248,257,282,318,319,342,385,437,438,439,440,442,448,450],[248,249,342,385,437,438,439,440,442,448,450],[248,249,250,251,252,253,342,385,437,438,439,440,442,448,450],[248,249,252,342,385,437,438,439,440,442,448,450],[248,254,342,385,437,438,439,440,442,448,450],[248,323,342,385,437,438,439,440,442,448,450],[248,254,325,326,327,342,385,437,438,439,440,442,448,450],[254,342,385,437,438,439,440,442,448,450],[244,248,342,385,437,438,439,440,442,448,450],[342,380,385,390,398,425,429,433,437,438,439,442,443,448,449,450],[342,385,437,438,439,440,442,448],[342,385,437,438,439,440,448,450],[342,380,385,437,438,440,442,448,450],[342,385,390,408,417,420,425,429,433,436,438,439,440,442,448,450],[342,385,435,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451],[342,385,390,398,399,406,425,434,437,438,439,440,442,448,450],[342,385,398,437,439,440,442,448,450],[342,385,437,438,439,440,442,450],[93,110,111,342,385,437,438,439,440,442,448,450],[111,112,342,385,437,438,439,440,442,448,450],[93,104,105,109,110,342,385,437,438,439,440,442,448,450],[92,93,102,103,104,105,107,342,385,437,438,439,440,442,448,450],[108,140,342,385,437,438,439,440,442,448,450],[85,342,385,437,438,439,440,442,448,450],[102,105,108,342,385,437,438,439,440,442,448,450],[105,109,342,385,437,438,439,440,442,448,450],[104,342,385,437,438,439,440,442,448,450],[105,107,108,110,113,342,385,437,438,439,440,442,448,450],[85,86,94,97,342,385,437,438,439,440,442,448,450],[98,342,385,437,438,439,440,442,448,450],[85,96,342,385,437,438,439,440,442,448,450],[96,342,385,437,438,439,440,442,448,450],[97,342,385,437,438,439,440,442,448,450],[85,87,88,89,90,91,94,95,97,98,342,385,437,438,439,440,442,448,450],[92,93,342,385,437,438,439,440,442,448,450],[85,99,100,342,385,437,438,439,440,442,448,450],[85,96,98,342,385,437,438,439,440,442,448,450],[90,94,97,98,342,385,437,438,439,440,442,448,450],[105,108,342,385,437,438,439,440,442,448,450],[105,342,385,437,438,439,440,442,448,450],[114,137,228,335,342,385,437,438,439,440,442,448,450],[105,108,227,342,385,437,438,439,440,442,448,450],[93,102,103,105,106,342,385,437,438,439,440,442,448,450],[93,103,104,108,109,342,385,437,438,439,440,442,448,450],[107,342,385,437,438,439,440,442,448,450],[145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,342,385,437,438,439,440,442,448,450],[145,342,385,437,438,439,440,442,448,450],[145,155,342,385,437,438,439,440,442,448,450],[245,246,342,385,437,438,439,440,442,448,450],[245,342,385,437,438,439,440,442,448,450],[342,352,356,385,428,437,438,439,440,442,448,450],[342,352,385,417,428,437,438,439,440,442,448,450],[342,347,385,437,438,439,440,442,448,450],[342,349,352,385,425,428,437,438,439,440,442,448,449,450],[342,385,405,425,437,438,439,440,442,448,449,450],[342,385,435,437,438,439,440,442,448,450],[342,347,385,435,437,438,439,440,442,448,450],[342,349,352,385,405,428,437,438,439,440,442,448,450],[342,344,345,348,351,385,397,417,428,437,438,439,440,442,448,450],[342,352,359,385,437,438,439,440,442,448,450],[342,344,350,385,437,438,439,440,442,448,450],[342,352,373,374,385,437,438,439,440,442,448,450],[342,348,352,385,420,428,435,437,438,439,440,442,448,450],[342,373,385,435,437,438,439,440,442,448,450],[342,346,347,385,435,437,438,439,440,442,448,450],[342,352,385,437,438,439,440,442,448,450],[342,346,347,348,349,350,351,352,353,354,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,374,375,376,377,378,379,385,437,438,439,440,442,448,450],[342,352,367,385,437,438,439,440,442,448,450],[342,352,359,360,385,437,438,439,440,442,448,450],[342,350,352,360,361,385,437,438,439,440,442,448,450],[342,351,385,437,438,439,440,442,448,450],[342,344,347,352,385,437,438,439,440,442,448,450],[342,352,356,360,361,385,437,438,439,440,442,448,450],[342,356,385,437,438,439,440,442,448,450],[342,350,352,355,385,428,437,438,439,440,442,448,450],[342,344,349,352,359,385,437,438,439,440,442,448,450],[342,385,417,437,438,439,440,442,448,450],[342,347,352,373,385,433,435,437,438,439,440,442,448,450],[248,254,255,256,320,321,322,324,328,329,330,331,332,342,385,437,438,439,440,442,448,450],[179,180,181,182,183,184,185,187,188,189,190,191,192,193,194,342,385,437,438,439,440,442,448,450],[179,342,385,437,438,439,440,442,448,450],[179,186,342,385,437,438,439,440,442,448,450],[135,342,385,437,438,439,440,442,448,450],[123,124,135,342,385,437,438,439,440,442,448,450],[125,126,342,385,437,438,439,440,442,448,450],[123,124,125,127,128,133,342,385,437,438,439,440,442,448,450],[124,125,342,385,437,438,439,440,442,448,450],[134,342,385,437,438,439,440,442,448,450],[125,342,385,437,438,439,440,442,448,450],[123,124,125,128,129,130,131,132,342,385,437,438,439,440,442,448,450],[101,143,178,342,385,437,438,439,440,442,447,448,450],[96,101,342,385,437,438,439,440,442,448,450],[96,101,211,235,342,385,437,438,439,440,442,448,450],[96,100,101,114,137,144,206,207,208,229,335,342,385,437,438,439,440,442,448,450],[96,101,216,217,342,385,437,438,439,440,442,448,450],[96,101,211,218,342,385,437,438,439,440,442,448,450],[96,101,137,205,342,385,437,438,439,440,442,448,450],[96,101,138,207,342,385,437,438,439,440,442,448,450],[101,142,342,385,437,438,439,440,442,448,450],[101,136,137,138,143,178,200,342,385,437,438,439,440,442,448,450],[101,114,137,143,229,335,342,385,437,438,439,440,442,448,450],[101,136,137,138,143,177,342,385,437,438,439,440,442,448,450],[101,137,342,385,437,438,439,440,442,446,448,450],[101,137,195,196,342,385,437,438,439,440,442,448,450],[101,137,138,198,342,385,437,438,439,440,442,448,450],[101,137,197,199,342,385,437,438,439,440,442,448,450],[101,114,137,142,202,229,335,342,385,437,438,439,440,442,448,450],[101,114,137,229,335,342,385,437,438,439,440,442,448,450],[101,114,137,141,143,144,178,200,229,335,342,385,437,438,439,440,442,448,450],[101,114,136,137,138,141,142,143,178,200,204,209,210,211,229,335,342,385,437,438,439,440,442,448,450],[101,114,137,138,144,209,213,229,335,342,385,437,438,439,440,442,448,450],[101,114,137,143,144,209,211,213,215,218,219,220,221,229,335,342,385,437,438,439,440,442,448,450],[101,114,137,209,211,213,219,220,221,223,224,229,335,342,385,437,438,439,440,442,448,450],[101,114,119,120,121,122,137,138,139,142,201,203,209,211,212,214,222,225,226,229,335,342,385,437,438,439,440,442,448,450],[101,324,333,342,385,437,438,439,440,442,448,450],[114,137,143,229,342,385,437,438,439,440,442,448,450],[101,342,385,437,438,439,440,442,448,450],[101,136,137,342,385,437,438,439,440,442,448,450],[100,101,342,385,437,438,439,440,442,448,450]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"8bf8b5e44e3c9c36f98e1007e8b7018c0f38d8adc07aecef42f5200114547c70","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7a3c8b952931daebdfc7a2897c53c0a1c73624593fa070e46bd537e64dcd20a","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"b5ce7a470bc3628408429040c4e3a53a27755022a32fd05e2cb694e7015386c7","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"4245fee526a7d1754529d19227ecbf3be066ff79ebb6a380d78e41648f2f224d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"bde31fd423cd93b0eff97197a3f66df7c93e8c0c335cbeb113b7ff1ac35c23f4","impliedFormat":1},{"version":"7196813635df39c957fb00c4ef0b0c6d541a58d795690b871d3845a304313270","impliedFormat":1},{"version":"3f1f226503994e914cd8e3f60836433bab39e66fdc9bf854adbdbf8fedf235f5","impliedFormat":1},{"version":"6a29367c1c6dad01abb830ab8b88220b45e3bccadc9c44eb98b548c79b8114a0","impliedFormat":1},{"version":"602fe472c51d7cbc84a12bbb1233ddca8efeb14c0f96c9f5043846996bb65c94","impliedFormat":1},{"version":"20088910c5d671c33f54f1ed4b3ef010adcd5ac7670bdcae68b6a99c3d39a565","impliedFormat":1},{"version":"55ba843bdfd6d90ec2ebe854d991b9e2b5ec7367498418060b7f7c513b845830","impliedFormat":1},{"version":"1e5935a4a99bd72ed709caad872b55968430d9efd57acbbde9c35b81117542e4","impliedFormat":1},{"version":"6d5b33f862a60d8a3ab06dbc703679a5fb1c7cc15d04fca31eda60244f8548e6","impliedFormat":1},{"version":"07af913df1d81e6d4c963ceea4d5deedc0b49e91f1cf14283976b19d3b2caffc","impliedFormat":1},{"version":"d51e88e983b5141062f31f0fd79f29b1c355d93083f390d9990092161ebe6a31","impliedFormat":1},{"version":"6f9084330a0db9c63f66d070ea29810904385cf8349ba55caf27d9f385453e92","impliedFormat":1},{"version":"dfe9d1d117b41806a282f00fff5f615f9130b462746320ed6701bfa5f119b0e9","impliedFormat":1},{"version":"057e1e2e26d1a8113bdb17816a4561d08f5d03c59305c3ac581fcc2b2b2eae3e","impliedFormat":1},{"version":"d4930dad57b2451f1c27d5b63cba50521d223481a1c466a6ef07ec99dbbf77a0","impliedFormat":1},{"version":"26daa6e3d43941a8a5872ca5a1c404904ecb09bae64dd778a074d2579ce39bb4","impliedFormat":1},{"version":"4202109303eee66d9c55a8f2b9ca8a2bfddc299417cebca8368f1c16caba78ab","impliedFormat":1},{"version":"33ac3738a30cac7a3a0d34c219d24aeb8efe70e0088e2d82a061b1302975c9e0","impliedFormat":1},{"version":"d41393eec4438dd812940c3efa292499b3031d31b1d8d4d72a269b95b341f3cf","impliedFormat":1},{"version":"074388271346577d825792a48a86992091d913aaf31c9b5ea3cac25bd474c45a","impliedFormat":1},{"version":"984c26e8864dc326bf6f7a72f89625b3facd86a901d406b7e54aca3d6ef9d674","impliedFormat":1},{"version":"7b7ff39f07939974bf05d962473edf639147545fae61e39ae6734a4ede55b008","impliedFormat":1},{"version":"5c9b631fd684665b7ab77aadfae34060a03e049bf2b39166a4e3878a2fe978dc","impliedFormat":1},{"version":"65783c6213a9709a5b28a29e8c6602aa4f441fcd3428e5ef30c7d42ed762b34c","impliedFormat":1},{"version":"90195f314de6f89ab09c81406d3d4b5cfe684520f237e3f89941ba4c70d3383a","impliedFormat":1},{"version":"3fb52d31b8bcd5d78ce4d849e1fcefd5fbc5106dbf393a61c2e4c2c9ec936057","impliedFormat":1},{"version":"6f57d264fbb19264ae5aebe606037360c323871fe0287255d93ed864c8baa04d","impliedFormat":1},{"version":"b98e9017e21e894141be4c1811052825875a8f97f7a86fd9c8a9991f3b99cea4","impliedFormat":1},{"version":"ca3251ff37b9334ebe11efe63afb88c9f15cc4d6921456a86d697fc93d185d7f","impliedFormat":1},{"version":"3d70943897bc336fe28c721b463bab2fcda5def22457ea7881e7cd436c79bc34","impliedFormat":1},{"version":"84a488c5fe017f799e54ff0fda5eed362f01553ae989548ded98865cb3930c51","impliedFormat":1},{"version":"54339569e505c6ab0f9a437ebd895982796e81ffeace2bab4eee302b9eeeb9fa","impliedFormat":1},{"version":"e00dbc9d2c4dba5e071dc5e6ef53d119dcc80ab6865a07cbf6ba180571092778","impliedFormat":1},{"version":"d9319dd7ee3028f6080ad2585937e54fae397d311e31b3622635d518ecb275b2","impliedFormat":1},{"version":"c5d2f6d8d4a028465f3f45eaeeb46eaf0a27bf0317a4afabc9bfd1abd280542a","impliedFormat":1},{"version":"039ded2c755746d4d93c865d26222f8279a9e836ccefc258ba3ab73b810322a6","impliedFormat":1},{"version":"c06ddac0b3d757a3f3f1cff517d97ef1cc576e87ed8d41139b66858ef1897c89","impliedFormat":1},{"version":"cac2b5c3ae79b2e38c249e55e87a4566d65f01aa3b54065f2f709e179ebc3300","impliedFormat":1},{"version":"d5cd11ab7215913cafbf31130abb0ab5b6f2aa4d71f8b1723b0b2f6f7e42711a","impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"b542939a35357458e62f8229c2d7578ae888d63d3ab837395d7bb8a3064c205e","impliedFormat":1},{"version":"3a5af4fba7b27b815bb40f52715aedebaa4b371da3e5a664e7e0798c9b638825","impliedFormat":1},{"version":"8485b6da53ec35637d072e516631d25dae53984500de70a6989058f24354666f","impliedFormat":1},{"version":"ebe80346928736532e4a822154eb77f57ef3389dbe2b3ba4e571366a15448ef2","impliedFormat":1},{"version":"49c632082dc8a916353288d3d8b2dc82b3471794249a381d090d960c8ceac908","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"71addb585c2db7b8e53dc1b0bcfa58c6c67c6e4fa2b968942046749d66f82e7e","impliedFormat":1},{"version":"c76b0c5727302341d0bdfa2cc2cee4b19ff185b554edb6e8543f0661d8487116","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"e703cfacb9965c4d4155346c65a0091ecded90ea98874ed6b3f36286577c4dde","impliedFormat":1},{"version":"f5ef066942e4f0bd98200aa6a6694b831e73200c9b3ade77ad0aa2409e8fe1b1","impliedFormat":1},{"version":"b9e99cd94f4166a245f5158f7286c05406e2a4c694619bceb7a4f3519d1d768e","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"a6b19a9b937cf0a0c468918d7339277cb9f15aae20265c49361fc8bb5c5b9d39","signature":"17f581f013f0537e8a78334df990affa5862e442ec63196a965ceffd417d7f0d"},{"version":"d10e37bdbbbd478baccd364ccddbafbfe91a17cdbd01b4316cf79a887cf227c0","signature":"0f4d8259d54aa21d8b6ad414943605d260b25ebbcf9eb44c187da51045789041"},{"version":"5e117a100fc39f9b797afe7fe36308edef16a0f85c67d3f282569b650196ceae","signature":"dea5744654abab7796c02eb37de13fb07340c774cbaed24d624a8e72d495c701"},{"version":"a96e5997dfce8772db850d21651195b7482a2ec56bde8b3ecdfabb8a39ce5248","impliedFormat":1},{"version":"6a1ddeec9ffd84b5182e20fa8a39efeaeacbd06a5b23cd11f28f3b73f8716e82","impliedFormat":1},"c9222d3a75efd77962e3784bdb610486333aca9a3e31fbe370cd0229b630ee8e",{"version":"c8cf5450c4a5966f2239282b2dc041b6b5109e16272bbaa3e325af12a74c4e2a","signature":"f95d99cdbbbad67049fd79ddce774ae4e4a60dbc4a992923ba886c63f913dba3"},{"version":"9c374cf13d8c88dba9b9ac307c8d6453e1f3449ca1a2478c7f70ec35aa7a5c08","signature":"633c15dd505f55263fd53b403222c28754add162e32ebb30eb327b17d6b5da2f"},{"version":"7bb53546e9bd6e3f22804497a41d4b885674e7b15b7d64c7d3f83722dfd2b456","impliedFormat":1},{"version":"4083e6d84bfe72b0835b600185c7b7ce321da3d6053f866859185eefc161e7a0","impliedFormat":1},{"version":"b883e245dc30c73b655ffe175712cac82981fc999d6284685f0ed7c1dac8aa6f","impliedFormat":1},{"version":"626e3504b81883fa94578c2a97eff345fadc5eae17a57c39f585655eef5b8272","impliedFormat":1},{"version":"e9a15eeba29ceb0ee109dd5e0282d2877d8165d87251f2ea9741a82685a25c61","impliedFormat":1},{"version":"c6cb06cc021d9149301f3c51762a387f9d7571feed74273b157d934c56857fac","impliedFormat":1},{"version":"cd7c133395a1c72e7c9e546f62292f839819f50a8aa46050f8588b63ef56df88","impliedFormat":1},{"version":"196f5f74208ce4accea017450ed2abc9ce4ab13c29a9ea543db4c2d715a19183","impliedFormat":1},{"version":"4687c961ab2e3107379f139d22932253afb7dd52e75a18890e70d4a376cdf5d9","impliedFormat":1},{"version":"ae8cfe2e3bdef3705fc294d07869a0ab8a52d9b623d1cc0482b6fc2be262b015","impliedFormat":1},{"version":"94c8e9c00244bbf1c868ca526b12b4db1fab144e3f5e18af3591b5b471854157","impliedFormat":1},{"version":"827d576995f67a6205c0f048ae32f6a1cf7bda9a7a76917ab286ef11d7987fd7","impliedFormat":1},{"version":"cb5dc83310a61d2bb351ddcdcaa6ec1cf60cc965d26ce6f156a28b4062e96ab2","impliedFormat":1},{"version":"0091cb2456a823e123fe76faa8b94dea81db421770d9a9c9ade1b111abe0fcd1","impliedFormat":1},{"version":"034d811fd7fb2262ad35b21df0ecab14fdd513e25dbf563572068e3f083957d9","impliedFormat":1},{"version":"298bcc906dd21d62b56731f9233795cd11d88e062329f5df7cdb4e499207cdd4","impliedFormat":1},{"version":"f7e64be58c24f2f0b7116bed8f8c17e6543ddcdc1f46861d5c54217b4a47d731","impliedFormat":1},{"version":"966394e0405e675ca1282edbfa5140df86cb6dc025e0f957985f059fe4b9d5d6","impliedFormat":1},{"version":"b0587deb3f251b7ad289240c54b7c41161bb6488807d1f713e0a14c540cbcaee","impliedFormat":1},{"version":"4254aab77d0092cab52b34c2e0ab235f24f82a5e557f11d5409ae02213386e29","impliedFormat":1},{"version":"19db45929fad543b26b12504ee4e3ff7d9a8bddc1fc3ed39723c2259e3a4590f","impliedFormat":1},{"version":"b21934bebe4cd01c02953ab8d17be4d33d69057afdb5469be3956e84a09a8d99","impliedFormat":1},{"version":"b2b734c414d440c92a17fd409fa8dac89f425031a6fc7843bac765c6c174d1ca","impliedFormat":1},{"version":"239f39e8ad95065f5188a7acd8dbefbbbf94d9e00c460ffdc331e24bc1f63a54","impliedFormat":1},{"version":"d44f78893cb79e00e16a028e3023a65c1f2968352378e8e323f8c8f88b8da495","impliedFormat":1},{"version":"32afc9daae92391cb4efeb0d2dac779dc0fb17c69be0eb171fd5ed7f7908eeb4","impliedFormat":1},{"version":"b835c6e093ad9cda87d376c248735f7e4081f64d304b7c54a688f1276875cbf0","impliedFormat":1},{"version":"a9eabe1d0b20e967a18758a77884fbd61b897d72a57ddd9bf7ea6ef1a3f4514b","impliedFormat":1},{"version":"64c5059e7d7a80fe99d7dad639f3ba765f8d5b42c5b265275d7cd68f8426be75","impliedFormat":1},{"version":"05dc1970dc02c54db14d23ff7a30af00efbd7735313aa8af45c4fd4f5c3d3a33","impliedFormat":1},{"version":"a0caf07fe750954ad4cf079c5cf036be2191a758c2700424085ffde6af60d185","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"eab89b3aa37e9e48b2679f4abe685d56ac371daa8fbe68526c6b0c914eb28474","impliedFormat":1},{"version":"a51083bdd8249d6cd7c64d228aa68d531b7de0420941b65e8d5cb822a659b838","signature":"d13846c9c15ddfc123185dbc805e78f2e095c761a83c8fa881155f8550f5bd0f"},{"version":"cff399d99c68e4fafdd5835d443a980622267a39ac6f3f59b9e3d60d60c4f133","impliedFormat":1},{"version":"6ada175c0c585e89569e8feb8ff6fc9fc443d7f9ca6340b456e0f94cbef559bf","impliedFormat":1},{"version":"e56e4d95fad615c97eb0ae39c329a4cda9c0af178273a9173676cc9b14b58520","impliedFormat":1},{"version":"73e8dfd5e7d2abc18bdb5c5873e64dbdd1082408dd1921cad6ff7130d8339334","impliedFormat":1},{"version":"fc820b2f0c21501f51f79b58a21d3fa7ae5659fc1812784dbfbb72af147659ee","impliedFormat":1},{"version":"4f041ef66167b5f9c73101e5fd8468774b09429932067926f9b2960cc3e4f99d","impliedFormat":1},{"version":"31501b8fc4279e78f6a05ca35e365e73c0b0c57d06dbe8faecb10c7254ce7714","impliedFormat":1},{"version":"7bc76e7d4bbe3764abaf054aed3a622c5cdbac694e474050d71ce9d4ab93ea4b","impliedFormat":1},{"version":"ff4e9db3eb1e95d7ba4b5765e4dc7f512b90fb3b588adfd5ca9b0d9d7a56a1ae","impliedFormat":1},{"version":"f205fd03cd15ea054f7006b7ef8378ef29c315149da0726f4928d291e7dce7b9","impliedFormat":1},{"version":"d683908557d53abeb1b94747e764b3bd6b6226273514b96a942340e9ce4b7be7","impliedFormat":1},{"version":"7c6d5704e2f236fddaf8dbe9131d998a4f5132609ef795b78c3b63f46317f88a","impliedFormat":1},{"version":"d05bd4d28c12545827349b0ac3a79c50658d68147dad38d13e97e22353544496","impliedFormat":1},{"version":"b6436d90a5487d9b3c3916b939f68e43f7eaca4b0bb305d897d5124180a122b9","impliedFormat":1},{"version":"04ace6bedd6f59c30ea6df1f0f8d432c728c8bc5c5fd0c5c1c80242d3ab51977","impliedFormat":1},{"version":"57a8a7772769c35ba7b4b1ba125f0812deec5c7102a0d04d9e15b1d22880c9e8","impliedFormat":1},{"version":"badcc9d59770b91987e962f8e3ddfa1e06671b0e4c5e2738bbd002255cad3f38","impliedFormat":1},{"version":"8c9a974ff2f7df855ef9b1a26882b2a4acc9e4bb9c1c28b852b089bc1f6c6903","signature":"a5c163eb02cc5f4557008ef059dc394429fc5525bb9f6eb39952de545289f576"},{"version":"e56f84133b5966522d8ccb28c7621bc422a43c29c65de2aa7fb38fc52f5595df","signature":"8de138c9f58c496c1514cbcd2fec15b0b22b5bd4b335934ade774c7851bb354d"},{"version":"3cdb239b61c443f5b8ab9c3cc12c924485e6b47e76605d99e011150673e5cede","impliedFormat":1},{"version":"2799a05b81f7f5046230ec6ea25d3f06b0ad464d7be80584f434667ed02c8c06","signature":"6b1af415bceddc10421c6d67fed82b389b1ce4be2ae30a05545da420e3a07c13"},{"version":"d89524f18daff3916098f819d3d84be3650cd686bb8ffff46d0d4163b32832e1","signature":"1d30be2a0b02012e6a9600ef79b5abcf01b9f3e2d2a59e81d7f9b779b3bfcd23"},{"version":"b93e86bbfb882aca2346cc68ed23ea4b63138652e3207900349d8073f898f49c","signature":"bde392d9c7cc36ac095f2764da609afe5e93d7b32183aff86502dd35b430d3ed"},{"version":"dc1a7b93a02ba9141e549fc0fd5d6acb2928212625f5f6bdc7aadf551cae5d38","impliedFormat":1},{"version":"af5a65048bd57707a9fc009e893ad84a7b82d5a9d5a37db2fa3cd0afbc027529","signature":"43031d634a31c11edf26b9ebcc8f394c72a6058e8c117bfa10101ad97a3fa87b"},{"version":"437f36e068f146fdebfd1966461c5a3e4fe80ba05045287c91de990d4906ff50","signature":"f1a175679b6e0d7c4e4d0f2c04d1fa2fc0fd74f201ceb63de78733652a14b4d0"},{"version":"2f22d267e6e1ffd2b214e53fc590325257f77ffe07f6a8db2ce970b980101459","signature":"2aad3f985f8ef8e14a128a2ccb6033860008fcfa605636ba5e846836003fffd0"},{"version":"87433a9e6215bcec8f9a3a8e644762bf400f0ad405d144f8887942e9a01872fa","signature":"342474a59c7643d5393691c431c1f96d02ce2ffd70b90a9974a9085c7668ddb6"},{"version":"08956ddea2fe4febf4937fa308cf2916f28c2017116c7d60837caab776b30dea","signature":"6d4a4dbdbdae39c276d18da23329118c1e428de3c3d2a08da2dff487076a29bf"},{"version":"29b85631b108a9d11c68c4b4eb223bc97ce1543559324088fbc58571613fd56e","signature":"e941e42ff414b6bcad66700b0965f9c0579b5cd852f35298b644414fde7996e2"},{"version":"7a2da008bc7e01a48ee572beeeb289051fa610f9948af1081ad5fb1c59123762","signature":"c1b02ec31a038993badeacfe812e3166806798d702bfee813cfadb3dedd974ca"},{"version":"651519aa84cf1dc44355224aa52cbb3cf5c40ab6b4987bf106dbb5e507a635ff","signature":"e88a233df81c20d0008794f7123bf21e48d4f04aeecd054b81bc52e87a70d954"},{"version":"53bff24081a37cab57bb8dc8348d57f4da1516d4f4538ed121fcae2e8da8127c","signature":"eb2d687d6a8b91588f70df24dbb02ffdcdba2bfea676464a4927271474d22551"},{"version":"5328caaa6f6df26f60dc71c93acb075f631adcc5b0725e0a93d1776db2702673","signature":"13ed346ce3a362415231b83fead5ae8bd830bc7791976a97dfe913dd7e142792"},{"version":"4a5ac1097185766d4c8e3cc3fc2c5ce38abadef48a282d9ca82482d894cb3a93","signature":"0f349a0eb72efa3c8f06c27eee3b79da2d1032f6e505ab789917bd9da50ac8a8"},{"version":"c29babf20bc9258b5886d53f68535904e23b0c9791f31d6f47beae2f854bb3e3","signature":"36f9d776a13eff8bf7d2c9227aea9a525fbb0bd6a11dcd578152cbdf49e2dc5c"},{"version":"ebd6dc5d2cdd06aad635393d58e1c58ac5e0e32440b14613346b2495cc8d71e4","signature":"a5f902164a287ffe33d250b51b66369c46781d90bc51f7326a62187879fd051b"},{"version":"27f5b076a9f740b61ca436c712c8ac4b05d874b4f98c897658b24af6602388c6","signature":"1d9e8c00cfd2a642d6d829ddce4b6f10afd7be2e3b6952af518eab861fc67fd7"},{"version":"8ace20675fb902592d2be273b0b9aae90cd1778c56b7475d672542ae5ed8c6f6","signature":"ee45b685a6828605493b586600e2f28777e3538abb1dfdf12783fa41bf21afc6"},{"version":"4e7875c28594fec5732ceec49ea9f065559c5cb0bd18a1ff39d563babaf0666e","signature":"6277f1d345e30d0e7f8f0e3259b066e4e3c7ab1da02b5e14c2c2ed73f54bcbcf"},{"version":"50284434117491cf8afbc162eb5ee3ad1e988260060e903d308e67edde60da0e","signature":"c05afdfbe873aaad24e3f6ded38d8365c837926057a210d809ac6d8062a046d1"},{"version":"965c8f61d43c225dc5a5a94d8e90cf21dfa1bb275d75b2ca6033d8bd1d0ced82","signature":"cc31fdc37b9d6de2f0d102bea13a8bc208297d2db799a0cc61cbf050b78000ec"},{"version":"05b81c4c0e06cf7a29d262cb4d855b3646a097b775e8718d1eb7c69c8ff5138b","signature":"e23ec5c29a860b3a36474cbc8726b3e4f6430641610a34769ddeea6ccc8f050f"},{"version":"273f845ae41feba87914bba16cc2fc89f17c5d8b52369a7282b2f952c668730c","signature":"f0462305486a9dcac70222580791bfe638d21f25baa60903dc9e66f0ffe630c1"},{"version":"779a449e1c674198d0143d0eb7bbe89b089972688cc3a4e725824567ffe3f259","signature":"c7f954d41864425027a1c11eed354e46fe8ced6ab751f7ecc590e9e0a5352d3a"},{"version":"485aaadb817ef4e289b48a6dcf280e980a5e86ad7d5a3cb8f2eddc68ab5f53fa","signature":"f826f225f7f1fa39a2ed78d4eba268888093f702a4ba812d4bb32fcfb0490c3a"},{"version":"9f68f9126dfcbbc2cc6b2ab1b230ae9cef9f76d4a8b95c0537520d1913a5ca21","signature":"5b220a8a1b3c977b9aa968c9b88089fa52b2167734b5b37577a267297d38bcd7"},{"version":"cb97ef306b019c63c4524a75b7fb376e5d105b0ce7102efe56fbdc236d311ca8","signature":"dfda7b28fd1dc62e70371bc8adbba79a970c8ec6b65eb31a5189c67ac45d9edb"},{"version":"3d9ec582f26b971a7e515afc5e8e72ff22ef655a94a296598afb9fef074c1d15","impliedFormat":1},{"version":"fd4d39cb7501a5d9e9d2b5ef7635783db7df53e7516c258688b589bf2a0c72a6","impliedFormat":1},{"version":"0ac9964568520b8ec8e11bc1fc0b837fc545a0d1987f46a4980c1310c6555e41","impliedFormat":1},{"version":"474f3692fc4cbc71556b2a0d8cf59da2aad03f1632488739e63d07806d388ba2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"430a6a98d50f649799e193e88d22585a0ca3ea09575310d707b4e66129122ec7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6183eb7ad4e400fe8dc2393a14607fda82e31a7aa443e07f88c447079e4b067a","signature":"a995d81d19277a70fcc46127b38de892098f2f5755686285ae1a9625f7277993"},{"version":"82b32119ebf85ee45a08a86f2e6b3ddce87599f88c7fd4ccd5eeee318fc48e13","signature":"f6b4900414f4d5d10455a4c4a58b81e43b461b617ad200b3dcbfe9ebb00e6295"},{"version":"026d6bacb74905cd014c33dd89ce7c3a01ad8163025b1fc61fc2414594ab1da7","signature":"3d2970a8eb042a38384fa1744c3514da1f0879c67f76f8b5a8d4d4bc3318e4b2"},{"version":"93a0ba7fe1f9cb3cf584e0a211d5dea329a2673a85f2da4a0136b60945bd7165","signature":"705db089dc65616aa30a606b9ef40bd80e802bcf37b2f423289a188b93e063c7"},{"version":"e3cb529dd32186be10646b7dc9dd39160182c0ab5d99681880cef60e0744a8ac","signature":"2c70b0999b69ca8e8860d78aee7451fa5782c1496d0bd46ea7c8676110e5ebed"},{"version":"f1c5e027add899325e6f44605b4ad3b58533f8d40c475fe60b52c7a86c5ab1e9","signature":"5537099d25600e8c3fe973079985db4088b5aa0cfaaaeb0ac13fc973f796228b"},{"version":"0297c159085d7943334ccc790e15c9f25ec49c160b134d5eb4a4211771f683af","signature":"c6cae190fd0d6e9498ebc12333baa7a5d09c0da775e29de23d92fc02eab5fb23"},{"version":"751fc2da118e781558227d15bacaceab5de0b652b17eee337fab676ac3280f9b","signature":"9be01ec43e194db4b7321bb0f1485fd7b858f9184bcff7db1ce77558d588e2fa"},{"version":"ee2d362c8b77a66f649b9c831496b98673ca51aebe80e076ace3b777c62c579c","signature":"cbefc3aa46a6fe75aec95fd77093d928ad710ec21d1c23d2a8846a7cb81e7df4"},{"version":"e08b9b41b85ac48fef39a7f53616d640e69ad3d6056cd49a668a86d64b49a68c","signature":"a0ce4ecf14dd0a0b2f2f0c98ec244342f40a273e1bae405c0b48cbb55acf294e"},{"version":"1dd742b5845d069e91bb9f863bcdebb1d33ad518b931f9e0a5792fc3e9f19453","signature":"ec6b833e89b2abfadef7c3d6d61ee6c198e28c174b31c43707cf771f8fb2415e"},{"version":"32ffef3e1ab9b6763db4fb9b3008e35422e0e602fe2613f254d1ebbb1e9cd5f0","signature":"3d0b388961be6d1dd53999aee7a4fc268b359a4775abe9e6436f8b716dc33847"},{"version":"2be2227c3810dfd84e46674fd33b8d09a4a28ad9cb633ed536effd411665ea1e","impliedFormat":1},{"version":"c8fe48c4437d4ead0a841128d179f8bb99e0e38f9ccb80ca6be14833e30bc129","impliedFormat":99},{"version":"11f332afe8b12119ddab845c9e7717baea46ef22e102c73fd8f469e9dda83526","impliedFormat":99},{"version":"a94a81b7b78657b3e6dc8bc66363b87b72fa974a7b22007a577095640c7b6709","impliedFormat":99},{"version":"e65c8e749594f45bd86a5175ea5c14291a5c27a4660fe87a79964291bee7b46d","impliedFormat":99},{"version":"8a24c222db3c1a9a8af1337b837ca9f91481b9adf9e95ee1aea6a32b6fd390b2","impliedFormat":99},{"version":"c276ce0aeb917344fa70e676ae25f2b71cc45ca0a3b75132165f2195621c9286","impliedFormat":99},{"version":"edbe7c2c4ae8896291ebfc00855de9aa16f1bc327844951995a43004787b8133","impliedFormat":99},{"version":"e2f263213c18957aac4a25480e8e3967c178636e0113a5af6b3c6b4b7a222eb0","impliedFormat":99},{"version":"9bfc671ab234a4d685d8bead5849518d14b8a2e053086576378c5a223406f5dc","impliedFormat":99},{"version":"fedfc5b99bd5b7cd8e434a9d1803053567e1a0929de6e752bc91e88b40a3f1d6","impliedFormat":99},{"version":"c9f84df727f6125d6390f379d070edce342bc037b1c653ce1df1e3f9dae9f1bf","impliedFormat":99},{"version":"ed25c267b43223ab3a33238fdbf239dabab3eafb348790bc91f27973616b8566","impliedFormat":99},{"version":"12baec7a4e2c3acddd09ab665e0ae262395044396e41ecde616fefdd33dc75ff","impliedFormat":99},{"version":"100985057cdd198e32b471b9c92a39080e5e50720b2cb290d04ddf40fbe71c84","impliedFormat":99},{"version":"333d9b9067c0213cd7b275d1d78bab0577ba31ef7a63306ab65a74e83a546a65","impliedFormat":99},{"version":"85566a0b81339b43e063f5cd8cc49a9b9bc177bc5ad3ffd5e4874700040ec11e","impliedFormat":99},{"version":"c2688779f6804c3bc6dfa33d05a810464c684a74f92aee6b0f0d4bcd7dbeed6d","impliedFormat":99},{"version":"16331f489efb6af7d06037074020644d9175f70a7a6466d926f63e74af5a77d8","impliedFormat":99},{"version":"2b2b8b64b39f152439ecb9f04b3d6c1d88d35c75bf14a4eb98f1cc791f092366","impliedFormat":99},{"version":"395548b309c8fe9ffadd8b1055898fffa29bd28ea1f8079f33e48a65601589e2","impliedFormat":99},{"version":"e38871affeac7cf4dd4cc3a55714ff38d55f137c30788d30e454a6e3058f36bc","impliedFormat":99},{"version":"783a0f8fb88d659272c1ac541719e32235881815705b44fb63b6af579885ea75","impliedFormat":99},{"version":"6a60957e322c4c060ddf3073130cbcbcbc5e639e21cd2279df43184bfa8cb9a3","impliedFormat":99},{"version":"5b353617eeb8a37c7a9497ebaeacc027bd7487eec10ffbebca41dcdc2634af70","impliedFormat":99},{"version":"cedbd20d98f3fd7c1fa00742292ab5b13c3fec266ae41b90c47b716ef06cd983","impliedFormat":99},{"version":"9713bcf79cd728919262a2a543484a5f9bd24a15cfec1cee096d9d17a9f5524d","impliedFormat":99},{"version":"35fb129972553f809a7045f3cb952c2598299548018a23238304c020cb16945f","impliedFormat":99},{"version":"855b0379a6b6e96eda055cff16da442b4a7a4548101848b9ae48bce22879569e","impliedFormat":99},{"version":"ea2ac8d236dddbce748dbaffcaa1bfcadae6fbcae1fd0a67e17d5e35d5e38dfc","impliedFormat":99},{"version":"a7750935d6a1cbd259861b5acf1c912f9d3b10efd8602f61fc858f04f261595d","impliedFormat":99},{"version":"e0aa3276d014f3c798dd3101af8c8545b56d79665a7a982b4cf6fe28551a3b56","impliedFormat":99},{"version":"ea744987345eb5ae036495b0185e95eeb7d2d999b0ef80265f79434e83863e9e","impliedFormat":99},{"version":"c3bc54ba21655aaf1db5bb97c42f56bbfe5a3a3c40e3884ef3ba2cdaa9f34c1f","impliedFormat":99},{"version":"705917c38d2e92347b5e57c1c6007da46f1005874ef2257cc8dfff59cba4710f","impliedFormat":99},{"version":"40925b4938b527a6267b1fe56a2e97cc52ea9d73eec90ea8e05df773a182101e","impliedFormat":99},{"version":"2930156137f4885c3ad168804c557edfc9bb88ae0e1df487f4adcdc771286ad7","impliedFormat":99},{"version":"b63e990c632eeee9375c2c43bbd5cdcb23418b79edcb57afa53edf4dd597b33c","impliedFormat":99},{"version":"721dcf072e75b71b5ab7a0bbbd6578f908c36a0bfaefa1454d3e43938bde67a5","impliedFormat":99},{"version":"5704f5ee2642dd0b810bb07ce6e4e51319ed4d6db78747ff54675e72c3fede06","impliedFormat":99},{"version":"da2be38a98356fdd540580a68338df2d2450ec071b1cb5bdbfe8e52075ddde9e","impliedFormat":99},{"version":"3af0bb87094d80e20b0d451626eef1e2da701891c41998ac0a6a6c91cff86f74","impliedFormat":99},{"version":"30a211e9de0dd587f8c690f9ed9378c15c79bcbe762dd85a61c548e5058c3fd6","impliedFormat":99},{"version":"a7cda498cd929d2f958ce49abbaef1abf999ec40884a04cd28ff34317d844e54","impliedFormat":99},{"version":"e48b510f40f29a89d9dbe19a9fca96d7f02b721aec6754fd5c242f9893d06508","impliedFormat":99},{"version":"30d88e2e7c4ca1cdfeb37cf05a2d7a351c68b14ac472e6238401ecb7b75686ea","impliedFormat":99},{"version":"03b34718c02b6225c2f7d7c374cb701ab04461a5cfa66d150531c9f31e39da49","impliedFormat":99},{"version":"7dfe7da785eafad3e3d0cc66545e97f1acf934ebe5b2ec8f4a34341a9ca76ed4","impliedFormat":99},{"version":"8c7829855345152b7b3c196e82147153115d5b568ff97be0e40d161e8d9d2f51","impliedFormat":99},{"version":"f30a36ff98b099ea8c635146dfdd1d810bc14ec303acb653ca938445047b0e41","impliedFormat":99},{"version":"07fa63aca536ca8d8d8c6a56eabcf77f746609921fe23d780a69e2c0a2a65701","impliedFormat":99},{"version":"5eac3facc9f59e960c00f41502b34a908776cfba6d7e1a5a4ead5030682b7434","impliedFormat":99},{"version":"d44f8de16b9c6ef4ebd88d4162bc24942bee9975f88162a8962bb572e62dc5df","impliedFormat":99},{"version":"0251c18e8c863bf5ef510043644299aceab6debf3d87aab8c8cfded5aef7d6af","impliedFormat":99},{"version":"292f7dc6b4be74f148f5e5b57b9e8a7f515d7d4f6183d3f9162e127e50959ba9","impliedFormat":99},{"version":"c1608d867d6ddda5c0f4736cf4959e2b2c6bcda660c4c72f7feb36b3998df2bb","impliedFormat":99},{"version":"02d77b0d27ecb78e28d3a376c6cdce05fabcf58f2fd01c102f031d8e375191da","impliedFormat":99},{"version":"daef84b3b89e60054fab1abaafe38eda673f88abdedc3920015d61f1cc5358b8","impliedFormat":99},{"version":"f3318054dc392b6661785263095ed8f1555f0d8f3ce534c8c2de8895b4ec7bd3","impliedFormat":99},{"version":"6c3aa7e0c4eb4d8d7fc24df037980369e70a28f9237cae77511b4cfc6a1b74d0","impliedFormat":99},{"version":"ecc7e0840690cc4b9a2587a4f550b292c35d36150c6c108803bbdfc3bead5b91","impliedFormat":99},{"version":"e11a23b343084cdec24d718fc64369dc8b6dece71314b41d4b5938f2a568834d","impliedFormat":99},{"version":"ce678766176812e8eda3f4925304d4159d806f50fa8a93a72da56e95dae8bbc8","impliedFormat":99},{"version":"bb21d35a36dc1db80a2cf29383bb7304919708cde205bbe246ec47176336e255","impliedFormat":99},{"version":"df657f732e32af7c7550da93e66dfdfa142fc1282b4a392ec78fc9aefbd6fdd0","impliedFormat":99},{"version":"b20ef0766a8a578e5c542aafaa8c53b7e2b0e32a5522f9cf18bc021a81d54dd7","impliedFormat":99},{"version":"9ea0cd8a367cab9b1c632740d1bd998f8c4dbbbda4505f47bebd38a46afbaaa6","impliedFormat":99},{"version":"97980bb49a7e4b15df6f988f914070c831a39426cd9a29a6f7a9af82f397b28c","impliedFormat":99},{"version":"3ddf05b5259b9a0e2b1da1559585655202670e1f78396b4d4efccea0195a41b4","impliedFormat":99},{"version":"1e99c59aadb1af6d090976ade8280ea37208e8f064f79e9a18231fe5b7232890","impliedFormat":99},{"version":"c7ee77eec320d6312899cd8c16484c82b98385e175c57ff00d49cc5a2c291e0d","impliedFormat":99},{"version":"b38d9a4927465a8a5d1ae84e00d323bedfc7f5e77f4bc360078c6f283b964acb","impliedFormat":99},{"version":"27d6b338ff280dc86ff167217c29d7e71b52bd25a3c3b8eb1f5a56c887571d00","impliedFormat":99},{"version":"da60046c4cc6b018869ea8fc71a7b7bf5591d9f5d90ee52c4a614ecc69ff3433","impliedFormat":99},{"version":"8bee1fe0b3dd1b324f08189d81e55f9952007ce2304df07a15568b821b7e524f","impliedFormat":99},{"version":"18cbde8e6ffd9d9f900137f468c17fc7dfa3c842987509739421f469af00c7ca","impliedFormat":99},{"version":"688cc2936f4d6442d33b4d34bd8fe98cfcf5a6e1949d2f5c93ae1ae33d8f5566","impliedFormat":99},{"version":"e040ae4e8d5207465a6485607ec757aa9e04fb45be6ee268b68fb1eafcec89bb","impliedFormat":99},{"version":"274f9df255d8434420c7cf47139f6d79daf36173e3ed65a19a25683789e52d3b","impliedFormat":99},{"version":"cdbf73d85e16beef06d286558afb193a84d16d0dbb0ef12cd6ce8eb0983719ae","impliedFormat":99},{"version":"de4344ae97511942ab43a77e26e7ecba1023e1ad8e6d32997e7c4c7b74972b76","impliedFormat":99},{"version":"c4a473a17dd603768a3fbde82d482ebca64adb82fbcc12dde46226715c7fc9a4","impliedFormat":99},{"version":"505c63eab1fac456cc7d16e20b1d81f54ae34ed65c93a3395dcb4a6f34cec76b","impliedFormat":99},{"version":"74fee195a96af82e03bde0523117de9378d522a0524105479b5fcc5387c70aeb","impliedFormat":99},{"version":"01aa9ee77b983bfcca74c665d886df80c30418ba700a83189cceb801f7151f04","impliedFormat":99},{"version":"319a3e563007d8e26b4223d35c90193eb9093946719f6f889af09f72bd954c8f","impliedFormat":99},{"version":"3931b0733b86684c5e9465079fff6180b792714c9f81f7bbbaabc8d16c966be3","impliedFormat":99},{"version":"bd3929200c4182c7f455fa2a63527577144bfbacc9800f7ba098532cfbad1241","impliedFormat":99},{"version":"acca586ccd3208fb36789b5e3f5fc91bee76343c34c36065f9bf5bf5f939b81d","impliedFormat":99},{"version":"a2753ca30d502d353d6812ede065915590a8690f45f1e1e0419d877419b39aa7","impliedFormat":99},{"version":"2089e569be8d22bfea69e97232ace1a27c9cb7a2fb03eb735e2cd6a2bd064abf","signature":"47b86c1e580c2960e850705fa0220d488d2c073d3ea7397758ec77ba7cb326e4"},"cfda212ad721992bbf2ce0190f469373762560346ce764fe52ba2f6b96ed1f84",{"version":"18a143a26bf81eccee66410d71fa5effacfe7a794f558360c3f15dfe023676e4","signature":"1de0e9eed9308489daf8d8bed0cb31606fcf857347e75903045d1cc50f183cab"},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fa51737611c21ba3a5ac02c4e1535741d58bec67c9bdf94b1837a31c97a2263","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"d2bc987ae352271d0d615a420dcf98cc886aa16b87fb2b569358c1fe0ca0773d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4f0539c58717cbc8b73acb29f9e992ab5ff20adba5f9b57130691c7f9b186a4d","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"76103716ba397bbb61f9fa9c9090dca59f39f9047cb1352b2179c5d8e7f4e8d0","impliedFormat":1},{"version":"f9677e434b7a3b14f0a9367f9dfa1227dfe3ee661792d0085523c3191ae6a1a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"115971d64632ea4742b5b115fb64ed04bcaae2c3c342f13d9ba7e3f9ee39c4e7","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"9057f224b79846e3a95baf6dad2c8103278de2b0c5eebda23fc8188171ad2398","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"e6f5a38687bebe43a4cef426b69d34373ef68be9a6b1538ec0a371e69f309354","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0476e6b51a47a8eaf5ee6ecab0d686f066f3081de9a572f1dde3b2a8a7fb055","impliedFormat":1},{"version":"1e289f30a48126935a5d408a91129a13a59c9b0f8c007a816f9f16ef821e144e","impliedFormat":1},{"version":"f96a023e442f02cf551b4cfe435805ccb0a7e13c81619d4da61ec835d03fe512","impliedFormat":1},{"version":"5135bdd72cc05a8192bd2e92f0914d7fc43ee077d1293dc622a049b7035a0afb","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"6d386bc0d7f3afa1d401afc3e00ed6b09205a354a9795196caed937494a713e6","impliedFormat":1},{"version":"5b2e73adcb25865d31c21accdc8f82de1eaded23c6f73230e474df156942380e","affectsGlobalScope":true,"impliedFormat":1},{"version":"23459c1915878a7c1e86e8bdb9c187cddd3aea105b8b1dfce512f093c969bc7e","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"64ede330464b9fd5d35327c32dd2770e7474127ed09769655ebce70992af5f44","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"c6b4e0a02545304935ecbf7de7a8e056a31bb50939b5b321c9d50a405b5a0bba","impliedFormat":1},{"version":"fab29e6d649aa074a6b91e3bdf2bff484934a46067f6ee97a30fcd9762ae2213","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"bcd0418abb8a5c9fe7db36a96ca75fc78455b0efab270ee89b8e49916eac5174","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"fbf68fc8057932b1c30107ebc37420f8d8dc4bef1253c4c2f9e141886c0df5ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"7d8b16d7f33d5081beac7a657a6d13f11a72cf094cc5e37cda1b9d8c89371951","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"5360a27d3ebca11b224d7d3e38e3e2c63f8290cb1fcf6c3610401898f8e68bc3","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"e679ff5aba9041b932fd3789f4a1c69ddaf015ee54c5879b5b1f4727bcbe00dd","affectsGlobalScope":true,"impliedFormat":1},{"version":"f689c4237b70ae6be5f0e4180e8833f34ace40529d1acc0676ab8fb8f70457d7","impliedFormat":1},{"version":"b02784111b3fc9c38590cd4339ff8718f9329a6f4d3fd66e9744a1dcd1d7e191","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"63b05afa6121657f25e99e1519596b0826cda026f09372c9100dfe21417f4bd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"78dc0513cc4f1642906b74dda42146bcbd9df7401717d6e89ea6d72d12ecb539","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1},{"version":"1ba59c8bbeed2cb75b239bb12041582fa3e8ef32f8d0bd0ec802e38442d3f317","impliedFormat":1},{"version":"e4e232719963c8926970dedb13d7c7963bd16d74a8a9151aa0ccd770cc620bf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"299e306eec3a9326fbd199be0077dbc9ecea3cc94c23d5422fda255cdd16fe2b","impliedFormat":1},{"version":"8fd47acbd61d016de5ca0ffa4ba6f128a0ade0b4aca805e90b70e42b256d00b6","impliedFormat":1},{"version":"8714e653cd017705e02ec7ea49d5bcfd3f895d236950b9a410f099b4e02c6f1f","impliedFormat":1},{"version":"1a2a036dd0b17d8a41629907d9fd0f45d68d48d274bbdfc7adca29df2e4f1f14","impliedFormat":1},{"version":"c874a66a6b151b1efe71602c5422e2a74d2dfd53313901980c33d68b91d1b483","affectsGlobalScope":true,"impliedFormat":1},{"version":"7115f1157a00937d712e042a011eb85e9d80b13eff78bac5f210ee852f96879d","impliedFormat":1},{"version":"0ac74c7586880e26b6a599c710b59284a284e084a2bbc82cd40fb3fbfdea71ae","affectsGlobalScope":true,"impliedFormat":1},{"version":"1181e359ac0ae3aa0159cd3323b5a872eab9f609cecba241baeb1d74189fa048","impliedFormat":1},{"version":"08b5dd8a0ad958a3b23e7f6001e09bc26e6b2b9b0c649678b44150cfa79ffc17","impliedFormat":1},{"version":"f448c8da440be938119bf0572de813ad14dd68feb914abb04a2c3195431c1b0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"b05b9ef20d18697e468c3ae9cecfff3f47e8976f9522d067047e3f236db06a41","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b01c99e412d76e9e4abfb41b608347f0c59734217dc89dfc3a6d74186190ffa","affectsGlobalScope":true,"impliedFormat":1},{"version":"1745f0b1ab53f414b4f8ebb2c6a902fda28d40f454edac8e92b4d7c974a2051c","affectsGlobalScope":true,"impliedFormat":1},{"version":"067f76ab5254b1bdfc94154730b7a30c12e3aad8b9d04ec62c0d6b7a1f40ea0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"76bf438aa034211ecbfceafe13cc259a823f107827862e8d1bc8b0ff5dce2261","affectsGlobalScope":true,"impliedFormat":1}],"root":[[137,139],[142,144],178,196,197,[199,201],[203,226],[230,243],[334,336]],"options":{"allowImportingTsExtensions":true,"allowSyntheticDefaultImports":true,"allowUnreachableCode":false,"allowUnusedLabels":false,"alwaysStrict":true,"declaration":true,"downlevelIteration":true,"esModuleInterop":true,"exactOptionalPropertyTypes":true,"jsx":4,"jsxImportSource":"hono/jsx","module":99,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitOverride":true,"noImplicitReturns":true,"noImplicitThis":true,"noPropertyAccessFromIndexSignature":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"strict":true,"strictBindCallApply":true,"strictFunctionTypes":true,"strictNullChecks":true,"strictPropertyInitialization":true,"target":99,"useUnknownInCatchVariables":true,"verbatimModuleSyntax":true},"referencedMap":[[245,1],[119,2],[117,3],[118,4],[122,5],[116,6],[115,7],[257,1],[291,8],[290,8],[289,1],[293,9],[294,9],[292,1],[260,1],[258,10],[261,11],[259,11],[262,1],[300,1],[301,1],[305,1],[302,1],[312,10],[311,1],[313,1],[314,12],[306,13],[310,14],[307,15],[303,1],[308,16],[309,17],[304,1],[277,10],[273,10],[276,10],[275,10],[274,10],[270,10],[269,10],[272,10],[271,10],[264,10],[265,18],[263,1],[268,19],[266,10],[318,20],[297,21],[299,21],[298,21],[295,22],[296,21],[316,1],[315,1],[317,1],[278,23],[279,1],[282,1],[285,1],[280,1],[287,1],[288,24],[284,1],[281,1],[283,1],[286,1],[267,1],[382,25],[383,25],[384,26],[342,27],[385,28],[386,29],[387,30],[337,1],[340,31],[338,1],[339,1],[388,32],[389,33],[390,34],[391,35],[392,36],[393,37],[394,37],[396,38],[395,39],[397,40],[398,41],[399,42],[381,43],[341,1],[400,44],[401,45],[402,46],[435,47],[403,48],[404,49],[405,50],[406,51],[407,52],[408,53],[409,54],[410,55],[411,56],[412,57],[413,57],[414,58],[415,1],[416,1],[417,59],[419,60],[418,61],[420,62],[421,63],[422,64],[423,65],[424,66],[425,67],[426,68],[427,69],[428,70],[429,71],[430,72],[431,73],[432,74],[433,75],[434,76],[436,77],[248,78],[256,79],[319,80],[320,81],[250,82],[254,83],[249,79],[253,84],[251,82],[321,79],[322,85],[255,85],[324,86],[323,79],[328,87],[327,85],[325,85],[326,88],[252,79],[329,79],[330,79],[331,89],[332,79],[343,1],[440,90],[451,1],[450,91],[442,92],[441,1],[439,93],[443,1],[437,94],[444,1],[452,95],[445,1],[449,96],[438,97],[446,1],[447,1],[448,98],[112,99],[113,100],[111,101],[108,102],[141,103],[100,104],[109,105],[110,106],[202,107],[114,108],[98,109],[87,110],[88,111],[86,1],[97,112],[89,113],[90,110],[96,114],[94,115],[99,110],[101,116],[91,117],[95,118],[120,119],[121,120],[229,121],[227,1],[228,122],[107,123],[102,1],[105,124],[106,125],[140,1],[103,1],[85,1],[104,1],[92,1],[93,1],[177,126],[146,127],[156,127],[147,127],[157,127],[148,127],[149,127],[164,127],[163,127],[165,127],[166,127],[158,127],[150,127],[159,127],[151,127],[160,127],[152,127],[154,127],[162,128],[155,127],[161,128],[167,128],[153,127],[168,127],[173,127],[174,127],[169,127],[145,1],[175,1],[171,127],[170,127],[172,127],[176,127],[244,1],[198,1],[82,1],[83,1],[15,1],[13,1],[14,1],[19,1],[18,1],[2,1],[20,1],[21,1],[22,1],[23,1],[24,1],[25,1],[26,1],[27,1],[3,1],[28,1],[29,1],[4,1],[30,1],[34,1],[31,1],[32,1],[33,1],[35,1],[36,1],[37,1],[5,1],[38,1],[39,1],[40,1],[41,1],[6,1],[45,1],[42,1],[43,1],[44,1],[46,1],[7,1],[47,1],[52,1],[53,1],[48,1],[49,1],[50,1],[51,1],[8,1],[57,1],[54,1],[55,1],[56,1],[58,1],[9,1],[59,1],[60,1],[61,1],[63,1],[62,1],[64,1],[65,1],[10,1],[66,1],[67,1],[68,1],[11,1],[69,1],[70,1],[71,1],[72,1],[73,1],[1,1],[74,1],[75,1],[12,1],[79,1],[77,1],[81,1],[84,1],[76,1],[80,1],[78,1],[17,1],[16,1],[247,129],[246,130],[359,131],[369,132],[358,131],[379,133],[350,134],[349,135],[378,136],[372,137],[377,138],[352,139],[366,140],[351,141],[375,142],[347,143],[346,136],[376,144],[348,145],[353,146],[354,1],[357,146],[344,1],[380,147],[370,148],[361,149],[362,150],[364,151],[360,152],[363,153],[373,136],[355,154],[356,155],[365,156],[345,157],[368,148],[367,146],[371,1],[374,158],[333,159],[195,160],[180,1],[181,1],[182,1],[183,1],[179,1],[184,161],[185,1],[187,162],[186,161],[188,161],[189,162],[190,161],[191,1],[192,161],[193,1],[194,1],[136,163],[125,164],[127,165],[134,166],[129,1],[130,1],[128,167],[131,163],[123,1],[124,1],[135,168],[126,169],[132,1],[133,170],[231,171],[232,172],[211,172],[233,172],[234,172],[224,172],[235,172],[236,173],[220,172],[215,172],[237,172],[238,172],[210,172],[209,174],[205,172],[239,172],[240,172],[218,175],[217,176],[216,172],[206,177],[213,172],[241,172],[219,172],[221,172],[242,172],[208,178],[223,172],[143,179],[204,180],[144,181],[178,182],[196,183],[197,184],[199,185],[200,186],[203,187],[137,188],[139,188],[201,189],[212,190],[214,191],[226,188],[222,192],[225,193],[230,194],[243,172],[334,195],[335,196],[336,197],[142,197],[138,198],[207,199]],"affectedFilesPendingEmit":[[231,17],[232,17],[211,17],[233,17],[234,17],[224,17],[235,17],[236,17],[220,17],[215,17],[237,17],[238,17],[210,17],[209,17],[205,17],[239,17],[240,17],[218,17],[217,17],[216,17],[206,17],[213,17],[241,17],[219,17],[221,17],[242,17],[208,17],[223,17],[143,17],[204,17],[144,17],[178,17],[196,17],[197,17],[199,17],[200,17],[203,17],[137,17],[139,17],[201,17],[212,17],[214,17],[226,17],[222,17],[225,17],[230,17],[243,17],[334,17],[336,17],[142,17],[138,17],[207,17]],"version":"5.8.3"}
\ No newline at end of file