Cleanup
This commit is contained in:
@@ -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": {
|
||||
"*": {
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1 +0,0 @@
|
||||
export { cn } from "./utils/cn";
|
||||
@@ -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 };
|
||||
11
packages/ui/src/makerkit/context/sidebar.context.ts
Normal file
11
packages/ui/src/makerkit/context/sidebar.context.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
const SidebarContext = createContext<{
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
}>({
|
||||
collapsed: false,
|
||||
setCollapsed: (_) => _,
|
||||
});
|
||||
|
||||
export { SidebarContext };
|
||||
264
packages/ui/src/makerkit/data-table.tsx
Normal file
264
packages/ui/src/makerkit/data-table.tsx
Normal 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],
|
||||
);
|
||||
}
|
||||
5
packages/ui/src/makerkit/divider.tsx
Normal file
5
packages/ui/src/makerkit/divider.tsx
Normal 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)} />;
|
||||
}
|
||||
24
packages/ui/src/makerkit/error-boundary.tsx
Normal file
24
packages/ui/src/makerkit/error-boundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
26
packages/ui/src/makerkit/global-loader.tsx
Normal file
26
packages/ui/src/makerkit/global-loader.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
packages/ui/src/makerkit/hooks/use-sidebar-state.ts
Normal file
26
packages/ui/src/makerkit/hooks/use-sidebar-state.ts
Normal 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;
|
||||
29
packages/ui/src/makerkit/if.tsx
Normal file
29
packages/ui/src/makerkit/if.tsx
Normal 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]);
|
||||
}
|
||||
202
packages/ui/src/makerkit/image-upload-input.tsx
Normal file
202
packages/ui/src/makerkit/image-upload-input.tsx
Normal 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;
|
||||
113
packages/ui/src/makerkit/image-uploader.tsx
Normal file
113
packages/ui/src/makerkit/image-uploader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
75
packages/ui/src/makerkit/is-route-active.ts
Normal file
75
packages/ui/src/makerkit/is-route-active.ts
Normal 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;
|
||||
}
|
||||
80
packages/ui/src/makerkit/language-dropdown-switcher.tsx
Normal file
80
packages/ui/src/makerkit/language-dropdown-switcher.tsx
Normal 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;
|
||||
62
packages/ui/src/makerkit/lazy-render.tsx
Normal file
62
packages/ui/src/makerkit/lazy-render.tsx
Normal 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>;
|
||||
}
|
||||
31
packages/ui/src/makerkit/loading-overlay.tsx
Normal file
31
packages/ui/src/makerkit/loading-overlay.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
82
packages/ui/src/makerkit/mdx/mdx-components.tsx
Normal file
82
packages/ui/src/makerkit/mdx/mdx-components.tsx
Normal 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;
|
||||
97
packages/ui/src/makerkit/mdx/mdx-renderer.module.css
Normal file
97
packages/ui/src/makerkit/mdx/mdx-renderer.module.css
Normal 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;
|
||||
}
|
||||
19
packages/ui/src/makerkit/mdx/mdx-renderer.tsx
Normal file
19
packages/ui/src/makerkit/mdx/mdx-renderer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
72
packages/ui/src/makerkit/mobile-navigation-dropdown.tsx
Normal file
72
packages/ui/src/makerkit/mobile-navigation-dropdown.tsx
Normal 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;
|
||||
73
packages/ui/src/makerkit/mobile-navigation-menu.tsx
Normal file
73
packages/ui/src/makerkit/mobile-navigation-menu.tsx
Normal 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;
|
||||
15
packages/ui/src/makerkit/navigation/navigation-container.tsx
Normal file
15
packages/ui/src/makerkit/navigation/navigation-container.tsx
Normal 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;
|
||||
125
packages/ui/src/makerkit/navigation/navigation-item.tsx
Normal file
125
packages/ui/src/makerkit/navigation/navigation-item.tsx
Normal 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: ``,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { NavigationMenuProps } from './navigation-menu';
|
||||
|
||||
export const NavigationMenuContext = createContext<NavigationMenuProps>({});
|
||||
49
packages/ui/src/makerkit/navigation/navigation-menu.tsx
Normal file
49
packages/ui/src/makerkit/navigation/navigation-menu.tsx
Normal 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`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
143
packages/ui/src/makerkit/otp-input.tsx
Normal file
143
packages/ui/src/makerkit/otp-input.tsx
Normal 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);
|
||||
}
|
||||
73
packages/ui/src/makerkit/page.tsx
Normal file
73
packages/ui/src/makerkit/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
307
packages/ui/src/makerkit/pricing-table.tsx
Normal file
307
packages/ui/src/makerkit/pricing-table.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
packages/ui/src/makerkit/profile-avatar.tsx
Normal file
38
packages/ui/src/makerkit/profile-avatar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
packages/ui/src/makerkit/sidebar-schema.ts
Normal file
30
packages/ui/src/makerkit/sidebar-schema.ts
Normal 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),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
});
|
||||
264
packages/ui/src/makerkit/sidebar.tsx
Normal file
264
packages/ui/src/makerkit/sidebar.tsx
Normal 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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
packages/ui/src/makerkit/spinner.tsx
Normal file
34
packages/ui/src/makerkit/spinner.tsx
Normal 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;
|
||||
168
packages/ui/src/makerkit/stepper.tsx
Normal file
168
packages/ui/src/makerkit/stepper.tsx
Normal 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} />,
|
||||
};
|
||||
}
|
||||
38
packages/ui/src/makerkit/top-loading-bar-indicator.tsx
Normal file
38
packages/ui/src/makerkit/top-loading-bar-indicator.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
packages/ui/src/makerkit/trans.tsx
Normal file
5
packages/ui/src/makerkit/trans.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Trans as TransComponent } from 'react-i18next/TransWithoutContext';
|
||||
|
||||
export function Trans(props: React.ComponentProps<typeof TransComponent>) {
|
||||
return <TransComponent {...props} />;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
143
packages/ui/src/shadcn/alert-dialog.tsx
Normal file
143
packages/ui/src/shadcn/alert-dialog.tsx
Normal 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,
|
||||
};
|
||||
65
packages/ui/src/shadcn/alert.tsx
Normal file
65
packages/ui/src/shadcn/alert.tsx
Normal 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 };
|
||||
@@ -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}
|
||||
42
packages/ui/src/shadcn/badge.tsx
Normal file
42
packages/ui/src/shadcn/badge.tsx
Normal 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 };
|
||||
59
packages/ui/src/shadcn/button.tsx
Normal file
59
packages/ui/src/shadcn/button.tsx
Normal 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 };
|
||||
70
packages/ui/src/shadcn/calendar.tsx
Normal file
70
packages/ui/src/shadcn/calendar.tsx
Normal 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 };
|
||||
@@ -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,
|
||||
31
packages/ui/src/shadcn/checkbox.tsx
Normal file
31
packages/ui/src/shadcn/checkbox.tsx
Normal 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 };
|
||||
@@ -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,
|
||||
@@ -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}>
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
82
packages/ui/src/shadcn/heading.tsx
Normal file
82
packages/ui/src/shadcn/heading.tsx
Normal 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>;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
1
packages/ui/src/shadcn/index.ts
Normal file
1
packages/ui/src/shadcn/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { cn } from '../utils/cn';
|
||||
24
packages/ui/src/shadcn/input.tsx
Normal file
24
packages/ui/src/shadcn/input.tsx
Normal 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 };
|
||||
@@ -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<
|
||||
131
packages/ui/src/shadcn/navigation-menu.tsx
Normal file
131
packages/ui/src/shadcn/navigation-menu.tsx
Normal 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,
|
||||
};
|
||||
34
packages/ui/src/shadcn/popover.tsx
Normal file
34
packages/ui/src/shadcn/popover.tsx
Normal 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 };
|
||||
45
packages/ui/src/shadcn/radio-group.tsx
Normal file
45
packages/ui/src/shadcn/radio-group.tsx
Normal 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 };
|
||||
@@ -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}
|
||||
165
packages/ui/src/shadcn/select.tsx
Normal file
165
packages/ui/src/shadcn/select.tsx
Normal 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,
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
31
packages/ui/src/shadcn/sonner.tsx
Normal file
31
packages/ui/src/shadcn/sonner.tsx
Normal 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 };
|
||||
@@ -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,
|
||||
@@ -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}
|
||||
23
packages/ui/src/shadcn/textarea.tsx
Normal file
23
packages/ui/src/shadcn/textarea.tsx
Normal 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 };
|
||||
31
packages/ui/src/shadcn/tooltip.tsx
Normal file
31
packages/ui/src/shadcn/tooltip.tsx
Normal 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 };
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
1
packages/ui/src/utils/index.ts
Normal file
1
packages/ui/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './cn';
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@acme/tsconfig/base.json",
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user