style: do inputs design modification
This commit is contained in:
BIN
build_log.txt
BIN
build_log.txt
Binary file not shown.
15
package-lock.json
generated
15
package-lock.json
generated
@@ -9895,6 +9895,21 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
|
"version": "16.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
|
||||||
|
"integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Edit, RotateCcw } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
|
||||||
import { usePermissions } from '@/hooks/usePermissions';
|
|
||||||
import type { Client } from '@/types';
|
import type { Client } from '@/types';
|
||||||
|
|
||||||
function formatDate(value?: string | null) {
|
function formatDate(value?: string | null) {
|
||||||
@@ -19,23 +15,9 @@ function formatDate(value?: string | null) {
|
|||||||
}).format(new Date(value));
|
}).format(new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseClientColumnsParams {
|
export function useClientColumns(): ColumnDef<Client>[] {
|
||||||
onEdit: (client: Client) => void;
|
|
||||||
onToggleStatus: (client: Client) => void;
|
|
||||||
pendingClientId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useClientColumns({
|
|
||||||
onEdit,
|
|
||||||
onToggleStatus,
|
|
||||||
pendingClientId,
|
|
||||||
}: UseClientColumnsParams): ColumnDef<Client>[] {
|
|
||||||
const { hasPermission } = usePermissions();
|
|
||||||
const canEdit = hasPermission(PERMISSIONS.CLIENT.UPDATE);
|
|
||||||
const canDelete = hasPermission(PERMISSIONS.CLIENT.DELETE);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const columns: ColumnDef<Client>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Client',
|
header: 'Client',
|
||||||
@@ -75,38 +57,5 @@ export function useClientColumns({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}, []);
|
||||||
if (!canEdit && !canDelete) return columns;
|
|
||||||
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <div className="text-right">Actions</div>,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const client = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
{canEdit ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={() => onEdit(client)}>
|
|
||||||
<Edit className="size-4" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canDelete ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={pendingClientId === client.id}
|
|
||||||
onClick={() => onToggleStatus(client)}
|
|
||||||
>
|
|
||||||
<RotateCcw className="mr-2 size-4" />
|
|
||||||
{client.is_active ? 'Deactivate' : 'Activate'}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingClientId]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
import { Edit, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import type { Client } from '@/types';
|
import type { Client } from '@/types';
|
||||||
|
|
||||||
interface ClientTableProps {
|
interface ClientTableProps {
|
||||||
@@ -18,6 +20,9 @@ interface ClientTableProps {
|
|||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
onSortingChange: (sorting: SortingState) => void;
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
|
onEdit: (client: Client) => void;
|
||||||
|
onToggleStatus: (client: Client) => void;
|
||||||
|
pendingClientId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClientTable({
|
export function ClientTable({
|
||||||
@@ -32,6 +37,9 @@ export function ClientTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onSortingChange,
|
onSortingChange,
|
||||||
|
onEdit,
|
||||||
|
onToggleStatus,
|
||||||
|
pendingClientId,
|
||||||
}: ClientTableProps) {
|
}: ClientTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -41,6 +49,21 @@ export function ClientTable({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
emptyTitle="No clients found."
|
emptyTitle="No clients found."
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit className="size-4" />,
|
||||||
|
permission: PERMISSIONS.CLIENT.UPDATE,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (client) => (client.is_active ? 'Deactivate' : 'Activate'),
|
||||||
|
icon: <RotateCcw className="size-4" />,
|
||||||
|
permission: PERMISSIONS.CLIENT.DELETE,
|
||||||
|
disabled: (client) => pendingClientId === client.id,
|
||||||
|
onClick: onToggleStatus,
|
||||||
|
},
|
||||||
|
]}
|
||||||
pagination={{
|
pagination={{
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -69,11 +69,7 @@ export default function ClientsPage() {
|
|||||||
[updateClientStatus],
|
[updateClientStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = useClientColumns({
|
const columns = useClientColumns();
|
||||||
onEdit: openEdit,
|
|
||||||
onToggleStatus: toggleStatus,
|
|
||||||
pendingClientId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = clientsQuery.data?.total ?? 0;
|
const total = clientsQuery.data?.total ?? 0;
|
||||||
const clients = clientsQuery.data?.items ?? [];
|
const clients = clientsQuery.data?.items ?? [];
|
||||||
@@ -118,6 +114,9 @@ export default function ClientsPage() {
|
|||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
onSortingChange={setSorting}
|
onSortingChange={setSorting}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onToggleStatus={toggleStatus}
|
||||||
|
pendingClientId={pendingClientId}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
|
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
|
||||||
import { Globe, Loader2, Package as PackageIcon } from 'lucide-react';
|
import { Globe, Loader2, Package as PackageIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { FormField } from '@/components/form';
|
import { FormField } from '@/components/form';
|
||||||
@@ -36,7 +36,7 @@ interface PackageDialogProps {
|
|||||||
onProjectChange: (projectId: string) => void;
|
onProjectChange: (projectId: string) => void;
|
||||||
register: UseFormRegister<PackageFormValues>;
|
register: UseFormRegister<PackageFormValues>;
|
||||||
errors: FieldErrors<PackageFormValues>;
|
errors: FieldErrors<PackageFormValues>;
|
||||||
touchedFields: UseFormReturn<PackageFormValues>['formState']['touchedFields'];
|
isSubmitted: boolean;
|
||||||
onSubmit: ComponentProps<'form'>['onSubmit'];
|
onSubmit: ComponentProps<'form'>['onSubmit'];
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
@@ -52,19 +52,15 @@ export function PackageDialog({
|
|||||||
onProjectChange,
|
onProjectChange,
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
}: PackageDialogProps) {
|
}: PackageDialogProps) {
|
||||||
const projectErrorMessage = touchedFields.project_id ? errors.project_id?.message : undefined;
|
const projectErrorMessage = isSubmitted ? errors.project_id?.message : undefined;
|
||||||
const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined;
|
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
|
||||||
const startErrorMessage = touchedFields.chainage_start_km
|
const startErrorMessage = isSubmitted ? errors.chainage_start_km?.message : undefined;
|
||||||
? errors.chainage_start_km?.message
|
const endErrorMessage = isSubmitted ? errors.chainage_end_km?.message : undefined;
|
||||||
: undefined;
|
|
||||||
const endErrorMessage = touchedFields.chainage_end_km
|
|
||||||
? errors.chainage_end_km?.message
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -155,7 +151,12 @@ export function PackageDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSaving || !canSubmit}>
|
<Button type="submit" disabled={isSaving || !canSubmit}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { Edit3, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { Package } from '@/types';
|
import type { Package } from '@/types';
|
||||||
@@ -35,8 +36,19 @@ export function PackageTable({
|
|||||||
title="Packages"
|
title="Packages"
|
||||||
data={packages}
|
data={packages}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onEdit={onEdit}
|
actions={[
|
||||||
onDelete={onDelete}
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit3 className="size-4" />,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: <Trash2 className="size-4" />,
|
||||||
|
className: 'text-destructive',
|
||||||
|
onClick: onDelete,
|
||||||
|
},
|
||||||
|
]}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
emptyTitle="No packages found."
|
emptyTitle="No packages found."
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ export function usePackageForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting, touchedFields },
|
formState: { errors, isSubmitting, isSubmitted },
|
||||||
} = useForm<PackageFormValues>({
|
} = useForm<PackageFormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onTouched',
|
mode: 'onSubmit',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
resolver: zodResolver(packageFormSchema),
|
resolver: zodResolver(packageFormSchema),
|
||||||
});
|
});
|
||||||
@@ -144,7 +144,7 @@ export function usePackageForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
projectId,
|
projectId,
|
||||||
setProjectId,
|
setProjectId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving: isSubmitting || saveMutation.isPending,
|
isSaving: isSubmitting || saveMutation.isPending,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function PackagePage() {
|
|||||||
projectId,
|
projectId,
|
||||||
setProjectId,
|
setProjectId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
} = packageForm;
|
} = packageForm;
|
||||||
@@ -119,7 +119,7 @@ export default function PackagePage() {
|
|||||||
onProjectChange={setProjectId}
|
onProjectChange={setProjectId}
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touchedFields={touchedFields}
|
isSubmitted={isSubmitted}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Edit, RotateCcw } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
|
||||||
import { usePermissions } from '@/hooks/usePermissions';
|
|
||||||
import type { Plan } from '@/types';
|
import type { Plan } from '@/types';
|
||||||
|
|
||||||
function formatDate(value?: string | null) {
|
function formatDate(value?: string | null) {
|
||||||
@@ -25,23 +21,9 @@ function formatPrice(value: string, cycle: string) {
|
|||||||
return `${price} / ${cycle}`;
|
return `${price} / ${cycle}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UsePlanColumnsParams {
|
export function usePlanColumns(): ColumnDef<Plan>[] {
|
||||||
onEdit: (plan: Plan) => void;
|
|
||||||
onToggleStatus: (plan: Plan) => void;
|
|
||||||
pendingPlanId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePlanColumns({
|
|
||||||
onEdit,
|
|
||||||
onToggleStatus,
|
|
||||||
pendingPlanId,
|
|
||||||
}: UsePlanColumnsParams): ColumnDef<Plan>[] {
|
|
||||||
const { hasPermission } = usePermissions();
|
|
||||||
const canEdit = hasPermission(PERMISSIONS.PLAN.UPDATE);
|
|
||||||
const canDelete = hasPermission(PERMISSIONS.PLAN.DELETE);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const columns: ColumnDef<Plan>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Plan',
|
header: 'Plan',
|
||||||
@@ -91,38 +73,5 @@ export function usePlanColumns({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}, []);
|
||||||
if (!canEdit && !canDelete) return columns;
|
|
||||||
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <div className="text-right">Actions</div>,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const plan = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
{canEdit ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={() => onEdit(plan)}>
|
|
||||||
<Edit className="size-4" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canDelete ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={pendingPlanId === plan.id}
|
|
||||||
onClick={() => onToggleStatus(plan)}
|
|
||||||
>
|
|
||||||
<RotateCcw className="mr-2 size-4" />
|
|
||||||
{plan.is_active ? 'Deactivate' : 'Activate'}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingPlanId]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function PlanSheet({
|
|||||||
placeholder="Basic plan for small teams"
|
placeholder="Basic plan for small teams"
|
||||||
{...register('description', { required: true })}
|
{...register('description', { required: true })}
|
||||||
required
|
required
|
||||||
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
import { Edit, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import type { Plan } from '@/types';
|
import type { Plan } from '@/types';
|
||||||
|
|
||||||
interface PlanTableProps {
|
interface PlanTableProps {
|
||||||
@@ -18,6 +20,9 @@ interface PlanTableProps {
|
|||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
onSortingChange: (sorting: SortingState) => void;
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
|
onEdit: (plan: Plan) => void;
|
||||||
|
onToggleStatus: (plan: Plan) => void;
|
||||||
|
pendingPlanId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlanTable({
|
export function PlanTable({
|
||||||
@@ -32,6 +37,9 @@ export function PlanTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onSortingChange,
|
onSortingChange,
|
||||||
|
onEdit,
|
||||||
|
onToggleStatus,
|
||||||
|
pendingPlanId,
|
||||||
}: PlanTableProps) {
|
}: PlanTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -41,6 +49,21 @@ export function PlanTable({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
emptyTitle="No plans found."
|
emptyTitle="No plans found."
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit className="size-4" />,
|
||||||
|
permission: PERMISSIONS.PLAN.UPDATE,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (plan) => (plan.is_active ? 'Deactivate' : 'Activate'),
|
||||||
|
icon: <RotateCcw className="size-4" />,
|
||||||
|
permission: PERMISSIONS.PLAN.DELETE,
|
||||||
|
disabled: (plan) => pendingPlanId === plan.id,
|
||||||
|
onClick: onToggleStatus,
|
||||||
|
},
|
||||||
|
]}
|
||||||
pagination={{
|
pagination={{
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -57,15 +57,8 @@ export default function PlansPage() {
|
|||||||
onSaved: () => handleSheetOpenChange(false),
|
onSaved: () => handleSheetOpenChange(false),
|
||||||
});
|
});
|
||||||
const { openCreate: prepareCreatePlan, openEdit: prepareEditPlan } = planForm;
|
const { openCreate: prepareCreatePlan, openEdit: prepareEditPlan } = planForm;
|
||||||
const {
|
const { register, control, onSubmit, planId, permissionIds, setPermissionIds, isSaving } =
|
||||||
register,
|
planForm;
|
||||||
control,
|
|
||||||
onSubmit,
|
|
||||||
planId,
|
|
||||||
permissionIds,
|
|
||||||
setPermissionIds,
|
|
||||||
isSaving,
|
|
||||||
} = planForm;
|
|
||||||
const statusMutation = usePlanStatusMutation();
|
const statusMutation = usePlanStatusMutation();
|
||||||
|
|
||||||
const openCreate = useCallback(() => {
|
const openCreate = useCallback(() => {
|
||||||
@@ -123,11 +116,7 @@ export default function PlansPage() {
|
|||||||
[statusMutation],
|
[statusMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = usePlanColumns({
|
const columns = usePlanColumns();
|
||||||
onEdit: openEdit,
|
|
||||||
onToggleStatus: toggleStatus,
|
|
||||||
pendingPlanId: statusMutation.variables?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const plans = plansQuery.data?.items ?? [];
|
const plans = plansQuery.data?.items ?? [];
|
||||||
const total = plansQuery.data?.total ?? plansQuery.data?.totalItems ?? 0;
|
const total = plansQuery.data?.total ?? plansQuery.data?.totalItems ?? 0;
|
||||||
@@ -172,6 +161,9 @@ export default function PlansPage() {
|
|||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
onSortingChange={setSorting}
|
onSortingChange={setSorting}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onToggleStatus={toggleStatus}
|
||||||
|
pendingPlanId={statusMutation.variables?.id}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
|
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
|
||||||
import { Layers, Loader2, MapPin, Route } from 'lucide-react';
|
import { Layers, Loader2, MapPin, Route } from 'lucide-react';
|
||||||
|
|
||||||
import { FormField } from '@/components/form';
|
import { FormField } from '@/components/form';
|
||||||
@@ -24,7 +24,7 @@ interface ProjectDialogProps {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
register: UseFormRegister<ProjectFormValues>;
|
register: UseFormRegister<ProjectFormValues>;
|
||||||
errors: FieldErrors<ProjectFormValues>;
|
errors: FieldErrors<ProjectFormValues>;
|
||||||
touchedFields: UseFormReturn<ProjectFormValues>['formState']['touchedFields'];
|
isSubmitted: boolean;
|
||||||
onSubmit: ComponentProps<'form'>['onSubmit'];
|
onSubmit: ComponentProps<'form'>['onSubmit'];
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
@@ -36,16 +36,16 @@ export function ProjectDialog({
|
|||||||
projectId,
|
projectId,
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
}: ProjectDialogProps) {
|
}: ProjectDialogProps) {
|
||||||
const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined;
|
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
|
||||||
const startLatErrorMessage = touchedFields.start_lat ? errors.start_lat?.message : undefined;
|
const startLatErrorMessage = isSubmitted ? errors.start_lat?.message : undefined;
|
||||||
const startLngErrorMessage = touchedFields.start_lng ? errors.start_lng?.message : undefined;
|
const startLngErrorMessage = isSubmitted ? errors.start_lng?.message : undefined;
|
||||||
const endLatErrorMessage = touchedFields.end_lat ? errors.end_lat?.message : undefined;
|
const endLatErrorMessage = isSubmitted ? errors.end_lat?.message : undefined;
|
||||||
const endLngErrorMessage = touchedFields.end_lng ? errors.end_lng?.message : undefined;
|
const endLngErrorMessage = isSubmitted ? errors.end_lng?.message : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { Edit3, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { Project } from '@/types';
|
import type { Project } from '@/types';
|
||||||
@@ -35,8 +36,19 @@ export function ProjectTable({
|
|||||||
title="Projects"
|
title="Projects"
|
||||||
data={projects}
|
data={projects}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onEdit={onEdit}
|
actions={[
|
||||||
onDelete={onDelete}
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit3 className="size-4" />,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: <Trash2 className="size-4" />,
|
||||||
|
className: 'text-destructive',
|
||||||
|
onClick: onDelete,
|
||||||
|
},
|
||||||
|
]}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
emptyTitle="No projects found."
|
emptyTitle="No projects found."
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ export function useProjectForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting, touchedFields },
|
formState: { errors, isSubmitting, isSubmitted },
|
||||||
} = useForm<ProjectFormValues>({
|
} = useForm<ProjectFormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onTouched',
|
mode: 'onSubmit',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
resolver: zodResolver(projectFormSchema),
|
resolver: zodResolver(projectFormSchema),
|
||||||
});
|
});
|
||||||
@@ -139,7 +139,7 @@ export function useProjectForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
resetForm: () => reset(defaultValues),
|
resetForm: () => reset(defaultValues),
|
||||||
projectId,
|
projectId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving: isSubmitting || saveMutation.isPending,
|
isSaving: isSubmitting || saveMutation.isPending,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function ProjectPage() {
|
|||||||
resetForm,
|
resetForm,
|
||||||
projectId,
|
projectId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
} = projectForm;
|
} = projectForm;
|
||||||
@@ -107,7 +107,7 @@ export default function ProjectPage() {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touchedFields={touchedFields}
|
isSubmitted={isSubmitted}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
|
|||||||
@@ -2,31 +2,13 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Edit, RotateCcw } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
|
||||||
import { usePermissions } from '@/hooks/usePermissions';
|
|
||||||
import type { Role } from '@/types';
|
import type { Role } from '@/types';
|
||||||
|
|
||||||
interface UseRoleColumnsParams {
|
export function useRoleColumns(): ColumnDef<Role>[] {
|
||||||
onEdit: (role: Role) => void;
|
|
||||||
onToggleStatus: (role: Role) => void;
|
|
||||||
isStatusPending: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRoleColumns({
|
|
||||||
onEdit,
|
|
||||||
onToggleStatus,
|
|
||||||
isStatusPending,
|
|
||||||
}: UseRoleColumnsParams): ColumnDef<Role>[] {
|
|
||||||
const { hasPermission } = usePermissions();
|
|
||||||
const canEdit = hasPermission(PERMISSIONS.ROLE.UPDATE);
|
|
||||||
const canDelete = hasPermission(PERMISSIONS.ROLE.DELETE);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const columns: ColumnDef<Role>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
@@ -60,38 +42,5 @@ export function useRoleColumns({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}, []);
|
||||||
if (!canEdit && !canDelete) return columns;
|
|
||||||
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <div className="text-right">Actions</div>,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const role = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
{canEdit ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={() => onEdit(role)}>
|
|
||||||
<Edit className="size-4" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canDelete ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={isStatusPending}
|
|
||||||
onClick={() => onToggleStatus(role)}
|
|
||||||
>
|
|
||||||
<RotateCcw className="mr-2 size-4" />
|
|
||||||
{role.effective_status ? 'Deactivate' : 'Activate'}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
}, [canDelete, canEdit, isStatusPending, onEdit, onToggleStatus]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
|
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import { FormField } from '@/components/form';
|
import { FormField } from '@/components/form';
|
||||||
@@ -28,7 +28,7 @@ interface RoleSheetProps {
|
|||||||
roleId?: number;
|
roleId?: number;
|
||||||
register: UseFormRegister<RoleFormValues>;
|
register: UseFormRegister<RoleFormValues>;
|
||||||
errors: FieldErrors<RoleFormValues>;
|
errors: FieldErrors<RoleFormValues>;
|
||||||
touchedFields: UseFormReturn<RoleFormValues>['formState']['touchedFields'];
|
isSubmitted: boolean;
|
||||||
onSubmit: ComponentProps<'form'>['onSubmit'];
|
onSubmit: ComponentProps<'form'>['onSubmit'];
|
||||||
permissionTree: PermissionTreeItem[];
|
permissionTree: PermissionTreeItem[];
|
||||||
permissionIds: number[];
|
permissionIds: number[];
|
||||||
@@ -44,7 +44,7 @@ export function RoleSheet({
|
|||||||
roleId,
|
roleId,
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
permissionTree,
|
permissionTree,
|
||||||
permissionIds,
|
permissionIds,
|
||||||
@@ -53,10 +53,8 @@ export function RoleSheet({
|
|||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
}: RoleSheetProps) {
|
}: RoleSheetProps) {
|
||||||
const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined;
|
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
|
||||||
const displayNameErrorMessage = touchedFields.display_name
|
const displayNameErrorMessage = isSubmitted ? errors.display_name?.message : undefined;
|
||||||
? errors.display_name?.message
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -74,12 +72,7 @@ export function RoleSheet({
|
|||||||
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
|
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
|
||||||
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
|
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FormField
|
<FormField id="role-name" label="Role Name" required error={nameErrorMessage}>
|
||||||
id="role-name"
|
|
||||||
label="Role Name"
|
|
||||||
required
|
|
||||||
error={nameErrorMessage}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
id="role-name"
|
id="role-name"
|
||||||
placeholder="Enter role name"
|
placeholder="Enter role name"
|
||||||
@@ -114,7 +107,7 @@ export function RoleSheet({
|
|||||||
<FormField
|
<FormField
|
||||||
label="Permissions"
|
label="Permissions"
|
||||||
required
|
required
|
||||||
error={errors.permission_ids?.message}
|
error={isSubmitted ? errors.permission_ids?.message : undefined}
|
||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
labelEnd={
|
labelEnd={
|
||||||
<span className="text-muted-foreground">{permissionIds.length} selected</span>
|
<span className="text-muted-foreground">{permissionIds.length} selected</span>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { Edit, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import type { Role } from '@/types';
|
import type { Role } from '@/types';
|
||||||
|
|
||||||
interface RoleTableProps {
|
interface RoleTableProps {
|
||||||
@@ -18,6 +20,9 @@ interface RoleTableProps {
|
|||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
onSortingChange: (sorting: SortingState) => void;
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
|
onEdit: (role: Role) => void;
|
||||||
|
onToggleStatus: (role: Role) => void;
|
||||||
|
isStatusPending: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoleTable({
|
export function RoleTable({
|
||||||
@@ -32,6 +37,9 @@ export function RoleTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onSortingChange,
|
onSortingChange,
|
||||||
|
onEdit,
|
||||||
|
onToggleStatus,
|
||||||
|
isStatusPending,
|
||||||
}: RoleTableProps) {
|
}: RoleTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -41,6 +49,21 @@ export function RoleTable({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
emptyTitle="No roles found."
|
emptyTitle="No roles found."
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit className="size-4" />,
|
||||||
|
permission: PERMISSIONS.ROLE.UPDATE,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (role) => (role.effective_status ? 'Deactivate' : 'Activate'),
|
||||||
|
icon: <RotateCcw className="size-4" />,
|
||||||
|
permission: PERMISSIONS.ROLE.DELETE,
|
||||||
|
disabled: () => isStatusPending,
|
||||||
|
onClick: onToggleStatus,
|
||||||
|
},
|
||||||
|
]}
|
||||||
pagination={{
|
pagination={{
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export function useRoleForm({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { errors, isSubmitting, touchedFields },
|
formState: { errors, isSubmitting, isSubmitted },
|
||||||
} = useForm<RoleFormValues>({
|
} = useForm<RoleFormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onTouched',
|
mode: 'onSubmit',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
resolver: zodResolver(roleFormSchema),
|
resolver: zodResolver(roleFormSchema),
|
||||||
});
|
});
|
||||||
@@ -136,7 +136,7 @@ export function useRoleForm({
|
|||||||
permissionIds,
|
permissionIds,
|
||||||
roleId,
|
roleId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving: isSubmitting || saveMutation.isPending,
|
isSaving: isSubmitting || saveMutation.isPending,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function RolesPage() {
|
|||||||
permissionIds,
|
permissionIds,
|
||||||
setPermissionIds,
|
setPermissionIds,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
} = roleForm;
|
} = roleForm;
|
||||||
@@ -88,11 +88,7 @@ export default function RolesPage() {
|
|||||||
[updateRoleStatus],
|
[updateRoleStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = useRoleColumns({
|
const columns = useRoleColumns();
|
||||||
onEdit: openEdit,
|
|
||||||
onToggleStatus: toggleStatus,
|
|
||||||
isStatusPending,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = rolesQuery.data?.total ?? 0;
|
const total = rolesQuery.data?.total ?? 0;
|
||||||
const roles = rolesQuery.data?.items ?? [];
|
const roles = rolesQuery.data?.items ?? [];
|
||||||
@@ -134,6 +130,9 @@ export default function RolesPage() {
|
|||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
onSortingChange={setSorting}
|
onSortingChange={setSorting}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onToggleStatus={toggleStatus}
|
||||||
|
isStatusPending={isStatusPending}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ export default function RolesPage() {
|
|||||||
roleId={roleId}
|
roleId={roleId}
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touchedFields={touchedFields}
|
isSubmitted={isSubmitted}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
permissionTree={permissionsQuery.permissionTree}
|
permissionTree={permissionsQuery.permissionTree}
|
||||||
permissionIds={permissionIds}
|
permissionIds={permissionIds}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
|
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
|
||||||
import { Loader2, MapPin, Milestone } from 'lucide-react';
|
import { Loader2, MapPin, Milestone } from 'lucide-react';
|
||||||
|
|
||||||
import { FormField } from '@/components/form';
|
import { FormField } from '@/components/form';
|
||||||
@@ -42,7 +42,7 @@ interface SegmentDialogProps {
|
|||||||
onDirectionChange: (direction: 'UP' | 'DOWN') => void;
|
onDirectionChange: (direction: 'UP' | 'DOWN') => void;
|
||||||
register: UseFormRegister<SegmentFormValues>;
|
register: UseFormRegister<SegmentFormValues>;
|
||||||
errors: FieldErrors<SegmentFormValues>;
|
errors: FieldErrors<SegmentFormValues>;
|
||||||
touchedFields: UseFormReturn<SegmentFormValues>['formState']['touchedFields'];
|
isSubmitted: boolean;
|
||||||
onSubmit: ComponentProps<'form'>['onSubmit'];
|
onSubmit: ComponentProps<'form'>['onSubmit'];
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
@@ -64,13 +64,13 @@ export function SegmentDialog({
|
|||||||
onDirectionChange,
|
onDirectionChange,
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
}: SegmentDialogProps) {
|
}: SegmentDialogProps) {
|
||||||
const getError = (field: keyof SegmentFormValues) =>
|
const getError = (field: keyof SegmentFormValues) =>
|
||||||
touchedFields[field] ? errors[field]?.message : undefined;
|
isSubmitted ? errors[field]?.message : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -131,7 +131,9 @@ export function SegmentDialog({
|
|||||||
Loading...
|
Loading...
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<SelectValue placeholder={projectId ? 'Choose a package' : 'Select project first'} />
|
<SelectValue
|
||||||
|
placeholder={projectId ? 'Choose a package' : 'Select project first'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -278,7 +280,12 @@ export function SegmentDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSaving || !canSubmit}>
|
<Button type="submit" disabled={isSaving || !canSubmit}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { Edit3, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { Chainage } from '@/types';
|
import type { Chainage } from '@/types';
|
||||||
@@ -35,8 +36,19 @@ export function SegmentTable({
|
|||||||
title="Segments"
|
title="Segments"
|
||||||
data={segments}
|
data={segments}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onEdit={onEdit}
|
actions={[
|
||||||
onDelete={onDelete}
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit3 className="size-4" />,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: <Trash2 className="size-4" />,
|
||||||
|
className: 'text-destructive',
|
||||||
|
onClick: onDelete,
|
||||||
|
},
|
||||||
|
]}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
emptyTitle="No segments found."
|
emptyTitle="No segments found."
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -86,10 +86,10 @@ export function useSegmentForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting, touchedFields },
|
formState: { errors, isSubmitting, isSubmitted },
|
||||||
} = useForm<SegmentFormValues>({
|
} = useForm<SegmentFormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onTouched',
|
mode: 'onSubmit',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
resolver: zodResolver(segmentFormSchema),
|
resolver: zodResolver(segmentFormSchema),
|
||||||
});
|
});
|
||||||
@@ -200,7 +200,7 @@ export function useSegmentForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
direction,
|
direction,
|
||||||
setDirection,
|
setDirection,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving: isSubmitting || saveMutation.isPending,
|
isSaving: isSubmitting || saveMutation.isPending,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function SegmentPage() {
|
|||||||
direction,
|
direction,
|
||||||
setDirection,
|
setDirection,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
} = segmentForm;
|
} = segmentForm;
|
||||||
@@ -136,7 +136,7 @@ export default function SegmentPage() {
|
|||||||
onDirectionChange={setDirection}
|
onDirectionChange={setDirection}
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touchedFields={touchedFields}
|
isSubmitted={isSubmitted}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Edit, Trash2 } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
|
||||||
import { usePermissions } from '@/hooks/usePermissions';
|
|
||||||
import type { Tenant } from '@/types';
|
import type { Tenant } from '@/types';
|
||||||
|
|
||||||
function formatDate(value?: string | null) {
|
function formatDate(value?: string | null) {
|
||||||
@@ -19,23 +15,9 @@ function formatDate(value?: string | null) {
|
|||||||
}).format(new Date(value));
|
}).format(new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseTenantColumnsParams {
|
export function useTenantColumns(): ColumnDef<Tenant>[] {
|
||||||
onEdit: (tenant: Tenant) => void;
|
|
||||||
onDelete: (tenant: Tenant) => void;
|
|
||||||
pendingDeleteId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTenantColumns({
|
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
pendingDeleteId,
|
|
||||||
}: UseTenantColumnsParams): ColumnDef<Tenant>[] {
|
|
||||||
const { hasPermission } = usePermissions();
|
|
||||||
const canEdit = hasPermission(PERMISSIONS.TENANT.UPDATE);
|
|
||||||
const canDelete = hasPermission(PERMISSIONS.TENANT.DELETE);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const columns: ColumnDef<Tenant>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Tenant',
|
header: 'Tenant',
|
||||||
@@ -76,37 +58,5 @@ export function useTenantColumns({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}, []);
|
||||||
if (!canEdit && !canDelete) return columns;
|
|
||||||
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <div className="text-right">Actions</div>,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const tenant = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
{canEdit ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={() => onEdit(tenant)}>
|
|
||||||
<Edit className="size-4" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canDelete ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={pendingDeleteId === tenant.id}
|
|
||||||
onClick={() => onDelete(tenant)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
}, [canDelete, canEdit, onDelete, onEdit, pendingDeleteId]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ export function TenantSheet({
|
|||||||
<Label>Client</Label>
|
<Label>Client</Label>
|
||||||
<Select value={clientId} onValueChange={onClientChange} disabled={isLookupsLoading}>
|
<Select value={clientId} onValueChange={onClientChange} disabled={isLookupsLoading}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={isLookupsLoading ? 'Loading clients...' : 'Select client'} />
|
<SelectValue
|
||||||
|
placeholder={isLookupsLoading ? 'Loading clients...' : 'Select client'}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{clients.map((client) => (
|
{clients.map((client) => (
|
||||||
@@ -123,7 +125,9 @@ export function TenantSheet({
|
|||||||
<Label>Subscription Plan</Label>
|
<Label>Subscription Plan</Label>
|
||||||
<Select value={planId} onValueChange={onPlanChange} disabled={isLookupsLoading}>
|
<Select value={planId} onValueChange={onPlanChange} disabled={isLookupsLoading}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={isLookupsLoading ? 'Loading plans...' : 'Select plan'} />
|
<SelectValue
|
||||||
|
placeholder={isLookupsLoading ? 'Loading plans...' : 'Select plan'}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{plans.map((plan) => (
|
{plans.map((plan) => (
|
||||||
@@ -147,7 +151,7 @@ export function TenantSheet({
|
|||||||
id="tenant-description"
|
id="tenant-description"
|
||||||
placeholder="North zone operations"
|
placeholder="North zone operations"
|
||||||
{...register('description')}
|
{...register('description')}
|
||||||
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
import { Edit, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import type { Tenant } from '@/types';
|
import type { Tenant } from '@/types';
|
||||||
|
|
||||||
interface TenantTableProps {
|
interface TenantTableProps {
|
||||||
@@ -18,6 +20,9 @@ interface TenantTableProps {
|
|||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
onSortingChange: (sorting: SortingState) => void;
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
|
onEdit: (tenant: Tenant) => void;
|
||||||
|
onDelete: (tenant: Tenant) => void;
|
||||||
|
pendingDeleteId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TenantTable({
|
export function TenantTable({
|
||||||
@@ -32,6 +37,9 @@ export function TenantTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onSortingChange,
|
onSortingChange,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
pendingDeleteId,
|
||||||
}: TenantTableProps) {
|
}: TenantTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -41,6 +49,22 @@ export function TenantTable({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
emptyTitle="No tenants found."
|
emptyTitle="No tenants found."
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit className="size-4" />,
|
||||||
|
permission: PERMISSIONS.TENANT.UPDATE,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: <Trash2 className="size-4" />,
|
||||||
|
permission: PERMISSIONS.TENANT.DELETE,
|
||||||
|
className: 'text-destructive',
|
||||||
|
disabled: (tenant) => pendingDeleteId === tenant.id,
|
||||||
|
onClick: onDelete,
|
||||||
|
},
|
||||||
|
]}
|
||||||
pagination={{
|
pagination={{
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -116,11 +116,7 @@ export default function TenantsPage() {
|
|||||||
[deleteMutation],
|
[deleteMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = useTenantColumns({
|
const columns = useTenantColumns();
|
||||||
onEdit: openEdit,
|
|
||||||
onDelete: handleDelete,
|
|
||||||
pendingDeleteId: deleteMutation.isPending ? deleteMutation.variables : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tenants = tenantsQuery.data?.items ?? [];
|
const tenants = tenantsQuery.data?.items ?? [];
|
||||||
const total = tenantsQuery.data?.total ?? 0;
|
const total = tenantsQuery.data?.total ?? 0;
|
||||||
@@ -169,6 +165,9 @@ export default function TenantsPage() {
|
|||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
onSortingChange={setSorting}
|
onSortingChange={setSorting}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
pendingDeleteId={deleteMutation.isPending ? deleteMutation.variables : undefined}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Edit, RotateCcw } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
|
||||||
import { usePermissions } from '@/hooks/usePermissions';
|
|
||||||
import type { AdministrationUser } from '@/types';
|
import type { AdministrationUser } from '@/types';
|
||||||
|
|
||||||
function formatDate(value?: string | null) {
|
function formatDate(value?: string | null) {
|
||||||
@@ -19,23 +15,9 @@ function formatDate(value?: string | null) {
|
|||||||
}).format(new Date(value));
|
}).format(new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseUserColumnsParams {
|
export function useUserColumns(): ColumnDef<AdministrationUser>[] {
|
||||||
onEdit: (user: AdministrationUser) => void;
|
|
||||||
onToggleStatus: (user: AdministrationUser) => void;
|
|
||||||
pendingUserId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUserColumns({
|
|
||||||
onEdit,
|
|
||||||
onToggleStatus,
|
|
||||||
pendingUserId,
|
|
||||||
}: UseUserColumnsParams): ColumnDef<AdministrationUser>[] {
|
|
||||||
const { hasPermission } = usePermissions();
|
|
||||||
const canEdit = hasPermission(PERMISSIONS.USER.UPDATE);
|
|
||||||
const canDelete = hasPermission(PERMISSIONS.USER.DELETE);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const columns: ColumnDef<AdministrationUser>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'first_name',
|
accessorKey: 'first_name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
@@ -88,38 +70,5 @@ export function useUserColumns({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}, []);
|
||||||
if (!canEdit && !canDelete) return columns;
|
|
||||||
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <div className="text-right">Actions</div>,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const user = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
{canEdit ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={() => onEdit(user)}>
|
|
||||||
<Edit className="size-4" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canDelete ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={pendingUserId === user.id || user.effective_status === 'pending'}
|
|
||||||
onClick={() => onToggleStatus(user)}
|
|
||||||
>
|
|
||||||
<RotateCcw className="mr-2 size-4" />
|
|
||||||
{user.effective_status === 'active' ? 'Deactivate' : 'Activate'}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingUserId]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, type ComponentProps } from 'react';
|
import { useRef, type ComponentProps } from 'react';
|
||||||
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
|
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import { FormField } from '@/components/form';
|
import { FormField } from '@/components/form';
|
||||||
@@ -24,7 +24,7 @@ interface UserSheetProps {
|
|||||||
userId?: number;
|
userId?: number;
|
||||||
register: UseFormRegister<UserFormValues>;
|
register: UseFormRegister<UserFormValues>;
|
||||||
errors: FieldErrors<UserFormValues>;
|
errors: FieldErrors<UserFormValues>;
|
||||||
touchedFields: UseFormReturn<UserFormValues>['formState']['touchedFields'];
|
isSubmitted: boolean;
|
||||||
onSubmit: ComponentProps<'form'>['onSubmit'];
|
onSubmit: ComponentProps<'form'>['onSubmit'];
|
||||||
roleId: string;
|
roleId: string;
|
||||||
onRoleChange: (roleId: string) => void;
|
onRoleChange: (roleId: string) => void;
|
||||||
@@ -38,7 +38,7 @@ export function UserSheet({
|
|||||||
userId,
|
userId,
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
roleId,
|
roleId,
|
||||||
onRoleChange,
|
onRoleChange,
|
||||||
@@ -46,11 +46,11 @@ export function UserSheet({
|
|||||||
isSaving,
|
isSaving,
|
||||||
}: UserSheetProps) {
|
}: UserSheetProps) {
|
||||||
const roleComboboxPortalRef = useRef<HTMLDivElement | null>(null);
|
const roleComboboxPortalRef = useRef<HTMLDivElement | null>(null);
|
||||||
const firstNameErrorMessage = touchedFields.first_name ? errors.first_name?.message : undefined;
|
const firstNameErrorMessage = isSubmitted ? errors.first_name?.message : undefined;
|
||||||
const lastNameErrorMessage = touchedFields.last_name ? errors.last_name?.message : undefined;
|
const lastNameErrorMessage = isSubmitted ? errors.last_name?.message : undefined;
|
||||||
const emailErrorMessage = touchedFields.email ? errors.email?.message : undefined;
|
const emailErrorMessage = isSubmitted ? errors.email?.message : undefined;
|
||||||
const phoneErrorMessage = touchedFields.phone_number ? errors.phone_number?.message : undefined;
|
const phoneErrorMessage = isSubmitted ? errors.phone_number?.message : undefined;
|
||||||
const roleErrorMessage = touchedFields.role_id ? errors.role_id?.message : undefined;
|
const roleErrorMessage = isSubmitted ? errors.role_id?.message : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -63,12 +63,7 @@ export function UserSheet({
|
|||||||
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-1 flex-col gap-5 px-4">
|
<form onSubmit={onSubmit} className="flex flex-1 flex-col gap-5 px-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FormField
|
<FormField id="first-name" label="First Name" required error={firstNameErrorMessage}>
|
||||||
id="first-name"
|
|
||||||
label="First Name"
|
|
||||||
required
|
|
||||||
error={firstNameErrorMessage}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
id="first-name"
|
id="first-name"
|
||||||
placeholder="Enter first name"
|
placeholder="Enter first name"
|
||||||
@@ -77,12 +72,7 @@ export function UserSheet({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField id="last-name" label="Last Name" required error={lastNameErrorMessage}>
|
||||||
id="last-name"
|
|
||||||
label="Last Name"
|
|
||||||
required
|
|
||||||
error={lastNameErrorMessage}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
id="last-name"
|
id="last-name"
|
||||||
placeholder="Enter last name"
|
placeholder="Enter last name"
|
||||||
@@ -92,12 +82,7 @@ export function UserSheet({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField id="email" label="Email" required error={emailErrorMessage}>
|
||||||
id="email"
|
|
||||||
label="Email"
|
|
||||||
required
|
|
||||||
error={emailErrorMessage}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
placeholder="name@example.com"
|
placeholder="name@example.com"
|
||||||
@@ -106,11 +91,7 @@ export function UserSheet({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField id="phone-number" label="Phone Number" error={phoneErrorMessage}>
|
||||||
id="phone-number"
|
|
||||||
label="Phone Number"
|
|
||||||
error={phoneErrorMessage}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
id="phone-number"
|
id="phone-number"
|
||||||
placeholder="+919876543210"
|
placeholder="+919876543210"
|
||||||
@@ -130,12 +111,7 @@ export function UserSheet({
|
|||||||
|
|
||||||
<div ref={roleComboboxPortalRef} />
|
<div ref={roleComboboxPortalRef} />
|
||||||
|
|
||||||
<input
|
<input type="hidden" {...register('role_id')} value={roleId} readOnly />
|
||||||
type="hidden"
|
|
||||||
{...register('role_id')}
|
|
||||||
value={roleId}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<DialogFooter className="px-0">
|
<DialogFooter className="px-0">
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
import { Edit, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import type { AdministrationUser } from '@/types';
|
import type { AdministrationUser } from '@/types';
|
||||||
|
|
||||||
interface UserTableProps {
|
interface UserTableProps {
|
||||||
@@ -18,6 +20,9 @@ interface UserTableProps {
|
|||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
onSortingChange: (sorting: SortingState) => void;
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
|
onEdit: (user: AdministrationUser) => void;
|
||||||
|
onToggleStatus: (user: AdministrationUser) => void;
|
||||||
|
pendingUserId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserTable({
|
export function UserTable({
|
||||||
@@ -32,6 +37,9 @@ export function UserTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onSortingChange,
|
onSortingChange,
|
||||||
|
onEdit,
|
||||||
|
onToggleStatus,
|
||||||
|
pendingUserId,
|
||||||
}: UserTableProps) {
|
}: UserTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -41,6 +49,21 @@ export function UserTable({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
emptyTitle="No users found."
|
emptyTitle="No users found."
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Edit className="size-4" />,
|
||||||
|
permission: PERMISSIONS.USER.UPDATE,
|
||||||
|
onClick: onEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (user) => (user.effective_status === 'active' ? 'Deactivate' : 'Activate'),
|
||||||
|
icon: <RotateCcw className="size-4" />,
|
||||||
|
permission: PERMISSIONS.USER.DELETE,
|
||||||
|
disabled: (user) => pendingUserId === user.id || user.effective_status === 'pending',
|
||||||
|
onClick: onToggleStatus,
|
||||||
|
},
|
||||||
|
]}
|
||||||
pagination={{
|
pagination={{
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
handleSubmit: submitForm,
|
handleSubmit: submitForm,
|
||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { errors, isSubmitting, touchedFields },
|
formState: { errors, isSubmitting, isSubmitted },
|
||||||
} = useForm<UserFormValues>({
|
} = useForm<UserFormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onTouched',
|
mode: 'onSubmit',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
resolver: zodResolver(userFormSchema),
|
resolver: zodResolver(userFormSchema),
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,8 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
const lastName = useWatch({ control, name: 'last_name' }) || '';
|
const lastName = useWatch({ control, name: 'last_name' }) || '';
|
||||||
const email = useWatch({ control, name: 'email' }) || '';
|
const email = useWatch({ control, name: 'email' }) || '';
|
||||||
const phoneNumber = useWatch({ control, name: 'phone_number' }) || '';
|
const phoneNumber = useWatch({ control, name: 'phone_number' }) || '';
|
||||||
const isPhoneValid = phoneNumber.trim() === '' || /^\+(?:[0-9] ?){6,14}[0-9]$/.test(phoneNumber.trim());
|
const isPhoneValid =
|
||||||
|
phoneNumber.trim() === '' || /^\+(?:[0-9] ?){6,14}[0-9]$/.test(phoneNumber.trim());
|
||||||
const canSubmit =
|
const canSubmit =
|
||||||
firstName.trim().length > 0 &&
|
firstName.trim().length > 0 &&
|
||||||
lastName.trim().length > 0 &&
|
lastName.trim().length > 0 &&
|
||||||
@@ -131,7 +132,7 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) {
|
|||||||
roleId,
|
roleId,
|
||||||
userId,
|
userId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving: isSubmitting || saveMutation.isPending,
|
isSaving: isSubmitting || saveMutation.isPending,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function UsersPage() {
|
|||||||
roleId,
|
roleId,
|
||||||
setRoleId,
|
setRoleId,
|
||||||
errors,
|
errors,
|
||||||
touchedFields,
|
isSubmitted,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
isSaving,
|
isSaving,
|
||||||
} = userForm;
|
} = userForm;
|
||||||
@@ -81,11 +81,7 @@ export default function UsersPage() {
|
|||||||
[updateUserStatus],
|
[updateUserStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = useUserColumns({
|
const columns = useUserColumns();
|
||||||
onEdit: openEdit,
|
|
||||||
onToggleStatus: toggleStatus,
|
|
||||||
pendingUserId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = usersQuery.data?.total ?? 0;
|
const total = usersQuery.data?.total ?? 0;
|
||||||
const users = usersQuery.data?.items ?? [];
|
const users = usersQuery.data?.items ?? [];
|
||||||
@@ -137,6 +133,9 @@ export default function UsersPage() {
|
|||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
onSortingChange={setSorting}
|
onSortingChange={setSorting}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onToggleStatus={toggleStatus}
|
||||||
|
pendingUserId={pendingUserId}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -146,7 +145,7 @@ export default function UsersPage() {
|
|||||||
userId={userId}
|
userId={userId}
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touchedFields={touchedFields}
|
isSubmitted={isSubmitted}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
roleId={roleId}
|
roleId={roleId}
|
||||||
onRoleChange={setRoleId}
|
onRoleChange={setRoleId}
|
||||||
|
|||||||
@@ -24,13 +24,13 @@
|
|||||||
--border: oklch(0.92 0.004 286.32);
|
--border: oklch(0.92 0.004 286.32);
|
||||||
--input: oklch(0.92 0.004 286.32);
|
--input: oklch(0.92 0.004 286.32);
|
||||||
--ring: oklch(0.702 0.183 293.541);
|
--ring: oklch(0.702 0.183 293.541);
|
||||||
--chart-1: oklch(0.65 0.18 25); /* Soft Red/Rose - High Priority */
|
--chart-1: oklch(0.65 0.18 25); /* Soft Red/Rose - High Priority */
|
||||||
--chart-2: oklch(0.78 0.12 75); /* Warm Amber - Distinct */
|
--chart-2: oklch(0.78 0.12 75); /* Warm Amber - Distinct */
|
||||||
--chart-3: oklch(0.68 0.12 245); /* Azure Blue - Cool Professional */
|
--chart-3: oklch(0.68 0.12 245); /* Azure Blue - Cool Professional */
|
||||||
--chart-4: oklch(0.75 0.1 165); /* Mint Teal - Balanced */
|
--chart-4: oklch(0.75 0.1 165); /* Mint Teal - Balanced */
|
||||||
--chart-5: oklch(0.85 0.08 195); /* Soft Cyan - Subdued */
|
--chart-5: oklch(0.85 0.08 195); /* Soft Cyan - Subdued */
|
||||||
--chart-6: oklch(0.62 0.22 295); /* Deep Violet - Theme Primary */
|
--chart-6: oklch(0.62 0.22 295); /* Deep Violet - Theme Primary */
|
||||||
--chart-8: oklch(0.58 0.2 335); /* Cool Magenta - Distant Accent */
|
--chart-8: oklch(0.58 0.2 335); /* Cool Magenta - Distant Accent */
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
48
src/components/data-table/TableActionButton.tsx
Normal file
48
src/components/data-table/TableActionButton.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { MouseEvent, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { PermissionGuard } from '@/guards';
|
||||||
|
import type { PermissionInput } from '@/hooks/usePermissions';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface TableActionButtonProps {
|
||||||
|
label: string;
|
||||||
|
permission?: PermissionInput;
|
||||||
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableActionButton({
|
||||||
|
label,
|
||||||
|
permission,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: TableActionButtonProps) {
|
||||||
|
return (
|
||||||
|
<PermissionGuard permissions={permission}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className={cn('size-8', className)}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</PermissionGuard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,12 +12,22 @@ import {
|
|||||||
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Button } from '@/components/ui/button';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { Edit3, Trash2 } from 'lucide-react';
|
import type { PermissionInput } from '@/hooks/usePermissions';
|
||||||
|
|
||||||
import TopHeader from './Header';
|
import TopHeader from './Header';
|
||||||
import TableHeader from './TableHeader';
|
import TableHeader from './TableHeader';
|
||||||
import { TableFooter } from './Footer';
|
import { TableFooter } from './Footer';
|
||||||
|
import { TableActionButton } from './TableActionButton';
|
||||||
|
|
||||||
|
export interface DataTableAction<TData> {
|
||||||
|
label: string | ((item: TData) => string);
|
||||||
|
icon: React.ReactNode;
|
||||||
|
onClick: (item: TData) => void;
|
||||||
|
permission?: PermissionInput;
|
||||||
|
disabled?: (item: TData) => boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataTableProps<TData, TValue> {
|
export interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@@ -26,8 +36,7 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
onAddNew?: () => void;
|
onAddNew?: () => void;
|
||||||
addButtonText?: string;
|
addButtonText?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onEdit?: (item: TData) => void;
|
actions?: DataTableAction<TData>[];
|
||||||
onDelete?: (item: TData) => void;
|
|
||||||
toolbar?: React.ReactNode;
|
toolbar?: React.ReactNode;
|
||||||
emptyTitle?: string;
|
emptyTitle?: string;
|
||||||
emptyDescription?: string;
|
emptyDescription?: string;
|
||||||
@@ -49,8 +58,7 @@ export function DataTable<TData, TValue>({
|
|||||||
onAddNew,
|
onAddNew,
|
||||||
addButtonText,
|
addButtonText,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
onEdit,
|
actions,
|
||||||
onDelete,
|
|
||||||
toolbar,
|
toolbar,
|
||||||
emptyTitle = 'No results found.',
|
emptyTitle = 'No results found.',
|
||||||
emptyDescription = 'Try adjusting your filters or search terms.',
|
emptyDescription = 'Try adjusting your filters or search terms.',
|
||||||
@@ -77,7 +85,7 @@ export function DataTable<TData, TValue>({
|
|||||||
const columns = React.useMemo(() => {
|
const columns = React.useMemo(() => {
|
||||||
const cols: ColumnDef<TData, TValue>[] = [...initialColumns];
|
const cols: ColumnDef<TData, TValue>[] = [...initialColumns];
|
||||||
|
|
||||||
if (onEdit || onDelete) {
|
if (actions?.length) {
|
||||||
cols.push({
|
cols.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <div className="text-right">Actions</div>,
|
header: () => <div className="text-right">Actions</div>,
|
||||||
@@ -85,37 +93,33 @@ export function DataTable<TData, TValue>({
|
|||||||
const item = row.original;
|
const item = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{onEdit && (
|
{actions.map((action) => {
|
||||||
<Button
|
const label =
|
||||||
variant="outline"
|
typeof action.label === 'function' ? action.label(item) : action.label;
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
return (
|
||||||
e.stopPropagation();
|
<TableActionButton
|
||||||
onEdit(item);
|
key={label}
|
||||||
}}
|
label={label}
|
||||||
>
|
permission={action.permission}
|
||||||
<Edit3 className="size-4" />
|
className={action.className}
|
||||||
</Button>
|
disabled={action.disabled?.(item)}
|
||||||
)}
|
onClick={(event) => {
|
||||||
{onDelete && (
|
event.stopPropagation();
|
||||||
<Button
|
action.onClick(item);
|
||||||
variant="outline"
|
}}
|
||||||
size="sm"
|
>
|
||||||
onClick={(e) => {
|
{action.icon}
|
||||||
e.stopPropagation();
|
</TableActionButton>
|
||||||
onDelete(item);
|
);
|
||||||
}}
|
})}
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cols;
|
return cols;
|
||||||
}, [initialColumns, onEdit, onDelete]);
|
}, [actions, initialColumns]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@@ -158,7 +162,8 @@ export function DataTable<TData, TValue>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4 rounded-lg border bg-card p-4">
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<section className="space-y-4 rounded-lg border bg-card p-4">
|
||||||
<TopHeader
|
<TopHeader
|
||||||
title={onAddNew ? title : undefined}
|
title={onAddNew ? title : undefined}
|
||||||
itemCount={pagination?.totalItems ?? data.length}
|
itemCount={pagination?.totalItems ?? data.length}
|
||||||
@@ -186,10 +191,7 @@ export function DataTable<TData, TValue>({
|
|||||||
))
|
))
|
||||||
) : table.getRowModel().rows?.length ? (
|
) : table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow
|
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||||
key={row.id}
|
|
||||||
data-state={row.getIsSelected() && 'selected'}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
@@ -216,6 +218,7 @@ export function DataTable<TData, TValue>({
|
|||||||
pageSize={pagination?.limit}
|
pageSize={pagination?.limit}
|
||||||
onPageSizeChange={pagination?.onLimitChange}
|
onPageSizeChange={pagination?.onLimitChange}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { Slot } from 'radix-ui';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3',
|
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] outline-none aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||||
secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90',
|
'bg-destructive text-white dark:bg-destructive/60 [a&]:hover:bg-destructive/90',
|
||||||
outline:
|
outline:
|
||||||
'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||||
ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||||
|
|||||||
@@ -1,54 +1,51 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { Slot } from "radix-ui"
|
import { Slot } from 'radix-ui';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
destructive:
|
destructive: 'bg-destructive text-white hover:bg-destructive/90 dark:bg-destructive/60',
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:border-ring dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
||||||
secondary:
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
ghost:
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
icon: "size-9",
|
icon: 'size-9',
|
||||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
"icon-sm": "size-8",
|
'icon-sm': 'size-8',
|
||||||
"icon-lg": "size-10",
|
'icon-lg': 'size-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
size = "default",
|
size = 'default',
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<'button'> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot.Root : "button"
|
const Comp = asChild ? Slot.Root : 'button';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -58,7 +55,7 @@ function Button({
|
|||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -1,42 +1,58 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
import { Combobox as ComboboxPrimitive } from '@base-ui/react';
|
||||||
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
|
import { CheckIcon, ChevronDownIcon, XIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
} from "@/components/ui/input-group"
|
} from '@/components/ui/input-group';
|
||||||
|
|
||||||
const Combobox = ComboboxPrimitive.Root
|
const Combobox = ComboboxPrimitive.Root;
|
||||||
|
|
||||||
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
||||||
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxTrigger({
|
function ComboboxTrigger({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
render,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Trigger.Props) {
|
}: ComboboxPrimitive.Trigger.Props) {
|
||||||
|
const icon = (
|
||||||
|
<ChevronDownIcon
|
||||||
|
data-slot="combobox-trigger-icon"
|
||||||
|
className="pointer-events-none size-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const triggerRender = React.isValidElement<{ children?: React.ReactNode }>(render)
|
||||||
|
? React.cloneElement(render, {
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{render.props.children}
|
||||||
|
{icon}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: render;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Trigger
|
<ComboboxPrimitive.Trigger
|
||||||
data-slot="combobox-trigger"
|
data-slot="combobox-trigger"
|
||||||
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
||||||
|
render={triggerRender}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDownIcon
|
{render ? null : icon}
|
||||||
data-slot="combobox-trigger-icon"
|
|
||||||
className="pointer-events-none size-4 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</ComboboxPrimitive.Trigger>
|
</ComboboxPrimitive.Trigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
||||||
@@ -49,7 +65,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|||||||
>
|
>
|
||||||
<XIcon className="pointer-events-none" />
|
<XIcon className="pointer-events-none" />
|
||||||
</ComboboxPrimitive.Clear>
|
</ComboboxPrimitive.Clear>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxInput({
|
function ComboboxInput({
|
||||||
@@ -60,15 +76,12 @@ function ComboboxInput({
|
|||||||
showClear = false,
|
showClear = false,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Input.Props & {
|
}: ComboboxPrimitive.Input.Props & {
|
||||||
showTrigger?: boolean
|
showTrigger?: boolean;
|
||||||
showClear?: boolean
|
showClear?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<InputGroup className={cn("w-auto", className)}>
|
<InputGroup className={cn('w-auto', className)}>
|
||||||
<ComboboxPrimitive.Input
|
<ComboboxPrimitive.Input render={<InputGroupInput disabled={disabled} />} {...props} />
|
||||||
render={<InputGroupInput disabled={disabled} />}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<InputGroupAddon align="inline-end">
|
<InputGroupAddon align="inline-end">
|
||||||
{showTrigger && (
|
{showTrigger && (
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
@@ -86,14 +99,14 @@ function ComboboxInput({
|
|||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
{children}
|
{children}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxContent({
|
function ComboboxContent({
|
||||||
className,
|
className,
|
||||||
side = "bottom",
|
side = 'bottom',
|
||||||
sideOffset = 6,
|
sideOffset = 6,
|
||||||
align = "start",
|
align = 'start',
|
||||||
alignOffset = 0,
|
alignOffset = 0,
|
||||||
anchor,
|
anchor,
|
||||||
container,
|
container,
|
||||||
@@ -101,9 +114,9 @@ function ComboboxContent({
|
|||||||
}: ComboboxPrimitive.Popup.Props &
|
}: ComboboxPrimitive.Popup.Props &
|
||||||
Pick<
|
Pick<
|
||||||
ComboboxPrimitive.Positioner.Props,
|
ComboboxPrimitive.Positioner.Props,
|
||||||
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
|
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
|
||||||
> & {
|
> & {
|
||||||
container?: ComboboxPrimitive.Portal.Props["container"]
|
container?: ComboboxPrimitive.Portal.Props['container'];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Portal container={container}>
|
<ComboboxPrimitive.Portal container={container}>
|
||||||
@@ -119,14 +132,14 @@ function ComboboxContent({
|
|||||||
data-slot="combobox-content"
|
data-slot="combobox-content"
|
||||||
data-chips={!!anchor}
|
data-chips={!!anchor}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/combobox-content relative max-h-96 w-[var(--anchor-width)] max-w-[var(--available-width)] min-w-[calc(var(--anchor-width)+1.75rem)] origin-[var(--transform-origin)] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-[var(--anchor-width)] 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 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
'group/combobox-content relative max-h-96 w-[var(--anchor-width)] max-w-[var(--available-width)] min-w-[calc(var(--anchor-width)+1.75rem)] origin-[var(--transform-origin)] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-[var(--anchor-width)] 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 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</ComboboxPrimitive.Positioner>
|
</ComboboxPrimitive.Positioner>
|
||||||
</ComboboxPrimitive.Portal>
|
</ComboboxPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
||||||
@@ -134,25 +147,21 @@ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
|||||||
<ComboboxPrimitive.List
|
<ComboboxPrimitive.List
|
||||||
data-slot="combobox-list"
|
data-slot="combobox-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[min(21.75rem,calc(var(--available-height)-2.25rem))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0",
|
'max-h-[min(21.75rem,calc(var(--available-height)-2.25rem))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxItem({
|
function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Item.Props) {
|
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Item
|
<ComboboxPrimitive.Item
|
||||||
data-slot="combobox-item"
|
data-slot="combobox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -166,39 +175,30 @@ function ComboboxItem({
|
|||||||
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
|
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
|
||||||
</ComboboxPrimitive.ItemIndicator>
|
</ComboboxPrimitive.ItemIndicator>
|
||||||
</ComboboxPrimitive.Item>
|
</ComboboxPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Group
|
<ComboboxPrimitive.Group data-slot="combobox-group" className={cn(className)} {...props} />
|
||||||
data-slot="combobox-group"
|
);
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxLabel({
|
function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.GroupLabel.Props) {
|
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.GroupLabel
|
<ComboboxPrimitive.GroupLabel
|
||||||
data-slot="combobox-label"
|
data-slot="combobox-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-xs text-muted-foreground pointer-coarse:px-3 pointer-coarse:py-2 pointer-coarse:text-sm",
|
'px-2 py-1.5 text-xs text-muted-foreground pointer-coarse:px-3 pointer-coarse:py-2 pointer-coarse:text-sm',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
||||||
return (
|
return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />;
|
||||||
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
||||||
@@ -206,42 +206,38 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
|||||||
<ComboboxPrimitive.Empty
|
<ComboboxPrimitive.Empty
|
||||||
data-slot="combobox-empty"
|
data-slot="combobox-empty"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
|
'hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxSeparator({
|
function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Separator.Props) {
|
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Separator
|
<ComboboxPrimitive.Separator
|
||||||
data-slot="combobox-separator"
|
data-slot="combobox-separator"
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxChips({
|
function ComboboxChips({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props) {
|
||||||
ComboboxPrimitive.Chips.Props) {
|
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Chips
|
<ComboboxPrimitive.Chips
|
||||||
data-slot="combobox-chips"
|
data-slot="combobox-chips"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-[3px] has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1.5 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
|
'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring has-aria-invalid:border-destructive has-data-[slot=combobox-chip]:px-1.5 dark:bg-input/30 dark:has-aria-invalid:border-destructive',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxChip({
|
function ComboboxChip({
|
||||||
@@ -250,14 +246,14 @@ function ComboboxChip({
|
|||||||
showRemove = true,
|
showRemove = true,
|
||||||
...props
|
...props
|
||||||
}: ComboboxPrimitive.Chip.Props & {
|
}: ComboboxPrimitive.Chip.Props & {
|
||||||
showRemove?: boolean
|
showRemove?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Chip
|
<ComboboxPrimitive.Chip
|
||||||
data-slot="combobox-chip"
|
data-slot="combobox-chip"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
'flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -272,25 +268,21 @@ function ComboboxChip({
|
|||||||
</ComboboxPrimitive.ChipRemove>
|
</ComboboxPrimitive.ChipRemove>
|
||||||
)}
|
)}
|
||||||
</ComboboxPrimitive.Chip>
|
</ComboboxPrimitive.Chip>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxChipsInput({
|
function ComboboxChipsInput({ className, children, ...props }: ComboboxPrimitive.Input.Props) {
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Input.Props) {
|
|
||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Input
|
<ComboboxPrimitive.Input
|
||||||
data-slot="combobox-chip-input"
|
data-slot="combobox-chip-input"
|
||||||
className={cn("min-w-16 flex-1 outline-none", className)}
|
className={cn('min-w-16 flex-1 outline-none', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useComboboxAnchor() {
|
function useComboboxAnchor() {
|
||||||
return React.useRef<HTMLDivElement | null>(null)
|
return React.useRef<HTMLDivElement | null>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -310,4 +302,4 @@ export {
|
|||||||
ComboboxTrigger,
|
ComboboxTrigger,
|
||||||
ComboboxValue,
|
ComboboxValue,
|
||||||
useComboboxAnchor,
|
useComboboxAnchor,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,34 +1,26 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from 'lucide-react';
|
||||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
import { Dialog as DialogPrimitive } from 'radix-ui';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
...props
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
...props
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
...props
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
...props
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
@@ -39,12 +31,12 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
'fixed inset-0 z-50 bg-black/50 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
@@ -54,7 +46,7 @@ function DialogContent({
|
|||||||
onInteractOutside,
|
onInteractOutside,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
@@ -62,12 +54,12 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
onInteractOutside={(event) => {
|
onInteractOutside={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
onInteractOutside?.(event)
|
onInteractOutside?.(event);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -75,7 +67,7 @@ function DialogContent({
|
|||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
@@ -83,17 +75,17 @@ function DialogContent({
|
|||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({
|
function DialogFooter({
|
||||||
@@ -101,16 +93,13 @@ function DialogFooter({
|
|||||||
showCloseButton = false,
|
showCloseButton = false,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -120,20 +109,11 @@ function DialogFooter({
|
|||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
className,
|
return <DialogPrimitive.Title data-slot="dialog-title" className={cn(className)} {...props} />;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
data-slot="dialog-title"
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
@@ -143,10 +123,10 @@ function DialogDescription({
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground", className)}
|
className={cn('text-muted-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -160,4 +140,4 @@ export {
|
|||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
|
'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30',
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
'h-9 min-w-0 has-[>textarea]:h-auto',
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||||
|
|
||||||
// Focus state.
|
// Focus state.
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
|
'has-[[data-slot=input-group-control]:focus-visible]:border-ring',
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
'has-[[data-slot][aria-invalid=true]]:border-destructive',
|
||||||
|
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
const inputGroupAddonVariants = cva(
|
||||||
@@ -41,27 +41,25 @@ const inputGroupAddonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
align: {
|
align: {
|
||||||
"inline-start":
|
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
|
||||||
"inline-end":
|
'block-start':
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',
|
||||||
"block-start":
|
'block-end':
|
||||||
"order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
|
'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',
|
||||||
"block-end":
|
|
||||||
"order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
align: "inline-start",
|
align: 'inline-start',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function InputGroupAddon({
|
function InputGroupAddon({
|
||||||
className,
|
className,
|
||||||
align = "inline-start",
|
align = 'inline-start',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
@@ -69,41 +67,37 @@ function InputGroupAddon({
|
|||||||
data-align={align}
|
data-align={align}
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
if ((e.target as HTMLElement).closest('button')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
e.currentTarget.parentElement?.querySelector('input')?.focus();
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
|
||||||
"flex items-center gap-2 text-sm shadow-none",
|
variants: {
|
||||||
{
|
size: {
|
||||||
variants: {
|
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
||||||
size: {
|
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
|
||||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
|
||||||
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
|
||||||
"icon-xs":
|
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
},
|
||||||
size: "xs",
|
defaultVariants: {
|
||||||
},
|
size: 'xs',
|
||||||
}
|
},
|
||||||
)
|
});
|
||||||
|
|
||||||
function InputGroupButton({
|
function InputGroupButton({
|
||||||
className,
|
className,
|
||||||
type = "button",
|
type = 'button',
|
||||||
variant = "ghost",
|
variant = 'ghost',
|
||||||
size = "xs",
|
size = 'xs',
|
||||||
...props
|
...props
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
VariantProps<typeof inputGroupButtonVariants>) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -113,51 +107,45 @@ function InputGroupButton({
|
|||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupInput({
|
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"input">) {
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupTextarea({
|
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -167,4 +155,4 @@ export {
|
|||||||
InputGroupText,
|
InputGroupText,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
InputGroupTextarea,
|
InputGroupTextarea,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
|
||||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
'focus-visible:border-ring',
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
'aria-invalid:border-destructive',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function ScrollArea({
|
|||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:outline-none"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function SelectTrigger({
|
|||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
"flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-placeholder:text-muted-foreground data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from 'lucide-react';
|
||||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
import { Dialog as SheetPrimitive } from 'radix-ui';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTrigger({
|
function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
...props
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetClose({
|
function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
...props
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetPortal({
|
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
...props
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetOverlay({
|
function SheetOverlay({
|
||||||
@@ -36,23 +30,23 @@ function SheetOverlay({
|
|||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetContent({
|
function SheetContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
side = "right",
|
side = 'right',
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
@@ -60,62 +54,59 @@ function SheetContent({
|
|||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
'fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500',
|
||||||
side === "right" &&
|
side === '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',
|
||||||
side === "left" &&
|
side === '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-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',
|
||||||
side === "top" &&
|
side === 'top' &&
|
||||||
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
'inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
side === "bottom" &&
|
side === 'bottom' &&
|
||||||
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
'inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-header"
|
data-slot="sheet-header"
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-footer"
|
data-slot="sheet-footer"
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTitle({
|
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
data-slot="sheet-title"
|
data-slot="sheet-title"
|
||||||
className={cn("text-foreground", className)}
|
className={cn('text-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetDescription({
|
function SheetDescription({
|
||||||
@@ -125,10 +116,10 @@ function SheetDescription({
|
|||||||
return (
|
return (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
data-slot="sheet-description"
|
data-slot="sheet-description"
|
||||||
className={cn("text-muted-foreground", className)}
|
className={cn('text-muted-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -140,4 +131,4 @@ export {
|
|||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ function SidebarGroupLabel({
|
|||||||
data-slot="sidebar-group-label"
|
data-slot="sidebar-group-label"
|
||||||
data-sidebar="group-label"
|
data-sidebar="group-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
'text-sidebar-foreground/70 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -408,7 +408,7 @@ function SidebarGroupAction({
|
|||||||
data-slot="sidebar-group-action"
|
data-slot="sidebar-group-action"
|
||||||
data-sidebar="group-action"
|
data-sidebar="group-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
// Increases the hit area of the button on mobile.
|
// Increases the hit area of the button on mobile.
|
||||||
'after:absolute after:-inset-2 md:after:hidden',
|
'after:absolute after:-inset-2 md:after:hidden',
|
||||||
'group-data-[collapsible=icon]:hidden',
|
'group-data-[collapsible=icon]:hidden',
|
||||||
@@ -453,7 +453,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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-hidden transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground 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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -540,7 +540,7 @@ function SidebarMenuAction({
|
|||||||
data-slot="sidebar-menu-action"
|
data-slot="sidebar-menu-action"
|
||||||
data-sidebar="menu-action"
|
data-sidebar="menu-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
// Increases the hit area of the button on mobile.
|
// Increases the hit area of the button on mobile.
|
||||||
'after:absolute after:-inset-2 md:after:hidden',
|
'after:absolute after:-inset-2 md:after:hidden',
|
||||||
'peer-data-[size=sm]/menu-button:top-1',
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
@@ -652,7 +652,7 @@ function SidebarMenuSubButton({
|
|||||||
data-size={size}
|
data-size={size}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||||
size === 'sm' && 'text-xs',
|
size === 'sm' && 'text-xs',
|
||||||
size === 'md' && 'text-sm',
|
size === 'md' && 'text-sm',
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive md:text-sm dark:bg-input/30',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
|||||||
Reference in New Issue
Block a user