diff --git a/bun.lock b/bun.lock
index c975194..3feb12d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -51,6 +51,7 @@
"tw-animate-css": "^1.3.5",
"typescript": "^5.8.3",
"uuid": "^11.1.0",
+ "vaul": "^1.1.2",
"zod": "^3.25.75",
},
"devDependencies": {
@@ -1453,6 +1454,8 @@
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
+ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
+
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
diff --git a/package.json b/package.json
index 7f92bee..05e5838 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"tw-animate-css": "^1.3.5",
"typescript": "^5.8.3",
"uuid": "^11.1.0",
+ "vaul": "^1.1.2",
"zod": "^3.25.75"
},
"devDependencies": {
diff --git a/src/components/activity/ActivityLog.tsx b/src/components/activity/ActivityLog.tsx
index c3e5431..d650d14 100644
--- a/src/components/activity/ActivityLog.tsx
+++ b/src/components/activity/ActivityLog.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState, useRef } from 'react';
import { Button } from '@/components/ui/button';
-import { ChevronDown, Download, RefreshCw, Search, Trash2 } from 'lucide-react';
+import { ChevronDown, Download, RefreshCw, Search, Trash2, Filter } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
@@ -36,6 +36,16 @@ import { toast } from 'sonner';
import { useLiveRefresh } from '@/hooks/useLiveRefresh';
import { useConfigStatus } from '@/hooks/useConfigStatus';
import { useNavigation } from '@/components/layout/MainLayout';
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from '@/components/ui/drawer';
type MirrorJobWithKey = MirrorJob & { _rowKey: string };
@@ -343,18 +353,225 @@ export function ActivityLog() {
setShowCleanupDialog(false);
};
+ // Check if any filters are active
+ const hasActiveFilters = !!(filter.status || filter.type || filter.name);
+ const activeFilterCount = [filter.status, filter.type, filter.name].filter(Boolean).length;
+
+ // Clear all filters
+ const clearFilters = () => {
+ setFilter({
+ searchTerm: filter.searchTerm,
+ status: '',
+ type: '',
+ name: '',
+ });
+ };
+
/* ------------------------------ UI ------------------------------ */
return (
-
+ {/* Mobile: Search bar with filter and action buttons */}
+
+
+
+
+
+ setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
+ }
+ />
+
+
+ {/* Mobile Filter Drawer */}
+
+
+
+
+ {activeFilterCount > 0 && (
+
+ {activeFilterCount}
+
+ )}
+
+
+
+
+ Filter Activities
+
+ Narrow down your activity log
+
+
+
+
+ {/* Active filters summary */}
+ {hasActiveFilters && (
+
+
+ {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active
+
+
+ Clear all
+
+
+ )}
+
+ {/* Status Filter */}
+
+
+ By Status
+ {filter.status && (
+
+ {filter.status.charAt(0).toUpperCase() + filter.status.slice(1)}
+
+ )}
+
+
+ setFilter((p) => ({
+ ...p,
+ status: v === 'all' ? '' : (v as RepoStatus),
+ }))
+ }
+ >
+
+
+
+
+ {['all', ...repoStatusEnum.options].map((s) => (
+
+
+ {s !== 'all' && (
+
+ )}
+ {s === 'all' ? 'All statuses' : s[0].toUpperCase() + s.slice(1)}
+
+
+ ))}
+
+
+
+
+ {/* Type Filter */}
+
+
+ By Type
+ {filter.type && (
+
+ {filter.type.charAt(0).toUpperCase() + filter.type.slice(1)}
+
+ )}
+
+
+ setFilter((p) => ({ ...p, type: v === 'all' ? '' : v }))
+ }
+ >
+
+
+
+
+ {['all', 'repository', 'organization'].map((t) => (
+
+
+ {t !== 'all' && (
+
+ )}
+ {t === 'all' ? 'All types' : t[0].toUpperCase() + t.slice(1)}
+
+
+ ))}
+
+
+
+
+ {/* Name Filter */}
+
+
+ By Name
+ {filter.name && (
+
+ Selected
+
+ )}
+
+
setFilter((p) => ({ ...p, name }))}
+ />
+
+
+
+
+
+
+ Apply Filters
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+
fetchActivities(false)}
+ title="Refresh activity log"
+ className="h-10 w-10 shrink-0"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Desktop: Original layout */}
+
{/* search input */}
-
-
+
+
setFilter((prev) => ({
@@ -365,8 +582,8 @@ export function ActivityLog() {
/>
- {/* Filter controls row */}
-
+ {/* Filter controls */}
+
{/* status select */}
-
-
+
+
{['all', ...repoStatusEnum.options].map((s) => (
- {s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)}
+
+ {s !== 'all' && (
+
+ )}
+ {s === 'all' ? 'All statuses' : s[0].toUpperCase() + s.slice(1)}
+
))}
- {/* type select - hidden on mobile */}
+ {/* type select */}
setFilter((p) => ({ ...p, type: v === 'all' ? '' : v }))
}
>
-
-
+
+
{['all', 'repository', 'organization'].map((t) => (
- {t === 'all' ? 'All Types' : t[0].toUpperCase() + t.slice(1)}
+
+ {t !== 'all' && (
+
+ )}
+ {t === 'all' ? 'All types' : t[0].toUpperCase() + t.slice(1)}
+
))}
- {/* repo/org name combobox - hidden on mobile */}
-
-
setFilter((p) => ({ ...p, name }))}
- />
-
+ {/* repo/org name combobox */}
+
setFilter((p) => ({ ...p, name }))}
+ />
- {/* Action buttons row */}
-
- {/* export dropdown - text hidden on mobile */}
+ {/* Action buttons */}
+
+ {/* export dropdown */}
-
-
- Export
-
+
+
+ Export
+
-
+
Export as CSV
@@ -443,11 +675,11 @@ export function ActivityLog() {
fetchActivities(false)} // Manual refresh, show loading skeleton
+ onClick={() => fetchActivities(false)}
title="Refresh activity log"
- className='h-8 w-8 sm:h-9 sm:w-9'
+ className="h-10 w-10"
>
-
+
{/* cleanup all activities */}
@@ -456,9 +688,9 @@ export function ActivityLog() {
size="icon"
onClick={handleCleanupClick}
title="Delete all activities"
- className="text-destructive hover:text-destructive h-8 w-8 sm:h-9 sm:w-9"
+ className="text-destructive hover:text-destructive h-10 w-10"
>
-
+
@@ -495,6 +727,26 @@ export function ActivityLog() {
+
+ {/* Mobile FAB for Export - only visible on mobile */}
+
+
+
+
+
+
+
+
+ Export as CSV
+
+
+ Export as JSON
+
+
+
);
}
diff --git a/src/components/activity/ActivityNameCombobox.tsx b/src/components/activity/ActivityNameCombobox.tsx
index bdac6f7..77d24f2 100644
--- a/src/components/activity/ActivityNameCombobox.tsx
+++ b/src/components/activity/ActivityNameCombobox.tsx
@@ -41,9 +41,14 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa
variant="outline"
role="combobox"
aria-expanded={open}
- className="w-[180px] justify-between"
+ className="w-full sm:w-[180px] justify-between h-10"
>
- {value ? value : "All Names"}
+
+ {value || "All names"}
+
@@ -62,7 +67,7 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa
}}
>
- All Names
+ All names
{names.map((name) => (
Automation & Maintenance
+
+
+
+
+
+ Background operations info
+
+
+
+
+
Background Operations
+
+ These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance.
+ Choose intervals that match your workflow and repository update frequency.
+
+
+
+
+
@@ -311,21 +330,6 @@ export function AutomationSettings({
-
-
-
-
-
-
- Background Operations
-
-
- These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance.
- Choose intervals that match your workflow and repository update frequency.
-
-
-
-
);
diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx
index c75eb5e..d6fdbbf 100644
--- a/src/components/config/ConfigTabs.tsx
+++ b/src/components/config/ConfigTabs.tsx
@@ -566,14 +566,14 @@ export function ConfigTabs() {
- Configuration Settings
+ Configuration
Configure your GitHub and Gitea connections, and set up automatic
mirroring.
-
+
{isSyncing ? (
<>
diff --git a/src/components/config/OrganizationStrategy.tsx b/src/components/config/OrganizationStrategy.tsx
index 8014393..c6f0315 100644
--- a/src/components/config/OrganizationStrategy.tsx
+++ b/src/components/config/OrganizationStrategy.tsx
@@ -303,7 +303,7 @@ export const OrganizationStrategy: React.FC = ({
Override Options
-
+
Fine-tune Your Mirror Destinations
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 3eea9c1..96e2ef2 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -125,42 +125,24 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
{isLoading ? (
) : user ? (
- <>
- {/* Desktop: Show avatar and logout button */}
-
-
-
-
- {user.username.charAt(0).toUpperCase()}
-
-
-
- Logout
+
+
+
+
+
+
+ {user.username.charAt(0).toUpperCase()}
+
+
-
-
- {/* Mobile: Avatar with dropdown */}
-
-
-
-
-
-
-
- {user.username.charAt(0).toUpperCase()}
-
-
-
-
-
-
-
- Logout
-
-
-
-
- >
+
+
+
+
+ Logout
+
+
+
) : (
Login
diff --git a/src/components/organizations/Organization.tsx b/src/components/organizations/Organization.tsx
index 78b28fb..575d553 100644
--- a/src/components/organizations/Organization.tsx
+++ b/src/components/organizations/Organization.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
-import { Search, RefreshCw, FlipHorizontal } from "lucide-react";
+import { Search, RefreshCw, FlipHorizontal, Filter } from "lucide-react";
import type { MirrorJob, Organization } from "@/lib/db/schema";
import { OrganizationList } from "./OrganizationsList";
import AddOrganizationDialog from "./AddOrganizationDialog";
@@ -27,6 +27,16 @@ import { toast } from "sonner";
import { useConfigStatus } from "@/hooks/useConfigStatus";
import { useNavigation } from "@/components/layout/MainLayout";
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer";
export function Organization() {
const [organizations, setOrganizations] = useState([]);
@@ -290,120 +300,351 @@ export function Organization() {
}
};
+ // Check if any filters are active
+ const hasActiveFilters = !!(filter.membershipRole || filter.status);
+ const activeFilterCount = [filter.membershipRole, filter.status].filter(Boolean).length;
+ // Clear all filters
+ const clearFilters = () => {
+ setFilter({
+ searchTerm: filter.searchTerm,
+ membershipRole: "",
+ status: "",
+ });
+ };
return (
{/* Search and filters */}
-
-
-
- setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
- }
- />
-
+ {/* Mobile: Search bar with filter button */}
+
+
+
+
+ setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
+ }
+ />
+
+
+ {/* Mobile Filter Drawer */}
+
+
+
+
+ {activeFilterCount > 0 && (
+
+ {activeFilterCount}
+
+ )}
+
+
+
+
+ Filter Organizations
+
+ Narrow down your organization list
+
+
+
+
+ {/* Active filters summary */}
+ {hasActiveFilters && (
+
+
+ {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active
+
+
+ Clear all
+
+
+ )}
- {/* Filter controls */}
-
- {/* Membership Role Filter */}
-
- setFilter((prev) => ({
- ...prev,
- membershipRole: value === "all" ? "" : (value as MembershipRole),
- }))
- }
- >
-
-
-
-
- {["all", ...membershipRoleEnum.options].map((role) => (
-
- {role === "all"
- ? "All Roles"
- : role
- .replace(/_/g, " ")
- .replace(/\b\w/g, (c) => c.toUpperCase())}
-
- ))}
-
-
+ {/* Role Filter */}
+
+
+ By Role
+ {filter.membershipRole && (
+
+ {filter.membershipRole
+ .replace(/_/g, " ")
+ .replace(/\b\w/g, (c) => c.toUpperCase())}
+
+ )}
+
+
+ setFilter((prev) => ({
+ ...prev,
+ membershipRole: value === "all" ? "" : (value as MembershipRole),
+ }))
+ }
+ >
+
+
+
+
+ {["all", ...membershipRoleEnum.options].map((role) => (
+
+
+ {role !== "all" && (
+
+ )}
+ {role === "all"
+ ? "All roles"
+ : role
+ .replace(/_/g, " ")
+ .replace(/\b\w/g, (c) => c.toUpperCase())}
+
+
+ ))}
+
+
+
- {/* Status Filter */}
-
- setFilter((prev) => ({
- ...prev,
- status:
- value === "all"
- ? ""
- : (value as
- | ""
- | "imported"
- | "mirroring"
- | "mirrored"
- | "failed"
- | "syncing"
- | "synced"),
- }))
- }
- >
-
-
-
-
- {[
- "all",
- "imported",
- "mirroring",
- "mirrored",
- "failed",
- "syncing",
- "synced",
- ].map((status) => (
-
- {status === "all"
- ? "All Statuses"
- : status.charAt(0).toUpperCase() + status.slice(1)}
-
- ))}
-
-
-
-
- {/* Action buttons */}
-
+ {/* Status Filter */}
+
+
+ By Status
+ {filter.status && (
+
+ {filter.status.charAt(0).toUpperCase() + filter.status.slice(1)}
+
+ )}
+
+
+ setFilter((prev) => ({
+ ...prev,
+ status:
+ value === "all"
+ ? ""
+ : (value as
+ | ""
+ | "imported"
+ | "mirroring"
+ | "mirrored"
+ | "failed"
+ | "syncing"
+ | "synced"),
+ }))
+ }
+ >
+
+
+
+
+ {[
+ "all",
+ "imported",
+ "mirroring",
+ "mirrored",
+ "failed",
+ "syncing",
+ "synced",
+ ].map((status) => (
+
+
+ {status !== "all" && (
+
+ )}
+ {status === "all"
+ ? "All statuses"
+ : status.charAt(0).toUpperCase() + status.slice(1)}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Apply Filters
+
+
+
+
+ Cancel
+
+
+
+
+
+
-
+
0}
- className="sm:size-default"
+ title="Mirror all organizations"
+ className="h-10 w-10 shrink-0"
>
-
- Mirror All
- Mirror
+
+
+ {/* Desktop: Original layout */}
+
+
+
+
+ setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
+ }
+ />
+
+
+ {/* Filter controls */}
+
+ {/* Membership Role Filter */}
+
+ setFilter((prev) => ({
+ ...prev,
+ membershipRole: value === "all" ? "" : (value as MembershipRole),
+ }))
+ }
+ >
+
+
+
+
+ {["all", ...membershipRoleEnum.options].map((role) => (
+
+
+ {role !== "all" && (
+
+ )}
+ {role === "all"
+ ? "All roles"
+ : role
+ .replace(/_/g, " ")
+ .replace(/\b\w/g, (c) => c.toUpperCase())}
+
+
+ ))}
+
+
+
+ {/* Status Filter */}
+
+ setFilter((prev) => ({
+ ...prev,
+ status:
+ value === "all"
+ ? ""
+ : (value as
+ | ""
+ | "imported"
+ | "mirroring"
+ | "mirrored"
+ | "failed"
+ | "syncing"
+ | "synced"),
+ }))
+ }
+ >
+
+
+
+
+ {[
+ "all",
+ "imported",
+ "mirroring",
+ "mirrored",
+ "failed",
+ "syncing",
+ "synced",
+ ].map((status) => (
+
+
+ {status !== "all" && (
+
+ )}
+ {status === "all"
+ ? "All statuses"
+ : status.charAt(0).toUpperCase() + status.slice(1)}
+
+
+ ))}
+
+
+
+
+ {/* Action buttons */}
+
+
+
+
+
+ 0}
+ className="h-10 px-4"
+ >
+
+ Mirror All
+
+
+
{
+ setFilter({
+ searchTerm: filter.searchTerm,
+ status: "",
+ organization: "",
+ owner: "",
+ });
+ };
+
return (
{/* Search and filters */}
-
-
-
- setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
- }
- />
-
+ {/* Mobile: Search bar with filter button */}
+
+
+
+
+ setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
+ }
+ />
+
+
+ {/* Mobile Filter Drawer */}
+
+
+
+
+ {activeFilterCount > 0 && (
+
+ {activeFilterCount}
+
+ )}
+
+
+
+
+ Filter Repositories
+
+ Narrow down your repository list
+
+
+
+
+ {/* Active filters summary */}
+ {hasActiveFilters && (
+
+
+ {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active
+
+
+ Clear all
+
+
+ )}
- {/* Owner Combobox */}
-
- setFilter((prev) => ({ ...prev, owner }))
- }
- />
+ {/* Owner Filter */}
+
+
+ By Owner
+ {filter.owner && (
+
+ Selected
+
+ )}
+
+
+ setFilter((prev) => ({ ...prev, owner }))
+ }
+ />
+
- {/* Organization Combobox */}
-
- setFilter((prev) => ({ ...prev, organization }))
- }
- />
-
- {/* Filter controls in a responsive row */}
-
-
- setFilter((prev) => ({
- ...prev,
- status: value === "all" ? "" : (value as RepoStatus),
- }))
- }
- >
-
-
-
-
- {["all", ...repoStatusEnum.options].map((status) => (
-
- {status === "all"
- ? "All Status"
- : status.charAt(0).toUpperCase() + status.slice(1)}
-
- ))}
-
-
+ {/* Organization Filter */}
+
+
+ By Organization
+ {filter.organization && (
+
+ Selected
+
+ )}
+
+
+ setFilter((prev) => ({ ...prev, organization }))
+ }
+ />
+
+ {/* Status Filter */}
+
+
+ By Status
+ {filter.status && (
+
+ {filter.status.charAt(0).toUpperCase() + filter.status.slice(1)}
+
+ )}
+
+
+ setFilter((prev) => ({
+ ...prev,
+ status: value === "all" ? "" : (value as RepoStatus),
+ }))
+ }
+ >
+
+
+
+
+ {["all", ...repoStatusEnum.options].map((status) => (
+
+
+ {status !== "all" && (
+
+ )}
+ {status === "all"
+ ? "All statuses"
+ : status.charAt(0).toUpperCase() + status.slice(1)}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Apply Filters
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+ {selectedRepoIds.size === 0 && (
+ 0}
+ title="Mirror all repositories"
+ className="h-10 w-10 shrink-0"
+ >
+
+
+ )}
+
+
+ {/* Desktop: Original layout */}
+
+
+
+
+ setFilter((prev) => ({ ...prev, searchTerm: e.target.value }))
+ }
+ />
+
+
+ {/* Owner Combobox */}
+
+ setFilter((prev) => ({ ...prev, owner }))
+ }
+ />
+
+ {/* Organization Combobox */}
+
+ setFilter((prev) => ({ ...prev, organization }))
+ }
+ />
+
+ {/* Filter controls in a responsive row */}
+
+
+ setFilter((prev) => ({
+ ...prev,
+ status: value === "all" ? "" : (value as RepoStatus),
+ }))
+ }
+ >
+
+
+
+
+ {["all", ...repoStatusEnum.options].map((status) => (
+
+
+ {status !== "all" && (
+
+ )}
+ {status === "all"
+ ? "All statuses"
+ : status.charAt(0).toUpperCase() + status.slice(1)}
+
+
+ ))}
+
+
+
+
+
+
+
- {/* Action buttons - separate row on mobile */}
-
+ {/* Action buttons - shows when items are selected or Mirror All on desktop */}
+
{selectedRepoIds.size === 0 ? (
0}
- className="w-full sm:w-auto"
+ className="w-auto"
>
Mirror All
) : (
- <>
+ <>
{selectedRepoIds.size} selected
diff --git a/src/components/repositories/RepositoryComboboxes.tsx b/src/components/repositories/RepositoryComboboxes.tsx
index 56f3ae4..c581e54 100644
--- a/src/components/repositories/RepositoryComboboxes.tsx
+++ b/src/components/repositories/RepositoryComboboxes.tsx
@@ -33,17 +33,22 @@ export function OwnerCombobox({ options, value, onChange, placeholder = "Owner"
variant="outline"
role="combobox"
aria-expanded={open}
- className="w-full sm:w-[160px] justify-between"
+ className="w-full sm:w-[160px] justify-between h-10"
>
- {value ? value : placeholder}
+
+ {value || "All owners"}
+
-
+
- No {placeholder.toLowerCase()} found.
+ No owners found.
- All
+ All owners
{options.map((option) => (
- {value ? value : placeholder}
+
+ {value || "All organizations"}
+
-
+
- No {placeholder.toLowerCase()} found.
+ No organizations found.
- All
+ All organizations
{options.map((option) => (
) {
+ return
+}
+
+function DrawerTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DrawerContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DrawerDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+}