This commit is contained in:
giancarlo
2024-03-24 02:23:22 +08:00
parent 648d77b430
commit bce3479368
589 changed files with 37067 additions and 9596 deletions

View File

@@ -1,5 +1,5 @@
{
"name": "@acme/ui",
"name": "@kit/ui",
"private": true,
"version": "0.1.0",
"scripts": {
@@ -20,27 +20,29 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-tooltip": "1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-navigation-menu": "^1.1.4",
"class-variance-authority": "^0.7.0",
"react-top-loading-bar": "2.3.1",
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"lucide-react": "0.307.0",
"sonner": "^1.4.41",
"tailwind-merge": "^2.2.0",
"zod": "^3.22.4"
},
"peerDependencies": {
"@tanstack/react-table": "^8.10.7",
"react": "^18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
"tailwindcss": "3.4.1",
"tailwindcss-animate": "^1.0.7"
"react-i18next": "^14.1.0"
},
"devDependencies": {
"@acme/eslint-config": "0.2.0",
"@acme/prettier-config": "0.1.0",
"@acme/tailwind-config": "0.1.0",
"@acme/tsconfig": "0.1.0",
"@kit/eslint-config": "0.2.0",
"@kit/prettier-config": "0.1.0",
"@kit/tailwind-config": "0.1.0",
"@kit/tsconfig": "0.1.0",
"@tanstack/react-table": "^8.11.3",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
@@ -58,34 +60,53 @@
"eslintConfig": {
"root": true,
"extends": [
"@acme/eslint-config/base",
"@acme/eslint-config/react"
"@kit/eslint-config/base",
"@kit/eslint-config/react"
]
},
"prettier": "@acme/prettier-config",
"prettier": "@kit/prettier-config",
"exports": {
".": "./src/index.ts",
"./avatar": "./src/avatar.tsx",
"./button": "./src/button.tsx",
"./calendar": "./src/calendar.tsx",
"./card": "./src/card.tsx",
"./checkbox": "./src/checkbox.tsx",
"./command": "./src/command.tsx",
"./data-table": "./src/data-table.tsx",
"./dialog": "./src/dialog.tsx",
"./dropdown-menu": "./src/dropdown-menu.tsx",
"./form": "./src/form.tsx",
"./icons": "./src/icons.tsx",
"./input": "./src/input.tsx",
"./label": "./src/label.tsx",
"./popover": "./src/popover.tsx",
"./scroll-area": "./src/scroll-area.tsx",
"./select": "./src/select.tsx",
"./sheet": "./src/sheet.tsx",
"./table": "./src/table.tsx",
"./tabs": "./src/tabs.tsx",
"./toaster": "./src/toaster.tsx",
"./use-toast": "./src/use-toast.tsx"
"./avatar": "./src/shadcn/avatar.tsx",
"./button": "./src/shadcn/button.tsx",
"./calendar": "./src/shadcn/calendar.tsx",
"./card": "./src/shadcn/card.tsx",
"./checkbox": "./src/shadcn/checkbox.tsx",
"./command": "./src/shadcn/command.tsx",
"./data-table": "./src/shadcn/data-table.tsx",
"./dialog": "./src/shadcn/dialog.tsx",
"./dropdown-menu": "./src/shadcn/dropdown-menu.tsx",
"./navigation-menu": "./src/shadcn/navigation-menu.tsx",
"./form": "./src/shadcn/form.tsx",
"./icons": "./src/shadcn/icons.tsx",
"./input": "./src/shadcn/input.tsx",
"./label": "./src/shadcn/label.tsx",
"./popover": "./src/shadcn/popover.tsx",
"./scroll-area": "./src/shadcn/scroll-area.tsx",
"./select": "./src/shadcn/select.tsx",
"./sheet": "./src/shadcn/sheet.tsx",
"./table": "./src/shadcn/table.tsx",
"./tabs": "./src/shadcn/tabs.tsx",
"./tooltip": "./src/shadcn/tooltip.tsx",
"./sonner": "./src/shadcn/sonner.tsx",
"./heading": "./src/shadcn/heading.tsx",
"./alert": "./src/shadcn/alert.tsx",
"./badge": "./src/shadcn/badge.tsx",
"./utils": "./src/utils/index.ts",
"./if": "./src/makerkit/if.tsx",
"./trans": "./src/makerkit/trans.tsx",
"./divider": "./src/makerkit/divider.tsx",
"./sidebar": "./src/makerkit/sidebar.tsx",
"./sidebar-schema": "./src/makerkit/sidebar-schema.ts",
"./spinner": "./src/makerkit/spinner.tsx",
"./page": "./src/makerkit/page.tsx",
"./image-uploader": "./src/makerkit/image-uploader.tsx",
"./global-loader": "./src/makerkit/global-loader.tsx",
"./error-boundary": "./src/makerkit/error-boundary.tsx",
"./auth-change-listener": "./src/makerkit/auth-change-listener.tsx",
"./otp-input": "./src/makerkit/otp-input.tsx",
"./loading-overlay": "./src/makerkit/loading-overlay.tsx",
"./profile-avatar": "./src/makerkit/profile-avatar.tsx",
"./mdx": "./src/makerkit/mdx/mdx-renderer.tsx"
},
"typesVersions": {
"*": {

View File

@@ -1,57 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "./utils/cn";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -1,69 +0,0 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { buttonVariants } from "./button";
import { cn } from "./utils/cn";
export type { DateRange } from "react-day-picker";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
),
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => (
<ChevronLeft className="h-4 w-4" {...props} />
),
IconRight: ({ ...props }) => (
<ChevronRight className="h-4 w-4" {...props} />
),
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };

View File

@@ -1,30 +0,0 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "./utils/cn";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -1 +0,0 @@
export { cn } from "./utils/cn";

View File

@@ -1,24 +0,0 @@
import * as React from "react";
import { cn } from "./utils/cn";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,11 @@
import { createContext } from 'react';
const SidebarContext = createContext<{
collapsed: boolean;
setCollapsed: (collapsed: boolean) => void;
}>({
collapsed: false,
setCollapsed: (_) => _,
});
export { SidebarContext };

View File

@@ -0,0 +1,264 @@
'use client';
import { Fragment, useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import type {
ColumnDef,
ColumnFiltersState,
PaginationState,
Table as ReactTable,
Row,
SortingState,
VisibilityState,
} from '@tanstack/react-table';
import classNames from 'clsx';
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
} from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import Trans from './trans';
interface ReactTableProps<T extends object> {
data: T[];
columns: ColumnDef<T>[];
renderSubComponent?: (props: { row: Row<T> }) => React.ReactElement;
pageIndex?: number;
pageSize?: number;
pageCount?: number;
onPaginationChange?: (pagination: PaginationState) => void;
tableProps?: React.ComponentProps<typeof Table> &
Record<`data-${string}`, string>;
}
export function DataTable<T extends object>({
data,
columns,
renderSubComponent,
pageIndex,
pageSize,
pageCount,
onPaginationChange,
tableProps,
}: ReactTableProps<T>) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: pageIndex ?? 0,
pageSize: pageSize ?? 15,
});
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const navigateToPage = useNavigateToNewPage();
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
pageCount,
state: {
pagination,
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
onPaginationChange: (updater) => {
const navigate = (page: number) => setTimeout(() => navigateToPage(page));
if (typeof updater === 'function') {
setPagination((prevState) => {
const nextState = updater(prevState);
if (onPaginationChange) {
onPaginationChange(nextState);
} else {
navigate(nextState.pageIndex);
}
return nextState;
});
} else {
setPagination(updater);
if (onPaginationChange) {
onPaginationChange(updater);
} else {
navigate(updater.pageIndex);
}
}
},
});
return (
<div
className={'dark:border-dark-800 rounded-md border border-gray-50 p-1'}
>
<Table {...tableProps}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
colSpan={header.colSpan}
style={{
width: header.column.getSize(),
}}
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<Fragment key={row.id}>
<TableRow
className={classNames({
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted':
row.getIsExpanded(),
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
style={{
width: cell.column.getSize(),
}}
key={cell.id}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
{renderSubComponent ? (
<TableRow key={row.id + '-expanded'}>
<TableCell colSpan={columns.length}>
{renderSubComponent({ row })}
</TableCell>
</TableRow>
) : null}
</Fragment>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={columns.length}>
<Pagination table={table} />
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
);
}
function Pagination<T>({
table,
}: React.PropsWithChildren<{
table: ReactTable<T>;
}>) {
return (
<div className="flex w-full items-center gap-2">
<Button
size={'icon'}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronsLeftIcon className={'h-4'} />
</Button>
<Button
size={'icon'}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeftIcon className={'h-4'} />
</Button>
<Button
size={'icon'}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ChevronRightIcon className={'h-4'} />
</Button>
<Button
size={'icon'}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<ChevronsRightIcon className={'h-4'} />
</Button>
<span className="flex items-center text-sm">
<Trans
i18nKey={'common:pageOfPages'}
values={{
page: table.getState().pagination.pageIndex + 1,
total: table.getPageCount(),
}}
/>
</span>
</div>
);
}
/**
* Navigates to a new page using the provided page index and optional page parameter.
*/
function useNavigateToNewPage(
props: { pageParam?: string } = {
pageParam: 'page',
},
) {
const router = useRouter();
const param = props.pageParam ?? 'page';
return useCallback(
(pageIndex: number) => {
const url = new URL(window.location.href);
url.searchParams.set(param, String(pageIndex + 1));
router.push(url.pathname + url.search);
},
[param, router],
);
}

View File

@@ -0,0 +1,5 @@
import { cn } from '../utils/cn';
export function Divider(props: { className?: string }) {
return <div className={cn('h-[1px] w-full bg-border', props.className)} />;
}

View File

@@ -0,0 +1,24 @@
import type { ReactNode } from 'react';
import { Component } from 'react';
export class ErrorBoundary extends Component<{
fallback: ReactNode;
children: ReactNode;
}> {
readonly state = { hasError: false, error: null };
static getDerivedStateFromError(error: unknown) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}

View File

@@ -0,0 +1,26 @@
import { LoadingOverlay } from './loading-overlay';
import { TopLoadingBarIndicator } from './top-loading-bar-indicator';
import { Trans } from './trans';
export function GlobalLoader({
children,
displayLogo = false,
fullPage = false,
}: React.PropsWithChildren<{
displayLogo?: boolean;
fullPage?: boolean;
}>) {
const Text = children ?? <Trans i18nKey={'common:loading'} />;
return (
<>
<TopLoadingBarIndicator />
<div className={'flex flex-1 flex-col items-center justify-center py-48'}>
<LoadingOverlay displayLogo={displayLogo} fullPage={fullPage}>
{Text}
</LoadingOverlay>
</div>
</>
);
}

View File

@@ -0,0 +1,26 @@
import { useCallback, useState } from 'react';
const SIDEBAR_COLLAPSED_STORAGE_KEY = 'sidebarState';
function useCollapsible(initialValue?: boolean) {
const [isCollapsed, setIsCollapsed] = useState(initialValue);
const onCollapseChange = useCallback((collapsed: boolean) => {
setIsCollapsed(collapsed);
storeCollapsibleState(collapsed);
}, []);
return [isCollapsed, onCollapseChange] as [boolean, typeof onCollapseChange];
}
function storeCollapsibleState(collapsed: boolean) {
// TODO: implement below
/*
setCookie(
SIDEBAR_COLLAPSED_STORAGE_KEY,
collapsed ? 'collapsed' : 'expanded',
);
*/
}
export default useCollapsible;

View File

@@ -0,0 +1,29 @@
import { useMemo } from 'react';
type Condition<Value = unknown> = Value | false | null | undefined | 0 | '';
export function If<Value = unknown>({
condition,
children,
fallback,
}: React.PropsWithoutRef<{
condition: Condition<Value>;
children: React.ReactNode | ((value: Value) => React.ReactNode);
fallback?: React.ReactNode;
}>) {
return useMemo(() => {
if (condition) {
if (typeof children === 'function') {
return <>{children(condition)}</>;
}
return <>{children}</>;
}
if (fallback) {
return <>{fallback}</>;
}
return null;
}, [condition, fallback, children]);
}

View File

@@ -0,0 +1,202 @@
'use client';
import type { FormEvent, MouseEventHandler } from 'react';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import cn from 'clsx';
import { UploadCloud, XIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Label } from '@kit/ui/label';
import { If } from './if';
type Props = Omit<React.InputHTMLAttributes<unknown>, 'value'> & {
image?: string | null;
onClear?: () => void;
onValueChange?: (props: { image: string; file: File }) => void;
visible?: boolean;
};
const IMAGE_SIZE = 22;
const ImageUploadInput = forwardRef<React.ElementRef<'input'>, Props>(
function ImageUploadInputComponent(
{
children,
image,
onClear,
onInput,
onValueChange,
visible = true,
...props
},
forwardedRef,
) {
const localRef = useRef<HTMLInputElement>();
const [state, setState] = useState({
image,
fileName: '',
});
const onInputChange = useCallback(
(e: FormEvent<HTMLInputElement>) => {
e.preventDefault();
const files = e.currentTarget.files;
if (files?.length) {
const file = files[0];
const data = URL.createObjectURL(file);
setState({
image: data,
fileName: file.name,
});
if (onValueChange) {
onValueChange({
image: data,
file,
});
}
}
if (onInput) {
onInput(e);
}
},
[onInput, onValueChange],
);
const onRemove = useCallback(() => {
setState({
image: '',
fileName: '',
});
if (localRef.current) {
localRef.current.value = '';
}
if (onClear) {
onClear();
}
}, [onClear]);
const imageRemoved: MouseEventHandler = useCallback(
(e) => {
e.preventDefault();
onRemove();
},
[onRemove],
);
const setRef = useCallback(
(input: HTMLInputElement) => {
localRef.current = input;
if (typeof forwardedRef === 'function') {
forwardedRef(localRef.current);
}
},
[forwardedRef],
);
useEffect(() => {
setState((state) => ({ ...state, image }));
}, [image]);
useEffect(() => {
if (!image) {
onRemove();
}
}, [image, onRemove]);
const Input = () => (
<input
{...props}
className={cn('hidden', props.className)}
ref={setRef}
type={'file'}
onInput={onInputChange}
accept="image/*"
aria-labelledby={'image-upload-input'}
/>
);
if (!visible) {
return <Input />;
}
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
id={'image-upload-input'}
className={`relative flex h-10 w-full cursor-pointer rounded-md border border-dashed border-input
bg-background px-3 py-2 text-sm outline-none ring-primary ring-offset-2 ring-offset-background transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:ring-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`}
>
<Input />
<div className={'flex items-center space-x-4'}>
<div className={'flex'}>
<If condition={!state.image}>
<UploadCloud className={'dark:text-dark-500 h-5 text-gray-500'} />
</If>
<If condition={state.image}>
<Image
loading={'lazy'}
style={{
width: IMAGE_SIZE,
height: IMAGE_SIZE,
}}
className={'object-contain'}
width={IMAGE_SIZE}
height={IMAGE_SIZE}
src={state.image!}
alt={props.alt ?? ''}
/>
</If>
</div>
<If condition={!state.image}>
<div className={'flex flex-auto'}>
<Label className={'cursor-pointer text-xs'}>{children}</Label>
</div>
</If>
<If condition={state.image}>
<div className={'flex flex-auto'}>
<If
condition={state.fileName}
fallback={
<Label className={'cursor-pointer truncate text-xs'}>
{children}
</Label>
}
>
<Label className={'truncate text-xs'}>{state.fileName}</Label>
</If>
</div>
</If>
<If condition={state.image}>
<Button
size={'icon'}
className={'!h-5 !w-5'}
onClick={imageRemoved}
>
<XIcon className="h-4" />
</Button>
</If>
</div>
</label>
);
},
);
export default ImageUploadInput;

View File

@@ -0,0 +1,113 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import Image from 'next/image';
import { ImageIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { Button } from '@kit/ui/button';
import ImageUploadInput from './image-upload-input';
import { Trans } from './trans';
function ImageUploader(
props: React.PropsWithChildren<{
value: string | null | undefined;
onValueChange: (value: File | null) => unknown;
}>,
) {
const [image, setImage] = useState(props.value);
const { setValue, register } = useForm<{
value: string | null | FileList;
}>({
defaultValues: {
value: props.value,
},
mode: 'onChange',
reValidateMode: 'onChange',
});
const control = register('value');
const onClear = useCallback(() => {
props.onValueChange(null);
setValue('value', null);
setImage('');
}, [props, setValue]);
const onValueChange = useCallback(
({ image, file }: { image: string; file: File }) => {
props.onValueChange(file);
setImage(image);
},
[props],
);
const Input = () => (
<ImageUploadInput
{...control}
accept={'image/*'}
className={'absolute h-full w-full'}
visible={false}
multiple={false}
onValueChange={onValueChange}
/>
);
useEffect(() => {
setImage(props.value);
}, [props.value]);
if (!image) {
return (
<FallbackImage descriptionSection={props.children}>
<Input />
</FallbackImage>
);
}
return (
<div className={'flex items-center space-x-4'}>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className={'relative h-20 w-20 animate-in fade-in zoom-in-50'}>
<Image fill className={'h-20 w-20 rounded-full'} src={image} alt={''} />
<Input />
</label>
<div>
<Button onClick={onClear} size={'sm'} variant={'ghost'}>
<Trans i18nKey={'common:clear'} />
</Button>
</div>
</div>
);
}
export default ImageUploader;
function FallbackImage(
props: React.PropsWithChildren<{
descriptionSection?: React.ReactNode;
}>,
) {
return (
<div className={'flex items-center space-x-4'}>
<label
className={
'relative flex h-20 w-20 cursor-pointer flex-col items-center justify-center rounded-full border border-border animate-in fade-in zoom-in-50 hover:border-primary'
}
>
<ImageIcon className={'h-8 text-primary'} />
{props.children}
</label>
{props.descriptionSection}
</div>
);
}

View File

@@ -0,0 +1,75 @@
const ROOT_PATH = '/';
/**
* @name isRouteActive
* @description A function to check if a route is active. This is used to
* highlight the active link in the navigation.
* @param targetLink - The link to check against
* @param currentRoute - the current route
* @param depth - how far down should segments be matched?
*/
export default function isRouteActive(
targetLink: string,
currentRoute: string,
depth: number,
) {
// we remove any eventual query param from the route's URL
const currentRoutePath = currentRoute.split('?')[0];
if (!isRoot(currentRoutePath) && isRoot(targetLink)) {
return false;
}
if (!currentRoutePath.includes(targetLink)) {
return false;
}
const isSameRoute = targetLink === currentRoutePath;
if (isSameRoute) {
return true;
}
return hasMatchingSegments(targetLink, currentRoutePath, depth);
}
function splitIntoSegments(href: string) {
return href.split('/').filter(Boolean);
}
function hasMatchingSegments(
targetLink: string,
currentRoute: string,
depth: number,
) {
const segments = splitIntoSegments(targetLink);
const matchingSegments = numberOfMatchingSegments(currentRoute, segments);
if (targetLink === currentRoute) {
return true;
}
// how far down should segments be matched?
// - if depth = 1 => only highlight the links of the immediate parent
// - if depth = 2 => for url = /account match /account/organization/members
return matchingSegments > segments.length - (depth - 1);
}
function numberOfMatchingSegments(href: string, segments: string[]) {
let count = 0;
for (const segment of splitIntoSegments(href)) {
// for as long as the segments match, keep counting + 1
if (segments.includes(segment)) {
count += 1;
} else {
return count;
}
}
return count;
}
function isRoot(path: string) {
return path === ROOT_PATH;
}

View File

@@ -0,0 +1,80 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useRefresh from '@kit/shared/hooks/use-refresh';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
const LanguageDropdownSwitcher: React.FC<{
onChange?: (locale: string) => unknown;
}> = ({ onChange }) => {
const { i18n } = useTranslation();
const refresh = useRefresh();
const { language: currentLanguage, options } = i18n;
const locales = (options.supportedLngs as string[]).filter(
(locale) => locale.toLowerCase() !== 'cimode',
);
const languageNames = useMemo(() => {
return new Intl.DisplayNames([currentLanguage], {
type: 'language',
});
}, [currentLanguage]);
const [value, setValue] = useState(i18n.language);
const languageChanged = useCallback(
async (locale: string) => {
setValue(locale);
if (onChange) {
onChange(locale);
}
await i18n.changeLanguage(locale);
return refresh();
},
[i18n, onChange, refresh],
);
return (
<Select value={value} onValueChange={languageChanged}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{locales.map((locale) => {
const label = capitalize(languageNames.of(locale) ?? locale);
const option = {
value: locale,
label,
};
return (
<SelectItem value={option.value} key={option.value}>
{option.label}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
};
function capitalize(lang: string) {
return lang.slice(0, 1).toUpperCase() + lang.slice(1);
}
export default LanguageDropdownSwitcher;

View File

@@ -0,0 +1,62 @@
'use client';
import { createRef, useLayoutEffect, useMemo, useState } from 'react';
/**
* @description Render a component lazily based on the IntersectionObserver
* appConfig provided.
* Full documentation at: https://makerkit.dev/docs/components-utilities#lazyrender
* @param children
* @param threshold
* @param rootMargin
* @param onVisible
* @constructor
*/
export function LazyRender({
children,
threshold,
rootMargin,
onVisible,
}: React.PropsWithChildren<{
threshold?: number;
rootMargin?: string;
onVisible?: () => void;
}>) {
const ref = useMemo(() => createRef<HTMLDivElement>(), []);
const [isVisible, setIsVisible] = useState(false);
useLayoutEffect(() => {
if (!ref.current) {
return;
}
const options = {
rootMargin: rootMargin ?? '0px',
threshold: threshold ?? 1,
};
const isIntersecting = (entry: IntersectionObserverEntry) =>
entry.isIntersecting || entry.intersectionRatio > 0;
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (isIntersecting(entry)) {
setIsVisible(true);
observer.disconnect();
if (onVisible) {
onVisible();
}
}
});
}, options);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [threshold, rootMargin, ref, onVisible]);
return <div ref={ref}>{isVisible ? children : null}</div>;
}

View File

@@ -0,0 +1,31 @@
import type { PropsWithChildren } from 'react';
import { cn } from '../utils/cn';
import Spinner from './spinner';
export function LoadingOverlay({
children,
className,
fullPage = true,
}: PropsWithChildren<{
className?: string;
fullPage?: boolean;
displayLogo?: boolean;
}>) {
return (
<div
className={cn(
'flex flex-col items-center justify-center space-y-4',
className,
{
[`fixed left-0 top-0 z-[100] h-screen w-screen bg-background`]:
fullPage,
},
)}
>
<Spinner className={'text-primary'} />
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,82 @@
import { forwardRef } from 'react';
import Image from 'next/image';
import { cn } from '../../shadcn';
import { LazyRender } from '../lazy-render';
const NextImage: React.FC<{
width: number;
height: number;
src: string;
alt: string;
class?: string;
}> = (props) => {
const className = cn(props.class, `object-cover`);
return <Image className={className} src={props.src} alt={props.alt} />;
};
const ExternalLink = forwardRef<
React.ElementRef<'a'>,
React.AnchorHTMLAttributes<unknown>
>(function ExternalLink(props, ref) {
const href = props.href ?? '';
const isRoot = href === '/';
const isInternalLink =
isRoot || href.startsWith(process.env.NEXT_PUBLIC_SITE_URL!);
if (isInternalLink) {
return (
<a {...props} ref={ref} href={href}>
{props.children}
</a>
);
}
return (
<a
href={href}
ref={ref}
{...props}
target="_blank"
rel="noopener noreferrer"
>
{props.children}
</a>
);
});
const Video: React.FC<{
src: string;
width?: string;
type?: string;
}> = ({ src, type, width }) => {
const useType = type ?? 'video/mp4';
return (
<LazyRender rootMargin={'-200px 0px'}>
<video
className="my-4"
width={width ?? `100%`}
height="auto"
playsInline
autoPlay
muted
loop
>
<source src={src} type={useType} />
</video>
</LazyRender>
);
};
const Components = {
img: NextImage,
a: ExternalLink,
Video,
Image: NextImage,
};
export default Components;

View File

@@ -0,0 +1,97 @@
.MDX h1 {
@apply mt-14 text-4xl font-bold;
}
.MDX h2 {
@apply mb-4 mt-12 text-2xl font-semibold lg:text-3xl;
}
.MDX h3 {
@apply mt-10 text-2xl font-bold;
}
.MDX h4 {
@apply mt-8 text-xl font-bold;
}
.MDX h5 {
@apply mt-6 text-lg font-semibold;
}
.MDX h6 {
@apply mt-2 text-base font-medium;
}
/**
Tailwind "dark" variants do not work with CSS Modules
We work it around using :global(.dark)
For more info: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomment-770215347
*/
:global(.dark) .MDX h1,
:global(.dark) .MDX h2,
:global(.dark) .MDX h3,
:global(.dark) .MDX h4,
:global(.dark) .MDX h5,
:global(.dark) .MDX h6 {
@apply text-white;
}
.MDX p {
@apply mb-4 mt-2 text-base leading-7;
}
.MDX li {
@apply relative my-1.5 text-base leading-7;
}
.MDX ul > li:before {
content: '-';
@apply mr-2;
}
.MDX ol > li:before {
@apply inline-flex font-medium;
content: counters(counts, '.') '. ';
font-feature-settings: 'tnum';
}
.MDX b,
.MDX strong {
@apply font-bold;
}
:global(.dark) .MDX b,
:global(.dark) .MDX strong {
@apply text-white;
}
.MDX img,
.MDX video {
@apply rounded-md;
}
.MDX ul,
.MDX ol {
@apply pl-1;
}
.MDX ol > li {
counter-increment: counts;
}
.MDX ol > li:before {
@apply mr-2 inline-flex font-semibold;
content: counters(counts, '.') '. ';
font-feature-settings: 'tnum';
}
.MDX blockquote {
@apply my-4 border-l-4 border-primary bg-muted px-6 py-4 text-lg font-medium text-gray-600;
}
.MDX pre {
@apply my-6 text-sm text-current;
}

View File

@@ -0,0 +1,19 @@
import type { MDXComponents } from 'mdx/types';
import { getMDXComponent } from 'next-contentlayer/hooks';
import Components from './mdx-components';
import styles from './mdx-renderer.module.css';
export function Mdx({
code,
}: React.PropsWithChildren<{
code: string;
}>) {
const Component = getMDXComponent(code);
return (
<div className={styles.MDX}>
<Component components={Components as unknown as MDXComponents} />
</div>
);
}

View File

@@ -0,0 +1,72 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { ChevronDownIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Trans } from '@kit/ui/trans';
function MobileNavigationDropdown({
links,
}: {
links: {
path: string;
label: string;
}[];
}) {
const path = usePathname();
const currentPathName = useMemo(() => {
return Object.values(links).find((link) => link.path === path)?.label;
}, [links, path]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'secondary'} className={'w-full'}>
<span
className={'flex w-full items-center justify-between space-x-2'}
>
<span>
<Trans i18nKey={currentPathName} defaults={currentPathName} />
</span>
<ChevronDownIcon className={'h-5'} />
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className={
'dark:divide-dark-700 w-screen divide-y divide-gray-100' +
' rounded-none'
}
>
{Object.values(links).map((link) => {
return (
<DropdownMenuItem asChild key={link.path}>
<Link
className={'flex h-12 w-full items-center'}
href={link.path}
>
<Trans i18nKey={link.label} defaults={link.label} />
</Link>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
export default MobileNavigationDropdown;

View File

@@ -0,0 +1,73 @@
import { useMemo } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { ChevronDownIcon } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Trans } from '@kit/ui/trans';
const MobileNavigationDropdown: React.FC<{
links: {
path: string;
label: string;
}[];
}> = ({ links }) => {
const path = usePathname();
const items = useMemo(
function MenuItems() {
return Object.values(links).map((link) => {
return (
<DropdownMenuItem key={link.path}>
<Link
className={'flex h-full w-full items-center'}
href={link.path}
>
<Trans i18nKey={link.label} defaults={link.label} />
</Link>
</DropdownMenuItem>
);
});
},
[links],
);
const currentPathName = useMemo(() => {
return Object.values(links).find((link) => link.path === path)?.label;
}, [links, path]);
return (
<DropdownMenu>
<DropdownMenuTrigger className={'w-full'}>
<div
className={
'Button dark:ring-dark-700 w-full justify-start ring-2 ring-gray-100'
}
>
<span
className={
'ButtonNormal flex w-full items-center justify-between space-x-2'
}
>
<span>
<Trans i18nKey={currentPathName} defaults={currentPathName} />
</span>
<ChevronDownIcon className={'h-5'} />
</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>{items}</DropdownMenuContent>
</DropdownMenu>
);
};
export default MobileNavigationDropdown;

View File

@@ -0,0 +1,15 @@
import { cn } from './utils';
const NavigationContainer: React.FC<{
className?: string;
}> = ({ children, className }) => {
return (
<div
className={cn(`dark:border-dark-800 border-b border-gray-50`, className)}
>
{children}
</div>
);
};
export default NavigationContainer;

View File

@@ -0,0 +1,125 @@
'use client';
import { useContext } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cva } from 'class-variance-authority';
import isRouteActive from '@kit/generic/is-route-active';
import Trans from '@/components/app/Trans';
import { NavigationMenuContext } from './navigation-menu-context';
import { cn } from './utils';
interface Link {
path: string;
label?: string;
}
const NavigationMenuItem: React.FC<{
link: Link;
depth?: number;
disabled?: boolean;
shallow?: boolean;
className?: string;
}> = ({ link, disabled, shallow, depth, ...props }) => {
const pathName = usePathname() ?? '';
const active = isRouteActive(link.path, pathName, depth ?? 3);
const menuProps = useContext(NavigationMenuContext);
const label = link.label;
const itemClassName = getNavigationMenuItemClassBuilder()({
active,
...menuProps,
});
const className = cn(itemClassName, props.className ?? ``);
return (
<li className={className}>
<Link
className={
'justify-center transition-transform duration-500 lg:justify-start'
}
aria-disabled={disabled}
href={disabled ? '' : link.path}
shallow={shallow ?? active}
>
<Trans i18nKey={label} defaults={label} />
</Link>
</li>
);
};
export default NavigationMenuItem;
function getNavigationMenuItemClassBuilder() {
return cva(
[
`flex items-center justify-center font-medium lg:justify-start rounded-md text-sm transition colors transform *:active:translate-y-[2px]`,
'*:p-1 *:lg:px-2.5 *:s-full *:flex *:items-center',
'aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
],
{
compoundVariants: [
// not active - shared
{
active: false,
className: `font-medium hover:underline`,
},
// active - shared
{
active: true,
className: `font-semibold`,
},
// active - pill
{
active: true,
pill: true,
className: `bg-gray-50 text-gray-800 dark:bg-primary-300/10`,
},
// not active - pill
{
active: false,
pill: true,
className: `hover:bg-gray-50 active:bg-gray-100 text-gray-500 dark:text-gray-300 dark:hover:bg-background dark:active:bg-dark-900/90`,
},
// not active - bordered
{
active: false,
bordered: true,
className: `hover:bg-gray-50 active:bg-gray-100 dark:active:bg-dark-800 dark:hover:bg-dark/90 transition-colors rounded-lg border-transparent`,
},
// active - bordered
{
active: true,
bordered: true,
className: `top-[0.4rem] border-b-[0.25rem] rounded-none border-primary bg-transparent pb-[0.55rem] text-primary-700 dark:text-white`,
},
// active - secondary
{
active: true,
secondary: true,
className: `bg-transparent font-semibold`,
},
],
variants: {
active: {
true: ``,
},
pill: {
true: `[&>*]:py-2`,
},
bordered: {
true: `relative h-10`,
},
secondary: {
true: ``,
},
},
},
);
}

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
import type { NavigationMenuProps } from './navigation-menu';
export const NavigationMenuContext = createContext<NavigationMenuProps>({});

View File

@@ -0,0 +1,49 @@
'use client';
import type { PropsWithChildren } from 'react';
import { cva } from 'class-variance-authority';
import { NavigationMenuContext } from './navigation-menu-context';
type Vertical = {
vertical?: boolean;
};
type Bordered = {
bordered?: boolean;
};
type Pill = {
pill?: boolean;
};
export type NavigationMenuProps = Vertical & (Bordered | Pill);
function NavigationMenu(props: PropsWithChildren<NavigationMenuProps>) {
const className = getNavigationMenuClassBuilder()(props);
return (
<ul className={className}>
<NavigationMenuContext.Provider value={props}>
{props.children}
</NavigationMenuContext.Provider>
</ul>
);
}
export default NavigationMenu;
function getNavigationMenuClassBuilder() {
return cva(['w-full dark:text-gray-300 items-center flex-wrap flex'], {
variants: {
vertical: {
true: `flex items-start justify-between space-x-2
lg:flex-col lg:justify-start lg:space-x-0 lg:space-y-1.5 [&>li>a]:w-full`,
},
bordered: {
true: `lg:space-x-3 border-b border-gray-100 dark:border-dark-800 pb-1.5`,
},
},
});
}

View File

@@ -0,0 +1,143 @@
import type { FormEventHandler } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { Input } from '../shadcn/input';
const DIGITS = 6;
export function OtpInput({
onValid,
onInvalid,
}: React.PropsWithChildren<{
onValid: (code: string) => void;
onInvalid: () => void;
}>) {
const digitsArray = useMemo(
() => Array.from({ length: DIGITS }, (_, i) => i),
[],
);
const { control, register, watch, setFocus, formState, setValue } = useForm({
mode: 'onChange',
reValidateMode: 'onChange',
defaultValues: {
values: digitsArray.map(() => ({ value: '' })),
},
});
useFieldArray({
control,
name: 'values',
shouldUnregister: true,
});
const { values } = watch();
const isFormValid = formState.isValid;
const code = (values ?? []).map(({ value }) => value).join('');
useEffect(() => {
if (!isFormValid) {
onInvalid();
return;
}
if (code.length === DIGITS) {
onValid(code);
return;
}
onInvalid();
}, [onInvalid, onValid, code, isFormValid]);
useEffect(() => {
setFocus('values.0.value');
}, [setFocus]);
const onInput: FormEventHandler<HTMLInputElement> = useCallback(
(target) => {
const element = target.currentTarget;
const isValid = element.reportValidity();
if (isValid) {
const nextIndex = Number(element.dataset.index) + 1;
if (nextIndex >= DIGITS) {
return;
}
setFocus(`values.${nextIndex}.value`);
}
},
[setFocus],
);
const onPaste = useCallback(
(event: React.ClipboardEvent<HTMLInputElement>) => {
const pasted = event.clipboardData.getData('text/plain');
// check if value is numeric
if (isNumeric(pasted)) {
const digits = getDigits(pasted, digitsArray);
digits.forEach((value, index) => {
setValue(`values.${index}.value`, value);
setFocus(`values.${index + 1}.value`);
});
}
},
[digitsArray, setFocus, setValue],
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Backspace') {
event.preventDefault();
const index = Number(event.currentTarget.dataset.inputIndex);
setValue(`values.${index}.value`, '');
setFocus(`values.${index - 1}.value`);
}
},
[setFocus, setValue],
);
return (
<div className={'flex justify-center space-x-2'}>
{digitsArray.map((digit, index) => {
const control = { ...register(`values.${digit}.value`) };
return (
<Input
autoComplete={'off'}
className={'w-10 text-center'}
data-index={digit}
pattern="[0-9]"
required
key={digit}
maxLength={1}
onInput={onInput}
onPaste={onPaste}
onKeyDown={handleKeyDown}
data-input-index={index}
{...control}
/>
);
})}
</div>
);
}
function isNumeric(pasted: string) {
const isNumericRegExp = /^-?\d+$/;
return isNumericRegExp.test(pasted);
}
function getDigits(pasted: string, digitsArray: number[]) {
return pasted.split('').slice(0, digitsArray.length);
}

View File

@@ -0,0 +1,73 @@
import { cn } from '../utils';
export function Page(
props: React.PropsWithChildren<{
sidebar?: React.ReactNode;
contentContainerClassName?: string;
className?: string;
}>,
) {
return (
<div className={cn('flex', props.className)}>
<div className={'hidden lg:block'}>{props.sidebar}</div>
<div
className={
props.contentContainerClassName ??
'mx-auto flex h-screen w-full flex-col overflow-y-auto'
}
>
{props.children}
</div>
</div>
);
}
export function PageBody(
props: React.PropsWithChildren<{
className?: string;
}>,
) {
const className = cn('w-full px-4 flex flex-col flex-1', props.className);
return <div className={className}>{props.children}</div>;
}
export function PageHeader({
children,
title,
description,
mobileNavigation,
}: React.PropsWithChildren<{
title: string | React.ReactNode;
description?: string | React.ReactNode;
mobileNavigation?: React.ReactNode;
}>) {
return (
<div className={'flex items-start justify-between p-4'}>
<div
className={
'flex items-center space-x-2 lg:flex-col lg:items-start lg:space-x-0'
}
>
<div className={'flex items-center lg:hidden'}>{mobileNavigation}</div>
<h1>
<span className={'flex items-center space-x-0.5 lg:space-x-2'}>
<span className={'text-xl font-semibold dark:text-white'}>
{title}
</span>
</span>
</h1>
<h2 className={'hidden lg:block'}>
<span className={'text-base font-normal text-muted-foreground'}>
{description}
</span>
</h2>
</div>
{children}
</div>
);
}

View File

@@ -0,0 +1,307 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { CheckCircleIcon, SparklesIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
import Heading from '@kit/ui/heading';
// TODO: pass in from app
import pathsConfig from '@kit/web/config/paths.config';
import pricingConfig from '@kit/web/config/pricing.config';
import { cn } from '../utils/cn';
import { If } from './if';
import { Trans } from './trans';
interface CheckoutButtonProps {
readonly stripePriceId?: string;
readonly recommended?: boolean;
}
interface PricingItemProps {
selectable: boolean;
product: {
name: string;
features: string[];
description: string;
recommended?: boolean;
badge?: string;
};
plan: {
name: string;
stripePriceId?: string;
price: string;
label?: string;
href?: string;
};
}
const STRIPE_PRODUCTS = pricingConfig.products;
const STRIPE_PLANS = STRIPE_PRODUCTS.reduce<string[]>((acc, product) => {
product.plans.forEach((plan) => {
if (plan.name && !acc.includes(plan.name)) {
acc.push(plan.name);
}
});
return acc;
}, []);
function PricingTable(
props: React.PropsWithChildren<{
CheckoutButton?: React.ComponentType<CheckoutButtonProps>;
}>,
) {
const [planVariant, setPlanVariant] = useState<string>(STRIPE_PLANS[0]);
return (
<div className={'flex flex-col space-y-12'}>
<div className={'flex justify-center'}>
<PlansSwitcher
plans={STRIPE_PLANS}
plan={planVariant}
setPlan={setPlanVariant}
/>
</div>
<div
className={
'flex flex-col items-start space-y-6 lg:space-y-0' +
' justify-center lg:flex-row lg:space-x-4'
}
>
{STRIPE_PRODUCTS.map((product) => {
const plan =
product.plans.find((item) => item.name === planVariant) ??
product.plans[0];
return (
<PricingItem
selectable
key={plan.stripePriceId ?? plan.name}
plan={plan}
product={product}
CheckoutButton={props.CheckoutButton}
/>
);
})}
</div>
</div>
);
}
export default PricingTable;
PricingTable.Item = PricingItem;
PricingTable.Price = Price;
PricingTable.FeaturesList = FeaturesList;
function PricingItem(
props: React.PropsWithChildren<
PricingItemProps & {
CheckoutButton?: React.ComponentType<CheckoutButtonProps>;
}
>,
) {
const recommended = props.product.recommended ?? false;
return (
<div
data-test={'subscription-plan'}
className={cn(
`
relative flex w-full flex-col justify-between space-y-6 rounded-lg
p-6 lg:w-4/12 xl:max-w-xs xl:p-8 2xl:w-3/12
`,
{
['border']: !recommended,
['border-2 border-primary']: recommended,
},
)}
>
<div className={'flex flex-col space-y-1'}>
<div className={'flex items-center space-x-4'}>
<Heading level={4}>{props.product.name}</Heading>
<If condition={props.product.badge}>
<div
className={cn(
`flex space-x-1 rounded-md px-2 py-1 text-xs font-medium`,
{
['bg-primary text-primary-foreground']: recommended,
['bg-muted text-muted-foreground']: !recommended,
},
)}
>
<If condition={recommended}>
<SparklesIcon className={'mr-1 h-4 w-4'} />
</If>
<span>{props.product.badge}</span>
</div>
</If>
</div>
<span className={'text-muted-foreground'}>
{props.product.description}
</span>
</div>
<div className={'flex items-end space-x-1'}>
<Price>{props.plan.price}</Price>
<If condition={props.plan.name}>
<span className={cn(`text-lg lowercase text-muted-foreground`)}>
<span>/</span>
<span>{props.plan.name}</span>
</span>
</If>
</div>
<div className={'text-current'}>
<FeaturesList features={props.product.features} />
</div>
<If condition={props.selectable}>
<If
condition={props.plan.stripePriceId && props.CheckoutButton}
fallback={
<DefaultCheckoutButton
recommended={recommended}
plan={props.plan}
/>
}
>
{(CheckoutButton) => (
<CheckoutButton
recommended={recommended}
stripePriceId={props.plan.stripePriceId}
/>
)}
</If>
</If>
</div>
);
}
function FeaturesList(
props: React.PropsWithChildren<{
features: string[];
}>,
) {
return (
<ul className={'flex flex-col space-y-2'}>
{props.features.map((feature) => {
return (
<ListItem key={feature}>
<Trans
i18nKey={`common:plans.features.${feature}`}
defaults={feature}
/>
</ListItem>
);
})}
</ul>
);
}
function Price({ children }: React.PropsWithChildren) {
// little trick to re-animate the price when switching plans
const key = Math.random();
return (
<div
key={key}
className={`duration-500 animate-in fade-in slide-in-from-left-4`}
>
<span className={'text-2xl font-bold lg:text-3xl xl:text-4xl'}>
{children}
</span>
</div>
);
}
function ListItem({ children }: React.PropsWithChildren) {
return (
<li className={'flex items-center space-x-3 font-medium'}>
<div>
<CheckCircleIcon className={'h-5 text-green-500'} />
</div>
<span className={'text-sm'}>{children}</span>
</li>
);
}
function PlansSwitcher(
props: React.PropsWithChildren<{
plans: string[];
plan: string;
setPlan: (plan: string) => void;
}>,
) {
return (
<div className={'flex'}>
{props.plans.map((plan, index) => {
const selected = plan === props.plan;
const className = cn('focus:!ring-0 !outline-none', {
'rounded-r-none border-r-transparent': index === 0,
'rounded-l-none': index === props.plans.length - 1,
['hover:bg-muted']: !selected,
['font-bold hover:bg-background hover:text-initial']: selected,
});
return (
<Button
key={plan}
variant={'outline'}
className={className}
onClick={() => props.setPlan(plan)}
>
<span className={'flex items-center space-x-1'}>
<If condition={selected}>
<CheckCircleIcon className={'h-4 text-green-500'} />
</If>
<span>
<Trans i18nKey={`common:plans.${plan}`} defaults={plan} />
</span>
</span>
</Button>
);
})}
</div>
);
}
function DefaultCheckoutButton(
props: React.PropsWithChildren<{
plan: PricingItemProps['plan'];
recommended?: boolean;
}>,
) {
const signUpPath = pathsConfig.auth.signUp;
const linkHref =
props.plan.href ?? `${signUpPath}?utm_source=${props.plan.stripePriceId}`;
const label = props.plan.label ?? 'common:getStarted';
return (
<div className={'bottom-0 left-0 w-full p-0'}>
<Button
className={'w-full'}
variant={props.recommended ? 'default' : 'outline'}
>
<Link href={linkHref}>
<Trans i18nKey={label} defaults={label} />
</Link>
</Button>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
type SessionProps = {
displayName?: string | null;
pictureUrl?: string | null;
};
type TextProps = {
text: string;
};
type ProfileAvatarProps = SessionProps | TextProps;
export function ProfileAvatar(props: ProfileAvatarProps) {
const avatarClassName = 'mx-auto w-9 h-9 group-focus:ring-2';
if ('text' in props) {
return (
<Avatar className={avatarClassName}>
<AvatarFallback>
<span className={'uppercase'}>{props.text.slice(0, 2)}</span>
</AvatarFallback>
</Avatar>
);
}
const initials = props.displayName?.slice(0, 2);
return (
<Avatar className={avatarClassName}>
<AvatarImage src={props.pictureUrl ?? undefined} />
<AvatarFallback>
<span className={'uppercase'}>{initials}</span>
</AvatarFallback>
</Avatar>
);
}

View File

@@ -0,0 +1,30 @@
import { z } from 'zod';
export const SidebarConfigSchema = z.object({
routes: z.array(
z.union([
z.object({
label: z.string(),
path: z.string(),
Icon: z.custom<React.ReactNode>(),
end: z.boolean().optional(),
}),
z.object({
label: z.string(),
collapsible: z.boolean().optional(),
collapsed: z.boolean().optional(),
children: z.array(
z.object({
label: z.string(),
path: z.string(),
Icon: z.custom<React.ReactNode>(),
end: z.boolean().optional(),
}),
),
}),
z.object({
divider: z.literal(true),
}),
]),
),
});

View File

@@ -0,0 +1,264 @@
'use client';
import { useContext, useId, useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cva } from 'class-variance-authority';
import { ChevronDownIcon } from 'lucide-react';
import { z } from 'zod';
import { Button } from '@kit/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '../shadcn/tooltip';
import { cn } from '../utils';
import { SidebarContext } from './context/sidebar.context';
import { If } from './if';
import isRouteActive from './is-route-active';
import { SidebarConfigSchema } from './sidebar-schema';
import { Trans } from './trans';
export type SidebarConfig = z.infer<typeof SidebarConfigSchema>;
export function Sidebar(props: {
collapsed?: boolean;
children:
| React.ReactNode
| ((props: {
collapsed: boolean;
setCollapsed: (collapsed: boolean) => void;
}) => React.ReactNode);
}) {
const [collapsed, setCollapsed] = useState(props.collapsed ?? false);
const className = getClassNameBuilder()({
collapsed,
});
const ctx = { collapsed, setCollapsed };
return (
<SidebarContext.Provider value={ctx}>
<div className={className}>
{typeof props.children === 'function'
? props.children(ctx)
: props.children}
</div>
</SidebarContext.Provider>
);
}
export function SidebarContent({
children,
className,
}: React.PropsWithChildren<{
className?: string;
}>) {
return (
<div className={cn('flex w-full flex-col space-y-1.5 px-4', className)}>
{children}
</div>
);
}
export function SidebarGroup({
label,
collapsed = false,
collapsible = true,
children,
}: React.PropsWithChildren<{
label: string | React.ReactNode;
collapsible?: boolean;
collapsed?: boolean;
}>) {
const { collapsed: sidebarCollapsed } = useContext(SidebarContext);
const [isGroupCollapsed, setIsGroupCollapsed] = useState(collapsed);
const id = useId();
const Title = (props: React.PropsWithChildren) => {
if (sidebarCollapsed) {
return null;
}
return (
<span className={'text-xs font-semibold uppercase text-muted-foreground'}>
{props.children}
</span>
);
};
const Wrapper = () => {
const className = cn(
'group flex items-center justify-between px-container space-x-2.5',
{
'py-2.5': !sidebarCollapsed,
},
);
if (collapsible) {
return (
<button
aria-expanded={!isGroupCollapsed}
aria-controls={id}
onClick={() => setIsGroupCollapsed(!isGroupCollapsed)}
className={className}
>
<Title>{label}</Title>
<If condition={collapsible}>
<ChevronDownIcon
className={cn(`h-3 transition duration-300`, {
'rotate-180': !isGroupCollapsed,
})}
/>
</If>
</button>
);
}
return (
<div className={className}>
<Title>{label}</Title>
</div>
);
};
return (
<div className={'flex flex-col space-y-1 py-1'}>
<Wrapper />
<If condition={collapsible ? !isGroupCollapsed : true}>
<div id={id} className={'flex flex-col space-y-1.5'}>
{children}
</div>
</If>
</div>
);
}
export function SidebarDivider() {
return (
<div className={'dark:border-dark-800 my-2 border-t border-gray-100'} />
);
}
export function SidebarItem({
end,
path,
children,
Icon,
}: React.PropsWithChildren<{
path: string;
Icon: React.ReactNode;
end?: boolean;
}>) {
const { collapsed } = useContext(SidebarContext);
const currentPath = usePathname() ?? '';
const active = isRouteActive(path, currentPath, end ? 0 : 3);
const variant = active ? 'default' : 'ghost';
const size = collapsed ? 'icon' : 'default';
return (
<Link key={path} href={path}>
<Button
className={cn('flex w-full', {
'justify-start space-x-2': !collapsed,
})}
size={size}
variant={variant}
>
<If condition={collapsed} fallback={Icon}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{Icon}</TooltipTrigger>
<TooltipContent side={'right'} sideOffset={20}>
{children}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</If>
<span
className={cn({
hidden: collapsed,
})}
>
{children}
</span>
</Button>
</Link>
);
}
function getClassNameBuilder() {
return cva(
['flex box-content h-screen flex-col border-r border-border relative'],
{
variants: {
collapsed: {
true: `w-[6rem]`,
false: `w-2/12 lg:w-[17rem]`,
},
},
},
);
}
export function SidebarNavigation({
config,
}: React.PropsWithChildren<{
config: SidebarConfig;
}>) {
return (
<>
{config.routes.map((item, index) => {
if ('divider' in item) {
return <SidebarDivider key={index} />;
}
if ('children' in item) {
return (
<SidebarGroup
key={item.label}
label={<Trans i18nKey={item.label} defaults={item.label} />}
collapsible={item.collapsible}
collapsed={item.collapsed}
>
{item.children.map((child) => {
return (
<SidebarItem
key={child.path}
end={child.end}
path={child.path}
Icon={child.Icon}
>
<Trans i18nKey={child.label} defaults={child.label} />
</SidebarItem>
);
})}
</SidebarGroup>
);
}
return (
<SidebarItem
key={item.path}
end={item.end}
path={item.path}
Icon={item.Icon}
>
<Trans i18nKey={item.label} defaults={item.label} />
</SidebarItem>
);
})}
</>
);
}

View File

@@ -0,0 +1,34 @@
import { cn } from '../utils/cn';
function Spinner(
props: React.PropsWithChildren<{
className?: string;
}>,
) {
return (
<div role="status">
<svg
aria-hidden="true"
className={cn(
`h-8 w-8 animate-spin fill-white text-primary dark:fill-primary dark:text-primary/30`,
props.className,
)}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
);
}
export default Spinner;

View File

@@ -0,0 +1,168 @@
'use client';
import { Fragment, useCallback } from 'react';
import { cva } from 'class-variance-authority';
import { cn } from '../utils/cn';
import { If } from './if';
import { Trans } from './trans';
type Variant = 'numbers' | 'default';
const classNameBuilder = getClassNameBuilder();
/**
* Renders a stepper component with multiple steps.
*
* @param {Object} props - The props object containing the following properties:
* - steps {string[]} - An array of strings representing the step labels.
* - currentStep {number} - The index of the currently active step.
* - variant {string} (optional) - The variant of the stepper component (default: 'default').
**/
function Stepper(props: {
steps: string[];
currentStep: number;
variant?: Variant;
}) {
const variant = props.variant ?? 'default';
const Steps = useCallback(() => {
return props.steps.map((labelOrKey, index) => {
const selected = props.currentStep === index;
const className = classNameBuilder({
selected,
variant,
});
const isNumberVariant = variant === 'numbers';
const labelClassName = cn({
['text-xs px-1.5 py-2']: !isNumberVariant,
});
const { label, number } = getStepLabel(labelOrKey, index);
return (
<Fragment key={index}>
<div aria-selected={selected} className={className}>
<span className={labelClassName}>
{number}
<If condition={!isNumberVariant}>. {label}</If>
</span>
</div>
<If condition={isNumberVariant}>
<StepDivider selected={selected}>{label}</StepDivider>
</If>
</Fragment>
);
});
}, [props.steps, props.currentStep, variant]);
// If there are no steps, don't render anything.
if (props.steps.length < 2) {
return null;
}
const containerClassName = cn({
['flex justify-between']: variant === 'numbers',
['flex space-x-1']: variant === 'default',
});
return (
<div className={containerClassName}>
<Steps />
</div>
);
}
export default Stepper;
function getClassNameBuilder() {
return cva(``, {
variants: {
variant: {
default: `flex flex-col h-[2.5px] w-full transition-colors duration-500`,
numbers:
'w-9 h-9 font-bold rounded-full flex items-center justify-center' +
' text-sm border',
},
selected: {
true: '',
false: '',
},
},
compoundVariants: [
{
variant: 'default',
selected: false,
className: 'text-gray-400 dark:text-gray-500',
},
{
variant: 'default',
selected: true,
className: 'bg-primary',
},
{
variant: 'default',
selected: false,
className: 'bg-gray-300 dark:bg-gray-800',
},
{
variant: 'numbers',
selected: true,
className: 'text-primary border-primary',
},
{
variant: 'numbers',
selected: false,
className: 'text-gray-400 dark:text-gray-500',
},
],
defaultVariants: {
variant: 'default',
selected: false,
},
});
}
function StepDivider({
selected,
children,
}: React.PropsWithChildren<{
selected: boolean;
}>) {
const spanClassName = cn('font-medium text-sm', {
['text-gray-400 dark:text-gray-500']: !selected,
['text-primary']: selected,
});
const className = cn(
'flex flex-1 last:flex-[0_0_0] items-center h-9 justify-center' +
' items-center w-full group px-3 flex space-x-3',
);
return (
<div className={className}>
<span className={spanClassName}>{children}</span>
<div
className={
'divider h-[1px] w-full bg-gray-200 transition-colors' +
' dark:bg-dark-600 group-last:hidden'
}
/>
</div>
);
}
function getStepLabel(labelOrKey: string, index: number) {
const number = (index + 1).toString();
return {
number,
label: <Trans i18nKey={labelOrKey} defaults={labelOrKey} />,
};
}

View File

@@ -0,0 +1,38 @@
'use client';
import { createRef, useEffect, useRef } from 'react';
import type { LoadingBarRef } from 'react-top-loading-bar';
import LoadingBar from 'react-top-loading-bar';
export function TopLoadingBarIndicator() {
const ref = createRef<LoadingBarRef>();
const runningRef = useRef(false);
useEffect(() => {
if (!ref.current || runningRef.current) {
return;
}
const loadingBarRef = ref.current;
loadingBarRef.continuousStart(0, 250);
runningRef.current = true;
return () => {
loadingBarRef.complete();
runningRef.current = false;
};
}, [ref]);
return (
<LoadingBar
height={4}
waitingTime={0}
shadow
className={'bg-primary'}
color={''}
ref={ref}
/>
);
}

View File

@@ -0,0 +1,5 @@
import { Trans as TransComponent } from 'react-i18next/TransWithoutContext';
export function Trans(props: React.ComponentProps<typeof TransComponent>) {
return <TransComponent {...props} />;
}

View File

@@ -1,31 +0,0 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "./utils/cn";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View File

@@ -1,120 +0,0 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "./utils/cn";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
position === "popper" && "translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};

View File

@@ -0,0 +1,143 @@
'use client';
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { buttonVariants } from '@kit/ui/button';
import { cn } from '../utils/cn';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className,
)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
className,
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from '../utils/cn';
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
success:
'border-green-600/50 text-green-600 dark:border-green-600 [&>svg]:text-green-600',
warning:
'border-orange-600/50 text-orange-600 dark:border-orange-600 [&>svg]:text-orange-600',
info: 'border-blue-600/50 text-blue-600 dark:border-blue-600 [&>svg]:text-blue-600',
},
},
defaultVariants: {
variant: 'default',
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,9 +1,10 @@
"use client";
'use client';
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '../utils/cn';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -12,7 +13,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
@@ -26,7 +27,7 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
@@ -39,7 +40,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className,
)}
{...props}

View File

@@ -0,0 +1,42 @@
import * as React from 'react';
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from '../utils/cn';
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
success:
'border-transparent bg-green-50 text-green-500 dark:bg-transparent',
warning:
'border-transparent bg-orange-50 text-orange-500 dark:bg-transparent',
info: 'border-transparent bg-blue-50 text-blue-500 dark:bg-transparent',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,59 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { cn } from '../utils/cn';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants };

View File

@@ -0,0 +1,70 @@
'use client';
import * as React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { cn } from '../utils/cn';
import { buttonVariants } from './button';
export type { DateRange } from 'react-day-picker';
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell:
'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: 'text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
),
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside: 'text-muted-foreground opacity-50',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: ({ ...props }) => (
<ChevronLeft className="h-4 w-4" {...props} />
),
IconRight: ({ ...props }) => (
<ChevronRight className="h-4 w-4" {...props} />
),
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar };

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import * as React from 'react';
import { cn } from "./utils/cn";
import { cn } from '../utils/cn';
const Card = React.forwardRef<
HTMLDivElement,
@@ -9,13 +9,13 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
'rounded-xl border bg-card text-card-foreground shadow-sm',
className,
)}
{...props}
/>
));
Card.displayName = "Card";
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -23,11 +23,11 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLParagraphElement,
@@ -35,16 +35,11 @@ const CardTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
>
{props.children}
</h3>
/>
));
CardTitle.displayName = "CardTitle";
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLParagraphElement,
@@ -52,19 +47,19 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = "CardContent";
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -72,11 +67,11 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(" flex items-center p-6 pt-0", className)}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
CardFooter.displayName = 'CardFooter';
export {
Card,

View File

@@ -0,0 +1,31 @@
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import { cn } from '../utils/cn';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -1,12 +1,13 @@
"use client";
'use client';
import * as React from "react";
import type { DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from 'react';
import { Dialog, DialogContent } from "./dialog";
import { cn } from "./utils/cn";
import { type DialogProps } from '@radix-ui/react-dialog';
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { Command as CommandPrimitive } from 'cmdk';
import { cn } from '../utils/cn';
import { Dialog, DialogContent } from './dialog';
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -15,7 +16,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
className,
)}
{...props}
@@ -28,7 +29,7 @@ type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-2xl">
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
@@ -41,13 +42,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
// eslint-disable-next-line react/no-unknown-property
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"placeholder:text-foreground-muted flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
@@ -63,7 +63,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...props}
/>
));
@@ -90,7 +90,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className,
)}
{...props}
@@ -105,7 +105,7 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
));
@@ -118,7 +118,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-[selected='true']:bg-accent aria-[selected='true']:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
className,
)}
{...props}
@@ -134,14 +134,14 @@ const CommandShortcut = ({
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
'ml-auto text-xs tracking-widest text-muted-foreground',
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
CommandShortcut.displayName = 'CommandShortcut';
export {
Command,

View File

@@ -1,11 +1,11 @@
"use client";
'use client';
import type { ColumnDef } from "@tanstack/react-table";
import type { ColumnDef } from '@tanstack/react-table';
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
} from '@tanstack/react-table';
import {
Table,
@@ -14,7 +14,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from "./table";
} from './table';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -57,7 +57,7 @@ export function DataTable<TData, TValue>({
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>

View File

@@ -1,10 +1,11 @@
"use client";
'use client';
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '../utils/cn';
const Dialog = DialogPrimitive.Root;
@@ -21,7 +22,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
@@ -38,14 +39,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
@@ -59,13 +60,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
'flex flex-col space-y-1.5 text-center sm:text-left',
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({
className,
@@ -73,13 +74,13 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -88,7 +89,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
'text-lg font-semibold leading-none tracking-tight',
className,
)}
{...props}
@@ -102,7 +103,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
@@ -112,8 +113,8 @@ export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,

View File

@@ -1,10 +1,15 @@
"use client";
'use client';
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from '@radix-ui/react-icons';
import { cn } from '../utils/cn';
const DropdownMenu = DropdownMenuPrimitive.Root;
@@ -27,14 +32,14 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
@@ -47,7 +52,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"text-on-popover z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
@@ -65,7 +70,8 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
@@ -83,8 +89,8 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className,
)}
{...props}
@@ -99,7 +105,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
checked={checked}
@@ -107,7 +113,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@@ -123,14 +129,14 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@@ -147,8 +153,8 @@ const DropdownMenuLabel = React.forwardRef<
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className,
)}
{...props}
@@ -162,7 +168,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
@@ -174,12 +180,12 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,

