@wakastellar/ui 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +6014 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/package.json +14 -3
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- package/src/components/waka-xp-bar/index.tsx +799 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils"
|
|
5
|
+
import { Button } from "../../components/button"
|
|
6
|
+
import { Sheet, SheetContent, SheetTitle, SheetDescription } from "../../components/sheet"
|
|
7
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../../components/avatar"
|
|
8
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
9
|
+
import {
|
|
10
|
+
Collapsible,
|
|
11
|
+
CollapsibleContent,
|
|
12
|
+
CollapsibleTrigger,
|
|
13
|
+
} from "../../components/collapsible"
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuTrigger,
|
|
20
|
+
} from "../../components/dropdown-menu"
|
|
21
|
+
import { Menu, ChevronDown, LogOut, Settings, User } from "lucide-react"
|
|
22
|
+
|
|
23
|
+
// ============ TYPES ============
|
|
24
|
+
|
|
25
|
+
export interface SidebarMenuItem {
|
|
26
|
+
/** Identifiant unique */
|
|
27
|
+
id: string
|
|
28
|
+
/** Label du menu */
|
|
29
|
+
label: string
|
|
30
|
+
/** Icône (composant React) */
|
|
31
|
+
icon?: React.ReactNode
|
|
32
|
+
/** URL de navigation */
|
|
33
|
+
href?: string
|
|
34
|
+
/** Callback au clic */
|
|
35
|
+
onClick?: () => void
|
|
36
|
+
/** Sous-menus */
|
|
37
|
+
children?: SidebarMenuItem[]
|
|
38
|
+
/** Menu actif */
|
|
39
|
+
active?: boolean
|
|
40
|
+
/** Badge/compteur */
|
|
41
|
+
badge?: string | number
|
|
42
|
+
/** Désactivé */
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SidebarUserConfig {
|
|
47
|
+
/** Nom de l'utilisateur */
|
|
48
|
+
name: string
|
|
49
|
+
/** Email ou sous-titre */
|
|
50
|
+
email?: string
|
|
51
|
+
/** URL de l'avatar */
|
|
52
|
+
avatarUrl?: string
|
|
53
|
+
/** Initiales pour le fallback */
|
|
54
|
+
initials?: string
|
|
55
|
+
/** Actions du menu utilisateur */
|
|
56
|
+
actions?: {
|
|
57
|
+
id: string
|
|
58
|
+
label: string
|
|
59
|
+
icon?: React.ReactNode
|
|
60
|
+
onClick?: () => void
|
|
61
|
+
href?: string
|
|
62
|
+
variant?: "default" | "destructive"
|
|
63
|
+
}[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SidebarLogoConfig {
|
|
67
|
+
/** Image du logo */
|
|
68
|
+
src?: string
|
|
69
|
+
/** Texte alternatif */
|
|
70
|
+
alt?: string
|
|
71
|
+
/** Texte à côté du logo */
|
|
72
|
+
title?: string
|
|
73
|
+
/** URL au clic sur le logo */
|
|
74
|
+
href?: string
|
|
75
|
+
/** Callback au clic */
|
|
76
|
+
onClick?: () => void
|
|
77
|
+
/** Composant logo personnalisé */
|
|
78
|
+
component?: React.ReactNode
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface WakaSidebarProps {
|
|
82
|
+
/** Configuration du logo */
|
|
83
|
+
logo?: SidebarLogoConfig
|
|
84
|
+
/** Éléments du menu */
|
|
85
|
+
menu: SidebarMenuItem[]
|
|
86
|
+
/** Configuration utilisateur */
|
|
87
|
+
user?: SidebarUserConfig
|
|
88
|
+
/** Largeur de la sidebar (desktop) */
|
|
89
|
+
width?: number
|
|
90
|
+
/** Breakpoint pour le mode mobile (en px) */
|
|
91
|
+
mobileBreakpoint?: number
|
|
92
|
+
/** Sidebar ouverte (mode mobile) */
|
|
93
|
+
open?: boolean
|
|
94
|
+
/** Callback changement d'état (mode mobile) */
|
|
95
|
+
onOpenChange?: (open: boolean) => void
|
|
96
|
+
/** Classes CSS additionnelles */
|
|
97
|
+
className?: string
|
|
98
|
+
/** Classes CSS du contenu */
|
|
99
|
+
contentClassName?: string
|
|
100
|
+
/** Couleur de fond */
|
|
101
|
+
backgroundColor?: string
|
|
102
|
+
/** Couleur du texte */
|
|
103
|
+
textColor?: string
|
|
104
|
+
/** Couleur de l'élément actif */
|
|
105
|
+
activeColor?: string
|
|
106
|
+
/** Couleur de survol */
|
|
107
|
+
hoverColor?: string
|
|
108
|
+
/** Position du menu utilisateur */
|
|
109
|
+
userPosition?: "top" | "bottom"
|
|
110
|
+
/** Afficher le bouton hamburger */
|
|
111
|
+
showHamburger?: boolean
|
|
112
|
+
/** Position du bouton hamburger (pour usage externe) */
|
|
113
|
+
hamburgerPosition?: "left" | "right"
|
|
114
|
+
/** Rendu personnalisé d'un item */
|
|
115
|
+
renderItem?: (item: SidebarMenuItem, isChild: boolean) => React.ReactNode
|
|
116
|
+
/** Footer personnalisé */
|
|
117
|
+
footer?: React.ReactNode
|
|
118
|
+
/** Header personnalisé (après le logo) */
|
|
119
|
+
header?: React.ReactNode
|
|
120
|
+
/** Mode de positionnement : "fixed" pour app layout, "relative" pour preview/demo */
|
|
121
|
+
position?: "fixed" | "relative"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============ CONTEXT ============
|
|
125
|
+
|
|
126
|
+
interface SidebarContextValue {
|
|
127
|
+
activeId: string | null
|
|
128
|
+
setActiveId: (id: string | null) => void
|
|
129
|
+
expandedIds: string[]
|
|
130
|
+
toggleExpanded: (id: string) => void
|
|
131
|
+
isMobile: boolean
|
|
132
|
+
closeOnNavigate: boolean
|
|
133
|
+
onClose: () => void
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const SidebarContext = React.createContext<SidebarContextValue | null>(null)
|
|
137
|
+
|
|
138
|
+
function useSidebarContext() {
|
|
139
|
+
const context = React.useContext(SidebarContext)
|
|
140
|
+
if (!context) {
|
|
141
|
+
throw new Error("Sidebar components must be used within WakaSidebar")
|
|
142
|
+
}
|
|
143
|
+
return context
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============ COMPOSANTS INTERNES ============
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Logo de la sidebar
|
|
150
|
+
*/
|
|
151
|
+
function SidebarLogo({ config }: { config: SidebarLogoConfig }) {
|
|
152
|
+
const content = config.component ? (
|
|
153
|
+
config.component
|
|
154
|
+
) : (
|
|
155
|
+
<div className="flex items-center gap-3">
|
|
156
|
+
{config.src && (
|
|
157
|
+
<img
|
|
158
|
+
src={config.src}
|
|
159
|
+
alt={config.alt || "Logo"}
|
|
160
|
+
className="h-8 w-auto object-contain"
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
{config.title && (
|
|
164
|
+
<span className="text-lg font-bold tracking-wide">{config.title}</span>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if (config.href) {
|
|
170
|
+
return (
|
|
171
|
+
<a href={config.href} className="block" onClick={config.onClick}>
|
|
172
|
+
{content}
|
|
173
|
+
</a>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (config.onClick) {
|
|
178
|
+
return (
|
|
179
|
+
<button type="button" onClick={config.onClick} className="block text-left">
|
|
180
|
+
{content}
|
|
181
|
+
</button>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return <div>{content}</div>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Item de menu avec sous-menus collapsibles
|
|
190
|
+
*/
|
|
191
|
+
function SidebarMenuItemComponent({
|
|
192
|
+
item,
|
|
193
|
+
isChild = false,
|
|
194
|
+
renderItem,
|
|
195
|
+
}: {
|
|
196
|
+
item: SidebarMenuItem
|
|
197
|
+
isChild?: boolean
|
|
198
|
+
renderItem?: (item: SidebarMenuItem, isChild: boolean) => React.ReactNode
|
|
199
|
+
}) {
|
|
200
|
+
const { activeId, setActiveId, expandedIds, toggleExpanded, isMobile, closeOnNavigate, onClose } =
|
|
201
|
+
useSidebarContext()
|
|
202
|
+
|
|
203
|
+
const hasChildren = item.children && item.children.length > 0
|
|
204
|
+
const isExpanded = expandedIds.includes(item.id)
|
|
205
|
+
const isActive = activeId === item.id || item.active
|
|
206
|
+
|
|
207
|
+
const handleClick = () => {
|
|
208
|
+
if (item.disabled) return
|
|
209
|
+
|
|
210
|
+
if (hasChildren) {
|
|
211
|
+
toggleExpanded(item.id)
|
|
212
|
+
} else {
|
|
213
|
+
setActiveId(item.id)
|
|
214
|
+
item.onClick?.()
|
|
215
|
+
if (isMobile && closeOnNavigate) {
|
|
216
|
+
onClose()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Rendu personnalisé
|
|
222
|
+
if (renderItem) {
|
|
223
|
+
return <>{renderItem(item, isChild)}</>
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const itemContent = (
|
|
227
|
+
<div
|
|
228
|
+
className={cn(
|
|
229
|
+
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200",
|
|
230
|
+
"hover:bg-white/10",
|
|
231
|
+
isActive && !hasChildren && "bg-sidebar-active text-sidebar-active-foreground",
|
|
232
|
+
isChild && "ml-6 text-[13px]",
|
|
233
|
+
item.disabled && "opacity-50 cursor-not-allowed"
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
{item.icon && (
|
|
237
|
+
<span className={cn("flex-shrink-0", isChild ? "h-4 w-4" : "h-5 w-5")}>
|
|
238
|
+
{item.icon}
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
<span className="flex-1 truncate">{item.label}</span>
|
|
242
|
+
{item.badge !== undefined && (
|
|
243
|
+
<span className="ml-auto flex h-5 min-w-[20px] items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground">
|
|
244
|
+
{item.badge}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
{hasChildren && (
|
|
248
|
+
<ChevronDown
|
|
249
|
+
className={cn(
|
|
250
|
+
"h-4 w-4 transition-transform duration-200",
|
|
251
|
+
isExpanded && "rotate-180"
|
|
252
|
+
)}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// Avec sous-menus
|
|
259
|
+
if (hasChildren) {
|
|
260
|
+
return (
|
|
261
|
+
<Collapsible open={isExpanded} onOpenChange={() => toggleExpanded(item.id)}>
|
|
262
|
+
<CollapsibleTrigger asChild>
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
className="w-full text-left"
|
|
266
|
+
disabled={item.disabled}
|
|
267
|
+
>
|
|
268
|
+
{itemContent}
|
|
269
|
+
</button>
|
|
270
|
+
</CollapsibleTrigger>
|
|
271
|
+
<CollapsibleContent className="space-y-1 pt-1">
|
|
272
|
+
{item.children?.map((child) => (
|
|
273
|
+
<SidebarMenuItemComponent
|
|
274
|
+
key={child.id}
|
|
275
|
+
item={child}
|
|
276
|
+
isChild
|
|
277
|
+
renderItem={renderItem}
|
|
278
|
+
/>
|
|
279
|
+
))}
|
|
280
|
+
</CollapsibleContent>
|
|
281
|
+
</Collapsible>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Lien ou bouton simple
|
|
286
|
+
if (item.href) {
|
|
287
|
+
return (
|
|
288
|
+
<a
|
|
289
|
+
href={item.href}
|
|
290
|
+
onClick={(e) => {
|
|
291
|
+
if (item.disabled) {
|
|
292
|
+
e.preventDefault()
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
handleClick()
|
|
296
|
+
}}
|
|
297
|
+
className="block"
|
|
298
|
+
>
|
|
299
|
+
{itemContent}
|
|
300
|
+
</a>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
className="w-full text-left"
|
|
308
|
+
disabled={item.disabled}
|
|
309
|
+
onClick={handleClick}
|
|
310
|
+
>
|
|
311
|
+
{itemContent}
|
|
312
|
+
</button>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Menu utilisateur
|
|
318
|
+
*/
|
|
319
|
+
function SidebarUser({ config }: { config: SidebarUserConfig }) {
|
|
320
|
+
const initials =
|
|
321
|
+
config.initials ||
|
|
322
|
+
config.name
|
|
323
|
+
.split(" ")
|
|
324
|
+
.map((n) => n[0])
|
|
325
|
+
.join("")
|
|
326
|
+
.slice(0, 2)
|
|
327
|
+
.toUpperCase()
|
|
328
|
+
|
|
329
|
+
const defaultActions: SidebarUserConfig["actions"] = [
|
|
330
|
+
{ id: "profile", label: "Profil", icon: <User className="h-4 w-4" /> },
|
|
331
|
+
{ id: "settings", label: "Paramètres", icon: <Settings className="h-4 w-4" /> },
|
|
332
|
+
{ id: "logout", label: "Déconnexion", icon: <LogOut className="h-4 w-4" />, variant: "destructive" },
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
const actions = config.actions ?? defaultActions
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<DropdownMenu>
|
|
339
|
+
<DropdownMenuTrigger asChild>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className="flex w-full items-center gap-3 rounded-lg p-2 text-left transition-colors hover:bg-white/10"
|
|
343
|
+
>
|
|
344
|
+
<Avatar className="h-10 w-10 border-2 border-sidebar-active">
|
|
345
|
+
<AvatarImage src={config.avatarUrl} alt={config.name} />
|
|
346
|
+
<AvatarFallback className="bg-sidebar-active text-sidebar-active-foreground text-sm font-medium">
|
|
347
|
+
{initials}
|
|
348
|
+
</AvatarFallback>
|
|
349
|
+
</Avatar>
|
|
350
|
+
<div className="flex-1 min-w-0">
|
|
351
|
+
<p className="text-sm font-medium truncate">{config.name}</p>
|
|
352
|
+
{config.email && (
|
|
353
|
+
<p className="text-xs text-sidebar-foreground/70 truncate">{config.email}</p>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
<ChevronDown className="h-4 w-4 flex-shrink-0 opacity-70" />
|
|
357
|
+
</button>
|
|
358
|
+
</DropdownMenuTrigger>
|
|
359
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
360
|
+
{actions.map((action, index) => (
|
|
361
|
+
<React.Fragment key={action.id}>
|
|
362
|
+
{action.variant === "destructive" && index > 0 && <DropdownMenuSeparator />}
|
|
363
|
+
<DropdownMenuItem
|
|
364
|
+
onClick={action.onClick}
|
|
365
|
+
className={cn(action.variant === "destructive" && "text-destructive focus:text-destructive")}
|
|
366
|
+
asChild={!!action.href}
|
|
367
|
+
>
|
|
368
|
+
{action.href ? (
|
|
369
|
+
<a href={action.href} className="flex items-center gap-2">
|
|
370
|
+
{action.icon}
|
|
371
|
+
{action.label}
|
|
372
|
+
</a>
|
|
373
|
+
) : (
|
|
374
|
+
<span className="flex items-center gap-2">
|
|
375
|
+
{action.icon}
|
|
376
|
+
{action.label}
|
|
377
|
+
</span>
|
|
378
|
+
)}
|
|
379
|
+
</DropdownMenuItem>
|
|
380
|
+
</React.Fragment>
|
|
381
|
+
))}
|
|
382
|
+
</DropdownMenuContent>
|
|
383
|
+
</DropdownMenu>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Contenu de la sidebar
|
|
389
|
+
*/
|
|
390
|
+
function SidebarContent({
|
|
391
|
+
logo,
|
|
392
|
+
menu,
|
|
393
|
+
user,
|
|
394
|
+
userPosition = "bottom",
|
|
395
|
+
header,
|
|
396
|
+
footer,
|
|
397
|
+
renderItem,
|
|
398
|
+
}: Pick<WakaSidebarProps, "logo" | "menu" | "user" | "userPosition" | "header" | "footer" | "renderItem">) {
|
|
399
|
+
return (
|
|
400
|
+
<div className="flex h-full flex-col">
|
|
401
|
+
{/* Logo */}
|
|
402
|
+
{logo && (
|
|
403
|
+
<div className="flex-shrink-0 p-4 pb-2">
|
|
404
|
+
<SidebarLogo config={logo} />
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{/* Header personnalisé */}
|
|
409
|
+
{header && <div className="flex-shrink-0 px-4 py-2">{header}</div>}
|
|
410
|
+
|
|
411
|
+
{/* User en haut */}
|
|
412
|
+
{user && userPosition === "top" && (
|
|
413
|
+
<div className="flex-shrink-0 px-3 py-2 border-b border-white/10">
|
|
414
|
+
<SidebarUser config={user} />
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
{/* Menu scrollable */}
|
|
419
|
+
<ScrollArea className="flex-1 px-3 py-4">
|
|
420
|
+
<nav className="space-y-1">
|
|
421
|
+
{menu.map((item) => (
|
|
422
|
+
<SidebarMenuItemComponent key={item.id} item={item} renderItem={renderItem} />
|
|
423
|
+
))}
|
|
424
|
+
</nav>
|
|
425
|
+
</ScrollArea>
|
|
426
|
+
|
|
427
|
+
{/* Footer personnalisé */}
|
|
428
|
+
{footer && <div className="flex-shrink-0 px-4 py-2 border-t border-white/10">{footer}</div>}
|
|
429
|
+
|
|
430
|
+
{/* User en bas */}
|
|
431
|
+
{user && userPosition === "bottom" && (
|
|
432
|
+
<div className="flex-shrink-0 p-3 border-t border-white/10">
|
|
433
|
+
<SidebarUser config={user} />
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ============ COMPOSANT PRINCIPAL ============
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* WakaSidebar - Sidebar personnalisable avec menu hamburger responsive
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```tsx
|
|
447
|
+
* <WakaSidebar
|
|
448
|
+
* logo={{ src: "/logo.svg", title: "WAKASTART" }}
|
|
449
|
+
* menu={[
|
|
450
|
+
* { id: "dashboard", label: "Tableau de bord", icon: <Home /> },
|
|
451
|
+
* {
|
|
452
|
+
* id: "admin",
|
|
453
|
+
* label: "Administration",
|
|
454
|
+
* icon: <Settings />,
|
|
455
|
+
* children: [
|
|
456
|
+
* { id: "users", label: "Utilisateurs" },
|
|
457
|
+
* { id: "roles", label: "Rôles" },
|
|
458
|
+
* ],
|
|
459
|
+
* },
|
|
460
|
+
* ]}
|
|
461
|
+
* user={{
|
|
462
|
+
* name: "John Doe",
|
|
463
|
+
* email: "john@example.com",
|
|
464
|
+
* avatarUrl: "/avatar.jpg",
|
|
465
|
+
* }}
|
|
466
|
+
* />
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
export function WakaSidebar({
|
|
470
|
+
logo,
|
|
471
|
+
menu,
|
|
472
|
+
user,
|
|
473
|
+
width = 260,
|
|
474
|
+
mobileBreakpoint = 768,
|
|
475
|
+
open,
|
|
476
|
+
onOpenChange,
|
|
477
|
+
className,
|
|
478
|
+
contentClassName,
|
|
479
|
+
backgroundColor,
|
|
480
|
+
textColor,
|
|
481
|
+
activeColor,
|
|
482
|
+
hoverColor,
|
|
483
|
+
userPosition = "bottom",
|
|
484
|
+
showHamburger = true,
|
|
485
|
+
hamburgerPosition = "left",
|
|
486
|
+
renderItem,
|
|
487
|
+
footer,
|
|
488
|
+
header,
|
|
489
|
+
position = "fixed",
|
|
490
|
+
}: WakaSidebarProps) {
|
|
491
|
+
// État interne pour mobile si non contrôlé
|
|
492
|
+
const [internalOpen, setInternalOpen] = React.useState(false)
|
|
493
|
+
const isControlled = open !== undefined
|
|
494
|
+
const isOpen = isControlled ? open : internalOpen
|
|
495
|
+
const setIsOpen = isControlled ? onOpenChange! : setInternalOpen
|
|
496
|
+
|
|
497
|
+
// Détection mobile
|
|
498
|
+
const [isMobile, setIsMobile] = React.useState(false)
|
|
499
|
+
|
|
500
|
+
React.useEffect(() => {
|
|
501
|
+
const checkMobile = () => {
|
|
502
|
+
setIsMobile(window.innerWidth < mobileBreakpoint)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
checkMobile()
|
|
506
|
+
window.addEventListener("resize", checkMobile)
|
|
507
|
+
return () => window.removeEventListener("resize", checkMobile)
|
|
508
|
+
}, [mobileBreakpoint])
|
|
509
|
+
|
|
510
|
+
// État des menus
|
|
511
|
+
const [activeId, setActiveId] = React.useState<string | null>(() => {
|
|
512
|
+
// Trouver l'élément actif par défaut
|
|
513
|
+
const findActive = (items: SidebarMenuItem[]): string | null => {
|
|
514
|
+
for (const item of items) {
|
|
515
|
+
if (item.active) return item.id
|
|
516
|
+
if (item.children) {
|
|
517
|
+
const childActive = findActive(item.children)
|
|
518
|
+
if (childActive) return childActive
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return null
|
|
522
|
+
}
|
|
523
|
+
return findActive(menu)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
const [expandedIds, setExpandedIds] = React.useState<string[]>(() => {
|
|
527
|
+
// Ouvrir les parents des éléments actifs
|
|
528
|
+
const findParentsOfActive = (items: SidebarMenuItem[], parentIds: string[] = []): string[] => {
|
|
529
|
+
for (const item of items) {
|
|
530
|
+
if (item.active) return parentIds
|
|
531
|
+
if (item.children) {
|
|
532
|
+
const result = findParentsOfActive(item.children, [...parentIds, item.id])
|
|
533
|
+
if (result.length > parentIds.length) return result
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return []
|
|
537
|
+
}
|
|
538
|
+
return findParentsOfActive(menu)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const toggleExpanded = React.useCallback((id: string) => {
|
|
542
|
+
setExpandedIds((prev) =>
|
|
543
|
+
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
|
|
544
|
+
)
|
|
545
|
+
}, [])
|
|
546
|
+
|
|
547
|
+
const contextValue: SidebarContextValue = {
|
|
548
|
+
activeId,
|
|
549
|
+
setActiveId,
|
|
550
|
+
expandedIds,
|
|
551
|
+
toggleExpanded,
|
|
552
|
+
isMobile,
|
|
553
|
+
closeOnNavigate: true,
|
|
554
|
+
onClose: () => setIsOpen(false),
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Styles personnalisés
|
|
558
|
+
const customStyles = {
|
|
559
|
+
"--sidebar-bg": backgroundColor || "hsl(222 47% 11%)",
|
|
560
|
+
"--sidebar-text": textColor || "hsl(210 40% 96%)",
|
|
561
|
+
"--sidebar-active": activeColor || "hsl(187 85% 43%)",
|
|
562
|
+
"--sidebar-active-foreground": "hsl(222 47% 11%)",
|
|
563
|
+
"--sidebar-hover": hoverColor || "rgba(255, 255, 255, 0.1)",
|
|
564
|
+
} as React.CSSProperties
|
|
565
|
+
|
|
566
|
+
const sidebarClasses = cn(
|
|
567
|
+
"bg-[var(--sidebar-bg)] text-[var(--sidebar-text)]",
|
|
568
|
+
"[&_.bg-sidebar-active]:bg-[var(--sidebar-active)]",
|
|
569
|
+
"[&_.text-sidebar-active-foreground]:text-[var(--sidebar-active-foreground)]",
|
|
570
|
+
"[&_.text-sidebar-foreground\\/70]:text-[var(--sidebar-text)]/70",
|
|
571
|
+
"[&_.border-sidebar-active]:border-[var(--sidebar-active)]",
|
|
572
|
+
contentClassName
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
// Contenu de la sidebar
|
|
576
|
+
const sidebarContent = (
|
|
577
|
+
<SidebarContext.Provider value={contextValue}>
|
|
578
|
+
<div className={cn("h-full", sidebarClasses)} style={customStyles}>
|
|
579
|
+
<SidebarContent
|
|
580
|
+
logo={logo}
|
|
581
|
+
menu={menu}
|
|
582
|
+
user={user}
|
|
583
|
+
userPosition={userPosition}
|
|
584
|
+
header={header}
|
|
585
|
+
footer={footer}
|
|
586
|
+
renderItem={renderItem}
|
|
587
|
+
/>
|
|
588
|
+
</div>
|
|
589
|
+
</SidebarContext.Provider>
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<>
|
|
594
|
+
{/* Bouton hamburger (pour mobile) */}
|
|
595
|
+
{showHamburger && isMobile && (
|
|
596
|
+
<Button
|
|
597
|
+
variant="ghost"
|
|
598
|
+
size="icon"
|
|
599
|
+
className={cn(
|
|
600
|
+
"fixed top-3 z-50",
|
|
601
|
+
hamburgerPosition === "left" ? "left-3" : "right-3"
|
|
602
|
+
)}
|
|
603
|
+
onClick={() => setIsOpen(true)}
|
|
604
|
+
>
|
|
605
|
+
<Menu className="h-6 w-6" />
|
|
606
|
+
<span className="sr-only">Ouvrir le menu</span>
|
|
607
|
+
</Button>
|
|
608
|
+
)}
|
|
609
|
+
|
|
610
|
+
{/* Mode Desktop */}
|
|
611
|
+
{!isMobile && (
|
|
612
|
+
<aside
|
|
613
|
+
className={cn(
|
|
614
|
+
position === "fixed" ? "fixed left-0 top-0 h-screen z-40" : "relative h-full",
|
|
615
|
+
className
|
|
616
|
+
)}
|
|
617
|
+
style={{ width }}
|
|
618
|
+
>
|
|
619
|
+
{sidebarContent}
|
|
620
|
+
</aside>
|
|
621
|
+
)}
|
|
622
|
+
|
|
623
|
+
{/* Mode Mobile (Sheet) */}
|
|
624
|
+
{isMobile && (
|
|
625
|
+
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
|
626
|
+
<SheetContent
|
|
627
|
+
side="left"
|
|
628
|
+
className={cn("w-[280px] p-0 border-0", className)}
|
|
629
|
+
style={customStyles}
|
|
630
|
+
aria-describedby="sidebar-description"
|
|
631
|
+
>
|
|
632
|
+
<SheetTitle className="sr-only">Menu de navigation</SheetTitle>
|
|
633
|
+
<SheetDescription id="sidebar-description" className="sr-only">
|
|
634
|
+
Menu principal de l'application
|
|
635
|
+
</SheetDescription>
|
|
636
|
+
{sidebarContent}
|
|
637
|
+
</SheetContent>
|
|
638
|
+
</Sheet>
|
|
639
|
+
)}
|
|
640
|
+
</>
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ============ HOOK UTILITAIRE ============
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Hook pour gérer l'état de la sidebar externalement
|
|
648
|
+
*/
|
|
649
|
+
export function useSidebar() {
|
|
650
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
651
|
+
|
|
652
|
+
const open = React.useCallback(() => setIsOpen(true), [])
|
|
653
|
+
const close = React.useCallback(() => setIsOpen(false), [])
|
|
654
|
+
const toggle = React.useCallback(() => setIsOpen((prev) => !prev), [])
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
isOpen,
|
|
658
|
+
setIsOpen,
|
|
659
|
+
open,
|
|
660
|
+
close,
|
|
661
|
+
toggle,
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============ COMPOSANT LAYOUT AVEC SIDEBAR ============
|
|
666
|
+
|
|
667
|
+
export interface SidebarLayoutProps {
|
|
668
|
+
/** Configuration de la sidebar */
|
|
669
|
+
sidebar: WakaSidebarProps
|
|
670
|
+
/** Contenu principal */
|
|
671
|
+
children: React.ReactNode
|
|
672
|
+
/** Largeur de la sidebar (override) */
|
|
673
|
+
sidebarWidth?: number
|
|
674
|
+
/** Classes CSS du conteneur */
|
|
675
|
+
className?: string
|
|
676
|
+
/** Classes CSS du contenu principal */
|
|
677
|
+
contentClassName?: string
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Layout avec sidebar intégrée
|
|
682
|
+
*/
|
|
683
|
+
export function SidebarLayout({
|
|
684
|
+
sidebar,
|
|
685
|
+
children,
|
|
686
|
+
sidebarWidth = 260,
|
|
687
|
+
className,
|
|
688
|
+
contentClassName,
|
|
689
|
+
}: SidebarLayoutProps) {
|
|
690
|
+
const [isMobile, setIsMobile] = React.useState(false)
|
|
691
|
+
const breakpoint = sidebar.mobileBreakpoint || 768
|
|
692
|
+
|
|
693
|
+
React.useEffect(() => {
|
|
694
|
+
const checkMobile = () => setIsMobile(window.innerWidth < breakpoint)
|
|
695
|
+
checkMobile()
|
|
696
|
+
window.addEventListener("resize", checkMobile)
|
|
697
|
+
return () => window.removeEventListener("resize", checkMobile)
|
|
698
|
+
}, [breakpoint])
|
|
699
|
+
|
|
700
|
+
return (
|
|
701
|
+
<div className={cn("min-h-screen bg-background", className)}>
|
|
702
|
+
<WakaSidebar {...sidebar} width={sidebarWidth} />
|
|
703
|
+
<main
|
|
704
|
+
className={cn("min-h-screen transition-all duration-300", contentClassName)}
|
|
705
|
+
style={{ marginLeft: isMobile ? 0 : sidebarWidth }}
|
|
706
|
+
>
|
|
707
|
+
{children}
|
|
708
|
+
</main>
|
|
709
|
+
</div>
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export default WakaSidebar
|