From 8c34de8a6bbc03d0ff1c53b58630bd65449458de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20Bak=C4=B1r?= Date: Sun, 22 Jun 2025 20:15:43 +0300 Subject: [PATCH 1/4] feat: modern styling --- app/globals.css | 83 ++-- components/ui/sidebar.tsx | 401 ++++++++++---------- screens/field-item.tsx | 36 +- screens/form-builder/components/header.tsx | 30 ++ screens/form-builder/components/sidebar.tsx | 73 ++++ screens/form-builder/index.tsx | 122 +++--- screens/form-field-list/index.tsx | 2 +- screens/form-preview/index.tsx | 71 ++-- tailwind.config.js | 1 + 9 files changed, 507 insertions(+), 312 deletions(-) create mode 100644 screens/form-builder/components/header.tsx create mode 100644 screens/form-builder/components/sidebar.tsx diff --git a/app/globals.css b/app/globals.css index a735b92..b4d95ba 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @custom-variant dark (&:is(.dark *)); @plugin "@tailwindcss/typography"; @plugin "tailwindcss-animate"; @@ -37,6 +37,7 @@ --sidebar-accent-foreground: hsl(240 5.9% 10%); --sidebar-border: hsl(220 13% 91%); --sidebar-ring: hsl(217.2 91.2% 59.8%); + --sidebar: hsl(0 0% 98%); } .dark { @@ -72,6 +73,7 @@ --sidebar-accent-foreground: hsl(240 4.8% 95.9%); --sidebar-border: hsl(240 3.7% 15.9%); --sidebar-ring: hsl(217.2 91.2% 59.8%); + --sidebar: hsl(240 5.9% 10%); } @theme { @@ -108,47 +110,71 @@ --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); --color-transparent: transparent; - + --radius-xs: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); - + --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-marquee: marquee var(--duration) linear infinite; --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; - --animate-border-beam: border-beam calc(var(--duration)*1s) infinite linear; - --animate-ripple: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite; - + --animate-border-beam: border-beam calc(var(--duration) * 1s) infinite linear; + --animate-ripple: ripple var(--duration, 2s) ease calc(var(--i, 0) * 0.2s) + infinite; + @keyframes accordion-down { - from { height: 0; } - to { height: var(--radix-accordion-content-height); } + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } } - + @keyframes accordion-up { - from { height: var(--radix-accordion-content-height); } - to { height: 0; } + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } } - + @keyframes marquee { - from { transform: translateX(0); } - to { transform: translateX(calc(-100% - var(--gap))); } + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } } - + @keyframes marquee-vertical { - from { transform: translateY(0); } - to { transform: translateY(calc(-100% - var(--gap))); } + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } } - + @keyframes border-beam { - 100% { offset-distance: 100%; } + 100% { + offset-distance: 100%; + } } - + @keyframes ripple { - 0%, 100% { transform: translate(-50%, -50%) scale(1); } - 50% { transform: translate(-50%, -50%) scale(0.9); } + 0%, + 100% { + transform: translate(-50%, -50%) scale(1); + } + 50% { + transform: translate(-50%, -50%) scale(0.9); + } } } @@ -161,7 +187,7 @@ button { cursor: pointer; } - [class*="border"] { + [class*='border'] { @apply border-border; } } @@ -178,3 +204,14 @@ text-wrap: balance; } } + +@theme inline { + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index ac9a56e..26f592e 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -1,33 +1,39 @@ -"use client" - -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { VariantProps, cva } from "class-variance-authority" -import { PanelLeft } from "lucide-react" - -import { useIsMobile } from "@/hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { Sheet, SheetContent } from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" +'use client' + +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { VariantProps, cva } from 'class-variance-authority' +import { PanelLeft } from 'lucide-react' + +import { useIsMobile } from '@/hooks/use-mobile' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' +import { Skeleton } from '@/components/ui/skeleton' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/ui/tooltip" +} from '@/components/ui/tooltip' -const SIDEBAR_COOKIE_NAME = "sidebar:state" +const SIDEBAR_COOKIE_NAME = 'sidebar_state' const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "12rem" -const SIDEBAR_WIDTH_MOBILE = "12rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" +const SIDEBAR_WIDTH = '16rem' +const SIDEBAR_WIDTH_MOBILE = '18rem' +const SIDEBAR_WIDTH_ICON = '3rem' +const SIDEBAR_KEYBOARD_SHORTCUT = 'b' -type SidebarContext = { - state: "expanded" | "collapsed" +type SidebarContextProps = { + state: 'expanded' | 'collapsed' open: boolean setOpen: (open: boolean) => void openMobile: boolean @@ -36,12 +42,12 @@ type SidebarContext = { toggleSidebar: () => void } -const SidebarContext = React.createContext(null) +const SidebarContext = React.createContext(null) function useSidebar() { const context = React.useContext(SidebarContext) if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") + throw new Error('useSidebar must be used within a SidebarProvider.') } return context @@ -49,7 +55,7 @@ function useSidebar() { const SidebarProvider = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> & { + React.ComponentProps<'div'> & { defaultOpen?: boolean open?: boolean onOpenChange?: (open: boolean) => void @@ -65,7 +71,7 @@ const SidebarProvider = React.forwardRef< children, ...props }, - ref + ref, ) => { const isMobile = useIsMobile() const [openMobile, setOpenMobile] = React.useState(false) @@ -76,18 +82,17 @@ const SidebarProvider = React.forwardRef< const open = openProp ?? _open const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value if (setOpenProp) { - return setOpenProp?.( - typeof value === "function" ? value(open) : value - ) + setOpenProp(openState) + } else { + _setOpen(openState) } - _setOpen(value) - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` }, - [setOpenProp, open] + [setOpenProp, open], ) // Helper to toggle the sidebar. @@ -109,15 +114,15 @@ const SidebarProvider = React.forwardRef< } } - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) }, [toggleSidebar]) // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" + const state = open ? 'expanded' : 'collapsed' - const contextValue = React.useMemo( + const contextValue = React.useMemo( () => ({ state, open, @@ -127,7 +132,15 @@ const SidebarProvider = React.forwardRef< setOpenMobile, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + ], ) return ( @@ -136,14 +149,14 @@ const SidebarProvider = React.forwardRef<
) - } + }, ) -SidebarProvider.displayName = "SidebarProvider" +SidebarProvider.displayName = 'SidebarProvider' const Sidebar = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" + React.ComponentProps<'div'> & { + side?: 'left' | 'right' + variant?: 'sidebar' | 'floating' | 'inset' + collapsible?: 'offcanvas' | 'icon' | 'none' } >( ( { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', className, children, ...props }, - ref + ref, ) => { const { isMobile, state, openMobile, setOpenMobile } = useSidebar() - if (collapsible === "none") { + if (collapsible === 'none') { return (
+ + Sidebar + Displays the mobile sidebar. +
{children}
@@ -216,34 +233,34 @@ const Sidebar = React.forwardRef< return (
{/* This is what handles the sidebar gap on desktop */}
) - } + }, ) -Sidebar.displayName = "Sidebar" +Sidebar.displayName = 'Sidebar' const SidebarTrigger = React.forwardRef< React.ElementRef, @@ -272,7 +289,7 @@ const SidebarTrigger = React.forwardRef< data-sidebar="trigger" variant="ghost" size="icon" - className={cn("h-7 w-7", className)} + className={cn('h-7 w-7', className)} onClick={(event) => { onClick?.(event) toggleSidebar() @@ -284,11 +301,11 @@ const SidebarTrigger = React.forwardRef< ) }) -SidebarTrigger.displayName = "SidebarTrigger" +SidebarTrigger.displayName = 'SidebarTrigger' const SidebarRail = React.forwardRef< HTMLButtonElement, - React.ComponentProps<"button"> + React.ComponentProps<'button'> >(({ className, ...props }, ref) => { const { toggleSidebar } = useSidebar() @@ -301,37 +318,37 @@ const SidebarRail = React.forwardRef< onClick={toggleSidebar} title="Toggle Sidebar" className={cn( - "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", - "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", - "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", - "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", - "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", - "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", - className + 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex', + '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize', + '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize', + 'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar', + '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2', + '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2', + className, )} {...props} /> ) }) -SidebarRail.displayName = "SidebarRail" +SidebarRail.displayName = 'SidebarRail' const SidebarInset = React.forwardRef< HTMLDivElement, - React.ComponentProps<"main"> + React.ComponentProps<'main'> >(({ className, ...props }, ref) => { return (
) }) -SidebarInset.displayName = "SidebarInset" +SidebarInset.displayName = 'SidebarInset' const SidebarInput = React.forwardRef< React.ElementRef, @@ -342,44 +359,44 @@ const SidebarInput = React.forwardRef< ref={ref} data-sidebar="input" className={cn( - "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", - className + 'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring', + className, )} {...props} /> ) }) -SidebarInput.displayName = "SidebarInput" +SidebarInput.displayName = 'SidebarInput' const SidebarHeader = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> + React.ComponentProps<'div'> >(({ className, ...props }, ref) => { return (
) }) -SidebarHeader.displayName = "SidebarHeader" +SidebarHeader.displayName = 'SidebarHeader' const SidebarFooter = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> + React.ComponentProps<'div'> >(({ className, ...props }, ref) => { return (
) }) -SidebarFooter.displayName = "SidebarFooter" +SidebarFooter.displayName = 'SidebarFooter' const SidebarSeparator = React.forwardRef< React.ElementRef, @@ -389,154 +406,154 @@ const SidebarSeparator = React.forwardRef< ) }) -SidebarSeparator.displayName = "SidebarSeparator" +SidebarSeparator.displayName = 'SidebarSeparator' const SidebarContent = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> + React.ComponentProps<'div'> >(({ className, ...props }, ref) => { return (
) }) -SidebarContent.displayName = "SidebarContent" +SidebarContent.displayName = 'SidebarContent' const SidebarGroup = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> + React.ComponentProps<'div'> >(({ className, ...props }, ref) => { return (
) }) -SidebarGroup.displayName = "SidebarGroup" +SidebarGroup.displayName = 'SidebarGroup' const SidebarGroupLabel = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> & { asChild?: boolean } + React.ComponentProps<'div'> & { asChild?: boolean } >(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "div" + const Comp = asChild ? Slot : 'div' return ( svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", - className + 'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', + 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', + className, )} {...props} /> ) }) -SidebarGroupLabel.displayName = "SidebarGroupLabel" +SidebarGroupLabel.displayName = 'SidebarGroupLabel' const SidebarGroupAction = React.forwardRef< HTMLButtonElement, - React.ComponentProps<"button"> & { asChild?: boolean } + React.ComponentProps<'button'> & { asChild?: boolean } >(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( svg]:size-4 [&>svg]:shrink-0", + 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 after:md:hidden", - "group-data-[collapsible=icon]:hidden", - className + 'after:absolute after:-inset-2 after:md:hidden', + 'group-data-[collapsible=icon]:hidden', + className, )} {...props} /> ) }) -SidebarGroupAction.displayName = "SidebarGroupAction" +SidebarGroupAction.displayName = 'SidebarGroupAction' const SidebarGroupContent = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> + React.ComponentProps<'div'> >(({ className, ...props }, ref) => (
)) -SidebarGroupContent.displayName = "SidebarGroupContent" +SidebarGroupContent.displayName = 'SidebarGroupContent' const SidebarMenu = React.forwardRef< HTMLUListElement, - React.ComponentProps<"ul"> + React.ComponentProps<'ul'> >(({ className, ...props }, ref) => (
    )) -SidebarMenu.displayName = "SidebarMenu" +SidebarMenu.displayName = 'SidebarMenu' const SidebarMenuItem = React.forwardRef< HTMLLIElement, - React.ComponentProps<"li"> + React.ComponentProps<'li'> >(({ className, ...props }, ref) => (
  • )) -SidebarMenuItem.displayName = "SidebarMenuItem" +SidebarMenuItem.displayName = 'SidebarMenuItem' const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', { variants: { variant: { - default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', outline: - "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', }, size: { - default: "h-8 text-sm", - sm: "h-7 text-xs", - lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", + default: 'h-8 text-sm', + sm: 'h-7 text-xs', + lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, - } + }, ) const SidebarMenuButton = React.forwardRef< HTMLButtonElement, - React.ComponentProps<"button"> & { + React.ComponentProps<'button'> & { asChild?: boolean isActive?: boolean tooltip?: string | React.ComponentProps @@ -546,15 +563,15 @@ const SidebarMenuButton = React.forwardRef< { asChild = false, isActive = false, - variant = "default", - size = "default", + variant = 'default', + size = 'default', tooltip, className, ...props }, - ref + ref, ) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' const { isMobile, state } = useSidebar() const button = ( @@ -572,7 +589,7 @@ const SidebarMenuButton = React.forwardRef< return button } - if (typeof tooltip === "string") { + if (typeof tooltip === 'string') { tooltip = { children: tooltip, } @@ -584,70 +601,70 @@ const SidebarMenuButton = React.forwardRef<