From 432f563bf65060def36f212a2bd29778517f06f3 Mon Sep 17 00:00:00 2001 From: Abdul Haris Djafar Date: Mon, 18 May 2026 12:39:49 +0700 Subject: [PATCH 01/35] refactor(app): port editorial tokens, redesign Login Bring the editorial design language from the landing page into the main SPA, starting with shared tokens and the Login screen. Foundation (src/index.css) - Import Geist & Geist Mono via Google Fonts - Remove the dead duplicate HSL :root/.dark block left from an earlier shadcn migration; only the oklch block was actually in effect - @theme additions expose editorial tokens as v4 utilities: ink-0..800, paper / paper-muted / dim / faint, brand / brand-soft / brand-dim, font-sans (Geist), font-mono (Geist Mono) - Dark-mode shadcn semantic vars remapped: - --primary now ClickHouse yellow -> all - - - - + + {/* Form */} +
+ + + ( + + + Email or username + + +
+ + +
+
+ +
+ )} + /> + + ( + + + Password + + +
+ + + +
+
+ +
+ )} + /> + + {error && ( +
+ ! + {error} +
)} - /> - {error && ( - - {error} - - )} - - - - - - -
- - Role-based access control + {isLoading ? ( + <> + + Signing in… + + ) : ( + <> + Sign in + + + )} + + +
-
- - + + {/* Footer */} +
+ + + RBAC enforced + + + v{__CH_UI_VERSION__} + +
+
+ + {/* Demo creds hint (dev convenience) */} +

+ Demo · admin@localhost · admin123! +

