Compare commits

..

17 Commits

119 changed files with 27688 additions and 10487 deletions

View File

@@ -18,8 +18,9 @@ out
.git .git
.gitignore .gitignore
*.log *.log
.env.local #.env
.env*.local #.env.*
!.env.example
.vscode .vscode
.idea .idea

View File

@@ -17,8 +17,10 @@ NEXT_PUBLIC_SENTRY_PROJECT_NAME=example
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com # convex-backend:3210 CONVEX_SELF_HOSTED_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=

0
.npmrc
View File

1669
AGENTS.md

File diff suppressed because it is too large Load Diff

287
README.md
View File

@@ -32,15 +32,21 @@ A production-ready Turborepo starter with Next.js, Expo, and self-hosted Convex
## Getting Started ## 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.

View File

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

View File

@@ -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"
} }

View File

@@ -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/**'],

View File

@@ -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"
} }

View File

@@ -1 +1 @@
module.exports = require('@acme/tailwind-config/postcss-config'); module.exports = require('@gib/tailwind-config/postcss-config');

View File

@@ -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;

View File

@@ -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';
function PostCard(props: { const Index = () => {
post: RouterOutputs['post']['all'][number]; const { isAuthenticated, isLoading } = useConvexAuth();
onDelete: () => void; const { signOut } = useAuthActions();
}) { const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
return (
<View className='bg-muted flex flex-row rounded-lg p-4'>
<View className='grow'>
<Link
asChild
href={{
pathname: '/post/[id]',
params: { id: props.post.id },
}}
>
<Pressable className=''>
<Text className='text-primary text-xl font-semibold'>
{props.post.title}
</Text>
<Text className='text-foreground mt-2'>{props.post.content}</Text>
</Pressable>
</Link>
</View>
<Pressable onPress={props.onDelete}>
<Text className='text-primary font-bold uppercase'>Delete</Text>
</Pressable>
</View>
);
}
function CreatePost() {
const queryClient = useQueryClient();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const { mutate, error } = useMutation(
trpc.post.create.mutationOptions({
async onSuccess() {
setTitle('');
setContent('');
await queryClient.invalidateQueries(trpc.post.all.queryFilter());
},
}),
);
return ( return (
<View className='mt-4 flex gap-2'> <SafeAreaView className='bg-background flex-1'>
<TextInput <Stack.Screen options={{ title: 'Home' }} />
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight' <View className='flex-1 items-center justify-center gap-4 p-6'>
value={title} <Text className='text-foreground text-4xl font-bold'>
onChangeText={setTitle} Convex Monorepo
placeholder='Title'
/>
{error?.data?.zodError?.fieldErrors.title && (
<Text className='text-destructive mb-2'>
{error.data.zodError.fieldErrors.title}
</Text> </Text>
)} <Text className='text-muted-foreground text-center text-base'>
<TextInput Your self-hosted Expo + Convex starter
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> </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() { {isLoading ? (
const { data: session } = authClient.useSession(); <Text className='text-muted-foreground'>Loading...</Text>
) : isAuthenticated ? (
return ( <View className='w-full gap-3'>
<> <Text className='text-foreground text-center text-lg'>
<Text className='text-foreground pb-2 text-center text-xl font-semibold'> Welcome{user?.name ? `, ${user.name}` : ''}!
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
</Text> </Text>
<Pressable <Pressable
onPress={() => className='bg-primary items-center rounded-md p-3'
session onPress={() => void signOut()}
? 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> <Text className='text-primary-foreground font-semibold'>
Sign Out
</Text>
</Pressable> </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> </View>
) : (
<LegendList <View className='w-full gap-3'>
data={postQuery.data ?? []} <Text className='text-muted-foreground text-center'>
estimatedItemSize={20} Sign in to get started
keyExtractor={(item) => item.id} </Text>
ItemSeparatorComponent={() => <View className='h-2' />} {/* Add sign-in UI here — see apps/next/src/app/(auth)/sign-in for patterns */}
renderItem={(p) => ( </View>
<PostCard
post={p.item}
onDelete={() => deletePostMutation.mutate(p.item.id)}
/>
)} )}
/>
<CreatePost />
</View> </View>
</SafeAreaView> </SafeAreaView>
); );
} };
export default Index;

View File

@@ -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;

View File

@@ -1,3 +1,3 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'nativewind/theme'; @import 'nativewind/theme';
@import '@acme/tailwind-config/theme'; @import '@gib/tailwind-config/theme';

View File

@@ -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';

View File

@@ -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,
}),
],
});

