Move to Sidebar layout, add new theme for Demonic Pacts

This commit is contained in:
2026-01-17 05:49:32 +08:00
parent e807d3c380
commit b6f74d7905
10 changed files with 1173 additions and 37 deletions

View File

@@ -12,6 +12,7 @@ import About from './pages/About';
import Settings from './pages/Settings';
import store from './store';
import ThemeProvider from './components/ThemeProvider';
import { SidebarProvider } from './context/SidebarContext';
import Statistics from './pages/Statistics';
import Calculators from './pages/Calculators';
import Faq from './pages/Faq';
@@ -53,6 +54,7 @@ export default function App() {
return (
<Provider store={store}>
<SidebarProvider>
<ThemeProvider>
<div className='App'>
<BrowserRouter basename='/'>
@@ -82,6 +84,7 @@ export default function App() {
</BrowserRouter>
</div>
</ThemeProvider>
</SidebarProvider>
</Provider>
);
}

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import Page from './common/Page';
import NavBar, { NavItem } from './common/NavBar';
import SideBar from './common/SideBar';
import { NavItem } from './common/NavBar';
import FeedbackModal from './FeedbackModal';
import ManageDataModal from './ManageDataModal';
import images from '../assets/images';
@@ -24,38 +25,51 @@ export default function PageWrapper({ children }) {
new NavItem('Calculators', 'primary', 0, 2).withRouterLink('/calculators').withIconFont('calculate'),
new NavItem('Groups', 'primary', 0, 3).withRouterLink('/groups').withIconFont('groups'),
new NavItem('Character', 'secondary', 1, 0).withCustomRenderFn(
() => <Character.NavBarItem key='character' setCharacterModalOpen={setCharacterModalOpen} />,
() => <Character.CollapsedMenu key='character' setCharacterModalOpen={setCharacterModalOpen} />
(isCollapsed, onNavigate) => (
<Character.SideBarItem
key='character'
setCharacterModalOpen={setCharacterModalOpen}
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
)
),
new NavItem('Import', 'secondary', 2, 0).withCustomRenderFn(
() => <ManageData.NavBarItem key='manage' setManageDataModalType={setManageDataModalType} />,
() => <ManageData.CollapsedMenu key='manage' setManageDataModalType={setManageDataModalType} />
new NavItem('Data', 'secondary', 2, 0).withCustomRenderFn(
(isCollapsed, onNavigate) => (
<ManageData.SideBarItem
key='manage'
setManageDataModalType={setManageDataModalType}
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
)
),
// TODO re-enable user login
// new NavItem('Login', 'secondary', 3, 0).withCustomRenderFn(
// () => <AuthButton.NavBarItem key='login' />,
// () => <AuthButton.CollapsedMenu key='login' />
// ),
new NavItem('Settings', 'overflow', 3, 1).withRouterLink('/settings').withIconFont('settings'),
new NavItem('FAQ', 'overflow', 3, 2).withRouterLink('/faq').withIconFont('help_outline'),
new NavItem('About', 'overflow', 3, 3).withRouterLink('/about').withIconFont('info'),
new NavItem('Discord', 'overflow', 4, 0).withHref('https://discord.gg/GQ5kVyU', '_blank').withIconFont('discord'),
new NavItem('Feedback', 'overflow', 4, 1).withCustomRenderFn(
() => <Feedback.NavBarItem key='feedback' setFeedbackModalOpen={setFeedbackModalOpen} />,
() => <Feedback.CollapsedMenu key='feedback' setFeedbackModalOpen={setFeedbackModalOpen} />
),
new NavItem('Github', 'overflow', 4, 2)
new NavItem('Github', 'overflow', 4, 1)
.withHref('https://github.com/osrs-reldo/os-league-tools', '_blank')
.withIconFont('code'),
new NavItem('Feedback', 'overflow', 4, 2).withCustomRenderFn(
(isCollapsed, onNavigate) => (
<Feedback.SideBarItem
key='feedback'
setFeedbackModalOpen={setFeedbackModalOpen}
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
)
),
new NavItem('Tip Jar', 'overflow', 4, 3)
.withHref('https://ko-fi.com/osleaguetools', '_blank')
.withIconFont('savings'),
new NavItem('FAQ', 'overflow', 4, 4).withRouterLink('/faq').withIconFont('help_outline'),
new NavItem('About', 'overflow', 4, 5).withRouterLink('/about').withIconFont('info'),
];
return (
<Page limitContentWidth={limitContentWidth}>
<Page.Nav>
<NavBar navItems={navItems} brandName='OS League Tools' brandLogo={images[`icon-${theme}.png`]} />
<SideBar navItems={navItems} brandName='OS League Tools' brandLogo={images[`icon-${theme}.png`]} />
</Page.Nav>
<Page.Body>
{children}

View File

@@ -1,11 +1,27 @@
import React from 'react';
import { getLayoutSlots, LayoutSlot } from './util/layout';
import { useSidebar } from '../../context/SidebarContext';
import useBreakpoint, { MEDIA_QUERIES, MODE } from '../../hooks/useBreakpoint';
function Page({ children, sidebarPosition = 'left', limitContentWidth = true }) {
const { nav, banner, sidebar, body } = getLayoutSlots(children);
const { isCollapsed } = useSidebar();
const isDesktop = useBreakpoint(MEDIA_QUERIES.LG, MODE.GREATER_OR_EQ);
// Determine content margin class based on sidebar state
const contentMarginClass = isDesktop
? isCollapsed
? 'main-content-sidebar-collapsed'
: 'main-content-sidebar-expanded'
: 'main-content-mobile-header';
return (
<div className='bg-secondary w-full h-full min-h-screen'>
<div className='flex min-h-screen'>
{/* Left sidebar navigation */}
{nav}
{/* Main content area */}
<div className={`flex-1 bg-secondary main-content-with-sidebar ${contentMarginClass}`}>
<div className='py-5 page-wrapper'>
{banner && <div>{banner}</div>}
<div className='flex md:flex-row flex-col justify-center'>
@@ -15,6 +31,7 @@ function Page({ children, sidebarPosition = 'left', limitContentWidth = true })
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,315 @@
import React, { useRef } from 'react';
import { Link, NavLink } from 'react-router-dom';
import _ from 'lodash';
import useBreakpoint, { MEDIA_QUERIES, MODE } from '../../hooks/useBreakpoint';
import useClickListener from '../../hooks/useClickListener';
import { useSidebar } from '../../context/SidebarContext';
// Re-export NavItem from NavBar for convenience
export { NavItem } from './NavBar';
// Group NavItems by their variant (slot)
function groupNavItemsByVariant(navItems) {
const groups = {};
for (const item of navItems) {
const variant = item.variant || item.props?.slot || 'primary';
if (groups[variant]) {
groups[variant].push(item);
} else {
groups[variant] = [item];
}
}
return groups;
}
export default function SideBar({ navItems, brandName, brandLogo }) {
const { isCollapsed, toggleCollapse, isDrawerOpen, closeDrawer } = useSidebar();
const isDesktop = useBreakpoint(MEDIA_QUERIES.LG, MODE.GREATER_OR_EQ);
const drawerRef = useRef(null);
useClickListener(drawerRef, closeDrawer, true);
const {
primary: primaryNavItems,
secondary: secondaryNavItems,
overflow: overflowNavItems,
} = groupNavItemsByVariant(navItems);
// Group overflow items by collapseGroup for organization
const overflowGroups = getCollapseGroups(overflowNavItems || []);
// Desktop sidebar
if (isDesktop) {
return (
<aside className={`sidebar-nav ${isCollapsed ? 'sidebar-nav-collapsed' : 'sidebar-nav-expanded'}`}>
{/* Brand */}
<SideBarBrand logo={brandLogo} name={brandName} isCollapsed={isCollapsed} />
{/* Navigation content */}
<div className='sidebar-nav-content'>
{/* Primary navigation */}
<SideBarSection>
{primaryNavItems &&
primaryNavItems.map(navItem => (
<SideBarLink key={navItem.id} item={navItem} isCollapsed={isCollapsed} />
))}
</SideBarSection>
<SideBarDivider />
{/* Secondary navigation (Character, Manage Data) */}
<SideBarSection>
{secondaryNavItems &&
secondaryNavItems.map(navItem => (
<SideBarLink key={navItem.id} item={navItem} isCollapsed={isCollapsed} />
))}
</SideBarSection>
<SideBarDivider />
{/* Overflow items grouped */}
{overflowGroups.map((group, i) => (
<React.Fragment key={i}>
<SideBarSection>
{group.map(navItem => (
<SideBarLink key={navItem.id} item={navItem} isCollapsed={isCollapsed} />
))}
</SideBarSection>
{i < overflowGroups.length - 1 && <SideBarDivider />}
</React.Fragment>
))}
</div>
{/* Toggle button */}
<button
type='button'
className='sidebar-nav-toggle'
onClick={toggleCollapse}
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
<span className='icon-sm text-primary-alt'>
{isCollapsed ? 'chevron_right' : 'chevron_left'}
</span>
</button>
</aside>
);
}
// Mobile: header + drawer
return (
<>
<MobileHeader logo={brandLogo} name={brandName} />
<SideBarDrawer
innerRef={drawerRef}
isOpen={isDrawerOpen}
brandLogo={brandLogo}
brandName={brandName}
primaryNavItems={primaryNavItems}
secondaryNavItems={secondaryNavItems}
overflowGroups={overflowGroups}
onClose={closeDrawer}
/>
<SideBarBackdrop isOpen={isDrawerOpen} onClick={closeDrawer} />
</>
);
}
function SideBarBrand({ logo, name, isCollapsed }) {
return (
<Link to='/' className='sidebar-nav-brand'>
<img src={logo} className='sidebar-nav-brand-logo' alt='' />
{!isCollapsed && <span className='sidebar-nav-brand-text'>{name}</span>}
</Link>
);
}
function SideBarSection({ title, children }) {
return (
<div className='sidebar-nav-section'>
{title && <div className='sidebar-nav-section-header'>{title}</div>}
{children}
</div>
);
}
function SideBarDivider() {
return <div className='sidebar-nav-divider' />;
}
function SideBarLink({ item, isCollapsed, onClick }) {
// If item has a custom render function for sidebar, use it
if (item.renderSidebarFn) {
return item.renderSidebarFn(isCollapsed, onClick);
}
// If item has a standard render function, use it with modifications
if (item.renderFn) {
return item.renderFn(isCollapsed, onClick);
}
const icon = item.iconFont && (
<span className='sidebar-nav-link-icon icon-base'>{item.iconFont}</span>
);
const label = !isCollapsed && <span className='sidebar-nav-link-label'>{item.label}</span>;
// External link
if (item.href) {
return (
<a
className='sidebar-nav-link'
href={item.href}
target={item.target || '_blank'}
rel='noopener noreferrer'
onClick={onClick}
title={isCollapsed ? item.label : undefined}
>
{icon}
{label}
</a>
);
}
// Router link
if (item.to) {
return (
<NavLink
className={({ isActive }) => `sidebar-nav-link ${isActive ? 'active' : ''}`}
to={item.to}
onClick={onClick}
title={isCollapsed ? item.label : undefined}
>
{icon}
{label}
</NavLink>
);
}
// Button with onClick
if (item.onClick) {
return (
<button
type='button'
className='sidebar-nav-link w-full text-left'
onClick={() => {
item.onClick();
if (onClick) {
onClick();
}
}}
title={isCollapsed ? item.label : undefined}
>
{icon}
{label}
</button>
);
}
return null;
}
function MobileHeader({ logo, name }) {
const { openDrawer } = useSidebar();
return (
<header className='mobile-header'>
<button type='button' className='mobile-header-menu-btn' onClick={openDrawer} aria-label='Open menu'>
<span className='icon-xl text-primary'>menu</span>
</button>
<Link to='/' className='flex items-center'>
<img src={logo} className='h-6 w-6 mr-2' alt='' />
<span className='text-primary font-semibold uppercase'>{name}</span>
</Link>
</header>
);
}
function SideBarDrawer({
innerRef,
isOpen,
brandLogo,
brandName,
primaryNavItems,
secondaryNavItems,
overflowGroups,
onClose,
}) {
return (
<aside
ref={innerRef}
className={`sidebar-drawer ${isOpen ? 'sidebar-drawer-open' : 'sidebar-drawer-closed'}`}
>
{/* Brand */}
<div className='sidebar-nav-brand' onClick={onClose}>
<Link to='/' className='flex items-center'>
<img src={brandLogo} className='sidebar-nav-brand-logo' alt='' />
<span className='sidebar-nav-brand-text'>{brandName}</span>
</Link>
</div>
{/* Navigation content */}
<div className='sidebar-nav-content'>
{/* Primary navigation */}
<SideBarSection>
{primaryNavItems &&
primaryNavItems.map(navItem => (
<SideBarLink key={navItem.id} item={navItem} isCollapsed={false} onClick={onClose} />
))}
</SideBarSection>
<SideBarDivider />
{/* Secondary navigation */}
<SideBarSection>
{secondaryNavItems &&
secondaryNavItems.map(navItem => (
<SideBarLink key={navItem.id} item={navItem} isCollapsed={false} onClick={onClose} />
))}
</SideBarSection>
<SideBarDivider />
{/* Overflow items grouped */}
{overflowGroups.map((group, i) => (
<React.Fragment key={i}>
<SideBarSection>
{group.map(navItem => (
<SideBarLink key={navItem.id} item={navItem} isCollapsed={false} onClick={onClose} />
))}
</SideBarSection>
{i < overflowGroups.length - 1 && <SideBarDivider />}
</React.Fragment>
))}
</div>
</aside>
);
}
function SideBarBackdrop({ isOpen, onClick }) {
return (
<div
className={`sidebar-drawer-backdrop ${isOpen ? 'sidebar-drawer-backdrop-visible' : 'sidebar-drawer-backdrop-hidden'}`}
onClick={onClick}
aria-hidden='true'
/>
);
}
function getCollapseGroups(items) {
const groupMapping = {};
for (const navItem of items) {
const groupId = navItem.collapseGroup || -1;
if (groupMapping[groupId]) {
groupMapping[groupId].push(navItem);
} else {
groupMapping[groupId] = [navItem];
}
}
const sortedIds = Object.keys(groupMapping).sort();
const groups = [];
for (const groupId of sortedIds) {
const sortedGroup = _.sortBy(groupMapping[groupId], ['collapseOrder']);
groups.push(sortedGroup);
}
return groups;
}

View File

@@ -133,4 +133,89 @@ function HiscoresIcon({ isHiscoreError }) {
);
}
export default { NavBarItem, CollapsedMenu };
function SideBarItem({ setCharacterModalOpen, isCollapsed, onNavigate }) {
const [isExpanded, setExpanded] = useState(false);
const dispatch = useDispatch();
const menuRef = useRef(null);
const characterState = useSelector(state => state.character);
const activeCharacter = characterState.characters[characterState.activeCharacter];
const isHiscoreError = !!characterState.hiscoresCache.error;
useClickListener(menuRef, () => setExpanded(false), true);
if (!activeCharacter) {
return (
<button
className='sidebar-nav-link w-full text-left'
type='button'
onClick={() => {
setCharacterModalOpen(true);
if (onNavigate) {
onNavigate();
}
}}
title={isCollapsed ? 'Character setup' : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>manage_accounts</span>
{!isCollapsed && <span className='sidebar-nav-link-label'>Character</span>}
</button>
);
}
return (
<div className='relative' ref={menuRef}>
<button
className='sidebar-nav-link w-full text-left'
type='button'
onClick={() => setExpanded(!isExpanded)}
title={isCollapsed ? activeCharacter : undefined}
>
<span className={`sidebar-nav-link-icon icon-base ${isHiscoreError ? 'text-error' : ''}`}>
{isHiscoreError ? 'error' : 'account_circle'}
</span>
{!isCollapsed && (
<>
<span className='sidebar-nav-link-label'>{activeCharacter}</span>
<span className='ml-auto icon-sm text-primary-alt'>{isExpanded ? 'expand_less' : 'expand_more'}</span>
</>
)}
</button>
{isExpanded && !isCollapsed && (
<div className='bg-secondary-alt pl-4'>
<button
className='sidebar-nav-link w-full text-left text-sm'
onClick={() => dispatch(fetchHiscores(characterState, null, true))}
type='button'
>
{characterState.hiscoresCache.loading ? (
<Spinner size={Spinner.SIZE.sm} invertColorForDarkMode={false} />
) : (
<>
<span className={`sidebar-nav-link-icon icon-base ${isHiscoreError ? 'text-error' : 'text-primary-alt'}`}>
{isHiscoreError ? 'sync_problem' : 'cached'}
</span>
<span className='sidebar-nav-link-label'>Update hiscores</span>
</>
)}
</button>
<button
className='sidebar-nav-link w-full text-left text-sm'
onClick={() => {
setCharacterModalOpen(true);
setExpanded(false);
if (onNavigate) {
onNavigate();
}
}}
type='button'
>
<span className='sidebar-nav-link-icon icon-base text-primary-alt'>manage_accounts</span>
<span className='sidebar-nav-link-label'>Manage characters</span>
</button>
</div>
)}
</div>
);
}
export default { NavBarItem, CollapsedMenu, SideBarItem };

View File

@@ -28,4 +28,23 @@ function CollapsedMenu({ setFeedbackModalOpen }) {
);
}
export default { NavBarItem, CollapsedMenu };
function SideBarItem({ setFeedbackModalOpen, isCollapsed, onNavigate }) {
return (
<button
className='sidebar-nav-link w-full text-left'
onClick={() => {
setFeedbackModalOpen(true);
if (onNavigate) {
onNavigate();
}
}}
type='button'
title={isCollapsed ? 'Feedback' : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>pest_control</span>
{!isCollapsed && <span className='sidebar-nav-link-label'>Feedback</span>}
</button>
);
}
export default { NavBarItem, CollapsedMenu, SideBarItem };

View File

@@ -81,4 +81,75 @@ function CollapsedMenu({ setManageDataModalType }) {
);
}
export default { NavBarItem, CollapsedMenu };
function SideBarItem({ setManageDataModalType, isCollapsed, onNavigate }) {
const [isExpanded, setExpanded] = useState(false);
const menuRef = useRef(null);
useClickListener(menuRef, () => setExpanded(false), true);
return (
<div className='relative' ref={menuRef}>
<button
className='sidebar-nav-link w-full text-left'
type='button'
onClick={() => setExpanded(!isExpanded)}
title={isCollapsed ? 'Manage Data' : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>inventory_2</span>
{!isCollapsed && (
<>
<span className='sidebar-nav-link-label'>Data</span>
<span className='ml-auto icon-sm text-primary-alt'>{isExpanded ? 'expand_less' : 'expand_more'}</span>
</>
)}
</button>
{isExpanded && !isCollapsed && (
<div className='bg-secondary-alt pl-4'>
<button
className='sidebar-nav-link w-full text-left text-sm'
onClick={() => {
setManageDataModalType('import');
setExpanded(false);
if (onNavigate) {
onNavigate();
}
}}
type='button'
>
<span className='sidebar-nav-link-icon icon-base text-primary-alt'>file_download</span>
<span className='sidebar-nav-link-label'>Import</span>
</button>
<button
className='sidebar-nav-link w-full text-left text-sm'
onClick={() => {
setManageDataModalType('export');
setExpanded(false);
if (onNavigate) {
onNavigate();
}
}}
type='button'
>
<span className='sidebar-nav-link-icon icon-base text-primary-alt'>file_upload</span>
<span className='sidebar-nav-link-label'>Export</span>
</button>
<button
className='sidebar-nav-link w-full text-left text-sm'
onClick={() => {
setManageDataModalType('reset');
setExpanded(false);
if (onNavigate) {
onNavigate();
}
}}
type='button'
>
<span className='sidebar-nav-link-icon icon-base text-primary-alt'>dangerous</span>
<span className='sidebar-nav-link-label'>Reset</span>
</button>
</div>
)}
</div>
);
}
export default { NavBarItem, CollapsedMenu, SideBarItem };

View File

@@ -0,0 +1,48 @@
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
const SidebarContext = createContext();
const STORAGE_KEY = 'sidebar-collapsed';
export function SidebarProvider({ children }) {
const [isCollapsed, setIsCollapsed] = useState(() => {
const stored = localStorage.getItem(STORAGE_KEY);
return stored === 'true';
});
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, isCollapsed);
}, [isCollapsed]);
const toggleCollapse = useCallback(() => setIsCollapsed(prev => !prev), []);
const toggleDrawer = useCallback(() => setIsDrawerOpen(prev => !prev), []);
const closeDrawer = useCallback(() => setIsDrawerOpen(false), []);
const openDrawer = useCallback(() => setIsDrawerOpen(true), []);
const value = useMemo(
() => ({
isCollapsed,
setIsCollapsed,
isDrawerOpen,
setIsDrawerOpen,
toggleCollapse,
toggleDrawer,
closeDrawer,
openDrawer,
}),
[isCollapsed, isDrawerOpen, toggleCollapse, toggleDrawer, closeDrawer, openDrawer]
);
return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;
}
export function useSidebar() {
const context = useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
}
export default SidebarContext;

View File

@@ -2180,6 +2180,459 @@ select[multiple]:focus option:checked {
box-shadow: inset 0px -7px 5px -5px var(--tw-shadow-color), 3px -3px 3px -3px var(--tw-shadow-color),
-3px -3px 3px -3px var(--tw-shadow-color);
}
/* SIDEBAR NAVIGATION */
.sidebar-nav {
position: fixed;
left: 0px;
top: 0px;
z-index: 20;
height: 100%;
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.sidebar-nav:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(63 63 70 / var(--tw-bg-opacity, 1));
}
.sidebar-nav {
display: flex;
flex-direction: column;
transition-property: all;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.sidebar-nav:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(113 113 122 / var(--tw-border-opacity, 1));
}
.sidebar-nav {
border-right-width: 1px;
}
.sidebar-nav-expanded {
width: 16rem;
}
.sidebar-nav-collapsed {
width: 4rem;
}
.sidebar-nav-brand {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.sidebar-nav-brand:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(113 113 122 / var(--tw-border-opacity, 1));
}
.sidebar-nav-brand {
display: flex;
height: 4rem;
align-items: center;
border-bottom-width: 1px;
padding: 1rem;
}
.sidebar-nav-brand-logo {
height: 2rem;
width: 2rem;
flex-shrink: 0;
}
.sidebar-nav-brand-text {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity, 1));
}
.sidebar-nav-brand-text:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(244 244 245 / var(--tw-text-opacity, 1));
}
.sidebar-nav-brand-text {
margin-left: 0.75rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 600;
text-transform: uppercase;
overflow: hidden;
white-space: nowrap;
}
.sidebar-nav-content {
flex: 1 1 0%;
overflow-y: auto;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.sidebar-nav-section {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.sidebar-nav-section-header {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
.sidebar-nav-section-header:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(161 161 170 / var(--tw-text-opacity, 1));
}
.sidebar-nav-section-header {
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-size: 0.75rem;
line-height: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
overflow: hidden;
white-space: nowrap;
}
.sidebar-nav-link {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity, 1));
}
.sidebar-nav-link:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(244 244 245 / var(--tw-text-opacity, 1));
}
.sidebar-nav-link {
display: flex;
align-items: center;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
cursor: pointer;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.sidebar-nav-link:hover:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-link:hover:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(39 39 42 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-link.active {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-link.active:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(70 70 78 / var(--tw-bg-opacity, 1));
}
.theme-tl-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(164 206 39 / var(--tw-text-opacity, 1));
}
.theme-tl-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(100 144 68 / var(--tw-text-opacity, 1));
}
.theme-tb-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(229 217 147 / var(--tw-text-opacity, 1));
}
.theme-tb-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(99 66 40 / var(--tw-text-opacity, 1));
}
.theme-sl-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(19 213 145 / var(--tw-text-opacity, 1));
}
.theme-sl-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(0 128 118 / var(--tw-text-opacity, 1));
}
.theme-tr-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(220 139 54 / var(--tw-text-opacity, 1));
}
.theme-tr-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(180 74 30 / var(--tw-text-opacity, 1));
}
.theme-re-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(32 195 254 / var(--tw-text-opacity, 1));
}
.theme-re-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(3 79 146 / var(--tw-text-opacity, 1));
}
.theme-mono-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(249 250 251 / var(--tw-text-opacity, 1));
}
.theme-mono-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
.theme-dp-dark .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(235 78 104 / var(--tw-text-opacity, 1));
}
.theme-dp-light .sidebar-nav-link.active {
--tw-text-opacity: 1;
color: rgb(107 28 35 / var(--tw-text-opacity, 1));
}
.theme-tl-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(164 206 39 / var(--tw-border-opacity, 1));
}
.theme-tl-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(100 144 68 / var(--tw-border-opacity, 1));
}
.theme-tb-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(229 217 147 / var(--tw-border-opacity, 1));
}
.theme-tb-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(99 66 40 / var(--tw-border-opacity, 1));
}
.theme-sl-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(19 213 145 / var(--tw-border-opacity, 1));
}
.theme-sl-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(0 128 118 / var(--tw-border-opacity, 1));
}
.theme-tr-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(220 139 54 / var(--tw-border-opacity, 1));
}
.theme-tr-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(180 74 30 / var(--tw-border-opacity, 1));
}
.theme-re-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(32 195 254 / var(--tw-border-opacity, 1));
}
.theme-re-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(3 79 146 / var(--tw-border-opacity, 1));
}
.theme-mono-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(249 250 251 / var(--tw-border-opacity, 1));
}
.theme-mono-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(55 65 81 / var(--tw-border-opacity, 1));
}
.theme-dp-dark .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(235 78 104 / var(--tw-border-opacity, 1));
}
.theme-dp-light .sidebar-nav-link.active {
--tw-border-opacity: 1;
border-color: rgb(107 28 35 / var(--tw-border-opacity, 1));
}
.sidebar-nav-link.active {
border-right-width: 2px;
}
.sidebar-nav-link-icon {
flex-shrink: 0;
font-size: 1.25rem;
line-height: 1.75rem;
}
.sidebar-nav-link-label {
margin-left: 0.75rem;
overflow: hidden;
white-space: nowrap;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875rem;
line-height: 1.25rem;
text-transform: uppercase;
}
.sidebar-nav-divider {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-divider:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(113 113 122 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-divider {
margin-left: 1rem;
margin-right: 1rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
height: 1px;
}
.sidebar-nav-toggle {
position: absolute;
right: -0.75rem;
top: 50%;
height: 1.5rem;
width: 1.5rem;
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
border-radius: 9999px;
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-toggle:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(63 63 70 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-toggle {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.sidebar-nav-toggle:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(113 113 122 / var(--tw-border-opacity, 1));
}
.sidebar-nav-toggle {
border-width: 1px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: center;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.sidebar-nav-toggle:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-toggle:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(70 70 78 / var(--tw-bg-opacity, 1));
}
.sidebar-nav-toggle {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
/* Mobile drawer styles */
.sidebar-drawer-backdrop {
position: fixed;
inset: 0px;
z-index: 30;
background-color: rgb(0 0 0 / 0.5);
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.sidebar-drawer-backdrop-hidden {
pointer-events: none;
opacity: 0;
}
.sidebar-drawer-backdrop-visible {
opacity: 1;
}
.sidebar-drawer {
position: fixed;
left: 0px;
top: 0px;
z-index: 40;
height: 100%;
width: 16rem;
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.sidebar-drawer:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(63 63 70 / var(--tw-bg-opacity, 1));
}
.sidebar-drawer {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.sidebar-drawer:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(113 113 122 / var(--tw-border-opacity, 1));
}
.sidebar-drawer {
border-right-width: 1px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
transition-property: transform;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
}
.sidebar-drawer-closed {
--tw-translate-x: -100%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sidebar-drawer-open {
--tw-translate-x: 0px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
/* Mobile header bar */
.mobile-header {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
z-index: 10;
}
@media (min-width: 768px) {.mobile-header {
display: none;
}
}
.mobile-header {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.mobile-header:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(63 63 70 / var(--tw-bg-opacity, 1));
}
.mobile-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.mobile-header:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(113 113 122 / var(--tw-border-opacity, 1));
}
.mobile-header {
border-bottom-width: 1px;
}
.mobile-header-menu-btn:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.mobile-header-menu-btn:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(39 39 42 / var(--tw-bg-opacity, 1));
}
.mobile-header-menu-btn {
cursor: pointer;
border-radius: 0.25rem;
padding: 0.5rem;
}
/* Content offset for fixed sidebar */
.main-content-with-sidebar {
transition-property: all;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@media (min-width: 1024px) {.main-content-sidebar-expanded {
margin-left: 16rem;
}.main-content-sidebar-collapsed {
margin-left: 4rem;
}
}
.main-content-mobile-header {
padding-top: 3.5rem;
}
@media (min-width: 1024px) {.main-content-mobile-header {
padding-top: 0px;
}
}
.\!visible {
visibility: visible !important;
}
@@ -2387,6 +2840,9 @@ select[multiple]:focus option:checked {
.h-5 {
height: 1.25rem;
}
.h-6 {
height: 1.5rem;
}
.h-8 {
height: 2rem;
}
@@ -2459,6 +2915,9 @@ select[multiple]:focus option:checked {
.w-5 {
width: 1.25rem;
}
.w-6 {
width: 1.5rem;
}
.w-60 {
width: 15rem;
}

View File

@@ -360,6 +360,111 @@
box-shadow: inset 0px -7px 5px -5px var(--tw-shadow-color), 3px -3px 3px -3px var(--tw-shadow-color),
-3px -3px 3px -3px var(--tw-shadow-color);
}
/* SIDEBAR NAVIGATION */
.sidebar-nav {
@apply fixed left-0 top-0 h-full z-20;
@apply bg-primary;
@apply flex flex-col;
@apply transition-all duration-300 ease-in-out;
@apply border-r border-subdued;
}
.sidebar-nav-expanded {
@apply w-64;
}
.sidebar-nav-collapsed {
@apply w-16;
}
.sidebar-nav-brand {
@apply flex items-center p-4 border-b border-subdued h-16;
}
.sidebar-nav-brand-logo {
@apply h-8 w-8 flex-shrink-0;
}
.sidebar-nav-brand-text {
@apply ml-3 text-lg font-semibold uppercase text-primary;
@apply whitespace-nowrap overflow-hidden;
}
.sidebar-nav-content {
@apply flex-1 overflow-y-auto py-2;
}
.sidebar-nav-section {
@apply py-1;
}
.sidebar-nav-section-header {
@apply px-4 py-2 text-xs uppercase tracking-wider text-secondary-alt;
@apply whitespace-nowrap overflow-hidden;
}
.sidebar-nav-link {
@apply flex items-center px-4 py-3 text-primary;
@apply hover:bg-hover transition-colors cursor-pointer;
}
.sidebar-nav-link.active {
@apply text-accent border-r-2 border-accent bg-primary-alt;
}
.sidebar-nav-link-icon {
@apply text-xl flex-shrink-0;
}
.sidebar-nav-link-label {
@apply ml-3 font-mono text-sm uppercase whitespace-nowrap overflow-hidden;
}
.sidebar-nav-divider {
@apply h-px mx-4 my-2 bg-subdued;
}
.sidebar-nav-toggle {
@apply absolute -right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full;
@apply bg-primary border border-subdued;
@apply flex items-center justify-center cursor-pointer;
@apply hover:bg-primary-alt transition-colors;
@apply shadow-sm;
}
.sidebar-nav-footer {
@apply border-t border-subdued p-2;
}
/* Mobile drawer styles */
.sidebar-drawer-backdrop {
@apply fixed inset-0 bg-black/50 z-30;
@apply transition-opacity duration-300;
}
.sidebar-drawer-backdrop-hidden {
@apply opacity-0 pointer-events-none;
}
.sidebar-drawer-backdrop-visible {
@apply opacity-100;
}
.sidebar-drawer {
@apply fixed left-0 top-0 h-full w-64 z-40;
@apply bg-primary border-r border-subdued;
@apply transform transition-transform duration-300 ease-in-out;
@apply flex flex-col;
}
.sidebar-drawer-closed {
@apply -translate-x-full;
}
.sidebar-drawer-open {
@apply translate-x-0;
}
/* Mobile header bar */
.mobile-header {
@apply md:hidden fixed top-0 left-0 right-0 z-10;
@apply bg-primary p-3 flex items-center gap-3;
@apply border-b border-subdued;
}
.mobile-header-menu-btn {
@apply p-2 bg-hover rounded cursor-pointer;
}
/* Content offset for fixed sidebar */
.main-content-with-sidebar {
@apply transition-all duration-300 ease-in-out;
}
.main-content-sidebar-expanded {
@apply lg:ml-64;
}
.main-content-sidebar-collapsed {
@apply lg:ml-16;
}
.main-content-mobile-header {
@apply pt-14 lg:pt-0;
}
}
@layer utilities {