updates
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState } from "react"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { Settings2, Plus, ArrowLeft } from "lucide-react"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
||||
import { DataTable } from "@/shared/data-view/table-view"
|
||||
import { createActionsColumn } from "@/shared/data-view/table-view"
|
||||
import { useCrudDialog, type CrudDialogClient, type UseCrudDialogOptions } from "./use-crud-dialog"
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type CrudDialogProps<TClient extends CrudDialogClient> = UseCrudDialogOptions<TClient> & {
|
||||
/** Dialog title shown in the header */
|
||||
title: string
|
||||
/** Table columns (receives `openEdit` and `handleDelete` helpers) */
|
||||
columns: (helpers: {
|
||||
openEdit: (row: any) => void
|
||||
handleDelete: (row: any) => Promise<void>
|
||||
}) => ColumnDef<any>[]
|
||||
/** Render the create/edit form */
|
||||
renderForm: (props: {
|
||||
resourceId: string | null
|
||||
initialData: any
|
||||
onSuccess: () => void
|
||||
}) => React.ReactNode
|
||||
/** Optional trigger button; defaults to a settings icon button */
|
||||
trigger?: React.ReactNode
|
||||
/** Custom trigger class */
|
||||
triggerClassName?: string
|
||||
}
|
||||
|
||||
// ── Component ──
|
||||
|
||||
export function CrudDialog<TClient extends CrudDialogClient>({
|
||||
title,
|
||||
columns: columnsFn,
|
||||
renderForm,
|
||||
trigger,
|
||||
triggerClassName,
|
||||
...hookOptions
|
||||
}: CrudDialogProps<TClient>) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const crud = useCrudDialog(hookOptions)
|
||||
|
||||
const columns = columnsFn({
|
||||
openEdit: crud.openEdit,
|
||||
handleDelete: crud.handleDelete,
|
||||
})
|
||||
|
||||
// Add actions column
|
||||
const allColumns: ColumnDef<any>[] = [
|
||||
...columns,
|
||||
createActionsColumn({
|
||||
onEdit: crud.openEdit,
|
||||
onDelete: crud.handleDelete,
|
||||
}),
|
||||
]
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
crud.closeForm()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{trigger ? (
|
||||
<div onClick={() => setIsOpen(true)}>{trigger}</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={triggerClassName ?? "h-5 w-5"}
|
||||
onClick={() => setIsOpen(true)}
|
||||
title={`Manage ${title}`}
|
||||
>
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={(v) => { if (!v) handleClose() }}>
|
||||
<DialogContent className="min-w-2xl max-w-3xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
{crud.isFormOpen && (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
onClick={crud.closeForm}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
{crud.isFormOpen
|
||||
? (crud.editingId ? `Edit ${title}` : `New ${title}`)
|
||||
: title}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[75vh]">
|
||||
{crud.isFormOpen ? (
|
||||
<div className="px-1">
|
||||
{renderForm({
|
||||
resourceId: crud.editingId,
|
||||
initialData: crud.editingItem,
|
||||
onSuccess: crud.handleFormSuccess,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 px-1">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={crud.openCreate}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add {title}
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={allColumns}
|
||||
data={crud.items}
|
||||
pagination={crud.pagination}
|
||||
sorting={crud.sorting}
|
||||
onChange={crud.handleChange}
|
||||
isLoading={crud.isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CrudDialog, type CrudDialogProps } from "./crud-dialog"
|
||||
export { useCrudDialog, type CrudDialogClient, type UseCrudDialogOptions } from "./use-crud-dialog"
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import type { DataViewChangeEvent, DataViewPaginationState, DataViewSorting } from "@/shared/data-view/table-view"
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type CrudDialogClient = {
|
||||
list(query?: any): Promise<any>
|
||||
create(payload: any): Promise<any>
|
||||
update(id: string, payload: any): Promise<any>
|
||||
destroy(id: string): Promise<any>
|
||||
}
|
||||
|
||||
export type UseCrudDialogOptions<TClient extends CrudDialogClient> = {
|
||||
queryKey: string[]
|
||||
getClient: () => TClient
|
||||
resourceLabel?: string
|
||||
}
|
||||
|
||||
// ── Hook ──
|
||||
|
||||
export function useCrudDialog<TClient extends CrudDialogClient>({
|
||||
queryKey,
|
||||
getClient,
|
||||
resourceLabel = "item",
|
||||
}: UseCrudDialogOptions<TClient>) {
|
||||
const client = getClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// ── Local pagination state (no URL pollution) ──
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [sortBy, setSortBy] = useState<string | null>(null)
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null)
|
||||
|
||||
// ── Form dialog state ──
|
||||
const [isFormOpen, setIsFormOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [editingItem, setEditingItem] = useState<any>(null)
|
||||
|
||||
const fullQueryKey = [...queryKey, { page, pageSize, sortBy, sortOrder }]
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: fullQueryKey,
|
||||
queryFn: () => {
|
||||
const params: Record<string, unknown> = { page, per_page: pageSize }
|
||||
if (sortBy) params.sort_by = sortBy
|
||||
if (sortOrder) params.sort_order = sortOrder
|
||||
return client.list(params)
|
||||
},
|
||||
})
|
||||
|
||||
const responseData = (data as any)?.data ?? []
|
||||
const items = Array.isArray(responseData) ? responseData : []
|
||||
const meta = (data as any)?.meta
|
||||
|
||||
const pagination: DataViewPaginationState = {
|
||||
page,
|
||||
pageSize,
|
||||
pageCount: meta?.last_page ?? 1,
|
||||
total: meta?.total ?? 0,
|
||||
}
|
||||
|
||||
const sorting: DataViewSorting = sortBy
|
||||
? [{ id: sortBy, desc: sortOrder === "desc" }]
|
||||
: []
|
||||
|
||||
const handleChange = (event: DataViewChangeEvent) => {
|
||||
switch (event.type) {
|
||||
case "pagination":
|
||||
setPage(event.pagination.page)
|
||||
setPageSize(event.pagination.pageSize)
|
||||
break
|
||||
case "sorting": {
|
||||
const sort = event.sorting[0]
|
||||
setSortBy(sort?.id ?? null)
|
||||
setSortOrder(sort ? (sort.desc ? "desc" : "asc") : null)
|
||||
setPage(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const invalidateQuery = () => {
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
}
|
||||
|
||||
const { mutateAsync: deleteItem } = useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
const promise = client.destroy(id)
|
||||
toast.promise(promise, {
|
||||
loading: `Deleting ${resourceLabel}...`,
|
||||
success: `${resourceLabel} deleted`,
|
||||
error: `Failed to delete ${resourceLabel}`,
|
||||
})
|
||||
return promise
|
||||
},
|
||||
onSuccess: invalidateQuery,
|
||||
})
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingId(null)
|
||||
setEditingItem(null)
|
||||
setIsFormOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (row: any) => {
|
||||
setEditingId(String(row.id))
|
||||
setEditingItem(row)
|
||||
setIsFormOpen(true)
|
||||
}
|
||||
|
||||
const closeForm = () => {
|
||||
setIsFormOpen(false)
|
||||
setEditingId(null)
|
||||
setEditingItem(null)
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
const confirmed = await confirm({
|
||||
title: `Delete this ${resourceLabel}?`,
|
||||
description: "This action cannot be undone.",
|
||||
confirmLabel: "Delete",
|
||||
variant: "destructive",
|
||||
})
|
||||
if (confirmed) await deleteItem(String(row.id))
|
||||
}
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
invalidateQuery()
|
||||
closeForm()
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
pagination,
|
||||
sorting,
|
||||
handleChange,
|
||||
isFormOpen,
|
||||
editingId,
|
||||
editingItem,
|
||||
openCreate,
|
||||
openEdit,
|
||||
closeForm,
|
||||
handleDelete,
|
||||
handleFormSuccess,
|
||||
invalidateQuery,
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Button } from '@/shared/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/components/ui/dialog'
|
||||
import { ScrollArea } from '@/shared/components/ui/scroll-area'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { cn } from '../lib/utils'
|
||||
|
||||
export const formDialogParams = {
|
||||
dialog: parseAsBoolean.withDefault(false),
|
||||
@@ -61,23 +62,29 @@ export default function FormDialog(props: {
|
||||
children: (resourceId: string | null) => React.ReactNode
|
||||
title: string
|
||||
paramKey?: string
|
||||
classNames?: {
|
||||
trigger?: string
|
||||
dialogContent?: string
|
||||
scrollArea?: string
|
||||
}
|
||||
|
||||
}) {
|
||||
const { isOpen, resourceId, open, close } = useFormDialog(props.paramKey)
|
||||
const { isOpen, resourceId, open, close } = useFormDialog(props.paramKey)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size='lg' onClick={() => open()}>
|
||||
<Button size='sm' className={cn(props.classNames?.trigger)} onClick={() => open()}>
|
||||
<Plus />
|
||||
{props.title}
|
||||
</Button>
|
||||
<Dialog open={isOpen} onOpenChange={(v) => { if (!v) close() }}>
|
||||
<DialogContent className='min-w-xl'>
|
||||
<DialogContent className={`min-w-xl ${cn(props.classNames?.dialogContent)}`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='text-2xl font-bold'>
|
||||
{props.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className='max-h-[80vh] px-4'>
|
||||
<ScrollArea className={`max-h-[80vh] px-4 ${cn(props.classNames?.scrollArea)}`}>
|
||||
{props.children(resourceId)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
|
||||
@@ -54,6 +54,12 @@ export function AsyncSelectField<TOption = AsyncOption>({
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
disabled={disabled}
|
||||
isItemEqualToValue={(item, val) => {
|
||||
if (item !== null && typeof item === "object" && "value" in (item as object)) {
|
||||
return (item as any).value === (val as any)?.value
|
||||
}
|
||||
return item === val
|
||||
}}
|
||||
onInputValueChange={(val, { reason }) => {
|
||||
if (reason === "input-change") {
|
||||
onInputValueChange?.(val)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useFormContext, type FieldValues, type FieldPath } from "react-hook-form"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { RefreshCw } from "lucide-react"
|
||||
|
||||
import { useAuthApi } from "@/shared/useApi"
|
||||
import { AUTO_GENERATE_ROUTES } from "@garage/api"
|
||||
import { FieldShell } from "../field-shell"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
|
||||
type RhfAutoGenerateFieldProps<
|
||||
TValues extends FieldValues,
|
||||
TName extends FieldPath<TValues>,
|
||||
> = {
|
||||
name: TName
|
||||
label?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
table: string
|
||||
/** When true, fetches the next code immediately on mount */
|
||||
autoFetch?: boolean
|
||||
}
|
||||
|
||||
export function RhfAutoGenerateField<
|
||||
TValues extends FieldValues,
|
||||
TName extends FieldPath<TValues>,
|
||||
>({
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
disabled,
|
||||
placeholder,
|
||||
table,
|
||||
autoFetch = false,
|
||||
}: RhfAutoGenerateFieldProps<TValues, TName>) {
|
||||
const api = useAuthApi()
|
||||
const { setValue, watch, formState: { errors } } = useFormContext<TValues>()
|
||||
const value = watch(name)
|
||||
const error = errors[name]
|
||||
|
||||
const [enabled, setEnabled] = useState(autoFetch)
|
||||
|
||||
const { isFetching } = useQuery({
|
||||
queryKey: [AUTO_GENERATE_ROUTES.BY_TABLE, table],
|
||||
queryFn: async () => {
|
||||
const res = await api.autoGenerate.generate(table)
|
||||
const generated = (res as any)?.data
|
||||
if (generated) {
|
||||
setValue(name, generated as any, { shouldValidate: true })
|
||||
}
|
||||
return res
|
||||
},
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
|
||||
const handleGenerate = () => {
|
||||
setEnabled(false)
|
||||
// Reset and re-enable to trigger a fresh fetch
|
||||
setTimeout(() => setEnabled(true), 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldShell
|
||||
label={label}
|
||||
error={error?.message as string | undefined}
|
||||
description={description}
|
||||
required={required}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={value ?? ""}
|
||||
onChange={(e) => setValue(name, e.target.value as any, { shouldValidate: true })}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
aria-invalid={!!error || undefined}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={disabled || isFetching}
|
||||
onClick={handleGenerate}
|
||||
title="Auto-generate"
|
||||
>
|
||||
<RefreshCw className={isFetching ? "animate-spin" : ""} />
|
||||
</Button>
|
||||
</div>
|
||||
</FieldShell>
|
||||
)
|
||||
}
|
||||
@@ -39,3 +39,4 @@ export { RhfAsyncSelectField, RhfAsyncMultiSelectField, type InlineCreateFormPro
|
||||
export { SimpleTitleForm, type SimpleTitleFormProps } from "./fields/simple-title-form"
|
||||
export { RhfDateField } from "./fields/rhf-date-field"
|
||||
export { RhfTimeField } from "./fields/rhf-time-field"
|
||||
export { RhfAutoGenerateField } from "./fields/rhf-auto-generate-field"
|
||||
|
||||
@@ -137,7 +137,7 @@ function SidebarProvider({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-card",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -168,7 +168,7 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
|
||||
"flex h-full w-(--sidebar-width) flex-col bg-card text-sidebar-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -186,7 +186,7 @@ function Sidebar({
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
className="w-(--sidebar-width) bg-card p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
@@ -241,7 +241,7 @@ function Sidebar({
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
|
||||
className="flex size-full flex-col bg-card group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -288,10 +288,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-card-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||
"in-data-[side=left]:cursor-w-resize rtl:in-data-[side=left]:cursor-e-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize rtl:[[data-side=left][data-state=collapsed]_&]:cursor-w-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize rtl:[[data-side=right][data-state=collapsed]_&]:cursor-e-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 rtl:group-data-[collapsible=offcanvas]:-translate-x-0 group-data-[collapsible=offcanvas]:after:start-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 rtl:group-data-[collapsible=offcanvas]:-translate-x-0 group-data-[collapsible=offcanvas]:after:start-full hover:group-data-[collapsible=offcanvas]:bg-card",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-end-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-start-2",
|
||||
className
|
||||
@@ -358,7 +358,7 @@ function SidebarSeparator({
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
className={cn("mx-2 w-auto bg-card-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -421,7 +421,7 @@ function SidebarGroupAction({
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute top-3.5 end-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"absolute top-3.5 end-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-card-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -466,13 +466,13 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-card-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-card-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-card-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-card-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
default: "hover:bg-card-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-card-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm " ,
|
||||
@@ -553,7 +553,7 @@ function SidebarMenuAction({
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute top-1.5 end-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"absolute top-1.5 end-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-card-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
||||
className
|
||||
@@ -666,7 +666,7 @@ function SidebarMenuSubButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"flex h-7 min-w-0 -translate-x-px rtl:translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"flex h-7 min-w-0 -translate-x-px rtl:translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-card-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-card-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-card-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user