Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
import { cn } from '@/lib/utils'

/**
* Root accordion container that manages the expanded/collapsed state of its items.
*
* Wraps the Base UI `Accordion.Root` primitive with a flex-column layout and
* forwards all native props.
*
* @param props - Props passed through to `AccordionPrimitive.Root`.
* @param props.className - Additional CSS classes merged via `cn`.
*/
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
className={cn('flex w-full flex-col', className)}
{...props}
/>
)
}

/**
* A single collapsible section within an `Accordion`.
*
* Renders a bottom border between sibling items (except the last one).
*
* @param props - Props passed through to `AccordionPrimitive.Item`.
* @param props.className - Additional CSS classes merged via `cn`.
*/
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn('not-last:border-b', className)}
{...props}
/>
)
}

/**
* Clickable header that toggles the visibility of an `AccordionContent` panel.
*
* Displays a chevron icon that rotates based on the expanded state, and
* supports hover, focus-visible, and disabled visual states.
*
* @param props - Props passed through to `AccordionPrimitive.Trigger`.
* @param props.className - Additional CSS classes merged via `cn`.
* @param props.children - Label content rendered inside the trigger button.
*/
function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left font-medium text-sm outline-none transition-all hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
className
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}

/**
* Collapsible panel that reveals the content of an `AccordionItem`.
*
* Animates open/close with height-based transitions and renders children
* inside an inner container with consistent spacing.
*
* @param props - Props passed through to `AccordionPrimitive.Panel`.
* @param props.className - Additional CSS classes merged via `cn`.
* @param props.children - Content displayed when the accordion item is expanded.
*/
function AccordionContent({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className="overflow-hidden text-sm data-closed:animate-accordion-up data-open:animate-accordion-down"
{...props}
>
<div
className={cn(
'h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
className
)}
>
{children}
</div>
</AccordionPrimitive.Panel>
)
}

export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
85 changes: 45 additions & 40 deletions src/features/settings/ui/ToolbarOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ import {
RiUnderline,
} from 'react-icons/ri'
import { useToolbarConfig } from '@/app/providers/toolbar-config-provider'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
Expand Down Expand Up @@ -98,15 +104,10 @@ function SortableToolbarItem({
isDragging,
} = useSortable({ id: item.key })

const style = {
transform: CSS.Transform.toString(transform),
transition,
}

return (
<div
ref={setNodeRef}
style={style}
style={{ transform: CSS.Transform.toString(transform), transition }}
className={cn(
'flex items-center justify-between rounded-md px-3 py-1.5',
isDragging && 'z-50 bg-accent opacity-80 shadow-sm'
Expand Down Expand Up @@ -180,41 +181,45 @@ export function ToolbarOption() {
)

return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between px-3">
<p className="font-medium text-muted-foreground text-xs">
<Accordion defaultValue={[]}>
<AccordionItem value="toolbar">
<AccordionTrigger className="py-2 text-xs">
Formatting Toolbar
</p>
{isCustomized && (
<Button
variant="ghost"
size="sm"
className="h-6 gap-1 px-2 text-xs"
onClick={reset}
</AccordionTrigger>
<AccordionContent>
{isCustomized && (
<div className="flex justify-end px-3">
<Button
variant="ghost"
size="sm"
className="h-6 gap-1 px-2 text-xs"
onClick={reset}
>
<RotateCcw className="h-3 w-3" />
Reset
</Button>
</div>
)}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<RotateCcw className="h-3 w-3" />
Reset
</Button>
)}
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={items.map((i) => i.key)}
strategy={verticalListSortingStrategy}
>
{items.map((item) => (
<SortableToolbarItem
key={item.key}
item={item}
onToggle={toggleVisibility}
/>
))}
</SortableContext>
</DndContext>
</div>
<SortableContext
items={items.map((i) => i.key)}
strategy={verticalListSortingStrategy}
>
{items.map((item) => (
<SortableToolbarItem
key={item.key}
item={item}
onToggle={toggleVisibility}
/>
))}
</SortableContext>
</DndContext>
</AccordionContent>
</AccordionItem>
</Accordion>
)
}
Loading