Compare commits

..

No commits in common. "main" and "feat/redesign-ui" have entirely different histories.

144 changed files with 963 additions and 9812 deletions

View file

@ -11,15 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v5
uses: https://github.com/actions/checkout@v4
- name: Set up Go
uses: https://github.com/actions/setup-go@v5
with:
go-version-file: go.mod
- name: Clean vendor
run: rm -rf vendor
- name: Test
run: go test -race -v ./...

View file

@ -1,25 +0,0 @@
name: Tests
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Clean vendor
run: rm -rf vendor
- name: Test
run: go test -race -v ./...

6
.gitignore vendored
View file

@ -1,11 +1,5 @@
node_modules/
.agent/
internal/spa/dist/
frontend/node_modules/
frontend/dist/
frontend/bun.lock
frontend/bun.lockb
frontend/package-lock.json
*.exe
*.exe~
*.dll

View file

@ -221,4 +221,4 @@ Includes Valkey 8 with health checks out of the box.
## License
[AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html)
MIT

View file

@ -80,14 +80,9 @@ func main() {
h := httpapi.NewHandler(svc, acSvc.Suggestions, cfg.Server.SourceURL)
mux := http.NewServeMux()
// HTML template routes
mux.HandleFunc("/", h.Index)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/preferences", h.Preferences)
// API routes
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/autocompleter", h.Autocompleter)
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
@ -99,9 +94,8 @@ func main() {
var subFS fs.FS = staticFS
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))
// Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → security headers → handler.
// Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → handler.
var handler http.Handler = mux
handler = middleware.SecurityHeaders(middleware.SecurityHeadersConfig{})(handler)
handler = middleware.CORS(middleware.CORSConfig{
AllowedOrigins: cfg.CORS.AllowedOrigins,
AllowedMethods: cfg.CORS.AllowedMethods,
@ -113,7 +107,6 @@ func main() {
Requests: cfg.RateLimit.Requests,
Window: cfg.RateLimitWindow(),
CleanupInterval: cfg.RateLimitCleanupInterval(),
TrustedProxies: cfg.RateLimit.TrustedProxies,
}, logger)(handler)
handler = middleware.GlobalRateLimit(middleware.GlobalRateLimitConfig{
Requests: cfg.GlobalRateLimit.Requests,

View file

@ -1,358 +0,0 @@
# Frontend Replacement Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the Go template-based frontend with the search-zen-50 React SPA, embedded in the Go binary as a single deployment.
**Architecture:** Build React app → embed in Go binary via `//go:embed` → serve via Go HTTP server with SPA fallback routing. React calls `/search?format=json` and `/autocompleter?q=` APIs.
**Tech Stack:** Go (embed), React 18, Vite, TailwindCSS, React Router, @tanstack/react-query
---
## File Map
| File | Action |
|------|--------|
| `cmd/kafka/main.go` | Modify - replace template handlers with SPA handler |
| `internal/spa/spa.go` | Create - embed React build, serve static files, SPA fallback |
| `internal/spa/dist/` | Build output - React build artifacts (gitignored) |
| `src/hooks/use-search.ts` | Modify - replace mock with real API calls |
| `src/lib/mock-data.ts` | Keep types, remove MOCK_RESPONSE usage |
---
## Task 1: Build React App
**Files:**
- Build: `/tmp/search-zen-50/dist/` (output directory)
- [ ] **Step 1: Install dependencies and build**
```bash
cd /tmp/search-zen-50 && bun install && bun run build
```
Expected: `dist/` directory created with `index.html`, `assets/` folder containing JS/CSS bundles
- [ ] **Step 2: Verify dist contents**
```bash
ls /tmp/search-zen-50/dist/ && ls /tmp/search-zen-50/dist/assets/ | head -10
```
Expected: `index.html` exists, `assets/` contains `.js` and `.css` files
---
## Task 2: Create SPA Go Package
**Files:**
- Create: `internal/spa/spa.go`
```go
package spa
import (
"embed"
"io/fs"
"net/http"
"path"
)
//go:embed all:dist
var distFS embed.FS
// DistFS returns the embedded dist directory as an fs.FS.
func DistFS() (fs.FS, error) {
return fs.Sub(distFS, "dist")
}
// NewHandler returns an HTTP handler that:
// - Serves static files from the embedded dist/ directory
// - Falls back to index.html for SPA routing (any non-API path)
func NewHandler() http.Handler {
dist, err := DistFS()
if err != nil {
panic("spa: embedded dist not found: " + err.Error())
}
return &spaHandler{dist: dist}
}
type spaHandler struct {
dist fs.FS
}
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// API paths are handled by Go API handlers - this should never be reached
// since Go mux dispatches to specific handlers first. But if reached,
// pass through to FileServer which will return 404 for unknown paths.
// Try to serve the requested file first
filePath := path.Clean(r.URL.Path)
f, err := h.dist.Open(filePath)
if err == nil {
f.Close()
// File exists - serve it via FileServer
http.FileServer(http.FS(h.dist)).ServeHTTP(w, r)
return
}
// Fallback to index.html for SPA routing
indexFile, err := h.dist.Open("index.html")
if err != nil {
http.Error(w, "index.html not found in embedded files", http.StatusInternalServerError)
return
}
indexFile.Close()
http.FileServer(http.FS(h.dist)).ServeHTTP(w, r)
}
```
---
## Task 3: Wire SPA Handler in main.go
**Files:**
- Modify: `cmd/kafka/main.go`
- [ ] **Step 1: Replace handlers with SPA**
In `main.go`, find and replace the `mux.HandleFunc` section (lines 82-88) and the static file serving section (lines 90-96).
Old code (lines 82-96):
```go
mux := http.NewServeMux()
mux.HandleFunc("/", h.Index)
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/autocompleter", h.Autocompleter)
mux.HandleFunc("/preferences", h.Preferences)
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
// Serve embedded static files (CSS, JS, images).
staticFS, err := views.StaticFS()
if err != nil {
log.Fatalf("failed to load static files: %v", err)
}
var subFS fs.FS = staticFS
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))
```
New code:
```go
mux := http.NewServeMux()
// API routes - handled by Go
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/autocompleter", h.Autocompleter)
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
// SPA handler - serves React app for all other routes
spaHandler := spa.NewHandler()
mux.Handle("/", spaHandler)
```
- [ ] **Step 2: Add spa import**
Add to imports (after `"github.com/metamorphosis-dev/kafka/internal/search"`):
```go
"github.com/metamorphosis-dev/kafka/internal/spa"
```
- [ ] **Step 3: Remove unused views import if needed**
If `views` is only used for `StaticFS()`, remove the import. The template rendering functions (`RenderIndex`, etc.) won't be needed anymore.
- [ ] **Step 4: Verify build**
```bash
cd /home/ashie/git/kafka && go build ./cmd/kafka/
```
Expected: Builds successfully (may fail on embed if dist not found - continue to next task)
---
## Task 4: Wire React to Real API
**Files:**
- Modify: `src/hooks/use-search.ts` in `/tmp/search-zen-50/`
- [ ] **Step 1: Replace mock search with real API call**
Replace the `search` function in `use-search.ts`:
Old code (lines 23-36):
```typescript
const search = useCallback(async (query: string) => {
if (!query.trim()) return;
setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));
// Simulate network delay
await new Promise((r) => setTimeout(r, 800));
setState((prev) => ({
...prev,
isLoading: false,
results: { ...MOCK_RESPONSE, query },
}));
}, []);
```
New code:
```typescript
const search = useCallback(async (query: string) => {
if (!query.trim()) return;
setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));
try {
const response = await fetch(`/search?format=json&q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setState((prev) => ({
...prev,
isLoading: false,
results: data,
}));
} catch (err) {
setState((prev) => ({
...prev,
isLoading: false,
error: err instanceof Error ? err.message : "Search failed",
}));
}
}, []);
```
- [ ] **Step 2: Remove mock data import**
Remove the mock import line (should be near line 2):
```typescript
import { MOCK_RESPONSE, type SearXNGResponse, type Category } from "@/lib/mock-data";
```
Replace with:
```typescript
import type { SearXNGResponse, Category } from "@/lib/mock-data";
```
- [ ] **Step 3: Keep the CATEGORIES export**
Ensure `mock-data.ts` still exports `CATEGORIES` and `Category` type. The file should look like:
```typescript
// Keep these exports - used by CategoryTabs and preferences
export const CATEGORIES = ["general", "it", "images", "news"] as const;
export type Category = typeof CATEGORIES[number];
// Keep interfaces
export interface SearchResult {
url: string;
title: string;
content: string;
engine: string;
parsed_url: [string, string, string, string, string];
engines: string[];
positions: number[];
score: number;
category: string;
pretty_url: string;
img_src?: string;
thumbnail?: string;
publishedDate?: string;
}
export interface SearXNGResponse {
query: string;
number_of_results: number;
results: SearchResult[];
answers: string[];
corrections: string[];
infoboxes: any[];
suggestions: string[];
unresponsive_engines: string[];
}
```
---
## Task 5: Rebuild React and Verify
**Files:**
- Build: `/tmp/search-zen-50/dist/`
- [ ] **Step 1: Rebuild with changes**
```bash
cd /tmp/search-zen-50 && bun run build
```
- [ ] **Step 2: Copy dist to kafka**
```bash
rm -rf /home/ashie/git/kafka/internal/spa/dist
cp -r /tmp/search-zen-50/dist /home/ashie/git/kafka/internal/spa/dist
```
- [ ] **Step 3: Verify Go build**
```bash
cd /home/ashie/git/kafka && go build ./cmd/kafka/ && echo "Build successful"
```
Expected: "Build successful"
---
## Task 6: Test the Integration
- [ ] **Step 1: Start the server**
```bash
cd /home/ashie/git/kafka && ./kafka -config config.toml &
sleep 2
```
- [ ] **Step 2: Test homepage**
```bash
curl -s http://localhost:8080/ | head -20
```
Expected: HTML with `<div id="root"></div>` from React app
- [ ] **Step 3: Test API**
```bash
curl -s "http://localhost:8080/search?format=json&q=test" | head -50
```
Expected: JSON search response
- [ ] **Step 4: Clean up**
```bash
pkill -f "./kafka" 2>/dev/null; echo "Done"
```
---
## Dependencies
- Node.js/Bun for building React app
- Go 1.24+ for embed functionality
- No new Go dependencies
## Notes
- The `internal/spa/dist/` folder should be gitignored (build artifact)
- The `internal/spa/dist/` copy is needed for the embed to work at compile time
- Preferences page is entirely client-side (localStorage) - no backend needed
- Autocomplete can be added later by modifying `SearchInput.tsx` to call `/autocompleter`

View file

@ -1,328 +0,0 @@
# Brave Search Frontend Redesign — Design Specification
## Overview
Redesign the kafka frontend to match Brave Search's clean, functional aesthetic with emphasis on layout changes: three-column results page, category tiles on homepage, and a hybrid preferences system with full-page `/preferences` route.
## Design Principles
1. **Brave-like layout** — Three-column results, full-page preferences, homepage tiles
2. **Preserve existing design tokens** — Keep current CSS variables (colors, spacing, radii)
3. **CSS Grid for layout** — Three-column grid for results, flexible layouts elsewhere
4. **Hybrid preferences** — Quick popover for common settings (theme + engines), full `/preferences` page for all options
5. **Minimal HTML changes** — Restructure templates where needed for layout, reuse existing partials
6. **localStorage-only preferences** — No server-side persistence; all preferences stored in browser localStorage
---
## 1. Homepage Redesign
### Current State
- Centered hero with logo, tagline, and search box
- No visual categorization of search types
### New Layout
```
┌─────────────────────────────────────────────────────────────┐
│ [Logo] [⚙ Preferences]│
├─────────────────────────────────────────────────────────────┤
│ │
│ [🔍 Search Box] │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ News │ │ Images │ │ Videos │ │ Maps │ ... │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ "Search the web privately..." │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Implementation
- **File:** `internal/views/templates/index.html`
- **Structure:** Search hero + category tiles grid
- **Tiles:** Static links to `/search?q=...` with category parameter (e.g., `&category=images`)
- **Styling:** Grid of icon+label cards below search box, subtle hover effects
### Category Tiles
| Category | Icon | Notes |
|----------|------|-------|
| All | 🌐 | Default, no category param |
| News | 📰 | |
| Images | 🖼️ | |
| Videos | 🎬 | |
| Maps | 🗺️ | |
| Shopping | 🛒 | Future: connect to shopping engine |
| Music | 🎵 | Future: connect to music engine |
| Weather | 🌤️ | Future: connect to weather API |
| Sports | ⚽ | Future |
| Cryptocurrency | ₿ | Future |
Categories marked "Future" are included in the UI but may not have backend support yet. Category tiles that lack backend support display grayed out with a "Coming soon" tooltip.
---
## 2. Results Page — Three-Column Layout
### Current State
- Two columns: compact search bar spanning top, main results + right sidebar
### New Layout
```
┌─────────────────────────────────────────────────────────────┐
│ [Logo] [⚙ Preferences]│
├─────────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌────────────────────────────┐ ┌──────────┐│
│ │ Nav │ │ 🔍 [ Search Input ] │ │ Related ││
│ │ ─────── │ └────────────────────────────┘ │ Searches ││
│ │ All │ About 1,240 results (0.42s) │ ││
│ │ Images │ ┌──────────────────────────┐ │ │ ─────── ││
│ │ Videos │ │ Result Card │ │ │ Suggestions│
│ │ News │ │ Title, URL, Description │ │ │ ││
│ │ Maps │ └──────────────────────────┘ │ └──────────┘│
│ │ Shopping│ ┌──────────────────────────┐ │ │
│ │ ... │ │ Result Card │ │ │
│ │ │ │ ... │ │ │
│ │ ─────── │ └──────────────────────────┘ │ │
│ │ Filters │ ... │ │
│ │ Time │ │ │
│ │ Type │ [Pagination] │ │
│ └─────────┘ │ │
└─────────────────────────────────────────────────────────────┘
```
### Implementation
- **Files:** `internal/views/templates/results.html`, `internal/views/templates/base.html`
- **Left Sidebar (desktop, sticky):**
- Category navigation links (All, Images, Videos, News, Maps, Shopping, Music, Weather)
- Filters section (Time range, Result type) — collapsible
- Hidden on mobile (< 768px)
- **Center Column:**
- Compact search bar
- Results count meta: "About {n} results ({time}s)"
- Result cards (unchanged markup)
- Pagination
- **Right Sidebar:**
- Related searches (existing suggestions)
- Additional panels as needed
### Filters
**Time Range Options:**
| Label | Query Param |
|-------|-------------|
| Any time | (none) |
| Past hour | `&time=h` |
| Past 24 hours | `&time=d` |
| Past week | `&time=w` |
| Past month | `&time=m` |
| Past year | `&time=y` |
**Result Type Options:**
| Label | Query Param |
|-------|-------------|
| All results | (none) |
| News | `&type=news` |
| Videos | `&type=video` |
| Images | `&type=image` |
Filter state persists in URL query params and is preserved across HTMX navigation via `hx-include`.
### Mobile Behavior
| Breakpoint | Layout |
|------------|--------|
| < 768px | Single column, no left sidebar |
| 768px - 1024px | Two columns (center + right sidebar), no left nav |
| > 1024px | Full three columns |
On mobile (< 768px):
- Category filters accessible via a horizontal scrollable chip row above results
- Both sidebars hidden
- Search bar full-width
---
## 3. Preferences Page — Full-Page Hybrid
### Current State
- Popover triggered by gear icon in header
- JavaScript-rendered from localStorage
- Sections: Appearance, Engines, Search Defaults
### New Layout
```
┌─────────────────────────────────────────────────────────────┐
│ [Logo] [⚙ Preferences]│
├─────────────────────────────────────────────────────────────┤
│ ┌────────────────┐ ┌─────────────────────────────────────┐│
│ │ Nav │ │ Content ││
│ │ ───────────── │ │ ││
│ │ Search │ │ [Section Content] ││
│ │ Privacy │ │ ││
│ │ Tabs │ │ ││
│ │ Appearance │ │ ││
│ │ Sidebar │ │ ││
│ │ Content │ │ ││
│ │ Languages │ │ ││
│ │ Regional │ │ ││
│ └────────────────┘ └─────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
```
### Sections (Brave-style)
1. **Search** — Default engine, safe search, language
2. **Privacy** — Tracking protection toggle (UI only, always on), request DNT header toggle
3. **Tabs** — New tab behavior (placeholder section)
4. **Appearance** — Theme (Light/Dark/System), results font size
5. **Sidebar** — Sidebar visibility toggle
6. **Content** — Filter explicit results (SafeSearch), auto-play media toggle
7. **Languages** — UI language (English only for now), search language
8. **Regional** — Region/Country, timezone (placeholder)
### Implementation
- **Route:** Add `GET /preferences` and `POST /preferences` to `internal/httpapi/`
- **Template:** `internal/views/templates/preferences.html`
- **Storage:** localStorage-only. GET handler renders page shell, JavaScript populates form values from localStorage. POST handler receives form data, writes to localStorage, re-renders page.
- **Quick Settings Popover:** Keep existing popover for **theme toggle and engine toggles only** (lightweight, localStorage). SafeSearch and Format settings move exclusively to full preferences page.
- **Styling:** Match existing design tokens, section headers, form controls
### Preferences Nav (Mobile)
- Horizontal scrollable nav on mobile (< 768px)
- Active section highlighted
---
## 4. Component Changes
### Header
- Logo + site name (unchanged)
- Preferences button (unchanged)
### Search Box
- Homepage: Larger, prominent, centered
- Results page: Compact, full-width within center column
### Result Cards
- Keep existing structure
- Consider subtle styling improvements (spacing, typography)
### Category Tiles (Homepage)
- Icon + label per category
- Hover: slight scale + shadow
### Left Sidebar (Results Page)
- Sticky positioning (`position: sticky; top: calc(var(--header-height) + 1rem)`)
- Category links with active state indicator
- Collapsible filter sections
### Preferences Nav
- Vertical nav with section icons
- Active state indicator
- Mobile: horizontal scroll
---
## 5. CSS Architecture
### Existing (Retain)
- CSS custom properties (design tokens)
- Component-level styles
- Dark mode via `[data-theme="dark"]`
### New
**Layout Grid for three-column results:**
```css
.results-layout {
display: grid;
grid-template-columns: 200px 1fr 240px;
gap: 2rem;
align-items: start;
}
```
**Sticky Left Sidebar:**
```css
.results-layout .left-sidebar {
position: sticky;
top: calc(var(--header-height) + 1.5rem);
max-height: calc(100vh - var(--header-height) - 3rem);
overflow-y: auto;
}
```
**Preferences page layout:**
```css
.preferences-layout {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
}
```
**Category tiles grid:**
```css
.category-tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 1rem;
}
```
**Mobile breakpoints:**
```css
@media (max-width: 768px) {
.results-layout {
grid-template-columns: 1fr;
}
.results-layout .left-sidebar,
.results-layout .right-sidebar {
display: none;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.results-layout {
grid-template-columns: 1fr 220px;
}
.results-layout .left-sidebar {
display: none;
}
}
```
---
## 6. Files to Modify
| File | Change |
|------|--------|
| `internal/views/templates/index.html` | Add category tiles |
| `internal/views/templates/results.html` | Add left sidebar, restructure for three columns |
| `internal/views/templates/base.html` | Minimal changes (no structural changes needed) |
| `internal/views/templates/preferences.html` | **New** — full preferences page |
| `internal/views/static/css/kafka.css` | Add layout grids, category tiles, sidebar styles, sticky positioning, mobile breakpoints |
| `internal/views/static/js/settings.js` | Reduce popover to theme + engines; add preferences page JS |
| `internal/httpapi/httpapi.go` | Add `/preferences` route (GET + POST) |
| `internal/views/views.go` | Add preferences template rendering |
---
## 7. Priority Order
1. **Phase 1:** CSS layout framework (three-column grid, new variables, breakpoints)
2. **Phase 2:** Results page three-column layout
3. **Phase 3:** Homepage category tiles
4. **Phase 4:** Preferences page (quick popover first, then full page)
5. **Phase 5:** Polish and mobile responsiveness
---
## Out of Scope
- Backend search logic changes
- New engine implementations (category tiles for future engines are UI placeholders only)
- Caching or performance improvements
- User authentication/account system
- Server-side preference storage

