Add a static images manager and polish dashboard layout and fonts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-16 23:56:17 +03:30
co-authored by Cursor
parent e79211e3cd
commit b9024ad77a
57 changed files with 2025 additions and 122 deletions
+18 -1
View File
@@ -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;
+16 -2
View File
@@ -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));
}
}
+49 -4
View File
@@ -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);
}
}
+9 -2
View File
@@ -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}
+16
View File
@@ -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');
+2
View File
@@ -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': 'تغییر رمز عبور',
+6 -4
View File
@@ -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
View File
@@ -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',