Compare commits
16 Commits
d2eea9880a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 01cdd5ab4f | |||
| ff8c39dc75 | |||
| 56fe2a2af3 | |||
| d16f4287ce | |||
| 0bc04dbf6b | |||
| 6e78140103 | |||
| 8c62780dcb | |||
| 2d0a34347b | |||
| 5e37d10300 | |||
| 1e61e34fb8 | |||
| 07dc8d7976 | |||
| ee99ab11c9 | |||
| 0ecf6238de | |||
| 60dc57ddf7 | |||
| a8bb610be7 | |||
| 81e6a5aaa6 |
@@ -18,8 +18,9 @@ out
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
*.log
|
*.log
|
||||||
.env.local
|
#.env
|
||||||
.env*.local
|
#.env.*
|
||||||
|
!.env.example
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
@@ -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_URL=https://api.convex.example.com # convex-backend:3210
|
||||||
CONVEX_SELF_HOSTED_ADMIN_KEY= # Generate after hosted on docker
|
CONVEX_SELF_HOSTED_ADMIN_KEY= # Generate after hosted on docker
|
||||||
# Convex Auth
|
# 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_API_KEY=
|
||||||
|
USESEND_URL=https://usesend.example.com
|
||||||
|
USESEND_FROM_EMAIL=My App <noreply@example.com>
|
||||||
AUTH_AUTHENTIK_ID=
|
AUTH_AUTHENTIK_ID=
|
||||||
AUTH_AUTHENTIK_SECRET=
|
AUTH_AUTHENTIK_SECRET=
|
||||||
AUTH_AUTHENTIK_ISSUER=
|
AUTH_AUTHENTIK_ISSUER=
|
||||||
|
|||||||
287
README.md
287
README.md
@@ -32,15 +32,21 @@ A production-ready Turborepo starter with Next.js, Expo, and self-hosted Convex
|
|||||||
|
|
||||||
## Getting Started
|
## 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
|
### 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)
|
- [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
|
```bash
|
||||||
git clone https://git.gbrown.org/gib/convex-monorepo
|
git clone https://git.gbrown.org/gib/convex-monorepo
|
||||||
@@ -48,88 +54,152 @@ cd convex-monorepo
|
|||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Configure Environment Variables
|
If you're using this as a template for a new project, remove the existing remote and
|
||||||
|
add your own:
|
||||||
Create a `.env` file in the project root with the following variables:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Convex Backend (Self-Hosted)
|
git remote remove origin
|
||||||
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
|
git remote add origin https://your-git-host.com/your/new-repo.git
|
||||||
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=
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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
|
```bash
|
||||||
cd docker/
|
cd docker/
|
||||||
cp .env.example .env
|
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
|
```bash
|
||||||
cd docker/
|
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
|
```bash
|
||||||
- **Dashboard:** http://localhost:6791
|
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
|
```bash
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
bun run scripts/generateKeys.mjs
|
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
|
```bash
|
||||||
cd packages/backend
|
# From packages/backend/
|
||||||
bun with-env npx convex env set AUTH_AUTHENTIK_ID "your-value"
|
bun with-env npx convex env set JWT_PRIVATE_KEY "your-private-key"
|
||||||
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "your-value"
|
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 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 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
|
```bash
|
||||||
# From project root
|
# From project root
|
||||||
bun dev:next # Next.js app + Convex backend
|
bun dev:next # Next.js app + Convex backend (most common)
|
||||||
# or
|
# or
|
||||||
bun dev # All apps (Next.js + Expo + Backend)
|
bun dev # All apps (Next.js + Expo + Backend)
|
||||||
```
|
```
|
||||||
@@ -137,7 +207,7 @@ bun dev # All apps (Next.js + Expo + Backend)
|
|||||||
**App URLs:**
|
**App URLs:**
|
||||||
|
|
||||||
- **Next.js:** http://localhost:3000
|
- **Next.js:** http://localhost:3000
|
||||||
- **Convex Dashboard:** http://localhost:6791
|
- **Convex Dashboard:** https://dashboard.convex.example.com
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -193,7 +263,7 @@ convex-monorepo/
|
|||||||
│
|
│
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── backend/ # Convex backend
|
│ ├── backend/ # Convex backend
|
||||||
│ │ ├── convex/ # Convex functions (synced to cloud)
|
│ │ ├── convex/ # Convex functions (synced to deployment)
|
||||||
│ │ ├── scripts/ # Utilities (generateKeys.mjs)
|
│ │ ├── scripts/ # Utilities (generateKeys.mjs)
|
||||||
│ │ └── types/ # Shared types
|
│ │ └── types/ # Shared types
|
||||||
│ └── ui/ # shadcn/ui components
|
│ └── ui/ # shadcn/ui components
|
||||||
@@ -223,16 +293,16 @@ convex-monorepo/
|
|||||||
- **OAuth:** Authentik SSO integration
|
- **OAuth:** Authentik SSO integration
|
||||||
- **Password:** Custom password auth with email verification
|
- **Password:** Custom password auth with email verification
|
||||||
- **OTP:** Email verification via self-hosted UseSend
|
- **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
|
### Next.js App
|
||||||
|
|
||||||
- **App Router:** Next.js 16 with React Server Components
|
- **App Router:** Next.js 16 with React Server Components
|
||||||
- **Data Preloading:** Server-side data fetching with Convex
|
- **Data Preloading:** SSR data fetching with `preloadQuery` + `usePreloadedQuery`
|
||||||
- **Middleware:** Route protection & authentication
|
- **Middleware:** Route protection & IP-based security (`src/proxy.ts`)
|
||||||
- **Styling:** Tailwind CSS v4 with dark mode
|
- **Styling:** Tailwind CSS v4 with dark mode (OKLCH-based theme)
|
||||||
- **Analytics:** Plausible (privacy-focused)
|
- **Analytics:** Plausible (privacy-focused, proxied through Next.js)
|
||||||
- **Monitoring:** Sentry error tracking
|
- **Monitoring:** Sentry error tracking & performance
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
@@ -244,87 +314,113 @@ convex-monorepo/
|
|||||||
|
|
||||||
### Developer Experience
|
### Developer Experience
|
||||||
|
|
||||||
- **Monorepo:** Turborepo for efficient builds
|
- **Monorepo:** Turborepo for efficient builds and caching
|
||||||
- **Type Safety:** Strict TypeScript throughout
|
- **Type Safety:** Strict TypeScript throughout
|
||||||
- **Code Quality:** ESLint + Prettier with auto-fix
|
- **Code Quality:** ESLint + Prettier with auto-fix
|
||||||
- **Hot Reload:** Fast refresh for all packages
|
- **Hot Reload:** Fast refresh for all packages
|
||||||
- **Catalog Deps:** Centralized version management
|
- **Catalog Deps:** Centralized dependency version management
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment
|
## 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
|
```bash
|
||||||
cd docker/
|
cd docker/
|
||||||
|
sudo docker compose up -d convex-backend convex-dashboard
|
||||||
# Start all services
|
# Wait for backend health check to pass, then:
|
||||||
docker compose up -d
|
sudo docker compose up -d next-app
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
# Stop services
|
|
||||||
docker compose down
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Services:**
|
**Services:**
|
||||||
|
|
||||||
- `next-app` - Next.js standalone build
|
- `next-app` — Next.js standalone build
|
||||||
- `convex-backend` - Convex backend (port 3210)
|
- `convex-backend` — Convex backend (port 3210)
|
||||||
- `convex-dashboard` - Admin dashboard (port 6791)
|
- `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
|
### Production Checklist
|
||||||
|
|
||||||
- [ ] Update environment variables in `docker/.env`
|
- [ ] Fill out `docker/.env` with your domain names and secrets
|
||||||
- [ ] Generate `CONVEX_SELF_HOSTED_ADMIN_KEY`
|
- [ ] Start `convex-backend` and `convex-dashboard` containers
|
||||||
- [ ] Configure reverse proxy (Nginx/Traefik)
|
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./generate_convex_admin_key`
|
||||||
- [ ] Set up SSL certificates
|
- [ ] Reverse-proxy both Convex services via nginx-proxy-manager with SSL
|
||||||
- [ ] Sync auth environment variables to Convex
|
- [ ] Fill out root `/.env` with all environment variables
|
||||||
- [ ] Configure backup strategy for `docker/data/`
|
- [ ] Generate JWT keys and sync all env vars to Convex (`bun with-env npx convex env set ...`)
|
||||||
- [ ] Test authentication flow
|
- [ ] Update `CONVEX_SITE_URL` in the Convex Dashboard to your production Next.js URL
|
||||||
- [ ] Enable Sentry error tracking
|
- [ ] Build and start the `next-app` container
|
||||||
|
- [ ] Back up `docker/data/` regularly (contains all Convex database data)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **[AGENTS.md](./AGENTS.md)** - Comprehensive guide for AI agents & developers
|
- **[AGENTS.md](./AGENTS.md)** — Comprehensive guide for AI agents & developers
|
||||||
- **[Convex Docs](https://docs.convex.dev)** - Official Convex documentation
|
- **[Convex Docs](https://docs.convex.dev)** — Official Convex documentation
|
||||||
- **[Turborepo Docs](https://turbo.build/repo/docs)** - Turborepo documentation
|
- **[Turborepo Docs](https://turbo.build/repo/docs)** — Turborepo documentation
|
||||||
- **[Next.js Docs](https://nextjs.org/docs)** - Next.js documentation
|
- **[Next.js Docs](https://nextjs.org/docs)** — Next.js documentation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## 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
|
```typescript
|
||||||
// ✅ Correct
|
// ✅ Correct
|
||||||
|
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
|
||||||
// ❌ Wrong
|
// ❌ Wrong — will fail at runtime
|
||||||
import { api } from '@gib/backend/convex/_generated/api';
|
import { api } from '@gib/backend/convex/_generated/api';
|
||||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker containers won't start
|
### 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`
|
2. Verify environment variables in `docker/.env`
|
||||||
3. Ensure ports 3210 and 6791 are available
|
3. Ensure the `nginx-bridge` network exists: `sudo docker network create nginx-bridge`
|
||||||
4. Check network configuration (`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
|
- Single quotes, trailing commas
|
||||||
- 80 character line width
|
- 80 character line width
|
||||||
- ESLint + Prettier enforced
|
- ESLint + Prettier enforced
|
||||||
|
- `const fn = () => {}` over `function fn()` (strong preference)
|
||||||
- Import order: Types → React → Next → Third-party → @gib → Local
|
- Import order: Types → React → Next → Third-party → @gib → Local
|
||||||
|
|
||||||
Run `bun lint:fix` and `bun format:fix` before committing.
|
Run `bun lint:fix` and `bun format:fix` before committing.
|
||||||
|
|||||||
@@ -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"],{"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/src/app/index.tsx",{"size":1935,"mtime":1774546215689,"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/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"68"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":65,"mtime":1774546126644,"hash":"69","data":"70"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":678,"mtime":1774546226606,"hash":"71","data":"72"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts",{"size":909,"mtime":1774546187217,"data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":566,"mtime":1774546138669,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":836,"mtime":1774546198917,"hash":"76","data":"77"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":89,"mtime":1774546134256,"hash":"78","data":"79"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.prettiercache",{"size":4929,"mtime":1774544663692},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"80"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2229,"mtime":1774546163623,"hash":"81","data":"82"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":171,"mtime":1774031879321,"hash":"83","data":"84"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":274,"mtime":1774546113649,"hash":"85","data":"86"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"87","data":"88"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639000,"hash":"89","data":"90"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"91","data":"92"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639000,"hash":"93","data":"94"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"95","data":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"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"},"73c235a66242df70b69394cce29d1ed3",{"hashOfOptions":"101"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"102"},"1e8ac0d261e95efb19d290ffcf70ce36","b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"103"},"ead19d73283f9d8e08b55c896c9fd570",{"hashOfOptions":"104"},{"hashOfOptions":"105"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"106"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"107"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"108"},"863da15dbd856008b7c24077ca746d91","d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"109"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"110"},"1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"111"},"6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"112"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"113"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"114"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"115"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"116"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"117"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"118"},"3000879843","3103968608","384110377","141502567","1235541372","1050155876","2025343866","4147067111","4228440757","3451484829","4039211292","3318113268","2585374463","45764855","1418614640","2754339483","1950506033","3468872477"]
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"base": {
|
"base": {
|
||||||
"node": "22.12.0",
|
"node": "22.12.0",
|
||||||
"pnpm": "9.15.4",
|
"bun": "1.3.10",
|
||||||
"ios": {
|
"ios": {
|
||||||
"resourceClass": "m-medium"
|
"resourceClass": "m-medium"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { baseConfig } from '@acme/eslint-config/base';
|
|
||||||
import { reactConfig } from '@acme/eslint-config/react';
|
|
||||||
import { defineConfig } from 'eslint/config';
|
import { defineConfig } from 'eslint/config';
|
||||||
|
|
||||||
|
import { baseConfig } from '@gib/eslint-config/base';
|
||||||
|
import { reactConfig } from '@gib/eslint-config/react';
|
||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
{
|
{
|
||||||
ignores: ['.expo/**', 'expo-plugins/**'],
|
ignores: ['.expo/**', 'expo-plugins/**'],
|
||||||
|
|||||||
@@ -49,8 +49,7 @@
|
|||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.2",
|
"react-native-web": "~0.21.2",
|
||||||
"react-native-worklets": "~0.5.2",
|
"react-native-worklets": "~0.5.2"
|
||||||
"superjson": "2.2.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gib/eslint-config": "workspace:*",
|
"@gib/eslint-config": "workspace:*",
|
||||||
@@ -65,4 +64,3 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@gib/prettier-config"
|
"prettier": "@gib/prettier-config"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = require('@acme/tailwind-config/postcss-config');
|
module.exports = require('@gib/tailwind-config/postcss-config');
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
import { useColorScheme } from 'react-native';
|
import { useColorScheme } from 'react-native';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { ConvexAuthProvider } from '@convex-dev/auth/react';
|
||||||
|
|
||||||
import { queryClient } from '~/utils/api';
|
import { convex } from '~/utils/convex';
|
||||||
|
|
||||||
import '../styles.css';
|
import '../styles.css';
|
||||||
|
|
||||||
// This is the main layout of the app
|
const RootLayout = () => {
|
||||||
// It wraps your pages with the providers they need
|
|
||||||
export default function RootLayout() {
|
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<ConvexAuthProvider client={convex}>
|
||||||
{/*
|
|
||||||
The Stack component displays the current page.
|
|
||||||
It also allows you to configure your screens
|
|
||||||
*/}
|
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: '#c03484',
|
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
|
||||||
},
|
},
|
||||||
|
headerTintColor: colorScheme === 'dark' ? '#fafaf9' : '#1c1917',
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colorScheme == 'dark' ? '#09090B' : '#FFFFFF',
|
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<StatusBar />
|
<StatusBar style='auto' />
|
||||||
</QueryClientProvider>
|
</ConvexAuthProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default RootLayout;
|
||||||
|
|||||||
@@ -1,172 +1,54 @@
|
|||||||
import { useState } from 'react';
|
import { Pressable, Text, View } from 'react-native';
|
||||||
import { Pressable, Text, TextInput, View } from 'react-native';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Link, Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { LegendList } from '@legendapp/list';
|
import { useAuthActions } from '@convex-dev/auth/react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useConvexAuth, useQuery } from 'convex/react';
|
||||||
|
|
||||||
import type { RouterOutputs } from '~/utils/api';
|
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||||
import { trpc } from '~/utils/api';
|
|
||||||
import { authClient } from '~/utils/auth';
|
const Index = () => {
|
||||||
|
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||||
|
const { signOut } = useAuthActions();
|
||||||
|
const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
|
||||||
|
|
||||||
function PostCard(props: {
|
|
||||||
post: RouterOutputs['post']['all'][number];
|
|
||||||
onDelete: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<View className='bg-muted flex flex-row rounded-lg p-4'>
|
<SafeAreaView className='bg-background flex-1'>
|
||||||
<View className='grow'>
|
<Stack.Screen options={{ title: 'Home' }} />
|
||||||
<Link
|
<View className='flex-1 items-center justify-center gap-4 p-6'>
|
||||||
asChild
|
<Text className='text-foreground text-4xl font-bold'>
|
||||||
href={{
|
Convex Monorepo
|
||||||
pathname: '/post/[id]',
|
</Text>
|
||||||
params: { id: props.post.id },
|
<Text className='text-muted-foreground text-center text-base'>
|
||||||
}}
|
Your self-hosted Expo + Convex starter
|
||||||
>
|
</Text>
|
||||||
<Pressable className=''>
|
|
||||||
<Text className='text-primary text-xl font-semibold'>
|
{isLoading ? (
|
||||||
{props.post.title}
|
<Text className='text-muted-foreground'>Loading...</Text>
|
||||||
|
) : isAuthenticated ? (
|
||||||
|
<View className='w-full gap-3'>
|
||||||
|
<Text className='text-foreground text-center text-lg'>
|
||||||
|
Welcome{user?.name ? `, ${user.name}` : ''}!
|
||||||
</Text>
|
</Text>
|
||||||
<Text className='text-foreground mt-2'>{props.post.content}</Text>
|
<Pressable
|
||||||
</Pressable>
|
className='bg-primary items-center rounded-md p-3'
|
||||||
</Link>
|
onPress={() => void signOut()}
|
||||||
</View>
|
>
|
||||||
<Pressable onPress={props.onDelete}>
|
<Text className='text-primary-foreground font-semibold'>
|
||||||
<Text className='text-primary font-bold uppercase'>Delete</Text>
|
Sign Out
|
||||||
</Pressable>
|
</Text>
|
||||||
</View>
|
</Pressable>
|
||||||
);
|
</View>
|
||||||
}
|
) : (
|
||||||
|
<View className='w-full gap-3'>
|
||||||
function CreatePost() {
|
<Text className='text-muted-foreground text-center'>
|
||||||
const queryClient = useQueryClient();
|
Sign in to get started
|
||||||
|
</Text>
|
||||||
const [title, setTitle] = useState('');
|
{/* Add sign-in UI here — see apps/next/src/app/(auth)/sign-in for patterns */}
|
||||||
const [content, setContent] = useState('');
|
</View>
|
||||||
|
)}
|
||||||
const { mutate, error } = useMutation(
|
|
||||||
trpc.post.create.mutationOptions({
|
|
||||||
async onSuccess() {
|
|
||||||
setTitle('');
|
|
||||||
setContent('');
|
|
||||||
await queryClient.invalidateQueries(trpc.post.all.queryFilter());
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='mt-4 flex gap-2'>
|
|
||||||
<TextInput
|
|
||||||
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
|
|
||||||
value={title}
|
|
||||||
onChangeText={setTitle}
|
|
||||||
placeholder='Title'
|
|
||||||
/>
|
|
||||||
{error?.data?.zodError?.fieldErrors.title && (
|
|
||||||
<Text className='text-destructive mb-2'>
|
|
||||||
{error.data.zodError.fieldErrors.title}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<TextInput
|
|
||||||
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
|
|
||||||
value={content}
|
|
||||||
onChangeText={setContent}
|
|
||||||
placeholder='Content'
|
|
||||||
/>
|
|
||||||
{error?.data?.zodError?.fieldErrors.content && (
|
|
||||||
<Text className='text-destructive mb-2'>
|
|
||||||
{error.data.zodError.fieldErrors.content}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Pressable
|
|
||||||
className='bg-primary flex items-center rounded-sm p-2'
|
|
||||||
onPress={() => {
|
|
||||||
mutate({
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className='text-foreground'>Create</Text>
|
|
||||||
</Pressable>
|
|
||||||
{error?.data?.code === 'UNAUTHORIZED' && (
|
|
||||||
<Text className='text-destructive mt-2'>
|
|
||||||
You need to be logged in to create a post
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MobileAuth() {
|
|
||||||
const { data: session } = authClient.useSession();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text className='text-foreground pb-2 text-center text-xl font-semibold'>
|
|
||||||
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
|
|
||||||
</Text>
|
|
||||||
<Pressable
|
|
||||||
onPress={() =>
|
|
||||||
session
|
|
||||||
? authClient.signOut()
|
|
||||||
: authClient.signIn.social({
|
|
||||||
provider: 'discord',
|
|
||||||
callbackURL: '/',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className='bg-primary flex items-center rounded-sm p-2'
|
|
||||||
>
|
|
||||||
<Text>{session ? 'Sign Out' : 'Sign In With Discord'}</Text>
|
|
||||||
</Pressable>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const postQuery = useQuery(trpc.post.all.queryOptions());
|
|
||||||
|
|
||||||
const deletePostMutation = useMutation(
|
|
||||||
trpc.post.delete.mutationOptions({
|
|
||||||
onSettled: () =>
|
|
||||||
queryClient.invalidateQueries(trpc.post.all.queryFilter()),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView className='bg-background'>
|
|
||||||
{/* Changes page title visible on the header */}
|
|
||||||
<Stack.Screen options={{ title: 'Home Page' }} />
|
|
||||||
<View className='bg-background h-full w-full p-4'>
|
|
||||||
<Text className='text-foreground pb-2 text-center text-5xl font-bold'>
|
|
||||||
Create <Text className='text-primary'>T3</Text> Turbo
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<MobileAuth />
|
|
||||||
|
|
||||||
<View className='py-2'>
|
|
||||||
<Text className='text-primary font-semibold italic'>
|
|
||||||
Press on a post
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<LegendList
|
|
||||||
data={postQuery.data ?? []}
|
|
||||||
estimatedItemSize={20}
|
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
ItemSeparatorComponent={() => <View className='h-2' />}
|
|
||||||
renderItem={(p) => (
|
|
||||||
<PostCard
|
|
||||||
post={p.item}
|
|
||||||
onDelete={() => deletePostMutation.mutate(p.item.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreatePost />
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { SafeAreaView, Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
import { Stack, useGlobalSearchParams } from 'expo-router';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { Stack, useLocalSearchParams } from 'expo-router';
|
||||||
|
|
||||||
import { trpc } from '~/utils/api';
|
const Post = () => {
|
||||||
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
export default function Post() {
|
|
||||||
const { id } = useGlobalSearchParams<{ id: string }>();
|
|
||||||
const { data } = useQuery(trpc.post.byId.queryOptions({ id }));
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className='bg-background'>
|
<SafeAreaView className='bg-background flex-1'>
|
||||||
<Stack.Screen options={{ title: data.title }} />
|
<Stack.Screen options={{ title: 'Post' }} />
|
||||||
<View className='h-full w-full p-4'>
|
<View className='flex-1 p-4'>
|
||||||
<Text className='text-primary py-2 text-3xl font-bold'>
|
<Text className='text-foreground text-2xl font-bold'>Post {id}</Text>
|
||||||
{data.title}
|
<Text className='text-muted-foreground mt-2'>
|
||||||
|
Implement your post detail screen here using Convex queries.
|
||||||
</Text>
|
</Text>
|
||||||
<Text className='text-foreground py-4'>{data.content}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Post;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'nativewind/theme';
|
@import 'nativewind/theme';
|
||||||
@import '@acme/tailwind-config/theme';
|
@import '@gib/tailwind-config/theme';
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import type { AppRouter } from '@acme/api';
|
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
|
||||||
import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
|
|
||||||
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
|
||||||
import superjson from 'superjson';
|
|
||||||
|
|
||||||
import { authClient } from './auth';
|
|
||||||
import { getBaseUrl } from './base-url';
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A set of typesafe hooks for consuming your API.
|
|
||||||
*/
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: createTRPCClient({
|
|
||||||
links: [
|
|
||||||
loggerLink({
|
|
||||||
enabled: (opts) =>
|
|
||||||
process.env.NODE_ENV === 'development' ||
|
|
||||||
(opts.direction === 'down' && opts.result instanceof Error),
|
|
||||||
colorMode: 'ansi',
|
|
||||||
}),
|
|
||||||
httpBatchLink({
|
|
||||||
transformer: superjson,
|
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
|
||||||
headers() {
|
|
||||||
const headers = new Map<string, string>();
|
|
||||||
headers.set('x-trpc-source', 'expo-react');
|
|
||||||
|
|
||||||
const cookies = authClient.getCookie();
|
|
||||||
if (cookies) {
|
|
||||||
headers.set('Cookie', cookies);
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type { RouterInputs, RouterOutputs } from '@acme/api';
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import * as SecureStore from 'expo-secure-store';
|
|
||||||
import { expoClient } from '@better-auth/expo/client';
|
|
||||||
import { createAuthClient } from 'better-auth/react';
|
|
||||||
|
|
||||||
import { getBaseUrl } from './base-url';
|
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
|
||||||
baseURL: getBaseUrl(),
|
|
||||||
plugins: [
|
|
||||||
expoClient({
|
|
||||||
scheme: 'expo',
|
|
||||||
storagePrefix: 'expo',
|
|
||||||
storage: SecureStore,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
26
apps/expo/src/utils/convex.ts
Normal file
26
apps/expo/src/utils/convex.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Constants from 'expo-constants';
|
||||||
|
import { ConvexReactClient } from 'convex/react';
|
||||||
|
|
||||||
|
const getConvexUrl = (): string => {
|
||||||
|
// Allow override via Expo extra config (set in app.config.ts for production)
|
||||||
|
const fromConfig = Constants.expoConfig?.extra?.convexUrl as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
if (fromConfig) return fromConfig;
|
||||||
|
|
||||||
|
// Fall back to deriving from the dev server host (same pattern as getBaseUrl)
|
||||||
|
const debuggerHost = Constants.expoConfig?.hostUri;
|
||||||
|
const localhost = debuggerHost?.split(':')[0];
|
||||||
|
|
||||||
|
if (!localhost) {
|
||||||
|
throw new Error(
|
||||||
|
'Could not determine Convex URL. Set extra.convexUrl in app.config.ts for production.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point at the self-hosted Convex backend on the local network
|
||||||
|
// Update this port if your Convex backend runs on a different port
|
||||||
|
return `http://${localhost}:3210`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convex = new ConvexReactClient(getConvexUrl());
|
||||||
1
apps/next/.cache/.eslintcache
Normal file
1
apps/next/.cache/.eslintcache
Normal file
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
@@ -1,11 +1,13 @@
|
|||||||
import { withSentryConfig } from '@sentry/nextjs';
|
import { withSentryConfig } from '@sentry/nextjs';
|
||||||
|
import { createJiti } from 'jiti';
|
||||||
import { withPlausibleProxy } from 'next-plausible';
|
import { withPlausibleProxy } from 'next-plausible';
|
||||||
|
|
||||||
import { env } from './src/env.js';
|
const jiti = createJiti(import.meta.url);
|
||||||
|
await jiti.import('./src/env');
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = withPlausibleProxy({
|
const config = withPlausibleProxy({
|
||||||
customDomain: env.NEXT_PUBLIC_PLAUSIBLE_URL,
|
customDomain: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
|
||||||
})({
|
})({
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
images: {
|
images: {
|
||||||
@@ -30,12 +32,12 @@ const config = withPlausibleProxy({
|
|||||||
const sentryConfig = {
|
const sentryConfig = {
|
||||||
// For all available options, see:
|
// For all available options, see:
|
||||||
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
|
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
|
||||||
org: env.NEXT_PUBLIC_SENTRY_ORG,
|
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
|
||||||
project: env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
|
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
|
||||||
sentryUrl: env.NEXT_PUBLIC_SENTRY_URL,
|
sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL,
|
||||||
authToken: env.SENTRY_AUTH_TOKEN,
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
// Only print logs for uploading source maps in CI
|
// Only print logs for uploading source maps in CI
|
||||||
silent: !env.CI,
|
silent: !process.env.CI,
|
||||||
// For all available options, see:
|
// For all available options, see:
|
||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun with-env next build",
|
"build": "bun with-env next build",
|
||||||
|
"build:env": "bun with-env next build",
|
||||||
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
||||||
"dev": "bun with-env next dev --turbo",
|
"dev": "bun with-env next dev --turbo",
|
||||||
"dev:tunnel": "bun with-env next dev --turbo",
|
"dev:tunnel": "bun with-env next dev --turbo",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const Profile = async () => {
|
|||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<Card className='border-border/40'>
|
<Card className='border-border/40'>
|
||||||
<ProfileHeader preloadedUser={preloadedUser} />
|
<ProfileHeader />
|
||||||
<AvatarUpload preloadedUser={preloadedUser} />
|
<AvatarUpload preloadedUser={preloadedUser} />
|
||||||
<Separator className='my-6' />
|
<Separator className='my-6' />
|
||||||
<UserInfoForm
|
<UserInfoForm
|
||||||
|
|||||||
@@ -252,24 +252,27 @@ const SignIn = () => {
|
|||||||
<Tabs
|
<Tabs
|
||||||
defaultValue={flow}
|
defaultValue={flow}
|
||||||
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
|
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
|
||||||
className='items-center'
|
className='flex-col items-center'
|
||||||
>
|
>
|
||||||
<TabsList className='py-6'>
|
<TabsList>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value='signIn'
|
value='signIn'
|
||||||
className='cursor-pointer p-6 text-2xl font-bold'
|
className='cursor-pointer px-6 py-2 text-2xl font-bold'
|
||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value='signUp'
|
value='signUp'
|
||||||
className='cursor-pointer p-6 text-2xl font-bold'
|
className='cursor-pointer px-6 py-2 text-2xl font-bold'
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='signIn'>
|
<TabsContent
|
||||||
<Card className='bg-card/50 min-w-xs sm:min-w-sm'>
|
value='signIn'
|
||||||
|
className='flex min-h-[560px] flex-row items-center'
|
||||||
|
>
|
||||||
|
<Card className='bg-card/50 min-w-xs py-10 sm:min-w-sm'>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...signInForm}>
|
<Form {...signInForm}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useEffect } from 'react';
|
|||||||
import Footer from '@/components/layout/footer';
|
import Footer from '@/components/layout/footer';
|
||||||
import Header from '@/components/layout/header';
|
import Header from '@/components/layout/header';
|
||||||
import { ConvexClientProvider } from '@/components/providers';
|
import { ConvexClientProvider } from '@/components/providers';
|
||||||
|
import { env } from '@/env';
|
||||||
import { generateMetadata } from '@/lib/metadata';
|
import { generateMetadata } from '@/lib/metadata';
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import PlausibleProvider from 'next-plausible';
|
import PlausibleProvider from 'next-plausible';
|
||||||
@@ -45,8 +46,8 @@ const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
return (
|
return (
|
||||||
<PlausibleProvider
|
<PlausibleProvider
|
||||||
domain='convexmonorepo.gbrown.org'
|
domain={env.NEXT_PUBLIC_SITE_URL.trim().replace(/^https?:\/\//, '')}
|
||||||
customDomain='https://plausible.gbrown.org'
|
customDomain={env.NEXT_PUBLIC_PLAUSIBLE_URL}
|
||||||
>
|
>
|
||||||
<html lang='en' suppressHydrationWarning>
|
<html lang='en' suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ const RootLayout = ({
|
|||||||
return (
|
return (
|
||||||
<ConvexAuthNextjsServerProvider>
|
<ConvexAuthNextjsServerProvider>
|
||||||
<PlausibleProvider
|
<PlausibleProvider
|
||||||
domain={env.NEXT_PUBLIC_SITE_URL}
|
domain={env.NEXT_PUBLIC_SITE_URL.trim().replace(/^https?:\/\//, '')}
|
||||||
customDomain={env.NEXT_PUBLIC_PLAUSIBLE_URL}
|
customDomain={env.NEXT_PUBLIC_PLAUSIBLE_URL}
|
||||||
>
|
>
|
||||||
<html lang='en'>
|
<html lang='en' suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,38 +1,31 @@
|
|||||||
import Image from 'next/image';
|
export const CTA = () => (
|
||||||
import Link from 'next/link';
|
<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>
|
||||||
|
|
||||||
import { Button } from '@gib/ui/button';
|
{/* Quick Start Command */}
|
||||||
|
<div className='mt-12'>
|
||||||
export function CTA() {
|
<p className='text-muted-foreground mb-3 text-sm font-medium'>
|
||||||
return (
|
Quick Start
|
||||||
<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-gradient-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>
|
</p>
|
||||||
|
<div className='border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4'>
|
||||||
{/* Quick Start Command */}
|
<code className='text-sm'>
|
||||||
<div className='mt-12'>
|
git clone https://git.gbrown.org/gib/convex-monorepo.git
|
||||||
<p className='text-muted-foreground mb-3 text-sm font-medium'>
|
<br />
|
||||||
Quick Start
|
cd convex-monorepo
|
||||||
</p>
|
<br />
|
||||||
<div className='border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4'>
|
bun i
|
||||||
<code className='text-sm'>
|
</code>
|
||||||
git clone https://git.gbrown.org/gib/convex-monorepo.git
|
|
||||||
<br />
|
|
||||||
cd convex-monorepo
|
|
||||||
<br />
|
|
||||||
bun i
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
</section>
|
||||||
}
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@gib/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@gib/ui';
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
@@ -57,36 +57,34 @@ const features = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Features() {
|
export const Features = () => (
|
||||||
return (
|
<section id='features' className='container mx-auto px-4 py-24'>
|
||||||
<section id='features' className='container mx-auto px-4 py-24'>
|
<div className='mx-auto max-w-6xl'>
|
||||||
<div className='mx-auto max-w-6xl'>
|
{/* Section Header */}
|
||||||
{/* Section Header */}
|
<div className='mb-16 text-center'>
|
||||||
<div className='mb-16 text-center'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
Everything You Need to Ship Fast
|
||||||
Everything You Need to Ship Fast
|
</h2>
|
||||||
</h2>
|
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
||||||
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
A complete monorepo template with all the tools and patterns you need
|
||||||
A complete monorepo template with all the tools and patterns you
|
for production-ready applications.
|
||||||
need for production-ready applications.
|
</p>
|
||||||
</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>
|
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,127 +2,125 @@ import { Kanit } from 'next/font/google';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Button } from '@gib/ui/button';
|
import { Button } from '@gib/ui';
|
||||||
|
|
||||||
const kanitSans = Kanit({
|
const kanitSans = Kanit({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
weight: ['400', '500', '600', '700'],
|
weight: ['400', '500', '600', '700'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Hero() {
|
export const Hero = () => (
|
||||||
return (
|
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
|
||||||
<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'>
|
||||||
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
|
{/* Badge */}
|
||||||
{/* Badge */}
|
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
|
||||||
<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 className='mr-2'>🚀</span>
|
<span>Production-ready monorepo template</span>
|
||||||
<span>Production-ready monorepo template</span>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Heading */}
|
{/* 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'>
|
<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{' '}
|
Build Full-Stack Apps with{' '}
|
||||||
<span
|
<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]`}
|
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
|
<Image
|
||||||
</span>
|
src='/misc/gitea/gitea.svg'
|
||||||
</h1>
|
alt='Gitea'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
View Source Code
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Features Quick List */}
|
||||||
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
|
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
|
||||||
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship
|
<div className='flex items-center gap-2'>
|
||||||
web and mobile apps faster with shared code, type-safe backend, and
|
<svg
|
||||||
complete control over your infrastructure.
|
className='h-5 w-5 text-green-500'
|
||||||
</p>
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
{/* CTA Buttons */}
|
stroke='currentColor'
|
||||||
<div className='flex flex-col gap-3 sm:flex-row'>
|
>
|
||||||
<Button size='lg' variant='outline' asChild>
|
<path
|
||||||
<Link
|
strokeLinecap='round'
|
||||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
strokeLinejoin='round'
|
||||||
target='_blank'
|
strokeWidth={2}
|
||||||
rel='noopener noreferrer'
|
d='M5 13l4 4L19 7'
|
||||||
>
|
/>
|
||||||
<Image
|
</svg>
|
||||||
src='/misc/gitea/gitea.svg'
|
<span>TypeScript</span>
|
||||||
alt='Gitea'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
View Source Code
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
{/* Features Quick List */}
|
<svg
|
||||||
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
|
className='h-5 w-5 text-green-500'
|
||||||
<div className='flex items-center gap-2'>
|
fill='none'
|
||||||
<svg
|
viewBox='0 0 24 24'
|
||||||
className='h-5 w-5 text-green-500'
|
stroke='currentColor'
|
||||||
fill='none'
|
>
|
||||||
viewBox='0 0 24 24'
|
<path
|
||||||
stroke='currentColor'
|
strokeLinecap='round'
|
||||||
>
|
strokeLinejoin='round'
|
||||||
<path
|
strokeWidth={2}
|
||||||
strokeLinecap='round'
|
d='M5 13l4 4L19 7'
|
||||||
strokeLinejoin='round'
|
/>
|
||||||
strokeWidth={2}
|
</svg>
|
||||||
d='M5 13l4 4L19 7'
|
<span>Self-Hosted</span>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
<div className='flex items-center gap-2'>
|
||||||
<span>TypeScript</span>
|
<svg
|
||||||
</div>
|
className='h-5 w-5 text-green-500'
|
||||||
<div className='flex items-center gap-2'>
|
fill='none'
|
||||||
<svg
|
viewBox='0 0 24 24'
|
||||||
className='h-5 w-5 text-green-500'
|
stroke='currentColor'
|
||||||
fill='none'
|
>
|
||||||
viewBox='0 0 24 24'
|
<path
|
||||||
stroke='currentColor'
|
strokeLinecap='round'
|
||||||
>
|
strokeLinejoin='round'
|
||||||
<path
|
strokeWidth={2}
|
||||||
strokeLinecap='round'
|
d='M5 13l4 4L19 7'
|
||||||
strokeLinejoin='round'
|
/>
|
||||||
strokeWidth={2}
|
</svg>
|
||||||
d='M5 13l4 4L19 7'
|
<span>Real-time</span>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
<div className='flex items-center gap-2'>
|
||||||
<span>Self-Hosted</span>
|
<svg
|
||||||
</div>
|
className='h-5 w-5 text-green-500'
|
||||||
<div className='flex items-center gap-2'>
|
fill='none'
|
||||||
<svg
|
viewBox='0 0 24 24'
|
||||||
className='h-5 w-5 text-green-500'
|
stroke='currentColor'
|
||||||
fill='none'
|
>
|
||||||
viewBox='0 0 24 24'
|
<path
|
||||||
stroke='currentColor'
|
strokeLinecap='round'
|
||||||
>
|
strokeLinejoin='round'
|
||||||
<path
|
strokeWidth={2}
|
||||||
strokeLinecap='round'
|
d='M5 13l4 4L19 7'
|
||||||
strokeLinejoin='round'
|
/>
|
||||||
strokeWidth={2}
|
</svg>
|
||||||
d='M5 13l4 4L19 7'
|
<span>Auth Included</span>
|
||||||
/>
|
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
</section>
|
||||||
}
|
);
|
||||||
|
|||||||
@@ -36,44 +36,42 @@ const techStack = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function TechStack() {
|
export const TechStack = () => (
|
||||||
return (
|
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
|
||||||
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
|
<div className='container mx-auto px-4 py-24'>
|
||||||
<div className='container mx-auto px-4 py-24'>
|
<div className='mx-auto max-w-6xl'>
|
||||||
<div className='mx-auto max-w-6xl'>
|
{/* Section Header */}
|
||||||
{/* Section Header */}
|
<div className='mb-16 text-center'>
|
||||||
<div className='mb-16 text-center'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
Modern Tech Stack
|
||||||
Modern Tech Stack
|
</h2>
|
||||||
</h2>
|
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
||||||
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
Built with the latest and greatest tools for maximum productivity
|
||||||
Built with the latest and greatest tools for maximum productivity
|
and performance.
|
||||||
and performance.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tech Stack Grid */}
|
{/* Tech Stack Grid */}
|
||||||
<div className='grid gap-12 md:grid-cols-3'>
|
<div className='grid gap-12 md:grid-cols-3'>
|
||||||
{techStack.map((stack) => (
|
{techStack.map((stack) => (
|
||||||
<div key={stack.category}>
|
<div key={stack.category}>
|
||||||
<h3 className='mb-6 text-xl font-semibold'>{stack.category}</h3>
|
<h3 className='mb-6 text-xl font-semibold'>{stack.category}</h3>
|
||||||
<ul className='space-y-4'>
|
<ul className='space-y-4'>
|
||||||
{stack.technologies.map((tech) => (
|
{stack.technologies.map((tech) => (
|
||||||
<li key={tech.name}>
|
<li key={tech.name}>
|
||||||
<div className='text-foreground font-medium'>
|
<div className='text-foreground font-medium'>
|
||||||
{tech.name}
|
{tech.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{tech.description}
|
{tech.description}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
</section>
|
||||||
}
|
);
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { Preloaded } from 'convex/react';
|
|
||||||
import { usePreloadedQuery } from 'convex/react';
|
|
||||||
|
|
||||||
import type { api } from '@gib/backend/convex/_generated/api.js';
|
|
||||||
import { CardDescription, CardHeader, CardTitle } from '@gib/ui';
|
import { CardDescription, CardHeader, CardTitle } from '@gib/ui';
|
||||||
|
|
||||||
interface ProfileCardProps {
|
const ProfileHeader = () => {
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => {
|
|
||||||
const user = usePreloadedQuery(preloadedUser);
|
|
||||||
return (
|
return (
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='text-xl'>Account Settings</CardTitle>
|
<CardTitle className='text-xl'>Account Settings</CardTitle>
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export const UserInfoForm = ({
|
|||||||
}: UserInfoFormProps) => {
|
}: UserInfoFormProps) => {
|
||||||
const user = usePreloadedQuery(preloadedUser);
|
const user = usePreloadedQuery(preloadedUser);
|
||||||
const userProvider = usePreloadedQuery(preloadedProvider);
|
const userProvider = usePreloadedQuery(preloadedProvider);
|
||||||
|
const providerMap: Record<string, string> = {
|
||||||
|
unknown: 'Provider',
|
||||||
|
authentik: "Gib's Auth",
|
||||||
|
};
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const updateUser = useMutation(api.auth.updateUser);
|
const updateUser = useMutation(api.auth.updateUser);
|
||||||
@@ -137,16 +141,17 @@ export const UserInfoForm = ({
|
|||||||
{...field}
|
{...field}
|
||||||
type='email'
|
type='email'
|
||||||
placeholder='john@example.com'
|
placeholder='john@example.com'
|
||||||
disabled={userProvider !== 'email'}
|
disabled={userProvider !== 'password'}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{userProvider === 'email' ? (
|
{userProvider === 'password' ? (
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Your email address for account notifications
|
Your email address for account notifications
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
) : (
|
) : (
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Email is managed through your {userProvider} account
|
Email is managed through your{' '}
|
||||||
|
{providerMap[userProvider ?? 'unknown']} account
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
)}
|
)}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function Header(headerProps: ComponentProps<'header'>) {
|
|||||||
alt='Convex Monorepo'
|
alt='Convex Monorepo'
|
||||||
width={50}
|
width={50}
|
||||||
height={50}
|
height={50}
|
||||||
className='invert dark:invert-0'
|
className='w-15 invert dark:invert-0'
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`mb-3 hidden font-extrabold lg:inline lg:text-5xl ${kanitSans.className}`}
|
className={`mb-3 hidden font-extrabold lg:inline lg:text-5xl ${kanitSans.className}`}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import { ConvexReactClient } from 'convex/react';
|
|||||||
|
|
||||||
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
|
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
|
||||||
|
|
||||||
export function ConvexClientProvider({ children }: { children: ReactNode }) {
|
export const ConvexClientProvider = ({ children }: { children: ReactNode }) => (
|
||||||
return (
|
<ConvexAuthNextjsProvider client={convex}>
|
||||||
<ConvexAuthNextjsProvider client={convex}>
|
{children}
|
||||||
{children}
|
</ConvexAuthNextjsProvider>
|
||||||
</ConvexAuthNextjsProvider>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ import { createEnv } from '@t3-oss/env-nextjs';
|
|||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
shared: {
|
server: {
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(['development', 'production', 'test'])
|
.enum(['development', 'production', 'test'])
|
||||||
.default('development'),
|
.default('development'),
|
||||||
},
|
SKIP_ENV_VALIDATION: z.boolean().default(false),
|
||||||
/**
|
|
||||||
* Specify your server-side environment variables schema here.
|
|
||||||
* This way you can ensure the app isn't built with invalid env vars.
|
|
||||||
*/
|
|
||||||
server: {
|
|
||||||
SENTRY_AUTH_TOKEN: z.string(),
|
SENTRY_AUTH_TOKEN: z.string(),
|
||||||
|
CI: z.boolean().default(false),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,10 +27,11 @@ export const env = createEnv({
|
|||||||
/**
|
/**
|
||||||
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
|
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
|
||||||
*/
|
*/
|
||||||
experimental__runtimeEnv: {
|
runtimeEnv: {
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
SKIP_ENV_VALIDATION: process.env.SKIP_ENV_VALIDATION,
|
||||||
|
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||||
CI: process.env.CI,
|
CI: process.env.CI,
|
||||||
SITE_URL: process.env.SITE_URL,
|
|
||||||
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
||||||
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
|
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
|
||||||
NEXT_PUBLIC_PLAUSIBLE_URL: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
|
NEXT_PUBLIC_PLAUSIBLE_URL: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
|
||||||
@@ -44,6 +41,6 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME:
|
NEXT_PUBLIC_SENTRY_PROJECT_NAME:
|
||||||
process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
|
process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
|
||||||
},
|
},
|
||||||
skipValidation:
|
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||||
!!process.env.CI || process.env.npm_lifecycle_event === 'lint',
|
emptyStringAsUndefined: true,
|
||||||
});
|
});
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
import { env } from '@/env';
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
import { env } from './env.js';
|
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
integrations: [
|
integrations: [
|
||||||
@@ -20,7 +19,7 @@ Sentry.init({
|
|||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
enableLogs: true,
|
enableLogs: true,
|
||||||
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
|
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
|
||||||
replaysSessionSampleRate: 0.5,
|
replaysSessionSampleRate: 1.0,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
import { env } from '@/env';
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
import { env } from './env.js';
|
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2022", "dom", "dom.iterable"],
|
"lib": ["ES2022", "dom", "dom.iterable"],
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"types": ["node"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -52,7 +52,6 @@
|
|||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.2",
|
"react-native-web": "~0.21.2",
|
||||||
"react-native-worklets": "~0.5.2",
|
"react-native-worklets": "~0.5.2",
|
||||||
"superjson": "2.2.3",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gib/eslint-config": "workspace:*",
|
"@gib/eslint-config": "workspace:*",
|
||||||
@@ -181,6 +180,7 @@
|
|||||||
"@next/eslint-plugin-next": "^16.0.0",
|
"@next/eslint-plugin-next": "^16.0.0",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-prefer-arrow-functions": "^3.9.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-turbo": "^2.5.8",
|
"eslint-plugin-turbo": "^2.5.8",
|
||||||
@@ -1951,6 +1951,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-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": ["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=="],
|
"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=="],
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
# Next Envrionment Variables
|
# Next Envrionment Variables
|
||||||
NODE_ENV=production
|
|
||||||
NETWORK=nginx-bridge
|
NETWORK=nginx-bridge
|
||||||
NEXT_CONTAINER_NAME=next-app
|
NEXT_CONTAINER_NAME=next-app
|
||||||
NEXT_DOMAIN_NAME=gbrown.org
|
NEXT_DOMAIN_NAME=gbrown.org
|
||||||
# Port is disabled by default as suggested
|
NEXT_PORT=3000
|
||||||
# config is to have reverse proxy on the same
|
NODE_ENV=production
|
||||||
# network so you can just forward to the
|
|
||||||
# port on the internal network.
|
|
||||||
# NEXT_PORT=3000
|
|
||||||
SENTRY_AUTH_TOKEN=
|
SENTRY_AUTH_TOKEN=
|
||||||
NEXT_PUBLIC_SITE_URL=https://gbrown.org
|
NEXT_PUBLIC_SITE_URL=https://gbrown.org
|
||||||
NEXT_PUBLIC_CONVEX_URL=https://api.convex.gbrown.org
|
NEXT_PUBLIC_CONVEX_URL=https://api.convex.gbrown.org
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ ENV NODE_ENV=production
|
|||||||
RUN bun run build --filter=@gib/next
|
RUN bun run build --filter=@gib/next
|
||||||
|
|
||||||
# Runner stage
|
# Runner stage
|
||||||
FROM node:20-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -9,16 +9,8 @@ services:
|
|||||||
dockerfile: ./docker/Dockerfile
|
dockerfile: ./docker/Dockerfile
|
||||||
image: ${NEXT_CONTAINER_NAME}:alpine
|
image: ${NEXT_CONTAINER_NAME}:alpine
|
||||||
container_name: ${NEXT_CONTAINER_NAME}
|
container_name: ${NEXT_CONTAINER_NAME}
|
||||||
env_file: [.env]
|
|
||||||
environment:
|
environment:
|
||||||
- SENTRY_AUTH_TOKEN
|
- NODE_ENV
|
||||||
- NEXT_PUBLIC_SITE_URL
|
|
||||||
- NEXT_PUBLIC_CONVEX_URL
|
|
||||||
- NEXT_PUBLIC_PLAUSIBLE_URL
|
|
||||||
- NEXT_PUBLIC_SENTRY_DSN
|
|
||||||
- NEXT_PUBLIC_SENTRY_URL
|
|
||||||
- NEXT_PUBLIC_SENTRY_ORG
|
|
||||||
- NEXT_PUBLIC_SENTRY_PROJECT_NAME
|
|
||||||
hostname: ${NEXT_CONTAINER_NAME}
|
hostname: ${NEXT_CONTAINER_NAME}
|
||||||
domainname: ${NEXT_DOMAIN_NAME}
|
domainname: ${NEXT_DOMAIN_NAME}
|
||||||
networks: ['${NETWORK:-nginx-bridge}']
|
networks: ['${NETWORK:-nginx-bridge}']
|
||||||
@@ -38,7 +30,6 @@ services:
|
|||||||
#ports: ['${BACKEND_PORT:-3210}:3210','${SITE_PROXY_PORT:-3211}:3211']
|
#ports: ['${BACKEND_PORT:-3210}:3210','${SITE_PROXY_PORT:-3211}:3211']
|
||||||
volumes: [./data:/convex/data]
|
volumes: [./data:/convex/data]
|
||||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
env_file: ['.env']
|
|
||||||
environment:
|
environment:
|
||||||
- INSTANCE_NAME
|
- INSTANCE_NAME
|
||||||
- INSTANCE_SECRET
|
- INSTANCE_SECRET
|
||||||
@@ -67,7 +58,6 @@ services:
|
|||||||
#user: 1000:1000
|
#user: 1000:1000
|
||||||
#ports: ['${DASHBOARD_PORT:-6791}:6791']
|
#ports: ['${DASHBOARD_PORT:-6791}:6791']
|
||||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
env_file: [.env]
|
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${PORT:-3210}}
|
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${PORT:-3210}}
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -54,16 +54,6 @@ export const getUser = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getAllUsers = query(async (ctx) => {
|
|
||||||
const users = await ctx.db.query('users').collect();
|
|
||||||
return users ?? null;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAllUserIds = query(async (ctx) => {
|
|
||||||
const users = await ctx.db.query('users').collect();
|
|
||||||
return users.map((u) => u._id);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateUser = mutation({
|
export const updateUser = mutation({
|
||||||
args: {
|
args: {
|
||||||
name: v.optional(v.string()),
|
name: v.optional(v.string()),
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ export const validatePassword = (password: string): boolean => {
|
|||||||
if (
|
if (
|
||||||
password.length < 8 ||
|
password.length < 8 ||
|
||||||
password.length > 100 ||
|
password.length > 100 ||
|
||||||
|
/\s/.test(password) ||
|
||||||
!/\d/.test(password) ||
|
!/\d/.test(password) ||
|
||||||
!/[a-z]/.test(password) ||
|
!/[a-z]/.test(password) ||
|
||||||
!/[A-Z]/.test(password)
|
!/[A-Z]/.test(password) ||
|
||||||
|
!/[\p{P}\p{S}]/u.test(password)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
|
|||||||
id: 'usesend',
|
id: 'usesend',
|
||||||
type: 'email',
|
type: 'email',
|
||||||
name: 'UseSend',
|
name: 'UseSend',
|
||||||
from: 'Study Buddy <admin@techtracker.gbrown.org>',
|
from: process.env.USESEND_FROM_EMAIL ?? 'noreply@example.com',
|
||||||
maxAge: 24 * 60 * 60, // 24 hours
|
maxAge: 24 * 60 * 60, // 24 hours
|
||||||
|
|
||||||
async generateVerificationToken() {
|
async generateVerificationToken() {
|
||||||
@@ -21,13 +21,14 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async sendVerificationRequest(params) {
|
async sendVerificationRequest(params) {
|
||||||
const { identifier: to, provider, url, theme, token } = params;
|
const { identifier: to, provider, url, token } = params;
|
||||||
//const { host } = new URL(url);
|
// Derive a display name from the site URL, fallback to 'App'
|
||||||
const host = 'TechTracker';
|
const siteUrl = process.env.USESEND_FROM_EMAIL ?? '';
|
||||||
|
const appName = siteUrl.split('@')[1]?.split('.')[0] ?? 'App';
|
||||||
|
|
||||||
const useSend = new UseSend(
|
const useSend = new UseSend(
|
||||||
process.env.USESEND_API_KEY!,
|
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
|
// 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!,
|
from: provider.from!,
|
||||||
to: [to],
|
to: [to],
|
||||||
subject: isPasswordReset
|
subject: isPasswordReset
|
||||||
? `Reset your password - ${host}`
|
? `Reset your password - ${appName}`
|
||||||
: `Sign in to ${host}`,
|
: `Sign in to ${appName}`,
|
||||||
text: isPasswordReset
|
text: isPasswordReset
|
||||||
? `Your password reset code is ${token}`
|
? `Your password reset code is ${token}`
|
||||||
: `Your sign in code is ${token}`,
|
: `Your sign in code is ${token}`,
|
||||||
|
|||||||
@@ -3,22 +3,12 @@ import { defineSchema, defineTable } from 'convex/server';
|
|||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
|
|
||||||
const applicationTables = {
|
const applicationTables = {
|
||||||
// Users contains name image & email.
|
/*
|
||||||
// If you would like to save any other information,
|
* Below is the users table definition from authTables
|
||||||
// I would recommend including this profiles table
|
* You can add additional fields here. You can also remove
|
||||||
// where you can include settings & anything else you would like tied to the user.
|
* the users table here & create a 'profiles' table if you
|
||||||
profiles: defineTable({
|
* prefer to keep auth data separate from application data.
|
||||||
userId: v.id('users'),
|
*/
|
||||||
theme_preference: v.optional(v.string()),
|
|
||||||
}).index('userId', ['userId']),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineSchema({
|
|
||||||
...authTables,
|
|
||||||
// Default table for users directly from authTable.
|
|
||||||
// You can extend it if you would like, but it may
|
|
||||||
// be better to just use the profiles table example
|
|
||||||
// below.
|
|
||||||
users: defineTable({
|
users: defineTable({
|
||||||
name: v.optional(v.string()),
|
name: v.optional(v.string()),
|
||||||
image: v.optional(v.string()),
|
image: v.optional(v.string()),
|
||||||
@@ -27,9 +17,18 @@ export default defineSchema({
|
|||||||
phone: v.optional(v.string()),
|
phone: v.optional(v.string()),
|
||||||
phoneVerificationTime: v.optional(v.number()),
|
phoneVerificationTime: v.optional(v.number()),
|
||||||
isAnonymous: v.optional(v.boolean()),
|
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')),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.index('email', ['email'])
|
.index('email', ['email'])
|
||||||
.index('name', ['name'])
|
.index('phone', ['phone'])
|
||||||
.index('phone', ['phone']),
|
/* Indexes below here are custom & not defined in authTables */
|
||||||
|
.index('name', ['name']),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineSchema({
|
||||||
|
...authTables,
|
||||||
...applicationTables,
|
...applicationTables,
|
||||||
});
|
});
|
||||||
|
|||||||
1
packages/ui/.cache/.eslintcache
Normal file
1
packages/ui/.cache/.eslintcache
Normal file
File diff suppressed because one or more lines are too long
1
packages/ui/.cache/.prettiercache
Normal file
1
packages/ui/.cache/.prettiercache
Normal file
File diff suppressed because one or more lines are too long
10982
packages/ui/.cache/tsbuildinfo.json
Normal file
10982
packages/ui/.cache/tsbuildinfo.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,68 +4,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.tsx",
|
".": "./src/index.tsx",
|
||||||
"./accordion": "./src/accordion.tsx",
|
"./hooks": "./src/index.tsx",
|
||||||
"./alert": "./src/alert.tsx",
|
"./hooks/*": "./src/hooks/*"
|
||||||
"./alert-dialog": "./src/alert-dialog.tsx",
|
|
||||||
"./aspect-ratio": "./src/aspect-ratio.tsx",
|
|
||||||
"./avatar": "./src/avatar.tsx",
|
|
||||||
"./badge": "./src/badge.tsx",
|
|
||||||
"./based-avatar": "./src/based-avatar.tsx",
|
|
||||||
"./based-progress": "./src/based-progress.tsx",
|
|
||||||
"./breadcrumb": "./src/breadcrumb.tsx",
|
|
||||||
"./button": "./src/button.tsx",
|
|
||||||
"./button-group": "./src/button-group.tsx",
|
|
||||||
"./calendar": "./src/calendar.tsx",
|
|
||||||
"./card": "./src/card.tsx",
|
|
||||||
"./carousel": "./src/carousel.tsx",
|
|
||||||
"./chart": "./src/chart.tsx",
|
|
||||||
"./checkbox": "./src/checkbox.tsx",
|
|
||||||
"./collapsible": "./src/collapsible.tsx",
|
|
||||||
"./combobox": "./src/combobox.tsx",
|
|
||||||
"./command": "./src/command.tsx",
|
|
||||||
"./context-menu": "./src/context-menu.tsx",
|
|
||||||
"./dialog": "./src/dialog.tsx",
|
|
||||||
"./drawer": "./src/drawer.tsx",
|
|
||||||
"./dropdown-menu": "./src/dropdown-menu.tsx",
|
|
||||||
"./empty": "./src/empty.tsx",
|
|
||||||
"./field": "./src/field.tsx",
|
|
||||||
"./form": "./src/form.tsx",
|
|
||||||
"./hover-card": "./src/hover-card.tsx",
|
|
||||||
"./image-crop": "./src/image-crop.tsx",
|
|
||||||
"./input": "./src/input.tsx",
|
|
||||||
"./input-group": "./src/input-group.tsx",
|
|
||||||
"./input-otp": "./src/input-otp.tsx",
|
|
||||||
"./item": "./src/item.tsx",
|
|
||||||
"./kbd": "./src/kbd.tsx",
|
|
||||||
"./label": "./src/label.tsx",
|
|
||||||
"./menubar": "./src/menubar.tsx",
|
|
||||||
"./native-select": "./src/native-select.tsx",
|
|
||||||
"./navigation-menu": "./src/navigation-menu.tsx",
|
|
||||||
"./pagination": "./src/pagination.tsx",
|
|
||||||
"./popover": "./src/popover.tsx",
|
|
||||||
"./progress": "./src/progress.tsx",
|
|
||||||
"./radio-group": "./src/radio-group.tsx",
|
|
||||||
"./resizeable": "./src/resizeable.tsx",
|
|
||||||
"./scroll-area": "./src/scroll-area.tsx",
|
|
||||||
"./select": "./src/select.tsx",
|
|
||||||
"./separator": "./src/separator.tsx",
|
|
||||||
"./sheet": "./src/sheet.tsx",
|
|
||||||
"./sidebar": "./src/sidebar.tsx",
|
|
||||||
"./skeleton": "./src/skeleton.tsx",
|
|
||||||
"./slider": "./src/slider.tsx",
|
|
||||||
"./sonner": "./src/sonner.tsx",
|
|
||||||
"./spinner": "./src/spinner.tsx",
|
|
||||||
"./status-message": "./src/status-message.tsx",
|
|
||||||
"./submit-button": "./src/submit-button.tsx",
|
|
||||||
"./switch": "./src/switch.tsx",
|
|
||||||
"./table": "./src/table.tsx",
|
|
||||||
"./tabs": "./src/tabs.tsx",
|
|
||||||
"./textarea": "./src/textarea.tsx",
|
|
||||||
"./theme": "./src/theme.tsx",
|
|
||||||
"./toast": "./src/toast.tsx",
|
|
||||||
"./toggle": "./src/toggle.tsx",
|
|
||||||
"./toggle-group": "./src/toggle-group.tsx",
|
|
||||||
"./tooltip": "./src/tooltip.tsx"
|
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -6,82 +6,74 @@ import { Accordion as AccordionPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Accordion({
|
const Accordion = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) => (
|
||||||
return (
|
<AccordionPrimitive.Root
|
||||||
<AccordionPrimitive.Root
|
data-slot='accordion'
|
||||||
data-slot='accordion'
|
className={cn('flex w-full flex-col', className)}
|
||||||
className={cn('flex w-full flex-col', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionItem({
|
const AccordionItem = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) => (
|
||||||
return (
|
<AccordionPrimitive.Item
|
||||||
<AccordionPrimitive.Item
|
data-slot='accordion-item'
|
||||||
data-slot='accordion-item'
|
className={cn('not-last:border-b', className)}
|
||||||
className={cn('not-last:border-b', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionTrigger({
|
const AccordionTrigger = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) => (
|
||||||
return (
|
<AccordionPrimitive.Header className='flex'>
|
||||||
<AccordionPrimitive.Header className='flex'>
|
<AccordionPrimitive.Trigger
|
||||||
<AccordionPrimitive.Trigger
|
data-slot='accordion-trigger'
|
||||||
data-slot='accordion-trigger'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
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'
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
{children}
|
||||||
className={cn(
|
<ChevronDownIcon
|
||||||
'[&_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',
|
data-slot='accordion-trigger-icon'
|
||||||
className,
|
className='pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden'
|
||||||
)}
|
/>
|
||||||
>
|
<ChevronUpIcon
|
||||||
{children}
|
data-slot='accordion-trigger-icon'
|
||||||
</div>
|
className='pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline'
|
||||||
</AccordionPrimitive.Content>
|
/>
|
||||||
);
|
</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 };
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
|
|||||||
@@ -5,182 +5,160 @@ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { Button, cn } from '@gib/ui';
|
import { Button, cn } from '@gib/ui';
|
||||||
|
|
||||||
function AlertDialog({
|
const AlertDialog = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) => (
|
||||||
return <AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />;
|
<AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function AlertDialogTrigger({
|
const AlertDialogTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) => (
|
||||||
return (
|
<AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
|
||||||
<AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogPortal({
|
const AlertDialogPortal = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) => (
|
||||||
return (
|
<AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
|
||||||
<AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogOverlay({
|
const AlertDialogOverlay = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) => (
|
||||||
return (
|
<AlertDialogPrimitive.Overlay
|
||||||
<AlertDialogPrimitive.Overlay
|
data-slot='alert-dialog-overlay'
|
||||||
data-slot='alert-dialog-overlay'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogContent({
|
const AlertDialogContent = ({
|
||||||
className,
|
className,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||||
size?: 'default' | 'sm';
|
size?: 'default' | 'sm';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<AlertDialogPortal>
|
||||||
<AlertDialogPortal>
|
<AlertDialogOverlay />
|
||||||
<AlertDialogOverlay />
|
<AlertDialogPrimitive.Content
|
||||||
<AlertDialogPrimitive.Content
|
data-slot='alert-dialog-content'
|
||||||
data-slot='alert-dialog-content'
|
data-size={size}
|
||||||
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'
|
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
</AlertDialogPortal>
|
||||||
}
|
);
|
||||||
|
|
||||||
function AlertDialogFooter({
|
const AlertDialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='alert-dialog-header'
|
||||||
data-slot='alert-dialog-footer'
|
className={cn(
|
||||||
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]',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogMedia({
|
const AlertDialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='alert-dialog-footer'
|
||||||
data-slot='alert-dialog-media'
|
className={cn(
|
||||||
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',
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogTitle({
|
const AlertDialogMedia = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<AlertDialogPrimitive.Title
|
data-slot='alert-dialog-media'
|
||||||
data-slot='alert-dialog-title'
|
className={cn(
|
||||||
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",
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogDescription({
|
const AlertDialogTitle = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) => (
|
||||||
return (
|
<AlertDialogPrimitive.Title
|
||||||
<AlertDialogPrimitive.Description
|
data-slot='alert-dialog-title'
|
||||||
data-slot='alert-dialog-description'
|
className={cn(
|
||||||
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',
|
||||||
'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...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,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
size = 'default',
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||||
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
|
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) => (
|
||||||
return (
|
<Button variant={variant} size={size} asChild>
|
||||||
<Button variant={variant} size={size} asChild>
|
<AlertDialogPrimitive.Action
|
||||||
<AlertDialogPrimitive.Action
|
data-slot='alert-dialog-action'
|
||||||
data-slot='alert-dialog-action'
|
className={cn(className)}
|
||||||
className={cn(className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</Button>
|
||||||
</Button>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogCancel({
|
const AlertDialogCancel = ({
|
||||||
className,
|
className,
|
||||||
variant = 'outline',
|
variant = 'outline',
|
||||||
size = 'default',
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||||
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
|
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) => (
|
||||||
return (
|
<Button variant={variant} size={size} asChild>
|
||||||
<Button variant={variant} size={size} asChild>
|
<AlertDialogPrimitive.Cancel
|
||||||
<AlertDialogPrimitive.Cancel
|
data-slot='alert-dialog-cancel'
|
||||||
data-slot='alert-dialog-cancel'
|
className={cn(className)}
|
||||||
className={cn(className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</Button>
|
||||||
</Button>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
|||||||
@@ -20,58 +20,50 @@ const alertVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Alert({
|
const Alert = ({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='alert'
|
||||||
data-slot='alert'
|
role='alert'
|
||||||
role='alert'
|
className={cn(alertVariants({ variant }), className)}
|
||||||
className={cn(alertVariants({ variant }), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
const AlertTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='alert-title'
|
||||||
data-slot='alert-title'
|
className={cn(
|
||||||
className={cn(
|
'[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3',
|
||||||
'[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDescription({
|
const AlertDescription = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='alert-description'
|
||||||
data-slot='alert-description'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
|
const AlertAction = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='alert-action'
|
||||||
data-slot='alert-action'
|
className={cn('absolute top-2 right-2', className)}
|
||||||
className={cn('absolute top-2 right-2', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { AspectRatio as AspectRatioPrimitive } from 'radix-ui';
|
import { AspectRatio as AspectRatioPrimitive } from 'radix-ui';
|
||||||
|
|
||||||
function AspectRatio({
|
const AspectRatio = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) => (
|
||||||
return <AspectRatioPrimitive.Root data-slot='aspect-ratio' {...props} />;
|
<AspectRatioPrimitive.Root data-slot='aspect-ratio' {...props} />
|
||||||
}
|
);
|
||||||
export { AspectRatio };
|
export { AspectRatio };
|
||||||
|
|||||||
@@ -5,99 +5,87 @@ import { Avatar as AvatarPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Avatar({
|
const Avatar = ({
|
||||||
className,
|
className,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||||
size?: 'default' | 'sm' | 'lg';
|
size?: 'default' | 'sm' | 'lg';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<AvatarPrimitive.Root
|
||||||
<AvatarPrimitive.Root
|
data-slot='avatar'
|
||||||
data-slot='avatar'
|
data-size={size}
|
||||||
data-size={size}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarImage({
|
const AvatarImage = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) => (
|
||||||
return (
|
<AvatarPrimitive.Image
|
||||||
<AvatarPrimitive.Image
|
data-slot='avatar-image'
|
||||||
data-slot='avatar-image'
|
className={cn('aspect-square size-full', className)}
|
||||||
className={cn('aspect-square size-full', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarFallback({
|
const AvatarFallback = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) => (
|
||||||
return (
|
<AvatarPrimitive.Fallback
|
||||||
<AvatarPrimitive.Fallback
|
data-slot='avatar-fallback'
|
||||||
data-slot='avatar-fallback'
|
className={cn(
|
||||||
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',
|
||||||
'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
|
const AvatarBadge = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
data-slot='avatar-badge'
|
||||||
data-slot='avatar-badge'
|
className={cn(
|
||||||
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',
|
||||||
'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=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=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',
|
||||||
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
const AvatarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='avatar-group'
|
||||||
data-slot='avatar-group'
|
className={cn(
|
||||||
className={cn(
|
'group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2',
|
||||||
'group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarGroupCount({
|
const AvatarGroupCount = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='avatar-group-count'
|
||||||
data-slot='avatar-group-count'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ const badgeVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Badge({
|
const Badge = ({
|
||||||
className,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'> &
|
}: React.ComponentProps<'span'> &
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) => {
|
||||||
const Comp = asChild ? Slot.Root : 'span';
|
const Comp = asChild ? Slot.Root : 'span';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +44,6 @@ function Badge({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Badge, badgeVariants };
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
@@ -4,40 +4,42 @@ import { Slot } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
const Breadcrumb = ({ ...props }: React.ComponentProps<'nav'>) => (
|
||||||
return <nav aria-label='breadcrumb' data-slot='breadcrumb' {...props} />;
|
<nav aria-label='breadcrumb' data-slot='breadcrumb' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
const BreadcrumbList = ({
|
||||||
return (
|
className,
|
||||||
<ol
|
...props
|
||||||
data-slot='breadcrumb-list'
|
}: React.ComponentProps<'ol'>) => (
|
||||||
className={cn(
|
<ol
|
||||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
data-slot='breadcrumb-list'
|
||||||
className,
|
className={cn(
|
||||||
)}
|
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
const BreadcrumbItem = ({
|
||||||
return (
|
className,
|
||||||
<li
|
...props
|
||||||
data-slot='breadcrumb-item'
|
}: React.ComponentProps<'li'>) => (
|
||||||
className={cn('inline-flex items-center gap-1.5', className)}
|
<li
|
||||||
{...props}
|
data-slot='breadcrumb-item'
|
||||||
/>
|
className={cn('inline-flex items-center gap-1.5', className)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function BreadcrumbLink({
|
const BreadcrumbLink = ({
|
||||||
asChild,
|
asChild,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'a'> & {
|
}: React.ComponentProps<'a'> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) => {
|
||||||
const Comp = asChild ? Slot.Root : 'a';
|
const Comp = asChild ? Slot.Root : 'a';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,56 +49,53 @@ function BreadcrumbLink({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
const BreadcrumbPage = ({
|
||||||
return (
|
className,
|
||||||
<span
|
...props
|
||||||
data-slot='breadcrumb-page'
|
}: React.ComponentProps<'span'>) => (
|
||||||
role='link'
|
<span
|
||||||
aria-disabled='true'
|
data-slot='breadcrumb-page'
|
||||||
aria-current='page'
|
role='link'
|
||||||
className={cn('text-foreground font-normal', className)}
|
aria-disabled='true'
|
||||||
{...props}
|
aria-current='page'
|
||||||
/>
|
className={cn('text-foreground font-normal', className)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
const BreadcrumbSeparator = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'li'>) {
|
}: React.ComponentProps<'li'>) => (
|
||||||
return (
|
<li
|
||||||
<li
|
data-slot='breadcrumb-separator'
|
||||||
data-slot='breadcrumb-separator'
|
role='presentation'
|
||||||
role='presentation'
|
aria-hidden='true'
|
||||||
aria-hidden='true'
|
className={cn('[&>svg]:size-3.5', className)}
|
||||||
className={cn('[&>svg]:size-3.5', className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children ?? <ChevronRight />}
|
||||||
{children ?? <ChevronRight />}
|
</li>
|
||||||
</li>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
const BreadcrumbEllipsis = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
data-slot='breadcrumb-ellipsis'
|
||||||
data-slot='breadcrumb-ellipsis'
|
role='presentation'
|
||||||
role='presentation'
|
aria-hidden='true'
|
||||||
aria-hidden='true'
|
className={cn('flex size-9 items-center justify-center', className)}
|
||||||
className={cn('flex size-9 items-center justify-center', className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<MoreHorizontal className='size-4' />
|
||||||
<MoreHorizontal className='size-4' />
|
<span className='sr-only'>More</span>
|
||||||
<span className='sr-only'>More</span>
|
</span>
|
||||||
</span>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
|
|||||||
@@ -21,29 +21,27 @@ const buttonGroupVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function ButtonGroup({
|
const ButtonGroup = ({
|
||||||
className,
|
className,
|
||||||
orientation,
|
orientation,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
role='group'
|
||||||
role='group'
|
data-slot='button-group'
|
||||||
data-slot='button-group'
|
data-orientation={orientation}
|
||||||
data-orientation={orientation}
|
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonGroupText({
|
const ButtonGroupText = ({
|
||||||
className,
|
className,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) => {
|
||||||
const Comp = asChild ? Slot.Root : 'div';
|
const Comp = asChild ? Slot.Root : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,25 +53,23 @@ function ButtonGroupText({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function ButtonGroupSeparator({
|
const ButtonGroupSeparator = ({
|
||||||
className,
|
className,
|
||||||
orientation = 'vertical',
|
orientation = 'vertical',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
}: React.ComponentProps<typeof Separator>) => (
|
||||||
return (
|
<Separator
|
||||||
<Separator
|
data-slot='button-group-separator'
|
||||||
data-slot='button-group-separator'
|
orientation={orientation}
|
||||||
orientation={orientation}
|
className={cn(
|
||||||
className={cn(
|
'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
|
||||||
'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Button({
|
const Button = ({
|
||||||
className,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
size = 'default',
|
size = 'default',
|
||||||
@@ -51,7 +51,7 @@ function Button({
|
|||||||
}: React.ComponentProps<'button'> &
|
}: React.ComponentProps<'button'> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) => {
|
||||||
const Comp = asChild ? Slot.Root : 'button';
|
const Comp = asChild ? Slot.Root : 'button';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,6 +63,6 @@ function Button({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { DayPicker, getDefaultClassNames } from 'react-day-picker';
|
|||||||
|
|
||||||
import { Button, buttonVariants, cn } from '@gib/ui';
|
import { Button, buttonVariants, cn } from '@gib/ui';
|
||||||
|
|
||||||
function Calendar({
|
const Calendar = ({
|
||||||
className,
|
className,
|
||||||
classNames,
|
classNames,
|
||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
@@ -23,7 +23,7 @@ function Calendar({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayPicker> & {
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
buttonVariant?: React.ComponentProps<typeof Button>['variant'];
|
buttonVariant?: React.ComponentProps<typeof Button>['variant'];
|
||||||
}) {
|
}) => {
|
||||||
const defaultClassNames = getDefaultClassNames();
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -183,15 +183,15 @@ function Calendar({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function CalendarDayButton({
|
const CalendarDayButton = ({
|
||||||
className,
|
className,
|
||||||
day,
|
day,
|
||||||
modifiers,
|
modifiers,
|
||||||
locale,
|
locale,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
|
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) => {
|
||||||
const defaultClassNames = getDefaultClassNames();
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null);
|
const ref = React.useRef<HTMLButtonElement>(null);
|
||||||
@@ -222,6 +222,6 @@ function CalendarDayButton({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Calendar, CalendarDayButton };
|
export { Calendar, CalendarDayButton };
|
||||||
|
|||||||
@@ -2,95 +2,84 @@ import type * as React from 'react';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Card({
|
const Card = ({
|
||||||
className,
|
className,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
|
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='card'
|
||||||
data-slot='card'
|
data-size={size}
|
||||||
data-size={size}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='card-header'
|
||||||
data-slot='card-header'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='card-title'
|
||||||
data-slot='card-title'
|
className={cn(
|
||||||
className={cn(
|
'text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
|
||||||
'text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardDescription = ({
|
||||||
return (
|
className,
|
||||||
<div
|
...props
|
||||||
data-slot='card-description'
|
}: React.ComponentProps<'div'>) => (
|
||||||
className={cn('text-muted-foreground text-sm', className)}
|
<div
|
||||||
{...props}
|
data-slot='card-description'
|
||||||
/>
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardAction = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='card-action'
|
||||||
data-slot='card-action'
|
className={cn(
|
||||||
className={cn(
|
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='card-content'
|
||||||
data-slot='card-content'
|
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
|
||||||
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='card-footer'
|
||||||
data-slot='card-footer'
|
className={cn(
|
||||||
className={cn(
|
'bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3',
|
||||||
'bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Card,
|
Card,
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|||||||
type CarouselOptions = UseCarouselParameters[0];
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
type CarouselPlugin = UseCarouselParameters[1];
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
type CarouselProps = {
|
interface CarouselProps {
|
||||||
opts?: CarouselOptions;
|
opts?: CarouselOptions;
|
||||||
plugins?: CarouselPlugin;
|
plugins?: CarouselPlugin;
|
||||||
orientation?: 'horizontal' | 'vertical';
|
orientation?: 'horizontal' | 'vertical';
|
||||||
setApi?: (api: CarouselApi) => void;
|
setApi?: (api: CarouselApi) => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
type CarouselContextProps = {
|
type CarouselContextProps = {
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
@@ -30,7 +30,7 @@ type CarouselContextProps = {
|
|||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
function useCarousel() {
|
const useCarousel = () => {
|
||||||
const context = React.useContext(CarouselContext);
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -38,9 +38,9 @@ function useCarousel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
function Carousel({
|
const Carousel = ({
|
||||||
orientation = 'horizontal',
|
orientation = 'horizontal',
|
||||||
opts,
|
opts,
|
||||||
setApi,
|
setApi,
|
||||||
@@ -48,7 +48,7 @@ function Carousel({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & CarouselProps) {
|
}: React.ComponentProps<'div'> & CarouselProps) => {
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
@@ -128,9 +128,12 @@ function Carousel({
|
|||||||
</div>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
|
const CarouselContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'>) => {
|
||||||
const { carouselRef, orientation } = useCarousel();
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -149,9 +152,9 @@ function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
|
const CarouselItem = ({ className, ...props }: React.ComponentProps<'div'>) => {
|
||||||
const { orientation } = useCarousel();
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,14 +170,14 @@ function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function CarouselPrevious({
|
const CarouselPrevious = ({
|
||||||
className,
|
className,
|
||||||
variant = 'outline',
|
variant = 'outline',
|
||||||
size = 'icon-sm',
|
size = 'icon-sm',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) => {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,14 +200,14 @@ function CarouselPrevious({
|
|||||||
<span className='sr-only'>Previous slide</span>
|
<span className='sr-only'>Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function CarouselNext({
|
const CarouselNext = ({
|
||||||
className,
|
className,
|
||||||
variant = 'outline',
|
variant = 'outline',
|
||||||
size = 'icon-sm',
|
size = 'icon-sm',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) => {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -227,7 +230,7 @@ function CarouselNext({
|
|||||||
<span className='sr-only'>Next slide</span>
|
<span className='sr-only'>Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
|
|||||||
@@ -8,23 +8,24 @@ import { cn } from '@gib/ui';
|
|||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: '', dark: '.dark' } as const;
|
const THEMES = { light: '', dark: '.dark' } as const;
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = Record<
|
||||||
[k in string]: {
|
string,
|
||||||
|
{
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
icon?: React.ComponentType;
|
icon?: React.ComponentType;
|
||||||
} & (
|
} & (
|
||||||
| { color?: string; theme?: never }
|
| { color?: string; theme?: never }
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
);
|
)
|
||||||
};
|
>;
|
||||||
|
|
||||||
type ChartContextProps = {
|
interface ChartContextProps {
|
||||||
config: ChartConfig;
|
config: ChartConfig;
|
||||||
};
|
}
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
function useChart() {
|
const useChart = () => {
|
||||||
const context = React.useContext(ChartContext);
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -32,9 +33,9 @@ function useChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
function ChartContainer({
|
const ChartContainer = ({
|
||||||
id,
|
id,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
@@ -45,7 +46,7 @@ function ChartContainer({
|
|||||||
children: React.ComponentProps<
|
children: React.ComponentProps<
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
>['children'];
|
>['children'];
|
||||||
}) {
|
}) => {
|
||||||
const uniqueId = React.useId();
|
const uniqueId = React.useId();
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
|
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ function ChartContainer({
|
|||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(
|
const colorConfig = Object.entries(config).filter(
|
||||||
@@ -104,7 +105,7 @@ ${colorConfig
|
|||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
function ChartTooltipContent({
|
const ChartTooltipContent = ({
|
||||||
active,
|
active,
|
||||||
payload,
|
payload,
|
||||||
className,
|
className,
|
||||||
@@ -125,7 +126,7 @@ function ChartTooltipContent({
|
|||||||
indicator?: 'line' | 'dot' | 'dashed';
|
indicator?: 'line' | 'dot' | 'dashed';
|
||||||
nameKey?: string;
|
nameKey?: string;
|
||||||
labelKey?: string;
|
labelKey?: string;
|
||||||
}) {
|
}) => {
|
||||||
const { config } = useChart();
|
const { config } = useChart();
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
@@ -138,7 +139,7 @@ function ChartTooltipContent({
|
|||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const value =
|
const value =
|
||||||
!labelKey && typeof label === 'string'
|
!labelKey && typeof label === 'string'
|
||||||
? config[label as keyof typeof config]?.label || label
|
? config[label]?.label || label
|
||||||
: itemConfig?.label;
|
: itemConfig?.label;
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
@@ -248,11 +249,11 @@ function ChartTooltipContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend;
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
function ChartLegendContent({
|
const ChartLegendContent = ({
|
||||||
className,
|
className,
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
payload,
|
payload,
|
||||||
@@ -262,7 +263,7 @@ function ChartLegendContent({
|
|||||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
nameKey?: string;
|
nameKey?: string;
|
||||||
}) {
|
}) => {
|
||||||
const { config } = useChart();
|
const { config } = useChart();
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
@@ -306,13 +307,13 @@ function ChartLegendContent({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function getPayloadConfigFromPayload(
|
const getPayloadConfigFromPayload = (
|
||||||
config: ChartConfig,
|
config: ChartConfig,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
key: string,
|
key: string,
|
||||||
) {
|
) => {
|
||||||
if (typeof payload !== 'object' || payload === null) {
|
if (typeof payload !== 'object' || payload === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -341,10 +342,8 @@ function getPayloadConfigFromPayload(
|
|||||||
] as string;
|
] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
return configLabelKey in config
|
return configLabelKey in config ? config[configLabelKey] : config[key];
|
||||||
? config[configLabelKey]
|
};
|
||||||
: config[key as keyof typeof config];
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
|
|||||||
@@ -6,27 +6,25 @@ import { Checkbox as CheckboxPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Checkbox({
|
const Checkbox = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) => (
|
||||||
return (
|
<CheckboxPrimitive.Root
|
||||||
<CheckboxPrimitive.Root
|
data-slot='checkbox'
|
||||||
data-slot='checkbox'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot='checkbox-indicator'
|
||||||
|
className='grid place-content-center text-current transition-none [&>svg]:size-3.5'
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckIcon />
|
||||||
data-slot='checkbox-indicator'
|
</CheckboxPrimitive.Indicator>
|
||||||
className='grid place-content-center text-current transition-none [&>svg]:size-3.5'
|
</CheckboxPrimitive.Root>
|
||||||
>
|
);
|
||||||
<CheckIcon />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox };
|
export { Checkbox };
|
||||||
|
|||||||
@@ -2,32 +2,28 @@
|
|||||||
|
|
||||||
import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
|
import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
|
||||||
|
|
||||||
function Collapsible({
|
const Collapsible = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) => (
|
||||||
return <CollapsiblePrimitive.Root data-slot='collapsible' {...props} />;
|
<CollapsiblePrimitive.Root data-slot='collapsible' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function CollapsibleTrigger({
|
const CollapsibleTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => (
|
||||||
return (
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
<CollapsiblePrimitive.CollapsibleTrigger
|
data-slot='collapsible-trigger'
|
||||||
data-slot='collapsible-trigger'
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollapsibleContent({
|
const CollapsibleContent = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) => (
|
||||||
return (
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
<CollapsiblePrimitive.CollapsibleContent
|
data-slot='collapsible-content'
|
||||||
data-slot='collapsible-content'
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
|
|||||||
@@ -15,43 +15,42 @@ import {
|
|||||||
|
|
||||||
const Combobox = ComboboxPrimitive.Root;
|
const Combobox = ComboboxPrimitive.Root;
|
||||||
|
|
||||||
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
const ComboboxValue = ({ ...props }: ComboboxPrimitive.Value.Props) => (
|
||||||
return <ComboboxPrimitive.Value data-slot='combobox-value' {...props} />;
|
<ComboboxPrimitive.Value data-slot='combobox-value' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function ComboboxTrigger({
|
const ComboboxTrigger = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Trigger.Props) {
|
}: ComboboxPrimitive.Trigger.Props) => (
|
||||||
return (
|
<ComboboxPrimitive.Trigger
|
||||||
<ComboboxPrimitive.Trigger
|
data-slot='combobox-trigger'
|
||||||
data-slot='combobox-trigger'
|
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
||||||
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />
|
||||||
<ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />
|
</ComboboxPrimitive.Trigger>
|
||||||
</ComboboxPrimitive.Trigger>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
const ComboboxClear = ({
|
||||||
return (
|
className,
|
||||||
<ComboboxPrimitive.Clear
|
...props
|
||||||
data-slot='combobox-clear'
|
}: ComboboxPrimitive.Clear.Props) => (
|
||||||
className={cn(className)}
|
<ComboboxPrimitive.Clear
|
||||||
{...props}
|
data-slot='combobox-clear'
|
||||||
render={
|
className={cn(className)}
|
||||||
<InputGroupButton variant='ghost' size='icon-xs'>
|
{...props}
|
||||||
<XIcon className='pointer-events-none' />
|
render={
|
||||||
</InputGroupButton>
|
<InputGroupButton variant='ghost' size='icon-xs'>
|
||||||
}
|
<XIcon className='pointer-events-none' />
|
||||||
/>
|
</InputGroupButton>
|
||||||
);
|
}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function ComboboxInput({
|
const ComboboxInput = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -61,32 +60,30 @@ function ComboboxInput({
|
|||||||
}: ComboboxPrimitive.Input.Props & {
|
}: ComboboxPrimitive.Input.Props & {
|
||||||
showTrigger?: boolean;
|
showTrigger?: boolean;
|
||||||
showClear?: boolean;
|
showClear?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<InputGroup className={cn('w-auto', className)}>
|
||||||
<InputGroup className={cn('w-auto', className)}>
|
<ComboboxPrimitive.Input
|
||||||
<ComboboxPrimitive.Input
|
render={<InputGroupInput disabled={disabled} />}
|
||||||
render={<InputGroupInput disabled={disabled} />}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
<InputGroupAddon align='inline-end'>
|
||||||
<InputGroupAddon align='inline-end'>
|
{showTrigger && (
|
||||||
{showTrigger && (
|
<InputGroupButton
|
||||||
<InputGroupButton
|
size='icon-xs'
|
||||||
size='icon-xs'
|
variant='ghost'
|
||||||
variant='ghost'
|
render={<ComboboxTrigger />}
|
||||||
render={<ComboboxTrigger />}
|
data-slot='input-group-button'
|
||||||
data-slot='input-group-button'
|
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
|
||||||
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
|
disabled={disabled}
|
||||||
disabled={disabled}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{showClear && <ComboboxClear disabled={disabled} />}
|
||||||
{showClear && <ComboboxClear disabled={disabled} />}
|
</InputGroupAddon>
|
||||||
</InputGroupAddon>
|
{children}
|
||||||
{children}
|
</InputGroup>
|
||||||
</InputGroup>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxContent({
|
const ComboboxContent = ({
|
||||||
className,
|
className,
|
||||||
side = 'bottom',
|
side = 'bottom',
|
||||||
sideOffset = 6,
|
sideOffset = 6,
|
||||||
@@ -98,191 +95,178 @@ function ComboboxContent({
|
|||||||
Pick<
|
Pick<
|
||||||
ComboboxPrimitive.Positioner.Props,
|
ComboboxPrimitive.Positioner.Props,
|
||||||
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
|
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
|
||||||
>) {
|
>) => (
|
||||||
return (
|
<ComboboxPrimitive.Portal>
|
||||||
<ComboboxPrimitive.Portal>
|
<ComboboxPrimitive.Positioner
|
||||||
<ComboboxPrimitive.Positioner
|
side={side}
|
||||||
side={side}
|
sideOffset={sideOffset}
|
||||||
sideOffset={sideOffset}
|
align={align}
|
||||||
align={align}
|
alignOffset={alignOffset}
|
||||||
alignOffset={alignOffset}
|
anchor={anchor}
|
||||||
anchor={anchor}
|
className='isolate z-50'
|
||||||
className='isolate z-50'
|
>
|
||||||
>
|
<ComboboxPrimitive.Popup
|
||||||
<ComboboxPrimitive.Popup
|
data-slot='combobox-content'
|
||||||
data-slot='combobox-content'
|
data-chips={!!anchor}
|
||||||
data-chips={!!anchor}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</ComboboxPrimitive.Positioner>
|
||||||
</ComboboxPrimitive.Positioner>
|
</ComboboxPrimitive.Portal>
|
||||||
</ComboboxPrimitive.Portal>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
const ComboboxList = ({
|
||||||
return (
|
className,
|
||||||
<ComboboxPrimitive.List
|
...props
|
||||||
data-slot='combobox-list'
|
}: ComboboxPrimitive.List.Props) => (
|
||||||
className={cn(
|
<ComboboxPrimitive.List
|
||||||
'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',
|
data-slot='combobox-list'
|
||||||
className,
|
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',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function ComboboxItem({
|
const ComboboxItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Item.Props) {
|
}: ComboboxPrimitive.Item.Props) => (
|
||||||
return (
|
<ComboboxPrimitive.Item
|
||||||
<ComboboxPrimitive.Item
|
data-slot='combobox-item'
|
||||||
data-slot='combobox-item'
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<ComboboxPrimitive.ItemIndicator
|
||||||
<ComboboxPrimitive.ItemIndicator
|
render={
|
||||||
render={
|
<span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center'>
|
||||||
<span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center'>
|
<CheckIcon className='pointer-events-none' />
|
||||||
<CheckIcon className='pointer-events-none' />
|
</span>
|
||||||
</span>
|
}
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ComboboxPrimitive.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Group
|
|
||||||
data-slot='combobox-group'
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
);
|
</ComboboxPrimitive.Item>
|
||||||
}
|
);
|
||||||
|
|
||||||
function ComboboxLabel({
|
const ComboboxGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.GroupLabel.Props) {
|
}: ComboboxPrimitive.Group.Props) => (
|
||||||
return (
|
<ComboboxPrimitive.Group
|
||||||
<ComboboxPrimitive.GroupLabel
|
data-slot='combobox-group'
|
||||||
data-slot='combobox-label'
|
className={cn(className)}
|
||||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
const ComboboxLabel = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Separator.Props) {
|
}: ComboboxPrimitive.GroupLabel.Props) => (
|
||||||
return (
|
<ComboboxPrimitive.GroupLabel
|
||||||
<ComboboxPrimitive.Separator
|
data-slot='combobox-label'
|
||||||
data-slot='combobox-separator'
|
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
{...props}
|
||||||
{...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,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
||||||
ComboboxPrimitive.Chips.Props) {
|
ComboboxPrimitive.Chips.Props) => (
|
||||||
return (
|
<ComboboxPrimitive.Chips
|
||||||
<ComboboxPrimitive.Chips
|
data-slot='combobox-chips'
|
||||||
data-slot='combobox-chips'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChip({
|
const ComboboxChip = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showRemove = true,
|
showRemove = true,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Chip.Props & {
|
}: ComboboxPrimitive.Chip.Props & {
|
||||||
showRemove?: boolean;
|
showRemove?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ComboboxPrimitive.Chip
|
||||||
<ComboboxPrimitive.Chip
|
data-slot='combobox-chip'
|
||||||
data-slot='combobox-chip'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
{showRemove && (
|
||||||
{showRemove && (
|
<ComboboxPrimitive.ChipRemove
|
||||||
<ComboboxPrimitive.ChipRemove
|
className='-ml-1 opacity-50 hover:opacity-100'
|
||||||
className='-ml-1 opacity-50 hover:opacity-100'
|
data-slot='combobox-chip-remove'
|
||||||
data-slot='combobox-chip-remove'
|
render={
|
||||||
render={
|
<Button variant='ghost' size='icon-xs'>
|
||||||
<Button variant='ghost' size='icon-xs'>
|
<XIcon className='pointer-events-none' />
|
||||||
<XIcon className='pointer-events-none' />
|
</Button>
|
||||||
</Button>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</ComboboxPrimitive.Chip>
|
||||||
</ComboboxPrimitive.Chip>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChipsInput({
|
const ComboboxChipsInput = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Input.Props) {
|
}: ComboboxPrimitive.Input.Props) => (
|
||||||
return (
|
<ComboboxPrimitive.Input
|
||||||
<ComboboxPrimitive.Input
|
data-slot='combobox-chip-input'
|
||||||
data-slot='combobox-chip-input'
|
className={cn('min-w-16 flex-1 outline-none', className)}
|
||||||
className={cn('min-w-16 flex-1 outline-none', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useComboboxAnchor() {
|
const useComboboxAnchor = () => React.useRef<HTMLDivElement | null>(null);
|
||||||
return React.useRef<HTMLDivElement | null>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Combobox,
|
Combobox,
|
||||||
|
|||||||
@@ -15,23 +15,21 @@ import {
|
|||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
} from '@gib/ui';
|
} from '@gib/ui';
|
||||||
|
|
||||||
function Command({
|
const Command = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
}: React.ComponentProps<typeof CommandPrimitive>) => (
|
||||||
return (
|
<CommandPrimitive
|
||||||
<CommandPrimitive
|
data-slot='command'
|
||||||
data-slot='command'
|
className={cn(
|
||||||
className={cn(
|
'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-xl! p-1',
|
||||||
'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-xl! p-1',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandDialog({
|
const CommandDialog = ({
|
||||||
title = 'Command Palette',
|
title = 'Command Palette',
|
||||||
description = 'Search for a command to run...',
|
description = 'Search for a command to run...',
|
||||||
children,
|
children,
|
||||||
@@ -43,142 +41,126 @@ function CommandDialog({
|
|||||||
description?: string;
|
description?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<Dialog {...props}>
|
||||||
<Dialog {...props}>
|
<DialogHeader className='sr-only'>
|
||||||
<DialogHeader className='sr-only'>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
<DialogDescription>{description}</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
<DialogContent
|
||||||
<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(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
showCloseButton={showCloseButton}
|
{...props}
|
||||||
>
|
/>
|
||||||
{children}
|
<InputGroupAddon>
|
||||||
</DialogContent>
|
<SearchIcon className='size-4 shrink-0 opacity-50' />
|
||||||
</Dialog>
|
</InputGroupAddon>
|
||||||
);
|
</InputGroup>
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
function CommandInput({
|
const CommandList = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
}: React.ComponentProps<typeof CommandPrimitive.List>) => (
|
||||||
return (
|
<CommandPrimitive.List
|
||||||
<div data-slot='command-input-wrapper' className='p-1 pb-0'>
|
data-slot='command-list'
|
||||||
<InputGroup className='bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!'>
|
className={cn(
|
||||||
<CommandPrimitive.Input
|
'no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none',
|
||||||
data-slot='command-input'
|
className,
|
||||||
className={cn(
|
)}
|
||||||
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
{...props}
|
||||||
className,
|
/>
|
||||||
)}
|
);
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
|
||||||
<SearchIcon className='size-4 shrink-0 opacity-50' />
|
|
||||||
</InputGroupAddon>
|
|
||||||
</InputGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandList({
|
const CommandEmpty = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) => (
|
||||||
return (
|
<CommandPrimitive.Empty
|
||||||
<CommandPrimitive.List
|
data-slot='command-empty'
|
||||||
data-slot='command-list'
|
className={cn('py-6 text-center text-sm', className)}
|
||||||
className={cn(
|
{...props}
|
||||||
'no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none',
|
/>
|
||||||
className,
|
);
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandEmpty({
|
const CommandGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) => (
|
||||||
return (
|
<CommandPrimitive.Group
|
||||||
<CommandPrimitive.Empty
|
data-slot='command-group'
|
||||||
data-slot='command-empty'
|
className={cn(
|
||||||
className={cn('py-6 text-center text-sm', className)}
|
'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',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function CommandGroup({
|
const CommandSeparator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) => (
|
||||||
return (
|
<CommandPrimitive.Separator
|
||||||
<CommandPrimitive.Group
|
data-slot='command-separator'
|
||||||
data-slot='command-group'
|
className={cn('bg-border -mx-1 h-px', className)}
|
||||||
className={cn(
|
{...props}
|
||||||
'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 CommandSeparator({
|
const CommandItem = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) => (
|
||||||
return (
|
<CommandPrimitive.Item
|
||||||
<CommandPrimitive.Item
|
data-slot='command-item'
|
||||||
data-slot='command-item'
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<CheckIcon className='ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100' />
|
||||||
<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>
|
||||||
</CommandPrimitive.Item>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandShortcut({
|
const CommandShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
data-slot='command-shortcut'
|
||||||
data-slot='command-shortcut'
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
|
||||||
'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
|
|||||||
@@ -6,79 +6,69 @@ import { ContextMenu as ContextMenuPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function ContextMenu({
|
const ContextMenu = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) => (
|
||||||
return <ContextMenuPrimitive.Root data-slot='context-menu' {...props} />;
|
<ContextMenuPrimitive.Root data-slot='context-menu' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function ContextMenuTrigger({
|
const ContextMenuTrigger = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) => (
|
||||||
return (
|
<ContextMenuPrimitive.Trigger
|
||||||
<ContextMenuPrimitive.Trigger
|
data-slot='context-menu-trigger'
|
||||||
data-slot='context-menu-trigger'
|
className={cn('select-none', className)}
|
||||||
className={cn('select-none', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuGroup({
|
const ContextMenuGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) => (
|
||||||
return (
|
<ContextMenuPrimitive.Group data-slot='context-menu-group' {...props} />
|
||||||
<ContextMenuPrimitive.Group data-slot='context-menu-group' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuPortal({
|
const ContextMenuPortal = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) => (
|
||||||
return (
|
<ContextMenuPrimitive.Portal data-slot='context-menu-portal' {...props} />
|
||||||
<ContextMenuPrimitive.Portal data-slot='context-menu-portal' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSub({
|
const ContextMenuSub = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) => (
|
||||||
return <ContextMenuPrimitive.Sub data-slot='context-menu-sub' {...props} />;
|
<ContextMenuPrimitive.Sub data-slot='context-menu-sub' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function ContextMenuRadioGroup({
|
const ContextMenuRadioGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) => (
|
||||||
return (
|
<ContextMenuPrimitive.RadioGroup
|
||||||
<ContextMenuPrimitive.RadioGroup
|
data-slot='context-menu-radio-group'
|
||||||
data-slot='context-menu-radio-group'
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuContent({
|
const ContextMenuContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content> & {
|
||||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ContextMenuPrimitive.Portal>
|
||||||
<ContextMenuPrimitive.Portal>
|
<ContextMenuPrimitive.Content
|
||||||
<ContextMenuPrimitive.Content
|
data-slot='context-menu-content'
|
||||||
data-slot='context-menu-content'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</ContextMenuPrimitive.Portal>
|
||||||
</ContextMenuPrimitive.Portal>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuItem({
|
const ContextMenuItem = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
@@ -86,62 +76,56 @@ function ContextMenuItem({
|
|||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
variant?: 'default' | 'destructive';
|
variant?: 'default' | 'destructive';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ContextMenuPrimitive.Item
|
||||||
<ContextMenuPrimitive.Item
|
data-slot='context-menu-item'
|
||||||
data-slot='context-menu-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
data-variant={variant}
|
||||||
data-variant={variant}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSubTrigger({
|
const ContextMenuSubTrigger = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ContextMenuPrimitive.SubTrigger
|
||||||
<ContextMenuPrimitive.SubTrigger
|
data-slot='context-menu-sub-trigger'
|
||||||
data-slot='context-menu-sub-trigger'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
|
||||||
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSubContent({
|
const ContextMenuSubContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) => (
|
||||||
return (
|
<ContextMenuPrimitive.SubContent
|
||||||
<ContextMenuPrimitive.SubContent
|
data-slot='context-menu-sub-content'
|
||||||
data-slot='context-menu-sub-content'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuCheckboxItem({
|
const ContextMenuCheckboxItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
@@ -149,104 +133,94 @@ function ContextMenuCheckboxItem({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
data-slot='context-menu-checkbox-item'
|
||||||
data-slot='context-menu-checkbox-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
checked={checked}
|
||||||
checked={checked}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<span className='pointer-events-none absolute right-2'>
|
||||||
<span className='pointer-events-none absolute right-2'>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<CheckIcon />
|
||||||
<CheckIcon />
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
{children}
|
||||||
{children}
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuRadioItem({
|
const ContextMenuRadioItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ContextMenuPrimitive.RadioItem
|
||||||
<ContextMenuPrimitive.RadioItem
|
data-slot='context-menu-radio-item'
|
||||||
data-slot='context-menu-radio-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<span className='pointer-events-none absolute right-2'>
|
||||||
<span className='pointer-events-none absolute right-2'>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<CheckIcon />
|
||||||
<CheckIcon />
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
{children}
|
||||||
{children}
|
</ContextMenuPrimitive.RadioItem>
|
||||||
</ContextMenuPrimitive.RadioItem>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuLabel({
|
const ContextMenuLabel = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ContextMenuPrimitive.Label
|
||||||
<ContextMenuPrimitive.Label
|
data-slot='context-menu-label'
|
||||||
data-slot='context-menu-label'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
|
||||||
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSeparator({
|
const ContextMenuSeparator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) => (
|
||||||
return (
|
<ContextMenuPrimitive.Separator
|
||||||
<ContextMenuPrimitive.Separator
|
data-slot='context-menu-separator'
|
||||||
data-slot='context-menu-separator'
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuShortcut({
|
const ContextMenuShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
data-slot='context-menu-shortcut'
|
||||||
data-slot='context-menu-shortcut'
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||||
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
|||||||
@@ -6,148 +6,136 @@ import { Dialog as DialogPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { Button, cn } from '@gib/ui';
|
import { Button, cn } from '@gib/ui';
|
||||||
|
|
||||||
function Dialog({
|
const Dialog = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) => (
|
||||||
return <DialogPrimitive.Root data-slot='dialog' {...props} />;
|
<DialogPrimitive.Root data-slot='dialog' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DialogTrigger({
|
const DialogTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) => (
|
||||||
return <DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />;
|
<DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DialogPortal({
|
const DialogPortal = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) => (
|
||||||
return <DialogPrimitive.Portal data-slot='dialog-portal' {...props} />;
|
<DialogPrimitive.Portal data-slot='dialog-portal' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DialogClose({
|
const DialogClose = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) => (
|
||||||
return <DialogPrimitive.Close data-slot='dialog-close' {...props} />;
|
<DialogPrimitive.Close data-slot='dialog-close' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DialogOverlay({
|
const DialogOverlay = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) => (
|
||||||
return (
|
<DialogPrimitive.Overlay
|
||||||
<DialogPrimitive.Overlay
|
data-slot='dialog-overlay'
|
||||||
data-slot='dialog-overlay'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogContent({
|
const DialogContent = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<DialogPortal>
|
||||||
<DialogPortal>
|
<DialogOverlay />
|
||||||
<DialogOverlay />
|
<DialogPrimitive.Content
|
||||||
<DialogPrimitive.Content
|
data-slot='dialog-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'
|
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close asChild>
|
<DialogPrimitive.Close data-slot='dialog-close' asChild>
|
||||||
<Button variant='outline'>Close</Button>
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
className='absolute top-2 right-2'
|
||||||
|
size='icon-sm'
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className='sr-only'>Close</span>
|
||||||
|
</Button>
|
||||||
</DialogPrimitive.Close>
|
</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,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) => (
|
||||||
return (
|
<DialogPrimitive.Title
|
||||||
<DialogPrimitive.Title
|
data-slot='dialog-title'
|
||||||
data-slot='dialog-title'
|
className={cn('text-base leading-none font-medium', className)}
|
||||||
className={cn('text-base leading-none font-medium', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDescription({
|
const DialogDescription = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) => (
|
||||||
return (
|
<DialogPrimitive.Description
|
||||||
<DialogPrimitive.Description
|
data-slot='dialog-description'
|
||||||
data-slot='dialog-description'
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
|
||||||
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|||||||
@@ -5,117 +5,105 @@ import { Drawer as DrawerPrimitive } from 'vaul';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Drawer({
|
const Drawer = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||||
return <DrawerPrimitive.Root data-slot='drawer' {...props} />;
|
<DrawerPrimitive.Root data-slot='drawer' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DrawerTrigger({
|
const DrawerTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) => (
|
||||||
return <DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />;
|
<DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DrawerPortal({
|
const DrawerPortal = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) => (
|
||||||
return <DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />;
|
<DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DrawerClose({
|
const DrawerClose = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) => (
|
||||||
return <DrawerPrimitive.Close data-slot='drawer-close' {...props} />;
|
<DrawerPrimitive.Close data-slot='drawer-close' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DrawerOverlay({
|
const DrawerOverlay = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) => (
|
||||||
return (
|
<DrawerPrimitive.Overlay
|
||||||
<DrawerPrimitive.Overlay
|
data-slot='drawer-overlay'
|
||||||
data-slot='drawer-overlay'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerContent({
|
const DrawerContent = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) => (
|
||||||
return (
|
<DrawerPortal data-slot='drawer-portal'>
|
||||||
<DrawerPortal data-slot='drawer-portal'>
|
<DrawerOverlay />
|
||||||
<DrawerOverlay />
|
<DrawerPrimitive.Content
|
||||||
<DrawerPrimitive.Content
|
data-slot='drawer-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'
|
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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'>) {
|
const DrawerHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='drawer-header'
|
||||||
data-slot='drawer-footer'
|
className={cn(
|
||||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
'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',
|
||||||
{...props}
|
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,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) => (
|
||||||
return (
|
<DrawerPrimitive.Title
|
||||||
<DrawerPrimitive.Title
|
data-slot='drawer-title'
|
||||||
data-slot='drawer-title'
|
className={cn('text-foreground text-base font-medium', className)}
|
||||||
className={cn('text-foreground text-base font-medium', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerDescription({
|
const DrawerDescription = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) => (
|
||||||
return (
|
<DrawerPrimitive.Description
|
||||||
<DrawerPrimitive.Description
|
data-slot='drawer-description'
|
||||||
data-slot='drawer-description'
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
className={cn('text-muted-foreground text-sm', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Drawer,
|
Drawer,
|
||||||
|
|||||||
@@ -6,62 +6,51 @@ import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function DropdownMenu({
|
const DropdownMenu = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) => (
|
||||||
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
|
<DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
const DropdownMenuPortal = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
|
||||||
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
const DropdownMenuTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Trigger data-slot='dropdown-menu-trigger' {...props} />
|
||||||
<DropdownMenuPrimitive.Trigger
|
);
|
||||||
data-slot='dropdown-menu-trigger'
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
const DropdownMenuContent = ({
|
||||||
className,
|
className,
|
||||||
align = 'start',
|
align = 'start',
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Content
|
||||||
<DropdownMenuPrimitive.Content
|
data-slot='dropdown-menu-content'
|
||||||
data-slot='dropdown-menu-content'
|
sideOffset={sideOffset}
|
||||||
sideOffset={sideOffset}
|
align={align}
|
||||||
align={align}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</DropdownMenuPrimitive.Portal>
|
||||||
</DropdownMenuPrimitive.Portal>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
const DropdownMenuGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
|
||||||
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
const DropdownMenuItem = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
@@ -69,22 +58,20 @@ function DropdownMenuItem({
|
|||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
variant?: 'default' | 'destructive';
|
variant?: 'default' | 'destructive';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Item
|
||||||
<DropdownMenuPrimitive.Item
|
data-slot='dropdown-menu-item'
|
||||||
data-slot='dropdown-menu-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
data-variant={variant}
|
||||||
data-variant={variant}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
const DropdownMenuCheckboxItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
@@ -92,167 +79,151 @@ function DropdownMenuCheckboxItem({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
data-slot='dropdown-menu-checkbox-item'
|
||||||
data-slot='dropdown-menu-checkbox-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
checked={checked}
|
||||||
checked={checked}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
|
<span
|
||||||
|
className='pointer-events-none absolute right-2 flex items-center justify-center'
|
||||||
|
data-slot='dropdown-menu-checkbox-item-indicator'
|
||||||
>
|
>
|
||||||
<span
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
className='pointer-events-none absolute right-2 flex items-center justify-center'
|
<CheckIcon />
|
||||||
data-slot='dropdown-menu-checkbox-item-indicator'
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
>
|
</span>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
{children}
|
||||||
<CheckIcon />
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
);
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
const DropdownMenuRadioGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
data-slot='dropdown-menu-radio-group'
|
||||||
data-slot='dropdown-menu-radio-group'
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
const DropdownMenuRadioItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<DropdownMenuPrimitive.RadioItem
|
||||||
<DropdownMenuPrimitive.RadioItem
|
data-slot='dropdown-menu-radio-item'
|
||||||
data-slot='dropdown-menu-radio-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
|
<span
|
||||||
|
className='pointer-events-none absolute right-2 flex items-center justify-center'
|
||||||
|
data-slot='dropdown-menu-radio-item-indicator'
|
||||||
>
|
>
|
||||||
<span
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
className='pointer-events-none absolute right-2 flex items-center justify-center'
|
<CheckIcon />
|
||||||
data-slot='dropdown-menu-radio-item-indicator'
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
>
|
</span>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
{children}
|
||||||
<CheckIcon />
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
);
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
const DropdownMenuLabel = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Label
|
||||||
<DropdownMenuPrimitive.Label
|
data-slot='dropdown-menu-label'
|
||||||
data-slot='dropdown-menu-label'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
|
||||||
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
const DropdownMenuSeparator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.Separator
|
||||||
<DropdownMenuPrimitive.Separator
|
data-slot='dropdown-menu-separator'
|
||||||
data-slot='dropdown-menu-separator'
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
const DropdownMenuShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
data-slot='dropdown-menu-shortcut'
|
||||||
data-slot='dropdown-menu-shortcut'
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||||
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
const DropdownMenuSub = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) => (
|
||||||
return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
|
<DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
const DropdownMenuSubTrigger = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
data-slot='dropdown-menu-sub-trigger'
|
||||||
data-slot='dropdown-menu-sub-trigger'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
|
||||||
<ChevronRightIcon className='cn-rtl-flip ml-auto' />
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
const DropdownMenuSubContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) => (
|
||||||
return (
|
<DropdownMenuPrimitive.SubContent
|
||||||
<DropdownMenuPrimitive.SubContent
|
data-slot='dropdown-menu-sub-content'
|
||||||
data-slot='dropdown-menu-sub-content'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -3,28 +3,24 @@ import { cva } from 'class-variance-authority';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
|
const Empty = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='empty'
|
||||||
data-slot='empty'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
const EmptyHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='empty-header'
|
||||||
data-slot='empty-header'
|
className={cn('flex max-w-sm flex-col items-center gap-2', className)}
|
||||||
className={cn('flex max-w-sm flex-col items-center gap-2', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyMediaVariants = cva(
|
const emptyMediaVariants = cva(
|
||||||
'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
'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,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='empty-icon'
|
||||||
data-slot='empty-icon'
|
data-variant={variant}
|
||||||
data-variant={variant}
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
className={cn(emptyMediaVariants({ variant, className }))}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
const EmptyTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='empty-title'
|
||||||
data-slot='empty-title'
|
className={cn('text-sm font-medium tracking-tight', className)}
|
||||||
className={cn('text-sm font-medium tracking-tight', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
const EmptyDescription = ({
|
||||||
return (
|
className,
|
||||||
<div
|
...props
|
||||||
data-slot='empty-description'
|
}: React.ComponentProps<'p'>) => (
|
||||||
className={cn(
|
<div
|
||||||
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
data-slot='empty-description'
|
||||||
className,
|
className={cn(
|
||||||
)}
|
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
|
const EmptyContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='empty-content'
|
||||||
data-slot='empty-content'
|
className={cn(
|
||||||
className={cn(
|
'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
|
||||||
'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Empty,
|
Empty,
|
||||||
|
|||||||
@@ -6,58 +6,52 @@ import { cva } from 'class-variance-authority';
|
|||||||
|
|
||||||
import { cn, Label, Separator } from '@gib/ui';
|
import { cn, Label, Separator } from '@gib/ui';
|
||||||
|
|
||||||
export function FieldSet({
|
export const FieldSet = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'fieldset'>) {
|
}: React.ComponentProps<'fieldset'>) => (
|
||||||
return (
|
<fieldset
|
||||||
<fieldset
|
data-slot='field-set'
|
||||||
data-slot='field-set'
|
className={cn(
|
||||||
className={cn(
|
'flex flex-col gap-6',
|
||||||
'flex flex-col gap-6',
|
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
||||||
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldLegend({
|
export const FieldLegend = ({
|
||||||
className,
|
className,
|
||||||
variant = 'legend',
|
variant = 'legend',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
|
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) => (
|
||||||
return (
|
<legend
|
||||||
<legend
|
data-slot='field-legend'
|
||||||
data-slot='field-legend'
|
data-variant={variant}
|
||||||
data-variant={variant}
|
className={cn(
|
||||||
className={cn(
|
'mb-3 font-medium',
|
||||||
'mb-3 font-medium',
|
'data-[variant=legend]:text-base',
|
||||||
'data-[variant=legend]:text-base',
|
'data-[variant=label]:text-sm',
|
||||||
'data-[variant=label]:text-sm',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldGroup({
|
export const FieldGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='field-group'
|
||||||
data-slot='field-group'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
const fieldVariants = cva(
|
||||||
'group/field data-[invalid=true]:text-destructive flex w-full gap-3',
|
'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,
|
className,
|
||||||
orientation = 'vertical',
|
orientation = 'vertical',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
role='group'
|
||||||
role='group'
|
data-slot='field'
|
||||||
data-slot='field'
|
data-orientation={orientation}
|
||||||
data-orientation={orientation}
|
className={cn(fieldVariants({ orientation }), className)}
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldContent({
|
export const FieldContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='field-content'
|
||||||
data-slot='field-content'
|
className={cn(
|
||||||
className={cn(
|
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
|
||||||
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldLabel({
|
export const FieldLabel = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Label>) {
|
}: React.ComponentProps<typeof Label>) => (
|
||||||
return (
|
<Label
|
||||||
<Label
|
data-slot='field-label'
|
||||||
data-slot='field-label'
|
className={cn(
|
||||||
className={cn(
|
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
||||||
'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-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',
|
||||||
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldTitle({
|
export const FieldTitle = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='field-label'
|
||||||
data-slot='field-label'
|
className={cn(
|
||||||
className={cn(
|
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
||||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldDescription({
|
export const FieldDescription = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'p'>) {
|
}: React.ComponentProps<'p'>) => (
|
||||||
return (
|
<p
|
||||||
<p
|
data-slot='field-description'
|
||||||
data-slot='field-description'
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
||||||
'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',
|
||||||
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldSeparator({
|
export const FieldSeparator = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='field-separator'
|
||||||
data-slot='field-separator'
|
data-content={!!children}
|
||||||
data-content={!!children}
|
className={cn(
|
||||||
className={cn(
|
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
||||||
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<Separator className='absolute inset-0 top-1/2' />
|
||||||
<Separator className='absolute inset-0 top-1/2' />
|
{children && (
|
||||||
{children && (
|
<span
|
||||||
<span
|
className='bg-background text-muted-foreground relative mx-auto block w-fit px-2'
|
||||||
className='bg-background text-muted-foreground relative mx-auto block w-fit px-2'
|
data-slot='field-separator-content'
|
||||||
data-slot='field-separator-content'
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FieldError({
|
export const FieldError = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
errors: maybeErrors,
|
errors: maybeErrors,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
errors?: ({ message?: string } | undefined)[];
|
errors?: ({ message?: string } | undefined)[];
|
||||||
}) {
|
}) => {
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return children;
|
return children;
|
||||||
@@ -244,4 +226,4 @@ export function FieldError({
|
|||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import { cn, Label } from '@gib/ui';
|
|||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|
||||||
type FormFieldContextValue<
|
interface FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> = {
|
> {
|
||||||
name: TName;
|
name: TName;
|
||||||
};
|
}
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue,
|
{} as FormFieldContextValue,
|
||||||
@@ -62,15 +62,15 @@ const useFormField = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormItemContextValue = {
|
interface FormItemContextValue {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue,
|
{} as FormItemContextValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
const FormItem = ({ className, ...props }: React.ComponentProps<'div'>) => {
|
||||||
const id = React.useId();
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,12 +82,12 @@ function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
/>
|
/>
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function FormLabel({
|
const FormLabel = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) => {
|
||||||
const { error, formItemId } = useFormField();
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -99,9 +99,9 @@ function FormLabel({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
const FormControl = ({ ...props }: React.ComponentProps<typeof Slot>) => {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
useFormField();
|
useFormField();
|
||||||
|
|
||||||
@@ -118,9 +118,12 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
const FormDescription = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'p'>) => {
|
||||||
const { formDescriptionId } = useFormField();
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -131,9 +134,9 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
const FormMessage = ({ className, ...props }: React.ComponentProps<'p'>) => {
|
||||||
const { error, formMessageId } = useFormField();
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message ?? '') : props.children;
|
const body = error ? String(error?.message ?? '') : props.children;
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
|||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
|
|||||||
2
packages/ui/src/hooks/index.tsx
Normal file
2
packages/ui/src/hooks/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { useIsMobile } from './use-mobile';
|
||||||
|
export { useOnClickOutside } from './use-on-click-outside';
|
||||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
export function useIsMobile() {
|
export const useIsMobile = () => {
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
@@ -18,4 +18,4 @@ export function useIsMobile() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return !!isMobile;
|
return !!isMobile;
|
||||||
}
|
};
|
||||||
|
|||||||
60
packages/ui/src/hooks/use-on-click-outside.tsx
Normal file
60
packages/ui/src/hooks/use-on-click-outside.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { MousePointerClick, X } from 'lucide-react';
|
||||||
|
|
||||||
|
type EventType =
|
||||||
|
| 'mousedown'
|
||||||
|
| 'mouseup'
|
||||||
|
| 'touchstart'
|
||||||
|
| 'touchend'
|
||||||
|
| 'focusin'
|
||||||
|
| 'focusout';
|
||||||
|
|
||||||
|
export const useOnClickOutside = <T extends Element>(
|
||||||
|
ref: React.RefObject<T | null> | React.RefObject<T | null>[],
|
||||||
|
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
|
||||||
|
eventType: EventType = 'mousedown',
|
||||||
|
eventListenerOptions: AddEventListenerOptions = {},
|
||||||
|
): void => {
|
||||||
|
const savedHandler = React.useRef(handler);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
savedHandler.current = handler;
|
||||||
|
}, [handler]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const listener = (event: MouseEvent | TouchEvent | FocusEvent) => {
|
||||||
|
const target = event.target as Node;
|
||||||
|
|
||||||
|
// Do nothing if the target is not connected element with document
|
||||||
|
if (!target.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOutside = Array.isArray(ref)
|
||||||
|
? ref
|
||||||
|
.filter((r) => Boolean(r.current))
|
||||||
|
.every((r) => r.current && !r.current.contains(target))
|
||||||
|
: ref.current && !ref.current.contains(target);
|
||||||
|
|
||||||
|
if (isOutside) {
|
||||||
|
savedHandler.current(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
eventType,
|
||||||
|
listener as EventListener,
|
||||||
|
eventListenerOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(
|
||||||
|
eventType,
|
||||||
|
listener as EventListener,
|
||||||
|
eventListenerOptions,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [ref, eventType, eventListenerOptions]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { EventType };
|
||||||
@@ -5,40 +5,36 @@ import { HoverCard as HoverCardPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function HoverCard({
|
const HoverCard = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) => (
|
||||||
return <HoverCardPrimitive.Root data-slot='hover-card' {...props} />;
|
<HoverCardPrimitive.Root data-slot='hover-card' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function HoverCardTrigger({
|
const HoverCardTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) => (
|
||||||
return (
|
<HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
|
||||||
<HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HoverCardContent({
|
const HoverCardContent = ({
|
||||||
className,
|
className,
|
||||||
align = 'center',
|
align = 'center',
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) => (
|
||||||
return (
|
<HoverCardPrimitive.Portal data-slot='hover-card-portal'>
|
||||||
<HoverCardPrimitive.Portal data-slot='hover-card-portal'>
|
<HoverCardPrimitive.Content
|
||||||
<HoverCardPrimitive.Content
|
data-slot='hover-card-content'
|
||||||
data-slot='hover-card-content'
|
align={align}
|
||||||
align={align}
|
sideOffset={sideOffset}
|
||||||
sideOffset={sideOffset}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</HoverCardPrimitive.Portal>
|
||||||
</HoverCardPrimitive.Portal>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export const Cropper = ({
|
|||||||
</ImageCrop>
|
</ImageCrop>
|
||||||
);
|
);
|
||||||
|
|
||||||
export function Demo() {
|
export const Demo = () => {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [croppedImage, setCroppedImage] = useState<string | null>(null);
|
const [croppedImage, setCroppedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -432,4 +432,4 @@ export function Demo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -381,4 +381,4 @@ export {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
} from './tooltip';
|
} from './tooltip';
|
||||||
export { useIsMobile } from './hooks/use-mobile';
|
export { useIsMobile, useOnClickOutside } from './hooks';
|
||||||
|
|||||||
@@ -6,19 +6,17 @@ import { cva } from 'class-variance-authority';
|
|||||||
|
|
||||||
import { Button, cn, Input, Textarea } from '@gib/ui';
|
import { Button, cn, Input, Textarea } from '@gib/ui';
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
const InputGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='input-group'
|
||||||
data-slot='input-group'
|
role='group'
|
||||||
role='group'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
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",
|
"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,
|
className,
|
||||||
align = 'inline-start',
|
align = 'inline-start',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
}: React.ComponentProps<'div'> &
|
||||||
return (
|
VariantProps<typeof inputGroupAddonVariants>) => (
|
||||||
<div
|
<div
|
||||||
role='group'
|
role='group'
|
||||||
data-slot='input-group-addon'
|
data-slot='input-group-addon'
|
||||||
data-align={align}
|
data-align={align}
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if ((e.target as HTMLElement).closest('button')) {
|
if ((e.target as HTMLElement).closest('button')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.currentTarget.parentElement?.querySelector('input')?.focus();
|
e.currentTarget.parentElement?.querySelector('input')?.focus();
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
const inputGroupButtonVariants = cva(
|
||||||
'flex items-center gap-2 text-sm shadow-none',
|
'flex items-center gap-2 text-sm shadow-none',
|
||||||
@@ -81,68 +78,63 @@ const inputGroupButtonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function InputGroupButton({
|
const InputGroupButton = ({
|
||||||
className,
|
className,
|
||||||
type = 'button',
|
type = 'button',
|
||||||
variant = 'ghost',
|
variant = 'ghost',
|
||||||
size = 'xs',
|
size = 'xs',
|
||||||
...props
|
...props
|
||||||
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
VariantProps<typeof inputGroupButtonVariants>) => (
|
||||||
return (
|
<Button
|
||||||
<Button
|
type={type}
|
||||||
type={type}
|
data-size={size}
|
||||||
data-size={size}
|
variant={variant}
|
||||||
variant={variant}
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
const InputGroupText = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'input'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<Input
|
className={cn(
|
||||||
data-slot='input-group-control'
|
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className={cn(
|
className,
|
||||||
'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}
|
||||||
)}
|
/>
|
||||||
{...props}
|
);
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupTextarea({
|
const InputGroupInput = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'textarea'>) {
|
}: React.ComponentProps<'input'>) => (
|
||||||
return (
|
<Input
|
||||||
<Textarea
|
data-slot='input-group-control'
|
||||||
data-slot='input-group-control'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...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 {
|
export {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
|
|||||||
@@ -6,47 +6,46 @@ import { MinusIcon } from 'lucide-react';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function InputOTP({
|
const InputOTP = ({
|
||||||
className,
|
className,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof OTPInput> & {
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<OTPInput
|
||||||
<OTPInput
|
data-slot='input-otp'
|
||||||
data-slot='input-otp'
|
containerClassName={cn(
|
||||||
containerClassName={cn(
|
'cn-input-otp flex items-center has-disabled:opacity-50',
|
||||||
'cn-input-otp flex items-center has-disabled:opacity-50',
|
containerClassName,
|
||||||
containerClassName,
|
)}
|
||||||
)}
|
spellCheck={false}
|
||||||
spellCheck={false}
|
className={cn('disabled:cursor-not-allowed', className)}
|
||||||
className={cn('disabled:cursor-not-allowed', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
const InputOTPGroup = ({
|
||||||
return (
|
className,
|
||||||
<div
|
...props
|
||||||
data-slot='input-otp-group'
|
}: React.ComponentProps<'div'>) => (
|
||||||
className={cn(
|
<div
|
||||||
'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',
|
data-slot='input-otp-group'
|
||||||
className,
|
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',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function InputOTPSlot({
|
const InputOTPSlot = ({
|
||||||
index,
|
index,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
index: number;
|
index: number;
|
||||||
}) {
|
}) => {
|
||||||
const inputOTPContext = React.useContext(OTPInputContext);
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||||
|
|
||||||
@@ -68,19 +67,17 @@ function InputOTPSlot({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
|
const InputOTPSeparator = ({ ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='input-otp-separator'
|
||||||
data-slot='input-otp-separator'
|
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
|
||||||
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
|
role='separator'
|
||||||
role='separator'
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<MinusIcon />
|
||||||
<MinusIcon />
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ import type * as React from 'react';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
const Input = ({
|
||||||
return (
|
className,
|
||||||
<input
|
type,
|
||||||
type={type}
|
...props
|
||||||
data-slot='input'
|
}: React.ComponentProps<'input'>) => (
|
||||||
className={cn(
|
<input
|
||||||
'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',
|
type={type}
|
||||||
className,
|
data-slot='input'
|
||||||
)}
|
className={cn(
|
||||||
{...props}
|
'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 };
|
export { Input };
|
||||||
|
|||||||
@@ -5,33 +5,29 @@ import { Slot } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn, Separator } from '@gib/ui';
|
import { cn, Separator } from '@gib/ui';
|
||||||
|
|
||||||
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
const ItemGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
role='list'
|
||||||
role='list'
|
data-slot='item-group'
|
||||||
data-slot='item-group'
|
className={cn(
|
||||||
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',
|
||||||
'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemSeparator({
|
const ItemSeparator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
}: React.ComponentProps<typeof Separator>) => (
|
||||||
return (
|
<Separator
|
||||||
<Separator
|
data-slot='item-separator'
|
||||||
data-slot='item-separator'
|
orientation='horizontal'
|
||||||
orientation='horizontal'
|
className={cn('my-2', className)}
|
||||||
className={cn('my-2', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemVariants = cva(
|
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',
|
'[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,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
size = 'default',
|
size = 'default',
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> &
|
}: React.ComponentProps<'div'> &
|
||||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
VariantProps<typeof itemVariants> & { asChild?: boolean }) => {
|
||||||
const Comp = asChild ? Slot.Root : 'div';
|
const Comp = asChild ? Slot.Root : 'div';
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -73,7 +69,7 @@ function Item({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const itemMediaVariants = cva(
|
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',
|
'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,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='item-media'
|
||||||
data-slot='item-media'
|
data-variant={variant}
|
||||||
data-variant={variant}
|
className={cn(itemMediaVariants({ variant, className }))}
|
||||||
className={cn(itemMediaVariants({ variant, className }))}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
|
const ItemContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='item-content'
|
||||||
data-slot='item-content'
|
className={cn(
|
||||||
className={cn(
|
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
|
||||||
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
const ItemTitle = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='item-title'
|
||||||
data-slot='item-title'
|
className={cn(
|
||||||
className={cn(
|
'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
|
||||||
'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
const ItemDescription = ({
|
||||||
return (
|
className,
|
||||||
<p
|
...props
|
||||||
data-slot='item-description'
|
}: React.ComponentProps<'p'>) => (
|
||||||
className={cn(
|
<p
|
||||||
'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',
|
data-slot='item-description'
|
||||||
className,
|
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',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
|
const ItemActions = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='item-actions'
|
||||||
data-slot='item-actions'
|
className={cn('flex items-center gap-2', className)}
|
||||||
className={cn('flex items-center gap-2', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
const ItemHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='item-header'
|
||||||
data-slot='item-header'
|
className={cn(
|
||||||
className={cn(
|
'flex basis-full items-center justify-between gap-2',
|
||||||
'flex basis-full items-center justify-between gap-2',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
const ItemFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='item-footer'
|
||||||
data-slot='item-footer'
|
className={cn(
|
||||||
className={cn(
|
'flex basis-full items-center justify-between gap-2',
|
||||||
'flex basis-full items-center justify-between gap-2',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Item,
|
Item,
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
|
const Kbd = ({ className, ...props }: React.ComponentProps<'kbd'>) => (
|
||||||
return (
|
<kbd
|
||||||
<kbd
|
data-slot='kbd'
|
||||||
data-slot='kbd'
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
const KbdGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<kbd
|
||||||
<kbd
|
data-slot='kbd-group'
|
||||||
data-slot='kbd-group'
|
className={cn('inline-flex items-center gap-1', className)}
|
||||||
className={cn('inline-flex items-center gap-1', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Kbd, KbdGroup };
|
export { Kbd, KbdGroup };
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ import { Label as LabelPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Label({
|
const Label = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) => (
|
||||||
return (
|
<LabelPrimitive.Root
|
||||||
<LabelPrimitive.Root
|
data-slot='label'
|
||||||
data-slot='label'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
export { Label };
|
export { Label };
|
||||||
|
|||||||
@@ -6,89 +6,81 @@ import { Menubar as MenubarPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Menubar({
|
const Menubar = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Root>) => (
|
||||||
return (
|
<MenubarPrimitive.Root
|
||||||
<MenubarPrimitive.Root
|
data-slot='menubar'
|
||||||
data-slot='menubar'
|
className={cn(
|
||||||
className={cn(
|
'bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs',
|
||||||
'bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarMenu({
|
const MenubarMenu = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) => (
|
||||||
return <MenubarPrimitive.Menu data-slot='menubar-menu' {...props} />;
|
<MenubarPrimitive.Menu data-slot='menubar-menu' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function MenubarGroup({
|
const MenubarGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Group>) => (
|
||||||
return <MenubarPrimitive.Group data-slot='menubar-group' {...props} />;
|
<MenubarPrimitive.Group data-slot='menubar-group' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function MenubarPortal({
|
const MenubarPortal = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) => (
|
||||||
return <MenubarPrimitive.Portal data-slot='menubar-portal' {...props} />;
|
<MenubarPrimitive.Portal data-slot='menubar-portal' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function MenubarRadioGroup({
|
const MenubarRadioGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) => (
|
||||||
return (
|
<MenubarPrimitive.RadioGroup data-slot='menubar-radio-group' {...props} />
|
||||||
<MenubarPrimitive.RadioGroup data-slot='menubar-radio-group' {...props} />
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarTrigger({
|
const MenubarTrigger = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) => (
|
||||||
return (
|
<MenubarPrimitive.Trigger
|
||||||
<MenubarPrimitive.Trigger
|
data-slot='menubar-trigger'
|
||||||
data-slot='menubar-trigger'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarContent({
|
const MenubarContent = ({
|
||||||
className,
|
className,
|
||||||
align = 'start',
|
align = 'start',
|
||||||
alignOffset = -4,
|
alignOffset = -4,
|
||||||
sideOffset = 8,
|
sideOffset = 8,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Content>) => (
|
||||||
return (
|
<MenubarPortal>
|
||||||
<MenubarPortal>
|
<MenubarPrimitive.Content
|
||||||
<MenubarPrimitive.Content
|
data-slot='menubar-content'
|
||||||
data-slot='menubar-content'
|
align={align}
|
||||||
align={align}
|
alignOffset={alignOffset}
|
||||||
alignOffset={alignOffset}
|
sideOffset={sideOffset}
|
||||||
sideOffset={sideOffset}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</MenubarPortal>
|
||||||
</MenubarPortal>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarItem({
|
const MenubarItem = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
@@ -96,165 +88,149 @@ function MenubarItem({
|
|||||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
variant?: 'default' | 'destructive';
|
variant?: 'default' | 'destructive';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<MenubarPrimitive.Item
|
||||||
<MenubarPrimitive.Item
|
data-slot='menubar-item'
|
||||||
data-slot='menubar-item'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
data-variant={variant}
|
||||||
data-variant={variant}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarCheckboxItem({
|
const MenubarCheckboxItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) => (
|
||||||
return (
|
<MenubarPrimitive.CheckboxItem
|
||||||
<MenubarPrimitive.CheckboxItem
|
data-slot='menubar-checkbox-item'
|
||||||
data-slot='menubar-checkbox-item'
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
checked={checked}
|
||||||
checked={checked}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
||||||
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
<MenubarPrimitive.ItemIndicator>
|
||||||
<MenubarPrimitive.ItemIndicator>
|
<CheckIcon className='size-4' />
|
||||||
<CheckIcon className='size-4' />
|
</MenubarPrimitive.ItemIndicator>
|
||||||
</MenubarPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
{children}
|
||||||
{children}
|
</MenubarPrimitive.CheckboxItem>
|
||||||
</MenubarPrimitive.CheckboxItem>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarRadioItem({
|
const MenubarRadioItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) => (
|
||||||
return (
|
<MenubarPrimitive.RadioItem
|
||||||
<MenubarPrimitive.RadioItem
|
data-slot='menubar-radio-item'
|
||||||
data-slot='menubar-radio-item'
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
||||||
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
<MenubarPrimitive.ItemIndicator>
|
||||||
<MenubarPrimitive.ItemIndicator>
|
<CircleIcon className='size-2 fill-current' />
|
||||||
<CircleIcon className='size-2 fill-current' />
|
</MenubarPrimitive.ItemIndicator>
|
||||||
</MenubarPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
{children}
|
||||||
{children}
|
</MenubarPrimitive.RadioItem>
|
||||||
</MenubarPrimitive.RadioItem>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarLabel({
|
const MenubarLabel = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<MenubarPrimitive.Label
|
||||||
<MenubarPrimitive.Label
|
data-slot='menubar-label'
|
||||||
data-slot='menubar-label'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
className={cn(
|
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarSeparator({
|
const MenubarSeparator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) => (
|
||||||
return (
|
<MenubarPrimitive.Separator
|
||||||
<MenubarPrimitive.Separator
|
data-slot='menubar-separator'
|
||||||
data-slot='menubar-separator'
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarShortcut({
|
const MenubarShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
data-slot='menubar-shortcut'
|
||||||
data-slot='menubar-shortcut'
|
className={cn(
|
||||||
className={cn(
|
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarSub({
|
const MenubarSub = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) => (
|
||||||
return <MenubarPrimitive.Sub data-slot='menubar-sub' {...props} />;
|
<MenubarPrimitive.Sub data-slot='menubar-sub' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function MenubarSubTrigger({
|
const MenubarSubTrigger = ({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<MenubarPrimitive.SubTrigger
|
||||||
<MenubarPrimitive.SubTrigger
|
data-slot='menubar-sub-trigger'
|
||||||
data-slot='menubar-sub-trigger'
|
data-inset={inset}
|
||||||
data-inset={inset}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<ChevronRightIcon className='ml-auto h-4 w-4' />
|
||||||
<ChevronRightIcon className='ml-auto h-4 w-4' />
|
</MenubarPrimitive.SubTrigger>
|
||||||
</MenubarPrimitive.SubTrigger>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarSubContent({
|
const MenubarSubContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) => (
|
||||||
return (
|
<MenubarPrimitive.SubContent
|
||||||
<MenubarPrimitive.SubContent
|
data-slot='menubar-sub-content'
|
||||||
data-slot='menubar-sub-content'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Menubar,
|
Menubar,
|
||||||
|
|||||||
@@ -3,51 +3,49 @@ import { ChevronDownIcon } from 'lucide-react';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function NativeSelect({
|
const NativeSelect = ({
|
||||||
className,
|
className,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: Omit<React.ComponentProps<'select'>, 'size'> & { size?: 'sm' | 'default' }) {
|
}: Omit<React.ComponentProps<'select'>, 'size'> & {
|
||||||
return (
|
size?: 'sm' | 'default';
|
||||||
<div
|
}) => (
|
||||||
className='group/native-select relative w-fit has-[select:disabled]:opacity-50'
|
<div
|
||||||
data-slot='native-select-wrapper'
|
className='group/native-select relative w-fit has-[select:disabled]:opacity-50'
|
||||||
>
|
data-slot='native-select-wrapper'
|
||||||
<select
|
>
|
||||||
data-slot='native-select'
|
<select
|
||||||
data-size={size}
|
data-slot='native-select'
|
||||||
className={cn(
|
data-size={size}
|
||||||
'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',
|
className={cn(
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
'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',
|
||||||
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
className,
|
'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)}
|
|
||||||
{...props}
|
{...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 };
|
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
|
||||||
|
|||||||
@@ -5,155 +5,137 @@ import { NavigationMenu as NavigationMenuPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function NavigationMenu({
|
const NavigationMenu = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
viewport = true,
|
viewport = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
viewport?: boolean;
|
viewport?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<NavigationMenuPrimitive.Root
|
||||||
<NavigationMenuPrimitive.Root
|
data-slot='navigation-menu'
|
||||||
data-slot='navigation-menu'
|
data-viewport={viewport}
|
||||||
data-viewport={viewport}
|
className={cn(
|
||||||
className={cn(
|
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
||||||
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
{viewport && <NavigationMenuViewport />}
|
||||||
{viewport && <NavigationMenuViewport />}
|
</NavigationMenuPrimitive.Root>
|
||||||
</NavigationMenuPrimitive.Root>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavigationMenuList({
|
const NavigationMenuList = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) => (
|
||||||
return (
|
<NavigationMenuPrimitive.List
|
||||||
<NavigationMenuPrimitive.List
|
data-slot='navigation-menu-list'
|
||||||
data-slot='navigation-menu-list'
|
className={cn(
|
||||||
className={cn(
|
'group flex flex-1 list-none items-center justify-center gap-1',
|
||||||
'group flex flex-1 list-none items-center justify-center gap-1',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavigationMenuItem({
|
const NavigationMenuItem = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) => (
|
||||||
return (
|
<NavigationMenuPrimitive.Item
|
||||||
<NavigationMenuPrimitive.Item
|
data-slot='navigation-menu-item'
|
||||||
data-slot='navigation-menu-item'
|
className={cn('relative', className)}
|
||||||
className={cn('relative', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
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',
|
'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,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) => (
|
||||||
return (
|
<NavigationMenuPrimitive.Trigger
|
||||||
<NavigationMenuPrimitive.Trigger
|
data-slot='navigation-menu-trigger'
|
||||||
data-slot='navigation-menu-trigger'
|
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}{' '}
|
||||||
{children}{' '}
|
<ChevronDownIcon
|
||||||
<ChevronDownIcon
|
className='relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180'
|
||||||
className='relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180'
|
aria-hidden='true'
|
||||||
aria-hidden='true'
|
/>
|
||||||
/>
|
</NavigationMenuPrimitive.Trigger>
|
||||||
</NavigationMenuPrimitive.Trigger>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavigationMenuContent({
|
const NavigationMenuContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) => (
|
||||||
return (
|
<NavigationMenuPrimitive.Content
|
||||||
<NavigationMenuPrimitive.Content
|
data-slot='navigation-menu-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(
|
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',
|
'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)]',
|
||||||
'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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
|
||||||
function NavigationMenuViewport({
|
const NavigationMenuLink = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) => (
|
||||||
return (
|
<NavigationMenuPrimitive.Link
|
||||||
<div
|
data-slot='navigation-menu-link'
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute top-full left-0 isolate z-50 flex justify-center',
|
"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,
|
||||||
>
|
)}
|
||||||
<NavigationMenuPrimitive.Viewport
|
{...props}
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavigationMenuLink({
|
const NavigationMenuIndicator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) => (
|
||||||
return (
|
<NavigationMenuPrimitive.Indicator
|
||||||
<NavigationMenuPrimitive.Link
|
data-slot='navigation-menu-indicator'
|
||||||
data-slot='navigation-menu-link'
|
className={cn(
|
||||||
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',
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
/>
|
<div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' />
|
||||||
);
|
</NavigationMenuPrimitive.Indicator>
|
||||||
}
|
);
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
|
|||||||
@@ -7,118 +7,106 @@ import {
|
|||||||
|
|
||||||
import { Button, cn } from '@gib/ui';
|
import { Button, cn } from '@gib/ui';
|
||||||
|
|
||||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||||
return (
|
<nav
|
||||||
<nav
|
role='navigation'
|
||||||
role='navigation'
|
aria-label='pagination'
|
||||||
aria-label='pagination'
|
data-slot='pagination'
|
||||||
data-slot='pagination'
|
className={cn('mx-auto flex w-full justify-center', className)}
|
||||||
className={cn('mx-auto flex w-full justify-center', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PaginationContent({
|
const PaginationContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'ul'>) {
|
}: React.ComponentProps<'ul'>) => (
|
||||||
return (
|
<ul
|
||||||
<ul
|
data-slot='pagination-content'
|
||||||
data-slot='pagination-content'
|
className={cn('flex items-center gap-0.5', className)}
|
||||||
className={cn('flex items-center gap-0.5', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
const PaginationItem = ({ ...props }: React.ComponentProps<'li'>) => (
|
||||||
return <li data-slot='pagination-item' {...props} />;
|
<li data-slot='pagination-item' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
type PaginationLinkProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||||
React.ComponentProps<'a'>;
|
React.ComponentProps<'a'>;
|
||||||
|
|
||||||
function PaginationLink({
|
const PaginationLink = ({
|
||||||
className,
|
className,
|
||||||
isActive,
|
isActive,
|
||||||
size = 'icon',
|
size = 'icon',
|
||||||
...props
|
...props
|
||||||
}: PaginationLinkProps) {
|
}: PaginationLinkProps) => (
|
||||||
return (
|
<Button
|
||||||
<Button
|
asChild
|
||||||
asChild
|
variant={isActive ? 'outline' : 'ghost'}
|
||||||
variant={isActive ? 'outline' : 'ghost'}
|
size={size}
|
||||||
size={size}
|
className={cn(className)}
|
||||||
className={cn(className)}
|
>
|
||||||
>
|
<a
|
||||||
<a
|
aria-current={isActive ? 'page' : undefined}
|
||||||
aria-current={isActive ? 'page' : undefined}
|
data-slot='pagination-link'
|
||||||
data-slot='pagination-link'
|
data-active={isActive}
|
||||||
data-active={isActive}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</Button>
|
||||||
</Button>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PaginationPrevious({
|
const PaginationPrevious = ({
|
||||||
className,
|
className,
|
||||||
text = 'Previous',
|
text = 'Previous',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
|
||||||
return (
|
<PaginationLink
|
||||||
<PaginationLink
|
aria-label='Go to previous page'
|
||||||
aria-label='Go to previous page'
|
size='default'
|
||||||
size='default'
|
className={cn('pl-1.5!', className)}
|
||||||
className={cn('pl-1.5!', className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<ChevronLeftIcon data-icon='inline-start' className='cn-rtl-flip' />
|
||||||
<ChevronLeftIcon data-icon='inline-start' className='cn-rtl-flip' />
|
<span className='hidden sm:block'>{text}</span>
|
||||||
<span className='hidden sm:block'>{text}</span>
|
</PaginationLink>
|
||||||
</PaginationLink>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PaginationNext({
|
const PaginationNext = ({
|
||||||
className,
|
className,
|
||||||
text = 'Next',
|
text = 'Next',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
|
||||||
return (
|
<PaginationLink
|
||||||
<PaginationLink
|
aria-label='Go to next page'
|
||||||
aria-label='Go to next page'
|
size='default'
|
||||||
size='default'
|
className={cn('pr-1.5!', className)}
|
||||||
className={cn('pr-1.5!', className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<span className='hidden sm:block'>{text}</span>
|
||||||
<span className='hidden sm:block'>{text}</span>
|
<ChevronRightIcon data-icon='inline-end' className='cn-rtl-flip' />
|
||||||
<ChevronRightIcon data-icon='inline-end' className='cn-rtl-flip' />
|
</PaginationLink>
|
||||||
</PaginationLink>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PaginationEllipsis({
|
const PaginationEllipsis = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<'span'>) => (
|
||||||
return (
|
<span
|
||||||
<span
|
aria-hidden
|
||||||
aria-hidden
|
data-slot='pagination-ellipsis'
|
||||||
data-slot='pagination-ellipsis'
|
className={cn(
|
||||||
className={cn(
|
"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
|
||||||
"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<MoreHorizontalIcon />
|
||||||
<MoreHorizontalIcon />
|
<span className='sr-only'>More pages</span>
|
||||||
<span className='sr-only'>More pages</span>
|
</span>
|
||||||
</span>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Pagination,
|
Pagination,
|
||||||
|
|||||||
@@ -5,78 +5,73 @@ import { Popover as PopoverPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Popover({
|
const Popover = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) => (
|
||||||
return <PopoverPrimitive.Root data-slot='popover' {...props} />;
|
<PopoverPrimitive.Root data-slot='popover' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function PopoverTrigger({
|
const PopoverTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) => (
|
||||||
return <PopoverPrimitive.Trigger data-slot='popover-trigger' {...props} />;
|
<PopoverPrimitive.Trigger data-slot='popover-trigger' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function PopoverContent({
|
const PopoverContent = ({
|
||||||
className,
|
className,
|
||||||
align = 'center',
|
align = 'center',
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) => (
|
||||||
return (
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Content
|
||||||
<PopoverPrimitive.Content
|
data-slot='popover-content'
|
||||||
data-slot='popover-content'
|
align={align}
|
||||||
align={align}
|
sideOffset={sideOffset}
|
||||||
sideOffset={sideOffset}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</PopoverPrimitive.Portal>
|
||||||
</PopoverPrimitive.Portal>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverAnchor({
|
const PopoverAnchor = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) => (
|
||||||
return <PopoverPrimitive.Anchor data-slot='popover-anchor' {...props} />;
|
<PopoverPrimitive.Anchor data-slot='popover-anchor' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
const PopoverHeader = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'p'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<p
|
data-slot='popover-header'
|
||||||
data-slot='popover-description'
|
className={cn('flex flex-col gap-1 text-sm', className)}
|
||||||
className={cn('text-muted-foreground', className)}
|
{...props}
|
||||||
{...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 {
|
export {
|
||||||
Popover,
|
Popover,
|
||||||
|
|||||||
@@ -5,27 +5,25 @@ import { Progress as ProgressPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Progress({
|
const Progress = ({
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) => (
|
||||||
return (
|
<ProgressPrimitive.Root
|
||||||
<ProgressPrimitive.Root
|
data-slot='progress'
|
||||||
data-slot='progress'
|
className={cn(
|
||||||
className={cn(
|
'bg-muted relative flex h-1 w-full items-center overflow-x-hidden rounded-full',
|
||||||
'bg-muted relative flex h-1 w-full items-center overflow-x-hidden rounded-full',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<ProgressPrimitive.Indicator
|
||||||
<ProgressPrimitive.Indicator
|
data-slot='progress-indicator'
|
||||||
data-slot='progress-indicator'
|
className='bg-primary size-full flex-1 transition-all'
|
||||||
className='bg-primary size-full flex-1 transition-all'
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
/>
|
||||||
/>
|
</ProgressPrimitive.Root>
|
||||||
</ProgressPrimitive.Root>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Progress };
|
export { Progress };
|
||||||
|
|||||||
@@ -6,40 +6,36 @@ import { RadioGroup as RadioGroupPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function RadioGroup({
|
const RadioGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) => (
|
||||||
return (
|
<RadioGroupPrimitive.Root
|
||||||
<RadioGroupPrimitive.Root
|
data-slot='radio-group'
|
||||||
data-slot='radio-group'
|
className={cn('grid gap-3', className)}
|
||||||
className={cn('grid gap-3', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioGroupItem({
|
const RadioGroupItem = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) => (
|
||||||
return (
|
<RadioGroupPrimitive.Item
|
||||||
<RadioGroupPrimitive.Item
|
data-slot='radio-group-item'
|
||||||
data-slot='radio-group-item'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot='radio-group-indicator'
|
||||||
|
className='relative flex items-center justify-center'
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator
|
<CircleIcon className='fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2' />
|
||||||
data-slot='radio-group-indicator'
|
</RadioGroupPrimitive.Indicator>
|
||||||
className='relative flex items-center justify-center'
|
</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 };
|
export { RadioGroup, RadioGroupItem };
|
||||||
|
|||||||
@@ -5,49 +5,45 @@ import * as ResizablePrimitive from 'react-resizable-panels';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function ResizablePanelGroup({
|
const ResizablePanelGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ResizablePrimitive.GroupProps) {
|
}: ResizablePrimitive.GroupProps) => (
|
||||||
return (
|
<ResizablePrimitive.Group
|
||||||
<ResizablePrimitive.Group
|
data-slot='resizable-panel-group'
|
||||||
data-slot='resizable-panel-group'
|
className={cn(
|
||||||
className={cn(
|
'flex h-full w-full aria-[orientation=vertical]:flex-col',
|
||||||
'flex h-full w-full aria-[orientation=vertical]:flex-col',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
|
const ResizablePanel = ({ ...props }: ResizablePrimitive.PanelProps) => (
|
||||||
return <ResizablePrimitive.Panel data-slot='resizable-panel' {...props} />;
|
<ResizablePrimitive.Panel data-slot='resizable-panel' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function ResizableHandle({
|
const ResizableHandle = ({
|
||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ResizablePrimitive.SeparatorProps & {
|
}: ResizablePrimitive.SeparatorProps & {
|
||||||
withHandle?: boolean;
|
withHandle?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<ResizablePrimitive.Separator
|
||||||
<ResizablePrimitive.Separator
|
data-slot='resizable-handle'
|
||||||
data-slot='resizable-handle'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{withHandle && (
|
||||||
{withHandle && (
|
<div className='bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border'>
|
||||||
<div className='bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border'>
|
<GripVerticalIcon className='size-2.5' />
|
||||||
<GripVerticalIcon className='size-2.5' />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</ResizablePrimitive.Separator>
|
||||||
</ResizablePrimitive.Separator>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
||||||
|
|||||||
@@ -5,51 +5,47 @@ import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function ScrollArea({
|
const ScrollArea = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) => (
|
||||||
return (
|
<ScrollAreaPrimitive.Root
|
||||||
<ScrollAreaPrimitive.Root
|
data-slot='scroll-area'
|
||||||
data-slot='scroll-area'
|
className={cn('relative', className)}
|
||||||
className={cn('relative', className)}
|
{...props}
|
||||||
{...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
|
{children}
|
||||||
data-slot='scroll-area-viewport'
|
</ScrollAreaPrimitive.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'
|
<ScrollBar />
|
||||||
>
|
<ScrollAreaPrimitive.Corner />
|
||||||
{children}
|
</ScrollAreaPrimitive.Root>
|
||||||
</ScrollAreaPrimitive.Viewport>
|
);
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ScrollBar({
|
const ScrollBar = ({
|
||||||
className,
|
className,
|
||||||
orientation = 'vertical',
|
orientation = 'vertical',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) => (
|
||||||
return (
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
data-slot='scroll-area-scrollbar'
|
||||||
data-slot='scroll-area-scrollbar'
|
data-orientation={orientation}
|
||||||
data-orientation={orientation}
|
orientation={orientation}
|
||||||
orientation={orientation}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
data-slot='scroll-area-thumb'
|
||||||
data-slot='scroll-area-thumb'
|
className='bg-border relative flex-1 rounded-full'
|
||||||
className='bg-border relative flex-1 rounded-full'
|
/>
|
||||||
/>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar };
|
export { ScrollArea, ScrollBar };
|
||||||
|
|||||||
@@ -6,175 +6,161 @@ import { Select as SelectPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Select({
|
const Select = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) => (
|
||||||
return <SelectPrimitive.Root data-slot='select' {...props} />;
|
<SelectPrimitive.Root data-slot='select' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function SelectGroup({
|
const SelectGroup = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) => (
|
||||||
return <SelectPrimitive.Group data-slot='select-group' {...props} />;
|
<SelectPrimitive.Group data-slot='select-group' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function SelectValue({
|
const SelectValue = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) => (
|
||||||
return <SelectPrimitive.Value data-slot='select-value' {...props} />;
|
<SelectPrimitive.Value data-slot='select-value' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function SelectTrigger({
|
const SelectTrigger = ({
|
||||||
className,
|
className,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: 'sm' | 'default';
|
size?: 'sm' | 'default';
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<SelectPrimitive.Trigger
|
||||||
<SelectPrimitive.Trigger
|
data-slot='select-trigger'
|
||||||
data-slot='select-trigger'
|
data-size={size}
|
||||||
data-size={size}
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<SelectPrimitive.Icon asChild>
|
||||||
<SelectPrimitive.Icon asChild>
|
<ChevronDownIcon className='size-4 opacity-50' />
|
||||||
<ChevronDownIcon className='size-4 opacity-50' />
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Trigger>
|
||||||
</SelectPrimitive.Trigger>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
const SelectContent = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
position = 'item-aligned',
|
position = 'item-aligned',
|
||||||
align = 'center',
|
align = 'center',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) => (
|
||||||
return (
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Content
|
||||||
<SelectPrimitive.Content
|
data-slot='select-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(
|
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' &&
|
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',
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
|
||||||
className,
|
|
||||||
)}
|
)}
|
||||||
position={position}
|
|
||||||
align={align}
|
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
{children}
|
||||||
<SelectPrimitive.Viewport
|
</SelectPrimitive.Viewport>
|
||||||
className={cn(
|
<SelectScrollDownButton />
|
||||||
'p-1',
|
</SelectPrimitive.Content>
|
||||||
position === 'popper' &&
|
</SelectPrimitive.Portal>
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({
|
const SelectLabel = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) => (
|
||||||
return (
|
<SelectPrimitive.Label
|
||||||
<SelectPrimitive.Label
|
data-slot='select-label'
|
||||||
data-slot='select-label'
|
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({
|
const SelectItem = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) => (
|
||||||
return (
|
<SelectPrimitive.Item
|
||||||
<SelectPrimitive.Item
|
data-slot='select-item'
|
||||||
data-slot='select-item'
|
className={cn(
|
||||||
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",
|
||||||
"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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
|
<span
|
||||||
|
data-slot='select-item-indicator'
|
||||||
|
className='absolute right-2 flex size-3.5 items-center justify-center'
|
||||||
>
|
>
|
||||||
<span
|
<SelectPrimitive.ItemIndicator>
|
||||||
data-slot='select-item-indicator'
|
<CheckIcon className='size-4' />
|
||||||
className='absolute right-2 flex size-3.5 items-center justify-center'
|
</SelectPrimitive.ItemIndicator>
|
||||||
>
|
</span>
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
<CheckIcon className='size-4' />
|
</SelectPrimitive.Item>
|
||||||
</SelectPrimitive.ItemIndicator>
|
);
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
const SelectSeparator = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) => (
|
||||||
return (
|
<SelectPrimitive.Separator
|
||||||
<SelectPrimitive.Separator
|
data-slot='select-separator'
|
||||||
data-slot='select-separator'
|
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
const SelectScrollUpButton = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) => (
|
||||||
return (
|
<SelectPrimitive.ScrollUpButton
|
||||||
<SelectPrimitive.ScrollUpButton
|
data-slot='select-scroll-up-button'
|
||||||
data-slot='select-scroll-up-button'
|
className={cn(
|
||||||
className={cn(
|
'flex cursor-default items-center justify-center py-1',
|
||||||
'flex cursor-default items-center justify-center py-1',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<ChevronUpIcon className='size-4' />
|
||||||
<ChevronUpIcon className='size-4' />
|
</SelectPrimitive.ScrollUpButton>
|
||||||
</SelectPrimitive.ScrollUpButton>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
const SelectScrollDownButton = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) => (
|
||||||
return (
|
<SelectPrimitive.ScrollDownButton
|
||||||
<SelectPrimitive.ScrollDownButton
|
data-slot='select-scroll-down-button'
|
||||||
data-slot='select-scroll-down-button'
|
className={cn(
|
||||||
className={cn(
|
'flex cursor-default items-center justify-center py-1',
|
||||||
'flex cursor-default items-center justify-center py-1',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<ChevronDownIcon className='size-4' />
|
||||||
<ChevronDownIcon className='size-4' />
|
</SelectPrimitive.ScrollDownButton>
|
||||||
</SelectPrimitive.ScrollDownButton>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
|
|||||||
@@ -5,24 +5,22 @@ import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Separator({
|
const Separator = ({
|
||||||
className,
|
className,
|
||||||
orientation = 'horizontal',
|
orientation = 'horizontal',
|
||||||
decorative = true,
|
decorative = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) => (
|
||||||
return (
|
<SeparatorPrimitive.Root
|
||||||
<SeparatorPrimitive.Root
|
data-slot='separator'
|
||||||
data-slot='separator'
|
decorative={decorative}
|
||||||
decorative={decorative}
|
orientation={orientation}
|
||||||
orientation={orientation}
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Separator };
|
export { Separator };
|
||||||
|
|||||||
@@ -6,45 +6,45 @@ import { Dialog as SheetPrimitive } from 'radix-ui';
|
|||||||
|
|
||||||
import { cn } from '@gib/ui';
|
import { cn } from '@gib/ui';
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
const Sheet = ({
|
||||||
return <SheetPrimitive.Root data-slot='sheet' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTrigger({
|
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Root>) => (
|
||||||
return <SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />;
|
<SheetPrimitive.Root data-slot='sheet' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function SheetClose({
|
const SheetTrigger = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) => (
|
||||||
return <SheetPrimitive.Close data-slot='sheet-close' {...props} />;
|
<SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />
|
||||||
}
|
);
|
||||||
|
|
||||||
function SheetPortal({
|
const SheetClose = ({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) => (
|
||||||
return <SheetPrimitive.Portal data-slot='sheet-portal' {...props} />;
|
<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,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) => (
|
||||||
return (
|
<SheetPrimitive.Overlay
|
||||||
<SheetPrimitive.Overlay
|
data-slot='sheet-overlay'
|
||||||
data-slot='sheet-overlay'
|
className={cn(
|
||||||
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',
|
||||||
'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,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetContent({
|
const SheetContent = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
side = 'right',
|
side = 'right',
|
||||||
@@ -53,83 +53,73 @@ function SheetContent({
|
|||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<SheetPortal>
|
||||||
<SheetPortal>
|
<SheetOverlay />
|
||||||
<SheetOverlay />
|
<SheetPrimitive.Content
|
||||||
<SheetPrimitive.Content
|
data-slot='sheet-content'
|
||||||
data-slot='sheet-content'
|
className={cn(
|
||||||
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',
|
||||||
'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' &&
|
||||||
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',
|
||||||
'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' &&
|
||||||
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',
|
||||||
'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' &&
|
||||||
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',
|
||||||
'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' &&
|
||||||
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',
|
||||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
className,
|
||||||
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)}
|
|
||||||
{...props}
|
{...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'>) {
|
const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='sheet-header'
|
||||||
data-slot='sheet-footer'
|
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
{...props}
|
||||||
{...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,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) => (
|
||||||
return (
|
<SheetPrimitive.Title
|
||||||
<SheetPrimitive.Title
|
data-slot='sheet-title'
|
||||||
data-slot='sheet-title'
|
className={cn('text-foreground font-semibold', className)}
|
||||||
className={cn('text-foreground font-semibold', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetDescription({
|
const SheetDescription = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) => (
|
||||||
return (
|
<SheetPrimitive.Description
|
||||||
<SheetPrimitive.Description
|
data-slot='sheet-description'
|
||||||
data-slot='sheet-description'
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
className={cn('text-muted-foreground text-sm', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const SIDEBAR_WIDTH_MOBILE = '18rem';
|
|||||||
const SIDEBAR_WIDTH_ICON = '3rem';
|
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||||
|
|
||||||
type SidebarContextProps = {
|
interface SidebarContextProps {
|
||||||
state: 'expanded' | 'collapsed';
|
state: 'expanded' | 'collapsed';
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
@@ -39,20 +39,20 @@ type SidebarContextProps = {
|
|||||||
setOpenMobile: (open: boolean) => void;
|
setOpenMobile: (open: boolean) => void;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||||
|
|
||||||
function useSidebar() {
|
const useSidebar = () => {
|
||||||
const context = React.useContext(SidebarContext);
|
const context = React.useContext(SidebarContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useSidebar must be used within a SidebarProvider.');
|
throw new Error('useSidebar must be used within a SidebarProvider.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarProvider({
|
const SidebarProvider = ({
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
open: openProp,
|
open: openProp,
|
||||||
onOpenChange: setOpenProp,
|
onOpenChange: setOpenProp,
|
||||||
@@ -64,7 +64,7 @@ function SidebarProvider({
|
|||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
}) {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [openMobile, setOpenMobile] = React.useState(false);
|
const [openMobile, setOpenMobile] = React.useState(false);
|
||||||
|
|
||||||
@@ -148,9 +148,9 @@ function SidebarProvider({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function Sidebar({
|
const Sidebar = ({
|
||||||
side = 'left',
|
side = 'left',
|
||||||
variant = 'sidebar',
|
variant = 'sidebar',
|
||||||
collapsible = 'offcanvas',
|
collapsible = 'offcanvas',
|
||||||
@@ -161,7 +161,7 @@ function Sidebar({
|
|||||||
side?: 'left' | 'right';
|
side?: 'left' | 'right';
|
||||||
variant?: 'sidebar' | 'floating' | 'inset';
|
variant?: 'sidebar' | 'floating' | 'inset';
|
||||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||||
}) {
|
}) => {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
if (collapsible === 'none') {
|
if (collapsible === 'none') {
|
||||||
@@ -250,13 +250,13 @@ function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarTrigger({
|
const SidebarTrigger = ({
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) => {
|
||||||
const { toggleSidebar } = useSidebar();
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -276,9 +276,12 @@ function SidebarTrigger({
|
|||||||
<span className='sr-only'>Toggle Sidebar</span>
|
<span className='sr-only'>Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
|
const SidebarRail = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'button'>) => {
|
||||||
const { toggleSidebar } = useSidebar();
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -301,102 +304,100 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
|
const SidebarInset = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Input>) {
|
}: React.ComponentProps<'main'>) => (
|
||||||
return (
|
<main
|
||||||
<Input
|
data-slot='sidebar-inset'
|
||||||
data-slot='sidebar-input'
|
className={cn(
|
||||||
data-sidebar='input'
|
'bg-background relative flex w-full flex-1 flex-col',
|
||||||
className={cn('bg-background h-8 w-full shadow-none', className)}
|
'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',
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
const SidebarInput = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
}: React.ComponentProps<typeof Input>) => (
|
||||||
return (
|
<Input
|
||||||
<Separator
|
data-slot='sidebar-input'
|
||||||
data-slot='sidebar-separator'
|
data-sidebar='input'
|
||||||
data-sidebar='separator'
|
className={cn('bg-background h-8 w-full shadow-none', className)}
|
||||||
className={cn('bg-sidebar-border mx-2 w-auto', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
|
const SidebarHeader = ({
|
||||||
return (
|
className,
|
||||||
<div
|
...props
|
||||||
data-slot='sidebar-content'
|
}: React.ComponentProps<'div'>) => (
|
||||||
data-sidebar='content'
|
<div
|
||||||
className={cn(
|
data-slot='sidebar-header'
|
||||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
data-sidebar='header'
|
||||||
className,
|
className={cn('flex flex-col gap-2 p-2', className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
const SidebarFooter = ({
|
||||||
return (
|
className,
|
||||||
<div
|
...props
|
||||||
data-slot='sidebar-group'
|
}: React.ComponentProps<'div'>) => (
|
||||||
data-sidebar='group'
|
<div
|
||||||
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
data-slot='sidebar-footer'
|
||||||
{...props}
|
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,
|
className,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & { asChild?: boolean }) {
|
}: React.ComponentProps<'div'> & { asChild?: boolean }) => {
|
||||||
const Comp = asChild ? Slot.Root : 'div';
|
const Comp = asChild ? Slot.Root : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -411,13 +412,13 @@ function SidebarGroupLabel({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarGroupAction({
|
const SidebarGroupAction = ({
|
||||||
className,
|
className,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
|
}: React.ComponentProps<'button'> & { asChild?: boolean }) => {
|
||||||
const Comp = asChild ? Slot.Root : 'button';
|
const Comp = asChild ? Slot.Root : 'button';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -434,43 +435,40 @@ function SidebarGroupAction({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarGroupContent({
|
const SidebarGroupContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='sidebar-group-content'
|
||||||
data-slot='sidebar-group-content'
|
data-sidebar='group-content'
|
||||||
data-sidebar='group-content'
|
className={cn('w-full text-sm', className)}
|
||||||
className={cn('w-full text-sm', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
|
const SidebarMenu = ({ className, ...props }: React.ComponentProps<'ul'>) => (
|
||||||
return (
|
<ul
|
||||||
<ul
|
data-slot='sidebar-menu'
|
||||||
data-slot='sidebar-menu'
|
data-sidebar='menu'
|
||||||
data-sidebar='menu'
|
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||||
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
|
const SidebarMenuItem = ({
|
||||||
return (
|
className,
|
||||||
<li
|
...props
|
||||||
data-slot='sidebar-menu-item'
|
}: React.ComponentProps<'li'>) => (
|
||||||
data-sidebar='menu-item'
|
<li
|
||||||
className={cn('group/menu-item relative', className)}
|
data-slot='sidebar-menu-item'
|
||||||
{...props}
|
data-sidebar='menu-item'
|
||||||
/>
|
className={cn('group/menu-item relative', className)}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
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',
|
'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,
|
asChild = false,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
@@ -506,7 +504,7 @@ function SidebarMenuButton({
|
|||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
} & VariantProps<typeof sidebarMenuButtonVariants>) => {
|
||||||
const Comp = asChild ? Slot.Root : 'button';
|
const Comp = asChild ? Slot.Root : 'button';
|
||||||
const { isMobile, state } = useSidebar();
|
const { isMobile, state } = useSidebar();
|
||||||
|
|
||||||
@@ -542,9 +540,9 @@ function SidebarMenuButton({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarMenuAction({
|
const SidebarMenuAction = ({
|
||||||
className,
|
className,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
showOnHover = false,
|
showOnHover = false,
|
||||||
@@ -552,7 +550,7 @@ function SidebarMenuAction({
|
|||||||
}: React.ComponentProps<'button'> & {
|
}: React.ComponentProps<'button'> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
showOnHover?: boolean;
|
showOnHover?: boolean;
|
||||||
}) {
|
}) => {
|
||||||
const Comp = asChild ? Slot.Root : 'button';
|
const Comp = asChild ? Slot.Root : 'button';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -574,37 +572,35 @@ function SidebarMenuAction({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarMenuBadge({
|
const SidebarMenuBadge = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'>) {
|
}: React.ComponentProps<'div'>) => (
|
||||||
return (
|
<div
|
||||||
<div
|
data-slot='sidebar-menu-badge'
|
||||||
data-slot='sidebar-menu-badge'
|
data-sidebar='menu-badge'
|
||||||
data-sidebar='menu-badge'
|
className={cn(
|
||||||
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',
|
||||||
'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-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=sm]/menu-button:top-1',
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
'peer-data-[size=default]/menu-button:top-1.5',
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
'group-data-[collapsible=icon]:hidden',
|
||||||
'group-data-[collapsible=icon]:hidden',
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSkeleton({
|
const SidebarMenuSkeleton = ({
|
||||||
className,
|
className,
|
||||||
showIcon = false,
|
showIcon = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
}) {
|
}) => {
|
||||||
// Random width between 50 to 90%.
|
// Random width between 50 to 90%.
|
||||||
const width = React.useMemo(() => {
|
const width = React.useMemo(() => {
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
@@ -634,38 +630,37 @@ function SidebarMenuSkeleton({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
|
const SidebarMenuSub = ({
|
||||||
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({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'li'>) {
|
}: React.ComponentProps<'ul'>) => (
|
||||||
return (
|
<ul
|
||||||
<li
|
data-slot='sidebar-menu-sub'
|
||||||
data-slot='sidebar-menu-sub-item'
|
data-sidebar='menu-sub'
|
||||||
data-sidebar='menu-sub-item'
|
className={cn(
|
||||||
className={cn('group/menu-sub-item relative', className)}
|
'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',
|
||||||
{...props}
|
'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,
|
asChild = false,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
isActive = false,
|
isActive = false,
|
||||||
@@ -675,7 +670,7 @@ function SidebarMenuSubButton({
|
|||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}) {
|
}) => {
|
||||||
const Comp = asChild ? Slot.Root : 'a';
|
const Comp = asChild ? Slot.Root : 'a';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -695,7 +690,7 @@ function SidebarMenuSubButton({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user