View File

@@ -1,22 +1,21 @@
"use client";
import * as React from 'react';
import * as React from "react";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
import { Controller, FormProvider, useFormContext } from "react-hook-form";
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
import { Controller, FormProvider, useFormContext } from 'react-hook-form';
import { Label } from "./label";
import { cn } from "./utils/cn";
import { cn } from '../utils/cn';
import { Label } from './label';
const Form = FormProvider;
interface FormFieldContextValue<
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
> = {
name: TName;
}
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
@@ -43,7 +42,7 @@ const useFormField = () => {
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
@@ -58,9 +57,9 @@ const useFormField = () => {
};
};
interface FormItemContextValue {
type FormItemContextValue = {
id: string;
}
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
@@ -74,11 +73,11 @@ const FormItem = React.forwardRef<
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -89,13 +88,13 @@ const FormLabel = React.forwardRef<
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
className={cn(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
@@ -118,7 +117,7 @@ const FormControl = React.forwardRef<
/>
);
});
FormControl.displayName = "FormControl";
FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef<
HTMLParagraphElement,
@@ -130,12 +129,12 @@ const FormDescription = React.forwardRef<
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-[0.8rem] text-muted-foreground', className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<
HTMLParagraphElement,
@@ -152,14 +151,14 @@ const FormMessage = React.forwardRef<
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
FormMessage.displayName = 'FormMessage';
export {
useFormField,

View File

@@ -0,0 +1,82 @@
import { cn } from '../utils/cn';
type Level = 1 | 2 | 3 | 4 | 5 | 6;
export function Heading({
level,
children,
className,
}: React.PropsWithChildren<{ level?: Level; className?: string }>) {
switch (level) {
case 1:
return (
<h1
className={cn(
`font-heading scroll-m-20 text-4xl font-bold tracking-tight dark:text-white`,
className,
)}
>
{children}
</h1>
);
case 2:
return (
<h2
className={cn(
`font-heading scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0`,
className,
)}
>
{children}
</h2>
);
case 3:
return (
<h3
className={cn(
'font-heading scroll-m-20' +
' text-2xl font-semibold tracking-tight',
className,
)}
>
{children}
</h3>
);
case 4:
return (
<h4
className={cn(
'font-heading scroll-m-20 text-xl font-semibold tracking-tight',
className,
)}
>
{children}
</h4>
);
case 5:
return (
<h5
className={cn(
'font-heading scroll-m-20 text-lg font-medium',
className,
)}
>
{children}
</h5>
);
case 6:
return (
<h6
className={cn(
'font-heading scroll-m-20 text-base' + ' font-medium',
className,
)}
>
{children}
</h6>
);
default:
return <Heading level={1}>{children}</Heading>;
}
}

View File

@@ -1,5 +1,5 @@
import * as Lucide from "lucide-react";
import type { LucideProps } from "lucide-react";
import * as Lucide from 'lucide-react';
import type { LucideProps } from 'lucide-react';
export type Icon = (props: LucideProps) => JSX.Element;

View File

@@ -0,0 +1 @@
export { cn } from '../utils/cn';

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { cn } from '../utils/cn';
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = 'Input';
export { Input };

View File

@@ -1,14 +1,14 @@
"use client";
'use client';
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as LabelPrimitive from '@radix-ui/react-label';
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from '../utils/cn';
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
);
const Label = React.forwardRef<

