Add a static images manager and polish dashboard layout and fonts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
e79211e3cd
commit
b9024ad77a
@@ -69,6 +69,7 @@
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menuBtn:hover {
|
||||
@@ -256,11 +257,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1280px) {
|
||||
.menuBtn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.right {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profileInfo {
|
||||
display: none;
|
||||
}
|
||||
@@ -270,6 +281,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.iconBtn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header {
|
||||
padding: 12px 16px;
|
||||
|
||||
@@ -39,9 +39,16 @@ interface HeaderProps {
|
||||
hideMenu?: boolean
|
||||
/** Show website logo + name on the start side (visual right in RTL). */
|
||||
showBrand?: boolean
|
||||
navOpen?: boolean
|
||||
onMenuClick?: () => void
|
||||
}
|
||||
|
||||
export function Header({ hideMenu = false, showBrand = false }: HeaderProps) {
|
||||
export function Header({
|
||||
hideMenu = false,
|
||||
showBrand = false,
|
||||
navOpen = false,
|
||||
onMenuClick,
|
||||
}: HeaderProps) {
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuth()
|
||||
const { locale } = useLocale()
|
||||
@@ -129,7 +136,14 @@ export function Header({ hideMenu = false, showBrand = false }: HeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
) : !hideMenu ? (
|
||||
<button className={styles.menuBtn} aria-label={t('header.toggleMenu')}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuBtn}
|
||||
aria-label={t('header.toggleMenu')}
|
||||
aria-expanded={navOpen}
|
||||
aria-controls="dashboard-sidebar"
|
||||
onClick={onMenuClick}
|
||||
>
|
||||
<Menu size={22} />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.content {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
@@ -115,10 +116,19 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
@media (max-width: 1600px) {
|
||||
.gridHome {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.col6,
|
||||
.col4 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.col3 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
@@ -150,6 +160,8 @@
|
||||
|
||||
.dateBadge {
|
||||
align-self: flex-start;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.grid,
|
||||
|
||||
@@ -6,12 +6,30 @@
|
||||
.main {
|
||||
margin-inline-start: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navScrim {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.main {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.navScrim {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 180;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
background: var(--modal-overlay-bg);
|
||||
backdrop-filter: blur(var(--modal-overlay-blur, 16px));
|
||||
-webkit-backdrop-filter: blur(var(--modal-overlay-blur, 16px));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,60 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Outlet, useLocation } from 'react-router-dom'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { Sidebar } from './Sidebar'
|
||||
import { Header } from './Header'
|
||||
import styles from './PageLayout.module.css'
|
||||
|
||||
const NAV_EXPAND_QUERY = '(min-width: 1281px)'
|
||||
|
||||
export function PageLayout() {
|
||||
const { pathname } = useLocation()
|
||||
const t = useT()
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setNavOpen(false)
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(NAV_EXPAND_QUERY)
|
||||
function collapseWhenExpanded() {
|
||||
if (media.matches) setNavOpen(false)
|
||||
}
|
||||
media.addEventListener('change', collapseWhenExpanded)
|
||||
return () => media.removeEventListener('change', collapseWhenExpanded)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!navOpen) return
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') setNavOpen(false)
|
||||
}
|
||||
|
||||
const previousOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}, [navOpen])
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<Sidebar />
|
||||
<div className={styles.layout} data-nav-open={navOpen ? 'true' : undefined}>
|
||||
{navOpen ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.navScrim}
|
||||
aria-label={t('header.closeMenu')}
|
||||
onClick={() => setNavOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
<Sidebar open={navOpen} />
|
||||
|
||||
<div className={styles.main}>
|
||||
<Header />
|
||||
<Header navOpen={navOpen} onMenuClick={() => setNavOpen((open) => !open)} />
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.brandName {
|
||||
@@ -73,6 +76,9 @@
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav {
|
||||
@@ -170,8 +176,19 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1280px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
z-index: 200;
|
||||
inset-inline-start: calc(-1 * var(--sidebar-width));
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: inset-inline-start 0.25s ease, box-shadow 0.25s ease, visibility 0.25s;
|
||||
}
|
||||
|
||||
.open {
|
||||
inset-inline-start: 0;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ import { hasBusinessModule } from '../utils/businessModules'
|
||||
import meshkeeLogo from '../assets/meshkee-logo.png'
|
||||
import styles from './Sidebar.module.css'
|
||||
|
||||
export function Sidebar() {
|
||||
interface SidebarProps {
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
export function Sidebar({ open = false }: SidebarProps) {
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuth()
|
||||
const { locale } = useLocale()
|
||||
@@ -65,7 +69,10 @@ export function Sidebar() {
|
||||
}, [businessDomain, fallbackBusinessName, locale])
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<aside
|
||||
id="dashboard-sidebar"
|
||||
className={`${styles.sidebar} ${open ? styles.open : ''}`}
|
||||
>
|
||||
<div className={styles.brand}>
|
||||
<img
|
||||
src={logoUrl || meshkeeLogo}
|
||||
|
||||
@@ -14,6 +14,22 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'YekanBakh';
|
||||
src: url('/fonts/yekanbakh/YekanBakh-Regular.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'YekanBakh';
|
||||
src: url('/fonts/yekanbakh/YekanBakh-Bold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'YekanBakh';
|
||||
src: url('/fonts/yekanbakh/YekanBakh-Bold.woff2') format('woff2');
|
||||
|
||||
@@ -19,6 +19,7 @@ const en = {
|
||||
'nav.logout': 'Logout',
|
||||
|
||||
'header.toggleMenu': 'Toggle menu',
|
||||
'header.closeMenu': 'Close menu',
|
||||
'header.messages': 'Messages',
|
||||
'header.notifications': 'Notifications',
|
||||
'header.changePassword': 'Change password',
|
||||
@@ -407,6 +408,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'nav.logout': 'خروج',
|
||||
|
||||
'header.toggleMenu': 'باز و بسته کردن منو',
|
||||
'header.closeMenu': 'بستن منو',
|
||||
'header.messages': 'پیامها',
|
||||
'header.notifications': 'اعلانها',
|
||||
'header.changePassword': 'تغییر رمز عبور',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@import '@meshkee/dashboard-core/styles/tokens.css';
|
||||
|
||||
/* Customer: Montserrat (EN) + Yekan Bakh (FA) via --font-ui stack */
|
||||
/* Customer: Montserrat (EN) + Yekan Bakh (FA) via --font-ui stack.
|
||||
Named faces only in --font-en/--font-fa so Persian reaches YekanBakh. */
|
||||
:root {
|
||||
--font-en: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-fa: 'YekanBakh', Tahoma, sans-serif;
|
||||
--font-ui: var(--font-en), var(--font-fa);
|
||||
--font-en: 'Montserrat';
|
||||
--font-fa: 'YekanBakh';
|
||||
--font-ui: var(--font-en), var(--font-fa), -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma,
|
||||
sans-serif;
|
||||
|
||||
/* Neutral light surfaces (theme color stays on accents only) */
|
||||
--bg-gradient-start: #f4f5f7;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './fonts/iranyekan.css'
|
||||
import './fonts/yekanbakh.css'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (min-width: 1600px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (min-width: 1600px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--glass-shadow);
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const BUSINESS_PRIMARY_COLOR_IDS = [
|
||||
'red',
|
||||
'dark-red',
|
||||
'yellow',
|
||||
'black',
|
||||
'cyan',
|
||||
@@ -59,6 +60,16 @@ export const BUSINESS_PRIMARY_COLOR_PALETTE: Record<
|
||||
primaryDarkRgb: '220 38 38',
|
||||
...PURPLE_ACCENT,
|
||||
},
|
||||
'dark-red': {
|
||||
label: 'Dark Red',
|
||||
primary: '#991b1b',
|
||||
primaryDark: '#7f1d1d',
|
||||
primaryLight: '#fee2e2',
|
||||
primaryGlow: '#b91c1c',
|
||||
primaryRgb: '153 27 27',
|
||||
primaryDarkRgb: '127 29 29',
|
||||
...PURPLE_ACCENT,
|
||||
},
|
||||
yellow: {
|
||||
label: 'Yellow',
|
||||
primary: '#eab308',
|
||||
|
||||
Reference in New Issue
Block a user