Update AGENTS.md. Start fixing old weird errors

This commit is contained in:
2026-03-26 12:05:12 -05:00
parent 0bc04dbf6b
commit d16f4287ce
96 changed files with 18195 additions and 9182 deletions

View File

@@ -17,8 +17,10 @@ NEXT_PUBLIC_SENTRY_PROJECT_NAME=example
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com # convex-backend:3210
CONVEX_SELF_HOSTED_ADMIN_KEY= # Generate after hosted on docker
# Convex Auth
CONVEX_SITE_URL=https://convex.example.com # convex-backend:3211
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev; update in Convex Dashboard for production
USESEND_API_KEY=
USESEND_URL=https://usesend.example.com
USESEND_FROM_EMAIL=My App <noreply@example.com>
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=

0
.npmrc
View File

1717
AGENTS.md

File diff suppressed because it is too large Load Diff

287
README.md
View File

@@ -32,15 +32,21 @@ A production-ready Turborepo starter with Next.js, Expo, and self-hosted Convex
## Getting Started
This is a self-hosted template. The full setup requires a server (home server or VPS)
to host the Convex backend and dashboard, and a reverse proxy (nginx-proxy-manager is
recommended) to expose them over HTTPS. The Next.js app can run locally in dev mode
once the Convex containers are reachable.
### Prerequisites
- [Bun](https://bun.sh) (v1.2.19+)
- [Bun](https://bun.sh) (v1.2+)
- [Docker](https://www.docker.com/) & Docker Compose (for self-hosted Convex)
- Node.js 22.20.0+ (for compatibility)
- Node.js 22+ (for compatibility)
- A running nginx-proxy-manager instance (or similar reverse proxy) to expose Convex over HTTPS
### Development Setup
---
#### 1. Clone & Install
### Step 1 — Clone & Install
```bash
git clone https://git.gbrown.org/gib/convex-monorepo
@@ -48,88 +54,152 @@ cd convex-monorepo
bun install
```
#### 2. Configure Environment Variables
Create a `.env` file in the project root with the following variables:
If you're using this as a template for a new project, remove the existing remote and
add your own:
```bash
# Convex Backend (Self-Hosted)
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=<generated>
CONVEX_SITE_URL=https://convex.example.com
# Next.js Public
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_URL=
NEXT_PUBLIC_SENTRY_ORG=
NEXT_PUBLIC_SENTRY_PROJECT_NAME=
# Server-side
SENTRY_AUTH_TOKEN=
# Auth (will be synced to Convex)
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=
USESEND_API_KEY=
git remote remove origin
git remote add origin https://your-git-host.com/your/new-repo.git
```
**For local development:** Use `http://localhost:3210` for Convex URLs.
---
#### 3. Configure Docker Environment
### Step 2 — Configure the Docker Environment
Check and update environment variables in `docker/.env` for your deployment:
The `docker/` directory contains everything needed to run the Convex backend and the
Next.js app in production.
```bash
cd docker/
cp .env.example .env
# Edit .env with your configuration
```
#### 4. Start Self-Hosted Convex
Edit `docker/.env` and fill in your values. The variables below the Next.js app section
control the Convex containers — you'll need to choose:
Spin up the Convex backend and dashboard:
- `INSTANCE_NAME` — a unique name for your Convex instance
- `INSTANCE_SECRET` — a secret string (generate something random)
- `CONVEX_CLOUD_ORIGIN` — the public HTTPS URL for your Convex backend API (e.g. `https://api.convex.example.com`)
- `CONVEX_SITE_ORIGIN` — the public HTTPS URL for Convex Auth HTTP routes (e.g. `https://api.convex.example.com`)
- `NEXT_PUBLIC_DEPLOYMENT_URL` — the URL for the Convex dashboard (e.g. `https://dashboard.convex.example.com`)
---
### Step 3 — Start the Convex Containers
```bash
cd docker/
docker compose up -d convex-backend convex-dashboard
sudo docker compose up -d convex-backend convex-dashboard
```
**Services:**
Wait a moment for `convex-backend` to pass its health check, then verify both
containers are running:
- **Backend:** http://localhost:3210
- **Dashboard:** http://localhost:6791
```bash
sudo docker compose ps
```
#### 5. Generate Auth Keys & Sync Environment Variables
Reverse-proxy the two Convex services through nginx-proxy-manager (or your preferred
proxy) to the URLs you chose in Step 2. Both must be reachable over HTTPS before you
can proceed.
Generate JWT keys for Convex Auth:
---
### Step 4 — Generate the Convex Admin Key
With the backend container running, generate the admin key:
```bash
cd docker/
./generate_convex_admin_key
```
Copy the printed key — you'll need it as `CONVEX_SELF_HOSTED_ADMIN_KEY` in the root
`.env` file.
---
### Step 5 — Configure Root Environment Variables
Create the root `.env` file:
```bash
# From the repo root
cp .env.example .env
```
Fill out all values in `/.env`:
```bash
# Next.js
NODE_ENV=development
SENTRY_AUTH_TOKEN= # From your self-hosted Sentry
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_URL=https://sentry.example.com
NEXT_PUBLIC_SENTRY_ORG=sentry
NEXT_PUBLIC_SENTRY_PROJECT_NAME=my-project
# Convex
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY= # From Step 4
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev
# Auth (synced to Convex in Step 6)
USESEND_API_KEY=
USESEND_URL=https://usesend.example.com
USESEND_FROM_EMAIL=My App <noreply@example.com>
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=https://auth.example.com/application/o/my-app/
```
---
### Step 6 — Generate JWT Keys & Sync Environment Variables to Convex
Generate the RS256 JWT keypair needed for Convex Auth:
```bash
cd packages/backend
bun run scripts/generateKeys.mjs
```
Sync environment variables to Convex deployment (via CLI or Dashboard):
This prints `JWT_PRIVATE_KEY` and `JWKS` values. Sync them to your Convex deployment
along with all other backend environment variables:
```bash
cd packages/backend
bun with-env npx convex env set AUTH_AUTHENTIK_ID "your-value"
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "your-value"
# From packages/backend/
bun with-env npx convex env set JWT_PRIVATE_KEY "your-private-key"
bun with-env npx convex env set JWKS "your-jwks"
bun with-env npx convex env set AUTH_AUTHENTIK_ID "your-client-id"
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "your-client-secret"
bun with-env npx convex env set AUTH_AUTHENTIK_ISSUER "your-issuer-url"
bun with-env npx convex env set USESEND_API_KEY "your-api-key"
bun with-env npx convex env set CONVEX_SITE_URL "http://localhost:3000"
bun with-env npx convex env set USESEND_URL "https://usesend.example.com"
bun with-env npx convex env set USESEND_FROM_EMAIL "My App <noreply@example.com>"
```
**Important:** For local development, set `CONVEX_SITE_URL` to `http://localhost:3000`.
**For production auth to work**, you must also update `CONVEX_SITE_URL` in the Convex
Dashboard to your production Next.js URL. Go to
`https://dashboard.convex.example.com` → Settings → Environment Variables and set:
#### 6. Start Development Server
```
CONVEX_SITE_URL = https://example.com
```
The root `.env` value of `http://localhost:3000` is correct for local dev and should
not be changed — only update it in the Dashboard for production.
---
### Step 7 — Start the Development Server
```bash
# From project root
bun dev:next # Next.js app + Convex backend
bun dev:next # Next.js app + Convex backend (most common)
# or
bun dev # All apps (Next.js + Expo + Backend)
```
@@ -137,7 +207,7 @@ bun dev # All apps (Next.js + Expo + Backend)
**App URLs:**
- **Next.js:** http://localhost:3000
- **Convex Dashboard:** http://localhost:6791
- **Convex Dashboard:** https://dashboard.convex.example.com
---
@@ -193,7 +263,7 @@ convex-monorepo/
├── packages/
│ ├── backend/ # Convex backend
│ │ ├── convex/ # Convex functions (synced to cloud)
│ │ ├── convex/ # Convex functions (synced to deployment)
│ │ ├── scripts/ # Utilities (generateKeys.mjs)
│ │ └── types/ # Shared types
│ └── ui/ # shadcn/ui components
@@ -223,16 +293,16 @@ convex-monorepo/
- **OAuth:** Authentik SSO integration
- **Password:** Custom password auth with email verification
- **OTP:** Email verification via self-hosted UseSend
- **Session Management:** Secure cookie-based sessions
- **Session Management:** Secure cookie-based sessions (30-day max age)
### Next.js App
- **App Router:** Next.js 16 with React Server Components
- **Data Preloading:** Server-side data fetching with Convex
- **Middleware:** Route protection & authentication
- **Styling:** Tailwind CSS v4 with dark mode
- **Analytics:** Plausible (privacy-focused)
- **Monitoring:** Sentry error tracking
- **Data Preloading:** SSR data fetching with `preloadQuery` + `usePreloadedQuery`
- **Middleware:** Route protection & IP-based security (`src/proxy.ts`)
- **Styling:** Tailwind CSS v4 with dark mode (OKLCH-based theme)
- **Analytics:** Plausible (privacy-focused, proxied through Next.js)
- **Monitoring:** Sentry error tracking & performance
### Backend
@@ -244,87 +314,113 @@ convex-monorepo/
### Developer Experience
- **Monorepo:** Turborepo for efficient builds
- **Monorepo:** Turborepo for efficient builds and caching
- **Type Safety:** Strict TypeScript throughout
- **Code Quality:** ESLint + Prettier with auto-fix
- **Hot Reload:** Fast refresh for all packages
- **Catalog Deps:** Centralized version management
- **Catalog Deps:** Centralized dependency version management
---
## Deployment
### Docker (Recommended)
### Production Deployment (Docker)
Build and deploy with Docker Compose:
Once the Convex containers are running (they only need to be started once), deploying
a new version of the Next.js app is a two-command workflow:
```bash
# SSH onto your server, then:
cd docker/
# Build the new image
sudo docker compose build next-app
# Deploy it
sudo docker compose up -d next-app
```
To start all services from scratch:
```bash
cd docker/
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
sudo docker compose up -d convex-backend convex-dashboard
# Wait for backend health check to pass, then:
sudo docker compose up -d next-app
```
**Services:**
- `next-app` - Next.js standalone build
- `convex-backend` - Convex backend (port 3210)
- `convex-dashboard` - Admin dashboard (port 6791)
- `next-app` Next.js standalone build
- `convex-backend` Convex backend (port 3210)
- `convex-dashboard` Admin dashboard (port 6791)
**Network:** Uses `nginx-bridge` network (configurable in `compose.yml`).
**Network:** Uses `nginx-bridge` Docker network (reverse proxy via nginx-proxy-manager).
### Production Checklist
- [ ] Update environment variables in `docker/.env`
- [ ] Generate `CONVEX_SELF_HOSTED_ADMIN_KEY`
- [ ] Configure reverse proxy (Nginx/Traefik)
- [ ] Set up SSL certificates
- [ ] Sync auth environment variables to Convex
- [ ] Configure backup strategy for `docker/data/`
- [ ] Test authentication flow
- [ ] Enable Sentry error tracking
- [ ] Fill out `docker/.env` with your domain names and secrets
- [ ] Start `convex-backend` and `convex-dashboard` containers
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./generate_convex_admin_key`
- [ ] Reverse-proxy both Convex services via nginx-proxy-manager with SSL
- [ ] Fill out root `/.env` with all environment variables
- [ ] Generate JWT keys and sync all env vars to Convex (`bun with-env npx convex env set ...`)
- [ ] Update `CONVEX_SITE_URL` in the Convex Dashboard to your production Next.js URL
- [ ] Build and start the `next-app` container
- [ ] Back up `docker/data/` regularly (contains all Convex database data)
---
## Documentation
- **[AGENTS.md](./AGENTS.md)** - Comprehensive guide for AI agents & developers
- **[Convex Docs](https://docs.convex.dev)** - Official Convex documentation
- **[Turborepo Docs](https://turbo.build/repo/docs)** - Turborepo documentation
- **[Next.js Docs](https://nextjs.org/docs)** - Next.js documentation
- **[AGENTS.md](./AGENTS.md)** Comprehensive guide for AI agents & developers
- **[Convex Docs](https://docs.convex.dev)** Official Convex documentation
- **[Turborepo Docs](https://turbo.build/repo/docs)** Turborepo documentation
- **[Next.js Docs](https://nextjs.org/docs)** Next.js documentation
---
## Troubleshooting
### Backend typecheck shows help message
### Backend typecheck shows TypeScript help message
This is expected behavior. The backend package follows Convex's structure with only `convex/tsconfig.json` (no root tsconfig). See [AGENTS.md](./AGENTS.md) for details.
This is expected behavior. The backend package follows Convex's structure with only
`convex/tsconfig.json` (no root tsconfig). Running `bun typecheck` from the repo root
will show TypeScript's help text for `@gib/backend` — this is not an error.
### Imports from Convex require .js extension
### Imports from Convex require `.js` extension
The project uses ESM (`"type": "module"`), requiring explicit file extensions:
The project uses ESM (`"type": "module"`), which requires explicit file extensions:
```typescript
// ✅ Correct
// ❌ Wrong
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
// ❌ Wrong — will fail at runtime
import { api } from '@gib/backend/convex/_generated/api';
import { api } from '@gib/backend/convex/_generated/api.js';
```
### Docker containers won't start
1. Check Docker logs: `docker compose logs`
1. Check Docker logs: `sudo docker compose logs`
2. Verify environment variables in `docker/.env`
3. Ensure ports 3210 and 6791 are available
4. Check network configuration (`nginx-bridge`)
3. Ensure the `nginx-bridge` network exists: `sudo docker network create nginx-bridge`
4. Check that the required ports (3210, 6791) are not already in use
### Auth doesn't work in production
Make sure `CONVEX_SITE_URL` is set to your production Next.js URL in the **Convex
Dashboard** (not just in the root `.env` file). The root `.env` should always contain
`http://localhost:3000`; the Dashboard must have your production URL.
### Catalog updates break workspace
After updating dependencies, if you see `sherif` errors on `bun install`:
1. Never use `bun update` inside individual package directories
2. Edit the version in root `package.json` catalog section instead
3. Run `bun install` from the root
4. Verify with `bun lint:ws`
---
@@ -337,6 +433,7 @@ This is a personal monorepo template. Feel free to fork and adapt for your needs
- Single quotes, trailing commas
- 80 character line width
- ESLint + Prettier enforced
- `const fn = () => {}` over `function fn()` (strong preference)
- Import order: Types → React → Next → Third-party → @gib → Local
Run `bun lint:fix` and `bun format:fix` before committing.

View File

@@ -1 +1 @@
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21"],{"key":"22","value":"23"},{"key":"24","value":"25"},{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},{"key":"58","value":"59"},{"key":"60","value":"61"},{"key":"62","value":"63"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2249,"mtime":1766222924000,"hash":"64","data":"65"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639000,"hash":"66","data":"67"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639000,"hash":"68","data":"69"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1768372346938,"hash":"70","data":"71"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":163,"mtime":1766222924000,"hash":"72","data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":66,"mtime":1768155639000,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":567,"mtime":1766222924000,"hash":"76","data":"77"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"78","data":"79"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1768155639000,"hash":"80","data":"81"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"82","data":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1768155639000,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1768372346967,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":90,"mtime":1768155639000,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639000,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"92"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":275,"mtime":1768155639000,"hash":"93","data":"94"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/api.tsx",{"size":1326,"mtime":1768155639000,"hash":"95","data":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639000,"hash":"97","data":"98"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639000,"hash":"99","data":"100"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"101"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"102","data":"103"},"d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"104"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"105"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"106"},"73c235a66242df70b69394cce29d1ed3",{"hashOfOptions":"107"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"108"},"b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"109"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"110"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"111"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"112"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"113"},"cecbed1604a530a7cc099fecddddd76c",{"hashOfOptions":"114"},"ead19d73283f9d8e08b55c896c9fd570",{"hashOfOptions":"115"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"116"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"117"},"1e8ac0d261e95efb19d290ffcf70ce36","1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"118"},"5ff899a601102659dcbd2900e415ce8b",{"hashOfOptions":"119"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"120"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"121"},"863da15dbd856008b7c24077ca746d91","6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"122"},"1820601142","1684748001","3531839294","2748941218","956511134","132171752","3925902565","1550174236","2506462393","526883382","4111358426","2953691686","2383171816","2740949298","3787272667","3740930138","4230803759","3315245788","3164486579"]
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22"],{"key":"23","value":"24"},{"key":"25","value":"26"},{"key":"27","value":"28"},{"key":"29","value":"30"},{"key":"31","value":"32"},{"key":"33","value":"34"},{"key":"35","value":"36"},{"key":"37","value":"38"},{"key":"39","value":"40"},{"key":"41","value":"42"},{"key":"43","value":"44"},{"key":"45","value":"46"},{"key":"47","value":"48"},{"key":"49","value":"50"},{"key":"51","value":"52"},{"key":"53","value":"54"},{"key":"55","value":"56"},{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1768372346938,"hash":"67","data":"68"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639000,"hash":"69","data":"70"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"71"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":66,"mtime":1768155639000,"hash":"72","data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1768155639000,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1768372346967,"hash":"76","data":"77"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/api.tsx",{"size":1326,"mtime":1768155639000,"hash":"78","data":"79"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":567,"mtime":1766222924000,"hash":"80","data":"81"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1768155639000,"hash":"82","data":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":90,"mtime":1768155639000,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.prettiercache",{"size":4929,"mtime":1774544485533},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"86"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2255,"mtime":1774544484676,"hash":"87","data":"88"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":171,"mtime":1774031879321,"hash":"89","data":"90"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":275,"mtime":1768155639000,"hash":"91","data":"92"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"93","data":"94"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639000,"hash":"95","data":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"97","data":"98"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639000,"hash":"99","data":"100"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"101","data":"102"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639000,"hash":"103","data":"104"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639000,"hash":"105","data":"106"},"73c235a66242df70b69394cce29d1ed3",{"hashOfOptions":"107"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"108"},"1e8ac0d261e95efb19d290ffcf70ce36","b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"109"},"cecbed1604a530a7cc099fecddddd76c",{"hashOfOptions":"110"},"ead19d73283f9d8e08b55c896c9fd570",{"hashOfOptions":"111"},"5ff899a601102659dcbd2900e415ce8b",{"hashOfOptions":"112"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"113"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"114"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"115"},"863da15dbd856008b7c24077ca746d91","d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"116"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"117"},"1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"118"},"6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"119"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"120"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"121"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"122"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"123"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"124"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"125"},"3000879843","3103968608","384110377","68329755","141502567","3992868763","1050155876","2025343866","4147067111","4228440757","3451484829","4039211292","3318113268","2585374463","45764855","1418614640","2754339483","1950506033","3468872477"]