+ + ); } From 43203094ef8a72138c9a78480b3b7851f9f1817e Mon Sep 17 00:00:00 2001 From: Abdul Haris Djafar Date: Mon, 18 May 2026 12:47:52 +0700 Subject: [PATCH 02/35] refactor(app): redesign Home / Overview to editorial style Rewrite the authenticated landing page (/overview) to match the editorial language introduced in the Foundation + Login commit. What changed - BentoCard (gradient + blur orb) -> EditorialCard (hairline border, flat ink-100 surface) - StatCard (colored icon bg per metric) -> MetricCell (hairline grid cells, mono uppercase label, mono numeric value) - QuickAction (gradient icon bg) -> ActionCell (hairline cells, mono icon, accent border on hover) - ListItem (colored icon per type, gradient mask) -> flat hairline row with paper-dim icon, semantic status color only for query result (success/error) - SectionHeader (colored icon chip) -> editorial: mono index + dash + uppercase eyebrow, optional sentence-case title - EmptyState (circle icon) -> centered text + mono inline link - TabToggle (pill on white/5) -> editorial: hairline-bordered segmented control with ink-300 active bg - New inline primitives: EditorialCard, MetricCell, ActionCell, ListItem, SectionHeader, EmptyState, TabToggle, InlineLink Sections - Header: avatar chip (user initials, hairline border) + Workspace eyebrow + greeting; connection meta in hairline pill with mono version/uptime separators - Cluster: 6 metric cells in hairline 6-col grid - Start: 4 action cells in hairline 4-col grid - Continue working (conditional): brand-tinted card (border-brand/30 bg-brand/4) with Unsaved tag and unsaved-tab list - Quick access + Saved queries: two-card grid, fixed height, editorial toggle for Favorites/Recent - Recent activity: 3-col hairline grid of query rows with mono SQL preview, status pill, ms metric - ClickHouse resources: 4-col hairline grid linking docs/SQL/best practices/GitHub Functional logic unchanged - All data hooks (useSystemStats, useRecentQueries, useSavedQueries, useDatabases) untouched - All handlers (handleNewQuery, handleImport, handleTableClick, handleSavedQueryClick, handleTabClick, handleRefresh) preserved - formatRelativeTime / formatUptime / getGreeting helpers kept Verified - Geist Sans loaded on h1 (24px / 600 / paper) - Refresh button: hairline ink-500 border, paper-muted text - No console errors - 5 sections rendered in 1546px scrollable container Co-Authored-By: Claude Opus 4.7 --- src/pages/Home.tsx | 984 +++++++++++++++++++++++---------------------- 1 file changed, 500 insertions(+), 484 deletions(-) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index c745c1f..64e622f 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,39 +1,36 @@ import React, { useEffect, useMemo, useState } from "react"; -import { motion } from "framer-motion"; import { - Database, Activity, ArrowRight, - Terminal, - Star, - FileCode, - Table2, - Play, + ArrowUpRight, + BookOpen, CheckCircle2, - XCircle, - Sparkles, - History, ChevronRight, - Plus, - Upload, + Clock, + Code2, + Database, + ExternalLink, + FileCode, HardDrive, + History, Layers, - Users, - Zap, - ExternalLink, - BookOpen, - Code2, Lightbulb, + Play, + Plus, RefreshCw, - Clock, + Star, + Table2, + Terminal, + Upload, + Users, + XCircle, + Zap, type LucideIcon, } from "lucide-react"; import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; import { useNavigate } from "react-router-dom"; -import { Badge } from "@/components/ui/badge"; -import { useRecentQueries, useSavedQueries, useSystemStats, useDatabases } from "@/hooks"; import { useQueryClient } from "@tanstack/react-query"; +import { useRecentQueries, useSavedQueries, useSystemStats, useDatabases } from "@/hooks"; import { useAuthStore } from "@/stores/auth"; import { useExplorerStore, type RecentItem } from "@/stores/explorer"; import { useWorkspaceStore, genTabId } from "@/stores/workspace"; @@ -41,7 +38,9 @@ import { useRbacStore } from "@/stores/rbac"; import { cn, formatCompactNumber } from "@/lib/utils"; import UploadFromFile from "@/features/explorer/components/UploadFile"; -// --- Helper Functions --- +// ============================================ +// Helpers (unchanged from previous version) +// ============================================ function formatRelativeTime(timestamp: number): string { const now = Date.now(); @@ -66,81 +65,102 @@ function formatUptime(seconds: number): string { return `${minutes}m`; } +function getGreeting(): string { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; + if (hour < 18) return "Good afternoon"; + return "Good evening"; +} +// ============================================ +// Editorial primitives (inline) +// ============================================ -// --- Bento Card Base --- -const BentoCard: React.FC<{ +const LABEL_MONO = "font-mono text-[10px] uppercase tracking-[0.18em] text-paper-faint"; +const LABEL_MONO_BOLDER = "font-mono text-[11px] uppercase tracking-[0.16em] text-paper-muted"; + +const EditorialCard: React.FC<{ children: React.ReactNode; className?: string; - glowColor?: string; - noHover?: boolean; -}> = ({ children, className, glowColor = "bg-blue-500/20", noHover = false }) => ( - = ({ children, className }) => ( +
-
-
{children}
- + {children} +
); -// --- Stat Card (for server info) --- -const StatCard: React.FC<{ - icon: LucideIcon; +const SectionHeader: React.FC<{ + eyebrowIndex?: string | number; + eyebrow: string; + title?: string; + action?: React.ReactNode; + className?: string; +}> = ({ eyebrowIndex, eyebrow, title, action, className }) => ( +
+
+ + {eyebrowIndex !== undefined && ( + + {String(eyebrowIndex).padStart(2, "0")} + + )} + + {eyebrow} + + {title && ( +

{title}

+ )} +
+ {action} +
+); + +const MetricCell: React.FC<{ label: string; value: string | number; - color: string; - bgColor: string; -}> = ({ icon: Icon, label, value, color, bgColor }) => ( -
-
- -
-
-

{label}

-

{value}

+ icon: LucideIcon; +}> = ({ label, value, icon: Icon }) => ( +
+
+ {label} +
+ + {value} +
); -// --- Quick Action Button --- -const QuickAction: React.FC<{ +const ActionCell: React.FC<{ icon: LucideIcon; label: string; description: string; onClick: () => void; - color: string; - bgColor: string; -}> = ({ icon: Icon, label, description, onClick, color, bgColor }) => ( - = ({ icon: Icon, label, description, onClick }) => ( + ); -// --- List Item Base (Compact, Clickable) with smooth truncation --- const ListItem: React.FC<{ icon: LucideIcon; - iconBgColor: string; - iconColor: string; title: string; subtitle?: string; meta?: string; @@ -148,118 +168,135 @@ const ListItem: React.FC<{ onClick?: () => void; actionIcon?: LucideIcon; interactive?: boolean; -}> = ({ icon: Icon, iconBgColor, iconColor, title, subtitle, meta, badge, onClick, actionIcon: ActionIcon = ChevronRight, interactive = true }) => { - return ( - = ({ + icon: Icon, + title, + subtitle, + meta, + badge, + onClick, + actionIcon: ActionIcon = ChevronRight, + interactive = true, + status, +}) => ( + ); -// --- Empty State --- const EmptyState: React.FC<{ - icon: LucideIcon; message: string; actionLabel?: string; onAction?: () => void; -}> = ({ icon: Icon, message, actionLabel, onAction }) => ( -
-
- -
-