View file

@ -1,74 +0,0 @@
# Frontend Replacement: search-zen-50 Integration
## Status
Approved
## Overview
Replace the current Go template-based frontend (HTMX + Go templates) with the search-zen-50 React SPA. The React app is built statically and embedded into the Go binary, serving as a single binary deployment.
## Architecture
- **Build**: React/Vite app builds to `dist/` directory
- **Embed**: Go's `//go:embed` embeds the dist folder into the binary
- **Serve**: Go HTTP server serves static files and handles API routes
- **SPA routing**: Non-API routes serve `index.html` for React Router
## Changes
### Go Side
1. **Create `internal/spa/spa.go`**
- Embeds the React build (`dist/`) using `//go:embed`
- Serves static files (JS, CSS, images)
- Handles SPA fallback: serves `index.html` for all non-API routes
- Provides `SPAHandler` that wraps API routes
2. **Modify `cmd/kafka/main.go`**
- Import the embedded SPA files
- Route `/`, `/preferences`, and unknown routes to SPA handler
- Keep existing API routes: `/search`, `/autocompleter`, `/healthz`, `/opensearch.xml`
### React Side
1. **Modify `use-search.ts`**
- Replace mock data with real API call: `fetch("/search?format=json&q=${encodeURIComponent(query)}")`
- Map response to existing `SearXNGResponse` type (already matches)
2. **Add autocomplete** (optional enhancement)
- Call `/autocompleter?q=${encodeURIComponent(query)}`
- Display suggestions while typing
3. **Keep unchanged**
- All UI components
- Preferences page (localStorage-based)
- Routing (React Router)
## Data Flow
```
Browser → GET / → Go serves embedded index.html
Browser → GET /search?format=json&q=... → Go search handler → JSON
Browser → React renders results via use-search hook
```
## API Compatibility
The existing kafka API (`/search?format=json`) already matches the expected `SearXNGResponse` interface in the React code:
- `query: string`
- `number_of_results: number`
- `results: SearchResult[]`
- `suggestions: string[]`
- `unresponsive_engines: string[][]`
## File Changes
- **New**: `internal/spa/spa.go`
- **Modified**: `cmd/kafka/main.go` (wire SPA handler)
- **Modified**: `src/hooks/use-search.ts` (use real API)
- **Build step**: `npm run build` or `bun run build` in search-zen-50
## Dependencies
- React app uses `@tanstack/react-query` for API calls (already in package.json)
- No new Go dependencies needed

View file

