diff --git a/client/eslint.config.js b/client/eslint.config.js index 8c2c3fe..2eefe1f 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -24,6 +24,6 @@ export default defineConfig([globalIgnores(['dist']), { }, }, rules: { - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'no-unused-vars': 'off', }, }, ...storybook.configs["flat/recommended"]]) diff --git a/client/index.html b/client/index.html index e99bbf6..cdb6e80 100644 --- a/client/index.html +++ b/client/index.html @@ -2,6 +2,9 @@ + + + VDAP diff --git a/client/src/App.jsx b/client/src/App.jsx index e507f47..c8a52e0 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,7 +1,7 @@ import 'leaflet/dist/leaflet.css'; -import { useEffect } from 'react'; -import { Routes, Route, Navigate, Outlet } from 'react-router-dom'; -import { useRecipe, Grid, GridItem, useDialog, Flex, Heading } from "@chakra-ui/react"; +import {useEffect} from 'react'; +import {Routes, Route, Navigate, Outlet} from 'react-router-dom'; +import {useRecipe, Grid, GridItem, useDialog, Flex, Heading} from "@chakra-ui/react"; // --- NEW AUTH IMPORTS --- import useAuthStore from './store/authStore'; @@ -19,80 +19,75 @@ import GlobalMap from "./pages/maps/GlobalMap.jsx"; import Admin from "./pages/admin/Admin.jsx"; import MyDashboard from "./pages/home/tabs/MyDashboard.jsx"; +export default function App() { + // 1. Pull the initialization function from Zustand + const checkAuth = useAuthStore((state) => state.checkAuth); + + // 2. Ping Django to verify the HttpOnly cookie the moment the app boots + useEffect(() => { + checkAuth(); + }, [checkAuth]); + + return ( + + {/* --- Public Routes --- */} + }/> + + {/* --- Secure Routes --- */} + }> + + {/* --- Non-Public Routes (Now completely locked down) --- */} + }> + + }> + }/> + }/> + }/> + }/> + }/> + + + }/> + }/> + }/> + }/> + + + + + {/* --- Redirect Routes --- */} + }/> + + ); +} + +// The fundamental layout of the webpage function AppFoundation() { - const recipe = useRecipe({ key: "dashboard" }); + const recipe = useRecipe({key: "dashboard"}); const styles = recipe(); const dialog = useDialog(); return ( <> - - + + - + -
+
- - + + ) } -const Placeholder = ({ name }) => ( - - {name} Dashboard - Coming Soon - -); - -function App() { - // 1. Pull the initialization function from Zustand - const checkAuth = useAuthStore((state) => state.checkAuth); - - // 2. Ping Django to verify the HttpOnly cookie the moment the app boots - useEffect(() => { - checkAuth(); - }, [checkAuth]); - - return ( - - {/* --- Public Routes --- */} - } /> - - {/* --- SECURE ROUTES --- */} - }> - - {/* --- Non-Public Routes (Now completely locked down) --- */} - }> - - }> - } /> - } /> - } /> - } /> - } /> - - - } /> - } /> - } /> - } /> - - - - - {/* --- Redirect Routes --- */} - } /> - - ); -} - -export default App; \ No newline at end of file +// A dummy page that acts as a placeholder for pages that aren't developed yet but are navigatable to +const Placeholder = ({name}) => ( + + {name} Dashboard - Coming Soon + +); \ No newline at end of file diff --git a/client/src/components/dashboard/SettingsButton.jsx b/client/src/components/dashboard/SettingsButton.jsx index 751397f..f49d6ae 100644 --- a/client/src/components/dashboard/SettingsButton.jsx +++ b/client/src/components/dashboard/SettingsButton.jsx @@ -31,8 +31,11 @@ export function SettingsButton() { const cancelEdit = useDashboardStore(state => state.cancelEdit); const addWidget = useDashboardStore(state => state.addWidget); - const recipe = useRecipe({key: "glassTab"}); - const tabStyles = recipe(); + const glassTabRecipe = useRecipe({key: "glassTab"}); + const tabStyles = glassTabRecipe(); + + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); // Pull the glass container styles for the dropdown and modal const panelRecipe = useSlotRecipe({key: "glassPanel"}); @@ -43,8 +46,6 @@ export function SettingsButton() { // ⬅️ ➡️ This is the callbacks and scroll control to add the chevron arrows in the tab list const tabsRef = useRef(null); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(false); const checkScroll = useCallback(() => { if (tabsRef.current) { @@ -92,27 +93,17 @@ export function SettingsButton() { <> {isEditing ? ( // 📝 EDIT MODE: The 3-Pill Layout - + {/* 1. The Add Widget Button */} - setIsLibraryOpen(true)} - color={{base: "blue.500", _dark: "blue.300"}} - _hover={{bg: "rgba(66, 153, 225, 0.1)"}} + setIsLibraryOpen(true)} + color={{base: "blue.500", _dark: "blue.300"}} _hover={{bg: "rgba(66, 153, 225, 0.1)"}} > Widget Library {/* 2. The Save Button */} - @@ -120,12 +111,7 @@ export function SettingsButton() { {/* 3. The Discard Button */} - @@ -137,7 +123,7 @@ export function SettingsButton() { - {/* size increased to 24 to match tabs. flexShrink: 0 is CRITICAL */} + {/* settings icon size set to 24 to match tabs. flexShrink 0 is CRITICAL */} Dashboard Settings @@ -174,136 +160,52 @@ export function SettingsButton() { setIsLibraryOpen(e.open)} - size="xl" - placement="center" closeOnInteractOutside={true} closeOnEscapeKeyDown={true} + size="xl" + placement="center" > {/* Soft blur can be added to the background behind the modal */} {/**/} - - - - - Widgets Shop + + + Widgets Shop - + - - - {/* LEFT GLASS CRESCENT */} + {/* HORIZONTAL SCROLLABLE MENU OF WIDGET CATEGORIES */} + + {/* left glass crescent */} {canScrollLeft && ( - tabsRef.current.scrollBy({left: -200, behavior: 'smooth'})} - bg={{base: "rgba(255, 255, 255, 0.7)", _dark: "rgba(30, 30, 30, 0.8)"}} - backdropFilter="blur(1px)" - borderTopLeftRadius="full" - borderBottomLeftRadius="full" - - // THE MAGIC: Bites a transparent circle out of the flat right side! - WebkitMaskImage="radial-gradient(circle at 100% 50%, transparent 30px, black 19px)" - maskImage="radial-gradient(circle at 100% 50%, transparent 30px, black 35px)" - - color={{base: "gray.600", _dark: "gray.300"}} - transition="background-color 0.2s" - _hover={{ - color: {base: "blue.500", _dark: "blue.300"}, - bg: {base: "rgba(255, 255, 255, 0.9)", _dark: "rgba(40, 40, 40, 0.95)"} - }} - > - {/* Shifted slightly left to optically center it in the crescent mass */} - + tabsRef.current.scrollBy({left: -200, behavior: 'smooth'})}> + {/* Shifted slightly left to optically center it in the crescent mass */} )} - { - if (e.currentTarget) { - e.currentTarget.scrollLeft += e.deltaY; - } - }} + + {/* the categories */} + { if (e.currentTarget) e.currentTarget.scrollLeft += e.deltaY; }} > {categories.map(cat => ( - + {cat} ))} + + {/* right glass crescent */} {canScrollRight && ( - tabsRef.current.scrollBy({left: 200, behavior: 'smooth'})} - bg={{base: "rgba(255, 255, 255, 0.7)", _dark: "rgba(30, 30, 30, 0.8)"}} - backdropFilter="blur(1px)" - borderTopRightRadius="full" - borderBottomRightRadius="full" - - // THE MAGIC: Bites a transparent circle out of the flat left side! - WebkitMaskImage="radial-gradient(circle at 0% 50%, transparent 30px, black 19px)" - maskImage="radial-gradient(circle at 0% 50%, transparent 30px, black 35px)" - - color={{base: "gray.600", _dark: "gray.300"}} - transition="background-color 0.2s" - _hover={{ - color: {base: "blue.500", _dark: "blue.300"}, - bg: {base: "rgba(255, 255, 255, 0.9)", _dark: "rgba(40, 40, 40, 0.95)"} - }} - > + tabsRef.current.scrollBy({left: 200, behavior: 'smooth'})}> {/* Shifted slightly right to optically center it in the crescent mass */} )} - + + {/* THE WIDGET OPTIONS W/IN EACH CATEGORY */} {categories.map(cat => ( @@ -344,7 +246,7 @@ export function SettingsButton() { h="24px" flexShrink={0} borderRadius="full" - bg={{base: "blue.50", _dark: "whiteAlpha.200"}} + // bg={{base: "blue.50", _dark: "whiteAlpha.200"}} fontWeight="black" fontSize="xs" bg='transparent' diff --git a/client/src/components/layout/Header.jsx b/client/src/components/layout/Header.jsx index 82db471..e2bfa3a 100644 --- a/client/src/components/layout/Header.jsx +++ b/client/src/components/layout/Header.jsx @@ -1,6 +1,5 @@ -import { useRecipe, Button, Stack, Text, Flex } from "@chakra-ui/react" +import {useSlotRecipe, Text, Flex} from "@chakra-ui/react" import UserAvatarMenu from "./UserAvatarMenu.jsx"; -import { NavLink } from "react-router-dom"; import useAuthStore from "@/store/authStore"; import {SearchBar} from "@/components/dashboard/SearchBar.jsx"; @@ -9,27 +8,30 @@ import {SearchBar} from "@/components/dashboard/SearchBar.jsx"; // Accept a 'user' prop now, fallback to placeholder if empty export default function Header() { - const recipe = useRecipe({ key: "header" }); - const styles = recipe(); + const recipe = useSlotRecipe({key: "header"}); + const styles = recipe(); - const { user } = useAuthStore(); - console.log(user); - const displayName = user ? (user.firstName || user.username) : "Loading..."; - const avatarname = user ? (user.fullName || user.username) : "Loading..."; + const {user} = useAuthStore(); + console.log(user); + const displayName = user ? (user.firstName || user.username) : "Loading..."; + const avatarname = user ? (user.fullName || user.username) : "Loading..."; - const displayEmail = user?.email || "LDAP User"; + const displayEmail = user?.email || "LDAP User"; - return ( - - - {/* User Profile Section */} - - {displayName} - - - - ); + return ( + + + + + + {/* User Profile Section */} + + {displayName} + + + + ); } \ No newline at end of file diff --git a/client/src/components/layout/Sidebar.jsx b/client/src/components/layout/Sidebar.jsx index 2ea67ef..9b3c49e 100644 --- a/client/src/components/layout/Sidebar.jsx +++ b/client/src/components/layout/Sidebar.jsx @@ -1,9 +1,8 @@ -import {useSlotRecipe, Box, Button, IconButton} from "@chakra-ui/react"; +import {Mountain, House, Earth, MapPin, ShieldUser, PanelLeftClose, PanelLeftOpen} from 'lucide-react'; +import {useSlotRecipe, Box, Button, IconButton, Text, Flex} from "@chakra-ui/react"; import {ColorModeButton} from "@/components/ui/color-mode.jsx"; -import {useState} from "react"; import {NavLink} from "react-router-dom"; -import Tooltip from "../ui/Tooltip.jsx"; -import {Mountain, House, Earth, MapPin, ShieldUser, Menu, PanelLeftClose, PanelLeftOpen} from 'lucide-react'; +import {useState} from "react"; const SIDEBAR_DEFAULT_BUTTONS = [ {icon: House, name: "Home", url: "/home"}, @@ -14,40 +13,28 @@ const SIDEBAR_DEFAULT_BUTTONS = [ ]; export default function Sidebar() { - const [isOpen, setIsOpen] = useState(true); - const recipe = useSlotRecipe({key: "sidebar"}); const styles = recipe({state: isOpen ? "open" : "closed"}); - - // TODO we will need to call buttons here by authorized user, for now we update the visible buttons as simply the default buttons - const visibleButtons = SIDEBAR_DEFAULT_BUTTONS; + const visibleButtons = SIDEBAR_DEFAULT_BUTTONS; // TODO: call btns here according to users authorization const toggleSidebar = () => setIsOpen((prev) => !prev); return ( - {/* Home Link Logo */} - - {/* Navigation Buttons */} {visibleButtons.map((button) => { const Icon = button.icon; return ( - ); })} - - {/* Menu Trigger */} - + + {/* Collapse Sidebar and Theme Footer Buttons */} + + {isOpen ? : } - - {/* Theme Toggler */} - - - + + + + + ); } \ No newline at end of file diff --git a/client/src/css/index.css b/client/src/css/index.css index 3725a93..3b6c55d 100644 --- a/client/src/css/index.css +++ b/client/src/css/index.css @@ -1,3 +1,4 @@ :root { - font-family: Georgia, 'Times New Roman', Times, serif; + /*font-family: Georgia, 'Times New Roman', Times, serif;*/ + font-family: "Roboto", sans-serif; } \ No newline at end of file diff --git a/client/src/pages/home/Home.jsx b/client/src/pages/home/Home.jsx index 4f34457..16c8b40 100644 --- a/client/src/pages/home/Home.jsx +++ b/client/src/pages/home/Home.jsx @@ -1,4 +1,4 @@ -import {HStack, Flex, Box, useRecipe} from "@chakra-ui/react"; +import {HStack, Flex, Box, useRecipe, useSlotRecipe} from "@chakra-ui/react"; import {NavLink, Outlet, useLocation} from "react-router-dom"; import {SearchBar} from "../../components/dashboard/SearchBar.jsx"; import {SettingsButton} from "../../components/dashboard/SettingsButton.jsx"; @@ -16,30 +16,23 @@ const HOME_TABS = [ ]; export default function Home() { - const recipe = useRecipe({key: "glassTab"}); - const tabStyles = recipe(); + const glassTabsRecipe = useRecipe({key: "glassTab"}); + const homeRecipe = useSlotRecipe({key: "home"}); + const glassTabsStyle = glassTabsRecipe(); + const homeStyle = homeRecipe(); - const location = useLocation(); - const isPersonalDashboard = location.pathname === "/home" || location.pathname === "/home/"; + const location= useLocation(); + const isPersonalDashboard= location.pathname === "/home" || location.pathname === "/home/"; return ( - - - - + + {/* TABS CONTAINER */} - + {HOME_TABS.map((tab) => { const Icon = tab.icon; return ( - + {/* CRITICAL: 'end' prop forces exact match so "/home" deactivates on sub-routes*/} {/* flexShrink: 0 ensures the icon never squishes during the gooey animation */} {tab.name} @@ -49,7 +42,6 @@ export default function Home() { {/* The Settings button is now inline with the tabs and only renders on the personal dashboard */} {isPersonalDashboard && } - {/* EDIT MODE BUTTONS & CONTROLS */} @@ -57,10 +49,10 @@ export default function Home() { {/* Future global controls go here */} - - + {/* TABS CONTENT */} + diff --git a/client/src/pages/home/tabs/MyDashboard.jsx b/client/src/pages/home/tabs/MyDashboard.jsx index aed386f..b76981b 100644 --- a/client/src/pages/home/tabs/MyDashboard.jsx +++ b/client/src/pages/home/tabs/MyDashboard.jsx @@ -13,8 +13,8 @@ import {WIDGET_REGISTRY} from '@/constants/widgetRegistry.jsx'; // React-Grid-Layout requires this wrapper to automatically calculate screen width const ResponsiveGridLayout = WidthProvider(Responsive); -export default function MyDashboard() { +export default function MyDashboard() { // ☁️ Database Connection const {user} = useAuthStore(); @@ -28,25 +28,23 @@ export default function MyDashboard() { // 🔒 Safety lock to prevent infinite re-renders const hasHydrated = useRef(false); - // 1. Pull the new global slot recipe + // 1. Pull the global slot recipes const recipe = useSlotRecipe({key: "glassPanel"}); const styles = recipe({ layout: "widget" }); const popoverStyles = recipe({ layout: "popover" }); + // 2. Populate widgets useEffect(() => { if (user?.preferences?.dashboard_layout && !hasHydrated.current) { const savedLayout = user.preferences.dashboard_layout; // Safety check: Only inject if it's actually an array of widgets. // (Prevents a crash if MariaDB returns a default empty object {}) - if (Array.isArray(savedLayout)) { - setWidgets(savedLayout); - } + if (Array.isArray(savedLayout)) setWidgets(savedLayout); hasHydrated.current = true; } }, [user, setWidgets]); - // 🛠️ Format the data for RGL const rglLayout = widgets.map(w => ({ i: w.id, @@ -54,10 +52,10 @@ export default function MyDashboard() { y: w.y, w: w.w, h: w.h, - isResizable: false // 🚫 The magic lock! No resizing allowed globally. + isResizable: false // The magic lock! No resizing allowed globally. })); - // 3. THE COMPONENT MAPPER + // 3. The component mapper const renderWidget = (widget) => { const registryEntry = WIDGET_REGISTRY[widget.type]; @@ -70,28 +68,21 @@ export default function MyDashboard() { return ( <> - + {registryEntry.name} - {/* THE SETTINGS ELLIPSIS */} - {SettingsUI ? ( + {/* WIDGET SETTINGS BUTTON (ELLIPSES) */} + {SettingsUI && ( - + {/* NEW: The Positioner lifts the content out of the Flexbox flow */} - + }> - ) : ( - )} @@ -133,30 +122,28 @@ export default function MyDashboard() { rowHeight={30} isDraggable={isEditing} // Only allows dragging when Settings tools toggle this isResizable={false} // Double lock-down - onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget compactType="vertical" // Auto-packs widgets to the top + onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget > - {/* 3. 🎨 Paint the Widgets */} + {/* 🎨 Paint the Widgets */} {widgets.map(widget => ( - {/* 4. THE DELETE BUTTON */} + {/* THE DELETE BUTTON */} {isEditing && ( removeWidget(widget.id)} onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches @@ -172,7 +161,7 @@ export default function MyDashboard() { )} - {/* 4. SUSPENSE BOUNDARY wrapping the render function */} + {/* SUSPENSE BOUNDARY wrapping the render function */} diff --git a/client/src/theme.js b/client/src/theme.js index 41e836d..53edb4f 100644 --- a/client/src/theme.js +++ b/client/src/theme.js @@ -1,26 +1,52 @@ import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe} from "@chakra-ui/react"; -// RECIPES -const headerRecipe = defineRecipe({ +// CONSOLIDATED ✔ +const headerRecipe = defineSlotRecipe({ + slots: ["root", "search_bar", "user_info"], base: { - bg: "{transparent}", - color: "text", - width: "100%", - height: "100%", - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - } + root: { + bg: "{transparent}", + color: "text", + width: "100%", + height: "100%", + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + pl: 3, + }, + search_bar: { + width: "100%", + // input element in search bar component + "& input": { + width: "410px", + } + }, + user_info: { + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + gap: 3, + pr: 6, + + // The Text component + "& p": { + fontWeight: "600", + fontSize: "md" // equivalent to 16px + } + } + }, }); +// CONSOLIDATED ✔ const sidebarRecipe = defineSlotRecipe({ - slots: ["root", "logo", "footer", "button_text","buttons", "collapse_button"], + slots: ["root", "logo", "logo_text", "footer", "button_text", "buttons", "collapse_button", "color_mode_button"], base: { root: { // 1. Semi-transparent backgrounds bg: { - base: "linear-gradient(to bottom right, #99000040 20%, #FF660040 60%, #EEEE0040 100%)", + // base: "linear-gradient(to bottom right, #99000040 20%, #FF660040 60%, #EEEE0040 100%)", // this is too pale for light mode imo + base: "linear-gradient(to bottom right, #97282880 20%, #EB7F3160 60%, #FCAD3840 100%)", // colors from: https://colorhunt.co/palette/972828e45742eb7f31fcad38 _dark: "linear-gradient(to bottom right, #9900003C 20%, #FF66003C 60%, #FFC0003C 100%)" }, @@ -47,7 +73,7 @@ const sidebarRecipe = defineSlotRecipe({ justifyContent: "start", alignItems: "stretch", gap: 4, - transition: "width 0.4s ease", + transition: "width 0.5s ease", // CRITICAL: Adjust padding and overflow to allow the button to touch the right edge pl: 2, // Keep left padding @@ -62,7 +88,7 @@ const sidebarRecipe = defineSlotRecipe({ bg: "transparent", justifyContent: "flex-start", - transition: "background-color 0.4s ease, color 0.4s ease", + transition: "background-color 0.3s ease, color 0.3s ease", "& svg": { transition: "stroke-width 0.4s ease", @@ -70,11 +96,13 @@ const sidebarRecipe = defineSlotRecipe({ }, _hover: { - bg: "rgba(255, 255, 255, 0.1)", + bg: "rgba(255, 255, 255, 0.2)", color: "{text}", - fontWeight: "semibold", "& svg": { strokeWidth: "3" + }, + "& span": { + fontWeight: "700", } }, @@ -94,9 +122,11 @@ const sidebarRecipe = defineSlotRecipe({ _hover: { bg: "{MainBackground}", color: "{textInverted}", - fontWeight: "normal", "& svg": { strokeWidth: "2" // Keeps the active icon at normal thickness on hover + }, + "& span": { + fontWeight: "500", } }, @@ -131,98 +161,198 @@ const sidebarRecipe = defineSlotRecipe({ }, logo: { width: "100%", - height: "auto", - overflow: "hidden", + justifyContent: "flex-start", + ml: 2, + textStyle: "4xl", fontWeight: "bold", + overflow: "hidden", px: 0, }, + logo_text: { + transition: "opacity 0.2s ease, visibility 0.2s ease, margin 0.2s ease", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "clip", + }, footer: { + flexDirection: "row", + justifyContent: "space-between", + width: "100%", mt: "auto", - display: "flex", - justifyContent: "center", - alignItems: "center", - pb: 4, + pl: 2, pr: 2, - transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease" }, button_text: { transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease", - whiteSpace: "nowrap" + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "clip", + fontWeight: "500", }, collapse_button: { - "& svg": { flexShrink: "0" }, + "& svg": {flexShrink: "0"}, }, + color_mode_button: { + "& button": { + _hover: { + bg: "transparent", + }, + } + } + // opened_theme_button: { + // width: "100%", + // align: "center", + // justifyContent: "right", + // mr: 6, + // // "& button": { + // // alignSelf: "center", + // // transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease", + // // _hover: { + // // bg: "transparent", + // // } + // // } + // }, + // closed_theme_button: { + // // ml: 0.9, + // // "& button": { + // // transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease", + // // _hover: { + // // bg: "transparent", + // // } + // // } + // }, }, variants: { state: { open: { - root: { width: "190px" }, - logo: { justifyContent: "flex-start", ml: 2, textStyle: "4xl" }, - footer: { gap: 16, flexDirection: "row" }, + root: {width: "190px"}, button_text: {ml: 3, opacity: 1, visibility: "visible"}, + logo_text: {ml: 3, opacity: 1, visibility: "visible", transition: "opacity 0.5s ease, visibility 0.5s ease, margin 0.5s ease"}, + color_mode_button: { + "& button": { + position: "static", + }, + // the css below is equivalent to chakra's 'truncate' prop + // it makes sure the icon doesn't jump out of the bounds of the sidebar while it's transitioning from closed to open + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "clip", + }, }, closed: { - root: { width: "71px" }, - logo: { justifyContent: "center", ml: -1, textStyle: "3xl" }, - footer: { gap: 2, flexDirection: "column-reverse" }, - button_text: {ml: 0, opacity: 0, visibility: "hidden"}, + root: {width: "71px"}, + button_text: {opacity: 0, visibility: "hidden"}, + logo_text: {opacity: 0, visibility: "hidden", transition: "opacity 0.2s ease, visibility 0.2s ease, margin 0.2s ease"}, + color_mode_button: {"& button": {position: "relative", bottom: "35px", right: "37px",} + }, } } } }); +// CONSOLIDATED ✔ const dashboardRecipe = defineRecipe({ base: { - bg: "{MainBackground}" - } -}); + height: "100vh", + width: "100vw", + bg: "{MainBackground}", -const canvasRecipe = defineRecipe({ - base: { - bg: "{transparent}", - zIndex: "-1", - color: "{text}" - } -}); - -const widgetRecipe = defineSlotRecipe({ - slots: ["root", "header", "canvas"], - base: { - root: { - flexDirection: "column", - gap: "1", - position: "relative", - bg: "base100", - borderRadius: "md", - p: "1", - paddingBottom: "2", - border: "1px solid", - borderColor: "{base300}", - textAlign: "center", - height: "100%", - position: "fixed" - }, - header: { - flexShrink: "0", - bg: "{base300}", - color: "{text}", - cursor: "move", - borderRadius: "sm", - whiteSpace: "nowrap", - textAlign: "left", - pl: "2" - }, - canvas: { - flex: "1 1 auto", - bg: "transparent", - color: "{text}", - cursor: "default", - borderRadius: "md", + // selects the third div in the grid layout (the canvas) + "& > div:nth-of-type(3)": { + width: "100%", overflow: "auto", } } }); +// CONSOLIDATED ✔ +const homeRecipe = defineSlotRecipe({ + slots: ["root", "header", "body"], + base: { + root: { + flexDirection: "column", + height: "100%", + width: "100%", + }, + header: { + // Flex contianer as a header that contains all the tabs + width: "100%", + align: "center", + justify: "space-between", + top: "0", + p: 5, + position: "sticky", + zIndex: "dropdown", + bg: "transparent", + + // THE TABS + // Flex container renders as div for all the buttons within the header + "& > div": { + alignItems: "center", + gap: 2, + + // icon in each tab button + "& svg": { + // TODO: bring over svg styling + } + } + }, + body: { + flex: 1, + overflow: "auto", + } + }, +}) + +// const canvasRecipe = defineRecipe({ +// base: { +// bg: "{transparent}", +// zIndex: "-1", +// color: "{text}" +// } +// }); + +// const widgetRecipe = defineSlotRecipe({ +// slots: ["root", "header", "canvas"], +// base: { +// root: { +// flexDirection: "column", +// gap: "1", +// position: "relative", +// bg: "base100", +// borderRadius: "md", +// p: "1", +// paddingBottom: "2", +// border: "1px solid", +// borderColor: "{base300}", +// textAlign: "center", +// height: "100%", +// // position: "fixed" +// }, +// header: { +// flexShrink: "0", +// bg: "{base300}", +// color: "{text}", +// cursor: "move", +// borderRadius: "sm", +// whiteSpace: "nowrap", +// textAlign: "left", +// pl: "2" +// }, +// canvas: { +// flex: "1 1 auto", +// bg: "transparent", +// color: "{text}", +// cursor: "default", +// borderRadius: "md", +// overflow: "auto", +// } +// } +// }); + +// USED IN COMPONENTS: +// * Settings Button +// * My Dashboard const glassPanelRecipe = defineSlotRecipe({ slots: ["root", "header", "body"], @@ -246,26 +376,216 @@ const glassPanelRecipe = defineSlotRecipe({ variants: { layout: { widget: { - root: {width: "100%", height: "100%", borderRadius: "3xl"}, - header: {display: "flex", justifyContent: "space-between", alignItems: "center", px: 3, pt: 2, pb: 0}, - body: {flex: "1", overflow: "hidden", position: "relative", px: 1, pb: 1, pt: 0} + root: { + width: "100%", + height: "100%", + borderRadius: "3xl", + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + px: 3, + pt: 2, + pb: 0, + + "& > p": { + color: "titleColor", + fontWeight: "500", + fontSize: "16px", + + // the following three specifications are equivalent to Chakra's 'truncate' prop + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "clip", + }, + + "& > button": { + borderRadius: "xl", + + _hover: { + color: "white", + bg: "gray.500", + }, + } + }, + body: { + flex: "1", + overflow: "hidden", + position: "relative", + px: 1, + pb: 1, + pt: 0 + } }, dropdown: { - root: {borderRadius: "xl", p: 1} + root: { + borderRadius: "xl", + p: 1 + } // (Header and body slots are ignored here because dropdowns don't use them) }, popover: { root: { borderRadius: "2xl", - p: 4, + p: 0, boxShadow: "xl", minW: "240px", + maxW: "60%", + minH: "80%", + overflow: "hidden", // OVERRIDES: Make the background 85-90% opaque instead of 40% - bg: { base: "rgba(255, 255, 255, 0.75)", _dark: "rgba(20, 22, 25, 0.8)" }, + bg: { + base: "rgba(255, 255, 255, 0.75)", + _dark: "rgba(20, 22, 25, 0.8)" + }, // OVERRIDES: Increase the blur just to smooth out any stubborn gridlines backdropFilter: "blur(15px)", WebkitBackdropFilter: "blur(15px)", + + + // IMPORTANT: everything below is specific to the widget menu found in the settingsButton component + // dialog header + "& > div.chakra-dialog__header": { + bg: "transparent", + color: { + base: "gray.800", + _dark: "white" + }, + py: 4, + borderColor: { + base: "gray.200", + _dark: "whiteAlpha.200" + }, + + // title + "& h2": { + fontSize: "xl", + fontWeight: "bold" + }, + }, + + // dialog body + "& > div.chakra-dialog__body": { + p: 0, + + // box container of the widget category menu + "& div.chakra-tabs__root > div:first-child": { + position: "relative", + display: "flex", + maxW: "100%", + mx: 5, + mt: 4, + mb: 6 + }, + + // the horizontal widget categories menu + "& div.chakra-tabs__list": { + bg: { + base: "rgba(0, 0, 0, 0.04)", + _dark: "rgba(255, 255, 255, 0.05)" + }, + p: 1, + borderRadius: "full", + display: "flex", + flexWrap: "nowrap", + overflowX: "auto", + maxW: "100%", + border: "1px solid", + borderColor: { + base: "blackAlpha.50", + _dark: "whiteAlpha.100" + }, + scrollbarWidth: "none", + "&::-webkit-scrollbar": {display: "none"}, + + // the pill shaped menu options + "& button.chakra-tabs__trigger": { + px: 5, + py: 2, + borderRadius: "full", + whiteSpace: "nowrap", + flexShrink: 0, + color: { + base: "gray.500", + _dark: "gray.400" + }, + fontWeight: "medium", + transition: "background-color 0.2s, color 0.2s, box-shadow 0.2s", + _hover: { + color: { + base: "gray.800", + _dark: "gray.200" + }, + }, + _selected: { + bg: { + base: "white", + _dark: "rgba(255, 255, 255, 0.12)" + }, + color: { + base: "blue.600", + _dark: "blue.200" + }, + boxShadow: "sm", + fontWeight: "bold", + } + } + }, + + // base styling applied to both chevron navigators + "& div.chakra-tabs__root > div:first-child div:has(svg)": { + position: "absolute", + top: "0", + bottom: "0", + w: "50px", + alignItems: "center", + justifyContent: "center", + zIndex: 2, + cursor: "pointer", + bg: { + base: "rgba(255, 255, 255, 0.7)", + _dark: "rgba(30, 30, 30, 0.8)" + }, + color: { + base: "gray.600", + _dark: "gray.300" + }, + backdropFilter: "blur(1px)", + transition: "background-color 0.2s", + + _hover: { + color: { + base: "blue.500", + _dark: "blue.300" + }, + bg: { + base: "rgba(255, 255, 255, 0.9)", + _dark: "rgba(40, 40, 40, 0.95)" + }, + }, + + "&:has(.lucide-chevron-left)": { + left: 0, + borderTopLeftRadius: "full", + borderBottomLeftRadius: "full", + // Bites a transparent circle out of the flat right side! + WebkitMaskImage: "radial-gradient(circle at 100% 50%, transparent 30px, black 19px)", + maskImage: "radial-gradient(circle at 100% 50%, transparent 30px, black 35px)", + }, + + "&:has(.lucide-chevron-right)": { + right: 0, + borderTopRightRadius: "full", + borderBottomRightRadius: "full", + // THE MAGIC: Bites a transparent circle out of the flat left side! + WebkitMaskImage: "radial-gradient(circle at 0% 50%, transparent 30px, black 19px)", + maskImage: "radial-gradient(circle at 0% 50%, transparent 30px, black 35px)", + } + } + + } } } }, @@ -305,7 +625,7 @@ const glassTabRecipe = defineRecipe({ overflow: "hidden", whiteSpace: "nowrap", textDecoration: "none", - fontWeight: "bold", + fontWeight: "500", color: "fg.muted", // --- THE GLASS EFFECT (Inactive) --- @@ -423,16 +743,17 @@ const config = defineConfig({ theme: { semanticTokens: { colors: { - base100: {value: {base: "#f6f9faff", _dark: "#1617199e"}}, - base200: {value: {base: "#e6ecf1ff", _dark: "#242424"}}, - base300: {value: {base: "#d3dae4ff", _dark: "#0a0b0bff"}}, - primary: {value: {base: "#2B98F8", _dark: "#2B98F8"}}, - primaryLt: {value: {base: "#98cdffff", _dark: "#98cdffff"}}, - secondaryBG: {value: {base: "#EDF1F1", _dark: "#121212"}}, - border: {value: {base: "#000000", _dark: "#cbcbcb84"}}, + // base100: {value: {base: "#f6f9faff", _dark: "#1617199e"}}, + // base200: {value: {base: "#e6ecf1ff", _dark: "#242424"}}, + // base300: {value: {base: "#d3dae4ff", _dark: "#0a0b0bff"}}, + // primary: {value: {base: "#2B98F8", _dark: "#2B98F8"}}, + // primaryLt: {value: {base: "#98cdffff", _dark: "#98cdffff"}}, + // secondaryBG: {value: {base: "#EDF1F1", _dark: "#121212"}}, + // border: {value: {base: "#000000", _dark: "#cbcbcb84"}}, text: {value: {base: "#000000", _dark: "#FFFFFF"}}, textInverted: {value: {base: "#000000", _dark: "#FFFFFF"}}, - icon: {value: {base: "#000000", _dark: "#FFFFFF"}}, + // icon: {value: {base: "#000000", _dark: "#FFFFFF"}}, + gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "rgba(255, 255, 255, 0.1)"}}, MainBackground: {value: {base: "#F9F9F9", _dark: "#141414"}}, titleColor: {value: {base: "gray.800", _dark: "#FFFFFF"}}, @@ -446,17 +767,20 @@ const config = defineConfig({ }, }, recipes: { - header: headerRecipe, dashboard: dashboardRecipe, - canvas: canvasRecipe, + // canvas: canvasRecipe, glassTab: glassTabRecipe }, slotRecipes: { + header: headerRecipe, sidebar: sidebarRecipe, - widget: widgetRecipe, + // widget: widgetRecipe, glassPanel: glassPanelRecipe, + home: homeRecipe, }, }, }); + +// Define semantic tokens and export recipes export const system = createSystem(defaultConfig, config); \ No newline at end of file