From 4702150af139b97150b35f636966f7caf9e37c25 Mon Sep 17 00:00:00 2001 From: phaichayon Date: Thu, 16 Apr 2026 14:06:59 +0700 Subject: [PATCH] commit --- src/app/dashboard/react-query/page.tsx | 34 +++ src/app/dashboard/users/page.tsx | 31 ++ src/features/react-query-demo/api/queries.ts | 24 ++ .../components/pokemon-info.tsx | 99 ++++++ .../components/pokemon-skeleton.tsx | 52 ++++ src/features/react-query-demo/info-content.ts | 46 +++ src/features/users/api/mutations.ts | 27 ++ src/features/users/api/queries.ts | 17 ++ src/features/users/api/service.ts | 47 +++ src/features/users/api/types.ts | 28 ++ .../users/components/user-form-sheet.tsx | 189 ++++++++++++ .../users/components/user-listing.tsx | 31 ++ .../components/users-table/cell-action.tsx | 66 ++++ .../users/components/users-table/columns.tsx | 72 +++++ .../users/components/users-table/index.tsx | 61 ++++ .../users/components/users-table/options.tsx | 8 + src/features/users/info-content.ts | 41 +++ src/features/users/schemas/user.ts | 12 + src/hooks/use-breadcrumbs.tsx | 46 +++ src/hooks/use-callback-ref.tsx | 22 ++ src/hooks/use-controllable-state.tsx | 65 ++++ src/hooks/use-data-table.ts | 284 ++++++++++++++++++ src/hooks/use-debounce.tsx | 19 ++ src/hooks/use-debounced-callback.ts | 22 ++ src/hooks/use-media-query.ts | 19 ++ src/hooks/use-mobile.tsx | 19 ++ src/hooks/use-nav.ts | 180 +++++++++++ src/hooks/use-stepper.tsx | 100 ++++++ src/lib/api-client.ts | 14 + src/lib/compose-refs.ts | 63 ++++ src/lib/data-table.ts | 62 ++++ src/lib/format.ts | 17 ++ src/lib/parsers.ts | 81 +++++ src/lib/query-client.ts | 26 ++ src/lib/searchparams.ts | 22 ++ src/lib/utils.ts | 26 +- src/styles/globals.css | 77 +++++ src/styles/theme.css | 33 ++ src/styles/themes/astro-vista.css | 159 ++++++++++ src/styles/themes/claude.css | 172 +++++++++++ src/styles/themes/light-green.css | 165 ++++++++++ src/styles/themes/mono.css | 162 ++++++++++ src/styles/themes/neobrutualism.css | 162 ++++++++++ src/styles/themes/notebook.css | 173 +++++++++++ src/styles/themes/supabase.css | 167 ++++++++++ src/styles/themes/vercel.css | 159 ++++++++++ src/styles/themes/whatsapp.css | 163 ++++++++++ src/styles/themes/zen.css | 165 ++++++++++ src/types/data-table.ts | 40 +++ src/types/index.ts | 49 +++ 50 files changed, 3815 insertions(+), 3 deletions(-) create mode 100644 src/app/dashboard/react-query/page.tsx create mode 100644 src/app/dashboard/users/page.tsx create mode 100644 src/features/react-query-demo/api/queries.ts create mode 100644 src/features/react-query-demo/components/pokemon-info.tsx create mode 100644 src/features/react-query-demo/components/pokemon-skeleton.tsx create mode 100644 src/features/react-query-demo/info-content.ts create mode 100644 src/features/users/api/mutations.ts create mode 100644 src/features/users/api/queries.ts create mode 100644 src/features/users/api/service.ts create mode 100644 src/features/users/api/types.ts create mode 100644 src/features/users/components/user-form-sheet.tsx create mode 100644 src/features/users/components/user-listing.tsx create mode 100644 src/features/users/components/users-table/cell-action.tsx create mode 100644 src/features/users/components/users-table/columns.tsx create mode 100644 src/features/users/components/users-table/index.tsx create mode 100644 src/features/users/components/users-table/options.tsx create mode 100644 src/features/users/info-content.ts create mode 100644 src/features/users/schemas/user.ts create mode 100644 src/hooks/use-breadcrumbs.tsx create mode 100644 src/hooks/use-callback-ref.tsx create mode 100644 src/hooks/use-controllable-state.tsx create mode 100644 src/hooks/use-data-table.ts create mode 100644 src/hooks/use-debounce.tsx create mode 100644 src/hooks/use-debounced-callback.ts create mode 100644 src/hooks/use-media-query.ts create mode 100644 src/hooks/use-mobile.tsx create mode 100644 src/hooks/use-nav.ts create mode 100644 src/hooks/use-stepper.tsx create mode 100644 src/lib/api-client.ts create mode 100644 src/lib/compose-refs.ts create mode 100644 src/lib/data-table.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/parsers.ts create mode 100644 src/lib/query-client.ts create mode 100644 src/lib/searchparams.ts create mode 100644 src/styles/globals.css create mode 100644 src/styles/theme.css create mode 100644 src/styles/themes/astro-vista.css create mode 100644 src/styles/themes/claude.css create mode 100644 src/styles/themes/light-green.css create mode 100644 src/styles/themes/mono.css create mode 100644 src/styles/themes/neobrutualism.css create mode 100644 src/styles/themes/notebook.css create mode 100644 src/styles/themes/supabase.css create mode 100644 src/styles/themes/vercel.css create mode 100644 src/styles/themes/whatsapp.css create mode 100644 src/styles/themes/zen.css create mode 100644 src/types/data-table.ts create mode 100644 src/types/index.ts diff --git a/src/app/dashboard/react-query/page.tsx b/src/app/dashboard/react-query/page.tsx new file mode 100644 index 0000000..cfabdf2 --- /dev/null +++ b/src/app/dashboard/react-query/page.tsx @@ -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 ( + + + }> + + + + + ); +} diff --git a/src/app/dashboard/users/page.tsx b/src/app/dashboard/users/page.tsx new file mode 100644 index 0000000..4653f85 --- /dev/null +++ b/src/app/dashboard/users/page.tsx @@ -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; +}; + +export default async function UsersPage(props: PageProps) { + const searchParams = await props.searchParams; + searchParamsCache.parse(searchParams); + + return ( + } + > + + + ); +} diff --git a/src/features/react-query-demo/api/queries.ts b/src/features/react-query-demo/api/queries.ts new file mode 100644 index 0000000..525a62a --- /dev/null +++ b/src/features/react-query-demo/api/queries.ts @@ -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 => { + const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`); + if (!response.ok) throw new Error('Failed to fetch pokemon'); + return response.json(); + } + }); diff --git a/src/features/react-query-demo/components/pokemon-info.tsx b/src/features/react-query-demo/components/pokemon-info.tsx new file mode 100644 index 0000000..99ca042 --- /dev/null +++ b/src/features/react-query-demo/components/pokemon-info.tsx @@ -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 ( +
+ {/* Pokemon selector */} + + + Pick a Pokemon + + Each selection triggers useSuspenseQuery — cached results are instant, new + fetches show the Suspense fallback. + + + +
+ {POKEMON_IDS.map((id) => ( + + ))} +
+
+
+ + {/* Pokemon card */} + + +
+ {data.name} +
+ {data.types.map(({ type }) => ( + + {type.name} + + ))} +
+
+ + Height: {data.height / 10}m · Weight: {data.weight / 10}kg + +
+ +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {data.name} +
+ {data.stats.map((s) => ( +
+
+ {s.stat.name} + {s.base_stat} +
+ +
+ ))} +
+
+
+ +

+ Data from PokeAPI · Prefetched on server, hydrated on client +

+
+
+
+ ); +} diff --git a/src/features/react-query-demo/components/pokemon-skeleton.tsx b/src/features/react-query-demo/components/pokemon-skeleton.tsx new file mode 100644 index 0000000..b26f091 --- /dev/null +++ b/src/features/react-query-demo/components/pokemon-skeleton.tsx @@ -0,0 +1,52 @@ +import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card'; + +export function PokemonSkeleton() { + return ( +
+ {/* Selector card skeleton */} + + +
+
+ + +
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ ))} +
+ + + + {/* Pokemon card skeleton */} + + +
+
+
+
+
+ + +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+
+ + +
+ + +
+ ); +} diff --git a/src/features/react-query-demo/info-content.ts b/src/features/react-query-demo/info-content.ts new file mode 100644 index 0000000..005f1fc --- /dev/null +++ b/src/features/react-query-demo/info-content.ts @@ -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' + } + ] + } + ] +}; diff --git a/src/features/users/api/mutations.ts b/src/features/users/api/mutations.ts new file mode 100644 index 0000000..3ea82ac --- /dev/null +++ b/src/features/users/api/mutations.ts @@ -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 }); + } +}); diff --git a/src/features/users/api/queries.ts b/src/features/users/api/queries.ts new file mode 100644 index 0000000..e76c2da --- /dev/null +++ b/src/features/users/api/queries.ts @@ -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) + }); diff --git a/src/features/users/api/service.ts b/src/features/users/api/service.ts new file mode 100644 index 0000000..c938e18 --- /dev/null +++ b/src/features/users/api/service.ts @@ -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('/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('/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 { + 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); +} diff --git a/src/features/users/api/types.ts b/src/features/users/api/types.ts new file mode 100644 index 0000000..c101112 --- /dev/null +++ b/src/features/users/api/types.ts @@ -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; +}; diff --git a/src/features/users/components/user-form-sheet.tsx b/src/features/users/components/user-form-sheet.tsx new file mode 100644 index 0000000..4c1096b --- /dev/null +++ b/src/features/users/components/user-form-sheet.tsx @@ -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(); + + const isPending = createMutation.isPending || updateMutation.isPending; + + return ( + + + + {isEdit ? 'Edit User' : 'New User'} + + {isEdit + ? 'Update the user details below.' + : 'Fill in the details to create a new user.'} + + + +
+ + +
+ + +
+ + + + + + + + +
+
+
+ + + + + +
+
+ ); +} + +export function UserFormSheetTrigger() { + const [open, setOpen] = useState(false); + + return ( + <> + + + + ); +} diff --git a/src/features/users/components/user-listing.tsx b/src/features/users/components/user-listing.tsx new file mode 100644 index 0000000..5a5254d --- /dev/null +++ b/src/features/users/components/user-listing.tsx @@ -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 ( + + + + ); +} diff --git a/src/features/users/components/users-table/cell-action.tsx b/src/features/users/components/users-table/cell-action.tsx new file mode 100644 index 0000000..154872b --- /dev/null +++ b/src/features/users/components/users-table/cell-action.tsx @@ -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 ( + <> + setDeleteOpen(false)} + onConfirm={() => deleteMutation.mutate(data.id)} + loading={deleteMutation.isPending} + /> + + + + + + + Actions + setEditOpen(true)}> + Update + + setDeleteOpen(true)}> + Delete + + + + + ); +} diff --git a/src/features/users/components/users-table/columns.tsx b/src/features/users/components/users-table/columns.tsx new file mode 100644 index 0000000..299b537 --- /dev/null +++ b/src/features/users/components/users-table/columns.tsx @@ -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[] = [ + { + id: 'name', + accessorFn: (row) => `${row.first_name} ${row.last_name}`, + header: ({ column }: { column: Column }) => ( + + ), + cell: ({ row }) => ( +
+ + {row.original.first_name} {row.original.last_name} + + {row.original.email} +
+ ), + 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 }) => ( + + ), + cell: ({ cell }) => { + return ( + + {cell.getValue()} + + ); + }, + enableColumnFilter: true, + meta: { + label: 'roles', + variant: 'multiSelect' as const, + options: ROLE_OPTIONS + } + }, + { + accessorKey: 'status', + header: 'STATUS', + cell: ({ cell }) => { + const status = cell.getValue(); + const variant = + status === 'Active' ? 'default' : status === 'Inactive' ? 'secondary' : 'outline'; + return {status}; + } + }, + { + id: 'actions', + cell: ({ row }) => + } +]; diff --git a/src/features/users/components/users-table/index.tsx b/src/features/users/components/users-table/index.tsx new file mode 100644 index 0000000..b38f19a --- /dev/null +++ b/src/features/users/components/users-table/index.tsx @@ -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 ( + + + + ); +} + +export function UsersTableSkeleton() { + return ( +
+
+
+
+
+ ); +} diff --git a/src/features/users/components/users-table/options.tsx b/src/features/users/components/users-table/options.tsx new file mode 100644 index 0000000..f539062 --- /dev/null +++ b/src/features/users/components/users-table/options.tsx @@ -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' } +]; diff --git a/src/features/users/info-content.ts b/src/features/users/info-content.ts new file mode 100644 index 0000000..5df1253 --- /dev/null +++ b/src/features/users/info-content.ts @@ -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: [] + } + ] +}; diff --git a/src/features/users/schemas/user.ts b/src/features/users/schemas/user.ts new file mode 100644 index 0000000..1086f5f --- /dev/null +++ b/src/features/users/schemas/user.ts @@ -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; diff --git a/src/hooks/use-breadcrumbs.tsx b/src/hooks/use-breadcrumbs.tsx new file mode 100644 index 0000000..5e97cd5 --- /dev/null +++ b/src/hooks/use-breadcrumbs.tsx @@ -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 = { + '/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; +} diff --git a/src/hooks/use-callback-ref.tsx b/src/hooks/use-callback-ref.tsx new file mode 100644 index 0000000..e0102d9 --- /dev/null +++ b/src/hooks/use-callback-ref.tsx @@ -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 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 }; diff --git a/src/hooks/use-controllable-state.tsx b/src/hooks/use-controllable-state.tsx new file mode 100644 index 0000000..92cabf6 --- /dev/null +++ b/src/hooks/use-controllable-state.tsx @@ -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 = { + prop?: T | undefined; + defaultProp?: T | undefined; + onChange?: (state: T) => void; +}; + +type SetStateFn = (prevState?: T) => T; + +function useControllableState({ + prop, + defaultProp, + onChange = () => {} +}: UseControllableStateParams) { + const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({ + defaultProp, + onChange + }); + const isControlled = prop !== undefined; + const value = isControlled ? prop : uncontrolledProp; + const handleChange = useCallbackRef(onChange); + + const setValue: React.Dispatch> = React.useCallback( + (nextValue) => { + if (isControlled) { + const setter = nextValue as SetStateFn; + 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({ + defaultProp, + onChange +}: Omit, 'prop'>) { + const uncontrolledState = React.useState(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 }; diff --git a/src/hooks/use-data-table.ts b/src/hooks/use-data-table.ts new file mode 100644 index 0000000..4d98250 --- /dev/null +++ b/src/hooks/use-data-table.ts @@ -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 + extends + Omit< + TableOptions, + | 'state' + | 'pageCount' + | 'getCoreRowModel' + | 'manualFiltering' + | 'manualPagination' + | 'manualSorting' + >, + Required, 'pageCount'>> { + initialState?: Omit, 'sorting'> & { + sorting?: ExtendedColumnSort[]; + }; + history?: 'push' | 'replace'; + debounceMs?: number; + throttleMs?: number; + clearOnDefault?: boolean; + enableAdvancedFilter?: boolean; + scroll?: boolean; + shallow?: boolean; + startTransition?: React.TransitionStartFunction; +} + +export function useDataTable(props: UseDataTableProps) { + 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, 'parse'>>( + () => ({ + history, + scroll, + shallow, + throttleMs, + debounceMs, + clearOnDefault, + startTransition + }), + [history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition] + ); + + const [rowSelection, setRowSelection] = React.useState( + initialState?.rowSelection ?? {} + ); + const [columnVisibility, setColumnVisibility] = React.useState( + initialState?.columnVisibility ?? {} + ); + const [columnPinning, setColumnPinning] = React.useState( + 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) => { + 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(columnIds) + .withOptions(queryStateOptions) + .withDefault(initialState?.sorting ?? []) + ); + + const onSortingChange = React.useCallback( + (updaterOrValue: Updater) => { + if (typeof updaterOrValue === 'function') { + const newSorting = updaterOrValue(sorting); + setSorting(newSorting as ExtendedColumnSort[]); + } else { + setSorting(updaterOrValue as ExtendedColumnSort[]); + } + }, + [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 | Parser>>( + (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((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(initialColumnFilters); + + const onColumnFiltersChange = React.useCallback( + (updaterOrValue: Updater) => { + if (enableAdvancedFilter) return; + + setColumnFilters((prev) => { + const next = typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue; + + const filterUpdates = next.reduce>( + (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 }; +} diff --git a/src/hooks/use-debounce.tsx b/src/hooks/use-debounce.tsx new file mode 100644 index 0000000..2f87ae3 --- /dev/null +++ b/src/hooks/use-debounce.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/src/hooks/use-debounced-callback.ts b/src/hooks/use-debounced-callback.ts new file mode 100644 index 0000000..a22d174 --- /dev/null +++ b/src/hooks/use-debounced-callback.ts @@ -0,0 +1,22 @@ +import * as React from 'react'; + +import { useCallbackRef } from '@/hooks/use-callback-ref'; + +export function useDebouncedCallback 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) => { + window.clearTimeout(debounceTimerRef.current); + debounceTimerRef.current = window.setTimeout(() => handleCallback(...args), delay); + }, + [handleCallback, delay] + ); + + return setValue; +} diff --git a/src/hooks/use-media-query.ts b/src/hooks/use-media-query.ts new file mode 100644 index 0000000..50182c3 --- /dev/null +++ b/src/hooks/use-media-query.ts @@ -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 }; +} diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx new file mode 100644 index 0000000..ba553c6 --- /dev/null +++ b/src/hooks/use-mobile.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(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; +} diff --git a/src/hooks/use-nav.ts b/src/hooks/use-nav.ts new file mode 100644 index 0000000..96b4626 --- /dev/null +++ b/src/hooks/use-nav.ts @@ -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]); +} diff --git a/src/hooks/use-stepper.tsx b/src/hooks/use-stepper.tsx new file mode 100644 index 0000000..81d41b6 --- /dev/null +++ b/src/hooks/use-stepper.tsx @@ -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 + }; +} diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts new file mode 100644 index 0000000..1dc16f4 --- /dev/null +++ b/src/lib/api-client.ts @@ -0,0 +1,14 @@ +const BASE_URL = '/api'; + +export async function apiClient(endpoint: string, options?: RequestInit): Promise { + 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; +} diff --git a/src/lib/compose-refs.ts b/src/lib/compose-refs.ts new file mode 100644 index 0000000..a0d684b --- /dev/null +++ b/src/lib/compose-refs.ts @@ -0,0 +1,63 @@ +import * as React from 'react'; + +type PossibleRef = React.Ref | 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(ref: PossibleRef, 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(...refs: PossibleRef[]): React.RefCallback { + 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(...refs: PossibleRef[]): React.RefCallback { + // 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 }; diff --git a/src/lib/data-table.ts b/src/lib/data-table.ts new file mode 100644 index 0000000..28d545b --- /dev/null +++ b/src/lib/data-table.ts @@ -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({ + column +}: { + column: Column; +}): 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 = { + 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( + filters: ExtendedColumnFilter[] +): ExtendedColumnFilter[] { + 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) + ); +} diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..52f1651 --- /dev/null +++ b/src/lib/format.ts @@ -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 ''; + } +} diff --git a/src/lib/parsers.ts b/src/lib/parsers.ts new file mode 100644 index 0000000..15914da --- /dev/null +++ b/src/lib/parsers.ts @@ -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 = (columnIds?: string[] | Set) => { + 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[]; + } 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; + +export const getFiltersStateParser = (columnIds?: string[] | Set) => { + 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[]; + } 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 + ) + }); +}; diff --git a/src/lib/query-client.ts b/src/lib/query-client.ts new file mode 100644 index 0000000..c7cda18 --- /dev/null +++ b/src/lib/query-client.ts @@ -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; + } +} diff --git a/src/lib/searchparams.ts b/src/lib/searchparams.ts new file mode 100644 index 0000000..5b55582 --- /dev/null +++ b/src/lib/searchparams.ts @@ -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); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..b31a797 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,26 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; 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") + }`; } diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..443a990 --- /dev/null +++ b/src/styles/globals.css @@ -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; +} diff --git a/src/styles/theme.css b/src/styles/theme.css new file mode 100644 index 0000000..1bad05f --- /dev/null +++ b/src/styles/theme.css @@ -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); +} diff --git a/src/styles/themes/astro-vista.css b/src/styles/themes/astro-vista.css new file mode 100644 index 0000000..4070a4a --- /dev/null +++ b/src/styles/themes/astro-vista.css @@ -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); + } +} diff --git a/src/styles/themes/claude.css b/src/styles/themes/claude.css new file mode 100644 index 0000000..fe6fb2a --- /dev/null +++ b/src/styles/themes/claude.css @@ -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); + } +} diff --git a/src/styles/themes/light-green.css b/src/styles/themes/light-green.css new file mode 100644 index 0000000..a781bb8 --- /dev/null +++ b/src/styles/themes/light-green.css @@ -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); + } +} diff --git a/src/styles/themes/mono.css b/src/styles/themes/mono.css new file mode 100644 index 0000000..ff8c385 --- /dev/null +++ b/src/styles/themes/mono.css @@ -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); + } +} diff --git a/src/styles/themes/neobrutualism.css b/src/styles/themes/neobrutualism.css new file mode 100644 index 0000000..fd6e11b --- /dev/null +++ b/src/styles/themes/neobrutualism.css @@ -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); + } +} diff --git a/src/styles/themes/notebook.css b/src/styles/themes/notebook.css new file mode 100644 index 0000000..7108936 --- /dev/null +++ b/src/styles/themes/notebook.css @@ -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); +} diff --git a/src/styles/themes/supabase.css b/src/styles/themes/supabase.css new file mode 100644 index 0000000..41ba827 --- /dev/null +++ b/src/styles/themes/supabase.css @@ -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); + } +} diff --git a/src/styles/themes/vercel.css b/src/styles/themes/vercel.css new file mode 100644 index 0000000..261fd5d --- /dev/null +++ b/src/styles/themes/vercel.css @@ -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); + } +} diff --git a/src/styles/themes/whatsapp.css b/src/styles/themes/whatsapp.css new file mode 100644 index 0000000..a8d448b --- /dev/null +++ b/src/styles/themes/whatsapp.css @@ -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); + } +} diff --git a/src/styles/themes/zen.css b/src/styles/themes/zen.css new file mode 100644 index 0000000..de925fa --- /dev/null +++ b/src/styles/themes/zen.css @@ -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); + } +} diff --git a/src/types/data-table.ts b/src/types/data-table.ts new file mode 100644 index 0000000..a035fef --- /dev/null +++ b/src/types/data-table.ts @@ -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 { + label?: string; + placeholder?: string; + variant?: FilterVariant; + options?: Option[]; + range?: [number, number]; + unit?: string; + icon?: React.FC>; + } +} + +export interface Option { + label: string; + value: string; + count?: number; + icon?: React.FC>; +} + +export type FilterOperator = DataTableConfig['operators'][number]; +export type FilterVariant = DataTableConfig['filterVariants'][number]; +export type JoinOperator = DataTableConfig['joinOperators'][number]; + +export interface ExtendedColumnSort extends Omit { + id: Extract; +} + +export interface ExtendedColumnFilter extends FilterItemSchema { + id: Extract; +} + +export interface DataTableRowAction { + row: Row; + variant: 'update' | 'delete'; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..fc1b3da --- /dev/null +++ b/src/types/index.ts @@ -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;