@ -21,16 +21,13 @@
version = "0.1.0";
src = ./.;
vendorHash = "sha256-8wlKD+33s97oorCJTfHKAgE2Xp1HKXV+bSr6z29KrKM=";
vendorHash = "sha256-NbAa4QM/TI3BTuZs4glx9k3ZjSl2/2LQfKlQ7izR8Ho=";
# Run: nix build .#packages.x86_64-linux.default
# It will fail with the correct hash. Replace vendorHash with it.
# It will fail with the correct hash. Replace it here.
# Embed the templates and static files at build time.
ldflags = [ "-s" "-w" ];
# Remove stale vendor directory before buildGoModule deletes it.
preConfigure = "rm -rf vendor || true";
nativeCheckInputs = with pkgs; [ ];
# Tests require network; they run in CI instead.

24
frontend/.gitignore vendored
View file

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,3 +0,0 @@
# Welcome to your Lovable project
TODO: Document your project here

View file

@ -1,20 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View file

@ -1,26 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": "off",
},
},
);

View file

@ -1,29 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- TODO: Set the document title to the name of your application -->
<title>kafka — Private Meta-Search</title>
<meta name="description" content="A private, open-source meta-search engine.">
<meta name="author" content="kafka" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/2d8592e0-5fb7-43cb-9667-acedb919054e/id-preview-2816c106--a84eb44f-3d0f-4cc4-a71a-90d855b1baf7.lovable.app-1774201598294.png">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@Lovable" />
<meta name="twitter:image" content="https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/2d8592e0-5fb7-43cb-9667-acedb919054e/id-preview-2816c106--a84eb44f-3d0f-4cc4-a71a-90d855b1baf7.lovable.app-1774201598294.png">
<meta property="og:title" content="kafka — Private Meta-Search">
<meta name="twitter:title" content="kafka — Private Meta-Search">
<meta property="og:description" content="A private, open-source meta-search engine.">
<meta name="twitter:description" content="A private, open-source meta-search engine.">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,90 +0,0 @@
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
"recharts": "^2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@playwright/test": "^1.57.0",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"jsdom": "^20.0.3",
"lovable-tagger": "^1.1.13",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vitest": "^3.2.4"
}
}

View file

@ -1,3 +0,0 @@
// Re-export the base fixture from the package
// Override or extend test/expect here if needed
export { test, expect } from "lovable-agent-playwright-config/fixture";

View file

@ -1,10 +0,0 @@
import { createLovableConfig } from "lovable-agent-playwright-config/config";
export default createLovableConfig({
// Add your custom playwright configuration overrides here
// Example:
// timeout: 60000,
// use: {
// baseURL: 'http://localhost:3000',
// },
});