View File

@@ -0,0 +1,131 @@
'use client';
import * as React from 'react';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { cva } from 'class-variance-authority';
import { cn } from '../utils/cn';
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
'relative z-10 flex max-w-max flex-1 items-center justify-center',
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
'group flex flex-1 list-none items-center justify-center space-x-1',
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50',
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDownIcon
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ',
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn('absolute left-0 top-full flex justify-center')}>
<NavigationMenuPrimitive.Viewport
className={cn(
'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]',
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@@ -0,0 +1,34 @@
'use client';
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../utils/cn';
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -0,0 +1,45 @@
'use client';
import * as React from 'react';
import { CheckIcon } from '@radix-ui/react-icons';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { cn } from '../utils/cn';
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn('grid gap-2', className)}
{...props}
ref={ref}
/>
);
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };

View File

@@ -1,9 +1,10 @@
"use client";
'use client';
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '../utils/cn';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -11,7 +12,7 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
@@ -26,16 +27,16 @@ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 border-t border-t-transparent p-[1px]',
className,
)}
{...props}

View File

@@ -0,0 +1,165 @@
'use client';
import * as React from 'react';
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@radix-ui/react-icons';
import * as SelectPrimitive from '@radix-ui/react-select';
import { cn } from '../utils/cn';
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -1,12 +1,13 @@
"use client";
'use client';
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import { cn } from '../utils/cn';
const Sheet = SheetPrimitive.Root;
@@ -22,7 +23,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
@@ -32,20 +33,20 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: "right",
side: 'right',
},
},
);
@@ -57,7 +58,7 @@ interface SheetContentProps
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
@@ -81,13 +82,13 @@ const SheetHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
'flex flex-col space-y-2 text-center sm:text-left',
className,
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
@@ -95,13 +96,13 @@ const SheetFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@@ -109,7 +110,7 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
@@ -121,7 +122,7 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));