{message}

+}> = ({ message, actionLabel, onAction }) => ( +
+

{message}

{actionLabel && onAction && ( - + )}
); -// --- Tab Toggle --- const TabToggle: React.FC<{ - options: { id: string; label: string; icon: LucideIcon; color: string }[]; + options: { id: string; label: string }[]; active: string; onChange: (id: string) => void; }> = ({ options, active, onChange }) => ( -
- {options.map((opt) => ( - - ))} +
+ {options.map((opt) => { + const isActive = active === opt.id; + return ( + + ); + })}
); -// --- Main Page Component --- +const InlineLink: React.FC<{ + onClick?: () => void; + href?: string; + children: React.ReactNode; + external?: boolean; +}> = ({ onClick, href, children, external }) => { + const className = + "inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] text-paper-muted transition-colors hover:text-brand"; + if (href) { + return ( + + {children} + + + ); + } + return ( + + ); +}; + +// ============================================ +// Main page +// ============================================ + export default function HomePage() { const navigate = useNavigate(); const { isAdmin, username, activeConnectionId, activeConnectionName, version } = useAuthStore(); @@ -267,37 +304,32 @@ export default function HomePage() { const { tabs, setActiveTab, addTab } = useWorkspaceStore(); const { user: rbacUser } = useRbacStore(); - // Data hooks const { data: stats } = useSystemStats(); const { data: databaseList = [] } = useDatabases(); const usernameFilter = isAdmin ? undefined : username || undefined; const { data: recentQueries = [] } = useRecentQueries(10, usernameFilter); const { data: savedQueries = [] } = useSavedQueries(activeConnectionId || undefined); - // Filter saved queries by active connection - const filteredSavedQueries = useMemo(() => { - return savedQueries.filter((sq) => sq.connectionId === activeConnectionId); - }, [savedQueries, activeConnectionId]); + const filteredSavedQueries = useMemo( + () => savedQueries.filter((sq) => sq.connectionId === activeConnectionId), + [savedQueries, activeConnectionId] + ); - // UI State const [tablesTab, setTablesTab] = useState("favorites"); const [refreshCooldown, setRefreshCooldown] = useState(false); const queryClient = useQueryClient(); - // Fetch data on mount useEffect(() => { fetchFavorites(); fetchRecentItems(8); }, [fetchFavorites, fetchRecentItems]); - // SQL tabs const sqlTabs = useMemo( () => tabs.filter((tab) => tab.type === "sql" && typeof tab.content === "string" && tab.content.trim()), [tabs] ); const unsavedTabs = useMemo(() => sqlTabs.filter((tab) => tab.isDirty), [sqlTabs]); - // Global refresh handler const handleRefresh = () => { if (refreshCooldown) return; setRefreshCooldown(true); @@ -307,7 +339,6 @@ export default function HomePage() { setTimeout(() => setRefreshCooldown(false), 3000); }; - // Navigation handlers const handleNewQuery = () => { const newTab = { id: genTabId(), @@ -321,26 +352,28 @@ export default function HomePage() { }; const handleImport = () => { - // Open import wizard directly without navigation if (databaseList.length > 0) { openUploadFileModal(databaseList[0].name); } }; - const handleTableClick = (database: string, table?: string, targetConnectionId?: string | null, targetConnectionName?: string | null) => { - // Check if we need to switch connections + const handleTableClick = ( + database: string, + table?: string, + targetConnectionId?: string | null, + targetConnectionName?: string | null + ) => { if (targetConnectionId && targetConnectionId !== activeConnectionId) { useAuthStore.getState().setActiveConnection(targetConnectionId, targetConnectionName); toast.success(`Switched to connection: ${targetConnectionName || "target connection"}`); } - // Expand the database node in the tree expandNode(database); - // Create info tab directly to avoid duplicate tabs from URL params const tabId = `info-${database}${table ? `-${table}` : ""}`; const existingTab = tabs.find( - (t) => t.type === "information" && + (t) => + t.type === "information" && typeof t.content === "object" && t.content.database === database && t.content.table === table @@ -378,6 +411,12 @@ export default function HomePage() { }; const userDisplayName = rbacUser?.displayName || rbacUser?.username || "User"; + const userInitials = userDisplayName + .split(/\s+/) + .map((part) => part.charAt(0)) + .slice(0, 2) + .join("") + .toUpperCase(); const tablesToShow = useMemo(() => { const items = tablesTab === "favorites" ? favorites : recentItems; return items.filter((item) => item.connectionId === activeConnectionId); @@ -385,274 +424,258 @@ export default function HomePage() { const greeting = getGreeting(); return ( -
- {/* Import Wizard Modal */} +
-
- - {/* Header */} -
+
+ {/* ─── Header ─── */} +
-
- +
+ {userInitials || "·"}
-
-