View 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());

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`}
> >

View File

@@ -1,13 +1,7 @@
import Image from 'next/image'; export const CTA = () => (
import Link from 'next/link';
import { Button } from '@gib/ui/button';
export function CTA() {
return (
<section className='container mx-auto px-4 py-24'> <section className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-4xl'> <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'> <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'> <h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Ready to Build Something Amazing? Ready to Build Something Amazing?
</h2> </h2>
@@ -35,4 +29,3 @@ export function CTA() {
</div> </div>
</section> </section>
); );
}

View File

@@ -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,8 +57,7 @@ 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 */}
@@ -67,8 +66,8 @@ export function Features() {
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 A complete monorepo template with all the tools and patterns you need
need for production-ready applications. for production-ready applications.
</p> </p>
</div> </div>
@@ -89,4 +88,3 @@ export function Features() {
</div> </div>
</section> </section>
); );
}

View File

@@ -2,15 +2,14 @@ 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 */}
@@ -31,9 +30,9 @@ export function Hero() {
{/* Description */} {/* Description */}
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'> <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 A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web
web and mobile apps faster with shared code, type-safe backend, and and mobile apps faster with shared code, type-safe backend, and complete
complete control over your infrastructure. control over your infrastructure.
</p> </p>
{/* CTA Buttons */} {/* CTA Buttons */}
@@ -125,4 +124,3 @@ export function Hero() {
</div> </div>
</section> </section>
); );
}

View File

@@ -36,8 +36,7 @@ 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'>
@@ -76,4 +75,3 @@ export function TechStack() {
</div> </div>
</section> </section>
); );
}

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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}`}

View File

@@ -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>
); );
}

View File

@@ -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,
}); });

View File

@@ -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,
}); });

View File

@@ -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,

View File