View file

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,40 +0,0 @@
<svg width="150" height="39" viewBox="0 0 150 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M145.867 11.5547C145.143 11.5547 144.503 11.3984 143.945 11.0859C143.388 10.7682 142.951 10.2865 142.633 9.64062C142.315 8.99479 142.156 8.19271 142.156 7.23438C142.156 6.375 142.315 5.6224 142.633 4.97656C142.956 4.32552 143.398 3.82552 143.961 3.47656C144.523 3.1224 145.159 2.94531 145.867 2.94531C146.565 2.94531 147.177 3.10677 147.703 3.42969C148.229 3.7526 148.635 4.22656 148.922 4.85156C149.214 5.47135 149.359 6.22135 149.359 7.10156C149.359 7.26302 149.359 7.42448 149.359 7.58594H142.891V6.42969H148.539L147.836 6.8125C147.836 6.21875 147.758 5.72396 147.602 5.32812C147.451 4.93229 147.227 4.63802 146.93 4.44531C146.638 4.2526 146.279 4.15625 145.852 4.15625C145.419 4.15625 145.036 4.27083 144.703 4.5C144.37 4.72917 144.107 5.06771 143.914 5.51562C143.727 5.96354 143.633 6.5026 143.633 7.13281V7.21875C143.633 7.91146 143.727 8.48958 143.914 8.95312C144.102 9.41667 144.37 9.76302 144.719 9.99219C145.068 10.2214 145.484 10.3359 145.969 10.3359C146.474 10.3359 146.893 10.1875 147.227 9.89062C147.565 9.58854 147.773 9.16146 147.852 8.60938H149.312C149.25 9.19271 149.073 9.70573 148.781 10.1484C148.49 10.5911 148.094 10.9375 147.594 11.1875C147.099 11.4323 146.523 11.5547 145.867 11.5547Z" fill="black"/>
<path d="M136.898 3.17188H138.32V5.13281L138.211 5.08594C138.294 4.66927 138.448 4.30469 138.672 3.99219C138.901 3.67969 139.195 3.4375 139.555 3.26562C139.914 3.09375 140.328 3.00781 140.797 3.00781C140.917 3.00781 141.039 3.01823 141.164 3.03906V4.47656C141.065 4.45573 140.969 4.4401 140.875 4.42969C140.781 4.41927 140.682 4.41406 140.578 4.41406C140.099 4.41406 139.693 4.51042 139.359 4.70312C139.026 4.89583 138.771 5.1849 138.594 5.57031C138.422 5.95052 138.336 6.42448 138.336 6.99219V11.3281H136.898V3.17188Z" fill="black"/>
<path d="M131.508 11.5547C130.784 11.5547 130.143 11.3984 129.586 11.0859C129.029 10.7682 128.591 10.2865 128.273 9.64062C127.956 8.99479 127.797 8.19271 127.797 7.23438C127.797 6.375 127.956 5.6224 128.273 4.97656C128.596 4.32552 129.039 3.82552 129.602 3.47656C130.164 3.1224 130.799 2.94531 131.508 2.94531C132.206 2.94531 132.818 3.10677 133.344 3.42969C133.87 3.7526 134.276 4.22656 134.562 4.85156C134.854 5.47135 135 6.22135 135 7.10156C135 7.26302 135 7.42448 135 7.58594H128.531V6.42969H134.18L133.477 6.8125C133.477 6.21875 133.398 5.72396 133.242 5.32812C133.091 4.93229 132.867 4.63802 132.57 4.44531C132.279 4.2526 131.919 4.15625 131.492 4.15625C131.06 4.15625 130.677 4.27083 130.344 4.5C130.01 4.72917 129.747 5.06771 129.555 5.51562C129.367 5.96354 129.273 6.5026 129.273 7.13281V7.21875C129.273 7.91146 129.367 8.48958 129.555 8.95312C129.742 9.41667 130.01 9.76302 130.359 9.99219C130.708 10.2214 131.125 10.3359 131.609 10.3359C132.115 10.3359 132.534 10.1875 132.867 9.89062C133.206 9.58854 133.414 9.16146 133.492 8.60938H134.953C134.891 9.19271 134.714 9.70573 134.422 10.1484C134.13 10.5911 133.734 10.9375 133.234 11.1875C132.74 11.4323 132.164 11.5547 131.508 11.5547Z" fill="black"/>
<path d="M119.477 0.125H120.914V5.66406L120.703 5.05469C120.802 4.6224 120.971 4.2474 121.211 3.92969C121.456 3.61198 121.766 3.36979 122.141 3.20312C122.516 3.03125 122.943 2.94531 123.422 2.94531C123.958 2.94531 124.419 3.05729 124.805 3.28125C125.195 3.50521 125.492 3.82292 125.695 4.23438C125.898 4.64062 126 5.11719 126 5.66406V11.3281H124.562V5.82031C124.562 5.28385 124.43 4.8724 124.164 4.58594C123.904 4.29948 123.518 4.15625 123.008 4.15625C122.596 4.15625 122.232 4.25781 121.914 4.46094C121.602 4.66406 121.357 4.96875 121.18 5.375C121.003 5.77604 120.914 6.26302 120.914 6.83594V11.3281H119.477V0.125Z" fill="black"/>
<path d="M110.523 11.5547C109.799 11.5547 109.159 11.3984 108.602 11.0859C108.044 10.7682 107.607 10.2865 107.289 9.64062C106.971 8.99479 106.812 8.19271 106.812 7.23438C106.812 6.375 106.971 5.6224 107.289 4.97656C107.612 4.32552 108.055 3.82552 108.617 3.47656C109.18 3.1224 109.815 2.94531 110.523 2.94531C111.221 2.94531 111.833 3.10677 112.359 3.42969C112.885 3.7526 113.292 4.22656 113.578 4.85156C113.87 5.47135 114.016 6.22135 114.016 7.10156C114.016 7.26302 114.016 7.42448 114.016 7.58594H107.547V6.42969H113.195L112.492 6.8125C112.492 6.21875 112.414 5.72396 112.258 5.32812C112.107 4.93229 111.883 4.63802 111.586 4.44531C111.294 4.2526 110.935 4.15625 110.508 4.15625C110.076 4.15625 109.693 4.27083 109.359 4.5C109.026 4.72917 108.763 5.06771 108.57 5.51562C108.383 5.96354 108.289 6.5026 108.289 7.13281V7.21875C108.289 7.91146 108.383 8.48958 108.57 8.95312C108.758 9.41667 109.026 9.76302 109.375 9.99219C109.724 10.2214 110.141 10.3359 110.625 10.3359C111.13 10.3359 111.549 10.1875 111.883 9.89062C112.221 9.58854 112.43 9.16146 112.508 8.60938H113.969C113.906 9.19271 113.729 9.70573 113.438 10.1484C113.146 10.5911 112.75 10.9375 112.25 11.1875C111.755 11.4323 111.18 11.5547 110.523 11.5547Z" fill="black"/>
<path d="M98.5234 3.17188H100.055L102.359 9.90625H102.086L104.344 3.17188H105.805L102.844 11.3281H101.516L98.5234 3.17188Z" fill="black"/>
<path d="M95.6328 3.17188H97.0703V11.3281H95.6328V3.17188ZM96.3516 1.92188C96.1797 1.92188 96.0182 1.88021 95.8672 1.79688C95.7214 1.70833 95.6042 1.59115 95.5156 1.44531C95.4323 1.29427 95.3906 1.13281 95.3906 0.960938C95.3906 0.789062 95.4323 0.630208 95.5156 0.484375C95.6042 0.333333 95.7214 0.216146 95.8672 0.132812C96.0182 0.0442708 96.1797 0 96.3516 0C96.5234 0 96.6823 0.0442708 96.8281 0.132812C96.9792 0.216146 97.0964 0.333333 97.1797 0.484375C97.2682 0.630208 97.3125 0.789062 97.3125 0.960938C97.3125 1.13281 97.2682 1.29427 97.1797 1.44531C97.0964 1.59115 96.9792 1.70833 96.8281 1.79688C96.6823 1.88021 96.5234 1.92188 96.3516 1.92188Z" fill="black"/>
<path d="M91.8672 0.125H93.3047V11.3281H91.8672V0.125Z" fill="black"/>
<path d="M84.5391 0.125H85.9766V11.3281H84.5391V0.125Z" fill="black"/>
<path d="M80.7734 0.125H82.2109V11.3281H80.7734V0.125Z" fill="black"/>
<path d="M77.0078 3.17188H78.4453V11.3281H77.0078V3.17188ZM77.7266 1.92188C77.5547 1.92188 77.3932 1.88021 77.2422 1.79688C77.0964 1.70833 76.9792 1.59115 76.8906 1.44531C76.8073 1.29427 76.7656 1.13281 76.7656 0.960938C76.7656 0.789062 76.8073 0.630208 76.8906 0.484375C76.9792 0.333333 77.0964 0.216146 77.2422 0.132812C77.3932 0.0442708 77.5547 0 77.7266 0C77.8984 0 78.0573 0.0442708 78.2031 0.132812C78.3542 0.216146 78.4714 0.333333 78.5547 0.484375C78.6432 0.630208 78.6875 0.789062 78.6875 0.960938C78.6875 1.13281 78.6432 1.29427 78.5547 1.44531C78.4714 1.59115 78.3542 1.70833 78.2031 1.79688C78.0573 1.88021 77.8984 1.92188 77.7266 1.92188Z" fill="black"/>
<path d="M64.0703 3.17188H65.5391L67.5078 10.3203H66.8984L69.0312 3.17188H70.6406L72.7266 10.3203H72.1875L74.1016 3.17188H75.4766L73.0859 11.3281H71.7344L69.5078 3.61719H70.1641L67.8281 11.3281H66.4688L64.0703 3.17188Z" fill="black"/>
<path d="M56.2891 11.5547C55.8516 11.5547 55.4505 11.4714 55.0859 11.3047C54.7266 11.138 54.4167 10.8854 54.1562 10.5469C53.901 10.2083 53.7109 9.78646 53.5859 9.28125L53.8984 9.60156V11.3281H52.5078V3.17188H53.9453V4.99219L53.6016 5.21875C53.7109 4.74479 53.8958 4.33854 54.1562 4C54.4167 3.65625 54.7344 3.39583 55.1094 3.21875C55.4896 3.03646 55.9036 2.94531 56.3516 2.94531C57.0443 2.94531 57.638 3.1224 58.1328 3.47656C58.6328 3.82552 59.013 4.32552 59.2734 4.97656C59.5339 5.6224 59.6641 6.38021 59.6641 7.25C59.6641 8.11458 59.5286 8.8724 59.2578 9.52344C58.987 10.1693 58.5964 10.6693 58.0859 11.0234C57.5755 11.3776 56.9766 11.5547 56.2891 11.5547ZM56.0781 10.3359C56.526 10.3359 56.9062 10.2083 57.2188 9.95312C57.5312 9.69271 57.7656 9.33333 57.9219 8.875C58.0781 8.41146 58.1562 7.875 58.1562 7.26562C58.1562 6.65625 58.0781 6.11979 57.9219 5.65625C57.7656 5.1875 57.5312 4.82292 57.2188 4.5625C56.9062 4.29688 56.526 4.16406 56.0781 4.16406C55.6302 4.16406 55.2448 4.29688 54.9219 4.5625C54.6042 4.82292 54.362 5.1875 54.1953 5.65625C54.0339 6.11979 53.9531 6.65625 53.9531 7.26562C53.9531 7.86979 54.0339 8.40365 54.1953 8.86719C54.362 9.33073 54.6042 9.69271 54.9219 9.95312C55.2448 10.2083 55.6302 10.3359 56.0781 10.3359ZM52.5078 9.66406H53.9453V14.2109H52.5078V9.66406Z" fill="black"/>
<path d="M47.2578 11.5547C46.8203 11.5547 46.4193 11.4714 46.0547 11.3047C45.6953 11.138 45.3854 10.8854 45.125 10.5469C44.8698 10.2083 44.6797 9.78646 44.5547 9.28125L44.8672 9.60156V11.3281H43.4766V3.17188H44.9141V4.99219L44.5703 5.21875C44.6797 4.74479 44.8646 4.33854 45.125 4C45.3854 3.65625 45.7031 3.39583 46.0781 3.21875C46.4583 3.03646 46.8724 2.94531 47.3203 2.94531C48.013 2.94531 48.6068 3.1224 49.1016 3.47656C49.6016 3.82552 49.9818 4.32552 50.2422 4.97656C50.5026 5.6224 50.6328 6.38021 50.6328 7.25C50.6328 8.11458 50.4974 8.8724 50.2266 9.52344C49.9557 10.1693 49.5651 10.6693 49.0547 11.0234C48.5443 11.3776 47.9453 11.5547 47.2578 11.5547ZM47.0469 10.3359C47.4948 10.3359 47.875 10.2083 48.1875 9.95312C48.5 9.69271 48.7344 9.33333 48.8906 8.875C49.0469 8.41146 49.125 7.875 49.125 7.26562C49.125 6.65625 49.0469 6.11979 48.8906 5.65625C48.7344 5.1875 48.5 4.82292 48.1875 4.5625C47.875 4.29688 47.4948 4.16406 47.0469 4.16406C46.599 4.16406 46.2135 4.29688 45.8906 4.5625C45.5729 4.82292 45.3307 5.1875 45.1641 5.65625C45.0026 6.11979 44.9219 6.65625 44.9219 7.26562C44.9219 7.86979 45.0026 8.40365 45.1641 8.86719C45.3307 9.33073 45.5729 9.69271 45.8906 9.95312C46.2135 10.2083 46.599 10.3359 47.0469 10.3359ZM43.4766 9.66406H44.9141V14.2109H43.4766V9.66406Z" fill="black"/>
<path d="M37.2344 11.5547C36.7292 11.5547 36.2734 11.4583 35.8672 11.2656C35.4609 11.0677 35.1406 10.7865 34.9062 10.4219C34.6771 10.0573 34.5625 9.63281 34.5625 9.14844C34.5625 8.40365 34.7865 7.82552 35.2344 7.41406C35.6875 6.9974 36.3203 6.72396 37.1328 6.59375L38.4297 6.38281C38.763 6.32552 39.0234 6.26302 39.2109 6.19531C39.3984 6.1224 39.5365 6.02865 39.625 5.91406C39.7135 5.79427 39.7578 5.63542 39.7578 5.4375C39.7578 5.21875 39.6979 5.01042 39.5781 4.8125C39.4583 4.61458 39.2734 4.45312 39.0234 4.32812C38.7734 4.20312 38.4635 4.14062 38.0938 4.14062C37.5729 4.14062 37.1484 4.27865 36.8203 4.55469C36.4974 4.82552 36.3203 5.20052 36.2891 5.67969H34.7812C34.7969 5.15885 34.9453 4.69271 35.2266 4.28125C35.5078 3.86458 35.8958 3.53906 36.3906 3.30469C36.8854 3.0651 37.4531 2.94531 38.0938 2.94531C38.7448 2.94531 39.3047 3.0625 39.7734 3.29688C40.2422 3.52604 40.599 3.86198 40.8438 4.30469C41.0938 4.7474 41.2188 5.27604 41.2188 5.89062V9.4375C41.2188 9.81771 41.2448 10.1693 41.2969 10.4922C41.3542 10.8099 41.4349 11.013 41.5391 11.1016V11.3281H40.0156C39.9531 11.1042 39.901 10.8438 39.8594 10.5469C39.8229 10.2448 39.8047 9.95052 39.8047 9.66406L40.0625 9.74219C39.9375 10.0807 39.7396 10.388 39.4688 10.6641C39.1979 10.9401 38.8698 11.1589 38.4844 11.3203C38.099 11.4766 37.6823 11.5547 37.2344 11.5547ZM37.5547 10.3516C38.0078 10.3516 38.4036 10.25 38.7422 10.0469C39.0807 9.83854 39.3385 9.5599 39.5156 9.21094C39.6979 8.85677 39.7891 8.46354 39.7891 8.03125V6.8125L39.9531 6.83594C39.8021 7.00781 39.6094 7.14323 39.375 7.24219C39.1458 7.34115 38.8359 7.42969 38.4453 7.50781L37.5859 7.67969C37.0599 7.78906 36.6745 7.95573 36.4297 8.17969C36.1849 8.39844 36.0625 8.70573 36.0625 9.10156C36.0625 9.48177 36.2031 9.78646 36.4844 10.0156C36.7708 10.2396 37.1276 10.3516 37.5547 10.3516Z" fill="black"/>
<path d="M26.1641 3.17188H27.5859V5.13281L27.4766 5.08594C27.5599 4.66927 27.7135 4.30469 27.9375 3.99219C28.1667 3.67969 28.4609 3.4375 28.8203 3.26562C29.1797 3.09375 29.5938 3.00781 30.0625 3.00781C30.1823 3.00781 30.3047 3.01823 30.4297 3.03906V4.47656C30.3307 4.45573 30.2344 4.4401 30.1406 4.42969C30.0469 4.41927 29.9479 4.41406 29.8438 4.41406C29.3646 4.41406 28.9583 4.51042 28.625 4.70312C28.2917 4.89583 28.0365 5.1849 27.8594 5.57031C27.6875 5.95052 27.6016 6.42448 27.6016 6.99219V11.3281H26.1641V3.17188Z" fill="black"/>
<path d="M19.9688 11.5547C19.4167 11.5547 18.9401 11.4479 18.5391 11.2344C18.1432 11.0208 17.8385 10.7109 17.625 10.3047C17.4167 9.89844 17.3125 9.40885 17.3125 8.83594V3.17188H18.75V8.67188C18.75 9.19792 18.888 9.60677 19.1641 9.89844C19.4401 10.1901 19.8411 10.3359 20.3672 10.3359C20.7682 10.3359 21.1198 10.237 21.4219 10.0391C21.7292 9.84115 21.9688 9.54948 22.1406 9.16406C22.3125 8.77344 22.3984 8.29948 22.3984 7.74219V3.17188H23.8359V11.3281H22.4297V8.83594L22.6719 9.4375C22.5521 9.86979 22.3672 10.2448 22.1172 10.5625C21.8724 10.8802 21.5677 11.125 21.2031 11.2969C20.8385 11.4688 20.4271 11.5547 19.9688 11.5547Z" fill="black"/>
<path d="M11.8672 11.5547C11.138 11.5547 10.4974 11.3776 9.94531 11.0234C9.39323 10.6693 8.96615 10.1667 8.66406 9.51562C8.36719 8.86458 8.21875 8.10677 8.21875 7.24219C8.21875 6.3776 8.36719 5.6224 8.66406 4.97656C8.96615 4.32552 9.39323 3.82552 9.94531 3.47656C10.4974 3.1224 11.138 2.94531 11.8672 2.94531C12.5964 2.94531 13.237 3.1224 13.7891 3.47656C14.3411 3.82552 14.7656 4.32552 15.0625 4.97656C15.3646 5.6224 15.5156 6.3776 15.5156 7.24219C15.5156 8.10677 15.3646 8.86458 15.0625 9.51562C14.7656 10.1667 14.3411 10.6693 13.7891 11.0234C13.237 11.3776 12.5964 11.5547 11.8672 11.5547ZM11.8672 10.3359C12.3151 10.3359 12.6979 10.2161 13.0156 9.97656C13.3385 9.73177 13.5833 9.3776 13.75 8.91406C13.9219 8.45052 14.0078 7.89323 14.0078 7.24219C14.0078 6.26302 13.8203 5.50521 13.4453 4.96875C13.0703 4.43229 12.5443 4.16406 11.8672 4.16406C11.4193 4.16406 11.0339 4.28385 10.7109 4.52344C10.3932 4.76302 10.1484 5.11458 9.97656 5.57812C9.8099 6.03646 9.72656 6.59115 9.72656 7.24219C9.72656 7.89323 9.8099 8.45052 9.97656 8.91406C10.1484 9.3776 10.3932 9.73177 10.7109 9.97656C11.0339 10.2161 11.4193 10.3359 11.8672 10.3359Z" fill="black"/>
<path d="M3.39062 6.17969L3.71094 7.53906L0 0.125H1.64844L4.41406 6H3.86719L6.70312 0.125H8.26562L4.53906 7.53906L4.85938 6.17969V11.3281H3.39062V6.17969Z" fill="black"/>
<path d="M140.19 38.4648C139.789 38.4648 139.448 38.4033 139.165 38.2803C138.882 38.1572 138.661 37.9567 138.502 37.6787C138.347 37.3962 138.27 37.0293 138.27 36.5781V32.2236H137.012V31.1914H137.135C137.445 31.1914 137.693 31.1436 137.88 31.0479C138.071 30.9521 138.215 30.804 138.311 30.6035C138.406 30.403 138.463 30.1364 138.481 29.8037L138.522 29.0996H139.527V31.3008L139.377 31.1914H141.1V32.2236H139.527V36.4961C139.527 36.8197 139.607 37.0498 139.767 37.1865C139.931 37.3232 140.159 37.3916 140.45 37.3916C140.637 37.3916 140.81 37.3734 140.97 37.3369V38.3691C140.828 38.4056 140.699 38.4307 140.58 38.4443C140.462 38.458 140.332 38.4648 140.19 38.4648Z" fill="black"/>
<path d="M134.476 31.1914H135.733V38.3281H134.476V31.1914ZM135.104 30.0977C134.954 30.0977 134.813 30.0612 134.681 29.9883C134.553 29.9108 134.451 29.8083 134.373 29.6807C134.3 29.5485 134.264 29.4072 134.264 29.2568C134.264 29.1064 134.3 28.9674 134.373 28.8398C134.451 28.7077 134.553 28.6051 134.681 28.5322C134.813 28.4548 134.954 28.416 135.104 28.416C135.255 28.416 135.394 28.4548 135.521 28.5322C135.654 28.6051 135.756 28.7077 135.829 28.8398C135.907 28.9674 135.945 29.1064 135.945 29.2568C135.945 29.4072 135.907 29.5485 135.829 29.6807C135.756 29.8083 135.654 29.9108 135.521 29.9883C135.394 30.0612 135.255 30.0977 135.104 30.0977Z" fill="black"/>
<path d="M128.057 28.5254H129.314V31.1914H128.057V28.5254ZM125.965 38.5264C125.354 38.5264 124.83 38.3737 124.393 38.0684C123.955 37.7585 123.622 37.3232 123.395 36.7627C123.167 36.1976 123.053 35.5345 123.053 34.7734C123.053 34.0169 123.171 33.3538 123.408 32.7842C123.65 32.2145 123.994 31.7747 124.44 31.4648C124.887 31.1504 125.409 30.9932 126.006 30.9932C126.389 30.9932 126.737 31.0661 127.052 31.2119C127.371 31.3577 127.642 31.5788 127.865 31.875C128.093 32.1712 128.262 32.5404 128.371 32.9824L128.057 32.7021V31.1914H129.314V38.3281H128.098V36.7354L128.357 36.5371C128.212 37.1615 127.924 37.6491 127.496 38C127.068 38.3509 126.557 38.5264 125.965 38.5264ZM126.19 37.46C126.582 37.46 126.917 37.346 127.195 37.1182C127.478 36.8857 127.69 36.5667 127.831 36.1611C127.977 35.751 128.05 35.2793 128.05 34.7461C128.05 34.2174 127.977 33.7503 127.831 33.3447C127.69 32.9391 127.478 32.6247 127.195 32.4014C126.917 32.1735 126.582 32.0596 126.19 32.0596C125.799 32.0596 125.466 32.1735 125.192 32.4014C124.919 32.6247 124.714 32.9391 124.577 33.3447C124.44 33.7458 124.372 34.2129 124.372 34.7461C124.372 35.2793 124.44 35.751 124.577 36.1611C124.714 36.5667 124.919 36.8857 125.192 37.1182C125.466 37.346 125.799 37.46 126.19 37.46Z" fill="black"/>
<path d="M120.161 28.5254H121.419V38.3281H120.161V28.5254Z" fill="black"/>
<path d="M116.866 31.1914H118.124V38.3281H116.866V31.1914ZM117.495 30.0977C117.345 30.0977 117.203 30.0612 117.071 29.9883C116.944 29.9108 116.841 29.8083 116.764 29.6807C116.691 29.5485 116.654 29.4072 116.654 29.2568C116.654 29.1064 116.691 28.9674 116.764 28.8398C116.841 28.7077 116.944 28.6051 117.071 28.5322C117.203 28.4548 117.345 28.416 117.495 28.416C117.646 28.416 117.785 28.4548 117.912 28.5322C118.044 28.6051 118.147 28.7077 118.22 28.8398C118.297 28.9674 118.336 29.1064 118.336 29.2568C118.336 29.4072 118.297 29.5485 118.22 29.6807C118.147 29.8083 118.044 29.9108 117.912 29.9883C117.785 30.0612 117.646 30.0977 117.495 30.0977Z" fill="black"/>
<path d="M111.445 38.5264C110.962 38.5264 110.545 38.4329 110.194 38.2461C109.848 38.0592 109.581 37.7881 109.395 37.4326C109.212 37.0771 109.121 36.6488 109.121 36.1475V31.1914H110.379V36.0039C110.379 36.4642 110.5 36.8219 110.741 37.0771C110.983 37.3324 111.334 37.46 111.794 37.46C112.145 37.46 112.452 37.3734 112.717 37.2002C112.986 37.027 113.195 36.7718 113.346 36.4346C113.496 36.0928 113.571 35.6781 113.571 35.1904V31.1914H114.829V38.3281H113.599V36.1475L113.811 36.6738C113.706 37.0521 113.544 37.3802 113.325 37.6582C113.111 37.9362 112.844 38.1504 112.525 38.3008C112.206 38.4512 111.846 38.5264 111.445 38.5264Z" fill="black"/>
<path d="M104.589 38.5264C104.206 38.5264 103.855 38.4535 103.536 38.3076C103.222 38.1618 102.951 37.9408 102.723 37.6445C102.499 37.3483 102.333 36.9792 102.224 36.5371L102.497 36.8174V38.3281H101.28V31.1914H102.538V32.7842L102.237 32.9824C102.333 32.5677 102.495 32.2122 102.723 31.916C102.951 31.6152 103.229 31.3874 103.557 31.2324C103.889 31.0729 104.252 30.9932 104.644 30.9932C105.25 30.9932 105.769 31.1481 106.202 31.458C106.64 31.7633 106.972 32.2008 107.2 32.7705C107.428 33.3356 107.542 33.9987 107.542 34.7598C107.542 35.5163 107.424 36.1794 107.187 36.749C106.95 37.3141 106.608 37.7516 106.161 38.0615C105.715 38.3714 105.19 38.5264 104.589 38.5264ZM104.404 37.46C104.796 37.46 105.129 37.3483 105.402 37.125C105.676 36.8971 105.881 36.5827 106.018 36.1816C106.154 35.776 106.223 35.3066 106.223 34.7734C106.223 34.2402 106.154 33.7708 106.018 33.3652C105.881 32.9551 105.676 32.6361 105.402 32.4082C105.129 32.1758 104.796 32.0596 104.404 32.0596C104.012 32.0596 103.675 32.1758 103.393 32.4082C103.115 32.6361 102.903 32.9551 102.757 33.3652C102.616 33.7708 102.545 34.2402 102.545 34.7734C102.545 35.3021 102.616 35.7692 102.757 36.1748C102.903 36.5804 103.115 36.8971 103.393 37.125C103.675 37.3483 104.012 37.46 104.404 37.46ZM101.28 28.5254H102.538V31.1914H101.28V28.5254Z" fill="black"/>
<path d="M93.3369 38.5264C92.6989 38.5264 92.1383 38.3714 91.6553 38.0615C91.1722 37.7516 90.7985 37.3118 90.5342 36.7422C90.2744 36.1725 90.1445 35.5094 90.1445 34.7529C90.1445 33.9964 90.2744 33.3356 90.5342 32.7705C90.7985 32.2008 91.1722 31.7633 91.6553 31.458C92.1383 31.1481 92.6989 30.9932 93.3369 30.9932C93.9749 30.9932 94.5355 31.1481 95.0186 31.458C95.5016 31.7633 95.873 32.2008 96.1328 32.7705C96.3971 33.3356 96.5293 33.9964 96.5293 34.7529C96.5293 35.5094 96.3971 36.1725 96.1328 36.7422C95.873 37.3118 95.5016 37.7516 95.0186 38.0615C94.5355 38.3714 93.9749 38.5264 93.3369 38.5264ZM93.3369 37.46C93.7288 37.46 94.0638 37.3551 94.3418 37.1455C94.6243 36.9313 94.8385 36.6214 94.9844 36.2158C95.1348 35.8102 95.21 35.3226 95.21 34.7529C95.21 33.8962 95.0459 33.2331 94.7178 32.7637C94.3896 32.2943 93.9294 32.0596 93.3369 32.0596C92.945 32.0596 92.6077 32.1644 92.3252 32.374C92.0472 32.5837 91.833 32.8913 91.6826 33.2969C91.5368 33.6979 91.4639 34.1833 91.4639 34.7529C91.4639 35.3226 91.5368 35.8102 91.6826 36.2158C91.833 36.6214 92.0472 36.9313 92.3252 37.1455C92.6077 37.3551 92.945 37.46 93.3369 37.46Z" fill="black"/>
<path d="M88.333 38.4648C87.932 38.4648 87.5902 38.4033 87.3076 38.2803C87.0251 38.1572 86.804 37.9567 86.6445 37.6787C86.4896 37.3962 86.4121 37.0293 86.4121 36.5781V32.2236H85.1543V31.1914H85.2773C85.5872 31.1914 85.8356 31.1436 86.0225 31.0479C86.2139 30.9521 86.3574 30.804 86.4531 30.6035C86.5488 30.403 86.6058 30.1364 86.624 29.8037L86.665 29.0996H87.6699V31.3008L87.5195 31.1914H89.2422V32.2236H87.6699V36.4961C87.6699 36.8197 87.7497 37.0498 87.9092 37.1865C88.0732 37.3232 88.3011 37.3916 88.5928 37.3916C88.7796 37.3916 88.9528 37.3734 89.1123 37.3369V38.3691C88.971 38.4056 88.8411 38.4307 88.7227 38.4443C88.6042 38.458 88.4743 38.4648 88.333 38.4648Z" fill="black"/>
<path d="M78.0791 38.5264C77.4456 38.5264 76.8851 38.3896 76.3975 38.1162C75.9098 37.8382 75.527 37.4167 75.249 36.8516C74.971 36.2865 74.832 35.5846 74.832 34.7461C74.832 33.9941 74.971 33.3356 75.249 32.7705C75.5316 32.2008 75.9189 31.7633 76.4111 31.458C76.9033 31.1481 77.4593 30.9932 78.0791 30.9932C78.6898 30.9932 79.2253 31.1344 79.6855 31.417C80.1458 31.6995 80.5013 32.1143 80.752 32.6611C81.0072 33.2035 81.1348 33.8597 81.1348 34.6299C81.1348 34.7712 81.1348 34.9124 81.1348 35.0537H75.4746V34.042H80.417L79.8018 34.377C79.8018 33.8574 79.7334 33.4245 79.5967 33.0781C79.4645 32.7318 79.2686 32.4743 79.0088 32.3057C78.7536 32.137 78.4391 32.0527 78.0654 32.0527C77.6872 32.0527 77.3522 32.153 77.0605 32.3535C76.7689 32.554 76.5387 32.8503 76.3701 33.2422C76.2061 33.6341 76.124 34.1058 76.124 34.6572V34.7324C76.124 35.3385 76.2061 35.8444 76.3701 36.25C76.5342 36.6556 76.7689 36.9587 77.0742 37.1592C77.3796 37.3597 77.7441 37.46 78.168 37.46C78.61 37.46 78.9769 37.3301 79.2686 37.0703C79.5648 36.806 79.7471 36.4323 79.8154 35.9492H81.0938C81.0391 36.4596 80.8841 36.9085 80.6289 37.2959C80.3737 37.6833 80.0273 37.9863 79.5898 38.2051C79.1569 38.4193 78.6533 38.5264 78.0791 38.5264Z" fill="black"/>
<path d="M71.9404 28.5254H73.1982V38.3281H71.9404V28.5254Z" fill="black"/>
<path d="M67.3467 38.5264C66.9639 38.5264 66.613 38.4535 66.2939 38.3076C65.9795 38.1618 65.7083 37.9408 65.4805 37.6445C65.2572 37.3483 65.0908 36.9792 64.9814 36.5371L65.2549 36.8174V38.3281H64.0381V31.1914H65.2959V32.7842L64.9951 32.9824C65.0908 32.5677 65.2526 32.2122 65.4805 31.916C65.7083 31.6152 65.9863 31.3874 66.3145 31.2324C66.6471 31.0729 67.0094 30.9932 67.4014 30.9932C68.0075 30.9932 68.527 31.1481 68.96 31.458C69.3975 31.7633 69.7301 32.2008 69.958 32.7705C70.1859 33.3356 70.2998 33.9987 70.2998 34.7598C70.2998 35.5163 70.1813 36.1794 69.9443 36.749C69.7074 37.3141 69.3656 37.7516 68.9189 38.0615C68.4723 38.3714 67.9482 38.5264 67.3467 38.5264ZM67.1621 37.46C67.554 37.46 67.8867 37.3483 68.1602 37.125C68.4336 36.8971 68.6387 36.5827 68.7754 36.1816C68.9121 35.776 68.9805 35.3066 68.9805 34.7734C68.9805 34.2402 68.9121 33.7708 68.7754 33.3652C68.6387 32.9551 68.4336 32.6361 68.1602 32.4082C67.8867 32.1758 67.554 32.0596 67.1621 32.0596C66.7702 32.0596 66.4329 32.1758 66.1504 32.4082C65.8724 32.6361 65.6605 32.9551 65.5146 33.3652C65.3734 33.7708 65.3027 34.2402 65.3027 34.7734C65.3027 35.3021 65.3734 35.7692 65.5146 36.1748C65.6605 36.5804 65.8724 36.8971 66.1504 37.125C66.4329 37.3483 66.7702 37.46 67.1621 37.46ZM64.0381 28.5254H65.2959V31.1914H64.0381V28.5254Z" fill="black"/>
<path d="M58.5762 38.5264C58.1341 38.5264 57.7354 38.4421 57.3799 38.2734C57.0244 38.1003 56.7441 37.8542 56.5391 37.5352C56.3385 37.2161 56.2383 36.8447 56.2383 36.4209C56.2383 35.7692 56.4342 35.2633 56.8262 34.9033C57.2227 34.5387 57.7764 34.2995 58.4873 34.1855L59.6221 34.001C59.9137 33.9508 60.1416 33.8962 60.3057 33.8369C60.4697 33.7731 60.5905 33.6911 60.668 33.5908C60.7454 33.486 60.7842 33.347 60.7842 33.1738C60.7842 32.9824 60.7318 32.8001 60.627 32.627C60.5221 32.4538 60.3604 32.3125 60.1416 32.2031C59.9229 32.0938 59.6517 32.0391 59.3281 32.0391C58.8724 32.0391 58.501 32.1598 58.2139 32.4014C57.9313 32.6383 57.7764 32.9665 57.749 33.3857H56.4297C56.4434 32.93 56.5732 32.5221 56.8193 32.1621C57.0654 31.7975 57.4049 31.5127 57.8379 31.3076C58.2708 31.098 58.7676 30.9932 59.3281 30.9932C59.8978 30.9932 60.3877 31.0957 60.7979 31.3008C61.208 31.5013 61.5202 31.7952 61.7344 32.1826C61.9531 32.57 62.0625 33.0326 62.0625 33.5703V36.6738C62.0625 37.0065 62.0853 37.3141 62.1309 37.5967C62.181 37.8747 62.2516 38.0524 62.3428 38.1299V38.3281H61.0098C60.9551 38.1322 60.9095 37.9043 60.873 37.6445C60.8411 37.3802 60.8252 37.1227 60.8252 36.8721L61.0508 36.9404C60.9414 37.2367 60.7682 37.5055 60.5312 37.7471C60.2943 37.9886 60.0072 38.18 59.6699 38.3213C59.3327 38.458 58.9681 38.5264 58.5762 38.5264ZM58.8564 37.4736C59.2529 37.4736 59.5993 37.3848 59.8955 37.207C60.1917 37.0247 60.4173 36.7809 60.5723 36.4756C60.7318 36.1657 60.8115 35.8216 60.8115 35.4434V34.377L60.9551 34.3975C60.8229 34.5479 60.6543 34.6663 60.4492 34.7529C60.2487 34.8395 59.9775 34.917 59.6357 34.9854L58.8838 35.1357C58.4235 35.2314 58.0863 35.3773 57.8721 35.5732C57.6579 35.7646 57.5508 36.0335 57.5508 36.3799C57.5508 36.7126 57.6738 36.9792 57.9199 37.1797C58.1706 37.3757 58.4827 37.4736 58.8564 37.4736Z" fill="black"/>
<path d="M49.1768 31.1914H50.5166L52.5332 37.084H52.2939L54.2695 31.1914H55.5479L52.957 38.3281H51.7949L49.1768 31.1914Z" fill="black"/>
<path d="M45.2666 38.5264C44.6286 38.5264 44.068 38.3714 43.585 38.0615C43.1019 37.7516 42.7282 37.3118 42.4639 36.7422C42.2041 36.1725 42.0742 35.5094 42.0742 34.7529C42.0742 33.9964 42.2041 33.3356 42.4639 32.7705C42.7282 32.2008 43.1019 31.7633 43.585 31.458C44.068 31.1481 44.6286 30.9932 45.2666 30.9932C45.9046 30.9932 46.4652 31.1481 46.9482 31.458C47.4313 31.7633 47.8027 32.2008 48.0625 32.7705C48.3268 33.3356 48.459 33.9964 48.459 34.7529C48.459 35.5094 48.3268 36.1725 48.0625 36.7422C47.8027 37.3118 47.4313 37.7516 46.9482 38.0615C46.4652 38.3714 45.9046 38.5264 45.2666 38.5264ZM45.2666 37.46C45.6585 37.46 45.9935 37.3551 46.2715 37.1455C46.554 36.9313 46.7682 36.6214 46.9141 36.2158C47.0645 35.8102 47.1396 35.3226 47.1396 34.7529C47.1396 33.8962 46.9756 33.2331 46.6475 32.7637C46.3193 32.2943 45.859 32.0596 45.2666 32.0596C44.8747 32.0596 44.5374 32.1644 44.2549 32.374C43.9769 32.5837 43.7627 32.8913 43.6123 33.2969C43.4665 33.6979 43.3936 34.1833 43.3936 34.7529C43.3936 35.3226 43.4665 35.8102 43.6123 36.2158C43.7627 36.6214 43.9769 36.9313 44.2549 37.1455C44.5374 37.3551 44.8747 37.46 45.2666 37.46Z" fill="black"/>
<path d="M35.8672 28.5254H37.1523V37.5557L36.9336 37.166H41.4248V38.3281H35.8672V28.5254Z" fill="black"/>
<path d="M25.6064 28.5254H26.8643V34.6299H26.5293L29.6465 31.1914H31.2461L26.3311 36.4961L26.8643 35.4023V38.3281H25.6064V28.5254ZM27.7051 34.4932L28.3887 33.3037L31.5264 38.3281H30.0156L27.7051 34.4932Z" fill="black"/>
<path d="M21.2109 38.5264C20.5911 38.5264 20.0511 38.4261 19.5908 38.2256C19.1351 38.0205 18.7819 37.7311 18.5312 37.3574C18.2806 36.9837 18.1507 36.5417 18.1416 36.0312H19.4609C19.4792 36.487 19.6501 36.8402 19.9736 37.0908C20.2972 37.3369 20.7188 37.46 21.2383 37.46C21.6758 37.46 22.0267 37.3643 22.291 37.1729C22.5599 36.9814 22.6943 36.7217 22.6943 36.3936C22.6943 36.1292 22.5986 35.9219 22.4072 35.7715C22.2204 35.6211 21.915 35.5026 21.4912 35.416L20.3154 35.1768C19.6273 35.04 19.1214 34.8145 18.7979 34.5C18.4743 34.181 18.3125 33.7686 18.3125 33.2627C18.3125 32.8343 18.4242 32.4492 18.6475 32.1074C18.8708 31.7611 19.1921 31.4899 19.6113 31.2939C20.0306 31.0934 20.5251 30.9932 21.0947 30.9932C21.6781 30.9932 22.1771 31.0957 22.5918 31.3008C23.0065 31.5059 23.3232 31.7907 23.542 32.1553C23.7607 32.5153 23.8792 32.9255 23.8975 33.3857H22.6055C22.5872 32.9665 22.4391 32.6383 22.1611 32.4014C21.8877 32.1598 21.5231 32.0391 21.0674 32.0391C20.7803 32.0391 20.5296 32.0846 20.3154 32.1758C20.1012 32.2624 19.9349 32.3877 19.8164 32.5518C19.6979 32.7113 19.6387 32.8958 19.6387 33.1055C19.6387 33.3424 19.7207 33.5316 19.8848 33.6729C20.0488 33.8141 20.3086 33.9189 20.6641 33.9873L21.9766 34.2402C22.4141 34.3268 22.7832 34.4567 23.084 34.6299C23.3848 34.7985 23.6126 35.015 23.7676 35.2793C23.9271 35.5436 24.0068 35.8512 24.0068 36.2021C24.0068 36.6943 23.8838 37.1159 23.6377 37.4668C23.3962 37.8177 23.0635 38.082 22.6396 38.2598C22.2158 38.4375 21.7396 38.5264 21.2109 38.5264Z" fill="black"/>
<path d="M12.3926 28.5254H13.917L17.417 38.3281H16.0293L13.0215 29.5508H13.2607L10.2188 38.3281H8.8584L12.3926 28.5254ZM10.8203 34.1992H15.5986V35.3477H10.8203V34.1992Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,14 +0,0 @@
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Twitterbot
Allow: /
User-agent: facebookexternalhit
Allow: /
User-agent: *
Allow: /