View File

@@ -65,4 +65,3 @@
},
"prettier": "@gib/prettier-config"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { createJiti } from 'jiti';
import { withSentryConfig } from '@sentry/nextjs';
import { createJiti } from 'jiti';
import { withPlausibleProxy } from 'next-plausible';
const jiti = createJiti(import.meta.url);

View File

@@ -252,27 +252,27 @@ const SignIn = () => {
<Tabs
defaultValue={flow}
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
className='items-center flex-col'
className='flex-col items-center'
>
<TabsList>
<TabsTrigger
value='signIn'
className='cursor-pointer py-2 px-6 text-2xl font-bold'
className='cursor-pointer px-6 py-2 text-2xl font-bold'
>
Sign In
</TabsTrigger>
<TabsTrigger
value='signUp'
className='cursor-pointer py-2 px-6 text-2xl font-bold'
className='cursor-pointer px-6 py-2 text-2xl font-bold'
>
Sign Up
</TabsTrigger>
</TabsList>
<TabsContent
value='signIn'
className='min-h-[560px] items-center flex flex-row'
className='flex min-h-[560px] flex-row items-center'
>
<Card className='bg-card/50 min-w-xs sm:min-w-sm py-10'>
<Card className='bg-card/50 min-w-xs py-10 sm:min-w-sm'>
<CardContent>
<Form {...signInForm}>
<form

View File

@@ -10,12 +10,12 @@ import { useEffect } from 'react';
import Footer from '@/components/layout/footer';
import Header from '@/components/layout/header';
import { ConvexClientProvider } from '@/components/providers';
import { env } from '@/env';
import { generateMetadata } from '@/lib/metadata';
import * as Sentry from '@sentry/nextjs';
import PlausibleProvider from 'next-plausible';
import { Button, ThemeProvider, Toaster } from '@gib/ui';
import { env } from '@/env';
export const metadata: Metadata = generateMetadata();

View File

@@ -1,33 +1,31 @@
export function CTA() {
return (
<section className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-4xl'>
<div className='border-border/40 from-muted/50 to-muted/30 rounded-2xl border bg-linear-to-br p-8 text-center md:p-12'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Ready to Build Something Amazing?
</h2>
<p className='text-muted-foreground mb-8 text-lg'>
Clone the repository and start building your next project with
everything pre-configured.
</p>
export const CTA = () => (
<section className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-4xl'>
<div className='border-border/40 from-muted/50 to-muted/30 rounded-2xl border bg-linear-to-br p-8 text-center md:p-12'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Ready to Build Something Amazing?
</h2>
<p className='text-muted-foreground mb-8 text-lg'>
Clone the repository and start building your next project with
everything pre-configured.
</p>
{/* Quick Start Command */}
<div className='mt-12'>
<p className='text-muted-foreground mb-3 text-sm font-medium'>
Quick Start
</p>
<div className='border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4'>
<code className='text-sm'>
git clone https://git.gbrown.org/gib/convex-monorepo.git
<br />
cd convex-monorepo
<br />
bun i
</code>
</div>
{/* Quick Start Command */}
<div className='mt-12'>
<p className='text-muted-foreground mb-3 text-sm font-medium'>
Quick Start
</p>
<div className='border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4'>
<code className='text-sm'>
git clone https://git.gbrown.org/gib/convex-monorepo.git
<br />
cd convex-monorepo
<br />
bun i
</code>
</div>
</div>
</div>
</section>
);
}
</div>
</section>
);

View File

@@ -57,36 +57,34 @@ const features = [
},
];
export function Features() {
return (
<section id='features' className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-6xl'>
{/* Section Header */}
<div className='mb-16 text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
Everything You Need to Ship Fast
</h2>
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
A complete monorepo template with all the tools and patterns you
need for production-ready applications.
</p>
</div>
{/* Features Grid */}
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
{features.map((feature) => (
<Card key={feature.title} className='border-border/40'>
<CardHeader className='flex items-center gap-2'>
<div className='mb-2 text-3xl'>{feature.icon}</div>
<CardTitle className='text-xl'>{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<p className='text-muted-foreground'>{feature.description}</p>
</CardContent>
</Card>
))}
</div>
export const Features = () => (
<section id='features' className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-6xl'>
{/* Section Header */}
<div className='mb-16 text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
Everything You Need to Ship Fast
</h2>
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
A complete monorepo template with all the tools and patterns you need
for production-ready applications.
</p>
</div>
</section>
);
}
{/* Features Grid */}
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
{features.map((feature) => (
<Card key={feature.title} className='border-border/40'>
<CardHeader className='flex items-center gap-2'>
<div className='mb-2 text-3xl'>{feature.icon}</div>
<CardTitle className='text-xl'>{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<p className='text-muted-foreground'>{feature.description}</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
);

View File

@@ -9,120 +9,118 @@ const kanitSans = Kanit({
weight: ['400', '500', '600', '700'],
});
export function Hero() {
return (
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
{/* Badge */}
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
<span className='mr-2'>🚀</span>
<span>Production-ready monorepo template</span>
</div>
export const Hero = () => (
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
{/* Badge */}
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
<span className='mr-2'>🚀</span>
<span>Production-ready monorepo template</span>
</div>
{/* Heading */}
<h1 className='from-foreground to-foreground/70 bg-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl'>
Build Full-Stack Apps with{' '}
<span
className={`${kanitSans.className} to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent sm:text-6xl lg:text-7xl xl:text-8xl dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]`}
{/* Heading */}
<h1 className='from-foreground to-foreground/70 bg-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl'>
Build Full-Stack Apps with{' '}
<span
className={`${kanitSans.className} to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent sm:text-6xl lg:text-7xl xl:text-8xl dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]`}
>
convex monorepo
</span>
</h1>
{/* Description */}
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web
and mobile apps faster with shared code, type-safe backend, and complete
control over your infrastructure.
</p>
{/* CTA Buttons */}
<div className='flex flex-col gap-3 sm:flex-row'>
<Button size='lg' variant='outline' asChild>
<Link
href='https://git.gbrown.org/gib/convex-monorepo'
target='_blank'
rel='noopener noreferrer'
>
convex monorepo
</span>
</h1>
<Image
src='/misc/gitea/gitea.svg'
alt='Gitea'
width={20}
height={20}
/>
View Source Code
</Link>
</Button>
</div>
{/* Description */}
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship
web and mobile apps faster with shared code, type-safe backend, and
complete control over your infrastructure.
</p>
{/* CTA Buttons */}
<div className='flex flex-col gap-3 sm:flex-row'>
<Button size='lg' variant='outline' asChild>
<Link
href='https://git.gbrown.org/gib/convex-monorepo'
target='_blank'
rel='noopener noreferrer'
>
<Image
src='/misc/gitea/gitea.svg'
alt='Gitea'
width={20}
height={20}
/>
View Source Code
</Link>
</Button>
{/* Features Quick List */}
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>TypeScript</span>
</div>
{/* Features Quick List */}
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>TypeScript</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Self-Hosted</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Real-time</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Auth Included</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Self-Hosted</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Real-time</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Auth Included</span>
</div>
</div>
</section>
);
}
</div>
</section>
);

View File

@@ -36,44 +36,42 @@ const techStack = [
},
];
export function TechStack() {
return (
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
<div className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-6xl'>
{/* Section Header */}
<div className='mb-16 text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
Modern Tech Stack
</h2>
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
Built with the latest and greatest tools for maximum productivity
and performance.
</p>
</div>
export const TechStack = () => (
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
<div className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-6xl'>
{/* Section Header */}
<div className='mb-16 text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
Modern Tech Stack
</h2>
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
Built with the latest and greatest tools for maximum productivity
and performance.
</p>
</div>
{/* Tech Stack Grid */}
<div className='grid gap-12 md:grid-cols-3'>
{techStack.map((stack) => (
<div key={stack.category}>
<h3 className='mb-6 text-xl font-semibold'>{stack.category}</h3>
<ul className='space-y-4'>
{stack.technologies.map((tech) => (
<li key={tech.name}>
<div className='text-foreground font-medium'>
{tech.name}
</div>
<div className='text-muted-foreground text-sm'>
{tech.description}
</div>
</li>
))}
</ul>
</div>
))}
</div>
{/* Tech Stack Grid */}
<div className='grid gap-12 md:grid-cols-3'>
{techStack.map((stack) => (
<div key={stack.category}>
<h3 className='mb-6 text-xl font-semibold'>{stack.category}</h3>
<ul className='space-y-4'>
{stack.technologies.map((tech) => (
<li key={tech.name}>
<div className='text-foreground font-medium'>
{tech.name}
</div>
<div className='text-muted-foreground text-sm'>
{tech.description}
</div>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
);
}
</div>
</section>
);

View File

@@ -1,4 +1,5 @@
'use client';
import { CardDescription, CardHeader, CardTitle } from '@gib/ui';
const ProfileHeader = () => {

View File

@@ -53,7 +53,7 @@ export const UserInfoForm = ({
const userProvider = usePreloadedQuery(preloadedProvider);
const providerMap: Record<string, string> = {
unknown: 'Provider',
authentik: 'Gib\'s Auth',
authentik: "Gib's Auth",
};
const [loading, setLoading] = useState(false);
@@ -150,7 +150,8 @@ export const UserInfoForm = ({
</FormDescription>
) : (
<FormDescription>
Email is managed through your {providerMap[userProvider ?? 'unknown']} account
Email is managed through your{' '}
{providerMap[userProvider ?? 'unknown']} account
</FormDescription>
)}
<FormMessage />

View File

@@ -28,7 +28,7 @@ export default function Header(headerProps: ComponentProps<'header'>) {
alt='Convex Monorepo'
width={50}
height={50}
className='invert dark:invert-0 w-15'
className='w-15 invert dark:invert-0'
/>
<span
className={`mb-3 hidden font-extrabold lg:inline lg:text-5xl ${kanitSans.className}`}

View File

@@ -7,10 +7,8 @@ import { ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexAuthNextjsProvider client={convex}>
{children}
</ConvexAuthNextjsProvider>
);
}
export const ConvexClientProvider = ({ children }: { children: ReactNode }) => (
<ConvexAuthNextjsProvider client={convex}>
{children}
</ConvexAuthNextjsProvider>
);

View File

@@ -38,7 +38,8 @@ export const env = createEnv({
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL,
NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG,
NEXT_PUBLIC_SENTRY_PROJECT_NAME: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
NEXT_PUBLIC_SENTRY_PROJECT_NAME:
process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
emptyStringAsUndefined: true,

View File

@@ -1,6 +1,6 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
import { env } from '@/env';
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,

View File

@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/nextjs';
import { env } from '@/env';
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,

View File

@@ -181,6 +181,7 @@
"@next/eslint-plugin-next": "^16.0.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prefer-arrow-functions": "^3.9.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-turbo": "^2.5.8",
@@ -1951,6 +1952,8 @@
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
"eslint-plugin-prefer-arrow-functions": ["eslint-plugin-prefer-arrow-functions@3.9.1", "", { "dependencies": { "@typescript-eslint/types": "^8.19.1", "@typescript-eslint/utils": "^8.19.1" }, "peerDependencies": { "eslint": ">=9.17.0" } }, "sha512-Mr9Ia8i5ohfCMcZBRedXXWdDJIo30gdKdPfRQ5DtO62yzogB5ErVGwQWPz+ycRlQPKpCAlBHpJdp1TCeCtt+YA=="],
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],

View File

@@ -18,7 +18,7 @@ ENV NODE_ENV=production
RUN bun run build --filter=@gib/next
# Runner stage
FROM node:20-alpine AS runner
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
id: 'usesend',
type: 'email',
name: 'UseSend',
from: 'Convex Monorepo <admin@convexmonorepo.gbrown.org>',
from: process.env.USESEND_FROM_EMAIL ?? 'noreply@example.com',
maxAge: 24 * 60 * 60, // 24 hours
async generateVerificationToken() {
@@ -21,13 +21,14 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
},
async sendVerificationRequest(params) {
const { identifier: to, provider, url, theme, token } = params;
//const { host } = new URL(url);
const host = 'Convex Monorepo';
const { identifier: to, provider, url, token } = params;
// Derive a display name from the site URL, fallback to 'App'
const siteUrl = process.env.USESEND_FROM_EMAIL ?? '';
const appName = siteUrl.split('@')[1]?.split('.')[0] ?? 'App';
const useSend = new UseSend(
process.env.USESEND_API_KEY!,
'https://usesend.gbrown.org',
process.env.USESEND_URL!,
);
// For password reset, we want to send the code, not the magic link
@@ -38,8 +39,8 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
from: provider.from!,
to: [to],
subject: isPasswordReset
? `Reset your password - ${host}`
: `Sign in to ${host}`,
? `Reset your password - ${appName}`
: `Sign in to ${appName}`,
text: isPasswordReset
? `Your password reset code is ${token}`
: `Your sign in code is ${token}`,

View File

@@ -18,11 +18,9 @@ const applicationTables = {
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
/* Fields below here are custom & not defined in authTables */
themePreference: v.optional(v.union(
v.literal('light'),
v.literal('dark'),
v.literal('system'),
)),
themePreference: v.optional(
v.union(v.literal('light'), v.literal('dark'), v.literal('system')),
),
})
.index('email', ['email'])
.index('phone', ['phone'])

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,82 +6,74 @@ import { Accordion as AccordionPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Accordion({
const Accordion = ({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return (
<AccordionPrimitive.Root
data-slot='accordion'
className={cn('flex w-full flex-col', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof AccordionPrimitive.Root>) => (
<AccordionPrimitive.Root
data-slot='accordion'
className={cn('flex w-full flex-col', className)}
{...props}
/>
);
function AccordionItem({
const AccordionItem = ({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot='accordion-item'
className={cn('not-last:border-b', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof AccordionPrimitive.Item>) => (
<AccordionPrimitive.Item
data-slot='accordion-item'
className={cn('not-last:border-b', className)}
{...props}
/>
);
function AccordionTrigger({
const AccordionTrigger = ({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className='flex'>
<AccordionPrimitive.Trigger
data-slot='accordion-trigger'
className={cn(
'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4',
className,
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot='accordion-trigger-icon'
className='pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden'
/>
<ChevronUpIcon
data-slot='accordion-trigger-icon'
className='pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline'
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot='accordion-content'
className='data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm'
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) => (
<AccordionPrimitive.Header className='flex'>
<AccordionPrimitive.Trigger
data-slot='accordion-trigger'
className={cn(
'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4',
className,
)}
{...props}
>
<div
className={cn(
'[&_a]:hover:text-foreground h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className,
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
}
{children}
<ChevronDownIcon
data-slot='accordion-trigger-icon'
className='pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden'
/>
<ChevronUpIcon
data-slot='accordion-trigger-icon'
className='pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline'
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
const AccordionContent = ({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) => (
<AccordionPrimitive.Content
data-slot='accordion-content'
className='data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm'
{...props}
>
<div
className={cn(
'[&_a]:hover:text-foreground h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className,
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -5,182 +5,160 @@ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';
import { Button, cn } from '@gib/ui';
function AlertDialog({
const AlertDialog = ({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />;
}
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) => (
<AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />
);
function AlertDialogTrigger({
const AlertDialogTrigger = ({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
);
}
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) => (
<AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
);
function AlertDialogPortal({
const AlertDialogPortal = ({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
);
}
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) => (
<AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
);
function AlertDialogOverlay({
const AlertDialogOverlay = ({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot='alert-dialog-overlay'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) => (
<AlertDialogPrimitive.Overlay
data-slot='alert-dialog-overlay'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
function AlertDialogContent({
const AlertDialogContent = ({
className,
size = 'default',
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
size?: 'default' | 'sm';
}) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot='alert-dialog-content'
data-size={size}
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm',
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-dialog-header'
}) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot='alert-dialog-content'
data-size={size}
className={cn(
'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm',
className,
)}
{...props}
/>
);
}
</AlertDialogPortal>
);
function AlertDialogFooter({
const AlertDialogHeader = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-dialog-footer'
className={cn(
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='alert-dialog-header'
className={cn(
'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
className,
)}
{...props}
/>
);
function AlertDialogMedia({
const AlertDialogFooter = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-dialog-media'
className={cn(
"bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='alert-dialog-footer'
className={cn(
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
className,
)}
{...props}
/>
);
function AlertDialogTitle({
const AlertDialogMedia = ({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot='alert-dialog-title'
className={cn(
'text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='alert-dialog-media'
className={cn(
"bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
className,
)}
{...props}
/>
);
function AlertDialogDescription({
const AlertDialogTitle = ({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot='alert-dialog-description'
className={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) => (
<AlertDialogPrimitive.Title
data-slot='alert-dialog-title'
className={cn(
'text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
className,
)}
{...props}
/>
);
function AlertDialogAction({
const AlertDialogDescription = ({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) => (
<AlertDialogPrimitive.Description
data-slot='alert-dialog-description'
className={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
className,
)}
{...props}
/>
);
const AlertDialogAction = ({
className,
variant = 'default',
size = 'default',
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Action
data-slot='alert-dialog-action'
className={cn(className)}
{...props}
/>
</Button>
);
}
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) => (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Action
data-slot='alert-dialog-action'
className={cn(className)}
{...props}
/>
</Button>
);
function AlertDialogCancel({
const AlertDialogCancel = ({
className,
variant = 'outline',
size = 'default',
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Cancel
data-slot='alert-dialog-cancel'
className={cn(className)}
{...props}
/>
</Button>
);
}
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) => (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Cancel
data-slot='alert-dialog-cancel'
className={cn(className)}
{...props}
/>
</Button>
);
export {
AlertDialog,

View File

@@ -20,58 +20,50 @@ const alertVariants = cva(
},
);
function Alert({
const Alert = ({
className,
variant,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot='alert'
role='alert'
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) => (
<div
data-slot='alert'
role='alert'
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-title'
className={cn(
'[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3',
className,
)}
{...props}
/>
);
}
const AlertTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='alert-title'
className={cn(
'[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3',
className,
)}
{...props}
/>
);
function AlertDescription({
const AlertDescription = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-description'
className={cn(
'text-muted-foreground [&_a]:hover:text-foreground text-sm text-balance md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='alert-description'
className={cn(
'text-muted-foreground [&_a]:hover:text-foreground text-sm text-balance md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className,
)}
{...props}
/>
);
function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-action'
className={cn('absolute top-2 right-2', className)}
{...props}
/>
);
}
const AlertAction = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='alert-action'
className={cn('absolute top-2 right-2', className)}
{...props}
/>
);
export { Alert, AlertTitle, AlertDescription, AlertAction };

View File

@@ -2,9 +2,9 @@
import { AspectRatio as AspectRatioPrimitive } from 'radix-ui';
function AspectRatio({
const AspectRatio = ({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot='aspect-ratio' {...props} />;
}
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) => (
<AspectRatioPrimitive.Root data-slot='aspect-ratio' {...props} />
);
export { AspectRatio };

View File

@@ -5,99 +5,87 @@ import { Avatar as AvatarPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Avatar({
const Avatar = ({
className,
size = 'default',
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: 'default' | 'sm' | 'lg';
}) {
return (
<AvatarPrimitive.Root
data-slot='avatar'
data-size={size}
className={cn(
'group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6',
className,
)}
{...props}
/>
);
}
}) => (
<AvatarPrimitive.Root
data-slot='avatar'
data-size={size}
className={cn(
'group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6',
className,
)}
{...props}
/>
);
function AvatarImage({
const AvatarImage = ({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot='avatar-image'
className={cn('aspect-square size-full', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof AvatarPrimitive.Image>) => (
<AvatarPrimitive.Image
data-slot='avatar-image'
className={cn('aspect-square size-full', className)}
{...props}
/>
);
function AvatarFallback({
const AvatarFallback = ({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot='avatar-fallback'
className={cn(
'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) => (
<AvatarPrimitive.Fallback
data-slot='avatar-fallback'
className={cn(
'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs',
className,
)}
{...props}
/>
);
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot='avatar-badge'
className={cn(
'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full ring-2 select-none',
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className,
)}
{...props}
/>
);
}
const AvatarBadge = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
data-slot='avatar-badge'
className={cn(
'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full ring-2 select-none',
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className,
)}
{...props}
/>
);
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='avatar-group'
className={cn(
'group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2',
className,
)}
{...props}
/>
);
}
const AvatarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='avatar-group'
className={cn(
'group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2',
className,
)}
{...props}
/>
);
function AvatarGroupCount({
const AvatarGroupCount = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='avatar-group-count'
className={cn(
'bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='avatar-group-count'
className={cn(
'bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className,
)}
{...props}
/>
);
export {
Avatar,

View File

@@ -27,13 +27,13 @@ const badgeVariants = cva(
},
);
function Badge({
const Badge = ({
className,
variant = 'default',
asChild = false,
...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
VariantProps<typeof badgeVariants> & { asChild?: boolean }) => {
const Comp = asChild ? Slot.Root : 'span';
return (
@@ -44,6 +44,6 @@ function Badge({
{...props}
/>
);
}
};
export { Badge, badgeVariants };

View File

@@ -4,40 +4,42 @@ import { Slot } from 'radix-ui';
import { cn } from '@gib/ui';
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
return <nav aria-label='breadcrumb' data-slot='breadcrumb' {...props} />;
}
const Breadcrumb = ({ ...props }: React.ComponentProps<'nav'>) => (
<nav aria-label='breadcrumb' data-slot='breadcrumb' {...props} />
);
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
return (
<ol
data-slot='breadcrumb-list'
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className,
)}
{...props}
/>
);
}
const BreadcrumbList = ({
className,
...props
}: React.ComponentProps<'ol'>) => (
<ol
data-slot='breadcrumb-list'
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className,
)}
{...props}
/>
);
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot='breadcrumb-item'
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
);
}
const BreadcrumbItem = ({
className,
...props
}: React.ComponentProps<'li'>) => (
<li
data-slot='breadcrumb-item'
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
);
function BreadcrumbLink({
const BreadcrumbLink = ({
asChild,
className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean;
}) {
}) => {
const Comp = asChild ? Slot.Root : 'a';
return (
@@ -47,56 +49,53 @@ function BreadcrumbLink({
{...props}
/>
);
}
};
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot='breadcrumb-page'
role='link'
aria-disabled='true'
aria-current='page'
className={cn('text-foreground font-normal', className)}
{...props}
/>
);
}
const BreadcrumbPage = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
data-slot='breadcrumb-page'
role='link'
aria-disabled='true'
aria-current='page'
className={cn('text-foreground font-normal', className)}
{...props}
/>
);
function BreadcrumbSeparator({
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<'li'>) {
return (
<li
data-slot='breadcrumb-separator'
role='presentation'
aria-hidden='true'
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
}: React.ComponentProps<'li'>) => (
<li
data-slot='breadcrumb-separator'
role='presentation'
aria-hidden='true'
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
function BreadcrumbEllipsis({
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='breadcrumb-ellipsis'
role='presentation'
aria-hidden='true'
className={cn('flex size-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className='size-4' />
<span className='sr-only'>More</span>
</span>
);
}
}: React.ComponentProps<'span'>) => (
<span
data-slot='breadcrumb-ellipsis'
role='presentation'
aria-hidden='true'
className={cn('flex size-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className='size-4' />
<span className='sr-only'>More</span>
</span>
);
export {
Breadcrumb,

View File

@@ -21,29 +21,27 @@ const buttonGroupVariants = cva(
},
);
function ButtonGroup({
const ButtonGroup = ({
className,
orientation,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role='group'
data-slot='button-group'
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
);
}
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) => (
<div
role='group'
data-slot='button-group'
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
);
function ButtonGroupText({
const ButtonGroupText = ({
className,
asChild = false,
...props
}: React.ComponentProps<'div'> & {
asChild?: boolean;
}) {
}) => {
const Comp = asChild ? Slot.Root : 'div';
return (
@@ -55,25 +53,23 @@ function ButtonGroupText({
{...props}
/>
);
}
};
function ButtonGroupSeparator({
const ButtonGroupSeparator = ({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot='button-group-separator'
orientation={orientation}
className={cn(
'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof Separator>) => (
<Separator
data-slot='button-group-separator'
orientation={orientation}
className={cn(
'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
className,
)}
{...props}
/>
);
export {
ButtonGroup,

View File

@@ -42,7 +42,7 @@ const buttonVariants = cva(
},
);
function Button({
const Button = ({
className,
variant = 'default',
size = 'default',
@@ -51,7 +51,7 @@ function Button({
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
}) => {
const Comp = asChild ? Slot.Root : 'button';
return (
@@ -63,6 +63,6 @@ function Button({
{...props}
/>
);
}
};
export { Button, buttonVariants };

View File

@@ -11,7 +11,7 @@ import { DayPicker, getDefaultClassNames } from 'react-day-picker';
import { Button, buttonVariants, cn } from '@gib/ui';
function Calendar({
const Calendar = ({
className,
classNames,
showOutsideDays = true,
@@ -23,7 +23,7 @@ function Calendar({
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>['variant'];
}) {
}) => {
const defaultClassNames = getDefaultClassNames();
return (
@@ -183,15 +183,15 @@ function Calendar({
{...props}
/>
);
}
};
function CalendarDayButton({
const CalendarDayButton = ({
className,
day,
modifiers,
locale,
...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) => {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
@@ -222,6 +222,6 @@ function CalendarDayButton({
{...props}
/>
);
}
};
export { Calendar, CalendarDayButton };

View File

@@ -2,95 +2,84 @@ import type * as React from 'react';
import { cn } from '@gib/ui';
function Card({
const Card = ({
className,
size = 'default',
...props
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
return (
<div
data-slot='card'
data-size={size}
className={cn(
'ring-foreground/10 bg-card text-card-foreground group/card flex flex-col gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) => (
<div
data-slot='card'
data-size={size}
className={cn(
'ring-foreground/10 bg-card text-card-foreground group/card flex flex-col gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
className,
)}
{...props}
/>
);
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-header'
className={cn(
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3',
className,
)}
{...props}
/>
);
}
const CardHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='card-header'
className={cn(
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3',
className,
)}
{...props}
/>
);
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-title'
className={cn(
'text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
className,
)}
{...props}
/>
);
}
const CardTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='card-title'
className={cn(
'text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
className,
)}
{...props}
/>
);
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
const CardDescription = ({
className,
...props
}: React.ComponentProps<'div'>) => (
<div
data-slot='card-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-action'
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
);
}
const CardAction = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='card-action'
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
);
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-content'
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
{...props}
/>
);
}
const CardContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='card-content'
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
{...props}
/>
);
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-footer'
className={cn(
'bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3',
className,
)}
{...props}
/>
);
}
const CardFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='card-footer'
className={cn(
'bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3',
className,
)}
{...props}
/>
);
export {
Card,

View File

@@ -12,12 +12,12 @@ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
interface CarouselProps {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: 'horizontal' | 'vertical';
setApi?: (api: CarouselApi) => void;
};
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
@@ -30,7 +30,7 @@ type CarouselContextProps = {
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const useCarousel = () => {
const context = React.useContext(CarouselContext);
if (!context) {
@@ -38,9 +38,9 @@ function useCarousel() {
}
return context;
}
};
function Carousel({
const Carousel = ({
orientation = 'horizontal',
opts,
setApi,
@@ -48,7 +48,7 @@ function Carousel({
className,
children,
...props
}: React.ComponentProps<'div'> & CarouselProps) {
}: React.ComponentProps<'div'> & CarouselProps) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
@@ -128,9 +128,12 @@ function Carousel({
</div>
</CarouselContext.Provider>
);
}
};
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
const CarouselContent = ({
className,
...props
}: React.ComponentProps<'div'>) => {
const { carouselRef, orientation } = useCarousel();
return (
@@ -149,9 +152,9 @@ function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
/>
</div>
);
}
};
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
const CarouselItem = ({ className, ...props }: React.ComponentProps<'div'>) => {
const { orientation } = useCarousel();
return (
@@ -167,14 +170,14 @@ function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
{...props}
/>
);
}
};
function CarouselPrevious({
const CarouselPrevious = ({
className,
variant = 'outline',
size = 'icon-sm',
...props
}: React.ComponentProps<typeof Button>) {
}: React.ComponentProps<typeof Button>) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
@@ -197,14 +200,14 @@ function CarouselPrevious({
<span className='sr-only'>Previous slide</span>
</Button>
);
}
};
function CarouselNext({
const CarouselNext = ({
className,
variant = 'outline',
size = 'icon-sm',
...props
}: React.ComponentProps<typeof Button>) {
}: React.ComponentProps<typeof Button>) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
@@ -227,7 +230,7 @@ function CarouselNext({
<span className='sr-only'>Next slide</span>
</Button>
);
}
};
export {
type CarouselApi,

View File

@@ -8,23 +8,24 @@ import { cn } from '@gib/ui';
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const;
export type ChartConfig = {
[k in string]: {
export type ChartConfig = Record<
string,
{
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
)
>;
type ChartContextProps = {
interface ChartContextProps {
config: ChartConfig;
};
}
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const useChart = () => {
const context = React.useContext(ChartContext);
if (!context) {
@@ -32,9 +33,9 @@ function useChart() {
}
return context;
}
};
function ChartContainer({
const ChartContainer = ({
id,
className,
children,
@@ -45,7 +46,7 @@ function ChartContainer({
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>['children'];
}) {
}) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
@@ -67,7 +68,7 @@ function ChartContainer({
</div>
</ChartContext.Provider>
);
}
};
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
@@ -104,7 +105,7 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
const ChartTooltipContent = ({
active,
payload,
className,
@@ -125,7 +126,7 @@ function ChartTooltipContent({
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}) {
}) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
@@ -138,7 +139,7 @@ function ChartTooltipContent({
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label
? config[label]?.label || label
: itemConfig?.label;
if (labelFormatter) {
@@ -248,11 +249,11 @@ function ChartTooltipContent({
</div>
</div>
);
}
};
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
const ChartLegendContent = ({
className,
hideIcon = false,
payload,
@@ -262,7 +263,7 @@ function ChartLegendContent({
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean;
nameKey?: string;
}) {
}) => {
const { config } = useChart();
if (!payload?.length) {
@@ -306,13 +307,13 @@ function ChartLegendContent({
})}
</div>
);
}
};
function getPayloadConfigFromPayload(
const getPayloadConfigFromPayload = (
config: ChartConfig,
payload: unknown,
key: string,
) {
) => {
if (typeof payload !== 'object' || payload === null) {
return undefined;
}
@@ -341,10 +342,8 @@ function getPayloadConfigFromPayload(
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
return configLabelKey in config ? config[configLabelKey] : config[key];
};
export {
ChartContainer,

View File

@@ -6,27 +6,25 @@ import { Checkbox as CheckboxPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Checkbox({
const Checkbox = ({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot='checkbox'
className={cn(
'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
className,
)}
{...props}
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) => (
<CheckboxPrimitive.Root
data-slot='checkbox'
className={cn(
'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot='checkbox-indicator'
className='grid place-content-center text-current transition-none [&>svg]:size-3.5'
>
<CheckboxPrimitive.Indicator
data-slot='checkbox-indicator'
className='grid place-content-center text-current transition-none [&>svg]:size-3.5'
>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
export { Checkbox };

View File

@@ -2,32 +2,28 @@
import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
function Collapsible({
const Collapsible = ({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot='collapsible' {...props} />;
}
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) => (
<CollapsiblePrimitive.Root data-slot='collapsible' {...props} />
);
function CollapsibleTrigger({
const CollapsibleTrigger = ({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot='collapsible-trigger'
{...props}
/>
);
}
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot='collapsible-trigger'
{...props}
/>
);
function CollapsibleContent({
const CollapsibleContent = ({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot='collapsible-content'
{...props}
/>
);
}
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) => (
<CollapsiblePrimitive.CollapsibleContent
data-slot='collapsible-content'
{...props}
/>
);
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -15,43 +15,42 @@ import {
const Combobox = ComboboxPrimitive.Root;
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot='combobox-value' {...props} />;
}
const ComboboxValue = ({ ...props }: ComboboxPrimitive.Value.Props) => (
<ComboboxPrimitive.Value data-slot='combobox-value' {...props} />
);
function ComboboxTrigger({
const ComboboxTrigger = ({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot='combobox-trigger'
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />
</ComboboxPrimitive.Trigger>
);
}
}: ComboboxPrimitive.Trigger.Props) => (
<ComboboxPrimitive.Trigger
data-slot='combobox-trigger'
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />
</ComboboxPrimitive.Trigger>
);
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot='combobox-clear'
className={cn(className)}
{...props}
render={
<InputGroupButton variant='ghost' size='icon-xs'>
<XIcon className='pointer-events-none' />
</InputGroupButton>
}
/>
);
}
const ComboboxClear = ({
className,
...props
}: ComboboxPrimitive.Clear.Props) => (
<ComboboxPrimitive.Clear
data-slot='combobox-clear'
className={cn(className)}
{...props}
render={
<InputGroupButton variant='ghost' size='icon-xs'>
<XIcon className='pointer-events-none' />
</InputGroupButton>
}
/>
);
function ComboboxInput({
const ComboboxInput = ({
className,
children,
disabled = false,
@@ -61,32 +60,30 @@ function ComboboxInput({
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean;
showClear?: boolean;
}) {
return (
<InputGroup className={cn('w-auto', className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align='inline-end'>
{showTrigger && (
<InputGroupButton
size='icon-xs'
variant='ghost'
render={<ComboboxTrigger />}
data-slot='input-group-button'
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
);
}
}) => (
<InputGroup className={cn('w-auto', className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align='inline-end'>
{showTrigger && (
<InputGroupButton
size='icon-xs'
variant='ghost'
render={<ComboboxTrigger />}
data-slot='input-group-button'
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
);
function ComboboxContent({
const ComboboxContent = ({
className,
side = 'bottom',
sideOffset = 6,
@@ -98,191 +95,178 @@ function ComboboxContent({
Pick<
ComboboxPrimitive.Positioner.Props,
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className='isolate z-50'
>
<ComboboxPrimitive.Popup
data-slot='combobox-content'
data-chips={!!anchor}
className={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none',
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
}
>) => (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className='isolate z-50'
>
<ComboboxPrimitive.Popup
data-slot='combobox-content'
data-chips={!!anchor}
className={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none',
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot='combobox-list'
className={cn(
'no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0',
className,
)}
{...props}
/>
);
}
const ComboboxList = ({
className,
...props
}: ComboboxPrimitive.List.Props) => (
<ComboboxPrimitive.List
data-slot='combobox-list'
className={cn(
'no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0',
className,
)}
{...props}
/>
);
function ComboboxItem({
const ComboboxItem = ({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot='combobox-item'
className={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center'>
<CheckIcon className='pointer-events-none' />
</span>
}
/>
</ComboboxPrimitive.Item>
);
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot='combobox-group'
className={cn(className)}
{...props}
}: ComboboxPrimitive.Item.Props) => (
<ComboboxPrimitive.Item
data-slot='combobox-item'
className={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center'>
<CheckIcon className='pointer-events-none' />
</span>
}
/>
);
}
</ComboboxPrimitive.Item>
);
function ComboboxLabel({
const ComboboxGroup = ({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot='combobox-label'
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
);
}
}: ComboboxPrimitive.Group.Props) => (
<ComboboxPrimitive.Group
data-slot='combobox-group'
className={cn(className)}
{...props}
/>
);
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot='combobox-collection' {...props} />
);
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot='combobox-empty'
className={cn(
'text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex',
className,
)}
{...props}
/>
);
}
function ComboboxSeparator({
const ComboboxLabel = ({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot='combobox-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
}: ComboboxPrimitive.GroupLabel.Props) => (
<ComboboxPrimitive.GroupLabel
data-slot='combobox-label'
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
);
function ComboboxChips({
const ComboboxCollection = ({
...props
}: ComboboxPrimitive.Collection.Props) => (
<ComboboxPrimitive.Collection data-slot='combobox-collection' {...props} />
);
const ComboboxEmpty = ({
className,
...props
}: ComboboxPrimitive.Empty.Props) => (
<ComboboxPrimitive.Empty
data-slot='combobox-empty'
className={cn(
'text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex',
className,
)}
{...props}
/>
);
const ComboboxSeparator = ({
className,
...props
}: ComboboxPrimitive.Separator.Props) => (
<ComboboxPrimitive.Separator
data-slot='combobox-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
const ComboboxChips = ({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot='combobox-chips'
className={cn(
'dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1',
className,
)}
{...props}
/>
);
}
ComboboxPrimitive.Chips.Props) => (
<ComboboxPrimitive.Chips
data-slot='combobox-chips'
className={cn(
'dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1',
className,
)}
{...props}
/>
);
function ComboboxChip({
const ComboboxChip = ({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean;
}) {
return (
<ComboboxPrimitive.Chip
data-slot='combobox-chip'
className={cn(
'bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0',
className,
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
className='-ml-1 opacity-50 hover:opacity-100'
data-slot='combobox-chip-remove'
render={
<Button variant='ghost' size='icon-xs'>
<XIcon className='pointer-events-none' />
</Button>
}
/>
)}
</ComboboxPrimitive.Chip>
);
}
}) => (
<ComboboxPrimitive.Chip
data-slot='combobox-chip'
className={cn(
'bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0',
className,
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
className='-ml-1 opacity-50 hover:opacity-100'
data-slot='combobox-chip-remove'
render={
<Button variant='ghost' size='icon-xs'>
<XIcon className='pointer-events-none' />
</Button>
}
/>
)}
</ComboboxPrimitive.Chip>
);
function ComboboxChipsInput({
const ComboboxChipsInput = ({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot='combobox-chip-input'
className={cn('min-w-16 flex-1 outline-none', className)}
{...props}
/>
);
}
}: ComboboxPrimitive.Input.Props) => (
<ComboboxPrimitive.Input
data-slot='combobox-chip-input'
className={cn('min-w-16 flex-1 outline-none', className)}
{...props}
/>
);
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null);
}
const useComboboxAnchor = () => React.useRef<HTMLDivElement | null>(null);
export {
Combobox,

View File

@@ -15,23 +15,21 @@ import {
InputGroupAddon,
} from '@gib/ui';
function Command({
const Command = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot='command'
className={cn(
'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-xl! p-1',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof CommandPrimitive>) => (
<CommandPrimitive
data-slot='command'
className={cn(
'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-xl! p-1',
className,
)}
{...props}
/>
);
function CommandDialog({
const CommandDialog = ({
title = 'Command Palette',
description = 'Search for a command to run...',
children,
@@ -43,142 +41,126 @@ function CommandDialog({
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
<DialogHeader className='sr-only'>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
}) => (
<Dialog {...props}>
<DialogHeader className='sr-only'>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
'top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0',
className,
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
);
const CommandInput = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) => (
<div data-slot='command-input-wrapper' className='p-1 pb-0'>
<InputGroup className='bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!'>
<CommandPrimitive.Input
data-slot='command-input'
className={cn(
'top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0',
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
);
}
{...props}
/>
<InputGroupAddon>
<SearchIcon className='size-4 shrink-0 opacity-50' />
</InputGroupAddon>
</InputGroup>
</div>
);
function CommandInput({
const CommandList = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot='command-input-wrapper' className='p-1 pb-0'>
<InputGroup className='bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!'>
<CommandPrimitive.Input
data-slot='command-input'
className={cn(
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
<InputGroupAddon>
<SearchIcon className='size-4 shrink-0 opacity-50' />
</InputGroupAddon>
</InputGroup>
</div>
);
}
}: React.ComponentProps<typeof CommandPrimitive.List>) => (
<CommandPrimitive.List
data-slot='command-list'
className={cn(
'no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none',
className,
)}
{...props}
/>
);
function CommandList({
const CommandEmpty = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot='command-list'
className={cn(
'no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof CommandPrimitive.Empty>) => (
<CommandPrimitive.Empty
data-slot='command-empty'
className={cn('py-6 text-center text-sm', className)}
{...props}
/>
);
function CommandEmpty({
const CommandGroup = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot='command-empty'
className={cn('py-6 text-center text-sm', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof CommandPrimitive.Group>) => (
<CommandPrimitive.Group
data-slot='command-group'
className={cn(
'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
className,
)}
{...props}
/>
);
function CommandGroup({
const CommandSeparator = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot='command-group'
className={cn(
'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof CommandPrimitive.Separator>) => (
<CommandPrimitive.Separator
data-slot='command-separator'
className={cn('bg-border -mx-1 h-px', className)}
{...props}
/>
);
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot='command-separator'
className={cn('bg-border -mx-1 h-px', className)}
{...props}
/>
);
}
function CommandItem({
const CommandItem = ({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot='command-item'
className={cn(
"data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<CheckIcon className='ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100' />
</CommandPrimitive.Item>
);
}
}: React.ComponentProps<typeof CommandPrimitive.Item>) => (
<CommandPrimitive.Item
data-slot='command-item'
className={cn(
"data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<CheckIcon className='ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100' />
</CommandPrimitive.Item>
);
function CommandShortcut({
const CommandShortcut = ({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='command-shortcut'
className={cn(
'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'span'>) => (
<span
data-slot='command-shortcut'
className={cn(
'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
export {
Command,

View File

@@ -6,79 +6,69 @@ import { ContextMenu as ContextMenuPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function ContextMenu({
const ContextMenu = ({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot='context-menu' {...props} />;
}
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) => (
<ContextMenuPrimitive.Root data-slot='context-menu' {...props} />
);
function ContextMenuTrigger({
const ContextMenuTrigger = ({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger
data-slot='context-menu-trigger'
className={cn('select-none', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) => (
<ContextMenuPrimitive.Trigger
data-slot='context-menu-trigger'
className={cn('select-none', className)}
{...props}
/>
);
function ContextMenuGroup({
const ContextMenuGroup = ({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot='context-menu-group' {...props} />
);
}
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) => (
<ContextMenuPrimitive.Group data-slot='context-menu-group' {...props} />
);
function ContextMenuPortal({
const ContextMenuPortal = ({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot='context-menu-portal' {...props} />
);
}
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) => (
<ContextMenuPrimitive.Portal data-slot='context-menu-portal' {...props} />
);
function ContextMenuSub({
const ContextMenuSub = ({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot='context-menu-sub' {...props} />;
}
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) => (
<ContextMenuPrimitive.Sub data-slot='context-menu-sub' {...props} />
);
function ContextMenuRadioGroup({
const ContextMenuRadioGroup = ({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot='context-menu-radio-group'
{...props}
/>
);
}
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) => (
<ContextMenuPrimitive.RadioGroup
data-slot='context-menu-radio-group'
{...props}
/>
);
function ContextMenuContent({
const ContextMenuContent = ({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content> & {
side?: 'top' | 'right' | 'bottom' | 'left';
}) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot='context-menu-content'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 max-h-(--radix-context-menu-content-available-height) min-w-36 origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100',
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
}) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot='context-menu-content'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 max-h-(--radix-context-menu-content-available-height) min-w-36 origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100',
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
function ContextMenuItem({
const ContextMenuItem = ({
className,
inset,
variant = 'default',
@@ -86,62 +76,56 @@ function ContextMenuItem({
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<ContextMenuPrimitive.Item
data-slot='context-menu-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
}) => (
<ContextMenuPrimitive.Item
data-slot='context-menu-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
function ContextMenuSubTrigger({
const ContextMenuSubTrigger = ({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot='context-menu-sub-trigger'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
</ContextMenuPrimitive.SubTrigger>
);
}
}) => (
<ContextMenuPrimitive.SubTrigger
data-slot='context-menu-sub-trigger'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
</ContextMenuPrimitive.SubTrigger>
);
function ContextMenuSubContent({
const ContextMenuSubContent = ({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot='context-menu-sub-content'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 bg-popover text-popover-foreground z-50 min-w-32 origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-lg border p-1 shadow-lg duration-100',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) => (
<ContextMenuPrimitive.SubContent
data-slot='context-menu-sub-content'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 bg-popover text-popover-foreground z-50 min-w-32 origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-lg border p-1 shadow-lg duration-100',
className,
)}
{...props}
/>
);
function ContextMenuCheckboxItem({
const ContextMenuCheckboxItem = ({
className,
children,
checked,
@@ -149,104 +133,94 @@ function ContextMenuCheckboxItem({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot='context-menu-checkbox-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className='pointer-events-none absolute right-2'>
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
}) => (
<ContextMenuPrimitive.CheckboxItem
data-slot='context-menu-checkbox-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className='pointer-events-none absolute right-2'>
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
function ContextMenuRadioItem({
const ContextMenuRadioItem = ({
className,
children,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot='context-menu-radio-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className='pointer-events-none absolute right-2'>
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
}) => (
<ContextMenuPrimitive.RadioItem
data-slot='context-menu-radio-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className='pointer-events-none absolute right-2'>
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
function ContextMenuLabel({
const ContextMenuLabel = ({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot='context-menu-label'
data-inset={inset}
className={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
className,
)}
{...props}
/>
);
}
}) => (
<ContextMenuPrimitive.Label
data-slot='context-menu-label'
data-inset={inset}
className={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
className,
)}
{...props}
/>
);
function ContextMenuSeparator({
const ContextMenuSeparator = ({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot='context-menu-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) => (
<ContextMenuPrimitive.Separator
data-slot='context-menu-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
function ContextMenuShortcut({
const ContextMenuShortcut = ({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='context-menu-shortcut'
className={cn(
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'span'>) => (
<span
data-slot='context-menu-shortcut'
className={cn(
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
export {
ContextMenu,

View File

@@ -6,148 +6,136 @@ import { Dialog as DialogPrimitive } from 'radix-ui';
import { Button, cn } from '@gib/ui';
function Dialog({
const Dialog = ({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot='dialog' {...props} />;
}
}: React.ComponentProps<typeof DialogPrimitive.Root>) => (
<DialogPrimitive.Root data-slot='dialog' {...props} />
);
function DialogTrigger({
const DialogTrigger = ({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />;
}
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) => (
<DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />
);
function DialogPortal({
const DialogPortal = ({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot='dialog-portal' {...props} />;
}
}: React.ComponentProps<typeof DialogPrimitive.Portal>) => (
<DialogPrimitive.Portal data-slot='dialog-portal' {...props} />
);
function DialogClose({
const DialogClose = ({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot='dialog-close' {...props} />;
}
}: React.ComponentProps<typeof DialogPrimitive.Close>) => (
<DialogPrimitive.Close data-slot='dialog-close' {...props} />
);
function DialogOverlay({
const DialogOverlay = ({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot='dialog-overlay'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) => (
<DialogPrimitive.Overlay
data-slot='dialog-overlay'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
function DialogContent({
const DialogContent = ({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot='dialog-content'
className={cn(
'bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-sm ring-1 duration-100 outline-none sm:max-w-sm',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot='dialog-close' asChild>
<Button
variant='ghost'
className='absolute top-2 right-2'
size='icon-sm'
>
<XIcon />
<span className='sr-only'>Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='dialog-header'
className={cn('flex flex-col gap-2', className)}
{...props}
/>
);
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<'div'> & {
showCloseButton?: boolean;
}) {
return (
<div
data-slot='dialog-footer'
}) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot='dialog-content'
className={cn(
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 sm:flex-row sm:justify-end',
'bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-sm ring-1 duration-100 outline-none sm:max-w-sm',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant='outline'>Close</Button>
<DialogPrimitive.Close data-slot='dialog-close' asChild>
<Button
variant='ghost'
className='absolute top-2 right-2'
size='icon-sm'
>
<XIcon />
<span className='sr-only'>Close</span>
</Button>
</DialogPrimitive.Close>
)}
</div>
);
}
</DialogPrimitive.Content>
</DialogPortal>
);
function DialogTitle({
const DialogHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='dialog-header'
className={cn('flex flex-col gap-2', className)}
{...props}
/>
);
const DialogFooter = ({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<'div'> & {
showCloseButton?: boolean;
}) => (
<div
data-slot='dialog-footer'
className={cn(
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 sm:flex-row sm:justify-end',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant='outline'>Close</Button>
</DialogPrimitive.Close>
)}
</div>
);
const DialogTitle = ({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot='dialog-title'
className={cn('text-base leading-none font-medium', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DialogPrimitive.Title>) => (
<DialogPrimitive.Title
data-slot='dialog-title'
className={cn('text-base leading-none font-medium', className)}
{...props}
/>
);
function DialogDescription({
const DialogDescription = ({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot='dialog-description'
className={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DialogPrimitive.Description>) => (
<DialogPrimitive.Description
data-slot='dialog-description'
className={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
className,
)}
{...props}
/>
);
export {
Dialog,

View File

@@ -5,117 +5,105 @@ import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from '@gib/ui';
function Drawer({
const Drawer = ({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot='drawer' {...props} />;
}
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root data-slot='drawer' {...props} />
);
function DrawerTrigger({
const DrawerTrigger = ({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />;
}
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) => (
<DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />
);
function DrawerPortal({
const DrawerPortal = ({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />;
}
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) => (
<DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />
);
function DrawerClose({
const DrawerClose = ({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot='drawer-close' {...props} />;
}
}: React.ComponentProps<typeof DrawerPrimitive.Close>) => (
<DrawerPrimitive.Close data-slot='drawer-close' {...props} />
);
function DrawerOverlay({
const DrawerOverlay = ({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot='drawer-overlay'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) => (
<DrawerPrimitive.Overlay
data-slot='drawer-overlay'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
function DrawerContent({
const DrawerContent = ({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot='drawer-portal'>
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot='drawer-content'
className={cn(
'bg-background group/drawer-content fixed z-50 flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm',
className,
)}
{...props}
>
<div className='bg-muted mx-auto mt-4 hidden h-1 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block' />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='drawer-header'
}: React.ComponentProps<typeof DrawerPrimitive.Content>) => (
<DrawerPortal data-slot='drawer-portal'>
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot='drawer-content'
className={cn(
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-0.5 md:text-left',
'bg-background group/drawer-content fixed z-50 flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm',
className,
)}
{...props}
/>
);
}
>
<div className='bg-muted mx-auto mt-4 hidden h-1 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block' />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='drawer-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
}
const DrawerHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='drawer-header'
className={cn(
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-0.5 md:text-left',
className,
)}
{...props}
/>
);
function DrawerTitle({
const DrawerFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='drawer-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
const DrawerTitle = ({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot='drawer-title'
className={cn('text-foreground text-base font-medium', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DrawerPrimitive.Title>) => (
<DrawerPrimitive.Title
data-slot='drawer-title'
className={cn('text-foreground text-base font-medium', className)}
{...props}
/>
);
function DrawerDescription({
const DrawerDescription = ({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot='drawer-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DrawerPrimitive.Description>) => (
<DrawerPrimitive.Description
data-slot='drawer-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
export {
Drawer,

View File

@@ -6,62 +6,51 @@ import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function DropdownMenu({
const DropdownMenu = ({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) => (
<DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />
);
function DropdownMenuPortal({
const DropdownMenuPortal = ({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) => (
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
);
function DropdownMenuTrigger({
const DropdownMenuTrigger = ({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot='dropdown-menu-trigger'
{...props}
/>
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) => (
<DropdownMenuPrimitive.Trigger data-slot='dropdown-menu-trigger' {...props} />
);
function DropdownMenuContent({
const DropdownMenuContent = ({
className,
align = 'start',
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot='dropdown-menu-content'
sideOffset={sideOffset}
align={align}
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 data-[state=closed]:overflow-hidden',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot='dropdown-menu-content'
sideOffset={sideOffset}
align={align}
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 data-[state=closed]:overflow-hidden',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
function DropdownMenuGroup({
const DropdownMenuGroup = ({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) => (
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
);
function DropdownMenuItem({
const DropdownMenuItem = ({
className,
inset,
variant = 'default',
@@ -69,22 +58,20 @@ function DropdownMenuItem({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<DropdownMenuPrimitive.Item
data-slot='dropdown-menu-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
}) => (
<DropdownMenuPrimitive.Item
data-slot='dropdown-menu-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
function DropdownMenuCheckboxItem({
const DropdownMenuCheckboxItem = ({
className,
children,
checked,
@@ -92,167 +79,151 @@ function DropdownMenuCheckboxItem({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot='dropdown-menu-checkbox-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
}) => (
<DropdownMenuPrimitive.CheckboxItem
data-slot='dropdown-menu-checkbox-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span
className='pointer-events-none absolute right-2 flex items-center justify-center'
data-slot='dropdown-menu-checkbox-item-indicator'
>
<span
className='pointer-events-none absolute right-2 flex items-center justify-center'
data-slot='dropdown-menu-checkbox-item-indicator'
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
function DropdownMenuRadioGroup({
const DropdownMenuRadioGroup = ({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot='dropdown-menu-radio-group'
{...props}
/>
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) => (
<DropdownMenuPrimitive.RadioGroup
data-slot='dropdown-menu-radio-group'
{...props}
/>
);
function DropdownMenuRadioItem({
const DropdownMenuRadioItem = ({
className,
children,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot='dropdown-menu-radio-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
}) => (
<DropdownMenuPrimitive.RadioItem
data-slot='dropdown-menu-radio-item'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span
className='pointer-events-none absolute right-2 flex items-center justify-center'
data-slot='dropdown-menu-radio-item-indicator'
>
<span
className='pointer-events-none absolute right-2 flex items-center justify-center'
data-slot='dropdown-menu-radio-item-indicator'
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
function DropdownMenuLabel({
const DropdownMenuLabel = ({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot='dropdown-menu-label'
data-inset={inset}
className={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
className,
)}
{...props}
/>
);
}
}) => (
<DropdownMenuPrimitive.Label
data-slot='dropdown-menu-label'
data-inset={inset}
className={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
className,
)}
{...props}
/>
);
function DropdownMenuSeparator({
const DropdownMenuSeparator = ({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot='dropdown-menu-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) => (
<DropdownMenuPrimitive.Separator
data-slot='dropdown-menu-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
function DropdownMenuShortcut({
const DropdownMenuShortcut = ({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='dropdown-menu-shortcut'
className={cn(
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'span'>) => (
<span
data-slot='dropdown-menu-shortcut'
className={cn(
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
function DropdownMenuSub({
const DropdownMenuSub = ({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) => (
<DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />
);
function DropdownMenuSubTrigger({
const DropdownMenuSubTrigger = ({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot='dropdown-menu-sub-trigger'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
</DropdownMenuPrimitive.SubTrigger>
);
}
}) => (
<DropdownMenuPrimitive.SubTrigger
data-slot='dropdown-menu-sub-trigger'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
</DropdownMenuPrimitive.SubTrigger>
);
function DropdownMenuSubContent({
const DropdownMenuSubContent = ({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot='dropdown-menu-sub-content'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg p-1 shadow-lg ring-1 duration-100',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) => (
<DropdownMenuPrimitive.SubContent
data-slot='dropdown-menu-sub-content'
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg p-1 shadow-lg ring-1 duration-100',
className,
)}
{...props}
/>
);
export {
DropdownMenu,

View File

@@ -3,28 +3,24 @@ import { cva } from 'class-variance-authority';
import { cn } from '@gib/ui';
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='empty'
className={cn(
'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
className,
)}
{...props}
/>
);
}
const Empty = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='empty'
className={cn(
'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
className,
)}
{...props}
/>
);
function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='empty-header'
className={cn('flex max-w-sm flex-col items-center gap-2', className)}
{...props}
/>
);
}
const EmptyHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='empty-header'
className={cn('flex max-w-sm flex-col items-center gap-2', className)}
{...props}
/>
);
const emptyMediaVariants = cva(
'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
@@ -41,56 +37,51 @@ const emptyMediaVariants = cva(
},
);
function EmptyMedia({
const EmptyMedia = ({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot='empty-icon'
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
}
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) => (
<div
data-slot='empty-icon'
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='empty-title'
className={cn('text-sm font-medium tracking-tight', className)}
{...props}
/>
);
}
const EmptyTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='empty-title'
className={cn('text-sm font-medium tracking-tight', className)}
{...props}
/>
);
function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<div
data-slot='empty-description'
className={cn(
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
}
const EmptyDescription = ({
className,
...props
}: React.ComponentProps<'p'>) => (
<div
data-slot='empty-description'
className={cn(
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='empty-content'
className={cn(
'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
className,
)}
{...props}
/>
);
}
const EmptyContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='empty-content'
className={cn(
'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
className,
)}
{...props}
/>
);
export {
Empty,

View File

@@ -6,58 +6,52 @@ import { cva } from 'class-variance-authority';
import { cn, Label, Separator } from '@gib/ui';
export function FieldSet({
export const FieldSet = ({
className,
...props
}: React.ComponentProps<'fieldset'>) {
return (
<fieldset
data-slot='field-set'
className={cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'fieldset'>) => (
<fieldset
data-slot='field-set'
className={cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className,
)}
{...props}
/>
);
export function FieldLegend({
export const FieldLegend = ({
className,
variant = 'legend',
...props
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
return (
<legend
data-slot='field-legend'
data-variant={variant}
className={cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) => (
<legend
data-slot='field-legend'
data-variant={variant}
className={cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
className,
)}
{...props}
/>
);
export function FieldGroup({
export const FieldGroup = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='field-group'
className={cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='field-group'
className={cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
className,
)}
{...props}
/>
);
const fieldVariants = cva(
'group/field data-[invalid=true]:text-destructive flex w-full gap-3',
@@ -83,128 +77,116 @@ const fieldVariants = cva(
},
);
export function Field({
export const Field = ({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
return (
<div
role='group'
data-slot='field'
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
);
}
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) => (
<div
role='group'
data-slot='field'
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
);
export function FieldContent({
export const FieldContent = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='field-content'
className={cn(
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='field-content'
className={cn(
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
className,
)}
{...props}
/>
);
export function FieldLabel({
export const FieldLabel = ({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot='field-label'
className={cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof Label>) => (
<Label
data-slot='field-label'
className={cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
className,
)}
{...props}
/>
);
export function FieldTitle({
export const FieldTitle = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='field-label'
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='field-label'
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className,
)}
{...props}
/>
);
export function FieldDescription({
export const FieldDescription = ({
className,
...props
}: React.ComponentProps<'p'>) {
return (
<p
data-slot='field-description'
className={cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'p'>) => (
<p
data-slot='field-description'
className={cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
export function FieldSeparator({
export const FieldSeparator = ({
children,
className,
...props
}: React.ComponentProps<'div'> & {
children?: React.ReactNode;
}) {
return (
<div
data-slot='field-separator'
data-content={!!children}
className={cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
className,
)}
{...props}
>
<Separator className='absolute inset-0 top-1/2' />
{children && (
<span
className='bg-background text-muted-foreground relative mx-auto block w-fit px-2'
data-slot='field-separator-content'
>
{children}
</span>
)}
</div>
);
}
}) => (
<div
data-slot='field-separator'
data-content={!!children}
className={cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
className,
)}
{...props}
>
<Separator className='absolute inset-0 top-1/2' />
{children && (
<span
className='bg-background text-muted-foreground relative mx-auto block w-fit px-2'
data-slot='field-separator-content'
>
{children}
</span>
)}
</div>
);
export function FieldError({
export const FieldError = ({
className,
children,
errors: maybeErrors,
...props
}: React.ComponentProps<'div'> & {
errors?: ({ message?: string } | undefined)[];
}) {
}) => {
const content = useMemo(() => {
if (children) {
return children;
@@ -244,4 +226,4 @@ export function FieldError({
{content}
</div>
);
}
};

View File

@@ -15,12 +15,12 @@ import { cn, Label } from '@gib/ui';
const Form = FormProvider;
type FormFieldContextValue<
interface FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
> {
name: TName;
};
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
@@ -62,15 +62,15 @@ const useFormField = () => {
};
};
type FormItemContextValue = {
interface FormItemContextValue {
id: string;
};
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const FormItem = ({ className, ...props }: React.ComponentProps<'div'>) => {
const id = React.useId();
return (
@@ -82,12 +82,12 @@ function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
/>
</FormItemContext.Provider>
);
}
};
function FormLabel({
const FormLabel = ({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
}: React.ComponentProps<typeof LabelPrimitive.Root>) => {
const { error, formItemId } = useFormField();
return (
@@ -99,9 +99,9 @@ function FormLabel({
{...props}
/>
);
}
};
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const FormControl = ({ ...props }: React.ComponentProps<typeof Slot>) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
@@ -118,9 +118,12 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
{...props}
/>
);
}
};
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const FormDescription = ({
className,
...props
}: React.ComponentProps<'p'>) => {
const { formDescriptionId } = useFormField();
return (
@@ -131,9 +134,9 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
{...props}
/>
);
}
};
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const FormMessage = ({ className, ...props }: React.ComponentProps<'p'>) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? '') : props.children;
@@ -151,7 +154,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
{body}
</p>
);
}
};
export {
useFormField,

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
export const useIsMobile = () => {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
@@ -18,4 +18,4 @@ export function useIsMobile() {
}, []);
return !!isMobile;
}
};

View File

@@ -9,12 +9,12 @@ type EventType =
| 'focusin'
| 'focusout';
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
export const useOnClickOutside = <T,>(
ref: React.RefObject<T | null> | React.RefObject<T | null>[],
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
eventType: EventType = 'mousedown',
eventListenerOptions: AddEventListenerOptions = {},
): void {
): void => {
const savedHandler = React.useRef(handler);
React.useLayoutEffect(() => {
@@ -55,6 +55,6 @@ export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
);
};
}, [ref, eventType, eventListenerOptions]);
}
};
export type { EventType };

View File

@@ -5,40 +5,36 @@ import { HoverCard as HoverCardPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function HoverCard({
const HoverCard = ({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot='hover-card' {...props} />;
}
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) => (
<HoverCardPrimitive.Root data-slot='hover-card' {...props} />
);
function HoverCardTrigger({
const HoverCardTrigger = ({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
);
}
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) => (
<HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
);
function HoverCardContent({
const HoverCardContent = ({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot='hover-card-portal'>
<HoverCardPrimitive.Content
data-slot='hover-card-content'
align={align}
sideOffset={sideOffset}
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) => (
<HoverCardPrimitive.Portal data-slot='hover-card-portal'>
<HoverCardPrimitive.Content
data-slot='hover-card-content'
align={align}
sideOffset={sideOffset}
className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -378,7 +378,7 @@ export const Cropper = ({
</ImageCrop>
);
export function Demo() {
export const Demo = () => {
const [file, setFile] = useState<File | null>(null);
const [croppedImage, setCroppedImage] = useState<string | null>(null);
@@ -432,4 +432,4 @@ export function Demo() {
</div>
</div>
);
}
};

View File

@@ -6,19 +6,17 @@ import { cva } from 'class-variance-authority';
import { Button, cn, Input, Textarea } from '@gib/ui';
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='input-group'
role='group'
className={cn(
'border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
className,
)}
{...props}
/>
);
}
const InputGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='input-group'
role='group'
className={cn(
'border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
className,
)}
{...props}
/>
);
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
@@ -41,27 +39,26 @@ const inputGroupAddonVariants = cva(
},
);
function InputGroupAddon({
const InputGroupAddon = ({
className,
align = 'inline-start',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role='group'
data-slot='input-group-addon'
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return;
}
e.currentTarget.parentElement?.querySelector('input')?.focus();
}}
{...props}
/>
);
}
}: React.ComponentProps<'div'> &
VariantProps<typeof inputGroupAddonVariants>) => (
<div
role='group'
data-slot='input-group-addon'
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return;
}
e.currentTarget.parentElement?.querySelector('input')?.focus();
}}
{...props}
/>
);
const inputGroupButtonVariants = cva(
'flex items-center gap-2 text-sm shadow-none',
@@ -81,68 +78,63 @@ const inputGroupButtonVariants = cva(
},
);
function InputGroupButton({
const InputGroupButton = ({
className,
type = 'button',
variant = 'ghost',
size = 'xs',
...props
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
VariantProps<typeof inputGroupButtonVariants>) => (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
const InputGroupText = ({
className,
...props
}: React.ComponentProps<'input'>) {
return (
<Input
data-slot='input-group-control'
className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'span'>) => (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
function InputGroupTextarea({
const InputGroupInput = ({
className,
...props
}: React.ComponentProps<'textarea'>) {
return (
<Textarea
data-slot='input-group-control'
className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'input'>) => (
<Input
data-slot='input-group-control'
className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className,
)}
{...props}
/>
);
const InputGroupTextarea = ({
className,
...props
}: React.ComponentProps<'textarea'>) => (
<Textarea
data-slot='input-group-control'
className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className,
)}
{...props}
/>
);
export {
InputGroup,

View File

@@ -6,47 +6,46 @@ import { MinusIcon } from 'lucide-react';
import { cn } from '@gib/ui';
function InputOTP({
const InputOTP = ({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot='input-otp'
containerClassName={cn(
'cn-input-otp flex items-center has-disabled:opacity-50',
containerClassName,
)}
spellCheck={false}
className={cn('disabled:cursor-not-allowed', className)}
{...props}
/>
);
}
}) => (
<OTPInput
data-slot='input-otp'
containerClassName={cn(
'cn-input-otp flex items-center has-disabled:opacity-50',
containerClassName,
)}
spellCheck={false}
className={cn('disabled:cursor-not-allowed', className)}
{...props}
/>
);
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='input-otp-group'
className={cn(
'has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive flex items-center rounded-lg has-aria-invalid:ring-3',
className,
)}
{...props}
/>
);
}
const InputOTPGroup = ({
className,
...props
}: React.ComponentProps<'div'>) => (
<div
data-slot='input-otp-group'
className={cn(
'has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive flex items-center rounded-lg has-aria-invalid:ring-3',
className,
)}
{...props}
/>
);
function InputOTPSlot({
const InputOTPSlot = ({
index,
className,
...props
}: React.ComponentProps<'div'> & {
index: number;
}) {
}) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
@@ -68,19 +67,17 @@ function InputOTPSlot({
)}
</div>
);
}
};
function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='input-otp-separator'
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
role='separator'
{...props}
>
<MinusIcon />
</div>
);
}
const InputOTPSeparator = ({ ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='input-otp-separator'
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
role='separator'
{...props}
>
<MinusIcon />
</div>
);
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -2,18 +2,20 @@ import type * as React from 'react';
import { cn } from '@gib/ui';
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot='input'
className={cn(
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 file:text-foreground placeholder:text-muted-foreground h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
className,
)}
{...props}
/>
);
}
const Input = ({
className,
type,
...props
}: React.ComponentProps<'input'>) => (
<input
type={type}
data-slot='input'
className={cn(
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 file:text-foreground placeholder:text-muted-foreground h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
className,
)}
{...props}
/>
);
export { Input };

View File

@@ -5,33 +5,29 @@ import { Slot } from 'radix-ui';
import { cn, Separator } from '@gib/ui';
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
role='list'
data-slot='item-group'
className={cn(
'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
className,
)}
{...props}
/>
);
}
const ItemGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
role='list'
data-slot='item-group'
className={cn(
'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
className,
)}
{...props}
/>
);
function ItemSeparator({
const ItemSeparator = ({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot='item-separator'
orientation='horizontal'
className={cn('my-2', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof Separator>) => (
<Separator
data-slot='item-separator'
orientation='horizontal'
className={cn('my-2', className)}
{...props}
/>
);
const itemVariants = cva(
'[a]:hover:bg-muted group/item focus-visible:border-ring focus-visible:ring-ring/50 flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:ring-[3px] [a]:transition-colors',
@@ -55,14 +51,14 @@ const itemVariants = cva(
},
);
function Item({
const Item = ({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'div'> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
VariantProps<typeof itemVariants> & { asChild?: boolean }) => {
const Comp = asChild ? Slot.Root : 'div';
return (
<Comp
@@ -73,7 +69,7 @@ function Item({
{...props}
/>
);
}
};
const itemMediaVariants = cva(
'flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none',
@@ -92,95 +88,84 @@ const itemMediaVariants = cva(
},
);
function ItemMedia({
const ItemMedia = ({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot='item-media'
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
}
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) => (
<div
data-slot='item-media'
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='item-content'
className={cn(
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
className,
)}
{...props}
/>
);
}
const ItemContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='item-content'
className={cn(
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
className,
)}
{...props}
/>
);
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='item-title'
className={cn(
'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
className,
)}
{...props}
/>
);
}
const ItemTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='item-title'
className={cn(
'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
className,
)}
{...props}
/>
);
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot='item-description'
className={cn(
'text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-left text-sm leading-normal font-normal group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
}
const ItemDescription = ({
className,
...props
}: React.ComponentProps<'p'>) => (
<p
data-slot='item-description'
className={cn(
'text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-left text-sm leading-normal font-normal group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='item-actions'
className={cn('flex items-center gap-2', className)}
{...props}
/>
);
}
const ItemActions = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='item-actions'
className={cn('flex items-center gap-2', className)}
{...props}
/>
);
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='item-header'
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
);
}
const ItemHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='item-header'
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
);
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='item-footer'
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
);
}
const ItemFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='item-footer'
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
);
export {
Item,

View File

@@ -1,26 +1,22 @@
import { cn } from '@gib/ui';
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
<kbd
data-slot='kbd'
className={cn(
"bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
/>
);
}
const Kbd = ({ className, ...props }: React.ComponentProps<'kbd'>) => (
<kbd
data-slot='kbd'
className={cn(
"bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
/>
);
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<kbd
data-slot='kbd-group'
className={cn('inline-flex items-center gap-1', className)}
{...props}
/>
);
}
const KbdGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
<kbd
data-slot='kbd-group'
className={cn('inline-flex items-center gap-1', className)}
{...props}
/>
);
export { Kbd, KbdGroup };

View File

@@ -5,19 +5,17 @@ import { Label as LabelPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Label({
const Label = ({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot='label'
className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof LabelPrimitive.Root>) => (
<LabelPrimitive.Root
data-slot='label'
className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className,
)}
{...props}
/>
);
export { Label };

View File

@@ -6,89 +6,81 @@ import { Menubar as MenubarPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Menubar({
const Menubar = ({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot='menubar'
className={cn(
'bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.Root>) => (
<MenubarPrimitive.Root
data-slot='menubar'
className={cn(
'bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs',
className,
)}
{...props}
/>
);
function MenubarMenu({
const MenubarMenu = ({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot='menubar-menu' {...props} />;
}
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) => (
<MenubarPrimitive.Menu data-slot='menubar-menu' {...props} />
);
function MenubarGroup({
const MenubarGroup = ({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot='menubar-group' {...props} />;
}
}: React.ComponentProps<typeof MenubarPrimitive.Group>) => (
<MenubarPrimitive.Group data-slot='menubar-group' {...props} />
);
function MenubarPortal({
const MenubarPortal = ({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot='menubar-portal' {...props} />;
}
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) => (
<MenubarPrimitive.Portal data-slot='menubar-portal' {...props} />
);
function MenubarRadioGroup({
const MenubarRadioGroup = ({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot='menubar-radio-group' {...props} />
);
}
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) => (
<MenubarPrimitive.RadioGroup data-slot='menubar-radio-group' {...props} />
);
function MenubarTrigger({
const MenubarTrigger = ({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot='menubar-trigger'
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) => (
<MenubarPrimitive.Trigger
data-slot='menubar-trigger'
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none',
className,
)}
{...props}
/>
);
function MenubarContent({
const MenubarContent = ({
className,
align = 'start',
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot='menubar-content'
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</MenubarPortal>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.Content>) => (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot='menubar-content'
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</MenubarPortal>
);
function MenubarItem({
const MenubarItem = ({
className,
inset,
variant = 'default',
@@ -96,165 +88,149 @@ function MenubarItem({
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<MenubarPrimitive.Item
data-slot='menubar-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive! relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
}) => (
<MenubarPrimitive.Item
data-slot='menubar-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive! relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
function MenubarCheckboxItem({
const MenubarCheckboxItem = ({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot='menubar-checkbox-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<MenubarPrimitive.ItemIndicator>
<CheckIcon className='size-4' />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) => (
<MenubarPrimitive.CheckboxItem
data-slot='menubar-checkbox-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<MenubarPrimitive.ItemIndicator>
<CheckIcon className='size-4' />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
function MenubarRadioItem({
const MenubarRadioItem = ({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot='menubar-radio-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<MenubarPrimitive.ItemIndicator>
<CircleIcon className='size-2 fill-current' />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) => (
<MenubarPrimitive.RadioItem
data-slot='menubar-radio-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<MenubarPrimitive.ItemIndicator>
<CircleIcon className='size-2 fill-current' />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
function MenubarLabel({
const MenubarLabel = ({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
data-slot='menubar-label'
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
);
}
}) => (
<MenubarPrimitive.Label
data-slot='menubar-label'
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
);
function MenubarSeparator({
const MenubarSeparator = ({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot='menubar-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) => (
<MenubarPrimitive.Separator
data-slot='menubar-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
);
function MenubarShortcut({
const MenubarShortcut = ({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='menubar-shortcut'
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<'span'>) => (
<span
data-slot='menubar-shortcut'
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
function MenubarSub({
const MenubarSub = ({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot='menubar-sub' {...props} />;
}
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) => (
<MenubarPrimitive.Sub data-slot='menubar-sub' {...props} />
);
function MenubarSubTrigger({
const MenubarSubTrigger = ({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot='menubar-sub-trigger'
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className='ml-auto h-4 w-4' />
</MenubarPrimitive.SubTrigger>
);
}
}) => (
<MenubarPrimitive.SubTrigger
data-slot='menubar-sub-trigger'
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className='ml-auto h-4 w-4' />
</MenubarPrimitive.SubTrigger>
);
function MenubarSubContent({
const MenubarSubContent = ({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot='menubar-sub-content'
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) => (
<MenubarPrimitive.SubContent
data-slot='menubar-sub-content'
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
);
export {
Menubar,

View File

@@ -3,51 +3,49 @@ import { ChevronDownIcon } from 'lucide-react';
import { cn } from '@gib/ui';
function NativeSelect({
const NativeSelect = ({
className,
size = 'default',
...props
}: Omit<React.ComponentProps<'select'>, 'size'> & { size?: 'sm' | 'default' }) {
return (
<div
className='group/native-select relative w-fit has-[select:disabled]:opacity-50'
data-slot='native-select-wrapper'
>
<select
data-slot='native-select'
data-size={size}
className={cn(
'border-input selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
className,
)}
{...props}
/>
<ChevronDownIcon
className='text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none'
aria-hidden='true'
data-slot='native-select-icon'
/>
</div>
);
}
function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
return <option data-slot='native-select-option' {...props} />;
}
function NativeSelectOptGroup({
className,
...props
}: React.ComponentProps<'optgroup'>) {
return (
<optgroup
data-slot='native-select-optgroup'
className={cn(className)}
}: Omit<React.ComponentProps<'select'>, 'size'> & {
size?: 'sm' | 'default';
}) => (
<div
className='group/native-select relative w-fit has-[select:disabled]:opacity-50'
data-slot='native-select-wrapper'
>
<select
data-slot='native-select'
data-size={size}
className={cn(
'border-input selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
className,
)}
{...props}
/>
);
}
<ChevronDownIcon
className='text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none'
aria-hidden='true'
data-slot='native-select-icon'
/>
</div>
);
const NativeSelectOption = ({ ...props }: React.ComponentProps<'option'>) => (
<option data-slot='native-select-option' {...props} />
);
const NativeSelectOptGroup = ({
className,
...props
}: React.ComponentProps<'optgroup'>) => (
<optgroup
data-slot='native-select-optgroup'
className={cn(className)}
{...props}
/>
);
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };

View File

@@ -5,155 +5,137 @@ import { NavigationMenu as NavigationMenuPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function NavigationMenu({
const NavigationMenu = ({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot='navigation-menu'
data-viewport={viewport}
className={cn(
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
}) => (
<NavigationMenuPrimitive.Root
data-slot='navigation-menu'
data-viewport={viewport}
className={cn(
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
function NavigationMenuList({
const NavigationMenuList = ({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot='navigation-menu-list'
className={cn(
'group flex flex-1 list-none items-center justify-center gap-1',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) => (
<NavigationMenuPrimitive.List
data-slot='navigation-menu-list'
className={cn(
'group flex flex-1 list-none items-center justify-center gap-1',
className,
)}
{...props}
/>
);
function NavigationMenuItem({
const NavigationMenuItem = ({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot='navigation-menu-item'
className={cn('relative', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) => (
<NavigationMenuPrimitive.Item
data-slot='navigation-menu-item'
className={cn('relative', className)}
{...props}
/>
);
const navigationMenuTriggerStyle = cva(
'group bg-background hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50',
);
function NavigationMenuTrigger({
const NavigationMenuTrigger = ({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot='navigation-menu-trigger'
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDownIcon
className='relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180'
aria-hidden='true'
/>
</NavigationMenuPrimitive.Trigger>
);
}
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) => (
<NavigationMenuPrimitive.Trigger
data-slot='navigation-menu-trigger'
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDownIcon
className='relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180'
aria-hidden='true'
/>
</NavigationMenuPrimitive.Trigger>
);
function NavigationMenuContent({
const NavigationMenuContent = ({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot='navigation-menu-content'
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) => (
<NavigationMenuPrimitive.Content
data-slot='navigation-menu-content'
className={cn(
'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 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
className,
)}
{...props}
/>
);
const NavigationMenuViewport = ({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) => (
<div
className={cn('absolute top-full left-0 isolate z-50 flex justify-center')}
>
<NavigationMenuPrimitive.Viewport
data-slot='navigation-menu-viewport'
className={cn(
'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 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
'origin-top-center bg-popover text-popover-foreground data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
className,
)}
{...props}
/>
);
}
</div>
);
function NavigationMenuViewport({
const NavigationMenuLink = ({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
'absolute top-full left-0 isolate z-50 flex justify-center',
)}
>
<NavigationMenuPrimitive.Viewport
data-slot='navigation-menu-viewport'
className={cn(
'origin-top-center bg-popover text-popover-foreground data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
className,
)}
{...props}
/>
</div>
);
}
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) => (
<NavigationMenuPrimitive.Link
data-slot='navigation-menu-link'
className={cn(
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground data-[active=true]:hover:bg-accent data-[active=true]:focus:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
function NavigationMenuLink({
const NavigationMenuIndicator = ({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot='navigation-menu-link'
className={cn(
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground data-[active=true]:hover:bg-accent data-[active=true]:focus:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot='navigation-menu-indicator'
className={cn(
'data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
className,
)}
{...props}
>
<div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' />
</NavigationMenuPrimitive.Indicator>
);
}
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) => (
<NavigationMenuPrimitive.Indicator
data-slot='navigation-menu-indicator'
className={cn(
'data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
className,
)}
{...props}
>
<div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' />
</NavigationMenuPrimitive.Indicator>
);
export {
NavigationMenu,

View File

@@ -7,118 +7,106 @@ import {
import { Button, cn } from '@gib/ui';
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
return (
<nav
role='navigation'
aria-label='pagination'
data-slot='pagination'
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
}
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role='navigation'
aria-label='pagination'
data-slot='pagination'
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
function PaginationContent({
const PaginationContent = ({
className,
...props
}: React.ComponentProps<'ul'>) {
return (
<ul
data-slot='pagination-content'
className={cn('flex items-center gap-0.5', className)}
{...props}
/>
);
}
}: React.ComponentProps<'ul'>) => (
<ul
data-slot='pagination-content'
className={cn('flex items-center gap-0.5', className)}
{...props}
/>
);
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
return <li data-slot='pagination-item' {...props} />;
}
const PaginationItem = ({ ...props }: React.ComponentProps<'li'>) => (
<li data-slot='pagination-item' {...props} />
);
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<'a'>;
function PaginationLink({
const PaginationLink = ({
className,
isActive,
size = 'icon',
...props
}: PaginationLinkProps) {
return (
<Button
asChild
variant={isActive ? 'outline' : 'ghost'}
size={size}
className={cn(className)}
>
<a
aria-current={isActive ? 'page' : undefined}
data-slot='pagination-link'
data-active={isActive}
{...props}
/>
</Button>
);
}
}: PaginationLinkProps) => (
<Button
asChild
variant={isActive ? 'outline' : 'ghost'}
size={size}
className={cn(className)}
>
<a
aria-current={isActive ? 'page' : undefined}
data-slot='pagination-link'
data-active={isActive}
{...props}
/>
</Button>
);
function PaginationPrevious({
const PaginationPrevious = ({
className,
text = 'Previous',
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return (
<PaginationLink
aria-label='Go to previous page'
size='default'
className={cn('pl-1.5!', className)}
{...props}
>
<ChevronLeftIcon data-icon='inline-start' className='cn-rtl-flip' />
<span className='hidden sm:block'>{text}</span>
</PaginationLink>
);
}
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
<PaginationLink
aria-label='Go to previous page'
size='default'
className={cn('pl-1.5!', className)}
{...props}
>
<ChevronLeftIcon data-icon='inline-start' className='cn-rtl-flip' />
<span className='hidden sm:block'>{text}</span>
</PaginationLink>
);
function PaginationNext({
const PaginationNext = ({
className,
text = 'Next',
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return (
<PaginationLink
aria-label='Go to next page'
size='default'
className={cn('pr-1.5!', className)}
{...props}
>
<span className='hidden sm:block'>{text}</span>
<ChevronRightIcon data-icon='inline-end' className='cn-rtl-flip' />
</PaginationLink>
);
}
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
<PaginationLink
aria-label='Go to next page'
size='default'
className={cn('pr-1.5!', className)}
{...props}
>
<span className='hidden sm:block'>{text}</span>
<ChevronRightIcon data-icon='inline-end' className='cn-rtl-flip' />
</PaginationLink>
);
function PaginationEllipsis({
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
aria-hidden
data-slot='pagination-ellipsis'
className={cn(
"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<MoreHorizontalIcon />
<span className='sr-only'>More pages</span>
</span>
);
}
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
data-slot='pagination-ellipsis'
className={cn(
"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<MoreHorizontalIcon />
<span className='sr-only'>More pages</span>
</span>
);
export {
Pagination,

View File

@@ -5,78 +5,73 @@ import { Popover as PopoverPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Popover({
const Popover = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot='popover' {...props} />;
}
}: React.ComponentProps<typeof PopoverPrimitive.Root>) => (
<PopoverPrimitive.Root data-slot='popover' {...props} />
);
function PopoverTrigger({
const PopoverTrigger = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot='popover-trigger' {...props} />;
}
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) => (
<PopoverPrimitive.Trigger data-slot='popover-trigger' {...props} />
);
function PopoverContent({
const PopoverContent = ({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot='popover-content'
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
}: React.ComponentProps<typeof PopoverPrimitive.Content>) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot='popover-content'
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
function PopoverAnchor({
const PopoverAnchor = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot='popover-anchor' {...props} />;
}
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) => (
<PopoverPrimitive.Anchor data-slot='popover-anchor' {...props} />
);
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='popover-header'
className={cn('flex flex-col gap-1 text-sm', className)}
{...props}
/>
);
}
function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) {
return (
<div
data-slot='popover-title'
className={cn('font-medium', className)}
{...props}
/>
);
}
function PopoverDescription({
const PopoverHeader = ({
className,
...props
}: React.ComponentProps<'p'>) {
return (
<p
data-slot='popover-description'
className={cn('text-muted-foreground', className)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='popover-header'
className={cn('flex flex-col gap-1 text-sm', className)}
{...props}
/>
);
const PopoverTitle = ({ className, ...props }: React.ComponentProps<'h2'>) => (
<div
data-slot='popover-title'
className={cn('font-medium', className)}
{...props}
/>
);
const PopoverDescription = ({
className,
...props
}: React.ComponentProps<'p'>) => (
<p
data-slot='popover-description'
className={cn('text-muted-foreground', className)}
{...props}
/>
);
export {
Popover,

View File

@@ -5,27 +5,25 @@ import { Progress as ProgressPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Progress({
const Progress = ({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot='progress'
className={cn(
'bg-muted relative flex h-1 w-full items-center overflow-x-hidden rounded-full',
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot='progress-indicator'
className='bg-primary size-full flex-1 transition-all'
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
}: React.ComponentProps<typeof ProgressPrimitive.Root>) => (
<ProgressPrimitive.Root
data-slot='progress'
className={cn(
'bg-muted relative flex h-1 w-full items-center overflow-x-hidden rounded-full',
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot='progress-indicator'
className='bg-primary size-full flex-1 transition-all'
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
export { Progress };

View File

@@ -6,40 +6,36 @@ import { RadioGroup as RadioGroupPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function RadioGroup({
const RadioGroup = ({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot='radio-group'
className={cn('grid gap-3', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) => (
<RadioGroupPrimitive.Root
data-slot='radio-group'
className={cn('grid gap-3', className)}
{...props}
/>
);
function RadioGroupItem({
const RadioGroupItem = ({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot='radio-group-item'
className={cn(
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) => (
<RadioGroupPrimitive.Item
data-slot='radio-group-item'
className={cn(
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot='radio-group-indicator'
className='relative flex items-center justify-center'
>
<RadioGroupPrimitive.Indicator
data-slot='radio-group-indicator'
className='relative flex items-center justify-center'
>
<CircleIcon className='fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2' />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
<CircleIcon className='fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2' />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
export { RadioGroup, RadioGroupItem };

View File

@@ -5,49 +5,45 @@ import * as ResizablePrimitive from 'react-resizable-panels';
import { cn } from '@gib/ui';
function ResizablePanelGroup({
const ResizablePanelGroup = ({
className,
...props
}: ResizablePrimitive.GroupProps) {
return (
<ResizablePrimitive.Group
data-slot='resizable-panel-group'
className={cn(
'flex h-full w-full aria-[orientation=vertical]:flex-col',
className,
)}
{...props}
/>
);
}
}: ResizablePrimitive.GroupProps) => (
<ResizablePrimitive.Group
data-slot='resizable-panel-group'
className={cn(
'flex h-full w-full aria-[orientation=vertical]:flex-col',
className,
)}
{...props}
/>
);
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
return <ResizablePrimitive.Panel data-slot='resizable-panel' {...props} />;
}
const ResizablePanel = ({ ...props }: ResizablePrimitive.PanelProps) => (
<ResizablePrimitive.Panel data-slot='resizable-panel' {...props} />
);
function ResizableHandle({
const ResizableHandle = ({
withHandle,
className,
...props
}: ResizablePrimitive.SeparatorProps & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.Separator
data-slot='resizable-handle'
className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90',
className,
)}
{...props}
>
{withHandle && (
<div className='bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border'>
<GripVerticalIcon className='size-2.5' />
</div>
)}
</ResizablePrimitive.Separator>
);
}
}) => (
<ResizablePrimitive.Separator
data-slot='resizable-handle'
className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90',
className,
)}
{...props}
>
{withHandle && (
<div className='bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border'>
<GripVerticalIcon className='size-2.5' />
</div>
)}
</ResizablePrimitive.Separator>
);
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };

View File

@@ -5,51 +5,47 @@ import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function ScrollArea({
const ScrollArea = ({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot='scroll-area'
className={cn('relative', className)}
{...props}
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) => (
<ScrollAreaPrimitive.Root
data-slot='scroll-area'
className={cn('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot='scroll-area-viewport'
className='focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1'
>
<ScrollAreaPrimitive.Viewport
data-slot='scroll-area-viewport'
className='focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1'
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
function ScrollBar({
const ScrollBar = ({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot='scroll-area-scrollbar'
data-orientation={orientation}
orientation={orientation}
className={cn(
'flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot='scroll-area-thumb'
className='bg-border relative flex-1 rounded-full'
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot='scroll-area-scrollbar'
data-orientation={orientation}
orientation={orientation}
className={cn(
'flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot='scroll-area-thumb'
className='bg-border relative flex-1 rounded-full'
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
export { ScrollArea, ScrollBar };

View File

@@ -6,175 +6,161 @@ import { Select as SelectPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Select({
const Select = ({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot='select' {...props} />;
}
}: React.ComponentProps<typeof SelectPrimitive.Root>) => (
<SelectPrimitive.Root data-slot='select' {...props} />
);
function SelectGroup({
const SelectGroup = ({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot='select-group' {...props} />;
}
}: React.ComponentProps<typeof SelectPrimitive.Group>) => (
<SelectPrimitive.Group data-slot='select-group' {...props} />
);
function SelectValue({
const SelectValue = ({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot='select-value' {...props} />;
}
}: React.ComponentProps<typeof SelectPrimitive.Value>) => (
<SelectPrimitive.Value data-slot='select-value' {...props} />
);
function SelectTrigger({
const SelectTrigger = ({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default';
}) {
return (
<SelectPrimitive.Trigger
data-slot='select-trigger'
data-size={size}
className={cn(
"border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='text-'])]:text-muted-foreground flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className='size-4 opacity-50' />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
}) => (
<SelectPrimitive.Trigger
data-slot='select-trigger'
data-size={size}
className={cn(
"border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='text-'])]:text-muted-foreground flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className='size-4 opacity-50' />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
function SelectContent({
const SelectContent = ({
className,
children,
position = 'item-aligned',
align = 'center',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot='select-content'
}: React.ComponentProps<typeof SelectPrimitive.Content>) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot='select-content'
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
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}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'bg-popover text-popover-foreground 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
'p-1',
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,
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
)}
position={position}
align={align}
{...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)] scroll-my-1',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
function SelectLabel({
const SelectLabel = ({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot='select-label'
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof SelectPrimitive.Label>) => (
<SelectPrimitive.Label
data-slot='select-label'
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
);
function SelectItem({
const SelectItem = ({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot='select-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
}: React.ComponentProps<typeof SelectPrimitive.Item>) => (
<SelectPrimitive.Item
data-slot='select-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span
data-slot='select-item-indicator'
className='absolute right-2 flex size-3.5 items-center justify-center'
>
<span
data-slot='select-item-indicator'
className='absolute right-2 flex size-3.5 items-center justify-center'
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className='size-4' />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
<SelectPrimitive.ItemIndicator>
<CheckIcon className='size-4' />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
function SelectSeparator({
const SelectSeparator = ({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot='select-separator'
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof SelectPrimitive.Separator>) => (
<SelectPrimitive.Separator
data-slot='select-separator'
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props}
/>
);
function SelectScrollUpButton({
const SelectScrollUpButton = ({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot='select-scroll-up-button'
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUpIcon className='size-4' />
</SelectPrimitive.ScrollUpButton>
);
}
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) => (
<SelectPrimitive.ScrollUpButton
data-slot='select-scroll-up-button'
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUpIcon className='size-4' />
</SelectPrimitive.ScrollUpButton>
);
function SelectScrollDownButton({
const SelectScrollDownButton = ({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot='select-scroll-down-button'
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDownIcon className='size-4' />
</SelectPrimitive.ScrollDownButton>
);
}
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) => (
<SelectPrimitive.ScrollDownButton
data-slot='select-scroll-down-button'
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDownIcon className='size-4' />
</SelectPrimitive.ScrollDownButton>
);
export {
Select,

View File

@@ -5,24 +5,22 @@ import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from '@gib/ui';
function Separator({
const Separator = ({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot='separator'
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) => (
<SeparatorPrimitive.Root
data-slot='separator'
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>
);
export { Separator };

View File

@@ -6,45 +6,45 @@ import { Dialog as SheetPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot='sheet' {...props} />;
}
function SheetTrigger({
const Sheet = ({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />;
}
}: React.ComponentProps<typeof SheetPrimitive.Root>) => (
<SheetPrimitive.Root data-slot='sheet' {...props} />
);
function SheetClose({
const SheetTrigger = ({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot='sheet-close' {...props} />;
}
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) => (
<SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />
);
function SheetPortal({
const SheetClose = ({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot='sheet-portal' {...props} />;
}
}: React.ComponentProps<typeof SheetPrimitive.Close>) => (
<SheetPrimitive.Close data-slot='sheet-close' {...props} />
);
function SheetOverlay({
const SheetPortal = ({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) => (
<SheetPrimitive.Portal data-slot='sheet-portal' {...props} />
);
const SheetOverlay = ({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot='sheet-overlay'
className={cn(
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) => (
<SheetPrimitive.Overlay
data-slot='sheet-overlay'
className={cn(
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className,
)}
{...props}
/>
);
function SheetContent({
const SheetContent = ({
className,
children,
side = 'right',
@@ -53,83 +53,73 @@ function SheetContent({
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: 'top' | 'right' | 'bottom' | 'left';
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot='sheet-content'
className={cn(
'bg-background data-[state=closed]:animate-out data-[state=open]:animate-in fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right' &&
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' &&
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none'>
<XIcon className='size-4' />
<span className='sr-only'>Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sheet-header'
className={cn('flex flex-col gap-1.5 p-4', className)}
}) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot='sheet-content'
className={cn(
'bg-background data-[state=closed]:animate-out data-[state=open]:animate-in fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right' &&
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' &&
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
className,
)}
{...props}
/>
);
}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none'>
<XIcon className='size-4' />
<span className='sr-only'>Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
);
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sheet-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
}
const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='sheet-header'
className={cn('flex flex-col gap-1.5 p-4', className)}
{...props}
/>
);
function SheetTitle({
const SheetFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='sheet-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
const SheetTitle = ({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot='sheet-title'
className={cn('text-foreground font-semibold', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof SheetPrimitive.Title>) => (
<SheetPrimitive.Title
data-slot='sheet-title'
className={cn('text-foreground font-semibold', className)}
{...props}
/>
);
function SheetDescription({
const SheetDescription = ({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot='sheet-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof SheetPrimitive.Description>) => (
<SheetPrimitive.Description
data-slot='sheet-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
export {
Sheet,

View File

@@ -31,7 +31,7 @@ const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContextProps = {
interface SidebarContextProps {
state: 'expanded' | 'collapsed';
open: boolean;
setOpen: (open: boolean) => void;
@@ -39,20 +39,20 @@ type SidebarContextProps = {
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const useSidebar = () => {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider.');
}
return context;
}
};
function SidebarProvider({
const SidebarProvider = ({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
@@ -64,7 +64,7 @@ function SidebarProvider({
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
}) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
@@ -148,9 +148,9 @@ function SidebarProvider({
</TooltipProvider>
</SidebarContext.Provider>
);
}
};
function Sidebar({
const Sidebar = ({
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
@@ -161,7 +161,7 @@ function Sidebar({
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
}) {
}) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === 'none') {
@@ -250,13 +250,13 @@ function Sidebar({
</div>
</div>
);
}
};
function SidebarTrigger({
const SidebarTrigger = ({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
}: React.ComponentProps<typeof Button>) => {
const { toggleSidebar } = useSidebar();
return (
@@ -276,9 +276,12 @@ function SidebarTrigger({
<span className='sr-only'>Toggle Sidebar</span>
</Button>
);
}
};
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
const SidebarRail = ({
className,
...props
}: React.ComponentProps<'button'>) => {
const { toggleSidebar } = useSidebar();
return (
@@ -301,102 +304,100 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
{...props}
/>
);
}
};
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
return (
<main
data-slot='sidebar-inset'
className={cn(
'bg-background relative flex w-full flex-1 flex-col',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
className,
)}
{...props}
/>
);
}
function SidebarInput({
const SidebarInset = ({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot='sidebar-input'
data-sidebar='input'
className={cn('bg-background h-8 w-full shadow-none', className)}
{...props}
/>
);
}
}: React.ComponentProps<'main'>) => (
<main
data-slot='sidebar-inset'
className={cn(
'bg-background relative flex w-full flex-1 flex-col',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
className,
)}
{...props}
/>
);
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-header'
data-sidebar='header'
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-footer'
data-sidebar='footer'
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
}
function SidebarSeparator({
const SidebarInput = ({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot='sidebar-separator'
data-sidebar='separator'
className={cn('bg-sidebar-border mx-2 w-auto', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof Input>) => (
<Input
data-slot='sidebar-input'
data-sidebar='input'
className={cn('bg-background h-8 w-full shadow-none', className)}
{...props}
/>
);
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-content'
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}
/>
);
}
const SidebarHeader = ({
className,
...props
}: React.ComponentProps<'div'>) => (
<div
data-slot='sidebar-header'
data-sidebar='header'
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-group'
data-sidebar='group'
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
{...props}
/>
);
}
const SidebarFooter = ({
className,
...props
}: React.ComponentProps<'div'>) => (
<div
data-slot='sidebar-footer'
data-sidebar='footer'
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
function SidebarGroupLabel({
const SidebarSeparator = ({
className,
...props
}: React.ComponentProps<typeof Separator>) => (
<Separator
data-slot='sidebar-separator'
data-sidebar='separator'
className={cn('bg-sidebar-border mx-2 w-auto', className)}
{...props}
/>
);
const SidebarContent = ({
className,
...props
}: React.ComponentProps<'div'>) => (
<div
data-slot='sidebar-content'
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}
/>
);
const SidebarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='sidebar-group'
data-sidebar='group'
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
{...props}
/>
);
const SidebarGroupLabel = ({
className,
asChild = false,
...props
}: React.ComponentProps<'div'> & { asChild?: boolean }) {
}: React.ComponentProps<'div'> & { asChild?: boolean }) => {
const Comp = asChild ? Slot.Root : 'div';
return (
@@ -411,13 +412,13 @@ function SidebarGroupLabel({
{...props}
/>
);
}
};
function SidebarGroupAction({
const SidebarGroupAction = ({
className,
asChild = false,
...props
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
}: React.ComponentProps<'button'> & { asChild?: boolean }) => {
const Comp = asChild ? Slot.Root : 'button';
return (
@@ -434,43 +435,40 @@ function SidebarGroupAction({
{...props}
/>
);
}
};
function SidebarGroupContent({
const SidebarGroupContent = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-group-content'
data-sidebar='group-content'
className={cn('w-full text-sm', className)}
{...props}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='sidebar-group-content'
data-sidebar='group-content'
className={cn('w-full text-sm', className)}
{...props}
/>
);
function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
return (
<ul
data-slot='sidebar-menu'
data-sidebar='menu'
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
);
}
const SidebarMenu = ({ className, ...props }: React.ComponentProps<'ul'>) => (
<ul
data-slot='sidebar-menu'
data-sidebar='menu'
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
);
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot='sidebar-menu-item'
data-sidebar='menu-item'
className={cn('group/menu-item relative', className)}
{...props}
/>
);
}
const SidebarMenuItem = ({
className,
...props
}: React.ComponentProps<'li'>) => (
<li
data-slot='sidebar-menu-item'
data-sidebar='menu-item'
className={cn('group/menu-item relative', className)}
{...props}
/>
);
const sidebarMenuButtonVariants = cva(
'peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
@@ -494,7 +492,7 @@ const sidebarMenuButtonVariants = cva(
},
);
function SidebarMenuButton({
const SidebarMenuButton = ({
asChild = false,
isActive = false,
variant = 'default',
@@ -506,7 +504,7 @@ function SidebarMenuButton({
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
} & VariantProps<typeof sidebarMenuButtonVariants>) => {
const Comp = asChild ? Slot.Root : 'button';
const { isMobile, state } = useSidebar();
@@ -542,9 +540,9 @@ function SidebarMenuButton({
/>
</Tooltip>
);
}
};
function SidebarMenuAction({
const SidebarMenuAction = ({
className,
asChild = false,
showOnHover = false,
@@ -552,7 +550,7 @@ function SidebarMenuAction({
}: React.ComponentProps<'button'> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
}) => {
const Comp = asChild ? Slot.Root : 'button';
return (
@@ -574,37 +572,35 @@ function SidebarMenuAction({
{...props}
/>
);
}
};
function SidebarMenuBadge({
const SidebarMenuBadge = ({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-menu-badge'
data-sidebar='menu-badge'
className={cn(
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
'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}
/>
);
}
}: React.ComponentProps<'div'>) => (
<div
data-slot='sidebar-menu-badge'
data-sidebar='menu-badge'
className={cn(
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
'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}
/>
);
function SidebarMenuSkeleton({
const SidebarMenuSkeleton = ({
className,
showIcon = false,
...props
}: React.ComponentProps<'div'> & {
showIcon?: boolean;
}) {
}) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
@@ -634,38 +630,37 @@ function SidebarMenuSkeleton({
/>
</div>
);
}
};
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
return (
<ul
data-slot='sidebar-menu-sub'
data-sidebar='menu-sub'
className={cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
const SidebarMenuSub = ({
className,
...props
}: React.ComponentProps<'li'>) {
return (
<li
data-slot='sidebar-menu-sub-item'
data-sidebar='menu-sub-item'
className={cn('group/menu-sub-item relative', className)}
{...props}
/>
);
}
}: React.ComponentProps<'ul'>) => (
<ul
data-slot='sidebar-menu-sub'
data-sidebar='menu-sub'
className={cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
function SidebarMenuSubButton({
const SidebarMenuSubItem = ({
className,
...props
}: React.ComponentProps<'li'>) => (
<li
data-slot='sidebar-menu-sub-item'
data-sidebar='menu-sub-item'
className={cn('group/menu-sub-item relative', className)}
{...props}
/>
);
const SidebarMenuSubButton = ({
asChild = false,
size = 'md',
isActive = false,
@@ -675,7 +670,7 @@ function SidebarMenuSubButton({
asChild?: boolean;
size?: 'sm' | 'md';
isActive?: boolean;
}) {
}) => {
const Comp = asChild ? Slot.Root : 'a';
return (
@@ -695,7 +690,7 @@ function SidebarMenuSubButton({
{...props}
/>
);
}
};
export {
Sidebar,

View File

@@ -1,13 +1,11 @@
import { cn } from '@gib/ui';
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='skeleton'
className={cn('bg-accent animate-pulse rounded-md', className)}
{...props}
/>
);
}
const Skeleton = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot='skeleton'
className={cn('bg-accent animate-pulse rounded-md', className)}
{...props}
/>
);
export { Skeleton };

View File

@@ -5,14 +5,14 @@ import { Slider as SliderPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Slider({
const Slider = ({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
}: React.ComponentProps<typeof SliderPrimitive.Root>) => {
const _values = React.useMemo(
() =>
Array.isArray(value)
@@ -58,6 +58,6 @@ function Slider({
))}
</SliderPrimitive.Root>
);
}
};
export { Slider };

View File

@@ -2,15 +2,13 @@ import { Loader2Icon } from 'lucide-react';
import { cn } from '@gib/ui';
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<Loader2Icon
role='status'
aria-label='Loading'
className={cn('size-4 animate-spin', className)}
{...props}
/>
);
}
const Spinner = ({ className, ...props }: React.ComponentProps<'svg'>) => (
<Loader2Icon
role='status'
aria-label='Loading'
className={cn('size-4 animate-spin', className)}
{...props}
/>
);
export { Spinner };

View File

@@ -5,29 +5,27 @@ import { Switch as SwitchPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Switch({
const Switch = ({
className,
size = 'default',
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: 'sm' | 'default';
}) {
return (
<SwitchPrimitive.Root
data-slot='switch'
data-size={size}
className={cn(
'data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 aria-invalid:ring-3 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]',
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot='switch-thumb'
className='bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0'
/>
</SwitchPrimitive.Root>
);
}
}) => (
<SwitchPrimitive.Root
data-slot='switch'
data-size={size}
className={cn(
'data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 aria-invalid:ring-3 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]',
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot='switch-thumb'
className='bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0'
/>
</SwitchPrimitive.Root>
);
export { Switch };

View File

@@ -4,105 +4,92 @@ import type * as React from 'react';
import { cn } from '@gib/ui';
function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
<div
data-slot='table-container'
className='relative w-full overflow-x-auto'
>
<table
data-slot='table'
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return (
<thead
data-slot='table-header'
className={cn('[&_tr]:border-b', className)}
const Table = ({ className, ...props }: React.ComponentProps<'table'>) => (
<div data-slot='table-container' className='relative w-full overflow-x-auto'>
<table
data-slot='table'
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
);
}
</div>
);
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return (
<tbody
data-slot='table-body'
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return (
<tfoot
data-slot='table-footer'
className={cn(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return (
<tr
data-slot='table-row'
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
<th
data-slot='table-head'
className={cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return (
<td
data-slot='table-cell'
className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
);
}
function TableCaption({
const TableHeader = ({
className,
...props
}: React.ComponentProps<'caption'>) {
return (
<caption
data-slot='table-caption'
className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props}
/>
);
}
}: React.ComponentProps<'thead'>) => (
<thead
data-slot='table-header'
className={cn('[&_tr]:border-b', className)}
{...props}
/>
);
const TableBody = ({ className, ...props }: React.ComponentProps<'tbody'>) => (
<tbody
data-slot='table-body'
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
);
const TableFooter = ({
className,
...props
}: React.ComponentProps<'tfoot'>) => (
<tfoot
data-slot='table-footer'
className={cn(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
);
const TableRow = ({ className, ...props }: React.ComponentProps<'tr'>) => (
<tr
data-slot='table-row'
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className,
)}
{...props}
/>
);
const TableHead = ({ className, ...props }: React.ComponentProps<'th'>) => (
<th
data-slot='table-head'
className={cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
);
const TableCell = ({ className, ...props }: React.ComponentProps<'td'>) => (
<td
data-slot='table-cell'
className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
);
const TableCaption = ({
className,
...props
}: React.ComponentProps<'caption'>) => (
<caption
data-slot='table-caption'
className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props}
/>
);
export {
Table,

View File

@@ -7,23 +7,18 @@ import { Tabs as TabsPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function Tabs({
const Tabs = ({
className,
orientation = 'horizontal',
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot='tabs'
data-orientation={orientation}
className={cn(
'group/tabs flex gap-2 data-horizontal:flex-col',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof TabsPrimitive.Root>) => (
<TabsPrimitive.Root
data-slot='tabs'
data-orientation={orientation}
className={cn('group/tabs flex gap-2 data-horizontal:flex-col', className)}
{...props}
/>
);
const tabsListVariants = cva(
'group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-[3px] group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
@@ -40,52 +35,46 @@ const tabsListVariants = cva(
},
);
function TabsList({
const TabsList = ({
className,
variant = 'default',
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot='tabs-list'
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
VariantProps<typeof tabsListVariants>) => (
<TabsPrimitive.List
data-slot='tabs-list'
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
function TabsTrigger({
const TabsTrigger = ({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot='tabs-trigger'
className={cn(
"text-foreground/60 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
className,
)}
{...props}
/>
);
}
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) => (
<TabsPrimitive.Trigger
data-slot='tabs-trigger'
className={cn(
"text-foreground/60 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
className,
)}
{...props}
/>
);
function TabsContent({
const TabsContent = ({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot='tabs-content'
className={cn('flex-1 text-sm outline-none', className)}
{...props}
/>
);
}
}: React.ComponentProps<typeof TabsPrimitive.Content>) => (
<TabsPrimitive.Content
data-slot='tabs-content'
className={cn('flex-1 text-sm outline-none', className)}
{...props}
/>
);
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

View File

@@ -2,17 +2,18 @@ import * as React from 'react';
import { cn } from '@gib/ui';
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot='textarea'
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
{...props}
/>
);
}
const Textarea = ({
className,
...props
}: React.ComponentProps<'textarea'>) => (
<textarea
data-slot='textarea'
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
{...props}
/>
);
export { Textarea };

View File

@@ -1,16 +1,16 @@
'use client';
import type { ComponentProps } from 'react';
import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes';
import { Moon, Sun } from 'lucide-react';
import { Button, cn } from '@gib/ui';
import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes';
import { Button, cn } from '@gib/ui';
const ThemeProvider = ({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) => {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
};
interface ThemeToggleProps {
@@ -22,23 +22,21 @@ const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
const { setTheme, resolvedTheme } = useTheme();
return (
<Button
variant="outline"
size="icon"
variant='outline'
size='icon'
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
{...buttonProps}
className={cn('cursor-pointer', buttonProps?.className)}
>
<Sun
style={{ height: `${size}rem`, width: `${size}rem` }}
className='scale-100 rotate-0 transition-all
dark:scale-0 dark:-rotate-90'
className='scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90'
/>
<Moon
style={{ height: `${size}rem`, width: `${size}rem` }}
className='absolute scale-0 rotate-90 transition-all
dark:scale-100 dark:rotate-0'
className='absolute scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0'
/>
<span className="sr-only">Toggle theme</span>
<span className='sr-only'>Toggle theme</span>
</Button>
);
};

View File

@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
spacing: 0,
});
function ToggleGroup({
const ToggleGroup = ({
className,
variant,
size,
@@ -26,35 +26,33 @@ function ToggleGroup({
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & {
spacing?: number;
}) {
return (
<ToggleGroupPrimitive.Root
data-slot='toggle-group'
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ '--gap': spacing } as React.CSSProperties}
className={cn(
'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
}) => (
<ToggleGroupPrimitive.Root
data-slot='toggle-group'
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ '--gap': spacing } as React.CSSProperties}
className={cn(
'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
function ToggleGroupItem({
const ToggleGroupItem = ({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
VariantProps<typeof toggleVariants>) => {
const context = React.useContext(ToggleGroupContext);
return (
@@ -77,6 +75,6 @@ function ToggleGroupItem({
{children}
</ToggleGroupPrimitive.Item>
);
}
};
export { ToggleGroup, ToggleGroupItem };

View File

@@ -29,20 +29,18 @@ const toggleVariants = cva(
},
);
function Toggle({
const Toggle = ({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot='toggle'
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
VariantProps<typeof toggleVariants>) => (
<TogglePrimitive.Root
data-slot='toggle'
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
export { Toggle, toggleVariants };

View File

@@ -5,53 +5,49 @@ import { Tooltip as TooltipPrimitive } from 'radix-ui';
import { cn } from '@gib/ui';
function TooltipProvider({
const TooltipProvider = ({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot='tooltip-provider'
delayDuration={delayDuration}
{...props}
/>
);
}
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) => (
<TooltipPrimitive.Provider
data-slot='tooltip-provider'
delayDuration={delayDuration}
{...props}
/>
);
function Tooltip({
const Tooltip = ({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot='tooltip' {...props} />;
}
}: React.ComponentProps<typeof TooltipPrimitive.Root>) => (
<TooltipPrimitive.Root data-slot='tooltip' {...props} />
);
function TooltipTrigger({
const TooltipTrigger = ({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot='tooltip-trigger' {...props} />;
}
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) => (
<TooltipPrimitive.Trigger data-slot='tooltip-trigger' {...props} />
);
function TooltipContent({
const TooltipContent = ({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot='tooltip-content'
sideOffset={sideOffset}
className={cn(
'animate-in bg-foreground text-background fade-in-0 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className='bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]' />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
}: React.ComponentProps<typeof TooltipPrimitive.Content>) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot='tooltip-content'
sideOffset={sideOffset}
className={cn(
'animate-in bg-foreground text-background fade-in-0 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className='bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]' />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -1 +1 @@
[["1","2","3","4","5"],{"key":"6","value":"7"},{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/package.json",{"size":979,"mtime":1768166330000,"hash":"16","data":"17"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/nextjs.ts",{"size":440,"mtime":1768155639000,"hash":"18","data":"19"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/react.ts",{"size":592,"mtime":1768155639000,"hash":"20","data":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/base.ts",{"size":2508,"mtime":1768320541000,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"a5326aca75246da261fd2e354257b45a",{"hashOfOptions":"26"},"25c52c46972131dcc296288599ff108d",{"hashOfOptions":"27"},"2292935ede6baf909f6a0c61486e15da",{"hashOfOptions":"28"},"6a779439826cf31b5561a21273d134a9",{"hashOfOptions":"29"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"30"},"1249441784","4234667885","3945480854","3486090744","681484145"]
[["1","2","3","4","5","6"],{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},{"key":"13","value":"14"},{"key":"15","value":"16"},{"key":"17","value":"18"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/base.ts",{"size":2963,"mtime":1774544629518,"hash":"19","data":"20"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/nextjs.ts",{"size":440,"mtime":1768155639000,"hash":"21","data":"22"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/.cache/.prettiercache",{"size":1278,"mtime":1774544484642},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/package.json",{"size":1033,"mtime":1774544387169,"hash":"23","data":"24"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/react.ts",{"size":592,"mtime":1768155639000,"hash":"25","data":"26"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"27","data":"28"},"6a779439826cf31b5561a21273d134a9",{"hashOfOptions":"29"},"25c52c46972131dcc296288599ff108d",{"hashOfOptions":"30"},"a5326aca75246da261fd2e354257b45a",{"hashOfOptions":"31"},"2292935ede6baf909f6a0c61486e15da",{"hashOfOptions":"32"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"33"},"1686097143","2347540204","302976953","3406150487","1582266352"]

View File

@@ -2,10 +2,15 @@ import * as path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import eslint from '@eslint/js';
import importPlugin from 'eslint-plugin-import';
// eslint-plugin-prefer-arrow-functions doesn't ship flat config types — cast needed
import preferArrowFunctions from 'eslint-plugin-prefer-arrow-functions';
import turboPlugin from 'eslint-plugin-turbo';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preferArrowPlugin = preferArrowFunctions as any;
/**
* All packages that leverage t3-env should use this rule
*/
@@ -45,6 +50,7 @@ export const baseConfig = defineConfig(
plugins: {
import: importPlugin,
turbo: turboPlugin,
'prefer-arrow-functions': preferArrowPlugin,
},
extends: [
eslint.configs.recommended,
@@ -58,10 +64,6 @@ export const baseConfig = defineConfig(
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': [
'warn',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
'@typescript-eslint/no-misused-promises': [
2,
{ checksVoidReturn: { attributes: false } },
@@ -73,7 +75,17 @@ export const baseConfig = defineConfig(
},
],
'@typescript-eslint/no-non-null-assertion': 'error',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'@typescript-eslint/consistent-type-definitions': 'off',
'prefer-arrow-functions/prefer-arrow-functions': [
'warn',
{
allowNamedFunctions: false,
classPropertiesAllowed: false,
disallowPrototype: false,
returnStyle: 'unchanged',
singleReturnOnly: false,
},
],
},
},
{

View File

@@ -18,6 +18,7 @@
"@next/eslint-plugin-next": "^16.0.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prefer-arrow-functions": "^3.9.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-turbo": "^2.5.8",

View File

@@ -1 +1 @@
[["1","2","3"],{"key":"4","value":"5"},{"key":"6","value":"7"},{"key":"8","value":"9"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"10","data":"11"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/package.json",{"size":607,"mtime":1766222924000,"hash":"12","data":"13"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/index.js",{"size":1194,"mtime":1768372320442,"hash":"14","data":"15"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"16"},"11b634ce56ac720ac9a2860d77fbd2cc",{"hashOfOptions":"17"},"ecbaa91166a940dfcec8117059f52402",{"hashOfOptions":"18"},"67859251","550572982","1674623979"]
[["1","2","3","4"],{"key":"5","value":"6"},{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/.cache/.prettiercache",{"size":832,"mtime":1774544484482},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/index.js",{"size":1194,"mtime":1768372320442,"hash":"13","data":"14"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/package.json",{"size":607,"mtime":1774032385569,"hash":"15","data":"16"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"17","data":"18"},"ecbaa91166a940dfcec8117059f52402",{"hashOfOptions":"19"},"11b634ce56ac720ac9a2860d77fbd2cc",{"hashOfOptions":"20"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"21"},"1828250668","802511607","4250532914"]

View File

@@ -0,0 +1 @@
[{"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js":"1"},{"size":70,"mtime":1768155639000,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","suppressedMessages":"6","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"z9il2c","/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",[],[]]

View File

@@ -1 +1 @@
[["1","2","3","4","5"],{"key":"6","value":"7"},{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/package.json",{"size":851,"mtime":1766222924000,"hash":"16","data":"17"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768155639000,"hash":"18","data":"19"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768155639000,"hash":"20","data":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/theme.css",{"size":7273,"mtime":1768320378000,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"0d22e47f57739db9de04c6f8420d6fb5",{"hashOfOptions":"26"},"9a944fbda06979be39571bd9bd00b0d9",{"hashOfOptions":"27"},"b8fec960cb32340eea62ca1485093e68",{"hashOfOptions":"28"},"e40c2569ef375a9c828c80c0f9ce1bf2",{"hashOfOptions":"29"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"30"},"34296497","1053475182","4101125807","919761249","2958815992"]
[["1","2","3","4","5","6","7"],{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},{"key":"16","value":"17"},{"key":"18","value":"19"},{"key":"20","value":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.eslintcache",{"size":396,"mtime":1774544641255},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.prettiercache",{"size":1450,"mtime":1774544484418},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/package.json",{"size":851,"mtime":1774032407411,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768155639000,"hash":"26","data":"27"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/theme.css",{"size":7273,"mtime":1768320378000,"hash":"28","data":"29"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768155639000,"hash":"30","data":"31"},"0d22e47f57739db9de04c6f8420d6fb5",{"hashOfOptions":"32"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"33"},"b8fec960cb32340eea62ca1485093e68",{"hashOfOptions":"34"},"e40c2569ef375a9c828c80c0f9ce1bf2",{"hashOfOptions":"35"},"9a944fbda06979be39571bd9bd00b0d9",{"hashOfOptions":"36"},"286235122","2846522359","1288936688","2683656544","1694755693"]

View File

@@ -15,13 +15,13 @@
"CONVEX_SELF_HOSTED_ADMIN_KEY",
"CONVEX_SITE_URL",
"USESEND_API_KEY",
"USESEND_URL",
"USESEND_FROM_EMAIL",
"AUTH_AUTHENTIK_ID",
"AUTH_AUTHENTIK_SECRET",
"AUTH_AUTHENTIK_ISSUER"
],
"globalPassThroughEnv": [
"NODE_ENV"
],
"globalPassThroughEnv": ["NODE_ENV"],
"ui": "tui",
"tasks": {
"topo": {

View File

@@ -1,5 +1,5 @@
import { execSync } from "node:child_process";
import type { PlopTypes } from "@turbo/gen";
import { execSync } from 'node:child_process';
import type { PlopTypes } from '@turbo/gen';
interface PackageJson {
name: string;
@@ -9,58 +9,58 @@ interface PackageJson {
}
export default function generator(plop: PlopTypes.NodePlopAPI): void {
plop.setGenerator("init", {
description: "Generate a new package for the Acme Monorepo",
plop.setGenerator('init', {
description: 'Generate a new package for the Monorepo',
prompts: [
{
type: "input",
name: "name",
type: 'input',
name: 'name',
message:
"What is the name of the package? (You can skip the `@gib/` prefix)",
'What is the name of the package? (You can skip the `@gib/` prefix)',
},
{
type: "input",
name: "deps",
type: 'input',
name: 'deps',
message:
"Enter a space separated list of dependencies you would like to install",
'Enter a space separated list of dependencies you would like to install',
},
],
actions: [
(answers) => {
if ("name" in answers && typeof answers.name === "string") {
if (answers.name.startsWith("@gib/")) {
answers.name = answers.name.replace("@gib/", "");
if ('name' in answers && typeof answers.name === 'string') {
if (answers.name.startsWith('@gib/')) {
answers.name = answers.name.replace('@gib/', '');
}
}
return "Config sanitized";
return 'Config sanitized';
},
{
type: "add",
path: "packages/{{ name }}/eslint.config.ts",
templateFile: "templates/eslint.config.ts.hbs",
type: 'add',
path: 'packages/{{ name }}/eslint.config.ts',
templateFile: 'templates/eslint.config.ts.hbs',
},
{
type: "add",
path: "packages/{{ name }}/package.json",
templateFile: "templates/package.json.hbs",
type: 'add',
path: 'packages/{{ name }}/package.json',
templateFile: 'templates/package.json.hbs',
},
{
type: "add",
path: "packages/{{ name }}/tsconfig.json",
templateFile: "templates/tsconfig.json.hbs",
type: 'add',
path: 'packages/{{ name }}/tsconfig.json',
templateFile: 'templates/tsconfig.json.hbs',
},
{
type: "add",
path: "packages/{{ name }}/src/index.ts",
type: 'add',
path: 'packages/{{ name }}/src/index.ts',
template: "export const name = '{{ name }}';",
},
{
type: "modify",
path: "packages/{{ name }}/package.json",
type: 'modify',
path: 'packages/{{ name }}/package.json',
async transform(content, answers) {
if ("deps" in answers && typeof answers.deps === "string") {
if ('deps' in answers && typeof answers.deps === 'string') {
const pkg = JSON.parse(content) as PackageJson;
for (const dep of answers.deps.split(" ").filter(Boolean)) {
for (const dep of answers.deps.split(' ').filter(Boolean)) {
const version = await fetch(
`https://registry.npmjs.org/-/package/${dep}/dist-tags`,
)
@@ -78,17 +78,15 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void {
/**
* Install deps and format everything
*/
if ("name" in answers && typeof answers.name === "string") {
// execSync("pnpm dlx sherif@latest --fix", {
// stdio: "inherit",
// });
execSync("pnpm i", { stdio: "inherit" });
if ('name' in answers && typeof answers.name === 'string') {
execSync('bun install', { stdio: 'inherit' });
execSync(
`pnpm prettier --write packages/${answers.name}/** --list-different`,
`bun prettier --write packages/${answers.name}/** --list-different`,
{ stdio: 'inherit' },
);
return "Package scaffolded";
return 'Package scaffolded';
}
return "Package not scaffolded";
return 'Package not scaffolded';
},
],
});