@@ -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/*"]
}, },

View File

@@ -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=="],

View File

@@ -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

View File

@@ -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

View File

@@ -9,16 +9,16 @@ 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 - SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
- NEXT_PUBLIC_CONVEX_URL - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
- NEXT_PUBLIC_PLAUSIBLE_URL - NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${BACKEND_PORT:-3210}}
- NEXT_PUBLIC_SENTRY_DSN - NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
- NEXT_PUBLIC_SENTRY_URL - NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
- NEXT_PUBLIC_SENTRY_ORG - NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
- NEXT_PUBLIC_SENTRY_PROJECT_NAME - NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${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 +38,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 +66,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

View File

@@ -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()),

View File

@@ -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;
} }

View File

@@ -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}`,

View File

@@ -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,
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,68 +4,7 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.tsx", ".": "./src/index.tsx",
"./accordion": "./src/accordion.tsx", "./hooks": "./src/index.tsx"
"./alert": "./src/alert.tsx",
"./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": {

View File

@@ -6,38 +6,33 @@ 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'
@@ -59,14 +54,12 @@ function AccordionTrigger({
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
); );
}
function AccordionContent({ const AccordionContent = ({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) { }: React.ComponentProps<typeof AccordionPrimitive.Content>) => (
return (
<AccordionPrimitive.Content <AccordionPrimitive.Content
data-slot='accordion-content' data-slot='accordion-content'
className='data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm' className='data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm'
@@ -82,6 +75,5 @@ function AccordionContent({
</div> </div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
); );
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -5,33 +5,28 @@ 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(
@@ -41,16 +36,14 @@ function AlertDialogOverlay({
{...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
@@ -64,13 +57,11 @@ function AlertDialogContent({
/> />
</AlertDialogPortal> </AlertDialogPortal>
); );
}
function AlertDialogHeader({ const AlertDialogHeader = ({
className, className,
...props ...props
}: React.ComponentProps<'div'>) { }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='alert-dialog-header' data-slot='alert-dialog-header'
className={cn( className={cn(
@@ -80,13 +71,11 @@ function AlertDialogHeader({
{...props} {...props}
/> />
); );
}
function AlertDialogFooter({ const AlertDialogFooter = ({
className, className,
...props ...props
}: React.ComponentProps<'div'>) { }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='alert-dialog-footer' data-slot='alert-dialog-footer'
className={cn( className={cn(
@@ -96,13 +85,11 @@ function AlertDialogFooter({
{...props} {...props}
/> />
); );
}
function AlertDialogMedia({ const AlertDialogMedia = ({
className, className,
...props ...props
}: React.ComponentProps<'div'>) { }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='alert-dialog-media' data-slot='alert-dialog-media'
className={cn( className={cn(
@@ -112,13 +99,11 @@ function AlertDialogMedia({
{...props} {...props}
/> />
); );
}
function AlertDialogTitle({ const AlertDialogTitle = ({
className, className,
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) => (
return (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
data-slot='alert-dialog-title' data-slot='alert-dialog-title'
className={cn( className={cn(
@@ -128,13 +113,11 @@ function AlertDialogTitle({
{...props} {...props}
/> />
); );
}
function AlertDialogDescription({ const AlertDialogDescription = ({
className, className,
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) => (
return (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
data-slot='alert-dialog-description' data-slot='alert-dialog-description'
className={cn( className={cn(
@@ -144,16 +127,14 @@ function AlertDialogDescription({
{...props} {...props}
/> />
); );
}
function AlertDialogAction({ 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'
@@ -162,16 +143,14 @@ function AlertDialogAction({
/> />
</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'
@@ -180,7 +159,6 @@ function AlertDialogCancel({
/> />
</Button> </Button>
); );
}
export { export {
AlertDialog, AlertDialog,

View File

@@ -20,12 +20,11 @@ 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'
@@ -33,10 +32,8 @@ function Alert({
{...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(
@@ -46,13 +43,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
{...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(
@@ -62,16 +57,13 @@ function AlertDescription({
{...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 };

View File

@@ -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 };

View File

@@ -5,14 +5,13 @@ 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}
@@ -23,26 +22,22 @@ function Avatar({
{...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(
@@ -52,10 +47,8 @@ function AvatarFallback({
{...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(
@@ -68,10 +61,8 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
{...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(
@@ -81,13 +72,11 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
{...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(
@@ -97,7 +86,6 @@ function AvatarGroupCount({
{...props} {...props}
/> />
); );
}
export { export {
Avatar, Avatar,

View File

@@ -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 };

View File

@@ -4,12 +4,14 @@ 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,
...props
}: React.ComponentProps<'ol'>) => (
<ol <ol
data-slot='breadcrumb-list' data-slot='breadcrumb-list'
className={cn( className={cn(
@@ -19,25 +21,25 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
{...props} {...props}
/> />
); );
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) { const BreadcrumbItem = ({
return ( className,
...props
}: React.ComponentProps<'li'>) => (
<li <li
data-slot='breadcrumb-item' data-slot='breadcrumb-item'
className={cn('inline-flex items-center gap-1.5', className)} className={cn('inline-flex items-center gap-1.5', className)}
{...props} {...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,10 +49,12 @@ function BreadcrumbLink({
{...props} {...props}
/> />
); );
} };
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) { const BreadcrumbPage = ({
return ( className,
...props
}: React.ComponentProps<'span'>) => (
<span <span
data-slot='breadcrumb-page' data-slot='breadcrumb-page'
role='link' role='link'
@@ -60,14 +64,12 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
{...props} {...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'
@@ -78,13 +80,11 @@ function BreadcrumbSeparator({
{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'
@@ -96,7 +96,6 @@ function BreadcrumbEllipsis({
<span className='sr-only'>More</span> <span className='sr-only'>More</span>
</span> </span>
); );
}
export { export {
Breadcrumb, Breadcrumb,

View File

@@ -21,12 +21,11 @@ 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'
@@ -35,15 +34,14 @@ function ButtonGroup({
{...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,14 +53,13 @@ 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}
@@ -73,7 +70,6 @@ function ButtonGroupSeparator({
{...props} {...props}
/> />
); );
}
export { export {
ButtonGroup, ButtonGroup,

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -2,12 +2,11 @@ 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}
@@ -18,10 +17,8 @@ function Card({
{...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(
@@ -31,10 +28,8 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
{...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(
@@ -44,20 +39,19 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { const CardDescription = ({
return ( className,
...props
}: React.ComponentProps<'div'>) => (
<div <div
data-slot='card-description' data-slot='card-description'
className={cn('text-muted-foreground text-sm', className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...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(
@@ -67,20 +61,16 @@ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
{...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(
@@ -90,7 +80,6 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
export { export {
Card, Card,

View File

@@ -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,

View File

@@ -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,

View File

@@ -6,11 +6,10 @@ 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(
@@ -27,6 +26,5 @@ function Checkbox({
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
); );
}
export { Checkbox }; export { Checkbox };

View File

@@ -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 };

View File

@@ -15,16 +15,15 @@ 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)}
@@ -34,10 +33,11 @@ function ComboboxTrigger({
<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,
...props
}: ComboboxPrimitive.Clear.Props) => (
<ComboboxPrimitive.Clear <ComboboxPrimitive.Clear
data-slot='combobox-clear' data-slot='combobox-clear'
className={cn(className)} className={cn(className)}
@@ -49,9 +49,8 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
} }
/> />
); );
}
function ComboboxInput({ const ComboboxInput = ({
className, className,
children, children,
disabled = false, disabled = false,
@@ -61,8 +60,7 @@ 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} />}
@@ -84,9 +82,8 @@ function ComboboxInput({
{children} {children}
</InputGroup> </InputGroup>
); );
}
function ComboboxContent({ const ComboboxContent = ({
className, className,
side = 'bottom', side = 'bottom',
sideOffset = 6, sideOffset = 6,
@@ -98,8 +95,7 @@ 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}
@@ -121,10 +117,11 @@ function ComboboxContent({
</ComboboxPrimitive.Positioner> </ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal> </ComboboxPrimitive.Portal>
); );
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { const ComboboxList = ({
return ( className,
...props
}: ComboboxPrimitive.List.Props) => (
<ComboboxPrimitive.List <ComboboxPrimitive.List
data-slot='combobox-list' data-slot='combobox-list'
className={cn( className={cn(
@@ -134,14 +131,12 @@ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
{...props} {...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(
@@ -160,39 +155,39 @@ function ComboboxItem({
/> />
</ComboboxPrimitive.Item> </ComboboxPrimitive.Item>
); );
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { const ComboboxGroup = ({
return ( className,
...props
}: ComboboxPrimitive.Group.Props) => (
<ComboboxPrimitive.Group <ComboboxPrimitive.Group
data-slot='combobox-group' data-slot='combobox-group'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
); );
}
function ComboboxLabel({ const ComboboxLabel = ({
className, className,
...props ...props
}: ComboboxPrimitive.GroupLabel.Props) { }: ComboboxPrimitive.GroupLabel.Props) => (
return (
<ComboboxPrimitive.GroupLabel <ComboboxPrimitive.GroupLabel
data-slot='combobox-label' data-slot='combobox-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 ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { const ComboboxCollection = ({
return ( ...props
}: ComboboxPrimitive.Collection.Props) => (
<ComboboxPrimitive.Collection data-slot='combobox-collection' {...props} /> <ComboboxPrimitive.Collection data-slot='combobox-collection' {...props} />
); );
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { const ComboboxEmpty = ({
return ( className,
...props
}: ComboboxPrimitive.Empty.Props) => (
<ComboboxPrimitive.Empty <ComboboxPrimitive.Empty
data-slot='combobox-empty' data-slot='combobox-empty'
className={cn( className={cn(
@@ -202,27 +197,23 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
{...props} {...props}
/> />
); );
}
function ComboboxSeparator({ const ComboboxSeparator = ({
className, className,
...props ...props
}: ComboboxPrimitive.Separator.Props) { }: ComboboxPrimitive.Separator.Props) => (
return (
<ComboboxPrimitive.Separator <ComboboxPrimitive.Separator
data-slot='combobox-separator' data-slot='combobox-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 ComboboxChips({ 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(
@@ -232,17 +223,15 @@ function ComboboxChips({
{...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(
@@ -265,24 +254,19 @@ function ComboboxChip({
)} )}
</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,

View File

@@ -15,11 +15,10 @@ 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(
@@ -29,9 +28,8 @@ function Command({
{...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,8 +41,7 @@ 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>
@@ -61,13 +58,11 @@ function CommandDialog({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}
function CommandInput({ const CommandInput = ({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) => (
return (
<div data-slot='command-input-wrapper' className='p-1 pb-0'> <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!'> <InputGroup className='bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!'>
<CommandPrimitive.Input <CommandPrimitive.Input
@@ -84,13 +79,11 @@ function CommandInput({
</InputGroup> </InputGroup>
</div> </div>
); );
}
function CommandList({ const CommandList = ({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.List>) { }: React.ComponentProps<typeof CommandPrimitive.List>) => (
return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot='command-list' data-slot='command-list'
className={cn( className={cn(
@@ -100,26 +93,22 @@ function CommandList({
{...props} {...props}
/> />
); );
}
function CommandEmpty({ const CommandEmpty = ({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) { }: React.ComponentProps<typeof CommandPrimitive.Empty>) => (
return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot='command-empty' data-slot='command-empty'
className={cn('py-6 text-center text-sm', className)} className={cn('py-6 text-center text-sm', className)}
{...props} {...props}
/> />
); );
}
function CommandGroup({ const CommandGroup = ({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) { }: React.ComponentProps<typeof CommandPrimitive.Group>) => (
return (
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot='command-group' data-slot='command-group'
className={cn( className={cn(
@@ -129,27 +118,23 @@ function CommandGroup({
{...props} {...props}
/> />
); );
}
function CommandSeparator({ const CommandSeparator = ({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) { }: React.ComponentProps<typeof CommandPrimitive.Separator>) => (
return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot='command-separator' data-slot='command-separator'
className={cn('bg-border -mx-1 h-px', className)} className={cn('bg-border -mx-1 h-px', className)}
{...props} {...props}
/> />
); );
}
function CommandItem({ const 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(
@@ -162,13 +147,11 @@ function CommandItem({
<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(
@@ -178,7 +161,6 @@ function CommandShortcut({
{...props} {...props}
/> />
); );
}
export { export {
Command, Command,

View File

@@ -6,65 +6,56 @@ 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'
@@ -76,9 +67,8 @@ function ContextMenuContent({
/> />
</ContextMenuPrimitive.Portal> </ContextMenuPrimitive.Portal>
); );
}
function ContextMenuItem({ const ContextMenuItem = ({
className, className,
inset, inset,
variant = 'default', variant = 'default',
@@ -86,8 +76,7 @@ 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}
@@ -99,17 +88,15 @@ function ContextMenuItem({
{...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}
@@ -123,13 +110,11 @@ function ContextMenuSubTrigger({
<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(
@@ -139,9 +124,8 @@ function ContextMenuSubContent({
{...props} {...props}
/> />
); );
}
function ContextMenuCheckboxItem({ const ContextMenuCheckboxItem = ({
className, className,
children, children,
checked, checked,
@@ -149,8 +133,7 @@ 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}
@@ -169,17 +152,15 @@ function ContextMenuCheckboxItem({
{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}
@@ -197,16 +178,14 @@ function ContextMenuRadioItem({
{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}
@@ -217,26 +196,22 @@ function ContextMenuLabel({
{...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(
@@ -246,7 +221,6 @@ function ContextMenuShortcut({
{...props} {...props}
/> />
); );
}
export { export {
ContextMenu, ContextMenu,

View File

@@ -6,35 +6,34 @@ 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(
@@ -44,17 +43,15 @@ function DialogOverlay({
{...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
@@ -81,27 +78,23 @@ function DialogContent({
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
); );
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { const DialogHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='dialog-header' data-slot='dialog-header'
className={cn('flex flex-col gap-2', className)} className={cn('flex flex-col gap-2', className)}
{...props} {...props}
/> />
); );
}
function DialogFooter({ const DialogFooter = ({
className, className,
showCloseButton = false, showCloseButton = false,
children, children,
...props ...props
}: React.ComponentProps<'div'> & { }: React.ComponentProps<'div'> & {
showCloseButton?: boolean; showCloseButton?: boolean;
}) { }) => (
return (
<div <div
data-slot='dialog-footer' data-slot='dialog-footer'
className={cn( className={cn(
@@ -118,26 +111,22 @@ function DialogFooter({
)} )}
</div> </div>
); );
}
function DialogTitle({ 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(
@@ -147,7 +136,6 @@ function DialogDescription({
{...props} {...props}
/> />
); );
}
export { export {
Dialog, Dialog,

View File

@@ -5,35 +5,34 @@ 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(
@@ -43,14 +42,12 @@ function DrawerOverlay({
{...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
@@ -66,10 +63,8 @@ function DrawerContent({
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
); );
}
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { const DrawerHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='drawer-header' data-slot='drawer-header'
className={cn( className={cn(
@@ -79,43 +74,36 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { const DrawerFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='drawer-footer' data-slot='drawer-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)} className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props} {...props}
/> />
); );
}
function DrawerTitle({ 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,

View File

@@ -6,38 +6,30 @@ 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'
@@ -51,17 +43,14 @@ function DropdownMenuContent({
/> />
</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,8 +58,7 @@ 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}
@@ -82,9 +70,8 @@ function DropdownMenuItem({
{...props} {...props}
/> />
); );
}
function DropdownMenuCheckboxItem({ const DropdownMenuCheckboxItem = ({
className, className,
children, children,
checked, checked,
@@ -92,8 +79,7 @@ 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}
@@ -115,28 +101,24 @@ function DropdownMenuCheckboxItem({
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </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}
@@ -157,16 +139,14 @@ function DropdownMenuRadioItem({
{children} {children}
</DropdownMenuPrimitive.RadioItem> </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}
@@ -177,26 +157,22 @@ function DropdownMenuLabel({
{...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(
@@ -206,23 +182,21 @@ function DropdownMenuShortcut({
{...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}
@@ -236,13 +210,11 @@ function DropdownMenuSubTrigger({
<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(
@@ -252,7 +224,6 @@ function DropdownMenuSubContent({
{...props} {...props}
/> />
); );
}
export { export {
DropdownMenu, DropdownMenu,

View File

@@ -3,8 +3,7 @@ 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(
@@ -14,17 +13,14 @@ function Empty({ className, ...props }: React.ComponentProps<'div'>) {
{...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,12 +37,11 @@ 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}
@@ -54,20 +49,19 @@ function EmptyMedia({
{...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,
...props
}: React.ComponentProps<'p'>) => (
<div <div
data-slot='empty-description' data-slot='empty-description'
className={cn( className={cn(
@@ -77,10 +71,8 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
{...props} {...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(
@@ -90,7 +82,6 @@ function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
export { export {
Empty, Empty,

View File

@@ -6,11 +6,10 @@ 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(
@@ -21,14 +20,12 @@ export function FieldSet({
{...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}
@@ -41,13 +38,11 @@ export function FieldLegend({
{...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(
@@ -57,7 +52,6 @@ export function FieldGroup({
{...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,12 +77,11 @@ 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'
@@ -97,13 +90,11 @@ export function Field({
{...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(
@@ -113,13 +104,11 @@ export function FieldContent({
{...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(
@@ -131,13 +120,11 @@ export function FieldLabel({
{...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(
@@ -147,13 +134,11 @@ export function FieldTitle({
{...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(
@@ -165,16 +150,14 @@ export function FieldDescription({
{...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}
@@ -195,16 +178,15 @@ export function FieldSeparator({
)} )}
</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>
); );
} };

View File

@@ -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,

View File

@@ -0,0 +1,2 @@
export { useIsMobile } from './use-mobile';
export { useOnClickOutside } from './use-on-click-outside';

View File

@@ -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;
} };

View 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 };

View File

@@ -5,27 +5,24 @@ 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'
@@ -39,6 +36,5 @@ function HoverCardContent({
/> />
</HoverCardPrimitive.Portal> </HoverCardPrimitive.Portal>
); );
}
export { HoverCard, HoverCardTrigger, HoverCardContent }; export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -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>
); );
} };

View File

@@ -381,4 +381,4 @@ export {
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
} from './tooltip'; } from './tooltip';
export { useIsMobile } from './hooks/use-mobile'; export { useIsMobile, useOnClickOutside } from './hooks';

View File

@@ -6,8 +6,7 @@ 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'
@@ -18,7 +17,6 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
{...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,12 +39,12 @@ 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'
@@ -61,7 +59,6 @@ function InputGroupAddon({
{...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,15 +78,14 @@ 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}
@@ -98,10 +94,11 @@ function InputGroupButton({
{...props} {...props}
/> />
); );
}
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) { const InputGroupText = ({
return ( className,
...props
}: React.ComponentProps<'span'>) => (
<span <span
className={cn( className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
@@ -110,13 +107,11 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
{...props} {...props}
/> />
); );
}
function InputGroupInput({ const InputGroupInput = ({
className, className,
...props ...props
}: React.ComponentProps<'input'>) { }: React.ComponentProps<'input'>) => (
return (
<Input <Input
data-slot='input-group-control' data-slot='input-group-control'
className={cn( className={cn(
@@ -126,13 +121,11 @@ function InputGroupInput({
{...props} {...props}
/> />
); );
}
function InputGroupTextarea({ const InputGroupTextarea = ({
className, className,
...props ...props
}: React.ComponentProps<'textarea'>) { }: React.ComponentProps<'textarea'>) => (
return (
<Textarea <Textarea
data-slot='input-group-control' data-slot='input-group-control'
className={cn( className={cn(
@@ -142,7 +135,6 @@ function InputGroupTextarea({
{...props} {...props}
/> />
); );
}
export { export {
InputGroup, InputGroup,

View File

@@ -6,14 +6,13 @@ 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(
@@ -25,10 +24,11 @@ function InputOTP({
{...props} {...props}
/> />
); );
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) { const InputOTPGroup = ({
return ( className,
...props
}: React.ComponentProps<'div'>) => (
<div <div
data-slot='input-otp-group' data-slot='input-otp-group'
className={cn( className={cn(
@@ -38,15 +38,14 @@ function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...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,10 +67,9 @@ 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"
@@ -81,6 +79,5 @@ function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
<MinusIcon /> <MinusIcon />
</div> </div>
); );
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -2,8 +2,11 @@ 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,
type,
...props
}: React.ComponentProps<'input'>) => (
<input <input
type={type} type={type}
data-slot='input' data-slot='input'
@@ -14,6 +17,5 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
{...props} {...props}
/> />
); );
}
export { Input }; export { Input };

View File

@@ -5,8 +5,7 @@ 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'
@@ -17,13 +16,11 @@ function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
{...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'
@@ -31,7 +28,6 @@ function ItemSeparator({
{...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,12 +88,11 @@ 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}
@@ -105,10 +100,8 @@ function ItemMedia({
{...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(
@@ -118,10 +111,8 @@ function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
{...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(
@@ -131,10 +122,11 @@ function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) { const ItemDescription = ({
return ( className,
...props
}: React.ComponentProps<'p'>) => (
<p <p
data-slot='item-description' data-slot='item-description'
className={cn( className={cn(
@@ -144,20 +136,16 @@ function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
{...props} {...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(
@@ -167,10 +155,8 @@ function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
{...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(
@@ -180,7 +166,6 @@ function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
export { export {
Item, Item,

View File

@@ -1,7 +1,6 @@
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(
@@ -11,16 +10,13 @@ function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
{...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 };

View File

@@ -5,11 +5,10 @@ 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(
@@ -19,5 +18,4 @@ function Label({
{...props} {...props}
/> />
); );
}
export { Label }; export { Label };

View File

@@ -6,11 +6,10 @@ 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(
@@ -20,39 +19,35 @@ function Menubar({
{...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(
@@ -62,16 +57,14 @@ function MenubarTrigger({
{...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'
@@ -86,9 +79,8 @@ function MenubarContent({
/> />
</MenubarPortal> </MenubarPortal>
); );
}
function MenubarItem({ const MenubarItem = ({
className, className,
inset, inset,
variant = 'default', variant = 'default',
@@ -96,8 +88,7 @@ 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}
@@ -109,15 +100,13 @@ function MenubarItem({
{...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(
@@ -135,14 +124,12 @@ function MenubarCheckboxItem({
{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(
@@ -159,16 +146,14 @@ function MenubarRadioItem({
{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}
@@ -179,26 +164,22 @@ function MenubarLabel({
{...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(
@@ -208,23 +189,21 @@ function MenubarShortcut({
{...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}
@@ -238,13 +217,11 @@ function MenubarSubTrigger({
<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(
@@ -254,7 +231,6 @@ function MenubarSubContent({
{...props} {...props}
/> />
); );
}
export { export {
Menubar, Menubar,

View File

@@ -3,12 +3,13 @@ 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 <div
className='group/native-select relative w-fit has-[select:disabled]:opacity-50' className='group/native-select relative w-fit has-[select:disabled]:opacity-50'
data-slot='native-select-wrapper' data-slot='native-select-wrapper'
@@ -31,23 +32,20 @@ function NativeSelect({
/> />
</div> </div>
); );
}
function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) { const NativeSelectOption = ({ ...props }: React.ComponentProps<'option'>) => (
return <option data-slot='native-select-option' {...props} />; <option data-slot='native-select-option' {...props} />
} );
function NativeSelectOptGroup({ const NativeSelectOptGroup = ({
className, className,
...props ...props
}: React.ComponentProps<'optgroup'>) { }: React.ComponentProps<'optgroup'>) => (
return (
<optgroup <optgroup
data-slot='native-select-optgroup' data-slot='native-select-optgroup'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
); );
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }; export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };

View File

@@ -5,15 +5,14 @@ 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}
@@ -27,13 +26,11 @@ function NavigationMenu({
{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(
@@ -43,31 +40,27 @@ function NavigationMenuList({
{...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)}
@@ -80,13 +73,11 @@ function NavigationMenuTrigger({
/> />
</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( className={cn(
@@ -97,17 +88,13 @@ function NavigationMenuContent({
{...props} {...props}
/> />
); );
}
function NavigationMenuViewport({ const NavigationMenuViewport = ({
className, className,
...props ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) => (
return (
<div <div
className={cn( className={cn('absolute top-full left-0 isolate z-50 flex justify-center')}
'absolute top-full left-0 isolate z-50 flex justify-center',
)}
> >
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
data-slot='navigation-menu-viewport' data-slot='navigation-menu-viewport'
@@ -119,13 +106,11 @@ function NavigationMenuViewport({
/> />
</div> </div>
); );
}
function NavigationMenuLink({ const NavigationMenuLink = ({
className, className,
...props ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) => (
return (
<NavigationMenuPrimitive.Link <NavigationMenuPrimitive.Link
data-slot='navigation-menu-link' data-slot='navigation-menu-link'
className={cn( className={cn(
@@ -135,13 +120,11 @@ function NavigationMenuLink({
{...props} {...props}
/> />
); );
}
function NavigationMenuIndicator({ const NavigationMenuIndicator = ({
className, className,
...props ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) => (
return (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
data-slot='navigation-menu-indicator' data-slot='navigation-menu-indicator'
className={cn( className={cn(
@@ -153,7 +136,6 @@ function NavigationMenuIndicator({
<div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' /> <div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
); );
}
export { export {
NavigationMenu, NavigationMenu,

View File

@@ -7,8 +7,7 @@ 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'
@@ -17,37 +16,33 @@ function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
{...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'}
@@ -62,14 +57,12 @@ function PaginationLink({
/> />
</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'
@@ -80,14 +73,12 @@ function PaginationPrevious({
<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'
@@ -98,13 +89,11 @@ function PaginationNext({
<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'
@@ -118,7 +107,6 @@ function PaginationEllipsis({
<span className='sr-only'>More pages</span> <span className='sr-only'>More pages</span>
</span> </span>
); );
}
export { export {
Pagination, Pagination,

View File

@@ -5,25 +5,24 @@ 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'
@@ -37,46 +36,42 @@ function PopoverContent({
/> />
</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 ( className,
...props
}: React.ComponentProps<'div'>) => (
<div <div
data-slot='popover-header' data-slot='popover-header'
className={cn('flex flex-col gap-1 text-sm', className)} className={cn('flex flex-col gap-1 text-sm', className)}
{...props} {...props}
/> />
); );
}
function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) { const PopoverTitle = ({ className, ...props }: React.ComponentProps<'h2'>) => (
return (
<div <div
data-slot='popover-title' data-slot='popover-title'
className={cn('font-medium', className)} className={cn('font-medium', className)}
{...props} {...props}
/> />
); );
}
function PopoverDescription({ const PopoverDescription = ({
className, className,
...props ...props
}: React.ComponentProps<'p'>) { }: React.ComponentProps<'p'>) => (
return (
<p <p
data-slot='popover-description' data-slot='popover-description'
className={cn('text-muted-foreground', className)} className={cn('text-muted-foreground', className)}
{...props} {...props}
/> />
); );
}
export { export {
Popover, Popover,

View File

@@ -5,12 +5,11 @@ 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(
@@ -26,6 +25,5 @@ function Progress({
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
); );
}
export { Progress }; export { Progress };

View File

@@ -6,24 +6,21 @@ 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(
@@ -40,6 +37,5 @@ function RadioGroupItem({
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
); );
}
export { RadioGroup, RadioGroupItem }; export { RadioGroup, RadioGroupItem };

View File

@@ -5,11 +5,10 @@ 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(
@@ -19,20 +18,18 @@ function ResizablePanelGroup({
{...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(
@@ -48,6 +45,5 @@ function ResizableHandle({
)} )}
</ResizablePrimitive.Separator> </ResizablePrimitive.Separator>
); );
}
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }; export { ResizableHandle, ResizablePanel, ResizablePanelGroup };

View File

@@ -5,12 +5,11 @@ 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)}
@@ -26,14 +25,12 @@ function ScrollArea({
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </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}
@@ -50,6 +47,5 @@ function ScrollBar({
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
); );
}
export { ScrollArea, ScrollBar }; export { ScrollArea, ScrollBar };

View File

@@ -6,33 +6,32 @@ 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}
@@ -48,16 +47,14 @@ function SelectTrigger({
</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'
@@ -85,27 +82,23 @@ function SelectContent({
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </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(
@@ -125,26 +118,22 @@ function SelectItem({
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </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(
@@ -156,13 +145,11 @@ function SelectScrollUpButton({
<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(
@@ -174,7 +161,6 @@ function SelectScrollDownButton({
<ChevronDownIcon className='size-4' /> <ChevronDownIcon className='size-4' />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
); );
}
export { export {
Select, Select,

View File

@@ -5,13 +5,12 @@ 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}
@@ -23,6 +22,5 @@ function Separator({
{...props} {...props}
/> />
); );
}
export { Separator }; export { Separator };

View File

@@ -6,33 +6,34 @@ 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(
@@ -42,9 +43,8 @@ function SheetOverlay({
{...props} {...props}
/> />
); );
}
function SheetContent({ const SheetContent = ({
className, className,
children, children,
side = 'right', side = 'right',
@@ -53,8 +53,7 @@ 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
@@ -83,53 +82,44 @@ function SheetContent({
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
); );
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='sheet-header' data-slot='sheet-header'
className={cn('flex flex-col gap-1.5 p-4', className)} className={cn('flex flex-col gap-1.5 p-4', className)}
{...props} {...props}
/> />
); );
}
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { const SheetFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='sheet-footer' data-slot='sheet-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)} className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props} {...props}
/> />
); );
}
function SheetTitle({ 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,

View File

@@ -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,10 +304,12 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
{...props} {...props}
/> />
); );
} };
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) { const SidebarInset = ({
return ( className,
...props
}: React.ComponentProps<'main'>) => (
<main <main
data-slot='sidebar-inset' data-slot='sidebar-inset'
className={cn( className={cn(
@@ -315,13 +320,11 @@ function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
{...props} {...props}
/> />
); );
}
function SidebarInput({ const SidebarInput = ({
className, className,
...props ...props
}: React.ComponentProps<typeof Input>) { }: React.ComponentProps<typeof Input>) => (
return (
<Input <Input
data-slot='sidebar-input' data-slot='sidebar-input'
data-sidebar='input' data-sidebar='input'
@@ -329,10 +332,11 @@ function SidebarInput({
{...props} {...props}
/> />
); );
}
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) { const SidebarHeader = ({
return ( className,
...props
}: React.ComponentProps<'div'>) => (
<div <div
data-slot='sidebar-header' data-slot='sidebar-header'
data-sidebar='header' data-sidebar='header'
@@ -340,10 +344,11 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) { const SidebarFooter = ({
return ( className,
...props
}: React.ComponentProps<'div'>) => (
<div <div
data-slot='sidebar-footer' data-slot='sidebar-footer'
data-sidebar='footer' data-sidebar='footer'
@@ -351,13 +356,11 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function SidebarSeparator({ const SidebarSeparator = ({
className, className,
...props ...props
}: React.ComponentProps<typeof Separator>) { }: React.ComponentProps<typeof Separator>) => (
return (
<Separator <Separator
data-slot='sidebar-separator' data-slot='sidebar-separator'
data-sidebar='separator' data-sidebar='separator'
@@ -365,10 +368,11 @@ function SidebarSeparator({
{...props} {...props}
/> />
); );
}
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) { const SidebarContent = ({
return ( className,
...props
}: React.ComponentProps<'div'>) => (
<div <div
data-slot='sidebar-content' data-slot='sidebar-content'
data-sidebar='content' data-sidebar='content'
@@ -379,10 +383,8 @@ function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) { const SidebarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
return (
<div <div
data-slot='sidebar-group' data-slot='sidebar-group'
data-sidebar='group' data-sidebar='group'
@@ -390,13 +392,12 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
{...props} {...props}
/> />
); );
}
function SidebarGroupLabel({ 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,13 +435,12 @@ 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'
@@ -448,10 +448,8 @@ function SidebarGroupContent({
{...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'
@@ -459,10 +457,11 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
{...props} {...props}
/> />
); );
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) { const SidebarMenuItem = ({
return ( className,
...props
}: React.ComponentProps<'li'>) => (
<li <li
data-slot='sidebar-menu-item' data-slot='sidebar-menu-item'
data-sidebar='menu-item' data-sidebar='menu-item'
@@ -470,7 +469,6 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
{...props} {...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,13 +572,12 @@ 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'
@@ -596,15 +593,14 @@ function SidebarMenuBadge({
{...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,10 +630,12 @@ function SidebarMenuSkeleton({
/> />
</div> </div>
); );
} };
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) { const SidebarMenuSub = ({
return ( className,
...props
}: React.ComponentProps<'ul'>) => (
<ul <ul
data-slot='sidebar-menu-sub' data-slot='sidebar-menu-sub'
data-sidebar='menu-sub' data-sidebar='menu-sub'
@@ -649,13 +647,11 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
{...props} {...props}
/> />
); );
}
function SidebarMenuSubItem({ const SidebarMenuSubItem = ({
className, className,
...props ...props
}: React.ComponentProps<'li'>) { }: React.ComponentProps<'li'>) => (
return (
<li <li
data-slot='sidebar-menu-sub-item' data-slot='sidebar-menu-sub-item'
data-sidebar='menu-sub-item' data-sidebar='menu-sub-item'
@@ -663,9 +659,8 @@ function SidebarMenuSubItem({
{...props} {...props}
/> />
); );
}
function SidebarMenuSubButton({ 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