Improve mobile swipe tabs with native scroll-snap
- Rewrite SwipeTabs to use CSS scroll-snap for smoother transitions - Add GPU acceleration hints for fluid animations - Use native scrolling behavior instead of manual touch handling - Add swipe-tabs--snap and swipe-tabs--static variants - Improve height transitions and layout containment - Update dimension variables for better spacing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
326
frontend/src/components/SwipeTabs.tsx
Normal file
326
frontend/src/components/SwipeTabs.tsx
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type SwipeTabsProps<T extends string> = {
|
||||||
|
tabs: readonly T[];
|
||||||
|
activeTab: T;
|
||||||
|
onTabChange: (tab: T) => void;
|
||||||
|
renderPanel: (tab: T) => ReactNode;
|
||||||
|
className?: string;
|
||||||
|
panelClassName?: string;
|
||||||
|
renderWindow?: number;
|
||||||
|
threshold?: number;
|
||||||
|
directionRatio?: number;
|
||||||
|
respectScrollableTargets?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SCROLL_END_DELAY = 120;
|
||||||
|
const COARSE_POINTER_QUERY = '(pointer: coarse)';
|
||||||
|
const NO_HOVER_QUERY = '(hover: none)';
|
||||||
|
const FALLBACK_WIDTH_QUERY = '(max-width: 1024px)';
|
||||||
|
|
||||||
|
const getSwipeEnabled = () => {
|
||||||
|
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
||||||
|
const coarsePointer = window.matchMedia(COARSE_POINTER_QUERY).matches;
|
||||||
|
const noHover = window.matchMedia(NO_HOVER_QUERY).matches;
|
||||||
|
if (coarsePointer || noHover) return true;
|
||||||
|
const supportsTouch = ('ontouchstart' in window)
|
||||||
|
|| (typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0);
|
||||||
|
if (!supportsTouch) return false;
|
||||||
|
return window.matchMedia(FALLBACK_WIDTH_QUERY).matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SwipeTabs<T extends string>({
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
renderPanel,
|
||||||
|
className,
|
||||||
|
panelClassName,
|
||||||
|
renderWindow = 0,
|
||||||
|
}: SwipeTabsProps<T>) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const panelRefs = useRef(new Map<T, HTMLDivElement>());
|
||||||
|
const activeIndex = tabs.indexOf(activeTab);
|
||||||
|
const safeActiveIndex = activeIndex >= 0 ? activeIndex : 0;
|
||||||
|
const maxIndex = Math.max(0, tabs.length - 1);
|
||||||
|
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
const [containerHeight, setContainerHeight] = useState<number | null>(null);
|
||||||
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
|
const [scrollPosition, setScrollPosition] = useState(0);
|
||||||
|
const [swipeEnabled, setSwipeEnabled] = useState(() => getSwipeEnabled());
|
||||||
|
|
||||||
|
const scrollEndTimer = useRef<number | null>(null);
|
||||||
|
const scrollRaf = useRef<number | null>(null);
|
||||||
|
const scrollPositionRef = useRef(0);
|
||||||
|
const isUserScrolling = useRef(false);
|
||||||
|
const hasMounted = useRef(false);
|
||||||
|
const lastReportedIndex = useRef(safeActiveIndex);
|
||||||
|
|
||||||
|
const prefersReducedMotion = useMemo(() => {
|
||||||
|
if (typeof window === 'undefined' || !window.matchMedia) return true;
|
||||||
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||||||
|
const pointerMql = window.matchMedia(COARSE_POINTER_QUERY);
|
||||||
|
const hoverMql = window.matchMedia(NO_HOVER_QUERY);
|
||||||
|
const widthMql = window.matchMedia(FALLBACK_WIDTH_QUERY);
|
||||||
|
const update = () => setSwipeEnabled(getSwipeEnabled());
|
||||||
|
update();
|
||||||
|
|
||||||
|
const addListener = (mql: MediaQueryList) => {
|
||||||
|
if (mql.addEventListener) {
|
||||||
|
mql.addEventListener('change', update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mql.addListener(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeListener = (mql: MediaQueryList) => {
|
||||||
|
if (mql.removeEventListener) {
|
||||||
|
mql.removeEventListener('change', update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mql.removeListener(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
addListener(pointerMql);
|
||||||
|
addListener(hoverMql);
|
||||||
|
addListener(widthMql);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener(pointerMql);
|
||||||
|
removeListener(hoverMql);
|
||||||
|
removeListener(widthMql);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const element = containerRef.current;
|
||||||
|
const updateSize = () => {
|
||||||
|
const styles = window.getComputedStyle(element);
|
||||||
|
const paddingX = parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);
|
||||||
|
const width = element.clientWidth - (Number.isFinite(paddingX) ? paddingX : 0);
|
||||||
|
setContainerWidth(width > 0 ? width : element.clientWidth);
|
||||||
|
};
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(updateSize);
|
||||||
|
observer.observe(element);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const panel = panelRefs.current.get(activeTab);
|
||||||
|
if (!panel) {
|
||||||
|
setContainerHeight(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateHeight = () => setContainerHeight(panel.offsetHeight);
|
||||||
|
updateHeight();
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
window.addEventListener('resize', updateHeight);
|
||||||
|
return () => window.removeEventListener('resize', updateHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(updateHeight);
|
||||||
|
observer.observe(panel);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (scrollEndTimer.current !== null) {
|
||||||
|
window.clearTimeout(scrollEndTimer.current);
|
||||||
|
}
|
||||||
|
if (scrollRaf.current !== null) {
|
||||||
|
cancelAnimationFrame(scrollRaf.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
lastReportedIndex.current = safeActiveIndex;
|
||||||
|
}, [safeActiveIndex]);
|
||||||
|
|
||||||
|
const scrollToIndex = useCallback((index: number, behavior: ScrollBehavior) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container || containerWidth <= 0) return;
|
||||||
|
const targetLeft = index * containerWidth;
|
||||||
|
if (Math.abs(container.scrollLeft - targetLeft) <= 1) return;
|
||||||
|
container.scrollTo({ left: targetLeft, behavior });
|
||||||
|
}, [containerWidth]);
|
||||||
|
|
||||||
|
const scheduleScrollPositionUpdate = useCallback(() => {
|
||||||
|
if (scrollRaf.current !== null) return;
|
||||||
|
scrollRaf.current = window.requestAnimationFrame(() => {
|
||||||
|
scrollRaf.current = null;
|
||||||
|
setScrollPosition(scrollPositionRef.current);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!swipeEnabled) return;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container || containerWidth <= 0) return;
|
||||||
|
|
||||||
|
if (!isUserScrolling.current) {
|
||||||
|
isUserScrolling.current = true;
|
||||||
|
setIsScrolling(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawIndex = container.scrollLeft / containerWidth;
|
||||||
|
scrollPositionRef.current = rawIndex;
|
||||||
|
scheduleScrollPositionUpdate();
|
||||||
|
|
||||||
|
const roundedIndex = Math.round(rawIndex);
|
||||||
|
const clampedIndex = Math.max(0, Math.min(roundedIndex, maxIndex));
|
||||||
|
if (tabs[clampedIndex] && clampedIndex !== lastReportedIndex.current) {
|
||||||
|
lastReportedIndex.current = clampedIndex;
|
||||||
|
onTabChange(tabs[clampedIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollEndTimer.current !== null) {
|
||||||
|
window.clearTimeout(scrollEndTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollEndTimer.current = window.setTimeout(() => {
|
||||||
|
const rawIndex = container.scrollLeft / containerWidth;
|
||||||
|
const nextIndex = Math.round(rawIndex);
|
||||||
|
const clampedIndex = Math.max(0, Math.min(nextIndex, maxIndex));
|
||||||
|
const targetLeft = clampedIndex * containerWidth;
|
||||||
|
|
||||||
|
if (Math.abs(container.scrollLeft - targetLeft) > 1) {
|
||||||
|
container.scrollTo({ left: targetLeft, behavior: 'auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabs[clampedIndex] && clampedIndex !== lastReportedIndex.current) {
|
||||||
|
lastReportedIndex.current = clampedIndex;
|
||||||
|
onTabChange(tabs[clampedIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollPositionRef.current = targetLeft / containerWidth;
|
||||||
|
scheduleScrollPositionUpdate();
|
||||||
|
|
||||||
|
isUserScrolling.current = false;
|
||||||
|
setIsScrolling(false);
|
||||||
|
}, SCROLL_END_DELAY);
|
||||||
|
}, [activeTab, containerWidth, maxIndex, onTabChange, scheduleScrollPositionUpdate, swipeEnabled, tabs]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!swipeEnabled) return;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container || containerWidth <= 0) return;
|
||||||
|
if (isUserScrolling.current) return;
|
||||||
|
const behavior: ScrollBehavior = hasMounted.current && !prefersReducedMotion ? 'smooth' : 'auto';
|
||||||
|
scrollToIndex(safeActiveIndex, behavior);
|
||||||
|
hasMounted.current = true;
|
||||||
|
const currentPosition = container.scrollLeft / containerWidth;
|
||||||
|
scrollPositionRef.current = currentPosition;
|
||||||
|
setScrollPosition(currentPosition);
|
||||||
|
}, [containerWidth, prefersReducedMotion, safeActiveIndex, scrollToIndex, swipeEnabled]);
|
||||||
|
|
||||||
|
const visibleIndexes = useMemo(() => {
|
||||||
|
const indexes = new Set<number>();
|
||||||
|
if (!swipeEnabled) {
|
||||||
|
if (safeActiveIndex >= 0) {
|
||||||
|
indexes.add(safeActiveIndex);
|
||||||
|
}
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
const centers = new Set<number>();
|
||||||
|
centers.add(safeActiveIndex);
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
centers.add(0);
|
||||||
|
}
|
||||||
|
if (containerWidth > 0 && Number.isFinite(scrollPosition)) {
|
||||||
|
const base = Math.floor(scrollPosition);
|
||||||
|
const next = Math.ceil(scrollPosition);
|
||||||
|
centers.add(base);
|
||||||
|
centers.add(next);
|
||||||
|
}
|
||||||
|
centers.forEach((center) => {
|
||||||
|
for (let offset = -renderWindow; offset <= renderWindow; offset += 1) {
|
||||||
|
const index = center + offset;
|
||||||
|
if (index >= 0 && index < tabs.length) {
|
||||||
|
indexes.add(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return indexes;
|
||||||
|
}, [containerWidth, renderWindow, safeActiveIndex, scrollPosition, swipeEnabled, tabs.length]);
|
||||||
|
|
||||||
|
const containerStyle: CSSProperties = {
|
||||||
|
height: !isScrolling && containerHeight ? `${containerHeight}px` : 'auto',
|
||||||
|
transition: !isScrolling && containerHeight ? 'height 160ms ease-out' : 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!swipeEnabled) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`swipe-tabs swipe-tabs--static${className ? ` ${className}` : ''}`}
|
||||||
|
style={containerStyle}
|
||||||
|
>
|
||||||
|
<div className="swipe-tabs-track">
|
||||||
|
<div className="swipe-tabs-panel" aria-hidden={false}>
|
||||||
|
<div
|
||||||
|
className={`swipe-tabs-panel-inner${panelClassName ? ` ${panelClassName}` : ''}`}
|
||||||
|
ref={(node) => {
|
||||||
|
if (node) {
|
||||||
|
panelRefs.current.set(activeTab, node);
|
||||||
|
} else {
|
||||||
|
panelRefs.current.delete(activeTab);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderPanel(activeTab)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`swipe-tabs swipe-tabs--snap${className ? ` ${className}` : ''}${isScrolling ? ' is-scrolling' : ''}`}
|
||||||
|
style={containerStyle}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
<div className="swipe-tabs-track">
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
const shouldRender = visibleIndexes.has(index);
|
||||||
|
return (
|
||||||
|
<div key={tab} className="swipe-tabs-panel" aria-hidden={tab !== activeTab}>
|
||||||
|
<div
|
||||||
|
className={`swipe-tabs-panel-inner${panelClassName ? ` ${panelClassName}` : ''}`}
|
||||||
|
ref={(node) => {
|
||||||
|
if (node) {
|
||||||
|
panelRefs.current.set(tab, node);
|
||||||
|
} else {
|
||||||
|
panelRefs.current.delete(tab);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shouldRender ? renderPanel(tab) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useTranslation } from '../contexts/LanguageContext';
|
|||||||
import { useSidebar } from '../contexts/SidebarContext';
|
import { useSidebar } from '../contexts/SidebarContext';
|
||||||
import GeneralTab from '../components/admin/GeneralTab';
|
import GeneralTab from '../components/admin/GeneralTab';
|
||||||
import UsersTab from '../components/admin/UsersTab';
|
import UsersTab from '../components/admin/UsersTab';
|
||||||
|
import { SwipeTabs } from '../components/SwipeTabs';
|
||||||
import '../styles/AdminPanel.css';
|
import '../styles/AdminPanel.css';
|
||||||
|
|
||||||
type TabId = 'general' | 'users';
|
type TabId = 'general' | 'users';
|
||||||
@@ -13,6 +14,8 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toggleMobileMenu } = useSidebar();
|
const { toggleMobileMenu } = useSidebar();
|
||||||
const [activeTab, setActiveTab] = useState<TabId>(initialTab);
|
const [activeTab, setActiveTab] = useState<TabId>(initialTab);
|
||||||
|
const tabs: TabId[] = ['general', 'users'];
|
||||||
|
const renderPanel = (tab: TabId) => (tab === 'general' ? <GeneralTab /> : <UsersTab />);
|
||||||
|
|
||||||
if (!currentUser?.is_superuser) {
|
if (!currentUser?.is_superuser) {
|
||||||
return null;
|
return null;
|
||||||
@@ -46,10 +49,14 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-tab-content">
|
<SwipeTabs
|
||||||
{activeTab === 'general' && <GeneralTab />}
|
className="admin-tab-swipe"
|
||||||
{activeTab === 'users' && <UsersTab />}
|
tabs={tabs}
|
||||||
</div>
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
renderPanel={renderPanel}
|
||||||
|
panelClassName="admin-tab-content swipe-panel-content"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSidebar } from '../../contexts/SidebarContext';
|
|||||||
import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext';
|
import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext';
|
||||||
import type { ModuleId } from '../../contexts/ModulesContext';
|
import type { ModuleId } from '../../contexts/ModulesContext';
|
||||||
import Feature1Tab from '../../components/admin/Feature1Tab';
|
import Feature1Tab from '../../components/admin/Feature1Tab';
|
||||||
|
import { SwipeTabs } from '../../components/SwipeTabs';
|
||||||
import '../../styles/AdminPanel.css';
|
import '../../styles/AdminPanel.css';
|
||||||
|
|
||||||
type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
|
type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
|
||||||
@@ -281,6 +282,8 @@ export default function Features() {
|
|||||||
return position === 'bottom';
|
return position === 'bottom';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tabIds = ['config', ...topOrderModules, ...bottomOrderModules] as TabId[];
|
||||||
|
|
||||||
const renderConfigTab = () => {
|
const renderConfigTab = () => {
|
||||||
return (
|
return (
|
||||||
<div className="theme-tab-content">
|
<div className="theme-tab-content">
|
||||||
@@ -380,11 +383,11 @@ export default function Features() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = (tabId: TabId) => {
|
||||||
if (!hasInitialized || isLoading) {
|
if (!hasInitialized || isLoading) {
|
||||||
return <div className="loading">Loading...</div>;
|
return <div className="loading">Loading...</div>;
|
||||||
}
|
}
|
||||||
switch (activeTab) {
|
switch (tabId) {
|
||||||
case 'config':
|
case 'config':
|
||||||
return renderConfigTab();
|
return renderConfigTab();
|
||||||
case 'dashboard':
|
case 'dashboard':
|
||||||
@@ -501,9 +504,14 @@ export default function Features() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-tab-content">
|
<SwipeTabs
|
||||||
{renderTabContent()}
|
className="admin-tab-swipe"
|
||||||
</div>
|
tabs={tabIds}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={handleTabChange}
|
||||||
|
renderPanel={renderTabContent}
|
||||||
|
panelClassName="admin-tab-content swipe-panel-content"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useAuth } from '../../contexts/AuthContext';
|
|||||||
import { useSidebar } from '../../contexts/SidebarContext';
|
import { useSidebar } from '../../contexts/SidebarContext';
|
||||||
import type { SidebarMode } from '../../contexts/SidebarContext';
|
import type { SidebarMode } from '../../contexts/SidebarContext';
|
||||||
import { ChromePicker, HuePicker } from 'react-color';
|
import { ChromePicker, HuePicker } from 'react-color';
|
||||||
|
import { SwipeTabs } from '../../components/SwipeTabs';
|
||||||
import '../../styles/ThemeSettings.css';
|
import '../../styles/ThemeSettings.css';
|
||||||
|
|
||||||
type ThemeTab = 'colors' | 'appearance' | 'preview' | 'advanced';
|
type ThemeTab = 'colors' | 'appearance' | 'preview' | 'advanced';
|
||||||
@@ -43,6 +44,7 @@ export default function ThemeSettings() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { toggleMobileMenu, sidebarMode, setSidebarMode } = useSidebar();
|
const { toggleMobileMenu, sidebarMode, setSidebarMode } = useSidebar();
|
||||||
const isAdmin = user?.is_superuser || false;
|
const isAdmin = user?.is_superuser || false;
|
||||||
|
const tabs: ThemeTab[] = isAdmin ? ['colors', 'appearance', 'preview', 'advanced'] : ['colors', 'appearance', 'preview'];
|
||||||
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
||||||
text: '',
|
text: '',
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -262,70 +264,10 @@ export default function ThemeSettings() {
|
|||||||
setCustomColors({});
|
setCustomColors({});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderPanel = (tab: ThemeTab) => {
|
||||||
<main className="main-content theme-settings-root">
|
switch (tab) {
|
||||||
{/* Tab Tooltip */}
|
case 'colors':
|
||||||
{tooltip.visible && (
|
return (
|
||||||
<div
|
|
||||||
className="admin-tab-tooltip"
|
|
||||||
style={{ left: tooltip.left }}
|
|
||||||
>
|
|
||||||
{tooltip.text}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Modern Tab Navigation */}
|
|
||||||
<div className="page-tabs-container">
|
|
||||||
<div className="page-tabs-slider">
|
|
||||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
|
||||||
<span className="material-symbols-outlined">menu</span>
|
|
||||||
</button>
|
|
||||||
<div className="page-title-section">
|
|
||||||
<span className="page-title-text">{t.theme.title}</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-tabs-divider"></div>
|
|
||||||
<button
|
|
||||||
className={`admin-tab-btn ${activeTab === 'colors' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('colors')}
|
|
||||||
onMouseEnter={(e) => handleTabMouseEnter(t.theme.colorsTab, e)}
|
|
||||||
onMouseLeave={handleTabMouseLeave}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined">color_lens</span>
|
|
||||||
<span>{t.theme.colorsTab}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`admin-tab-btn ${activeTab === 'appearance' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('appearance')}
|
|
||||||
onMouseEnter={(e) => handleTabMouseEnter(t.theme.appearanceTab, e)}
|
|
||||||
onMouseLeave={handleTabMouseLeave}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined">tune</span>
|
|
||||||
<span>{t.theme.appearanceTab}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`admin-tab-btn ${activeTab === 'preview' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('preview')}
|
|
||||||
onMouseEnter={(e) => handleTabMouseEnter(t.theme.previewTab, e)}
|
|
||||||
onMouseLeave={handleTabMouseLeave}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined">visibility</span>
|
|
||||||
<span>{t.theme.previewTab}</span>
|
|
||||||
</button>
|
|
||||||
{isAdmin && (
|
|
||||||
<button
|
|
||||||
className={`admin-tab-btn ${activeTab === 'advanced' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('advanced')}
|
|
||||||
onMouseEnter={(e) => handleTabMouseEnter(t.theme.advancedTab, e)}
|
|
||||||
onMouseLeave={handleTabMouseLeave}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined">code</span>
|
|
||||||
<span>{t.theme.advancedTab}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-tab-content">
|
|
||||||
{activeTab === 'colors' && (
|
|
||||||
<div className="theme-tab-content">
|
<div className="theme-tab-content">
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
@@ -394,12 +336,11 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
case 'appearance':
|
||||||
{activeTab === 'appearance' && (
|
return (
|
||||||
<div className="theme-tab-content">
|
<div className="theme-tab-content">
|
||||||
<div className="appearance-grid">
|
<div className="appearance-grid">
|
||||||
{/* Border Radius Section */}
|
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">{t.theme.borderRadius}</h3>
|
<h3 className="section-title">{t.theme.borderRadius}</h3>
|
||||||
@@ -426,7 +367,6 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar Style Section */}
|
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">{t.theme.sidebarStyle}</h3>
|
<h3 className="section-title">{t.theme.sidebarStyle}</h3>
|
||||||
@@ -453,7 +393,6 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar Mode Section */}
|
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">{t.admin.sidebarMode}</h3>
|
<h3 className="section-title">{t.admin.sidebarMode}</h3>
|
||||||
@@ -480,7 +419,6 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Density Section */}
|
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">{t.theme.density}</h3>
|
<h3 className="section-title">{t.theme.density}</h3>
|
||||||
@@ -508,7 +446,6 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Font Family Section */}
|
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">{t.theme.fontFamily}</h3>
|
<h3 className="section-title">{t.theme.fontFamily}</h3>
|
||||||
@@ -535,9 +472,9 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
case 'preview':
|
||||||
{activeTab === 'preview' && (
|
return (
|
||||||
<div className="theme-tab-content">
|
<div className="theme-tab-content">
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
@@ -579,9 +516,10 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
case 'advanced':
|
||||||
{activeTab === 'advanced' && isAdmin && (
|
if (!isAdmin) return null;
|
||||||
|
return (
|
||||||
<div className="theme-tab-content">
|
<div className="theme-tab-content">
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
@@ -593,7 +531,6 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="advanced-colors-grid" style={{ marginTop: '2rem' }}>
|
<div className="advanced-colors-grid" style={{ marginTop: '2rem' }}>
|
||||||
{/* Light Theme Colors */}
|
|
||||||
<div className="color-theme-section">
|
<div className="color-theme-section">
|
||||||
<h3 className="color-theme-title">{t.theme.lightThemeColors}</h3>
|
<h3 className="color-theme-title">{t.theme.lightThemeColors}</h3>
|
||||||
<div className="color-controls-list">
|
<div className="color-controls-list">
|
||||||
@@ -648,7 +585,6 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dark Theme Colors */}
|
|
||||||
<div className="color-theme-section">
|
<div className="color-theme-section">
|
||||||
<h3 className="color-theme-title">{t.theme.darkThemeColors}</h3>
|
<h3 className="color-theme-title">{t.theme.darkThemeColors}</h3>
|
||||||
<div className="color-controls-list">
|
<div className="color-controls-list">
|
||||||
@@ -705,9 +641,83 @@ export default function ThemeSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="main-content theme-settings-root">
|
||||||
|
{/* Tab Tooltip */}
|
||||||
|
{tooltip.visible && (
|
||||||
|
<div
|
||||||
|
className="admin-tab-tooltip"
|
||||||
|
style={{ left: tooltip.left }}
|
||||||
|
>
|
||||||
|
{tooltip.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Modern Tab Navigation */}
|
||||||
|
<div className="page-tabs-container">
|
||||||
|
<div className="page-tabs-slider">
|
||||||
|
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||||
|
<span className="material-symbols-outlined">menu</span>
|
||||||
|
</button>
|
||||||
|
<div className="page-title-section">
|
||||||
|
<span className="page-title-text">{t.theme.title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-tabs-divider"></div>
|
||||||
|
<button
|
||||||
|
className={`admin-tab-btn ${activeTab === 'colors' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('colors')}
|
||||||
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.colorsTab, e)}
|
||||||
|
onMouseLeave={handleTabMouseLeave}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined">color_lens</span>
|
||||||
|
<span>{t.theme.colorsTab}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`admin-tab-btn ${activeTab === 'appearance' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('appearance')}
|
||||||
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.appearanceTab, e)}
|
||||||
|
onMouseLeave={handleTabMouseLeave}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined">tune</span>
|
||||||
|
<span>{t.theme.appearanceTab}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`admin-tab-btn ${activeTab === 'preview' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('preview')}
|
||||||
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.previewTab, e)}
|
||||||
|
onMouseLeave={handleTabMouseLeave}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined">visibility</span>
|
||||||
|
<span>{t.theme.previewTab}</span>
|
||||||
|
</button>
|
||||||
|
{isAdmin && (
|
||||||
|
<button
|
||||||
|
className={`admin-tab-btn ${activeTab === 'advanced' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('advanced')}
|
||||||
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.advancedTab, e)}
|
||||||
|
onMouseLeave={handleTabMouseLeave}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined">code</span>
|
||||||
|
<span>{t.theme.advancedTab}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SwipeTabs
|
||||||
|
className="admin-tab-swipe"
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
renderPanel={renderPanel}
|
||||||
|
panelClassName="admin-tab-content swipe-panel-content"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Color Picker Popup */}
|
{/* Color Picker Popup */}
|
||||||
{colorPickerState && (
|
{colorPickerState && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.api-keys-root .page-content {
|
.api-keys-root .page-content {
|
||||||
max-width: var(--container-lg);
|
max-width: var(--page-max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section Layout - matches theme-section spacing */
|
/* Section Layout - matches theme-section spacing */
|
||||||
|
|||||||
@@ -80,10 +80,6 @@
|
|||||||
border-color: rgba(var(--color-accent-rgb), 0.18);
|
border-color: rgba(var(--color-accent-rgb), 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.analytics-panel .section-title {
|
|
||||||
margin: 0 0 var(--space-3) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini-chart {
|
.mini-chart {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ body {
|
|||||||
|
|
||||||
/* Standard Section Title */
|
/* Standard Section Title */
|
||||||
.section-title {
|
.section-title {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 var(--section-title-gap) 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -185,7 +185,11 @@ body {
|
|||||||
|
|
||||||
/* Standard Section Header */
|
/* Standard Section Header */
|
||||||
.section-header {
|
.section-header {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: var(--section-title-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .section-title {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Standard Page Section */
|
/* Standard Page Section */
|
||||||
@@ -253,7 +257,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: var(--page-padding-y) var(--page-padding-x);
|
padding: var(--page-padding-y) var(--page-padding-x);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1600px;
|
max-width: var(--page-max-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +271,7 @@ body {
|
|||||||
/* Admin tab content (for tabbed interfaces) */
|
/* Admin tab content (for tabbed interfaces) */
|
||||||
.admin-tab-content {
|
.admin-tab-content {
|
||||||
padding: var(--page-padding-y) var(--page-padding-x);
|
padding: var(--page-padding-y) var(--page-padding-x);
|
||||||
max-width: 1600px;
|
max-width: var(--page-max-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -478,18 +482,96 @@ body {
|
|||||||
/* ========== TAB CONTENT ANIMATIONS ========== */
|
/* ========== TAB CONTENT ANIMATIONS ========== */
|
||||||
|
|
||||||
/* Prevent layout shift when switching tabs */
|
/* Prevent layout shift when switching tabs */
|
||||||
.admin-tab-content {
|
.admin-tab-content:not(.swipe-tabs):not(.swipe-panel-content) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
transition: grid-template-rows 0.25s ease;
|
transition: grid-template-rows 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-tab-content>div {
|
.admin-tab-content:not(.swipe-tabs):not(.swipe-panel-content)>div {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: tabFadeIn 0.25s ease forwards;
|
animation: tabFadeIn 0.25s ease forwards;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-tab-swipe {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Swipeable tab panels */
|
||||||
|
.swipe-tabs {
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-bg-main);
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs-track {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs-panel {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 1px;
|
||||||
|
background: var(--color-bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs--static {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs--static .swipe-tabs-panel {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs--snap {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs--snap::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs--snap .swipe-tabs-panel {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: paint;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
scroll-snap-stop: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-tabs-panel-inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tab-content.swipe-tabs {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tab-content.swipe-tabs>div {
|
||||||
|
opacity: 1;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-panel-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-panel-content>div {
|
||||||
|
opacity: 1;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes tabFadeIn {
|
@keyframes tabFadeIn {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -13,10 +13,6 @@
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-page-root .page-content {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Settings Sections - no card, just spacing */
|
/* Settings Sections - no card, just spacing */
|
||||||
.settings-section {
|
.settings-section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
|
|||||||
@@ -68,6 +68,11 @@
|
|||||||
transition: grid-template-rows 0.25s ease;
|
transition: grid-template-rows 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra side spacing just for the theme editor content */
|
||||||
|
.theme-settings-root .admin-tab-content .theme-tab-content {
|
||||||
|
padding: 0 calc(var(--page-padding-x) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* Smooth transition between tabs */
|
/* Smooth transition between tabs */
|
||||||
.theme-tab-content>div {
|
.theme-tab-content>div {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -1155,6 +1160,10 @@
|
|||||||
padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet);
|
padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-settings-root .admin-tab-content .theme-tab-content {
|
||||||
|
padding: 0 calc(var(--page-padding-x-tablet) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.color-grid-enhanced {
|
.color-grid-enhanced {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
@@ -1182,6 +1191,10 @@
|
|||||||
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-settings-root .admin-tab-content .theme-tab-content {
|
||||||
|
padding: 0 calc(var(--page-padding-x-mobile) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.theme-section {
|
.theme-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,15 +51,15 @@
|
|||||||
--sidebar-mobile-width: 280px;
|
--sidebar-mobile-width: 280px;
|
||||||
|
|
||||||
/* Page Layout Spacing */
|
/* Page Layout Spacing */
|
||||||
--page-padding-x: 2rem;
|
--page-padding-x: 2.5rem;
|
||||||
/* Horizontal padding for page content */
|
/* Horizontal padding for page content */
|
||||||
--page-padding-y: 2rem;
|
--page-padding-y: 2.5rem;
|
||||||
/* Vertical padding for page content */
|
/* Vertical padding for page content */
|
||||||
--page-padding-x-tablet: 1.5rem;
|
--page-padding-x-tablet: 2rem;
|
||||||
--page-padding-y-tablet: 1.5rem;
|
--page-padding-y-tablet: 2rem;
|
||||||
--page-padding-x-mobile: 1rem;
|
--page-padding-x-mobile: 1.25rem;
|
||||||
--page-padding-y-mobile: 1rem;
|
--page-padding-y-mobile: 1.25rem;
|
||||||
--page-max-width: 1400px;
|
--page-max-width: 1200px;
|
||||||
/* Maximum content width */
|
/* Maximum content width */
|
||||||
|
|
||||||
/* Container Widths */
|
/* Container Widths */
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
/* increased from 1.25rem */
|
/* increased from 1.25rem */
|
||||||
--section-gap-sm: 1.125rem;
|
--section-gap-sm: 1.125rem;
|
||||||
/* increased from 0.875rem */
|
/* increased from 0.875rem */
|
||||||
|
--section-title-gap: var(--section-gap);
|
||||||
|
/* Title-to-content spacing for section headers */
|
||||||
|
|
||||||
/* Semantic Spacing - Elements */
|
/* Semantic Spacing - Elements */
|
||||||
--element-gap: 0.375rem;
|
--element-gap: 0.375rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user