View file

@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View file

@ -1,31 +0,0 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { PreferencesProvider } from "@/contexts/PreferencesContext";
import Index from "./pages/Index.tsx";
import Preferences from "./pages/Preferences.tsx";
import NotFound from "./pages/NotFound.tsx";
const queryClient = new QueryClient();
const App = () => (
<QueryClientProvider client={queryClient}>
<PreferencesProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/preferences" element={<Preferences />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</PreferencesProvider>
</QueryClientProvider>
);
export default App;

View file

@ -1,39 +0,0 @@
import { Globe, Code, Image, Newspaper } from "lucide-react";
import { CATEGORIES, type Category } from "@/lib/mock-data";
const CATEGORY_META: Record<Category, { label: string; icon: React.ElementType }> = {
general: { label: "General", icon: Globe },
it: { label: "IT", icon: Code },
images: { label: "Images", icon: Image },
news: { label: "News", icon: Newspaper },
};
interface CategoryTabsProps {
active: Category;
onChange: (c: Category) => void;
}
export function CategoryTabs({ active, onChange }: CategoryTabsProps) {
return (
<div className="flex gap-1">
{CATEGORIES.map((cat) => {
const { label, icon: Icon } = CATEGORY_META[cat];
const isActive = cat === active;
return (
<button
key={cat}
onClick={() => onChange(cat)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-secondary"
}`}
>
<Icon size={14} />
{label}
</button>
);
})}
</div>
);
}

View file

@ -1,28 +0,0 @@
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
className?: string;
activeClassName?: string;
pendingClassName?: string;
}
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
return (
<RouterNavLink
ref={ref}
to={to}
className={({ isActive, isPending }) =>
cn(className, isActive && activeClassName, isPending && pendingClassName)
}
{...props}
/>
);
},
);
NavLink.displayName = "NavLink";
export { NavLink };

View file

@ -1,40 +0,0 @@
import type { SearchResult } from "@/lib/mock-data";
interface ResultCardProps {
result: SearchResult;
}
export function ResultCard({ result }: ResultCardProps) {
const domain = result.parsed_url[1];
const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
return (
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-3 -mx-4 rounded-lg transition-colors hover:bg-result-hover group"
>
<div className="flex items-center gap-2 mb-1">
<img src={faviconUrl} alt="" className="w-4 h-4 rounded-sm" loading="lazy" />
<span className="text-xs text-muted-foreground truncate">{result.pretty_url}</span>
{result.engines.length > 1 && (
<span className="text-[10px] text-muted-foreground/60 ml-auto shrink-0">
{result.engines.length} engines
</span>
)}
</div>
<h3 className="text-link group-hover:text-link-hover font-medium text-base leading-snug mb-1 line-clamp-1">
{result.title}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-2">
{result.content}
</p>
{result.publishedDate && (
<span className="text-xs text-muted-foreground/70 mt-1 inline-block">
{new Date(result.publishedDate).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })}
</span>
)}
</a>
);
}

View file

@ -1,19 +0,0 @@
export function ResultSkeleton() {
return (
<div className="space-y-6">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="animate-pulse space-y-2 px-4 py-3">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-muted rounded-sm" />
<div className="h-3 w-40 bg-muted rounded" />
</div>
<div className="h-5 w-3/4 bg-muted rounded" />
<div className="space-y-1.5">
<div className="h-3.5 w-full bg-muted rounded" />
<div className="h-3.5 w-5/6 bg-muted rounded" />
</div>
</div>
))}
</div>
);
}

View file

@ -1,43 +0,0 @@
import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { FormEvent, useRef, useEffect } from "react";
interface SearchInputProps {
query: string;
onQueryChange: (q: string) => void;
onSearch: (q: string) => void;
compact?: boolean;
autoFocus?: boolean;
}
export function SearchInput({ query, onQueryChange, onSearch, compact, autoFocus }: SearchInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (autoFocus) inputRef.current?.focus();
}, [autoFocus]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit} className={`flex items-center gap-2 w-full ${compact ? "max-w-xl" : "max-w-2xl"}`}>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={compact ? 16 : 20} />
<Input
ref={inputRef}
value={query}
onChange={(e) => onQueryChange(e.target.value)}
placeholder="Search the web privately..."
className={`pl-10 pr-4 border-input bg-background focus-visible:ring-ring ${compact ? "h-9 text-sm" : "h-12 text-base"}`}
/>
</div>
<Button type="submit" size={compact ? "sm" : "default"} className="bg-primary text-primary-foreground hover:bg-primary/90 shrink-0">
Search
</Button>
</form>
);
}

View file

@ -1,52 +0,0 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View file

@ -1,104 +0,0 @@
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View file

@ -1,43 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
),
);
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
),
);
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View file

@ -1,5 +0,0 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

View file

@ -1,38 +0,0 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View file

@ -1,29 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View file

@ -1,90 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View file

@ -1,47 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View file

@ -1,54 +0,0 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };

View file

@ -1,43 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View file

@ -1,224 +0,0 @@
import * as React from "react";
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(
({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
{...props}
/>
</div>
);
},
);
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
{...props}
/>
);
},
);
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
},
);
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
},
);
CarouselNext.displayName = "CarouselNext";
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };

View file

@ -1,303 +0,0 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };

View file

@ -1,26 +0,0 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View file

@ -1,9 +0,0 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -1,132 +0,0 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View file

@ -1,178 +0,0 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View file

@ -1,95 +0,0 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View file

@ -1,87 +0,0 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View file

@ -1,179 +0,0 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -1,129 +0,0 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
},
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
},
);
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
},
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
{body}
</p>
);
},
);
FormMessage.displayName = "FormMessage";
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };

View file

@ -1,27 +0,0 @@
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };

View file

@ -1,61 +0,0 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<React.ElementRef<typeof OTPInput>, React.ComponentPropsWithoutRef<typeof OTPInput>>(
({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
),
);
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center", className)} {...props} />,
);
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
),
);
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View file

@ -1,22 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View file

@ -1,17 +0,0 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View file

@ -1,207 +0,0 @@
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn("flex h-10 items-center space-x-1 rounded-md border bg-background p-1", className)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View file

@ -1,120 +0,0 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View file

@ -1,81 +0,0 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View file

@ -1,29 +0,0 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View file

@ -1,23 +0,0 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View file

@ -1,36 +0,0 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };

View file

@ -1,37 +0,0 @@
import { GripVertical } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View file

@ -1,38 +0,0 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View file

@ -1,143 +0,0 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View file

@ -1,20 +0,0 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View file

@ -1,107 +0,0 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};

View file

@ -1,637 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
});
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
});
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
},
);
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
},
);
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
)}
{...props}
/>
);
},
);
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
);
},
);
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
},
);
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
},
);
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
),
);
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
</Tooltip>
);
});
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ ...props }, ref) => (
<li ref={ref} {...props} />
));
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View file

@ -1,7 +0,0 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
}
export { Skeleton };

View file

@ -1,23 +0,0 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View file

@ -1,27 +0,0 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, toast } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster, toast };

View file

@ -1,27 +0,0 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View file

@ -1,72 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
),
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
);
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
),
);
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
),
);
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", className)}
{...props}
/>
),
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
),
);
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
),
);
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
),
);
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

View file

@ -1,53 +0,0 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View file

@ -1,21 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };

View file

@ -1,111 +0,0 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View file

@ -1,24 +0,0 @@
import { useToast } from "@/hooks/use-toast";
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View file

@ -1,49 +0,0 @@
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props}>
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };

View file

@ -1,37 +0,0 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };

View file

@ -1,28 +0,0 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -1,3 +0,0 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };

View file

@ -1,67 +0,0 @@
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
export interface SearchEngine {
id: string;
name: string;
enabled: boolean;
}
const DEFAULT_ENGINES: SearchEngine[] = [
{ id: "google", name: "Google", enabled: true },
{ id: "duckduckgo", name: "DuckDuckGo", enabled: true },
{ id: "brave", name: "Brave", enabled: true },
{ id: "bing", name: "Bing", enabled: false },
{ id: "wikipedia", name: "Wikipedia", enabled: true },
];
type Theme = "light" | "dark";
interface PreferencesState {
theme: Theme;
engines: SearchEngine[];
setTheme: (t: Theme) => void;
toggleEngine: (id: string) => void;
}
const PreferencesContext = createContext<PreferencesState | null>(null);
function loadFromStorage<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback;
}
}
export function PreferencesProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => loadFromStorage("kafka-theme", "light"));
const [engines, setEngines] = useState<SearchEngine[]>(() => loadFromStorage("kafka-engines", DEFAULT_ENGINES));
useEffect(() => {
document.documentElement.classList.toggle("dark", theme === "dark");
localStorage.setItem("kafka-theme", JSON.stringify(theme));
}, [theme]);
useEffect(() => {
localStorage.setItem("kafka-engines", JSON.stringify(engines));
}, [engines]);
const setTheme = (t: Theme) => setThemeState(t);
const toggleEngine = (id: string) => {
setEngines((prev) => prev.map((e) => (e.id === id ? { ...e, enabled: !e.enabled } : e)));
};
return (
<PreferencesContext.Provider value={{ theme, engines, setTheme, toggleEngine }}>
{children}
</PreferencesContext.Provider>
);
}
export function usePreferences() {
const ctx = useContext(PreferencesContext);
if (!ctx) throw new Error("usePreferences must be used within PreferencesProvider");
return ctx;
}

View file

@ -1,19 +0,0 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View file

@ -1,72 +0,0 @@
import { useState, useCallback } from "react";
import type { SearXNGResponse, Category } from "@/lib/mock-data";
interface SearchState {
query: string;
results: SearXNGResponse | null;
isLoading: boolean;
error: string | null;
activeCategory: Category;
hasSearched: boolean;
}
export function useSearch() {
const [state, setState] = useState<SearchState>({
query: "",
results: null,
isLoading: false,
error: null,
activeCategory: "general",
hasSearched: false,
});
const search = useCallback(async (query: string) => {
if (!query.trim()) return;
setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));
try {
const response = await fetch(`/search?format=json&q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setState((prev) => ({
...prev,
isLoading: false,
results: data,
}));
} catch (err) {
setState((prev) => ({
...prev,
isLoading: false,
error: err instanceof Error ? err.message : "Search failed",
}));
}
}, []);
const setCategory = useCallback((category: Category) => {
setState((prev) => ({ ...prev, activeCategory: category }));
}, []);
const setQuery = useCallback((query: string) => {
setState((prev) => ({ ...prev, query }));
}, []);
const reset = useCallback(() => {
setState({
query: "",
results: null,
isLoading: false,
error: null,
activeCategory: "general",
hasSearched: false,
});
}, []);
const filteredResults = state.results?.results.filter(
(r) => state.activeCategory === "general" || r.category === state.activeCategory
);
return { ...state, filteredResults, search, setCategory, setQuery, reset };
}

View file

@ -1,186 +0,0 @@
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View file

@ -1,84 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;700&family=Inter:wght@400;500;600&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 40 33% 97%;
--foreground: 220 20% 14%;
--card: 40 25% 95%;
--card-foreground: 220 20% 14%;
--popover: 40 33% 97%;
--popover-foreground: 220 20% 14%;
--primary: 168 60% 36%;
--primary-foreground: 0 0% 100%;
--secondary: 40 20% 92%;
--secondary-foreground: 220 20% 14%;
--muted: 40 15% 90%;
--muted-foreground: 220 10% 50%;
--accent: 168 60% 36%;
--accent-foreground: 0 0% 100%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 40 15% 87%;
--input: 40 15% 87%;
--ring: 168 60% 36%;
--radius: 0.5rem;
--link: 215 80% 50%;
--link-hover: 215 80% 40%;
--result-hover: 40 25% 94%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 220 20% 8%;
--foreground: 40 20% 90%;
--card: 220 18% 12%;
--card-foreground: 40 20% 90%;
--popover: 220 20% 8%;
--popover-foreground: 40 20% 90%;
--primary: 168 55% 45%;
--primary-foreground: 0 0% 100%;
--secondary: 220 15% 16%;
--secondary-foreground: 40 20% 90%;
--muted: 220 15% 18%;
--muted-foreground: 220 10% 55%;
--accent: 168 55% 45%;
--accent-foreground: 0 0% 100%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 100%;
--border: 220 15% 20%;
--input: 220 15% 20%;
--ring: 168 55% 45%;
--link: 215 75% 62%;
--link-hover: 215 75% 72%;
--result-hover: 220 18% 14%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground antialiased;
font-family: 'Inter', system-ui, sans-serif;
}
}

