commit
This commit is contained in:
34
src/app/dashboard/react-query/page.tsx
Normal file
34
src/app/dashboard/react-query/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
|
import { getQueryClient } from '@/lib/query-client';
|
||||||
|
import { pokemonOptions } from '@/features/react-query-demo/api/queries';
|
||||||
|
import { PokemonInfo } from '@/features/react-query-demo/components/pokemon-info';
|
||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import { PokemonSkeleton } from '@/features/react-query-demo/components/pokemon-skeleton';
|
||||||
|
import { reactQueryInfoContent } from '@/features/react-query-demo/info-content';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Dashboard: React Query'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReactQueryPage() {
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
|
||||||
|
// Prefetch on the server — data is ready before client JS loads
|
||||||
|
void queryClient.prefetchQuery(pokemonOptions(25));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
scrollable
|
||||||
|
pageTitle='React Query'
|
||||||
|
pageDescription='Server prefetch + client hydration + suspense query pattern.'
|
||||||
|
infoContent={reactQueryInfoContent}
|
||||||
|
>
|
||||||
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
|
<Suspense fallback={<PokemonSkeleton />}>
|
||||||
|
<PokemonInfo />
|
||||||
|
</Suspense>
|
||||||
|
</HydrationBoundary>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/app/dashboard/users/page.tsx
Normal file
31
src/app/dashboard/users/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import UserListingPage from '@/features/users/components/user-listing';
|
||||||
|
import { searchParamsCache } from '@/lib/searchparams';
|
||||||
|
import type { SearchParams } from 'nuqs/server';
|
||||||
|
import { usersInfoContent } from '@/features/users/info-content';
|
||||||
|
import { UserFormSheetTrigger } from '@/features/users/components/user-form-sheet';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Dashboard: Users'
|
||||||
|
};
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
searchParams: Promise<SearchParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function UsersPage(props: PageProps) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
searchParamsCache.parse(searchParams);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
scrollable={false}
|
||||||
|
pageTitle='Users'
|
||||||
|
pageDescription='Manage users (React Query + nuqs table pattern.)'
|
||||||
|
infoContent={usersInfoContent}
|
||||||
|
pageHeaderAction={<UserFormSheetTrigger />}
|
||||||
|
>
|
||||||
|
<UserListingPage />
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/features/react-query-demo/api/queries.ts
Normal file
24
src/features/react-query-demo/api/queries.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export type Pokemon = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
sprites: {
|
||||||
|
front_shiny: string;
|
||||||
|
front_default: string;
|
||||||
|
};
|
||||||
|
types: { type: { name: string } }[];
|
||||||
|
stats: { base_stat: number; stat: { name: string } }[];
|
||||||
|
height: number;
|
||||||
|
weight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pokemonOptions = (id: number = 25) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ['pokemon', id],
|
||||||
|
queryFn: async (): Promise<Pokemon> => {
|
||||||
|
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch pokemon');
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
});
|
||||||
99
src/features/react-query-demo/components/pokemon-info.tsx
Normal file
99
src/features/react-query-demo/components/pokemon-info.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { pokemonOptions } from '../api/queries';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
CardFooter
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
|
||||||
|
const POKEMON_IDS = [25, 1, 4, 7, 6, 150, 133, 39, 143, 94];
|
||||||
|
|
||||||
|
export function PokemonInfo() {
|
||||||
|
const [pokemonId, setPokemonId] = useState(25);
|
||||||
|
const { data } = useSuspenseQuery(pokemonOptions(pokemonId));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
{/* Pokemon selector */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pick a Pokemon</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Each selection triggers <code>useSuspenseQuery</code> — cached results are instant, new
|
||||||
|
fetches show the Suspense fallback.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{POKEMON_IDS.map((id) => (
|
||||||
|
<Button
|
||||||
|
key={id}
|
||||||
|
variant={pokemonId === id ? 'default' : 'outline'}
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setPokemonId(id)}
|
||||||
|
>
|
||||||
|
#{id}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pokemon card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<CardTitle className='capitalize'>{data.name}</CardTitle>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
{data.types.map(({ type }) => (
|
||||||
|
<Badge key={type.name} variant='secondary'>
|
||||||
|
{type.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Height: {data.height / 10}m · Weight: {data.weight / 10}kg
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='flex flex-col items-center gap-6 sm:flex-row'>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={data.sprites.front_shiny}
|
||||||
|
alt={data.name}
|
||||||
|
width={160}
|
||||||
|
height={160}
|
||||||
|
className='bg-muted/50 rounded-lg'
|
||||||
|
/>
|
||||||
|
<div className='flex-1 space-y-3'>
|
||||||
|
{data.stats.map((s) => (
|
||||||
|
<div key={s.stat.name} className='space-y-1'>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-muted-foreground capitalize'>{s.stat.name}</span>
|
||||||
|
<span className='font-medium'>{s.base_stat}</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={Math.min(s.base_stat, 150) / 1.5} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
Data from PokeAPI · Prefetched on server, hydrated on client
|
||||||
|
</p>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card';
|
||||||
|
|
||||||
|
export function PokemonSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className='animate-pulse space-y-6'>
|
||||||
|
{/* Selector card skeleton */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className='bg-muted h-6 w-40 rounded' />
|
||||||
|
<div className='bg-muted mt-2 h-4 w-72 rounded' />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
<div key={i} className='bg-muted h-9 w-14 rounded-md' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pokemon card skeleton */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<div className='bg-muted h-7 w-32 rounded' />
|
||||||
|
<div className='bg-muted h-5 w-16 rounded-full' />
|
||||||
|
</div>
|
||||||
|
<div className='bg-muted mt-2 h-4 w-48 rounded' />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='flex flex-col items-center gap-6 sm:flex-row'>
|
||||||
|
<div className='bg-muted size-40 rounded-lg' />
|
||||||
|
<div className='w-full flex-1 space-y-3'>
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<div key={i} className='space-y-1'>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<div className='bg-muted h-4 w-24 rounded' />
|
||||||
|
<div className='bg-muted h-4 w-8 rounded' />
|
||||||
|
</div>
|
||||||
|
<div className='bg-muted h-2 w-full rounded-full' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<div className='bg-muted h-3 w-64 rounded' />
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/features/react-query-demo/info-content.ts
Normal file
46
src/features/react-query-demo/info-content.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { InfobarContent } from '@/components/ui/infobar';
|
||||||
|
|
||||||
|
export const reactQueryInfoContent: InfobarContent = {
|
||||||
|
title: 'React Query Pattern',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'Server Prefetch',
|
||||||
|
description:
|
||||||
|
'Data is prefetched on the server using getQueryClient().prefetchQuery(). The dehydrated state is passed to HydrationBoundary so the client starts with cached data — no loading spinners on first load.',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'TanStack Query SSR Docs',
|
||||||
|
url: 'https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Query Options',
|
||||||
|
description:
|
||||||
|
'Query keys and fetch functions are defined in a shared queryOptions() object. This is reused across server prefetch and client hooks, keeping them in sync.',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'queryOptions API',
|
||||||
|
url: 'https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Suspense Query',
|
||||||
|
description:
|
||||||
|
'The client uses useSuspenseQuery() which integrates with React Suspense. Combined with server prefetch, data is available immediately — Suspense only shows the fallback on subsequent navigations if the cache is stale.',
|
||||||
|
links: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Optimistic Mutations',
|
||||||
|
description:
|
||||||
|
'Mutations use onMutate to optimistically update the cache before the request completes. On error, the previous state is rolled back. On settle, the query is invalidated to refetch fresh data.',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Optimistic Updates Guide',
|
||||||
|
url: 'https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
27
src/features/users/api/mutations.ts
Normal file
27
src/features/users/api/mutations.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { mutationOptions } from '@tanstack/react-query';
|
||||||
|
import { getQueryClient } from '@/lib/query-client';
|
||||||
|
import { createUser, updateUser, deleteUser } from './service';
|
||||||
|
import { userKeys } from './queries';
|
||||||
|
import type { UserMutationPayload } from './types';
|
||||||
|
|
||||||
|
export const createUserMutation = mutationOptions({
|
||||||
|
mutationFn: (data: UserMutationPayload) => createUser(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
getQueryClient().invalidateQueries({ queryKey: userKeys.all });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateUserMutation = mutationOptions({
|
||||||
|
mutationFn: ({ id, values }: { id: number; values: UserMutationPayload }) =>
|
||||||
|
updateUser(id, values),
|
||||||
|
onSuccess: () => {
|
||||||
|
getQueryClient().invalidateQueries({ queryKey: userKeys.all });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteUserMutation = mutationOptions({
|
||||||
|
mutationFn: (id: number) => deleteUser(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
getQueryClient().invalidateQueries({ queryKey: userKeys.all });
|
||||||
|
}
|
||||||
|
});
|
||||||
17
src/features/users/api/queries.ts
Normal file
17
src/features/users/api/queries.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
import { getUsers } from './service';
|
||||||
|
import type { User, UserFilters } from './types';
|
||||||
|
|
||||||
|
export type { User };
|
||||||
|
|
||||||
|
export const userKeys = {
|
||||||
|
all: ['users'] as const,
|
||||||
|
list: (filters: UserFilters) => [...userKeys.all, 'list', filters] as const,
|
||||||
|
detail: (id: number) => [...userKeys.all, 'detail', id] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usersQueryOptions = (filters: UserFilters) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: userKeys.list(filters),
|
||||||
|
queryFn: () => getUsers(filters)
|
||||||
|
});
|
||||||
47
src/features/users/api/service.ts
Normal file
47
src/features/users/api/service.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// ============================================================
|
||||||
|
// User Service — Data Access Layer
|
||||||
|
// ============================================================
|
||||||
|
// This is the ONLY file you modify when connecting to your backend.
|
||||||
|
// Queries (queries.ts) and components import from here — they never change.
|
||||||
|
//
|
||||||
|
// Pick your pattern and replace the function bodies below:
|
||||||
|
//
|
||||||
|
// 1. Server Actions + ORM (Prisma / Drizzle / Supabase)
|
||||||
|
// → Add 'use server' at the top of this file
|
||||||
|
// → Call your ORM directly in each function
|
||||||
|
//
|
||||||
|
// 2. Route Handlers + ORM
|
||||||
|
// → import { apiClient } from '@/lib/api-client'
|
||||||
|
// → return apiClient<UsersResponse>('/users?...')
|
||||||
|
// → Replace mock calls in route handlers (src/app/api/users/) with ORM
|
||||||
|
//
|
||||||
|
// 3. BFF — Route Handlers proxy to external backend (Laravel, Go, etc.)
|
||||||
|
// → import { apiClient } from '@/lib/api-client'
|
||||||
|
// → return apiClient<UsersResponse>('/users?...')
|
||||||
|
// → Route handlers proxy requests to your external backend service
|
||||||
|
//
|
||||||
|
// 4. Direct external API (frontend-only, no Next.js backend)
|
||||||
|
// → const res = await fetch('https://your-api.com/users?...')
|
||||||
|
// → return res.json()
|
||||||
|
//
|
||||||
|
// Current: Mock (in-memory fake data for demo/prototyping)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { fakeUsers } from '@/constants/mock-api-users';
|
||||||
|
import type { UserFilters, UsersResponse, UserMutationPayload } from './types';
|
||||||
|
|
||||||
|
export async function getUsers(filters: UserFilters): Promise<UsersResponse> {
|
||||||
|
return fakeUsers.getUsers(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(data: UserMutationPayload) {
|
||||||
|
return fakeUsers.createUser(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(id: number, data: UserMutationPayload) {
|
||||||
|
return fakeUsers.updateUser(id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(id: number) {
|
||||||
|
return fakeUsers.deleteUser(id);
|
||||||
|
}
|
||||||
28
src/features/users/api/types.ts
Normal file
28
src/features/users/api/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export type { User } from '@/constants/mock-api-users';
|
||||||
|
|
||||||
|
export type UserFilters = {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
roles?: string;
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsersResponse = {
|
||||||
|
success: boolean;
|
||||||
|
time: string;
|
||||||
|
message: string;
|
||||||
|
total_users: number;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
users: import('@/constants/mock-api-users').User[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserMutationPayload = {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
role: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
189
src/features/users/components/user-form-sheet.tsx
Normal file
189
src/features/users/components/user-form-sheet.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useAppForm, useFormFields } from '@/components/ui/tanstack-form';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle
|
||||||
|
} from '@/components/ui/sheet';
|
||||||
|
import { Icons } from '@/components/icons';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { createUserMutation, updateUserMutation } from '../api/mutations';
|
||||||
|
import type { User } from '../api/types';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { userSchema, type UserFormValues } from '../schemas/user';
|
||||||
|
import { ROLE_OPTIONS } from './users-table/options';
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = [
|
||||||
|
{ value: 'Active', label: 'Active' },
|
||||||
|
{ value: 'Inactive', label: 'Inactive' },
|
||||||
|
{ value: 'Invited', label: 'Invited' }
|
||||||
|
];
|
||||||
|
|
||||||
|
interface UserFormSheetProps {
|
||||||
|
user?: User;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserFormSheet({ user, open, onOpenChange }: UserFormSheetProps) {
|
||||||
|
const isEdit = !!user;
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
...createUserMutation,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('User created successfully');
|
||||||
|
onOpenChange(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => toast.error('Failed to create user')
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
...updateUserMutation,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('User updated successfully');
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
onError: () => toast.error('Failed to update user')
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
first_name: user?.first_name ?? '',
|
||||||
|
last_name: user?.last_name ?? '',
|
||||||
|
email: user?.email ?? '',
|
||||||
|
phone: user?.phone ?? '',
|
||||||
|
role: user?.role ?? '',
|
||||||
|
status: user?.status ?? 'Active'
|
||||||
|
} as UserFormValues,
|
||||||
|
validators: {
|
||||||
|
onSubmit: userSchema
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
if (isEdit) {
|
||||||
|
await updateMutation.mutateAsync({ id: user.id, values: value });
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { FormTextField, FormSelectField } = useFormFields<UserFormValues>();
|
||||||
|
|
||||||
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent className='flex flex-col'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{isEdit ? 'Edit User' : 'New User'}</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
{isEdit
|
||||||
|
? 'Update the user details below.'
|
||||||
|
: 'Fill in the details to create a new user.'}
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className='flex-1 overflow-auto'>
|
||||||
|
<form.AppForm>
|
||||||
|
<form.Form id='user-form-sheet' className='space-y-4'>
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
<FormTextField
|
||||||
|
name='first_name'
|
||||||
|
label='First Name'
|
||||||
|
required
|
||||||
|
placeholder='John'
|
||||||
|
validators={{
|
||||||
|
onBlur: z.string().min(2, 'First name must be at least 2 characters')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormTextField
|
||||||
|
name='last_name'
|
||||||
|
label='Last Name'
|
||||||
|
required
|
||||||
|
placeholder='Doe'
|
||||||
|
validators={{
|
||||||
|
onBlur: z.string().min(2, 'Last name must be at least 2 characters')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormTextField
|
||||||
|
name='email'
|
||||||
|
label='Email'
|
||||||
|
required
|
||||||
|
type='email'
|
||||||
|
placeholder='john@example.com'
|
||||||
|
validators={{
|
||||||
|
onBlur: z.string().email('Please enter a valid email')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormTextField
|
||||||
|
name='phone'
|
||||||
|
label='Phone'
|
||||||
|
required
|
||||||
|
type='tel'
|
||||||
|
placeholder='(555) 123-4567'
|
||||||
|
validators={{
|
||||||
|
onBlur: z.string().min(1, 'Phone number is required')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSelectField
|
||||||
|
name='role'
|
||||||
|
label='Role'
|
||||||
|
required
|
||||||
|
options={ROLE_OPTIONS}
|
||||||
|
placeholder='Select role'
|
||||||
|
validators={{
|
||||||
|
onBlur: z.string().min(1, 'Please select a role')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSelectField
|
||||||
|
name='status'
|
||||||
|
label='Status'
|
||||||
|
required
|
||||||
|
options={STATUS_OPTIONS}
|
||||||
|
placeholder='Select status'
|
||||||
|
validators={{
|
||||||
|
onBlur: z.string().min(1, 'Please select a status')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Form>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SheetFooter>
|
||||||
|
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form='user-form-sheet' isLoading={isPending}>
|
||||||
|
<Icons.check /> {isEdit ? 'Update User' : 'Create User'}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserFormSheetTrigger() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setOpen(true)}>
|
||||||
|
<Icons.add className='mr-2 h-4 w-4' /> Add User
|
||||||
|
</Button>
|
||||||
|
<UserFormSheet open={open} onOpenChange={setOpen} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/features/users/components/user-listing.tsx
Normal file
31
src/features/users/components/user-listing.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
|
import { getQueryClient } from '@/lib/query-client';
|
||||||
|
import { searchParamsCache } from '@/lib/searchparams';
|
||||||
|
import { usersQueryOptions } from '../api/queries';
|
||||||
|
import { UsersTable } from './users-table';
|
||||||
|
|
||||||
|
export default function UserListingPage() {
|
||||||
|
const page = searchParamsCache.get('page');
|
||||||
|
const search = searchParamsCache.get('name');
|
||||||
|
const pageLimit = searchParamsCache.get('perPage');
|
||||||
|
const roles = searchParamsCache.get('role');
|
||||||
|
const sort = searchParamsCache.get('sort');
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
page,
|
||||||
|
limit: pageLimit,
|
||||||
|
...(search && { search }),
|
||||||
|
...(roles && { roles }),
|
||||||
|
...(sort && { sort })
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
|
||||||
|
void queryClient.prefetchQuery(usersQueryOptions(filters));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
|
<UsersTable />
|
||||||
|
</HydrationBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/features/users/components/users-table/cell-action.tsx
Normal file
66
src/features/users/components/users-table/cell-action.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
import { AlertModal } from '@/components/modal/alert-modal';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { deleteUserMutation } from '../../api/mutations';
|
||||||
|
import type { User } from '../../api/types';
|
||||||
|
import { Icons } from '@/components/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { UserFormSheet } from '../user-form-sheet';
|
||||||
|
|
||||||
|
interface CellActionProps {
|
||||||
|
data: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CellAction({ data }: CellActionProps) {
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
...deleteUserMutation,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('User deleted successfully');
|
||||||
|
setDeleteOpen(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Failed to delete user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deleteOpen}
|
||||||
|
onClose={() => setDeleteOpen(false)}
|
||||||
|
onConfirm={() => deleteMutation.mutate(data.id)}
|
||||||
|
loading={deleteMutation.isPending}
|
||||||
|
/>
|
||||||
|
<UserFormSheet user={data} open={editOpen} onOpenChange={setEditOpen} />
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='ghost' className='h-8 w-8 p-0'>
|
||||||
|
<span className='sr-only'>Open menu</span>
|
||||||
|
<Icons.ellipsis className='h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem onClick={() => setEditOpen(true)}>
|
||||||
|
<Icons.edit className='mr-2 h-4 w-4' /> Update
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setDeleteOpen(true)}>
|
||||||
|
<Icons.trash className='mr-2 h-4 w-4' /> Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/features/users/components/users-table/columns.tsx
Normal file
72
src/features/users/components/users-table/columns.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { DataTableColumnHeader } from '@/components/ui/table/data-table-column-header';
|
||||||
|
import type { User } from '../../api/types';
|
||||||
|
import { Column, ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { Icons } from '@/components/icons';
|
||||||
|
import { CellAction } from './cell-action';
|
||||||
|
import { ROLE_OPTIONS } from './options';
|
||||||
|
|
||||||
|
export const columns: ColumnDef<User>[] = [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
accessorFn: (row) => `${row.first_name} ${row.last_name}`,
|
||||||
|
header: ({ column }: { column: Column<User, unknown> }) => (
|
||||||
|
<DataTableColumnHeader column={column} title='Name' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
{row.original.first_name} {row.original.last_name}
|
||||||
|
</span>
|
||||||
|
<span className='text-muted-foreground text-xs'>{row.original.email}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
label: 'Name',
|
||||||
|
placeholder: 'Search users...',
|
||||||
|
variant: 'text' as const,
|
||||||
|
icon: Icons.text
|
||||||
|
},
|
||||||
|
enableColumnFilter: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'phone',
|
||||||
|
header: 'PHONE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role',
|
||||||
|
accessorKey: 'role',
|
||||||
|
enableSorting: false,
|
||||||
|
header: ({ column }: { column: Column<User, unknown> }) => (
|
||||||
|
<DataTableColumnHeader column={column} title='Role' />
|
||||||
|
),
|
||||||
|
cell: ({ cell }) => {
|
||||||
|
return (
|
||||||
|
<Badge variant='outline' className='capitalize'>
|
||||||
|
{cell.getValue<User['role']>()}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
enableColumnFilter: true,
|
||||||
|
meta: {
|
||||||
|
label: 'roles',
|
||||||
|
variant: 'multiSelect' as const,
|
||||||
|
options: ROLE_OPTIONS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'STATUS',
|
||||||
|
cell: ({ cell }) => {
|
||||||
|
const status = cell.getValue<User['status']>();
|
||||||
|
const variant =
|
||||||
|
status === 'Active' ? 'default' : status === 'Inactive' ? 'secondary' : 'outline';
|
||||||
|
return <Badge variant={variant}>{status}</Badge>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
cell: ({ row }) => <CellAction data={row.original} />
|
||||||
|
}
|
||||||
|
];
|
||||||
61
src/features/users/components/users-table/index.tsx
Normal file
61
src/features/users/components/users-table/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DataTable } from '@/components/ui/table/data-table';
|
||||||
|
import { DataTableToolbar } from '@/components/ui/table/data-table-toolbar';
|
||||||
|
import { useDataTable } from '@/hooks/use-data-table';
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';
|
||||||
|
import { getSortingStateParser } from '@/lib/parsers';
|
||||||
|
import { usersQueryOptions } from '../../api/queries';
|
||||||
|
import { columns } from './columns';
|
||||||
|
|
||||||
|
const columnIds = columns.map((c) => c.id).filter(Boolean) as string[];
|
||||||
|
|
||||||
|
export function UsersTable() {
|
||||||
|
const [params] = useQueryStates({
|
||||||
|
page: parseAsInteger.withDefault(1),
|
||||||
|
perPage: parseAsInteger.withDefault(10),
|
||||||
|
name: parseAsString,
|
||||||
|
role: parseAsString,
|
||||||
|
sort: getSortingStateParser(columnIds).withDefault([])
|
||||||
|
});
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
page: params.page,
|
||||||
|
limit: params.perPage,
|
||||||
|
...(params.name && { search: params.name }),
|
||||||
|
...(params.role && { roles: params.role }),
|
||||||
|
...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = useSuspenseQuery(usersQueryOptions(filters));
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(data.total_users / params.perPage);
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: data.users,
|
||||||
|
columns,
|
||||||
|
pageCount,
|
||||||
|
shallow: true,
|
||||||
|
debounceMs: 500,
|
||||||
|
initialState: {
|
||||||
|
columnPinning: { right: ['actions'] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable table={table}>
|
||||||
|
<DataTableToolbar table={table} />
|
||||||
|
</DataTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UsersTableSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-1 animate-pulse flex-col gap-4'>
|
||||||
|
<div className='bg-muted h-10 w-full rounded' />
|
||||||
|
<div className='bg-muted h-96 w-full rounded-lg' />
|
||||||
|
<div className='bg-muted h-10 w-full rounded' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/features/users/components/users-table/options.tsx
Normal file
8
src/features/users/components/users-table/options.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const ROLE_OPTIONS = [
|
||||||
|
{ value: 'Developer', label: 'Developer' },
|
||||||
|
{ value: 'Designer', label: 'Designer' },
|
||||||
|
{ value: 'Manager', label: 'Manager' },
|
||||||
|
{ value: 'QA', label: 'QA' },
|
||||||
|
{ value: 'DevOps', label: 'DevOps' },
|
||||||
|
{ value: 'Product Owner', label: 'Product Owner' }
|
||||||
|
];
|
||||||
41
src/features/users/info-content.ts
Normal file
41
src/features/users/info-content.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { InfobarContent } from '@/components/ui/infobar';
|
||||||
|
|
||||||
|
export const usersInfoContent: InfobarContent = {
|
||||||
|
title: 'Users — React Query + nuqs Pattern',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'Overview',
|
||||||
|
description:
|
||||||
|
'This page demonstrates client-side data fetching with React Query combined with nuqs URL search params — as an alternative to the Products page which uses server-side RSC fetching. Both patterns use the same DataTable, useDataTable hook, and nuqs URL state.',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'TanStack Query SSR Docs',
|
||||||
|
url: 'https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Server Prefetch + Client Hydration',
|
||||||
|
description:
|
||||||
|
'The server component reads search params via searchParamsCache, builds filters, and calls queryClient.prefetchQuery(). The dehydrated state is passed to HydrationBoundary so the client starts with cached data. The client component reads the same search params via useQueryState and calls useSuspenseQuery with matching filters.',
|
||||||
|
links: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'URL State with nuqs',
|
||||||
|
description:
|
||||||
|
'Pagination, search, and role filters are synced to the URL via nuqs. The useDataTable hook manages the TanStack Table state and debounces filter changes before updating the URL. When the URL changes, React Query automatically refetches because the query key includes the filters.',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'nuqs Documentation',
|
||||||
|
url: 'https://nuqs.47ng.com'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Products vs Users Pattern',
|
||||||
|
description:
|
||||||
|
'Products: searchParams → RSC fetch → pass data as props to client table. Users: searchParams → server prefetch → HydrationBoundary → client useSuspenseQuery. The Users pattern enables background refetching, cache sharing across components, and optimistic mutations.',
|
||||||
|
links: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
12
src/features/users/schemas/user.ts
Normal file
12
src/features/users/schemas/user.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const userSchema = z.object({
|
||||||
|
first_name: z.string().min(2, 'First name must be at least 2 characters'),
|
||||||
|
last_name: z.string().min(2, 'Last name must be at least 2 characters'),
|
||||||
|
email: z.string().email('Please enter a valid email'),
|
||||||
|
phone: z.string().min(1, 'Phone number is required'),
|
||||||
|
role: z.string().min(1, 'Please select a role'),
|
||||||
|
status: z.string().min(1, 'Please select a status')
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserFormValues = z.infer<typeof userSchema>;
|
||||||
46
src/hooks/use-breadcrumbs.tsx
Normal file
46
src/hooks/use-breadcrumbs.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
type BreadcrumbItem = {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This allows to add custom title as well
|
||||||
|
const routeMapping: Record<string, BreadcrumbItem[]> = {
|
||||||
|
'/dashboard': [{ title: 'Dashboard', link: '/dashboard' }],
|
||||||
|
'/dashboard/employee': [
|
||||||
|
{ title: 'Dashboard', link: '/dashboard' },
|
||||||
|
{ title: 'Employee', link: '/dashboard/employee' }
|
||||||
|
],
|
||||||
|
'/dashboard/product': [
|
||||||
|
{ title: 'Dashboard', link: '/dashboard' },
|
||||||
|
{ title: 'Product', link: '/dashboard/product' }
|
||||||
|
]
|
||||||
|
// Add more custom mappings as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useBreadcrumbs() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const breadcrumbs = useMemo(() => {
|
||||||
|
// Check if we have a custom mapping for this exact path
|
||||||
|
if (routeMapping[pathname]) {
|
||||||
|
return routeMapping[pathname];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact match, fall back to generating breadcrumbs from the path
|
||||||
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
|
return segments.map((segment, index) => {
|
||||||
|
const path = `/${segments.slice(0, index + 1).join('/')}`;
|
||||||
|
return {
|
||||||
|
title: segment.charAt(0).toUpperCase() + segment.slice(1),
|
||||||
|
link: path
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
}
|
||||||
22
src/hooks/use-callback-ref.tsx
Normal file
22
src/hooks/use-callback-ref.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
|
||||||
|
* prop or avoid re-executing effects when passed as a dependency
|
||||||
|
*/
|
||||||
|
function useCallbackRef<T extends (...args: never[]) => unknown>(callback: T | undefined): T {
|
||||||
|
const callbackRef = React.useRef(callback);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/facebook/react/issues/19240
|
||||||
|
return React.useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCallbackRef };
|
||||||
65
src/hooks/use-controllable-state.tsx
Normal file
65
src/hooks/use-controllable-state.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { useCallbackRef } from '@/hooks/use-callback-ref';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx
|
||||||
|
*/
|
||||||
|
|
||||||
|
type UseControllableStateParams<T> = {
|
||||||
|
prop?: T | undefined;
|
||||||
|
defaultProp?: T | undefined;
|
||||||
|
onChange?: (state: T) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetStateFn<T> = (prevState?: T) => T;
|
||||||
|
|
||||||
|
function useControllableState<T>({
|
||||||
|
prop,
|
||||||
|
defaultProp,
|
||||||
|
onChange = () => {}
|
||||||
|
}: UseControllableStateParams<T>) {
|
||||||
|
const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({
|
||||||
|
defaultProp,
|
||||||
|
onChange
|
||||||
|
});
|
||||||
|
const isControlled = prop !== undefined;
|
||||||
|
const value = isControlled ? prop : uncontrolledProp;
|
||||||
|
const handleChange = useCallbackRef(onChange);
|
||||||
|
|
||||||
|
const setValue: React.Dispatch<React.SetStateAction<T | undefined>> = React.useCallback(
|
||||||
|
(nextValue) => {
|
||||||
|
if (isControlled) {
|
||||||
|
const setter = nextValue as SetStateFn<T>;
|
||||||
|
const value = typeof nextValue === 'function' ? setter(prop) : nextValue;
|
||||||
|
if (value !== prop) handleChange(value as T);
|
||||||
|
} else {
|
||||||
|
setUncontrolledProp(nextValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isControlled, prop, setUncontrolledProp, handleChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, setValue] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useUncontrolledState<T>({
|
||||||
|
defaultProp,
|
||||||
|
onChange
|
||||||
|
}: Omit<UseControllableStateParams<T>, 'prop'>) {
|
||||||
|
const uncontrolledState = React.useState<T | undefined>(defaultProp);
|
||||||
|
const [value] = uncontrolledState;
|
||||||
|
const prevValueRef = React.useRef(value);
|
||||||
|
const handleChange = useCallbackRef(onChange);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (prevValueRef.current !== value) {
|
||||||
|
handleChange(value as T);
|
||||||
|
prevValueRef.current = value;
|
||||||
|
}
|
||||||
|
}, [value, prevValueRef, handleChange]);
|
||||||
|
|
||||||
|
return uncontrolledState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useControllableState };
|
||||||
284
src/hooks/use-data-table.ts
Normal file
284
src/hooks/use-data-table.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type ColumnPinningState,
|
||||||
|
type PaginationState,
|
||||||
|
type RowSelectionState,
|
||||||
|
type SortingState,
|
||||||
|
type TableOptions,
|
||||||
|
type TableState,
|
||||||
|
type Updater,
|
||||||
|
type VisibilityState,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFacetedMinMaxValues,
|
||||||
|
getFacetedRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import {
|
||||||
|
type Parser,
|
||||||
|
type UseQueryStateOptions,
|
||||||
|
parseAsArrayOf,
|
||||||
|
parseAsInteger,
|
||||||
|
parseAsString,
|
||||||
|
useQueryState,
|
||||||
|
useQueryStates
|
||||||
|
} from 'nuqs';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { useDebouncedCallback } from '@/hooks/use-debounced-callback';
|
||||||
|
import { getSortingStateParser } from '@/lib/parsers';
|
||||||
|
import type { ExtendedColumnSort } from '@/types/data-table';
|
||||||
|
|
||||||
|
const PAGE_KEY = 'page';
|
||||||
|
const PER_PAGE_KEY = 'perPage';
|
||||||
|
const SORT_KEY = 'sort';
|
||||||
|
const ARRAY_SEPARATOR = ',';
|
||||||
|
const DEBOUNCE_MS = 300;
|
||||||
|
const THROTTLE_MS = 50;
|
||||||
|
|
||||||
|
interface UseDataTableProps<TData>
|
||||||
|
extends
|
||||||
|
Omit<
|
||||||
|
TableOptions<TData>,
|
||||||
|
| 'state'
|
||||||
|
| 'pageCount'
|
||||||
|
| 'getCoreRowModel'
|
||||||
|
| 'manualFiltering'
|
||||||
|
| 'manualPagination'
|
||||||
|
| 'manualSorting'
|
||||||
|
>,
|
||||||
|
Required<Pick<TableOptions<TData>, 'pageCount'>> {
|
||||||
|
initialState?: Omit<Partial<TableState>, 'sorting'> & {
|
||||||
|
sorting?: ExtendedColumnSort<TData>[];
|
||||||
|
};
|
||||||
|
history?: 'push' | 'replace';
|
||||||
|
debounceMs?: number;
|
||||||
|
throttleMs?: number;
|
||||||
|
clearOnDefault?: boolean;
|
||||||
|
enableAdvancedFilter?: boolean;
|
||||||
|
scroll?: boolean;
|
||||||
|
shallow?: boolean;
|
||||||
|
startTransition?: React.TransitionStartFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDataTable<TData>(props: UseDataTableProps<TData>) {
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
pageCount = -1,
|
||||||
|
initialState,
|
||||||
|
history = 'replace',
|
||||||
|
debounceMs = DEBOUNCE_MS,
|
||||||
|
throttleMs = THROTTLE_MS,
|
||||||
|
clearOnDefault = false,
|
||||||
|
enableAdvancedFilter = false,
|
||||||
|
scroll = false,
|
||||||
|
shallow = true,
|
||||||
|
startTransition,
|
||||||
|
...tableProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const queryStateOptions = React.useMemo<Omit<UseQueryStateOptions<string>, 'parse'>>(
|
||||||
|
() => ({
|
||||||
|
history,
|
||||||
|
scroll,
|
||||||
|
shallow,
|
||||||
|
throttleMs,
|
||||||
|
debounceMs,
|
||||||
|
clearOnDefault,
|
||||||
|
startTransition
|
||||||
|
}),
|
||||||
|
[history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(
|
||||||
|
initialState?.rowSelection ?? {}
|
||||||
|
);
|
||||||
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
|
||||||
|
initialState?.columnVisibility ?? {}
|
||||||
|
);
|
||||||
|
const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>(
|
||||||
|
initialState?.columnPinning ?? {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [page, setPage] = useQueryState(
|
||||||
|
PAGE_KEY,
|
||||||
|
parseAsInteger.withOptions(queryStateOptions).withDefault(1)
|
||||||
|
);
|
||||||
|
const [perPage, setPerPage] = useQueryState(
|
||||||
|
PER_PAGE_KEY,
|
||||||
|
parseAsInteger
|
||||||
|
.withOptions(queryStateOptions)
|
||||||
|
.withDefault(initialState?.pagination?.pageSize ?? 10)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagination: PaginationState = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
pageIndex: page - 1, // zero-based index -> one-based index
|
||||||
|
pageSize: perPage
|
||||||
|
};
|
||||||
|
}, [page, perPage]);
|
||||||
|
|
||||||
|
const onPaginationChange = React.useCallback(
|
||||||
|
(updaterOrValue: Updater<PaginationState>) => {
|
||||||
|
if (typeof updaterOrValue === 'function') {
|
||||||
|
const newPagination = updaterOrValue(pagination);
|
||||||
|
void setPage(newPagination.pageIndex + 1);
|
||||||
|
void setPerPage(newPagination.pageSize);
|
||||||
|
} else {
|
||||||
|
void setPage(updaterOrValue.pageIndex + 1);
|
||||||
|
void setPerPage(updaterOrValue.pageSize);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pagination, setPage, setPerPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnIds = React.useMemo(() => {
|
||||||
|
return new Set(columns.map((column) => column.id).filter(Boolean) as string[]);
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useQueryState(
|
||||||
|
SORT_KEY,
|
||||||
|
getSortingStateParser<TData>(columnIds)
|
||||||
|
.withOptions(queryStateOptions)
|
||||||
|
.withDefault(initialState?.sorting ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSortingChange = React.useCallback(
|
||||||
|
(updaterOrValue: Updater<SortingState>) => {
|
||||||
|
if (typeof updaterOrValue === 'function') {
|
||||||
|
const newSorting = updaterOrValue(sorting);
|
||||||
|
setSorting(newSorting as ExtendedColumnSort<TData>[]);
|
||||||
|
} else {
|
||||||
|
setSorting(updaterOrValue as ExtendedColumnSort<TData>[]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sorting, setSorting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterableColumns = React.useMemo(() => {
|
||||||
|
if (enableAdvancedFilter) return [];
|
||||||
|
|
||||||
|
return columns.filter((column) => column.enableColumnFilter);
|
||||||
|
}, [columns, enableAdvancedFilter]);
|
||||||
|
|
||||||
|
const filterParsers = React.useMemo(() => {
|
||||||
|
if (enableAdvancedFilter) return {};
|
||||||
|
|
||||||
|
return filterableColumns.reduce<Record<string, Parser<string> | Parser<string[]>>>(
|
||||||
|
(acc, column) => {
|
||||||
|
if (column.meta?.options) {
|
||||||
|
acc[column.id ?? ''] = parseAsArrayOf(parseAsString, ARRAY_SEPARATOR).withOptions(
|
||||||
|
queryStateOptions
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
acc[column.id ?? ''] = parseAsString.withOptions(queryStateOptions);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}, [filterableColumns, queryStateOptions, enableAdvancedFilter]);
|
||||||
|
|
||||||
|
const [filterValues, setFilterValues] = useQueryStates(filterParsers);
|
||||||
|
|
||||||
|
const debouncedSetFilterValues = useDebouncedCallback((values: typeof filterValues) => {
|
||||||
|
void setPage(1);
|
||||||
|
void setFilterValues(values);
|
||||||
|
}, debounceMs);
|
||||||
|
|
||||||
|
const initialColumnFilters: ColumnFiltersState = React.useMemo(() => {
|
||||||
|
if (enableAdvancedFilter) return [];
|
||||||
|
|
||||||
|
return Object.entries(filterValues).reduce<ColumnFiltersState>((filters, [key, value]) => {
|
||||||
|
if (value !== null) {
|
||||||
|
const processedValue = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: typeof value === 'string' && /[^a-zA-Z0-9]/.test(value)
|
||||||
|
? value.split(/[^a-zA-Z0-9]+/).filter(Boolean)
|
||||||
|
: [value];
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
id: key,
|
||||||
|
value: processedValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
}, []);
|
||||||
|
}, [filterValues, enableAdvancedFilter]);
|
||||||
|
|
||||||
|
const [columnFilters, setColumnFilters] =
|
||||||
|
React.useState<ColumnFiltersState>(initialColumnFilters);
|
||||||
|
|
||||||
|
const onColumnFiltersChange = React.useCallback(
|
||||||
|
(updaterOrValue: Updater<ColumnFiltersState>) => {
|
||||||
|
if (enableAdvancedFilter) return;
|
||||||
|
|
||||||
|
setColumnFilters((prev) => {
|
||||||
|
const next = typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue;
|
||||||
|
|
||||||
|
const filterUpdates = next.reduce<Record<string, string | string[] | null>>(
|
||||||
|
(acc, filter) => {
|
||||||
|
if (filterableColumns.find((column) => column.id === filter.id)) {
|
||||||
|
acc[filter.id] = filter.value as string | string[];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const prevFilter of prev) {
|
||||||
|
if (!next.some((filter) => filter.id === prevFilter.id)) {
|
||||||
|
filterUpdates[prevFilter.id] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedSetFilterValues(filterUpdates);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[debouncedSetFilterValues, filterableColumns, enableAdvancedFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
...tableProps,
|
||||||
|
columns,
|
||||||
|
initialState,
|
||||||
|
pageCount,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
sorting,
|
||||||
|
columnVisibility,
|
||||||
|
columnPinning,
|
||||||
|
rowSelection,
|
||||||
|
columnFilters
|
||||||
|
},
|
||||||
|
defaultColumn: {
|
||||||
|
...tableProps.defaultColumn,
|
||||||
|
enableColumnFilter: false
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onPaginationChange,
|
||||||
|
onSortingChange,
|
||||||
|
onColumnFiltersChange,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onColumnPinningChange: setColumnPinning,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
|
getFacetedMinMaxValues: getFacetedMinMaxValues(),
|
||||||
|
manualPagination: true,
|
||||||
|
manualSorting: true,
|
||||||
|
manualFiltering: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return { table, shallow, debounceMs, throttleMs };
|
||||||
|
}
|
||||||
19
src/hooks/use-debounce.tsx
Normal file
19
src/hooks/use-debounce.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
22
src/hooks/use-debounced-callback.ts
Normal file
22
src/hooks/use-debounced-callback.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { useCallbackRef } from '@/hooks/use-callback-ref';
|
||||||
|
|
||||||
|
export function useDebouncedCallback<T extends (...args: never[]) => unknown>(
|
||||||
|
callback: T,
|
||||||
|
delay: number
|
||||||
|
) {
|
||||||
|
const handleCallback = useCallbackRef(callback);
|
||||||
|
const debounceTimerRef = React.useRef(0);
|
||||||
|
React.useEffect(() => () => window.clearTimeout(debounceTimerRef.current), []);
|
||||||
|
|
||||||
|
const setValue = React.useCallback(
|
||||||
|
(...args: Parameters<T>) => {
|
||||||
|
window.clearTimeout(debounceTimerRef.current);
|
||||||
|
debounceTimerRef.current = window.setTimeout(() => handleCallback(...args), delay);
|
||||||
|
},
|
||||||
|
[handleCallback, delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return setValue;
|
||||||
|
}
|
||||||
19
src/hooks/use-media-query.ts
Normal file
19
src/hooks/use-media-query.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useMediaQuery() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||||
|
setIsOpen(mediaQuery.matches);
|
||||||
|
|
||||||
|
const handler = (e: MediaQueryListEvent) => {
|
||||||
|
setIsOpen(e.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handler);
|
||||||
|
return () => mediaQuery.removeEventListener('change', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isOpen };
|
||||||
|
}
|
||||||
19
src/hooks/use-mobile.tsx
Normal file
19
src/hooks/use-mobile.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||||
|
const onChange = () => {
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
};
|
||||||
|
mql.addEventListener('change', onChange);
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
return () => mql.removeEventListener('change', onChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return !!isMobile;
|
||||||
|
}
|
||||||
180
src/hooks/use-nav.ts
Normal file
180
src/hooks/use-nav.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully client-side hook for filtering navigation items based on RBAC
|
||||||
|
*
|
||||||
|
* This hook uses Clerk's client-side hooks to check permissions, roles, and organization
|
||||||
|
* without any server calls. This is perfect for navigation visibility (UX only).
|
||||||
|
*
|
||||||
|
* Performance:
|
||||||
|
* - All checks are synchronous (no server calls)
|
||||||
|
* - Instant filtering
|
||||||
|
* - No loading states
|
||||||
|
* - No UI flashing
|
||||||
|
*
|
||||||
|
* Note: For actual security (API routes, server actions), always use server-side checks.
|
||||||
|
* This is only for UI visibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useOrganization, useUser } from '@clerk/nextjs';
|
||||||
|
import type { NavItem, NavGroup } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to filter navigation items based on RBAC (fully client-side)
|
||||||
|
*
|
||||||
|
* @param items - Array of navigation items to filter
|
||||||
|
* @returns Filtered items
|
||||||
|
*/
|
||||||
|
export function useFilteredNavItems(items: NavItem[]) {
|
||||||
|
const { organization, membership } = useOrganization();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
// Memoize context and permissions
|
||||||
|
const accessContext = useMemo(() => {
|
||||||
|
const permissions = membership?.permissions || [];
|
||||||
|
const role = membership?.role;
|
||||||
|
|
||||||
|
return {
|
||||||
|
organization: organization ?? undefined,
|
||||||
|
user: user ?? undefined,
|
||||||
|
permissions: permissions as string[],
|
||||||
|
role: role ?? undefined,
|
||||||
|
hasOrg: !!organization
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- using stable primitives to avoid infinite re-renders from unstable Clerk object refs
|
||||||
|
}, [organization?.id, user?.id, membership?.permissions, membership?.role]);
|
||||||
|
|
||||||
|
// Filter items synchronously (all client-side)
|
||||||
|
const filteredItems = useMemo(() => {
|
||||||
|
return items
|
||||||
|
.filter((item) => {
|
||||||
|
// No access restrictions
|
||||||
|
if (!item.access) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check requireOrg
|
||||||
|
if (item.access.requireOrg && !accessContext.hasOrg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (item.access.permission) {
|
||||||
|
if (!accessContext.hasOrg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!accessContext.permissions.includes(item.access.permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role
|
||||||
|
if (item.access.role) {
|
||||||
|
if (!accessContext.hasOrg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (accessContext.role !== item.access.role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Plans and features require server-side checks with Clerk's has() function
|
||||||
|
// For navigation visibility, you can either:
|
||||||
|
// 1. Store plan/feature info in organization metadata (client-accessible)
|
||||||
|
// 2. Use server actions (current approach)
|
||||||
|
// 3. Skip plan/feature checks for navigation (recommended for performance)
|
||||||
|
|
||||||
|
// For now, if plan/feature is specified, we'll need to handle it differently
|
||||||
|
// Most navigation items won't need plan/feature checks anyway
|
||||||
|
if (item.access.plan || item.access.feature) {
|
||||||
|
// Option: Return true and let the page handle it, or use server action
|
||||||
|
// For now, we'll show it (page-level protection should handle it)
|
||||||
|
console.warn(
|
||||||
|
`Plan/feature checks for navigation items require server-side verification. ` +
|
||||||
|
`Item "${item.title}" will be shown, but page-level protection should be implemented.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((item) => {
|
||||||
|
// Recursively filter child items
|
||||||
|
if (item.items && item.items.length > 0) {
|
||||||
|
const filteredChildren = item.items.filter((childItem) => {
|
||||||
|
// No access restrictions
|
||||||
|
if (!childItem.access) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check requireOrg
|
||||||
|
if (childItem.access.requireOrg && !accessContext.hasOrg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (childItem.access.permission) {
|
||||||
|
if (!accessContext.hasOrg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!accessContext.permissions.includes(childItem.access.permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role
|
||||||
|
if (childItem.access.role) {
|
||||||
|
if (!accessContext.hasOrg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (accessContext.role !== childItem.access.role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan/feature checks (same warning as above)
|
||||||
|
if (childItem.access.plan || childItem.access.feature) {
|
||||||
|
console.warn(
|
||||||
|
`Plan/feature checks for navigation items require server-side verification. ` +
|
||||||
|
`Item "${childItem.title}" will be shown, but page-level protection should be implemented.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
items: filteredChildren
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}, [items, accessContext]);
|
||||||
|
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to filter navigation groups based on RBAC (fully client-side)
|
||||||
|
*
|
||||||
|
* @param groups - Array of navigation groups to filter
|
||||||
|
* @returns Filtered groups (empty groups are removed)
|
||||||
|
*/
|
||||||
|
export function useFilteredNavGroups(groups: NavGroup[]) {
|
||||||
|
const allItems = useMemo(() => groups.flatMap((g) => g.items), [groups]);
|
||||||
|
const filteredItems = useFilteredNavItems(allItems);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const filteredSet = new Set(filteredItems.map((item) => item.title));
|
||||||
|
return groups
|
||||||
|
.map((group) => ({
|
||||||
|
...group,
|
||||||
|
items: filteredItems.filter((item) =>
|
||||||
|
group.items.some((gi) => gi.title === item.title && filteredSet.has(gi.title))
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.filter((group) => group.items.length > 0);
|
||||||
|
}, [groups, filteredItems]);
|
||||||
|
}
|
||||||
100
src/hooks/use-stepper.tsx
Normal file
100
src/hooks/use-stepper.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import type { AnyFormApi } from '@tanstack/react-form';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import type { ZodTypeAny } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for handling cancel/back actions
|
||||||
|
*/
|
||||||
|
type HandleCancelOrBackOpts = {
|
||||||
|
onBack?: VoidFunction;
|
||||||
|
onCancel?: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the current step
|
||||||
|
*/
|
||||||
|
type StepState = {
|
||||||
|
value: number;
|
||||||
|
count: number;
|
||||||
|
goToNextStep: () => void;
|
||||||
|
goToPrevStep: () => void;
|
||||||
|
isCompleted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing multi-step form navigation and validation
|
||||||
|
*
|
||||||
|
* @param schemas - Array of Zod schemas for each step
|
||||||
|
* @returns Object with stepper state and methods
|
||||||
|
*/
|
||||||
|
export function useFormStepper(schemas: ZodTypeAny[]) {
|
||||||
|
const stepCount = schemas.length;
|
||||||
|
const [currentStep, setCurrentStep] = useState(1); // Start from 1
|
||||||
|
|
||||||
|
const goToNextStep = useCallback(() => {
|
||||||
|
setCurrentStep((prev) => Math.min(prev + 1, stepCount));
|
||||||
|
}, [stepCount]);
|
||||||
|
|
||||||
|
const goToPrevStep = useCallback(() => {
|
||||||
|
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const step: StepState = {
|
||||||
|
value: currentStep,
|
||||||
|
count: stepCount,
|
||||||
|
goToNextStep,
|
||||||
|
goToPrevStep,
|
||||||
|
isCompleted: currentStep === stepCount
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentValidator = schemas[currentStep - 1]; // Convert to 0-based for array access
|
||||||
|
const isFirstStep = currentStep === 1;
|
||||||
|
|
||||||
|
const triggerFormGroup = async (form: AnyFormApi) => {
|
||||||
|
const result = currentValidator.safeParse(form.state.values);
|
||||||
|
if (!result.success) {
|
||||||
|
await form.handleSubmit({ step: String(currentStep) });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextStepOrSubmit = async (form: AnyFormApi) => {
|
||||||
|
const result = await triggerFormGroup(form);
|
||||||
|
if (!result.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep < stepCount) {
|
||||||
|
goToNextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep === stepCount) {
|
||||||
|
form.handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelOrBack = (opts?: HandleCancelOrBackOpts) => {
|
||||||
|
if (isFirstStep || step.isCompleted) {
|
||||||
|
opts?.onCancel?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep > 1) {
|
||||||
|
opts?.onBack?.();
|
||||||
|
goToPrevStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
step, // Current step state
|
||||||
|
currentStep, // Current step number (1-based)
|
||||||
|
isFirstStep, // Whether current step is the first step
|
||||||
|
currentValidator, // Zod schema for current step
|
||||||
|
triggerFormGroup, // Validate current step fields
|
||||||
|
handleNextStepOrSubmit, // Handle next/submit action
|
||||||
|
handleCancelOrBack // Handle back/cancel action
|
||||||
|
};
|
||||||
|
}
|
||||||
14
src/lib/api-client.ts
Normal file
14
src/lib/api-client.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const BASE_URL = '/api';
|
||||||
|
|
||||||
|
export async function apiClient<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||||
|
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
63
src/lib/compose-refs.ts
Normal file
63
src/lib/compose-refs.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
type PossibleRef<T> = React.Ref<T> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a given ref to a given value
|
||||||
|
* This utility takes care of different types of refs: callback refs and RefObject(s)
|
||||||
|
*/
|
||||||
|
function setRef<T>(ref: PossibleRef<T>, value: T) {
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
return ref(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref !== null && ref !== undefined) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility to compose multiple refs together
|
||||||
|
* Accepts callback refs and RefObject(s)
|
||||||
|
*/
|
||||||
|
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
||||||
|
return (node) => {
|
||||||
|
let hasCleanup = false;
|
||||||
|
const cleanups = refs.map((ref) => {
|
||||||
|
const cleanup = setRef(ref, node);
|
||||||
|
if (!hasCleanup && typeof cleanup === 'function') {
|
||||||
|
hasCleanup = true;
|
||||||
|
}
|
||||||
|
return cleanup;
|
||||||
|
});
|
||||||
|
|
||||||
|
// React <19 will log an error to the console if a callback ref returns a
|
||||||
|
// value. We don't use ref cleanups internally so this will only happen if a
|
||||||
|
// user's ref callback returns a value, which we only expect if they are
|
||||||
|
// using the cleanup functionality added in React 19.
|
||||||
|
if (hasCleanup) {
|
||||||
|
return () => {
|
||||||
|
for (let i = 0; i < cleanups.length; i++) {
|
||||||
|
const cleanup = cleanups[i];
|
||||||
|
if (typeof cleanup === 'function') {
|
||||||
|
cleanup();
|
||||||
|
} else {
|
||||||
|
setRef(refs[i], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook that composes multiple refs
|
||||||
|
* Accepts callback refs and RefObject(s)
|
||||||
|
*/
|
||||||
|
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: memoize by all ref values
|
||||||
|
return React.useCallback(composeRefs(...refs), refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { composeRefs, useComposedRefs };
|
||||||
62
src/lib/data-table.ts
Normal file
62
src/lib/data-table.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { ExtendedColumnFilter, FilterOperator, FilterVariant } from '@/types/data-table';
|
||||||
|
import type { Column } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { dataTableConfig } from '@/config/data-table';
|
||||||
|
|
||||||
|
export function getCommonPinningStyles<TData>({
|
||||||
|
column
|
||||||
|
}: {
|
||||||
|
column: Column<TData>;
|
||||||
|
}): React.CSSProperties {
|
||||||
|
const isPinned = column.getIsPinned();
|
||||||
|
const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left');
|
||||||
|
const isFirstRightPinnedColumn = isPinned === 'right' && column.getIsFirstColumn('right');
|
||||||
|
|
||||||
|
return {
|
||||||
|
boxShadow: isLastLeftPinnedColumn
|
||||||
|
? '-5px 0 5px -5px var(--border) inset'
|
||||||
|
: isFirstRightPinnedColumn
|
||||||
|
? '5px 0 5px -5px var(--border) inset'
|
||||||
|
: undefined,
|
||||||
|
left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
|
||||||
|
right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
|
||||||
|
position: isPinned ? 'sticky' : 'relative',
|
||||||
|
background: isPinned ? 'var(--background)' : undefined,
|
||||||
|
width: column.getSize(),
|
||||||
|
zIndex: isPinned ? 1 : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterOperators(filterVariant: FilterVariant) {
|
||||||
|
const operatorMap: Record<FilterVariant, { label: string; value: FilterOperator }[]> = {
|
||||||
|
text: dataTableConfig.textOperators,
|
||||||
|
number: dataTableConfig.numericOperators,
|
||||||
|
range: dataTableConfig.numericOperators,
|
||||||
|
date: dataTableConfig.dateOperators,
|
||||||
|
dateRange: dataTableConfig.dateOperators,
|
||||||
|
boolean: dataTableConfig.booleanOperators,
|
||||||
|
select: dataTableConfig.selectOperators,
|
||||||
|
multiSelect: dataTableConfig.multiSelectOperators
|
||||||
|
};
|
||||||
|
|
||||||
|
return operatorMap[filterVariant] ?? dataTableConfig.textOperators;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultFilterOperator(filterVariant: FilterVariant) {
|
||||||
|
const operators = getFilterOperators(filterVariant);
|
||||||
|
|
||||||
|
return operators[0]?.value ?? (filterVariant === 'text' ? 'iLike' : 'eq');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValidFilters<TData>(
|
||||||
|
filters: ExtendedColumnFilter<TData>[]
|
||||||
|
): ExtendedColumnFilter<TData>[] {
|
||||||
|
return filters.filter(
|
||||||
|
(filter) =>
|
||||||
|
filter.operator === 'isEmpty' ||
|
||||||
|
filter.operator === 'isNotEmpty' ||
|
||||||
|
(Array.isArray(filter.value)
|
||||||
|
? filter.value.length > 0
|
||||||
|
: filter.value !== '' && filter.value !== null && filter.value !== undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/lib/format.ts
Normal file
17
src/lib/format.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function formatDate(
|
||||||
|
date: Date | string | number | undefined,
|
||||||
|
opts: Intl.DateTimeFormatOptions = {}
|
||||||
|
) {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
|
month: opts.month ?? 'long',
|
||||||
|
day: opts.day ?? 'numeric',
|
||||||
|
year: opts.year ?? 'numeric',
|
||||||
|
...opts
|
||||||
|
}).format(new Date(date));
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/lib/parsers.ts
Normal file
81
src/lib/parsers.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { createParser } from 'nuqs/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { dataTableConfig } from '@/config/data-table';
|
||||||
|
|
||||||
|
import type { ExtendedColumnFilter, ExtendedColumnSort } from '@/types/data-table';
|
||||||
|
|
||||||
|
const sortingItemSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
desc: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSortingStateParser = <TData>(columnIds?: string[] | Set<string>) => {
|
||||||
|
const validKeys = columnIds ? (columnIds instanceof Set ? columnIds : new Set(columnIds)) : null;
|
||||||
|
|
||||||
|
return createParser({
|
||||||
|
parse: (value) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
const result = z.array(sortingItemSchema).safeParse(parsed);
|
||||||
|
|
||||||
|
if (!result.success) return null;
|
||||||
|
|
||||||
|
if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data as ExtendedColumnSort<TData>[];
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serialize: (value) => JSON.stringify(value),
|
||||||
|
eq: (a, b) =>
|
||||||
|
a.length === b.length &&
|
||||||
|
a.every((item, index) => item.id === b[index]?.id && item.desc === b[index]?.desc)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterItemSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
value: z.union([z.string(), z.array(z.string())]),
|
||||||
|
variant: z.enum(dataTableConfig.filterVariants),
|
||||||
|
operator: z.enum(dataTableConfig.operators),
|
||||||
|
filterId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FilterItemSchema = z.infer<typeof filterItemSchema>;
|
||||||
|
|
||||||
|
export const getFiltersStateParser = <TData>(columnIds?: string[] | Set<string>) => {
|
||||||
|
const validKeys = columnIds ? (columnIds instanceof Set ? columnIds : new Set(columnIds)) : null;
|
||||||
|
|
||||||
|
return createParser({
|
||||||
|
parse: (value) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
const result = z.array(filterItemSchema).safeParse(parsed);
|
||||||
|
|
||||||
|
if (!result.success) return null;
|
||||||
|
|
||||||
|
if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data as ExtendedColumnFilter<TData>[];
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serialize: (value) => JSON.stringify(value),
|
||||||
|
eq: (a, b) =>
|
||||||
|
a.length === b.length &&
|
||||||
|
a.every(
|
||||||
|
(filter, index) =>
|
||||||
|
filter.id === b[index]?.id &&
|
||||||
|
filter.value === b[index]?.value &&
|
||||||
|
filter.variant === b[index]?.variant &&
|
||||||
|
filter.operator === b[index]?.operator
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
||||||
26
src/lib/query-client.ts
Normal file
26
src/lib/query-client.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { QueryClient, defaultShouldDehydrateQuery, isServer } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
function makeQueryClient() {
|
||||||
|
return new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 60 * 1000
|
||||||
|
},
|
||||||
|
dehydrate: {
|
||||||
|
shouldDehydrateQuery: (query) =>
|
||||||
|
defaultShouldDehydrateQuery(query) || query.state.status === 'pending'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let browserQueryClient: QueryClient | undefined = undefined;
|
||||||
|
|
||||||
|
export function getQueryClient() {
|
||||||
|
if (isServer) {
|
||||||
|
return makeQueryClient();
|
||||||
|
} else {
|
||||||
|
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
||||||
|
return browserQueryClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/lib/searchparams.ts
Normal file
22
src/lib/searchparams.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
createSearchParamsCache,
|
||||||
|
createSerializer,
|
||||||
|
parseAsInteger,
|
||||||
|
parseAsString
|
||||||
|
} from 'nuqs/server';
|
||||||
|
|
||||||
|
export const searchParams = {
|
||||||
|
page: parseAsInteger.withDefault(1),
|
||||||
|
perPage: parseAsInteger.withDefault(10),
|
||||||
|
name: parseAsString,
|
||||||
|
gender: parseAsString,
|
||||||
|
category: parseAsString,
|
||||||
|
role: parseAsString,
|
||||||
|
sort: parseAsString
|
||||||
|
// advanced filter
|
||||||
|
// filters: getFiltersStateParser().withDefault([]),
|
||||||
|
// joinOperator: parseAsStringEnum(['and', 'or']).withDefault('and')
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchParamsCache = createSearchParamsCache(searchParams);
|
||||||
|
export const serialize = createSerializer(searchParams);
|
||||||
@@ -1,6 +1,26 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(
|
||||||
|
bytes: number,
|
||||||
|
opts: {
|
||||||
|
decimals?: number;
|
||||||
|
sizeType?: "accurate" | "normal";
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const { decimals = 0, sizeType = "normal" } = opts;
|
||||||
|
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||||
|
const accurateSizes = ["Bytes", "KiB", "MiB", "GiB", "TiB"];
|
||||||
|
if (bytes === 0) return "0 Byte";
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${
|
||||||
|
sizeType === "accurate"
|
||||||
|
? (accurateSizes[i] ?? "Bytest")
|
||||||
|
: (sizes[i] ?? "Bytes")
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/styles/globals.css
Normal file
77
src/styles/globals.css
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@import './theme.css';
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global scrollbar styling to match ScrollArea */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border);
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* View Transition Wave Effect */
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
/* Ensure the outgoing view (old theme) is beneath */
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
/* Ensure the incoming view (new theme) is always on top */
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes reveal {
|
||||||
|
from {
|
||||||
|
/* Use CSS variables for the origin, defaulting to center if not set */
|
||||||
|
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
/* Use CSS variables for the origin, defaulting to center if not set */
|
||||||
|
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
/* Apply the reveal animation */
|
||||||
|
animation: reveal 0.4s ease-in-out forwards;
|
||||||
|
}
|
||||||
33
src/styles/theme.css
Normal file
33
src/styles/theme.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* Import individual theme files */
|
||||||
|
@import './themes/claude.css';
|
||||||
|
@import './themes/neobrutualism.css';
|
||||||
|
@import './themes/supabase.css';
|
||||||
|
@import './themes/vercel.css';
|
||||||
|
@import './themes/mono.css';
|
||||||
|
@import './themes/notebook.css';
|
||||||
|
@import './themes/light-green.css';
|
||||||
|
@import './themes/zen.css';
|
||||||
|
@import './themes/astro-vista.css';
|
||||||
|
@import './themes/whatsapp.css';
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply overscroll-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--header-height: calc(var(--spacing, 0.25rem) * 12 + 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply theme fonts to body for all themes */
|
||||||
|
/* This ensures theme-specific fonts override Next.js font variables */
|
||||||
|
/* Font variables are defined on [data-theme] (html element) */
|
||||||
|
/* We need to override Next.js font variables on body to use html's values */
|
||||||
|
/* Using initial removes the property, allowing parent (html) values to cascade */
|
||||||
|
[data-theme] body {
|
||||||
|
/* Remove Next.js font variables to allow theme variables from html to cascade */
|
||||||
|
--font-sans: initial;
|
||||||
|
--font-mono: initial;
|
||||||
|
--font-serif: initial;
|
||||||
|
/* Apply font-family - will now resolve from html's [data-theme] variables */
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
159
src/styles/themes/astro-vista.css
Normal file
159
src/styles/themes/astro-vista.css
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
[data-theme='astro-vista'] {
|
||||||
|
--background: oklch(0.9383 0.0042 236.4993);
|
||||||
|
--foreground: oklch(0.3211 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.3211 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.3211 0 0);
|
||||||
|
--primary: oklch(0.642 0.1691 38.5815);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.4138 0.0846 259.8759);
|
||||||
|
--secondary-foreground: oklch(1 0 0);
|
||||||
|
--muted: oklch(0.9846 0.0017 247.8389);
|
||||||
|
--muted-foreground: oklch(0.551 0.0234 264.3637);
|
||||||
|
--accent: oklch(0.9119 0.0222 243.8174);
|
||||||
|
--accent-foreground: oklch(0.3791 0.1378 265.5222);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.8452 0 0);
|
||||||
|
--input: oklch(0.97 0.0029 264.542);
|
||||||
|
--ring: oklch(0.6397 0.172 36.4421);
|
||||||
|
--chart-1: oklch(0.6693 0.0706 248.923);
|
||||||
|
--chart-2: oklch(0.6678 0.1546 41.62);
|
||||||
|
--chart-3: oklch(0.5957 0.1807 19.9763);
|
||||||
|
--chart-4: oklch(0.7859 0.1342 83.6986);
|
||||||
|
--chart-5: oklch(0.4227 0.0732 267.3899);
|
||||||
|
--sidebar: oklch(0.903 0.0046 258.3257);
|
||||||
|
--sidebar-foreground: oklch(0.3211 0 0);
|
||||||
|
--sidebar-primary: oklch(0.6397 0.172 36.4421);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9119 0.0222 243.8174);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3791 0.1378 265.5222);
|
||||||
|
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
||||||
|
--sidebar-ring: oklch(0.6397 0.172 36.4421);
|
||||||
|
--font-sans: var(--font-outfit), sans-serif;
|
||||||
|
--font-serif: var(--font-merriweather), serif;
|
||||||
|
--font-mono: var(--font-fira-code), monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: #1a1a1a;
|
||||||
|
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-md: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.25);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='astro-vista'].dark {
|
||||||
|
--background: oklch(0.2178 0 0);
|
||||||
|
--foreground: oklch(0.9219 0 0);
|
||||||
|
--card: oklch(0.2435 0 0);
|
||||||
|
--card-foreground: oklch(0.9219 0 0);
|
||||||
|
--popover: oklch(0.2435 0 0);
|
||||||
|
--popover-foreground: oklch(0.9219 0 0);
|
||||||
|
--primary: oklch(0.642 0.1691 38.5815);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.3743 0.0726 258.5213);
|
||||||
|
--secondary-foreground: oklch(0.9219 0 0);
|
||||||
|
--muted: oklch(0.285 0 0);
|
||||||
|
--muted-foreground: oklch(0.5999 0 0);
|
||||||
|
--accent: oklch(0.338 0.0589 267.5867);
|
||||||
|
--accent-foreground: oklch(0.8823 0.0571 254.1284);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.329 0 0);
|
||||||
|
--input: oklch(0.3092 0 0);
|
||||||
|
--ring: oklch(0.6397 0.172 36.4421);
|
||||||
|
--chart-1: oklch(0.7124 0.0606 248.6896);
|
||||||
|
--chart-2: oklch(0.6678 0.1546 41.62);
|
||||||
|
--chart-3: oklch(0.5957 0.1807 19.9763);
|
||||||
|
--chart-4: oklch(0.7859 0.1342 83.6986);
|
||||||
|
--chart-5: oklch(0.4227 0.0732 267.3899);
|
||||||
|
--sidebar: oklch(0.2393 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.9219 0 0);
|
||||||
|
--sidebar-primary: oklch(0.6397 0.172 36.4421);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.338 0.0589 267.5867);
|
||||||
|
--sidebar-accent-foreground: oklch(0.8823 0.0571 254.1284);
|
||||||
|
--sidebar-border: oklch(0.329 0 0);
|
||||||
|
--sidebar-ring: oklch(0.6397 0.172 36.4421);
|
||||||
|
--font-sans: var(--font-outfit), sans-serif;
|
||||||
|
--font-serif: var(--font-merriweather), serif;
|
||||||
|
--font-mono: var(--font-fira-code), monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: #1a1a1a;
|
||||||
|
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-md: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.1), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.1);
|
||||||
|
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 10.1961% / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='astro-vista'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/styles/themes/claude.css
Normal file
172
src/styles/themes/claude.css
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
[data-theme='claude'] {
|
||||||
|
--background: oklch(0.9818 0.0054 95.0986);
|
||||||
|
--foreground: oklch(0.3438 0.0269 95.7226);
|
||||||
|
--card: oklch(0.9818 0.0054 95.0986);
|
||||||
|
--card-foreground: oklch(0.1908 0.002 106.5859);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.2671 0.0196 98.939);
|
||||||
|
--primary: oklch(0.6171 0.1375 39.0427);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.9245 0.0138 92.9892);
|
||||||
|
--secondary-foreground: oklch(0.4334 0.0177 98.6048);
|
||||||
|
--muted: oklch(0.9341 0.0153 90.239);
|
||||||
|
--muted-foreground: oklch(0.6059 0.0075 97.4233);
|
||||||
|
--accent: oklch(0.9245 0.0138 92.9892);
|
||||||
|
--accent-foreground: oklch(0.2671 0.0196 98.939);
|
||||||
|
--destructive: oklch(0.1908 0.002 106.5859);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.8847 0.0069 97.3627);
|
||||||
|
--input: oklch(0.7621 0.0156 98.3528);
|
||||||
|
--ring: oklch(0.6171 0.1375 39.0427);
|
||||||
|
--chart-1: oklch(0.5583 0.1276 42.9956);
|
||||||
|
--chart-2: oklch(0.6898 0.1581 290.4107);
|
||||||
|
--chart-3: oklch(0.8816 0.0276 93.128);
|
||||||
|
--chart-4: oklch(0.8822 0.0403 298.1792);
|
||||||
|
--chart-5: oklch(0.5608 0.1348 42.0584);
|
||||||
|
--sidebar: oklch(0.9663 0.008 98.8792);
|
||||||
|
--sidebar-foreground: oklch(0.359 0.0051 106.6524);
|
||||||
|
--sidebar-primary: oklch(0.6171 0.1375 39.0427);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9881 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9245 0.0138 92.9892);
|
||||||
|
--sidebar-accent-foreground: oklch(0.325 0 0);
|
||||||
|
--sidebar-border: oklch(0.9401 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7731 0 0);
|
||||||
|
--font-sans:
|
||||||
|
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
|
monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: oklch(0 0 0);
|
||||||
|
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='claude'].dark {
|
||||||
|
--background: oklch(0.2679 0.0036 106.6427);
|
||||||
|
--foreground: oklch(0.8074 0.0142 93.0137);
|
||||||
|
--card: oklch(0.2679 0.0036 106.6427);
|
||||||
|
--card-foreground: oklch(0.9818 0.0054 95.0986);
|
||||||
|
--popover: oklch(0.3085 0.0035 106.6039);
|
||||||
|
--popover-foreground: oklch(0.9211 0.004 106.4781);
|
||||||
|
--primary: oklch(0.6724 0.1308 38.7559);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.9818 0.0054 95.0986);
|
||||||
|
--secondary-foreground: oklch(0.3085 0.0035 106.6039);
|
||||||
|
--muted: oklch(0.2213 0.0038 106.707);
|
||||||
|
--muted-foreground: oklch(0.7713 0.0169 99.0657);
|
||||||
|
--accent: oklch(0.213 0.0078 95.4245);
|
||||||
|
--accent-foreground: oklch(0.9663 0.008 98.8792);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.3618 0.0101 106.8928);
|
||||||
|
--input: oklch(0.4336 0.0113 100.2195);
|
||||||
|
--ring: oklch(0.6724 0.1308 38.7559);
|
||||||
|
--chart-1: oklch(0.5583 0.1276 42.9956);
|
||||||
|
--chart-2: oklch(0.6898 0.1581 290.4107);
|
||||||
|
--chart-3: oklch(0.213 0.0078 95.4245);
|
||||||
|
--chart-4: oklch(0.3074 0.0516 289.323);
|
||||||
|
--chart-5: oklch(0.5608 0.1348 42.0584);
|
||||||
|
--sidebar: oklch(0.2357 0.0024 67.7077);
|
||||||
|
--sidebar-foreground: oklch(0.8074 0.0142 93.0137);
|
||||||
|
--sidebar-primary: oklch(0.325 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9881 0 0);
|
||||||
|
--sidebar-accent: oklch(0.168 0.002 106.6177);
|
||||||
|
--sidebar-accent-foreground: oklch(0.8074 0.0142 93.0137);
|
||||||
|
--sidebar-border: oklch(0.9401 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7731 0 0);
|
||||||
|
--font-sans:
|
||||||
|
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
|
monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: oklch(0 0 0);
|
||||||
|
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='claude'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/styles/themes/light-green.css
Normal file
165
src/styles/themes/light-green.css
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
[data-theme='light-green'] {
|
||||||
|
--background: oklch(0.9892 0.0054 117.9205);
|
||||||
|
--foreground: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--primary: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.3717 0.0392 257.287);
|
||||||
|
--secondary-foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--muted: oklch(0.9683 0.0069 247.8956);
|
||||||
|
--muted-foreground: oklch(0.5544 0.0407 257.4166);
|
||||||
|
--accent: oklch(0.9819 0.0181 155.8263);
|
||||||
|
--accent-foreground: oklch(0.4479 0.1083 151.3277);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--input: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--ring: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--chart-1: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--chart-2: oklch(0.3717 0.0392 257.287);
|
||||||
|
--chart-3: oklch(0.7227 0.192 149.5793);
|
||||||
|
--chart-4: oklch(0.5544 0.0407 257.4166);
|
||||||
|
--chart-5: oklch(0.7107 0.0351 256.7878);
|
||||||
|
--sidebar: oklch(1 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--sidebar-primary: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--sidebar-primary-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--sidebar-border: oklch(0.9683 0.0069 247.8956);
|
||||||
|
--sidebar-ring: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--font-sans: var(--font-inter), system-ui, sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: var(--font-jetbrains-mono), monospace;
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 8px;
|
||||||
|
--shadow-blur: 20px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.05;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 0px 8px 20px 0px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-xs: 0px 8px 20px 0px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-sm: 0px 8px 20px 0px hsl(0 0% 0% / 0.05), 0px 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow: 0px 8px 20px 0px hsl(0 0% 0% / 0.05), 0px 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-md: 0px 8px 20px 0px hsl(0 0% 0% / 0.05), 0px 2px 4px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-lg: 0px 8px 20px 0px hsl(0 0% 0% / 0.05), 0px 4px 6px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xl: 0px 8px 20px 0px hsl(0 0% 0% / 0.05), 0px 8px 10px -1px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-2xl: 0px 8px 20px 0px hsl(0 0% 0% / 0.13);
|
||||||
|
--tracking-normal: -0.01em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='light-green'].dark {
|
||||||
|
--background: oklch(0.1288 0.0406 264.6952);
|
||||||
|
--foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--card: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--card-foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--popover: oklch(0.2077 0.0398 265.7549);
|
||||||
|
--popover-foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--primary: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.2795 0.0368 260.031);
|
||||||
|
--secondary-foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--muted: oklch(0.2795 0.0368 260.031);
|
||||||
|
--muted-foreground: oklch(0.7107 0.0351 256.7878);
|
||||||
|
--accent: oklch(0.3925 0.0896 152.5353);
|
||||||
|
--accent-foreground: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--destructive: oklch(0.4437 0.1613 26.8994);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.2795 0.0368 260.031);
|
||||||
|
--input: oklch(0.2795 0.0368 260.031);
|
||||||
|
--ring: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--chart-1: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--chart-2: oklch(0.6231 0.188 259.8145);
|
||||||
|
--chart-3: oklch(0.7227 0.192 149.5793);
|
||||||
|
--chart-4: oklch(0.6268 0.2325 303.9004);
|
||||||
|
--chart-5: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--sidebar: oklch(0.1288 0.0406 264.6952);
|
||||||
|
--sidebar-foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--sidebar-primary: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--sidebar-primary-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-accent: oklch(0.2795 0.0368 260.031);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9842 0.0034 247.8575);
|
||||||
|
--sidebar-border: oklch(0.2795 0.0368 260.031);
|
||||||
|
--sidebar-ring: oklch(0.8871 0.2122 128.5041);
|
||||||
|
--font-sans: var(--font-inter), system-ui, sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: var(--font-jetbrains-mono), monospace;
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 10px;
|
||||||
|
--shadow-blur: 25px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.4;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 0px 10px 25px 0px hsl(0 0% 0% / 0.2);
|
||||||
|
--shadow-xs: 0px 10px 25px 0px hsl(0 0% 0% / 0.2);
|
||||||
|
--shadow-sm: 0px 10px 25px 0px hsl(0 0% 0% / 0.4), 0px 1px 2px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow: 0px 10px 25px 0px hsl(0 0% 0% / 0.4), 0px 1px 2px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-md: 0px 10px 25px 0px hsl(0 0% 0% / 0.4), 0px 2px 4px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-lg: 0px 10px 25px 0px hsl(0 0% 0% / 0.4), 0px 4px 6px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-xl: 0px 10px 25px 0px hsl(0 0% 0% / 0.4), 0px 8px 10px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-2xl: 0px 10px 25px 0px hsl(0 0% 0% / 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='light-green'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||||
|
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||||
|
--tracking-normal: var(--tracking-normal);
|
||||||
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/styles/themes/mono.css
Normal file
162
src/styles/themes/mono.css
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
[data-theme='mono'] {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.1448 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.1448 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.1448 0 0);
|
||||||
|
--primary: oklch(0.5555 0 0);
|
||||||
|
--primary-foreground: oklch(0.9851 0 0);
|
||||||
|
--secondary: oklch(0.9702 0 0);
|
||||||
|
--secondary-foreground: oklch(0.2046 0 0);
|
||||||
|
--muted: oklch(0.9702 0 0);
|
||||||
|
--muted-foreground: oklch(0.5486 0 0);
|
||||||
|
--accent: oklch(0.9702 0 0);
|
||||||
|
--accent-foreground: oklch(0.2046 0 0);
|
||||||
|
--destructive: oklch(0.583 0.2387 28.4765);
|
||||||
|
--destructive-foreground: oklch(0.9702 0 0);
|
||||||
|
--border: oklch(0.9219 0 0);
|
||||||
|
--input: oklch(0.9219 0 0);
|
||||||
|
--ring: oklch(0.709 0 0);
|
||||||
|
--chart-1: oklch(0.5555 0 0);
|
||||||
|
--chart-2: oklch(0.5555 0 0);
|
||||||
|
--chart-3: oklch(0.5555 0 0);
|
||||||
|
--chart-4: oklch(0.5555 0 0);
|
||||||
|
--chart-5: oklch(0.5555 0 0);
|
||||||
|
--sidebar: oklch(0.9851 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.1448 0 0);
|
||||||
|
--sidebar-primary: oklch(0.2046 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9702 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||||
|
--sidebar-border: oklch(0.9219 0 0);
|
||||||
|
--sidebar-ring: oklch(0.709 0 0);
|
||||||
|
--font-sans: Geist Mono, monospace;
|
||||||
|
--font-serif: Geist Mono, monospace;
|
||||||
|
--font-mono: Geist Mono, monospace;
|
||||||
|
--radius: 0rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='mono'].dark {
|
||||||
|
--background: oklch(0.1448 0 0);
|
||||||
|
--foreground: oklch(0.9851 0 0);
|
||||||
|
--card: oklch(0.2134 0 0);
|
||||||
|
--card-foreground: oklch(0.9851 0 0);
|
||||||
|
--popover: oklch(0.2686 0 0);
|
||||||
|
--popover-foreground: oklch(0.9851 0 0);
|
||||||
|
--primary: oklch(0.5555 0 0);
|
||||||
|
--primary-foreground: oklch(0.9851 0 0);
|
||||||
|
--secondary: oklch(0.2686 0 0);
|
||||||
|
--secondary-foreground: oklch(0.9851 0 0);
|
||||||
|
--muted: oklch(0.2686 0 0);
|
||||||
|
--muted-foreground: oklch(0.709 0 0);
|
||||||
|
--accent: oklch(0.3715 0 0);
|
||||||
|
--accent-foreground: oklch(0.9851 0 0);
|
||||||
|
--destructive: oklch(0.7022 0.1892 22.2279);
|
||||||
|
--destructive-foreground: oklch(0.2686 0 0);
|
||||||
|
--border: oklch(0.3407 0 0);
|
||||||
|
--input: oklch(0.4386 0 0);
|
||||||
|
--ring: oklch(0.5555 0 0);
|
||||||
|
--chart-1: oklch(0.5555 0 0);
|
||||||
|
--chart-2: oklch(0.5555 0 0);
|
||||||
|
--chart-3: oklch(0.5555 0 0);
|
||||||
|
--chart-4: oklch(0.5555 0 0);
|
||||||
|
--chart-5: oklch(0.5555 0 0);
|
||||||
|
--sidebar: oklch(0.2046 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.9851 0 0);
|
||||||
|
--sidebar-primary: oklch(0.9851 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2046 0 0);
|
||||||
|
--sidebar-accent: oklch(0.2686 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0);
|
||||||
|
--sidebar-ring: oklch(0.4386 0 0);
|
||||||
|
--font-sans: Geist Mono, monospace;
|
||||||
|
--font-serif: Geist Mono, monospace;
|
||||||
|
--font-mono: Geist Mono, monospace;
|
||||||
|
--radius: 0rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
|
||||||
|
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='mono'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/styles/themes/neobrutualism.css
Normal file
162
src/styles/themes/neobrutualism.css
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
[data-theme='neobrutualism'] {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0 0 0);
|
||||||
|
--primary: oklch(0.6489 0.237 26.9728);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.968 0.211 109.7692);
|
||||||
|
--secondary-foreground: oklch(0 0 0);
|
||||||
|
--muted: oklch(0.9551 0 0);
|
||||||
|
--muted-foreground: oklch(0.3211 0 0);
|
||||||
|
--accent: oklch(0.5635 0.2408 260.8178);
|
||||||
|
--accent-foreground: oklch(1 0 0);
|
||||||
|
--destructive: oklch(0 0 0);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0 0 0);
|
||||||
|
--input: oklch(0 0 0);
|
||||||
|
--ring: oklch(0.6489 0.237 26.9728);
|
||||||
|
--chart-1: oklch(0.6489 0.237 26.9728);
|
||||||
|
--chart-2: oklch(0.968 0.211 109.7692);
|
||||||
|
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||||
|
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||||
|
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||||
|
--sidebar: oklch(0.9551 0 0);
|
||||||
|
--sidebar-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-primary: oklch(0.6489 0.237 26.9728);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||||
|
--sidebar-accent-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-border: oklch(0 0 0);
|
||||||
|
--sidebar-ring: oklch(0.6489 0.237 26.9728);
|
||||||
|
--font-sans: DM Sans, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: Space Mono, monospace;
|
||||||
|
--radius: 0px;
|
||||||
|
--shadow-x: 4px;
|
||||||
|
--shadow-y: 4px;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 1;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.5);
|
||||||
|
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.5);
|
||||||
|
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 1px 2px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 1px 2px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 2px 4px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 4px 6px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 8px 10px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.5);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='neobrutualism'].dark {
|
||||||
|
--background: oklch(0 0 0);
|
||||||
|
--foreground: oklch(1 0 0);
|
||||||
|
--card: oklch(0.3211 0 0);
|
||||||
|
--card-foreground: oklch(1 0 0);
|
||||||
|
--popover: oklch(0.3211 0 0);
|
||||||
|
--popover-foreground: oklch(1 0 0);
|
||||||
|
--primary: oklch(0.7044 0.1872 23.1858);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.9691 0.2005 109.6228);
|
||||||
|
--secondary-foreground: oklch(0 0 0);
|
||||||
|
--muted: oklch(0.2178 0 0);
|
||||||
|
--muted-foreground: oklch(0.8452 0 0);
|
||||||
|
--accent: oklch(0.6755 0.1765 252.2592);
|
||||||
|
--accent-foreground: oklch(0 0 0);
|
||||||
|
--destructive: oklch(1 0 0);
|
||||||
|
--destructive-foreground: oklch(0 0 0);
|
||||||
|
--border: oklch(1 0 0);
|
||||||
|
--input: oklch(1 0 0);
|
||||||
|
--ring: oklch(0.7044 0.1872 23.1858);
|
||||||
|
--chart-1: oklch(0.7044 0.1872 23.1858);
|
||||||
|
--chart-2: oklch(0.9691 0.2005 109.6228);
|
||||||
|
--chart-3: oklch(0.6755 0.1765 252.2592);
|
||||||
|
--chart-4: oklch(0.7395 0.2268 142.8504);
|
||||||
|
--chart-5: oklch(0.6131 0.2458 328.0714);
|
||||||
|
--sidebar: oklch(0 0 0);
|
||||||
|
--sidebar-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-primary: oklch(0.7044 0.1872 23.1858);
|
||||||
|
--sidebar-primary-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-accent: oklch(0.6755 0.1765 252.2592);
|
||||||
|
--sidebar-accent-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7044 0.1872 23.1858);
|
||||||
|
--font-sans: DM Sans, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: Space Mono, monospace;
|
||||||
|
--radius: 0px;
|
||||||
|
--shadow-x: 4px;
|
||||||
|
--shadow-y: 4px;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 1;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.5);
|
||||||
|
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.5);
|
||||||
|
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 1px 2px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 1px 2px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 2px 4px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 4px 6px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1), 4px 8px 10px -1px hsl(0 0% 0% / 1);
|
||||||
|
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='neobrutualism'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/styles/themes/notebook.css
Normal file
173
src/styles/themes/notebook.css
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
[data-theme='notebook'] {
|
||||||
|
--background: oklch(0.9821 0 0);
|
||||||
|
--foreground: oklch(0.3485 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.3485 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.3485 0 0);
|
||||||
|
--primary: oklch(0.4891 0 0);
|
||||||
|
--primary-foreground: oklch(0.9551 0 0);
|
||||||
|
--secondary: oklch(0.9006 0 0);
|
||||||
|
--secondary-foreground: oklch(0.3485 0 0);
|
||||||
|
--muted: oklch(0.9158 0 0);
|
||||||
|
--muted-foreground: oklch(0.4313 0 0);
|
||||||
|
--accent: oklch(0.9354 0.0456 94.8549);
|
||||||
|
--accent-foreground: oklch(0.4015 0.0436 37.9587);
|
||||||
|
--destructive: oklch(0.6627 0.0978 20.0041);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.5538 0.0025 17.232);
|
||||||
|
--input: oklch(1 0 0);
|
||||||
|
--ring: oklch(0.7058 0 0);
|
||||||
|
--chart-1: oklch(0.3211 0 0);
|
||||||
|
--chart-2: oklch(0.4495 0 0);
|
||||||
|
--chart-3: oklch(0.5693 0 0);
|
||||||
|
--chart-4: oklch(0.683 0 0);
|
||||||
|
--chart-5: oklch(0.7921 0 0);
|
||||||
|
--sidebar: oklch(0.9551 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.3485 0 0);
|
||||||
|
--sidebar-primary: oklch(0.4891 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9551 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9354 0.0456 94.8549);
|
||||||
|
--sidebar-accent-foreground: oklch(0.4015 0.0436 37.9587);
|
||||||
|
--sidebar-border: oklch(0.8078 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7058 0 0);
|
||||||
|
--font-sans: 'Architects Daughter', sans-serif;
|
||||||
|
--font-serif: 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: 'Courier New', Courier, monospace;
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--shadow-x: 1px;
|
||||||
|
--shadow-y: 4px;
|
||||||
|
--shadow-blur: 5px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.03;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 1px 4px 5px 0px hsl(0 0% 0% / 0.01);
|
||||||
|
--shadow-xs: 1px 4px 5px 0px hsl(0 0% 0% / 0.01);
|
||||||
|
--shadow-sm: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 1px 2px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 1px 2px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-md: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 2px 4px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-lg: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 4px 6px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-xl: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 8px 10px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-2xl: 1px 4px 5px 0px hsl(0 0% 0% / 0.07);
|
||||||
|
--tracking-normal: 0.5px;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='notebook'].dark {
|
||||||
|
--background: oklch(0.2891 0 0);
|
||||||
|
--foreground: oklch(0.8945 0 0);
|
||||||
|
--card: oklch(0.3211 0 0);
|
||||||
|
--card-foreground: oklch(0.8945 0 0);
|
||||||
|
--popover: oklch(0.3211 0 0);
|
||||||
|
--popover-foreground: oklch(0.8945 0 0);
|
||||||
|
--primary: oklch(0.7572 0 0);
|
||||||
|
--primary-foreground: oklch(0.2891 0 0);
|
||||||
|
--secondary: oklch(0.4676 0 0);
|
||||||
|
--secondary-foreground: oklch(0.8078 0 0);
|
||||||
|
--muted: oklch(0.3904 0 0);
|
||||||
|
--muted-foreground: oklch(0.7058 0 0);
|
||||||
|
--accent: oklch(0.9067 0 0);
|
||||||
|
--accent-foreground: oklch(0.3211 0 0);
|
||||||
|
--destructive: oklch(0.7915 0.0491 18.241);
|
||||||
|
--destructive-foreground: oklch(0.2891 0 0);
|
||||||
|
--border: oklch(0.4276 0 0);
|
||||||
|
--input: oklch(0.3211 0 0);
|
||||||
|
--ring: oklch(0.8078 0 0);
|
||||||
|
--chart-1: oklch(0.9521 0 0);
|
||||||
|
--chart-2: oklch(0.8576 0 0);
|
||||||
|
--chart-3: oklch(0.7572 0 0);
|
||||||
|
--chart-4: oklch(0.6534 0 0);
|
||||||
|
--chart-5: oklch(0.5452 0 0);
|
||||||
|
--sidebar: oklch(0.2478 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.8945 0 0);
|
||||||
|
--sidebar-primary: oklch(0.7572 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2478 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9067 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3211 0 0);
|
||||||
|
--sidebar-border: oklch(0.4276 0 0);
|
||||||
|
--sidebar-ring: oklch(0.8078 0 0);
|
||||||
|
--font-sans: 'Architects Daughter', sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: 'Fira Code', 'Courier New', monospace;
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--shadow-x: 1px;
|
||||||
|
--shadow-y: 4px;
|
||||||
|
--shadow-blur: 5px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.03;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 1px 4px 5px 0px hsl(0 0% 0% / 0.01);
|
||||||
|
--shadow-xs: 1px 4px 5px 0px hsl(0 0% 0% / 0.01);
|
||||||
|
--shadow-sm: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 1px 2px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 1px 2px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-md: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 2px 4px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-lg: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 4px 6px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-xl: 1px 4px 5px 0px hsl(0 0% 0% / 0.03), 1px 8px 10px -1px hsl(0 0% 0% / 0.03);
|
||||||
|
--shadow-2xl: 1px 4px 5px 0px hsl(0 0% 0% / 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='notebook'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
|
||||||
|
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||||
|
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||||
|
--tracking-normal: var(--tracking-normal);
|
||||||
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='notebook'] body {
|
||||||
|
letter-spacing: var(--tracking-normal);
|
||||||
|
}
|
||||||
167
src/styles/themes/supabase.css
Normal file
167
src/styles/themes/supabase.css
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
[data-theme='supabase'] {
|
||||||
|
--background: oklch(0.9911 0 0);
|
||||||
|
--foreground: oklch(0.2046 0 0);
|
||||||
|
--card: oklch(0.9911 0 0);
|
||||||
|
--card-foreground: oklch(0.2046 0 0);
|
||||||
|
--popover: oklch(0.9911 0 0);
|
||||||
|
--popover-foreground: oklch(0.4386 0 0);
|
||||||
|
--primary: oklch(0.8348 0.1302 160.908);
|
||||||
|
--primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||||
|
--secondary: oklch(0.994 0 0);
|
||||||
|
--secondary-foreground: oklch(0.2046 0 0);
|
||||||
|
--muted: oklch(0.9461 0 0);
|
||||||
|
--muted-foreground: oklch(0.2435 0 0);
|
||||||
|
--accent: oklch(0.9461 0 0);
|
||||||
|
--accent-foreground: oklch(0.2435 0 0);
|
||||||
|
--destructive: oklch(0.5523 0.1927 32.7272);
|
||||||
|
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
||||||
|
--border: oklch(0.9037 0 0);
|
||||||
|
--input: oklch(0.9731 0 0);
|
||||||
|
--ring: oklch(0.8348 0.1302 160.908);
|
||||||
|
--chart-1: oklch(0.8348 0.1302 160.908);
|
||||||
|
--chart-2: oklch(0.6231 0.188 259.8145);
|
||||||
|
--chart-3: oklch(0.6056 0.2189 292.7172);
|
||||||
|
--chart-4: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--chart-5: oklch(0.6959 0.1491 162.4796);
|
||||||
|
--sidebar: oklch(0.9911 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.5452 0 0);
|
||||||
|
--sidebar-primary: oklch(0.8348 0.1302 160.908);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||||
|
--sidebar-accent: oklch(0.9461 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
||||||
|
--sidebar-border: oklch(0.9037 0 0);
|
||||||
|
--sidebar-ring: oklch(0.8348 0.1302 160.908);
|
||||||
|
--font-sans: Outfit, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.17;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
||||||
|
--tracking-normal: 0.025em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='supabase'].dark {
|
||||||
|
--background: oklch(0.1822 0 0);
|
||||||
|
--foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--card: oklch(0.2046 0 0);
|
||||||
|
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--popover: oklch(0.2603 0 0);
|
||||||
|
--popover-foreground: oklch(0.7348 0 0);
|
||||||
|
--primary: oklch(0.4365 0.1044 156.7556);
|
||||||
|
--primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||||
|
--secondary: oklch(0.2603 0 0);
|
||||||
|
--secondary-foreground: oklch(0.9851 0 0);
|
||||||
|
--muted: oklch(0.2393 0 0);
|
||||||
|
--muted-foreground: oklch(0.7122 0 0);
|
||||||
|
--accent: oklch(0.3132 0 0);
|
||||||
|
--accent-foreground: oklch(0.9851 0 0);
|
||||||
|
--destructive: oklch(0.3123 0.0852 29.7877);
|
||||||
|
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
||||||
|
--border: oklch(0.2809 0 0);
|
||||||
|
--input: oklch(0.2603 0 0);
|
||||||
|
--ring: oklch(0.8003 0.1821 151.711);
|
||||||
|
--chart-1: oklch(0.8003 0.1821 151.711);
|
||||||
|
--chart-2: oklch(0.7137 0.1434 254.624);
|
||||||
|
--chart-3: oklch(0.709 0.1592 293.5412);
|
||||||
|
--chart-4: oklch(0.8369 0.1644 84.4286);
|
||||||
|
--chart-5: oklch(0.7845 0.1325 181.912);
|
||||||
|
--sidebar: oklch(0.1822 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.6301 0 0);
|
||||||
|
--sidebar-primary: oklch(0.4365 0.1044 156.7556);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||||
|
--sidebar-accent: oklch(0.3132 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||||
|
--sidebar-border: oklch(0.2809 0 0);
|
||||||
|
--sidebar-ring: oklch(0.8003 0.1821 151.711);
|
||||||
|
--font-sans: Outfit, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.17;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
||||||
|
--tracking-normal: 0.025em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='supabase'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||||
|
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||||
|
--tracking-normal: var(--tracking-normal);
|
||||||
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/styles/themes/vercel.css
Normal file
159
src/styles/themes/vercel.css
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
[data-theme='vercel'] {
|
||||||
|
--background: oklch(0.99 0 0);
|
||||||
|
--foreground: oklch(0 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0 0 0);
|
||||||
|
--popover: oklch(0.99 0 0);
|
||||||
|
--popover-foreground: oklch(0 0 0);
|
||||||
|
--primary: oklch(0 0 0);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.94 0 0);
|
||||||
|
--secondary-foreground: oklch(0 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.44 0 0);
|
||||||
|
--accent: oklch(0.94 0 0);
|
||||||
|
--accent-foreground: oklch(0 0 0);
|
||||||
|
--destructive: oklch(0.63 0.19 23.03);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.92 0 0);
|
||||||
|
--input: oklch(0.94 0 0);
|
||||||
|
--ring: oklch(0 0 0);
|
||||||
|
--chart-1: oklch(0.81 0.17 75.35);
|
||||||
|
--chart-2: oklch(0.55 0.22 264.53);
|
||||||
|
--chart-3: oklch(0.72 0 0);
|
||||||
|
--chart-4: oklch(0.92 0 0);
|
||||||
|
--chart-5: oklch(0.56 0 0);
|
||||||
|
--sidebar: oklch(0.99 0 0);
|
||||||
|
--sidebar-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-primary: oklch(0 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.94 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-border: oklch(0.94 0 0);
|
||||||
|
--sidebar-ring: oklch(0 0 0);
|
||||||
|
--font-sans: Geist, sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: Geist Mono, monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 2px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.18;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='vercel'].dark {
|
||||||
|
--background: oklch(0 0 0);
|
||||||
|
--foreground: oklch(1 0 0);
|
||||||
|
--card: oklch(0.14 0 0);
|
||||||
|
--card-foreground: oklch(1 0 0);
|
||||||
|
--popover: oklch(0.18 0 0);
|
||||||
|
--popover-foreground: oklch(1 0 0);
|
||||||
|
--primary: oklch(1 0 0);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.25 0 0);
|
||||||
|
--secondary-foreground: oklch(1 0 0);
|
||||||
|
--muted: oklch(0.23 0 0);
|
||||||
|
--muted-foreground: oklch(0.72 0 0);
|
||||||
|
--accent: oklch(0.32 0 0);
|
||||||
|
--accent-foreground: oklch(1 0 0);
|
||||||
|
--destructive: oklch(0.69 0.2 23.91);
|
||||||
|
--destructive-foreground: oklch(0 0 0);
|
||||||
|
--border: oklch(0.26 0 0);
|
||||||
|
--input: oklch(0.32 0 0);
|
||||||
|
--ring: oklch(0.72 0 0);
|
||||||
|
--chart-1: oklch(0.81 0.17 75.35);
|
||||||
|
--chart-2: oklch(0.58 0.21 260.84);
|
||||||
|
--chart-3: oklch(0.56 0 0);
|
||||||
|
--chart-4: oklch(0.44 0 0);
|
||||||
|
--chart-5: oklch(0.92 0 0);
|
||||||
|
--sidebar: oklch(0.18 0 0);
|
||||||
|
--sidebar-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-primary: oklch(1 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-accent: oklch(0.32 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-border: oklch(0.32 0 0);
|
||||||
|
--sidebar-ring: oklch(0.72 0 0);
|
||||||
|
--font-sans: Geist, sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: Geist Mono, monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 2px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.18;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='vercel'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/styles/themes/whatsapp.css
Normal file
163
src/styles/themes/whatsapp.css
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
[data-theme='whatsapp'] {
|
||||||
|
--background: oklch(0.9605 0.0046 258.3248);
|
||||||
|
--foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--primary: oklch(0.4335 0.0754 182.2315);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.9644 0.0208 166.1014);
|
||||||
|
--secondary-foreground: oklch(0.4335 0.0754 182.2315);
|
||||||
|
--muted: oklch(0.9605 0.0046 258.3248);
|
||||||
|
--muted-foreground: oklch(0.5589 0.0255 233.7233);
|
||||||
|
--accent: oklch(0.761 0.2015 149.7403);
|
||||||
|
--accent-foreground: oklch(1 0 0);
|
||||||
|
--destructive: oklch(0.6257 0.2058 29.0773);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--input: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--ring: oklch(0.761 0.2015 149.7403);
|
||||||
|
--chart-1: oklch(0.761 0.2015 149.7403);
|
||||||
|
--chart-2: oklch(0.4335 0.0754 182.2315);
|
||||||
|
--chart-3: oklch(0.5762 0.0995 182.3964);
|
||||||
|
--chart-4: oklch(0.7356 0.137 232.8053);
|
||||||
|
--chart-5: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--sidebar: oklch(1 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--sidebar-primary: oklch(0.4335 0.0754 182.2315);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9644 0.0208 166.1014);
|
||||||
|
--sidebar-accent-foreground: oklch(0.4335 0.0754 182.2315);
|
||||||
|
--sidebar-border: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--sidebar-ring: oklch(0.761 0.2015 149.7403);
|
||||||
|
--font-sans:
|
||||||
|
'Segoe UI', 'Helvetica Neue', Helvetica, 'Lucida Grande', Arial, Ubuntu, Cantarell, 'Fira Sans',
|
||||||
|
sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 2px;
|
||||||
|
--shadow-blur: 10px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-2xs: 0px 2px 10px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0px 2px 10px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0px 2px 10px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow: 0px 2px 10px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-md: 0px 2px 10px 0px hsl(0 0% 0% / 0.1), 0px 2px 4px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-lg: 0px 2px 10px 0px hsl(0 0% 0% / 0.1), 0px 4px 6px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-xl: 0px 2px 10px 0px hsl(0 0% 0% / 0.1), 0px 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-2xl: 0px 2px 10px 0px hsl(0 0% 0% / 0.25);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='whatsapp'].dark {
|
||||||
|
--background: oklch(0.1854 0.0182 238.2143);
|
||||||
|
--foreground: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--card: oklch(0.2848 0.023 235.6578);
|
||||||
|
--card-foreground: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--popover: oklch(0.2848 0.023 235.6578);
|
||||||
|
--popover-foreground: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--primary: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--primary-foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--secondary: oklch(0.2933 0.0423 172.8195);
|
||||||
|
--secondary-foreground: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--muted: oklch(0.2456 0.0195 239.1061);
|
||||||
|
--muted-foreground: oklch(0.6637 0.0236 235.1968);
|
||||||
|
--accent: oklch(0.761 0.2015 149.7403);
|
||||||
|
--accent-foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--destructive: oklch(0.6257 0.2058 29.0773);
|
||||||
|
--destructive-foreground: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--border: oklch(0.3351 0.0253 234.8586);
|
||||||
|
--input: oklch(0.3351 0.0253 234.8586);
|
||||||
|
--ring: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--chart-1: oklch(0.761 0.2015 149.7403);
|
||||||
|
--chart-2: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--chart-3: oklch(0.5762 0.0995 182.3964);
|
||||||
|
--chart-4: oklch(0.7356 0.137 232.8053);
|
||||||
|
--chart-5: oklch(0.4335 0.0754 182.2315);
|
||||||
|
--sidebar: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--sidebar-foreground: oklch(0.9436 0.0051 228.8204);
|
||||||
|
--sidebar-primary: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2153 0.0187 235.1251);
|
||||||
|
--sidebar-accent: oklch(0.2933 0.0423 172.8195);
|
||||||
|
--sidebar-accent-foreground: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--sidebar-border: oklch(0.3351 0.0253 234.8586);
|
||||||
|
--sidebar-ring: oklch(0.6509 0.1283 170.4258);
|
||||||
|
--font-sans:
|
||||||
|
'Segoe UI', 'Helvetica Neue', Helvetica, 'Lucida Grande', Arial, Ubuntu, Cantarell, 'Fira Sans',
|
||||||
|
sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 4px;
|
||||||
|
--shadow-blur: 12px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.4;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-2xs: 0px 4px 12px 0px hsl(0 0% 0% / 0.2);
|
||||||
|
--shadow-xs: 0px 4px 12px 0px hsl(0 0% 0% / 0.2);
|
||||||
|
--shadow-sm: 0px 4px 12px 0px hsl(0 0% 0% / 0.4), 0px 1px 2px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow: 0px 4px 12px 0px hsl(0 0% 0% / 0.4), 0px 1px 2px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-md: 0px 4px 12px 0px hsl(0 0% 0% / 0.4), 0px 2px 4px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-lg: 0px 4px 12px 0px hsl(0 0% 0% / 0.4), 0px 4px 6px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-xl: 0px 4px 12px 0px hsl(0 0% 0% / 0.4), 0px 8px 10px -1px hsl(0 0% 0% / 0.4);
|
||||||
|
--shadow-2xl: 0px 4px 12px 0px hsl(0 0% 0% / 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='whatsapp'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/styles/themes/zen.css
Normal file
165
src/styles/themes/zen.css
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
[data-theme='zen'] {
|
||||||
|
--background: oklch(0.9195 0.0169 88.003);
|
||||||
|
--foreground: oklch(0.235 0 0);
|
||||||
|
--card: oklch(0.953 0.0156 86.4257);
|
||||||
|
--card-foreground: oklch(0.235 0 0);
|
||||||
|
--popover: oklch(0.953 0.0156 86.4257);
|
||||||
|
--popover-foreground: oklch(0.235 0 0);
|
||||||
|
--primary: oklch(0.3012 0 0);
|
||||||
|
--primary-foreground: oklch(0.9169 0.0175 99.616);
|
||||||
|
--secondary: oklch(0.8647 0.0201 87.5232);
|
||||||
|
--secondary-foreground: oklch(0.3012 0 0);
|
||||||
|
--muted: oklch(0.834 0.0232 87.163);
|
||||||
|
--muted-foreground: oklch(0.4688 0.0136 84.5932);
|
||||||
|
--accent: oklch(0.9169 0.0175 99.616);
|
||||||
|
--accent-foreground: oklch(0.3012 0 0);
|
||||||
|
--destructive: oklch(0.5771 0.2152 27.325);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.8434 0.0231 87.1621);
|
||||||
|
--input: oklch(0.8434 0.0231 87.1621);
|
||||||
|
--ring: oklch(0.3012 0 0);
|
||||||
|
--chart-1: oklch(0.6863 0.1743 34.2614);
|
||||||
|
--chart-2: oklch(0.235 0 0);
|
||||||
|
--chart-3: oklch(0.4688 0.0136 84.5932);
|
||||||
|
--chart-4: oklch(0.7057 0.025 82.0932);
|
||||||
|
--chart-5: oklch(0.834 0.0232 87.163);
|
||||||
|
--sidebar: oklch(0.8985 0.0199 87.5195);
|
||||||
|
--sidebar-foreground: oklch(0.235 0 0);
|
||||||
|
--sidebar-primary: oklch(0.3012 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9169 0.0175 99.616);
|
||||||
|
--sidebar-accent: oklch(0.9169 0.0175 99.616);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3012 0 0);
|
||||||
|
--sidebar-border: oklch(0.8434 0.0231 87.1621);
|
||||||
|
--sidebar-ring: oklch(0.3012 0 0);
|
||||||
|
--font-sans: var(--font-inter), sans-serif;
|
||||||
|
--font-serif: var(--font-playfair-display), serif;
|
||||||
|
--font-mono: var(--font-jetbrains-mono), monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 4px;
|
||||||
|
--shadow-blur: 10px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 0px 4px 10px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0px 4px 10px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0px 4px 10px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow: 0px 4px 10px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-md: 0px 4px 10px 0px hsl(0 0% 0% / 0.1), 0px 2px 4px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-lg: 0px 4px 10px 0px hsl(0 0% 0% / 0.1), 0px 4px 6px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-xl: 0px 4px 10px 0px hsl(0 0% 0% / 0.1), 0px 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||||
|
--shadow-2xl: 0px 4px 10px 0px hsl(0 0% 0% / 0.25);
|
||||||
|
--tracking-normal: 0.01em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='zen'].dark {
|
||||||
|
--background: oklch(0.1913 0 0);
|
||||||
|
--foreground: oklch(0.9173 0.0133 82.4015);
|
||||||
|
--card: oklch(0.2264 0 0);
|
||||||
|
--card-foreground: oklch(0.9173 0.0133 82.4015);
|
||||||
|
--popover: oklch(0.2264 0 0);
|
||||||
|
--popover-foreground: oklch(0.9173 0.0133 82.4015);
|
||||||
|
--primary: oklch(0.852 0.0205 100.6306);
|
||||||
|
--primary-foreground: oklch(0.3329 0 0);
|
||||||
|
--secondary: oklch(0.252 0 0);
|
||||||
|
--secondary-foreground: oklch(0.852 0.0205 100.6306);
|
||||||
|
--muted: oklch(0.285 0 0);
|
||||||
|
--muted-foreground: oklch(0.6348 0.0113 81.7875);
|
||||||
|
--accent: oklch(0.3329 0 0);
|
||||||
|
--accent-foreground: oklch(0.852 0.0205 100.6306);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1 0 0);
|
||||||
|
--border: oklch(0.2931 0 0);
|
||||||
|
--input: oklch(0.2931 0 0);
|
||||||
|
--ring: oklch(0.852 0.0205 100.6306);
|
||||||
|
--chart-1: oklch(0.6863 0.1743 34.2614);
|
||||||
|
--chart-2: oklch(0.859 0.0209 74.6369);
|
||||||
|
--chart-3: oklch(0.6348 0.0113 81.7875);
|
||||||
|
--chart-4: oklch(0.4681 0.0069 84.5829);
|
||||||
|
--chart-5: oklch(0.3523 0 0);
|
||||||
|
--sidebar: oklch(0.173 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.9173 0.0133 82.4015);
|
||||||
|
--sidebar-primary: oklch(0.852 0.0205 100.6306);
|
||||||
|
--sidebar-primary-foreground: oklch(0.3329 0 0);
|
||||||
|
--sidebar-accent: oklch(0.3329 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.852 0.0205 100.6306);
|
||||||
|
--sidebar-border: oklch(0.2931 0 0);
|
||||||
|
--sidebar-ring: oklch(0.852 0.0205 100.6306);
|
||||||
|
--font-sans: var(--font-inter), sans-serif;
|
||||||
|
--font-serif: var(--font-playfair-display), serif;
|
||||||
|
--font-mono: var(--font-jetbrains-mono), monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 6px;
|
||||||
|
--shadow-blur: 15px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.3;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--shadow-2xs: 0px 6px 15px 0px hsl(0 0% 0% / 0.15);
|
||||||
|
--shadow-xs: 0px 6px 15px 0px hsl(0 0% 0% / 0.15);
|
||||||
|
--shadow-sm: 0px 6px 15px 0px hsl(0 0% 0% / 0.3), 0px 1px 2px -1px hsl(0 0% 0% / 0.3);
|
||||||
|
--shadow: 0px 6px 15px 0px hsl(0 0% 0% / 0.3), 0px 1px 2px -1px hsl(0 0% 0% / 0.3);
|
||||||
|
--shadow-md: 0px 6px 15px 0px hsl(0 0% 0% / 0.3), 0px 2px 4px -1px hsl(0 0% 0% / 0.3);
|
||||||
|
--shadow-lg: 0px 6px 15px 0px hsl(0 0% 0% / 0.3), 0px 4px 6px -1px hsl(0 0% 0% / 0.3);
|
||||||
|
--shadow-xl: 0px 6px 15px 0px hsl(0 0% 0% / 0.3), 0px 8px 10px -1px hsl(0 0% 0% / 0.3);
|
||||||
|
--shadow-2xl: 0px 6px 15px 0px hsl(0 0% 0% / 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='zen'] {
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||||
|
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||||
|
--tracking-normal: var(--tracking-normal);
|
||||||
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/types/data-table.ts
Normal file
40
src/types/data-table.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { DataTableConfig } from '@/config/data-table';
|
||||||
|
import type { FilterItemSchema } from '@/lib/parsers';
|
||||||
|
import type { ColumnSort, Row, RowData } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
declare module '@tanstack/react-table' {
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: Interface type parameters required by @tanstack/react-table
|
||||||
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
variant?: FilterVariant;
|
||||||
|
options?: Option[];
|
||||||
|
range?: [number, number];
|
||||||
|
unit?: string;
|
||||||
|
icon?: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
count?: number;
|
||||||
|
icon?: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterOperator = DataTableConfig['operators'][number];
|
||||||
|
export type FilterVariant = DataTableConfig['filterVariants'][number];
|
||||||
|
export type JoinOperator = DataTableConfig['joinOperators'][number];
|
||||||
|
|
||||||
|
export interface ExtendedColumnSort<TData> extends Omit<ColumnSort, 'id'> {
|
||||||
|
id: Extract<keyof TData, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtendedColumnFilter<TData> extends FilterItemSchema {
|
||||||
|
id: Extract<keyof TData, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableRowAction<TData> {
|
||||||
|
row: Row<TData>;
|
||||||
|
variant: 'update' | 'delete';
|
||||||
|
}
|
||||||
49
src/types/index.ts
Normal file
49
src/types/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Icons } from '@/components/icons';
|
||||||
|
|
||||||
|
export interface PermissionCheck {
|
||||||
|
permission?: string;
|
||||||
|
plan?: string;
|
||||||
|
feature?: string;
|
||||||
|
role?: string;
|
||||||
|
requireOrg?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
external?: boolean;
|
||||||
|
shortcut?: [string, string];
|
||||||
|
icon?: keyof typeof Icons;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
items?: NavItem[];
|
||||||
|
access?: PermissionCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavGroup {
|
||||||
|
label: string;
|
||||||
|
items: NavItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavItemWithChildren extends NavItem {
|
||||||
|
items: NavItemWithChildren[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavItemWithOptionalChildren extends NavItem {
|
||||||
|
items?: NavItemWithChildren[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FooterItem {
|
||||||
|
title: string;
|
||||||
|
items: {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
external?: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MainNavItem = NavItemWithOptionalChildren;
|
||||||
|
|
||||||
|
export type SidebarNavItem = NavItemWithChildren;
|
||||||
Reference in New Issue
Block a user