diff --git a/bun.lockb b/bun.lockb index d446646..baf41ca 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 67b98ef..b9946ba 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,13 @@ "lint": "next lint" }, "dependencies": { + "@chakra-ui/react": "^3.4.0", + "@emotion/react": "^11.14.0", + "next": "15.1.5", + "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "next": "15.1.5" + "react-icons": "^5.4.0" }, "devDependencies": { "typescript": "^5", diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..9d03ad4 --- /dev/null +++ b/src/app/blog/[slug]/page.tsx @@ -0,0 +1,8 @@ +export default async function Post({ params }: { params: { slug: string } }) { + const { slug } = params; + return ( +
+

Post: {slug}

+
+ ); +} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx new file mode 100644 index 0000000..e10b7e9 --- /dev/null +++ b/src/app/blog/page.tsx @@ -0,0 +1,3 @@ +export default function Blog() { + return

Blog

; +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..cd84664 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,74 @@ +"use client" + +import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react" +import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react" +import * as React from "react" + +type ImageProps = React.ImgHTMLAttributes + +export interface AvatarProps extends ChakraAvatar.RootProps { + name?: string + src?: string + srcSet?: string + loading?: ImageProps["loading"] + icon?: React.ReactElement + fallback?: React.ReactNode +} + +export const Avatar = React.forwardRef( + function Avatar(props, ref) { + const { name, src, srcSet, loading, icon, fallback, children, ...rest } = + props + return ( + + + {fallback} + + + {children} + + ) + }, +) + +interface AvatarFallbackProps extends ChakraAvatar.FallbackProps { + name?: string + icon?: React.ReactElement +} + +const AvatarFallback = React.forwardRef( + function AvatarFallback(props, ref) { + const { name, icon, children, ...rest } = props + return ( + + {children} + {name != null && children == null && <>{getInitials(name)}} + {name == null && children == null && ( + {icon} + )} + + ) + }, +) + +function getInitials(name: string) { + const names = name.trim().split(" ") + const firstName = names[0] != null ? names[0] : "" + const lastName = names.length > 1 ? names[names.length - 1] : "" + return firstName && lastName + ? `${firstName.charAt(0)}${lastName.charAt(0)}` + : firstName.charAt(0) +} + +interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {} + +export const AvatarGroup = React.forwardRef( + function AvatarGroup(props, ref) { + const { size, variant, borderless, ...rest } = props + return ( + + + + ) + }, +) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..21d5f4b --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,40 @@ +import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react" +import { + AbsoluteCenter, + Button as ChakraButton, + Span, + Spinner, +} from "@chakra-ui/react" +import * as React from "react" + +interface ButtonLoadingProps { + loading?: boolean + loadingText?: React.ReactNode +} + +export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} + +export const Button = React.forwardRef( + function Button(props, ref) { + const { loading, disabled, loadingText, children, ...rest } = props + return ( + + {loading && !loadingText ? ( + <> + + + + {children} + + ) : loading && loadingText ? ( + <> + + {loadingText} + + ) : ( + children + )} + + ) + }, +) diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..2a27c2f --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,25 @@ +import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" +import * as React from "react" + +export interface CheckboxProps extends ChakraCheckbox.RootProps { + icon?: React.ReactNode + inputProps?: React.InputHTMLAttributes + rootRef?: React.Ref +} + +export const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + const { icon, children, inputProps, rootRef, ...rest } = props + return ( + + + + {icon || } + + {children != null && ( + {children} + )} + + ) + }, +) diff --git a/src/components/ui/close-button.tsx b/src/components/ui/close-button.tsx new file mode 100644 index 0000000..94af488 --- /dev/null +++ b/src/components/ui/close-button.tsx @@ -0,0 +1,17 @@ +import type { ButtonProps } from "@chakra-ui/react" +import { IconButton as ChakraIconButton } from "@chakra-ui/react" +import * as React from "react" +import { LuX } from "react-icons/lu" + +export type CloseButtonProps = ButtonProps + +export const CloseButton = React.forwardRef< + HTMLButtonElement, + CloseButtonProps +>(function CloseButton(props, ref) { + return ( + + {props.children ?? } + + ) +}) diff --git a/src/components/ui/color-mode.tsx b/src/components/ui/color-mode.tsx new file mode 100644 index 0000000..a87f63b --- /dev/null +++ b/src/components/ui/color-mode.tsx @@ -0,0 +1,75 @@ +"use client" + +import type { IconButtonProps } from "@chakra-ui/react" +import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react" +import { ThemeProvider, useTheme } from "next-themes" +import type { ThemeProviderProps } from "next-themes" +import * as React from "react" +import { LuMoon, LuSun } from "react-icons/lu" + +export interface ColorModeProviderProps extends ThemeProviderProps {} + +export function ColorModeProvider(props: ColorModeProviderProps) { + return ( + + ) +} + +export type ColorMode = "light" | "dark" + +export interface UseColorModeReturn { + colorMode: ColorMode + setColorMode: (colorMode: ColorMode) => void + toggleColorMode: () => void +} + +export function useColorMode(): UseColorModeReturn { + const { resolvedTheme, setTheme } = useTheme() + const toggleColorMode = () => { + setTheme(resolvedTheme === "light" ? "dark" : "light") + } + return { + colorMode: resolvedTheme as ColorMode, + setColorMode: setTheme, + toggleColorMode, + } +} + +export function useColorModeValue(light: T, dark: T) { + const { colorMode } = useColorMode() + return colorMode === "dark" ? dark : light +} + +export function ColorModeIcon() { + const { colorMode } = useColorMode() + return colorMode === "dark" ? : +} + +interface ColorModeButtonProps extends Omit {} + +export const ColorModeButton = React.forwardRef< + HTMLButtonElement, + ColorModeButtonProps +>(function ColorModeButton(props, ref) { + const { toggleColorMode } = useColorMode() + return ( + }> + + + + + ) +}) diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..89d68a5 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,62 @@ +import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react" +import { CloseButton } from "./close-button" +import * as React from "react" + +interface DialogContentProps extends ChakraDialog.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + backdrop?: boolean +} + +export const DialogContent = React.forwardRef< + HTMLDivElement, + DialogContentProps +>(function DialogContent(props, ref) { + const { + children, + portalled = true, + portalRef, + backdrop = true, + ...rest + } = props + + return ( + + {backdrop && } + + + {children} + + + + ) +}) + +export const DialogCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDialog.CloseTriggerProps +>(function DialogCloseTrigger(props, ref) { + return ( + + + {props.children} + + + ) +}) + +export const DialogRoot = ChakraDialog.Root +export const DialogFooter = ChakraDialog.Footer +export const DialogHeader = ChakraDialog.Header +export const DialogBody = ChakraDialog.Body +export const DialogBackdrop = ChakraDialog.Backdrop +export const DialogTitle = ChakraDialog.Title +export const DialogDescription = ChakraDialog.Description +export const DialogTrigger = ChakraDialog.Trigger +export const DialogActionTrigger = ChakraDialog.ActionTrigger diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..ccb96c8 --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -0,0 +1,52 @@ +import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react" +import { CloseButton } from "./close-button" +import * as React from "react" + +interface DrawerContentProps extends ChakraDrawer.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + offset?: ChakraDrawer.ContentProps["padding"] +} + +export const DrawerContent = React.forwardRef< + HTMLDivElement, + DrawerContentProps +>(function DrawerContent(props, ref) { + const { children, portalled = true, portalRef, offset, ...rest } = props + return ( + + + + {children} + + + + ) +}) + +export const DrawerCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDrawer.CloseTriggerProps +>(function DrawerCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const DrawerTrigger = ChakraDrawer.Trigger +export const DrawerRoot = ChakraDrawer.Root +export const DrawerFooter = ChakraDrawer.Footer +export const DrawerHeader = ChakraDrawer.Header +export const DrawerBody = ChakraDrawer.Body +export const DrawerBackdrop = ChakraDrawer.Backdrop +export const DrawerDescription = ChakraDrawer.Description +export const DrawerTitle = ChakraDrawer.Title +export const DrawerActionTrigger = ChakraDrawer.ActionTrigger diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx new file mode 100644 index 0000000..dd3b66f --- /dev/null +++ b/src/components/ui/field.tsx @@ -0,0 +1,33 @@ +import { Field as ChakraField } from "@chakra-ui/react" +import * as React from "react" + +export interface FieldProps extends Omit { + label?: React.ReactNode + helperText?: React.ReactNode + errorText?: React.ReactNode + optionalText?: React.ReactNode +} + +export const Field = React.forwardRef( + function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = + props + return ( + + {label && ( + + {label} + + + )} + {children} + {helperText && ( + {helperText} + )} + {errorText && ( + {errorText} + )} + + ) + }, +) diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx new file mode 100644 index 0000000..5d8fb32 --- /dev/null +++ b/src/components/ui/input-group.tsx @@ -0,0 +1,53 @@ +import type { BoxProps, InputElementProps } from "@chakra-ui/react" +import { Group, InputElement } from "@chakra-ui/react" +import * as React from "react" + +export interface InputGroupProps extends BoxProps { + startElementProps?: InputElementProps + endElementProps?: InputElementProps + startElement?: React.ReactNode + endElement?: React.ReactNode + children: React.ReactElement + startOffset?: InputElementProps["paddingStart"] + endOffset?: InputElementProps["paddingEnd"] +} + +export const InputGroup = React.forwardRef( + function InputGroup(props, ref) { + const { + startElement, + startElementProps, + endElement, + endElementProps, + children, + startOffset = "6px", + endOffset = "6px", + ...rest + } = props + + const child = + React.Children.only>(children) + + return ( + + {startElement && ( + + {startElement} + + )} + {React.cloneElement(child, { + ...(startElement && { + ps: `calc(var(--input-height) - ${startOffset})`, + }), + ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), + ...children.props, + })} + {endElement && ( + + {endElement} + + )} + + ) + }, +) diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..3320659 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,59 @@ +import { Popover as ChakraPopover, Portal } from "@chakra-ui/react" +import { CloseButton } from "./close-button" +import * as React from "react" + +interface PopoverContentProps extends ChakraPopover.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const PopoverContent = React.forwardRef< + HTMLDivElement, + PopoverContentProps +>(function PopoverContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) +}) + +export const PopoverArrow = React.forwardRef< + HTMLDivElement, + ChakraPopover.ArrowProps +>(function PopoverArrow(props, ref) { + return ( + + + + ) +}) + +export const PopoverCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPopover.CloseTriggerProps +>(function PopoverCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const PopoverTitle = ChakraPopover.Title +export const PopoverDescription = ChakraPopover.Description +export const PopoverFooter = ChakraPopover.Footer +export const PopoverHeader = ChakraPopover.Header +export const PopoverRoot = ChakraPopover.Root +export const PopoverBody = ChakraPopover.Body +export const PopoverTrigger = ChakraPopover.Trigger diff --git a/src/components/ui/provider.tsx b/src/components/ui/provider.tsx new file mode 100644 index 0000000..fd0331b --- /dev/null +++ b/src/components/ui/provider.tsx @@ -0,0 +1,15 @@ +"use client" + +import { ChakraProvider, defaultSystem } from "@chakra-ui/react" +import { + ColorModeProvider, + type ColorModeProviderProps, +} from "./color-mode" + +export function Provider(props: ColorModeProviderProps) { + return ( + + + + ) +} diff --git a/src/components/ui/radio.tsx b/src/components/ui/radio.tsx new file mode 100644 index 0000000..b3919d0 --- /dev/null +++ b/src/components/ui/radio.tsx @@ -0,0 +1,24 @@ +import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" +import * as React from "react" + +export interface RadioProps extends ChakraRadioGroup.ItemProps { + rootRef?: React.Ref + inputProps?: React.InputHTMLAttributes +} + +export const Radio = React.forwardRef( + function Radio(props, ref) { + const { children, inputProps, rootRef, ...rest } = props + return ( + + + + {children && ( + {children} + )} + + ) + }, +) + +export const RadioGroup = ChakraRadioGroup.Root diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx new file mode 100644 index 0000000..55a7283 --- /dev/null +++ b/src/components/ui/slider.tsx @@ -0,0 +1,82 @@ +import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react" +import * as React from "react" + +export interface SliderProps extends ChakraSlider.RootProps { + marks?: Array + label?: React.ReactNode + showValue?: boolean +} + +export const Slider = React.forwardRef( + function Slider(props, ref) { + const { marks: marksProp, label, showValue, ...rest } = props + const value = props.defaultValue ?? props.value + + const marks = marksProp?.map((mark) => { + if (typeof mark === "number") return { value: mark, label: undefined } + return mark + }) + + const hasMarkLabel = !!marks?.some((mark) => mark.label) + + return ( + + {label && !showValue && ( + {label} + )} + {label && showValue && ( + + {label} + + + )} + + + + + + + + + ) + }, +) + +function SliderThumbs(props: { value?: number[] }) { + const { value } = props + return ( + + {(_, index) => ( + + + + )} + + ) +} + +interface SliderMarksProps { + marks?: Array +} + +const SliderMarks = React.forwardRef( + function SliderMarks(props, ref) { + const { marks } = props + if (!marks?.length) return null + + return ( + + {marks.map((mark, index) => { + const value = typeof mark === "number" ? mark : mark.value + const label = typeof mark === "number" ? undefined : mark.label + return ( + + + {label} + + ) + })} + + ) + }, +) diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..43a8a6c --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,46 @@ +import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react" +import * as React from "react" + +export interface TooltipProps extends ChakraTooltip.RootProps { + showArrow?: boolean + portalled?: boolean + portalRef?: React.RefObject + content: React.ReactNode + contentProps?: ChakraTooltip.ContentProps + disabled?: boolean +} + +export const Tooltip = React.forwardRef( + function Tooltip(props, ref) { + const { + showArrow, + children, + disabled, + portalled = true, + content, + contentProps, + portalRef, + ...rest + } = props + + if (disabled) return children + + return ( + + {children} + + + + {showArrow && ( + + + + )} + {content} + + + + + ) + }, +)