{greeting}, {userDisplayName}

-

What would you like to work on today?

+
+ Workspace +

+ {greeting}, {userDisplayName} +

- {/* Connection Info + Refresh */} -
+
{activeConnectionName && ( -
-
-
- {activeConnectionName} -
+
+ + + {activeConnectionName} + {version && ( <> -
- v{version} + + + v{version} + )} - {stats?.uptime && ( + {stats?.uptime !== undefined && stats.uptime > 0 && ( <> -
- up {formatUptime(stats.uptime)} + + + + {formatUptime(stats.uptime)} + )}
)}
-
+
- {/* Server Stats Row */} + {/* ─── Stats ─── */} {stats && ( -
- - - + +
+ + + + + + +
+ + )} + + {/* ─── Quick actions ─── */} +
+ +
+ - - navigate("/monitoring/metrics")} /> - navigate("/monitoring/logs")} />
- )} - - {/* Quick Actions */} -
- - - navigate("/monitoring/metrics")} - color="text-purple-400" - bgColor="bg-gradient-to-br from-purple-500 to-pink-600" - /> - navigate("/monitoring/logs")} - color="text-cyan-400" - bgColor="bg-gradient-to-br from-cyan-500 to-teal-600" - /> -
+
- {/* Continue Working Banner */} + {/* ─── Continue working ─── */} {unsavedTabs.length > 0 && ( - - navigate("/explorer")} - className="text-xs text-amber-400 hover:text-amber-300 hover:bg-amber-500/10" - > - Open All - - } - /> -
- {unsavedTabs.slice(0, 3).map((tab) => ( - Unsaved : undefined} - onClick={() => handleTabClick(tab.id)} - actionIcon={ArrowRight} - /> - ))} -
-
+
+ +
+
+ + + Unsaved + + + Continue working + + + {unsavedTabs.length} {unsavedTabs.length === 1 ? "tab" : "tabs"} + +
+ navigate("/explorer")}>Open all +
+
+ {unsavedTabs.slice(0, 3).map((tab) => ( + handleTabClick(tab.id)} + actionIcon={ArrowRight} + /> + ))} +
+
+
)} - {/* Main Bento Grid - 2 columns */} -
- - {/* Quick Access (Favorites/Recent) */} - - - } - /> -
+ {/* ─── Quick Access + Saved Queries ─── */} +
+ {/* Quick Access */} + +
+
+ {tablesTab === "favorites" ? ( + + ) : ( + + )} + Quick access +
+ +
+
{tablesToShow.length === 0 ? ( navigate("/explorer")} /> ) : ( -
+
{tablesToShow.map((item) => ( : undefined} - onClick={() => handleTableClick(item.database, item.table, item.connectionId, item.connectionName)} + meta={ + tablesTab === "recent" + ? formatRelativeTime((item as RecentItem).accessedAt) + : undefined + } + badge={ + tablesTab === "favorites" ? ( + + ) : undefined + } + onClick={() => + handleTableClick( + item.database, + item.table, + item.connectionId, + item.connectionName + ) + } /> ))}
)}
- + {/* Saved Queries */} - - 6 && ( - - ) - } - /> -
+ +
+
+ + Saved queries + {filteredSavedQueries.length > 0 && ( + + {filteredSavedQueries.length} + + )} +
+ {filteredSavedQueries.length > 6 && ( + navigate("/explorer")}>View all + )} +
+
{filteredSavedQueries.length === 0 ? ( navigate("/explorer")} /> ) : ( -
+
{filteredSavedQueries.map((sq) => ( Public : undefined} + subtitle={sq.query.slice(0, 60)} + badge={ + sq.isPublic ? ( + + Public + + ) : undefined + } onClick={() => handleSavedQueryClick(sq.query, sq.name)} actionIcon={Play} /> @@ -660,129 +683,122 @@ export default function HomePage() {
)}
- -
+
+
- {/* Recent Activity - Full Width at Bottom */} - + {/* ─── Recent activity ─── */} +
0 && ( - - ) + recentQueries.length > 0 ? ( + navigate("/monitoring/logs")}>View all + ) : undefined } /> {recentQueries.length === 0 ? ( - navigate("/explorer")} - /> + + navigate("/explorer")} + /> + ) : ( -
+
{recentQueries.slice(0, 6).map((q, i) => ( - + className="flex flex-col gap-2 border-b border-r border-ink-500 px-4 py-4" + > +
+ {q.status === "Success" ? ( + + + Success + + ) : ( + + + Error + + )} + + {formatRelativeTime(new Date(q.time).getTime())} + +
+

+ {q.query.slice(0, 120)} +

+ + {q.duration < 1 ? "<1" : q.duration.toFixed(0)} ms + +
))}
)} - - - {/* ClickHouse Resources */} - +
+ + {/* ─── ClickHouse resources ─── */} +
+ +
+ {[ + { + icon: BookOpen, + label: "Documentation", + desc: "Official ClickHouse docs", + href: "https://clickhouse.com/docs", + }, + { + icon: Code2, + label: "SQL reference", + desc: "Syntax & functions", + href: "https://clickhouse.com/docs/en/sql-reference", + }, + { + icon: Lightbulb, + label: "Best practices", + desc: "Performance tips", + href: "https://clickhouse.com/docs/en/operations/tips", + }, + { + icon: ExternalLink, + label: "GitHub", + desc: "Source & issues", + href: "https://github.com/ClickHouse/ClickHouse", + }, + ].map(({ icon: Icon, label, desc, href }) => ( + + +
+

{label}

+

{desc}

+
+ +
+ ))} +
+
+ +
); } - -// --- Greeting helper --- -function getGreeting(): string { - const hour = new Date().getHours(); - if (hour < 12) return "Good morning"; - if (hour < 18) return "Good afternoon"; - return "Good evening"; -} From 13b107be1967b807167ea54523b11dba627a8762 Mon Sep 17 00:00:00 2001 From: Abdul Haris Djafar Date: Mon, 18 May 2026 12:53:52 +0700 Subject: [PATCH 03/35] refactor(app): redesign FloatingDock / Sidebar to editorial style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dock is visible on every authenticated page, so this commit is where the editorial language meets persistent navigation. Visual changes - Surfaces: bg-black/40 + backdrop-blur-xl/2xl glassmorphism replaced with flat bg-ink-100 surfaces; sidebar uses bg-ink-50 (page color) for true flat anchoring - Borders: border-white/5..20 transparent borders -> border-ink-500 hairlines; hover state moves to border-ink-700 - Active nav indicator: spring-positioned purple-400 dot replaced with a 2x20px brand-yellow bar (left edge in sidebar, bottom edge in horizontal floating). Same Framer Motion layoutId so the animation still slides between items. - Logo: removed drop-shadow yellow glow; wordmark now reads "CH/UI" with the second half in paper-dim - Version pill: text-purple-400/80 -> font-mono text-paper-faint - Drag state border: purple -> brand yellow - Tooltips: rebuilt as mono uppercase pills with hairline ink-500 border and bg-ink-200 (TOOLTIP_CLASS constant, reused everywhere) - Hover-to-show trigger: lost the rounded-2xl glassmorphism pill and the rotating pulse; now a hairline mono chip with "SHOW DOCK" uppercase label and a small chevron - Settings popover: rebuilt as editorial menu — hairline border, mono eyebrow header, SettingRow primitive with hairline-bordered icon square, label + mono sub-text, brand dot for on-state Functional preservation - Two modes (sidebar / floating) with localStorage + DB sync per device type - All four placements (top/bottom/left/right) with drag-end detection - Auto-hide timing (3.5s) + hover trigger zone untouched - Orientation toggle, fullscreen toggle, reset position untouched - RBAC permission filtering of nav items untouched - Drag handle keeps 44x44px touch targets for mobile/tablet - AnimatePresence enter/exit timings preserved New primitives - TOOLTIP_CLASS constant (shared mono tooltip style) - SettingRow component for popover rows Verified - Sidebar: bg #0a0a0a, border-ink-500, backdrop-filter:none, 56px wide - Active marker: 2x20px bar in rgb(255, 204, 1) (brand yellow) - Nav links: paper-dim text on transparent bg - No console errors Co-Authored-By: Claude Opus 4.7 --- src/components/common/FloatingDock.tsx | 596 ++++++++++++------------- 1 file changed, 286 insertions(+), 310 deletions(-) diff --git a/src/components/common/FloatingDock.tsx b/src/components/common/FloatingDock.tsx index 7bc0fed..ec48a0d 100644 --- a/src/components/common/FloatingDock.tsx +++ b/src/components/common/FloatingDock.tsx @@ -59,7 +59,10 @@ type DockMode = "floating" | "sidebar"; type DockPreferences = DockPreferencesType; -// Load dock preferences from localStorage (fallback) +// ============================================ +// localStorage helpers (unchanged) +// ============================================ + function loadDockPlacementFromLocal(): DockPlacement { try { const saved = localStorage.getItem(DOCK_PLACEMENT_KEY); @@ -107,7 +110,7 @@ function loadAutoHideFromLocal(): boolean { } catch { // Ignore errors } - return true; // Default to auto-hide enabled + return true; } function saveAutoHideToLocal(autoHide: boolean): void { @@ -138,7 +141,6 @@ function saveDockModeToLocal(mode: DockMode): void { } } -// Database preference functions (device-aware: load/save per device type) async function loadDockPreferencesFromDb(deviceType: DeviceType): Promise { try { const preferences = await rbacUserPreferencesApi.getPreferences(); @@ -162,6 +164,13 @@ async function saveDockPreferencesToDb(deviceType: DeviceType, dockPrefs: DockPr } } +// ============================================ +// Editorial primitives +// ============================================ + +const TOOLTIP_CLASS = + "z-[100] rounded-xs border border-ink-500 bg-ink-200 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-paper-muted shadow-lg"; + interface DockItemProps { icon: React.ElementType; label: string; @@ -170,61 +179,56 @@ interface DockItemProps { isVertical?: boolean; } -const DockItem = ({ icon: Icon, label, to, isActive, isVertical }: DockItemProps) => { - return ( - - - - - {/* Active indicator dot */} - {isActive && ( - - )} - - - - - ( + + + + -
- {label} -
-
-
-
- ); -}; + {isActive && ( + + )} + + + + + {label} + + + +); -// Separator component const DockSeparator = ({ isVertical }: { isVertical: boolean }) => ( -
+
); +// ============================================ +// Main component +// ============================================ + export default function FloatingDock() { const location = useLocation(); const Logo = withBasePath("logo.svg"); @@ -235,7 +239,6 @@ export default function FloatingDock() { const dbSyncTimeoutRef = useRef(null); const lastLoadedDeviceRef = useRef(null); - // Dock state - initialize from localStorage for quick render const [orientation, setOrientation] = useState(loadDockOrientationFromLocal); const [placement, setPlacement] = useState(loadDockPlacementFromLocal); const [isDragging, setIsDragging] = useState(false); @@ -245,17 +248,12 @@ export default function FloatingDock() { const [dockMode, setDockMode] = useState(loadDockModeFromLocal); const [isFullscreen, setIsFullscreen] = useState(!!document.fullscreenElement); - // Sync fullscreen state with browser useEffect(() => { - const handleFullscreenChange = () => { - const fs = !!document.fullscreenElement; - setIsFullscreen(fs); - }; + const handleFullscreenChange = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener("fullscreenchange", handleFullscreenChange); return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); - // Toggle fullscreen const toggleFullscreen = useCallback(async () => { try { if (!document.fullscreenElement) { @@ -270,16 +268,10 @@ export default function FloatingDock() { } }, []); - // Use RBAC store for all authentication - const { - hasPermission, - hasAnyPermission, - isAuthenticated, - } = useRbacStore(); - + const { hasAnyPermission, isAuthenticated } = useRbacStore(); const deviceType = useDeviceType(); - // Load preferences from database (authenticated users, per device type; re-load when device type changes) + // Load preferences from database useEffect(() => { if (!isAuthenticated || lastLoadedDeviceRef.current === deviceType) return; @@ -312,7 +304,7 @@ export default function FloatingDock() { loadFromDb(); }, [isAuthenticated, deviceType]); - // Debounced save to database (per device type) + // Debounced save to database const saveToDatabaseDebounced = useCallback( (prefs: DockPreferences) => { if (dbSyncTimeoutRef.current) { @@ -328,7 +320,6 @@ export default function FloatingDock() { [isAuthenticated, deviceType] ); - // Cleanup timeout on unmount useEffect(() => { return () => { if (dbSyncTimeoutRef.current) { @@ -337,7 +328,6 @@ export default function FloatingDock() { }; }, []); - // Check permissions for various sections const canViewMonitoring = hasAnyPermission([ RBAC_PERMISSIONS.LIVE_QUERIES_VIEW, RBAC_PERMISSIONS.METRICS_VIEW, @@ -353,21 +343,17 @@ export default function FloatingDock() { RBAC_PERMISSIONS.AUDIT_VIEW, ]); - const canViewOverview = true; - const canViewExplorer = hasAnyPermission([ RBAC_PERMISSIONS.DB_VIEW, RBAC_PERMISSIONS.TABLE_VIEW, ]); - const canViewSettings = true; - const navItems = [ - ...(canViewOverview ? [{ icon: LayoutDashboard, label: "Home", to: "/overview" }] : []), + { icon: LayoutDashboard, label: "Home", to: "/overview" }, ...(canViewExplorer ? [{ icon: Database, label: "Explorer", to: "/explorer" }] : []), ...(canViewMonitoring ? [{ icon: Activity, label: "Monitoring", to: "/monitoring" }] : []), ...(canViewAdmin ? [{ icon: Shield, label: "Admin", to: "/admin" }] : []), - ...(canViewSettings ? [{ icon: UserCog, label: "Preferences", to: "/preferences" }] : []), + { icon: UserCog, label: "Preferences", to: "/preferences" }, ]; // Auto-hide logic @@ -382,7 +368,6 @@ export default function FloatingDock() { } if (isHovered) { - // Mouse is on the dock — stay visible and cancel any hide timer setIsVisible(true); if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); @@ -391,8 +376,6 @@ export default function FloatingDock() { return; } - // Mouse left the dock — start hide timer with a generous delay - // to avoid flickering when moving between dock items hideTimeoutRef.current = setTimeout(() => { setIsVisible(false); }, 3500); @@ -404,14 +387,12 @@ export default function FloatingDock() { }; }, [autoHide, isHovered, isDragging]); - // Handle drag end - save placement const handleDragEnd = (_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { setIsDragging(false); const { x, y } = info.point; const w = window.innerWidth; const h = window.innerHeight; - // Distances to edges const distTop = y; const distBottom = h - y; const distLeft = x; @@ -424,7 +405,6 @@ export default function FloatingDock() { else if (minDist === distLeft) newPlacement = "left"; else if (minDist === distRight) newPlacement = "right"; - // Auto-adjust orientation based on placement const newOrientation = (newPlacement === "left" || newPlacement === "right") ? "vertical" : "horizontal"; setPlacement(newPlacement); @@ -434,7 +414,6 @@ export default function FloatingDock() { saveToDatabaseDebounced({ mode: dockMode, orientation: newOrientation, autoHide, placement: newPlacement }); }; - // Toggle orientation const toggleOrientation = () => { const newOrientation = orientation === "horizontal" ? "vertical" : "horizontal"; setOrientation(newOrientation); @@ -442,7 +421,6 @@ export default function FloatingDock() { saveToDatabaseDebounced({ mode: dockMode, orientation: newOrientation, autoHide, placement }); }; - // Toggle auto-hide const toggleAutoHide = () => { const newAutoHide = !autoHide; setAutoHide(newAutoHide); @@ -450,7 +428,6 @@ export default function FloatingDock() { saveToDatabaseDebounced({ mode: dockMode, orientation, autoHide: newAutoHide, placement }); }; - // Reset placement const resetPosition = () => { setPlacement("bottom"); setOrientation("horizontal"); @@ -459,25 +436,20 @@ export default function FloatingDock() { saveToDatabaseDebounced({ mode: dockMode, orientation: "horizontal", autoHide, placement: "bottom" }); }; - // Toggle dock mode (floating <-> sidebar) const toggleDockMode = () => { const newMode = dockMode === "floating" ? "sidebar" : "floating"; setDockMode(newMode); saveDockModeToLocal(newMode); saveToDatabaseDebounced({ mode: newMode, orientation, autoHide, placement }); - // Dispatch event so App.tsx can respond window.dispatchEvent(new CustomEvent("dock:mode-change", { detail: { mode: newMode } })); }; - // Dispatch mode on mount useEffect(() => { window.dispatchEvent(new CustomEvent("dock:mode-change", { detail: { mode: dockMode } })); }, []); - // Start drag from handle const startDrag = (event: React.PointerEvent) => { - // Prevent default touch behaviors to ensure smooth dragging on mobile/tablet - if (event.pointerType === 'touch') { + if (event.pointerType === "touch") { event.preventDefault(); } dragControls.start(event); @@ -486,7 +458,6 @@ export default function FloatingDock() { const isVertical = orientation === "vertical"; const isSidebar = dockMode === "sidebar"; - // Calculate offsets for animations based on placement const getVisibleAnimation = () => { switch (placement) { case "top": return { x: "-50%", y: 12 }; @@ -510,36 +481,34 @@ export default function FloatingDock() { const visibleAnim = getVisibleAnimation(); const hiddenAnim = getHiddenAnimation(); - // Sidebar mode rendering (hidden during fullscreen — falls through to floating dock) + // ============================================ + // Sidebar mode rendering (hidden during fullscreen) + // ============================================ if (isSidebar && !isFullscreen) { return ( -
- {/* Logo & Branding */} +
+ {/* Logo */} - CHouse UI -
- CHouse - v{version} + CHouse UI +
+ CH/UI + v{version}
- + {/* Navigation */} -