View file

@ -1,127 +0,0 @@
export interface SearchResult {
url: string;
title: string;
content: string;
engine: string;
parsed_url: [string, string, string, string, string]; // scheme, host, path, query, fragment
engines: string[];
positions: number[];
score: number;
category: string;
pretty_url: string;
img_src?: string;
thumbnail?: string;
publishedDate?: string;
}
export interface SearXNGResponse {
query: string;
number_of_results: number;
results: SearchResult[];
answers: string[];
corrections: string[];
infoboxes: any[];
suggestions: string[];
unresponsive_engines: string[];
}
export const MOCK_RESPONSE: SearXNGResponse = {
query: "react meta search engine",
number_of_results: 42,
results: [
{
url: "https://github.com/searxng/searxng",
title: "SearXNG — A privacy-respecting, hackable metasearch engine",
content: "SearXNG is a free internet metasearch engine which aggregates results from more than 70 search services. Users are neither tracked nor profiled.",
engine: "google",
parsed_url: ["https", "github.com", "/searxng/searxng", "", ""],
engines: ["google", "bing", "duckduckgo"],
positions: [1, 2, 1],
score: 9.5,
category: "general",
pretty_url: "github.com/searxng/searxng",
},
{
url: "https://docs.searxng.org/",
title: "SearXNG Documentation — Getting Started Guide",
content: "Learn how to install, configure, and customize SearXNG for your own self-hosted search experience. Comprehensive documentation for administrators and developers.",
engine: "duckduckgo",
parsed_url: ["https", "docs.searxng.org", "/", "", ""],
engines: ["duckduckgo", "bing"],
positions: [3, 4],
score: 7.2,
category: "general",
pretty_url: "docs.searxng.org",
},
{
url: "https://react.dev/learn",
title: "Quick Start React",
content: "React lets you build user interfaces out of individual pieces called components. Create your own React components like Thumbnail, LikeButton, and Video.",
engine: "google",
parsed_url: ["https", "react.dev", "/learn", "", ""],
engines: ["google", "bing", "duckduckgo"],
positions: [2, 1, 3],
score: 8.8,
category: "it",
pretty_url: "react.dev/learn",
},
{
url: "https://tailwindcss.com/docs",
title: "Tailwind CSS — Rapidly build modern websites without ever leaving your HTML",
content: "A utility-first CSS framework packed with classes that can be composed to build any design, directly in your markup. Fast, flexible, and reliable.",
engine: "bing",
parsed_url: ["https", "tailwindcss.com", "/docs", "", ""],
engines: ["bing", "google"],
positions: [5, 6],
score: 6.4,
category: "it",
pretty_url: "tailwindcss.com/docs",
},
{
url: "https://www.wired.com/story/meta-search-engines-privacy/",
title: "Why Meta Search Engines Are the Future of Private Web Search",
content: "As concerns about data privacy grow, meta search engines offer a compelling alternative to traditional search giants. Here's how they protect your data while delivering results.",
engine: "google",
parsed_url: ["https", "www.wired.com", "/story/meta-search-engines-privacy/", "", ""],
engines: ["google", "duckduckgo"],
positions: [4, 5],
score: 5.9,
category: "news",
pretty_url: "wired.com/story/meta-search-engines-privacy",
publishedDate: "2024-01-15",
},
{
url: "https://arstechnica.com/information-technology/2024/self-hosted-search/",
title: "Self-hosted search: Taking back control of your queries",
content: "More developers are turning to self-hosted metasearch solutions. We look at the best options available today, from SearXNG to Whoogle and beyond.",
engine: "duckduckgo",
parsed_url: ["https", "arstechnica.com", "/information-technology/2024/self-hosted-search/", "", ""],
engines: ["duckduckgo", "google"],
positions: [6, 8],
score: 5.1,
category: "news",
pretty_url: "arstechnica.com/information-technology/2024/self-hosted-search",
publishedDate: "2024-02-20",
},
{
url: "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
title: "Fetch API - Web APIs | MDN",
content: "The Fetch API provides an interface for fetching resources across the network. It provides a more powerful and flexible feature set than XMLHttpRequest.",
engine: "google",
parsed_url: ["https", "developer.mozilla.org", "/en-US/docs/Web/API/Fetch_API", "", ""],
engines: ["google", "bing"],
positions: [7, 9],
score: 4.8,
category: "it",
pretty_url: "developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
},
],
answers: [],
corrections: [],
infoboxes: [],
suggestions: ["searxng setup guide", "private search engine comparison", "react search component"],
unresponsive_engines: [],
};
export const CATEGORIES = ["general", "it", "images", "news"] as const;
export type Category = typeof CATEGORIES[number];

