From aa318904b5fade01585536d2793c67076a029cc4 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 30 May 2026 17:16:07 +0800 Subject: [PATCH] fix(reader): show full bookmark ribbon in scrolled mode header (#4365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In scrolled mode, SectionInfo paints a solid `bg-base-100` `notch-area` mask over the top safe-area strip at z-10. The Ribbon was also z-10 but rendered earlier in the DOM, so the equal-z mask painted over the ribbon's upper (unsafe-area) half — only the lower 44px showed. In paginated mode the mask has no background, so the ribbon showed fully. Raise the ribbon to z-20 so the whole ribbon stays visible above the mask, and mark it pointer-events-none so taps still fall through to the notch mask's scroll-to-top handler. Co-authored-by: Claude Opus 4.8 (1M context) --- .../app/reader/components/Ribbon.test.tsx | 30 +++++++++++++++++++ .../src/app/reader/components/Ribbon.tsx | 6 +++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx diff --git a/apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx b/apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx new file mode 100644 index 00000000..009d8627 --- /dev/null +++ b/apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx @@ -0,0 +1,30 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render } from '@testing-library/react'; +import Ribbon from '@/app/reader/components/Ribbon'; + +vi.mock('@/store/themeStore', () => ({ + useThemeStore: () => ({ safeAreaInsets: { top: 48, right: 0, bottom: 0, left: 0 } }), +})); + +describe('Ribbon', () => { + it('stacks above the scrolled-mode notch mask so the full ribbon stays visible', () => { + // In scrolled mode SectionInfo paints a `bg-base-100` `notch-area` mask over the + // top safe-area strip at z-10. The ribbon renders earlier in the DOM, so it must + // sit on a higher layer than z-10 or its upper (unsafe-area) half gets covered. + const { container } = render(); + const ribbon = container.querySelector('.ribbon') as HTMLElement; + + expect(ribbon).not.toBeNull(); + expect(ribbon.classList.contains('z-10')).toBe(false); + expect(ribbon.classList.contains('z-20')).toBe(true); + // Decorative only: taps must fall through to the notch mask's scroll-to-top. + expect(ribbon.classList.contains('pointer-events-none')).toBe(true); + }); + + it('spans the safe-area inset plus the header bar height', () => { + const { container } = render(); + const ribbon = container.querySelector('.ribbon') as HTMLElement; + + expect(ribbon.style.height).toBe('92px'); // 48px safe-area top + 44px header bar + }); +}); diff --git a/apps/readest-app/src/app/reader/components/Ribbon.tsx b/apps/readest-app/src/app/reader/components/Ribbon.tsx index 440c5223..cf8055b0 100644 --- a/apps/readest-app/src/app/reader/components/Ribbon.tsx +++ b/apps/readest-app/src/app/reader/components/Ribbon.tsx @@ -9,9 +9,13 @@ interface RibbonProps { const Ribbon: React.FC = ({}) => { const { safeAreaInsets } = useThemeStore(); + // z-20 keeps the ribbon above the scrolled-mode `notch-area` mask (z-10 in + // SectionInfo) so its upper safe-area half isn't covered. return (