View File

@@ -0,0 +1,31 @@
'use client';
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
);
};
export { Toaster };

View File

@@ -1,28 +1,28 @@
import * as React from "react";
import * as React from 'react';
import { cn } from "./utils/cn";
import { cn } from '../utils/cn';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = "TableHeader";
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
@@ -30,39 +30,41 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement> & { disabled?: boolean }
>(({ className, disabled, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
disabled && "pointer-events-none text-muted-foreground opacity-80",
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className,
)}
{...props}
/>
));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -71,13 +73,13 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -85,11 +87,14 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn(
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
));
TableCell.displayName = "TableCell";
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
@@ -97,11 +102,11 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
TableCaption.displayName = 'TableCaption';
export {
Table,

View File

@@ -1,9 +1,10 @@
"use client";
'use client';
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from 'react';
import { cn } from "./utils/cn";
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../utils/cn';
const Tabs = TabsPrimitive.Root;
@@ -14,7 +15,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className,
)}
{...props}
@@ -29,7 +30,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className,
)}
{...props}
@@ -44,7 +45,7 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
{...props}

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import { cn } from '../utils/cn';
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = 'Textarea';
export { Textarea };

View File

@@ -0,0 +1,31 @@
'use client';
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '../utils/cn';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -1,7 +1,7 @@
// Inspired by react-hot-toast library
import * as React from "react";
import * as React from 'react';
import type { ToastActionElement, ToastProps } from "./toast";
import type { ToastActionElement, ToastProps } from './toast';
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
@@ -14,10 +14,10 @@ type ToasterToast = ToastProps & {
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const;
let count = 0;
@@ -31,20 +31,20 @@ type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
type: ActionType['ADD_TOAST'];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
type: ActionType['UPDATE_TOAST'];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
type: ActionType['DISMISS_TOAST'];
toastId?: ToasterToast['id'];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
type: ActionType['REMOVE_TOAST'];
toastId?: ToasterToast['id'];
};
interface State {
@@ -61,7 +61,7 @@ const addToRemoveQueue = (toastId: string) => {
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
type: 'REMOVE_TOAST',
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
@@ -71,13 +71,13 @@ const addToRemoveQueue = (toastId: string) => {
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
case 'ADD_TOAST':
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
case 'UPDATE_TOAST':
return {
...state,
toasts: state.toasts.map((t) =>
@@ -85,7 +85,7 @@ export const reducer = (state: State, action: Action): State => {
),
};
case "DISMISS_TOAST": {
case 'DISMISS_TOAST': {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
@@ -110,7 +110,7 @@ export const reducer = (state: State, action: Action): State => {
),
};
}
case "REMOVE_TOAST":
case 'REMOVE_TOAST':
if (action.toastId === undefined) {
return {
...state,
@@ -135,20 +135,20 @@ function dispatch(action: Action) {
});
}
type Toast = Omit<ToasterToast, "id">;
type Toast = Omit<ToasterToast, 'id'>;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
type: 'UPDATE_TOAST',
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
dispatch({
type: "ADD_TOAST",
type: 'ADD_TOAST',
toast: {
...props,
id,
@@ -182,7 +182,7 @@ function useToast() {
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
};
}

View File

@@ -1,128 +0,0 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "./utils/cn";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
{
variants: {
variant: {
default: "bg-background border",
destructive:
"group destructive border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-destructive/30 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,35 +0,0 @@
"use client";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "./toast";
import { useToast } from "./use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -1,6 +1,6 @@
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));

View File

@@ -0,0 +1 @@
export * from './cn';

View File

@@ -2,9 +2,9 @@
* This file is not used for any compilation purpose, it is only used
* for Tailwind Intellisense & Autocompletion in the source files
*/
import type { Config } from "tailwindcss";
import type { Config } from 'tailwindcss';
import baseConfig from "@acme/tailwind-config";
import baseConfig from '@kit/tailwind-config';
export default {
content: baseConfig.content,

View File

@@ -1,5 +1,5 @@
{
"extends": "@acme/tsconfig/base.json",
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},