View file

@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -1,5 +0,0 @@
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(<App />);

View file

@ -1,93 +0,0 @@
import { Search as SearchIcon, Settings } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { SearchInput } from "@/components/SearchInput";
import { CategoryTabs } from "@/components/CategoryTabs";
import { ResultCard } from "@/components/ResultCard";
import { ResultSkeleton } from "@/components/ResultSkeleton";
import { useSearch } from "@/hooks/use-search";
const Index = () => {
const { query, results, isLoading, hasSearched, activeCategory, filteredResults, search, setCategory, setQuery, reset } = useSearch();
const navigate = useNavigate();
const settingsButton = (
<button
onClick={() => navigate("/preferences")}
className="p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
aria-label="Preferences"
>
<Settings size={18} />
</button>
);
// Home state
if (!hasSearched) {
return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 relative">
<div className="absolute top-4 right-4">{settingsButton}</div>
<button onClick={reset} className="mb-8 flex items-center gap-2 group">
<SearchIcon size={28} className="text-primary" />
<h1 className="text-3xl font-mono font-bold text-foreground tracking-tight">kafka</h1>
</button>
<SearchInput query={query} onQueryChange={setQuery} onSearch={search} autoFocus />
<p className="mt-6 text-sm text-muted-foreground">Private meta-search, powered by open source.</p>
</div>
);
}
// Results state
return (
<div className="min-h-screen flex flex-col">
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border">
<div className="max-w-3xl mx-auto px-4 py-3 flex items-center gap-4">
<button onClick={reset} className="flex items-center gap-1.5 shrink-0 group">
<SearchIcon size={18} className="text-primary" />
<span className="font-mono font-bold text-foreground text-lg tracking-tight">kafka</span>
</button>
<SearchInput query={query} onQueryChange={setQuery} onSearch={search} compact />
<div className="shrink-0">{settingsButton}</div>
</div>
<div className="max-w-3xl mx-auto px-4 pb-2">
<CategoryTabs active={activeCategory} onChange={setCategory} />
</div>
</header>
<main className="flex-1 max-w-3xl mx-auto w-full px-4 py-6">
{isLoading ? (
<ResultSkeleton />
) : filteredResults && filteredResults.length > 0 ? (
<>
<p className="text-xs text-muted-foreground mb-4">
About {results?.number_of_results} results for "<span className="font-medium text-foreground">{results?.query}</span>"
</p>
<div className="space-y-1">
{filteredResults.map((result, i) => (
<ResultCard key={`${result.url}-${i}`} result={result} />
))}
</div>
{results?.suggestions && results.suggestions.length > 0 && (
<div className="mt-8 pt-6 border-t border-border">
<p className="text-xs text-muted-foreground mb-2">Related searches</p>
<div className="flex flex-wrap gap-2">
{results.suggestions.map((s) => (
<button
key={s}
onClick={() => { setQuery(s); search(s); }}
className="text-sm px-3 py-1.5 rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 transition-colors"
>
{s}
</button>
))}
</div>
</div>
)}
</>
) : (
<p className="text-muted-foreground text-center py-12">No results found for this category.</p>
)}
</main>
</div>
);
};
export default Index;

