Fix page content padding consistency

- Restore bottom padding for .page-content and .admin-tab-content
  across all breakpoints (was accidentally set to 0)
- Change bottom tab bar padding from max() to calc(padding + bar-height)
  so content has both regular margin AND space for fixed tab bar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-23 22:54:22 +01:00
parent 98fc8330f4
commit ec726da797
3 changed files with 97 additions and 70 deletions

View File

@@ -72,6 +72,7 @@ export function SwipeTabs<T extends string | number>({
}: SwipeTabsProps<T>) { }: SwipeTabsProps<T>) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null); const trackRef = useRef<HTMLDivElement>(null);
const activePanelRef = useRef<HTMLDivElement | null>(null);
const activeIndex = useMemo(() => { const activeIndex = useMemo(() => {
const index = tabs.indexOf(activeTab); const index = tabs.indexOf(activeTab);
return index >= 0 ? index : 0; return index >= 0 ? index : 0;
@@ -79,6 +80,7 @@ export function SwipeTabs<T extends string | number>({
const [displayIndex, setDisplayIndex] = useState(activeIndex); const [displayIndex, setDisplayIndex] = useState(activeIndex);
const displayIndexRef = useRef(displayIndex); const displayIndexRef = useRef(displayIndex);
const [containerHeight, setContainerHeight] = useState<number | null>(null);
const widthRef = useRef(0); const widthRef = useRef(0);
const baseOffsetRef = useRef(0); const baseOffsetRef = useRef(0);
const dragOffsetRef = useRef(0); const dragOffsetRef = useRef(0);
@@ -135,6 +137,13 @@ export function SwipeTabs<T extends string | number>({
} }
}, []); }, []);
const updateHeight = useCallback(() => {
const node = activePanelRef.current;
if (!node) return;
const nextHeight = Math.ceil(node.getBoundingClientRect().height);
setContainerHeight((prev) => (prev === nextHeight ? prev : nextHeight));
}, []);
const measureWidth = useCallback(() => { const measureWidth = useCallback(() => {
const width = containerRef.current?.getBoundingClientRect().width || 0; const width = containerRef.current?.getBoundingClientRect().width || 0;
if (width && width !== widthRef.current) { if (width && width !== widthRef.current) {
@@ -171,6 +180,18 @@ export function SwipeTabs<T extends string | number>({
applyTransform(0, false); applyTransform(0, false);
}, [applyTransform, displayIndex]); }, [applyTransform, displayIndex]);
useLayoutEffect(() => {
const node = activePanelRef.current;
if (!node) return undefined;
updateHeight();
if (typeof ResizeObserver === 'undefined') return undefined;
const observer = new ResizeObserver(() => updateHeight());
observer.observe(node);
return () => {
observer.disconnect();
};
}, [displayIndex, updateHeight]);
useEffect(() => { useEffect(() => {
return () => { return () => {
if (rafRef.current) { if (rafRef.current) {
@@ -540,6 +561,7 @@ export function SwipeTabs<T extends string | number>({
<div <div
ref={containerRef} ref={containerRef}
className={rootClassName} className={rootClassName}
style={containerHeight ? { height: `${containerHeight}px` } : undefined}
onPointerDown={usePointerEvents ? handlePointerDown : undefined} onPointerDown={usePointerEvents ? handlePointerDown : undefined}
onPointerMove={usePointerEvents ? handlePointerMove : undefined} onPointerMove={usePointerEvents ? handlePointerMove : undefined}
onPointerUp={usePointerEvents ? handlePointerUp : undefined} onPointerUp={usePointerEvents ? handlePointerUp : undefined}
@@ -555,7 +577,10 @@ export function SwipeTabs<T extends string | number>({
const key = tab !== null ? String(tab) : `empty-${slotIndex}`; const key = tab !== null ? String(tab) : `empty-${slotIndex}`;
return ( return (
<section key={key} className="swipe-tabs-panel"> <section key={key} className="swipe-tabs-panel">
<div className={panelInnerClasses}> <div
className={panelInnerClasses}
ref={slotIndex === 1 ? activePanelRef : undefined}
>
{tab !== null ? renderPanel(tab) : null} {tab !== null ? renderPanel(tab) : null}
</div> </div>
</section> </section>

View File

@@ -40,6 +40,7 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
const rafRef = useRef(0); const rafRef = useRef(0);
const [showLeft, setShowLeft] = useState(false); const [showLeft, setShowLeft] = useState(false);
const [showRight, setShowRight] = useState(false); const [showRight, setShowRight] = useState(false);
const heightRef = useRef(0);
const dragRef = useRef({ const dragRef = useRef({
pointerId: null as number | null, pointerId: null as number | null,
@@ -70,10 +71,21 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
const hasTabsValue = hasTabButtons ? 'true' : 'false'; const hasTabsValue = hasTabButtons ? 'true' : 'false';
node.dataset.hasTabs = hasTabsValue; node.dataset.hasTabs = hasTabsValue;
const container = node.closest('.page-tabs-container, .admin-tabs-container'); const container = node.closest('.page-tabs-container, .admin-tabs-container');
const mainContent = node.closest('.main-content');
let containerHeight = 0;
if (container instanceof HTMLElement) { if (container instanceof HTMLElement) {
container.dataset.hasTabs = hasTabsValue; container.dataset.hasTabs = hasTabsValue;
containerHeight = Math.ceil(container.getBoundingClientRect().height);
}
if (containerHeight && containerHeight !== heightRef.current) {
heightRef.current = containerHeight;
if (typeof document !== 'undefined') {
document.documentElement.style.setProperty('--tabs-bar-height', `${containerHeight}px`);
}
if (mainContent instanceof HTMLElement) {
mainContent.style.setProperty('--tabs-bar-height', `${containerHeight}px`);
}
} }
const mainContent = node.closest('.main-content');
if (mainContent instanceof HTMLElement) { if (mainContent instanceof HTMLElement) {
mainContent.dataset.hasTabs = hasTabsValue; mainContent.dataset.hasTabs = hasTabsValue;
} }
@@ -104,6 +116,7 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
updateOverflow(); updateOverflow();
const node = sliderRef.current; const node = sliderRef.current;
if (!node) return undefined; if (!node) return undefined;
const container = node.closest('.page-tabs-container, .admin-tabs-container');
const handleScroll = () => scheduleOverflowUpdate(); const handleScroll = () => scheduleOverflowUpdate();
node.addEventListener('scroll', handleScroll, { passive: true }); node.addEventListener('scroll', handleScroll, { passive: true });
@@ -111,6 +124,9 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
if (typeof ResizeObserver !== 'undefined') { if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(() => scheduleOverflowUpdate()); resizeObserver = new ResizeObserver(() => scheduleOverflowUpdate());
resizeObserver.observe(node); resizeObserver.observe(node);
if (container instanceof HTMLElement) {
resizeObserver.observe(container);
}
} else if (typeof window !== 'undefined') { } else if (typeof window !== 'undefined') {
window.addEventListener('resize', scheduleOverflowUpdate); window.addEventListener('resize', scheduleOverflowUpdate);
} }

View File

@@ -29,31 +29,12 @@
/* ========== PAGE STRUCTURE ========== */ /* ========== PAGE STRUCTURE ========== */
/* Page header container - fixed at top */ /* Page header container */
.page-tabs-container, .page-tabs-container,
.admin-tabs-container { .admin-tabs-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 0.75rem; padding: 0.75rem;
position: fixed;
top: 0;
left: var(--sidebar-width);
right: 0;
z-index: 100;
}
/* Handle collapsed sidebar for fixed header (desktop only) */
@media (min-width: 769px) {
[data-sidebar-collapsed='true'] .page-tabs-container,
[data-sidebar-collapsed='true'] .admin-tabs-container {
left: var(--sidebar-width-collapsed);
}
}
/* Add top padding to content to account for fixed header */
.page-content,
.admin-tab-swipe {
padding-top: calc(var(--page-padding-y) + 60px);
} }
/* Ensure no extra margin from body */ /* Ensure no extra margin from body */
@@ -394,7 +375,7 @@ label,
/* Main content wrapper - with symmetric padding from sidebar edge to window edge */ /* Main content wrapper - with symmetric padding from sidebar edge to window edge */
.page-content { .page-content {
padding: var(--page-padding-y) var(--page-padding-x) 0; padding: var(--page-padding-y) var(--page-padding-x);
width: 100%; width: 100%;
max-width: var(--page-max-width); max-width: var(--page-max-width);
margin: 0 auto; margin: 0 auto;
@@ -409,7 +390,7 @@ label,
/* 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) 0; padding: var(--page-padding-y) var(--page-padding-x);
max-width: var(--page-max-width); max-width: var(--page-max-width);
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
@@ -445,13 +426,8 @@ label,
padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet); padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet);
} }
.page-content,
.admin-tab-swipe {
padding-top: calc(var(--page-padding-y-tablet) + 60px);
}
.admin-tab-content { .admin-tab-content {
padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet) 0; padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet);
} }
} }
@@ -495,12 +471,6 @@ label,
.page-tabs-container, .page-tabs-container,
.admin-tabs-container { .admin-tabs-container {
padding: 0.75rem; padding: 0.75rem;
left: 0;
}
.page-content,
.admin-tab-swipe {
padding-top: calc(var(--page-padding-y-mobile) + 60px);
} }
.page-tabs-slider, .page-tabs-slider,
@@ -620,11 +590,11 @@ label,
} }
.page-content { .page-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
} }
.admin-tab-content { .admin-tab-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
} }
} }
@@ -637,11 +607,11 @@ label,
} }
.page-content { .page-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
} }
.admin-tab-content { .admin-tab-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
} }
} }
@@ -859,8 +829,8 @@ label,
/* Bottom position - fixed at bottom (global setting for all pages) */ /* Bottom position - fixed at bottom (global setting for all pages) */
/* Note: Primary positioning is handled by TabsScroller component with inline styles. /* Note: Primary positioning is handled by TabsScroller component with inline styles.
These CSS rules serve as fallback/additional styling. */ These CSS rules serve as fallback/additional styling. */
[data-tab-position='bottom'] .page-tabs-container, [data-tab-position='bottom'] .page-tabs-container[data-has-tabs='true'],
[data-tab-position='bottom'] .admin-tabs-container { [data-tab-position='bottom'] .admin-tabs-container[data-has-tabs='true'] {
position: fixed; position: fixed;
top: auto; top: auto;
bottom: 0; bottom: 0;
@@ -872,54 +842,62 @@ label,
} }
/* Handle collapsed sidebar */ /* Handle collapsed sidebar */
[data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container, [data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container[data-has-tabs='true'],
[data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container { [data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container[data-has-tabs='true'] {
left: var(--sidebar-width-collapsed); left: var(--sidebar-width-collapsed);
} }
/* Adjust slider styling for bottom position */ /* Adjust slider styling for bottom position */
[data-tab-position='bottom'] .page-tabs-slider, [data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'],
[data-tab-position='bottom'] .admin-tabs-slider { [data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
} }
/* Add bottom padding and remove top padding when bar is at bottom */ /* Add bottom padding and remove top padding when bar is at bottom */
[data-tab-position='bottom'] .main-content { [data-tab-position='bottom'] .main-content[data-has-tabs='true'] {
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); padding-bottom: 0;
} }
[data-tab-position='bottom'] .page-content, [data-tab-position='bottom'] .main-content[data-has-tabs='true'] .page-content,
[data-tab-position='bottom'] .admin-tab-swipe { [data-tab-position='bottom'] .main-content[data-has-tabs='true'] .admin-tab-swipe {
padding-top: var(--page-padding-y); padding-top: var(--page-padding-y);
} }
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .page-content,
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .admin-tab-content {
padding-bottom: calc(
var(--page-padding-y) +
var(--tabs-bar-height, calc(72px + env(safe-area-inset-bottom, 0px)))
);
}
/* Mobile: full width at bottom */ /* Mobile: full width at bottom */
@media (max-width: 768px) { @media (max-width: 768px) {
[data-tab-position='bottom'] .page-tabs-container, [data-tab-position='bottom'] .page-tabs-container[data-has-tabs='true'],
[data-tab-position='bottom'] .admin-tabs-container { [data-tab-position='bottom'] .admin-tabs-container[data-has-tabs='true'] {
left: 0; left: 0;
} }
/* Override collapsed state on mobile */ /* Override collapsed state on mobile */
[data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container, [data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container[data-has-tabs='true'],
[data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container { [data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container[data-has-tabs='true'] {
left: 0; left: 0;
} }
/* Hide divider on mobile when bar is at bottom */ /* Hide divider on mobile when bar is at bottom */
[data-tab-position='bottom'] .page-tabs-slider .page-tabs-divider, [data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .page-tabs-divider,
[data-tab-position='bottom'] .page-tabs-slider .admin-tabs-divider, [data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .admin-tabs-divider,
[data-tab-position='bottom'] .admin-tabs-slider .page-tabs-divider, [data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .page-tabs-divider,
[data-tab-position='bottom'] .admin-tabs-slider .admin-tabs-divider { [data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .admin-tabs-divider {
display: none; display: none;
} }
} }
/* Responsive position - top on desktop, bottom on mobile */ /* Responsive position - top on desktop, bottom on mobile */
@media (max-width: 768px) { @media (max-width: 768px) {
[data-tab-position='responsive'] .page-tabs-container, [data-tab-position='responsive'] .page-tabs-container[data-has-tabs='true'],
[data-tab-position='responsive'] .admin-tabs-container { [data-tab-position='responsive'] .admin-tabs-container[data-has-tabs='true'] {
position: fixed; position: fixed;
top: auto; top: auto;
bottom: 0; bottom: 0;
@@ -930,8 +908,8 @@ label,
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
} }
[data-tab-position='responsive'] .page-tabs-slider, [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'],
[data-tab-position='responsive'] .admin-tabs-slider { [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
/* padding-left: 72px inherited from mobile rules for menu button spacing */ /* padding-left: 72px inherited from mobile rules for menu button spacing */
@@ -946,20 +924,28 @@ label,
} }
/* Hide divider when bar is at bottom */ /* Hide divider when bar is at bottom */
[data-tab-position='responsive'] .page-tabs-slider .page-tabs-divider, [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .page-tabs-divider,
[data-tab-position='responsive'] .page-tabs-slider .admin-tabs-divider, [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .admin-tabs-divider,
[data-tab-position='responsive'] .admin-tabs-slider .page-tabs-divider, [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .page-tabs-divider,
[data-tab-position='responsive'] .admin-tabs-slider .admin-tabs-divider { [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .admin-tabs-divider {
display: none; display: none;
} }
/* Add bottom padding and remove top padding */ /* Add bottom padding and remove top padding */
[data-tab-position='responsive'] .main-content { [data-tab-position='responsive'] .main-content[data-has-tabs='true'] {
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); padding-bottom: 0;
} }
[data-tab-position='responsive'] .page-content, [data-tab-position='responsive'] .main-content[data-has-tabs='true'] .page-content,
[data-tab-position='responsive'] .admin-tab-swipe { [data-tab-position='responsive'] .main-content[data-has-tabs='true'] .admin-tab-swipe {
padding-top: var(--page-padding-y-mobile); padding-top: var(--page-padding-y-mobile);
} }
[data-tab-position='responsive'] .main-content[data-has-tabs='true'] .page-content,
[data-tab-position='responsive'] .main-content[data-has-tabs='true'] .admin-tab-content {
padding-bottom: calc(
var(--page-padding-y-mobile) +
var(--tabs-bar-height, calc(72px + env(safe-area-inset-bottom, 0px)))
);
}
} }