From 7efae3764586b00804da0455f22d5846fab00e0b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:27:43 +0100 Subject: [PATCH] fix: mark the active Pro tab and draw the stored tab count in the skeleton --- app/boot-skeleton.css | 28 ++++---- .../__tests__/pro-skeleton.test.tsx | 71 +++++++++++++++++-- components/boot-skeleton/pro-skeleton.tsx | 7 +- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/app/boot-skeleton.css b/app/boot-skeleton.css index ee9d33ba9..cdcf53fa4 100644 --- a/app/boot-skeleton.css +++ b/app/boot-skeleton.css @@ -154,20 +154,20 @@ /* The focused pane's active tab carries the accent top edge. */ --sk-pro-pane-accent: var(--color-primary); } -:root:not([data-boot-pro-main-kind]) [data-sk-pro-pane="main"] > [data-sk-pro-body="mail"], -:root:not([data-boot-pro-split-kind]) [data-sk-pro-pane="split"] > [data-sk-pro-body="mail"], -:root[data-boot-pro-main-kind="mail"] [data-sk-pro-pane="main"] > [data-sk-pro-body="mail"], -:root[data-boot-pro-main-kind="calendar"] [data-sk-pro-pane="main"] > [data-sk-pro-body="calendar"], -:root[data-boot-pro-main-kind="contacts"] [data-sk-pro-pane="main"] > [data-sk-pro-body="contacts"], -:root[data-boot-pro-main-kind="files"] [data-sk-pro-pane="main"] > [data-sk-pro-body="files"], -:root[data-boot-pro-main-kind="settings"] [data-sk-pro-pane="main"] > [data-sk-pro-body="settings"], -:root[data-boot-pro-main-kind="other"] [data-sk-pro-pane="main"] > [data-sk-pro-body="other"], -:root[data-boot-pro-split-kind="mail"] [data-sk-pro-pane="split"] > [data-sk-pro-body="mail"], -:root[data-boot-pro-split-kind="calendar"] [data-sk-pro-pane="split"] > [data-sk-pro-body="calendar"], -:root[data-boot-pro-split-kind="contacts"] [data-sk-pro-pane="split"] > [data-sk-pro-body="contacts"], -:root[data-boot-pro-split-kind="files"] [data-sk-pro-pane="split"] > [data-sk-pro-body="files"], -:root[data-boot-pro-split-kind="settings"] [data-sk-pro-pane="split"] > [data-sk-pro-body="settings"], -:root[data-boot-pro-split-kind="other"] [data-sk-pro-pane="split"] > [data-sk-pro-body="other"] { +:root:not([data-boot-pro-main-kind]) [data-sk-pro-pane="main"] [data-sk-pro-body="mail"], +:root:not([data-boot-pro-split-kind]) [data-sk-pro-pane="split"] [data-sk-pro-body="mail"], +:root[data-boot-pro-main-kind="mail"] [data-sk-pro-pane="main"] [data-sk-pro-body="mail"], +:root[data-boot-pro-main-kind="calendar"] [data-sk-pro-pane="main"] [data-sk-pro-body="calendar"], +:root[data-boot-pro-main-kind="contacts"] [data-sk-pro-pane="main"] [data-sk-pro-body="contacts"], +:root[data-boot-pro-main-kind="files"] [data-sk-pro-pane="main"] [data-sk-pro-body="files"], +:root[data-boot-pro-main-kind="settings"] [data-sk-pro-pane="main"] [data-sk-pro-body="settings"], +:root[data-boot-pro-main-kind="other"] [data-sk-pro-pane="main"] [data-sk-pro-body="other"], +:root[data-boot-pro-split-kind="mail"] [data-sk-pro-pane="split"] [data-sk-pro-body="mail"], +:root[data-boot-pro-split-kind="calendar"] [data-sk-pro-pane="split"] [data-sk-pro-body="calendar"], +:root[data-boot-pro-split-kind="contacts"] [data-sk-pro-pane="split"] [data-sk-pro-body="contacts"], +:root[data-boot-pro-split-kind="files"] [data-sk-pro-pane="split"] [data-sk-pro-body="files"], +:root[data-boot-pro-split-kind="settings"] [data-sk-pro-pane="split"] [data-sk-pro-body="settings"], +:root[data-boot-pro-split-kind="other"] [data-sk-pro-pane="split"] [data-sk-pro-body="other"] { display: block; } diff --git a/components/boot-skeleton/__tests__/pro-skeleton.test.tsx b/components/boot-skeleton/__tests__/pro-skeleton.test.tsx index 05fca09af..5ee8480b4 100644 --- a/components/boot-skeleton/__tests__/pro-skeleton.test.tsx +++ b/components/boot-skeleton/__tests__/pro-skeleton.test.tsx @@ -1,4 +1,6 @@ -import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { renderToString } from 'react-dom/server'; import { AppShellSkeleton } from '../shell'; import { ProSkeleton } from '../pro-skeleton'; @@ -48,15 +50,17 @@ describe('AppShellSkeleton under the Pro interface', () => { const panes = pro?.querySelectorAll('[data-sk-pro-pane]') ?? []; expect([...panes].map((p) => p.getAttribute('data-sk-pro-pane'))).toEqual(['main', 'split']); for (const pane of panes) { - // Tab strip right above the pane body, eight tab slots for the CSS to pick from. - expect(pane.previousElementSibling?.querySelectorAll('[data-sk-pro-tab]')).toHaveLength(8); - expect(classes(pane.previousElementSibling)).toEqual(expect.arrayContaining(['h-9', 'bg-secondary'])); - expect([...pane.children].map((b) => b.getAttribute('data-sk-pro-body'))).toEqual([ + // The tab strip, with eight tab slots for the CSS to pick from, right + // above the pane body. + const [strip, body] = [...pane.children]; + expect(strip.querySelectorAll('[data-sk-pro-tab]')).toHaveLength(8); + expect(classes(strip)).toEqual(expect.arrayContaining(['h-9', 'bg-secondary'])); + expect([...body.children].map((b) => b.getAttribute('data-sk-pro-body'))).toEqual([ 'mail', 'calendar', 'contacts', 'files', 'settings', 'other', ]); } // The split pane only shows with a split. - expect(classes(panes[1]?.parentElement)).toEqual(expect.arrayContaining(['hidden', 'boot-pro-split:flex'])); + expect(classes(panes[1])).toEqual(expect.arrayContaining(['hidden', 'boot-pro-split:flex'])); }); it('lets the login card win for a signed-out visitor', () => { @@ -75,3 +79,58 @@ describe('ProSkeleton', () => { expect(container.querySelector('[data-boot-skeleton="login"]')).not.toBeNull(); }); }); + +describe('the Pro CSS picks the stored tabs and pages', () => { + // The "pro" section of app/boot-skeleton.css, minus the Tailwind variants, + // applied to the real markup with the attributes the boot script sets. + const css = readFileSync(path.resolve(__dirname, '../../../app/boot-skeleton.css'), 'utf8') + .split('/* pro */')[1] + .replace(/@custom-variant[^;]*;/g, ''); + + let container: HTMLElement; + beforeEach(() => { + const style = document.createElement('style'); + style.id = 'pro-css'; + style.textContent = css; + document.head.appendChild(style); + container = document.createElement('div'); + container.innerHTML = renderToString(); + document.body.appendChild(container); + }); + afterEach(() => { + document.getElementById('pro-css')?.remove(); + container.remove(); + for (const a of [...document.documentElement.attributes]) { + if (a.name.startsWith('data-boot-')) document.documentElement.removeAttribute(a.name); + } + }); + + const boot = (attrs: Record) => { + for (const [k, v] of Object.entries(attrs)) document.documentElement.setAttribute(`data-boot-${k}`, v); + }; + const pane = (which: string) => container.querySelector(`[data-sk-pro-pane="${which}"]`)!; + const shown = (els: Iterable) => [...els].filter((el) => getComputedStyle(el).display !== 'none'); + const tabs = (which: string) => shown(pane(which).querySelectorAll('[data-sk-pro-tab]')); + const active = (which: string) => + [...pane(which).querySelectorAll('[data-sk-pro-tab]')].findIndex( + (el) => !['', 'transparent', 'rgba(0, 0, 0, 0)'].includes(getComputedStyle(el).backgroundColor), + ); + const body = (which: string) => + shown(pane(which).querySelectorAll('[data-sk-pro-body]')).map((el) => el.getAttribute('data-sk-pro-body')); + + it('draws one active mail tab without attributes', () => { + expect(tabs('main')).toHaveLength(1); + expect(active('main')).toBe(0); + expect(body('main')).toEqual(['mail']); + }); + + it('draws the stored count, the active tab and its page in each pane', () => { + boot({ pro: 'on', 'pro-main-tabs': '3', 'pro-main-active': '2', 'pro-main-kind': 'calendar', 'pro-split-tabs': '2', 'pro-split-active': '0', 'pro-split-kind': 'other', 'pro-split': 'right' }); + expect(tabs('main')).toHaveLength(3); + expect(active('main')).toBe(2); + expect(body('main')).toEqual(['calendar']); + expect(tabs('split')).toHaveLength(2); + expect(active('split')).toBe(0); + expect(body('split')).toEqual(['other']); + }); +}); diff --git a/components/boot-skeleton/pro-skeleton.tsx b/components/boot-skeleton/pro-skeleton.tsx index d8a8b81a9..8781da227 100644 --- a/components/boot-skeleton/pro-skeleton.tsx +++ b/components/boot-skeleton/pro-skeleton.tsx @@ -67,12 +67,9 @@ function TabStripSkeleton() { /** One pane: its tab strip over the active tab's page. */ function PaneSkeleton({ pane, className }: { pane: "main" | "split"; className?: string }) { return ( -
+
-
+
{BODIES.map((surface) => (