View file

@ -1,24 +0,0 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
}, [location.pathname]);
return (
<div className="flex min-h-screen items-center justify-center bg-muted">
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold">404</h1>
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
<a href="/" className="text-primary underline hover:text-primary/90">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;

View file

@ -1,88 +0,0 @@
import { useNavigate } from "react-router-dom";
import { ArrowLeft, Sun, Moon } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { usePreferences } from "@/contexts/PreferencesContext";
const Preferences = () => {
const navigate = useNavigate();
const { theme, engines, setTheme, toggleEngine } = usePreferences();
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border">
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={() => navigate("/")} className="text-muted-foreground hover:text-foreground">
<ArrowLeft size={18} />
</Button>
<h1 className="font-mono font-bold text-lg text-foreground tracking-tight">Preferences</h1>
</div>
</header>
<main className="max-w-2xl mx-auto px-4 py-8 space-y-6">
{/* Appearance */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Appearance</CardTitle>
<CardDescription className="text-muted-foreground">Choose your preferred theme.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-3">
<button
onClick={() => setTheme("light")}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-colors ${
theme === "light"
? "border-primary bg-primary/10 text-foreground"
: "border-border text-muted-foreground hover:border-muted-foreground/40"
}`}
>
<Sun size={18} />
<span className="text-sm font-medium">Light</span>
</button>
<button
onClick={() => setTheme("dark")}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-colors ${
theme === "dark"
? "border-primary bg-primary/10 text-foreground"
: "border-border text-muted-foreground hover:border-muted-foreground/40"
}`}
>
<Moon size={18} />
<span className="text-sm font-medium">Dark</span>
</button>
</div>
</CardContent>
</Card>
{/* Search Engines */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Search Engines</CardTitle>
<CardDescription className="text-muted-foreground">Enable or disable engines used for queries.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{engines.map((engine) => (
<div key={engine.id} className="flex items-center justify-between">
<Label htmlFor={engine.id} className="text-foreground font-medium cursor-pointer">
{engine.name}
</Label>
<Switch
id={engine.id}
checked={engine.enabled}
onCheckedChange={() => toggleEngine(engine.id)}
/>
</div>
))}
</div>
</CardContent>
</Card>
</main>
</div>
);
};
export default Preferences;

View file

@ -1,7 +0,0 @@
import { describe, it, expect } from "vitest";
describe("example", () => {
it("should pass", () => {
expect(true).toBe(true);
});
});

View file

@ -1,15 +0,0 @@
import "@testing-library/jest-dom";
Object.defineProperty(window, "matchMedia", {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => {},
}),
});

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -1,60 +0,0 @@
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: { "2xl": "1400px" },
},
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['IBM Plex Mono', 'monospace'],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
link: { DEFAULT: "hsl(var(--link))", hover: "hsl(var(--link-hover))" },
"result-hover": "hsl(var(--result-hover))",
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" } },
"accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" } },
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

View file

@ -1,35 +0,0 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"moduleDetection": "force",
"moduleResolution": "bundler",
"noEmit": true,
"noFallthroughCasesInSwitch": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"paths": {
"@/*": [
"./src/*"
]
},
"skipLibCheck": true,
"strict": false,
"target": "ES2020",
"types": [
"vitest/globals"
],
"useDefineForClassFields": true
},
"include": [
"src"
]
}

View file

@ -1,24 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"paths": {
"@/*": [
"./src/*"
]
},
"skipLibCheck": true,
"strictNullChecks": false
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View file

@ -1,21 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { componentTagger } from "lovable-tagger";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
server: {
host: "::",
port: 8080,
hmr: {
overlay: false,
},
},
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
}));

View file

@ -1,16 +0,0 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
},
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
});

4
go.mod
View file

@ -13,7 +13,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/net v0.52.0 // indirect
)
replace golang.org/x/net => golang.org/x/net v0.38.0
replace golang.org/x/net v0.52.0 => golang.org/x/net v0.33.0

48
go.sum
View file

@ -4,6 +4,8 @@ github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2h
github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@ -28,36 +30,68 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

Some files were not shown because too many files have changed in this diff Show more