diff --git a/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx b/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx new file mode 100644 index 00000000..2344680e --- /dev/null +++ b/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx @@ -0,0 +1,59 @@ +import '../../styles/app.css'; +import '@luke-ui/react/themes/paper.css'; +import '@luke-ui/react/themes/tactile.css'; +import { paperThemeClassName, tactileThemeClassName } from '@luke-ui/react/themes'; +import { act } from 'react'; +import type { Root } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; +import { afterEach, expect, test } from 'vite-plus/test'; +import { page } from 'vite-plus/test/context'; +import { EditorSkeleton } from './editor-skeleton'; + +let container: HTMLElement | undefined; +let root: Root | undefined; + +afterEach(() => { + if (root) act(() => root?.unmount()); + container?.remove(); + container = undefined; + root = undefined; +}); + +for (const [themeName, themeClassName] of [ + ['Tactile', tactileThemeClassName], + ['Paper', paperThemeClassName], +] as const) { + for (const mode of ['light', 'dark'] as const) { + test(`uses an opaque ${themeName} ${mode} popover surface behind its loading pill`, () => { + renderSkeleton(themeClassName, mode); + + const label = page.getByText('Loading editor', { exact: true }).nth(1).element(); + const pillBackground = getComputedStyle(label.parentElement as HTMLElement).backgroundColor; + const expectedBackground = getSemanticSurfaceBackground(); + + expect(pillBackground).toBe(expectedBackground); + expect(pillBackground).not.toBe('rgba(0, 0, 0, 0)'); + }); + } +} + +function renderSkeleton(themeClassName: string, mode: 'light' | 'dark') { + container = document.body.appendChild(document.createElement('div')); + container.className = `luke-ui-theme ${themeClassName}`; + container.dataset.colorMode = mode; + container.classList.toggle('dark', mode === 'dark'); + root = createRoot(container); + act(() => { + root?.render(); + }); +} + +function getSemanticSurfaceBackground() { + if (!container) throw new Error('Expected a theme root.'); + + const surface = container.appendChild(document.createElement('div')); + surface.style.backgroundColor = 'var(--color-fd-popover)'; + const background = getComputedStyle(surface).backgroundColor; + surface.remove(); + return background; +} diff --git a/apps/docs/src/components/playground/editor-skeleton.tsx b/apps/docs/src/components/playground/editor-skeleton.tsx index 31e11d33..912e5c52 100644 --- a/apps/docs/src/components/playground/editor-skeleton.tsx +++ b/apps/docs/src/components/playground/editor-skeleton.tsx @@ -46,7 +46,10 @@ export function EditorSkeleton({ code, showPill }: { code: string; showPill: boo ))} {showPill ? ( -
+
) : null} @@ -71,7 +74,7 @@ export function EditorSkeletonShapeScript() { /** Shared loading indicator so the editor and preview speak the same language. */ export function LoadingPill({ label }: { label: string }) { return ( -
+
{label}
diff --git a/apps/docs/src/components/playground/playground-layout.browser.test.ts b/apps/docs/src/components/playground/playground-layout.browser.test.ts index 257f8fa4..09561226 100644 --- a/apps/docs/src/components/playground/playground-layout.browser.test.ts +++ b/apps/docs/src/components/playground/playground-layout.browser.test.ts @@ -18,6 +18,8 @@ test('uses the desktop panel layout before JavaScript corrects the server orient expect(getComputedStyle(separator).cursor).toBe('col-resize'); expect(getComputedStyle(separator).inlineSize).toBe('1px'); expect(getComputedStyle(separator).blockSize).toBe('400px'); + expect(getComputedStyle(separator, '::after').width).toBe('6px'); + expect(getComputedStyle(separator, '::after').height).toBe('64px'); }); test('uses the mobile panel layout independently of the JavaScript orientation', async () => { @@ -28,6 +30,8 @@ test('uses the mobile panel layout independently of the JavaScript orientation', expect(getComputedStyle(separator).cursor).toBe('row-resize'); expect(getComputedStyle(separator).inlineSize).toBe('390px'); expect(getComputedStyle(separator).blockSize).toBe('1px'); + expect(getComputedStyle(separator, '::after').width).toBe('64px'); + expect(getComputedStyle(separator, '::after').height).toBe('6px'); }); function renderPanelLayout(inlineDirection: 'column' | 'row') { @@ -37,7 +41,7 @@ function renderPanelLayout(inlineDirection: 'column' | 'row') { const separator = document.createElement('div'); separator.className = - "relative z-10 shrink-0 [block-size:1px] [inline-size:auto] cursor-row-resize bg-fd-border before:absolute before:[inset-block:-0.5rem] before:[inset-inline:0] before:content-[''] after:absolute after:[block-size:0.125rem] after:[inline-size:2rem] after:rounded-full after:bg-fd-border after:transition-colors after:-translate-x-1/2 after:-translate-y-1/2 after:inset-bs-[50%] after:inset-s-[50%] after:content-[''] data-[separator=active]:after:bg-fd-muted-foreground/60 data-[separator=focus]:after:bg-fd-muted-foreground/60 data-[separator=hover]:after:bg-fd-muted-foreground/50 md:[block-size:auto] md:[inline-size:1px] md:cursor-col-resize md:before:[inset-block:0] md:before:[inset-inline:-0.5rem] md:after:[block-size:2rem] md:after:[inline-size:0.125rem]"; + "relative z-10 shrink-0 [block-size:1px] [inline-size:auto] cursor-row-resize bg-fd-border before:absolute before:[inset-block:-0.5rem] before:[inset-inline:0] before:content-[''] after:absolute after:[block-size:0.375rem] after:[inline-size:4rem] after:rounded-full after:bg-fd-border after:transition-colors after:-translate-x-1/2 after:-translate-y-1/2 after:inset-bs-[50%] after:inset-s-[50%] after:content-[''] data-[separator=active]:after:bg-fd-muted-foreground/80 data-[separator=focus]:after:bg-fd-muted-foreground/80 data-[separator=hover]:after:bg-fd-muted-foreground/65 md:[block-size:auto] md:[inline-size:1px] md:cursor-col-resize md:before:[inset-block:0] md:before:[inset-inline:-0.5rem] md:after:[block-size:4rem] md:after:[inline-size:0.375rem]"; group.append(separator); document.body.append(group); diff --git a/apps/docs/src/routes/$.tsx b/apps/docs/src/routes/$.tsx index 786b785c..fd786566 100644 --- a/apps/docs/src/routes/$.tsx +++ b/apps/docs/src/routes/$.tsx @@ -70,7 +70,11 @@ const clientLoader = browserCollections.docs.createClientLoader({ ) { const { githubUrl, markdownUrl, storybookUrl, ...pageProps } = props; return ( - + {frontmatter.title}
{frontmatter.description} @@ -95,7 +99,7 @@ function Page() { {clientLoader.useContent(data.path, { - className: '', + className: 'pb-16 md:pb-20 xl:pb-24', githubUrl: data.githubUrl, markdownUrl: data.markdownUrl, storybookUrl: data.storybookUrl, diff --git a/apps/docs/src/routes/playground/index.tsx b/apps/docs/src/routes/playground/index.tsx index eec26c89..0a3a28eb 100644 --- a/apps/docs/src/routes/playground/index.tsx +++ b/apps/docs/src/routes/playground/index.tsx @@ -169,7 +169,7 @@ function Playground() { { expect(result['color.surface.floating']).toBe(formatOklch(surfaces.floating)); expect(result['color.surface.overlay']).toBe(formatOklch(surfaces.overlay)); expect(result['color.scrim']).toBe(scrim); - expect(result['color.loadingSkeleton']).toBe(formatOklch(families.neutral[7])); + expect(result['color.loadingSkeleton']).toBe(formatOklch(families.neutral[8])); // Global text and borders use the neutral family. `border.control` is a solved // contrast boundary, not a scale-step alias, so it aliases the passed-through value. diff --git a/packages/@luke-ui/react/src/theme/semantic-map.ts b/packages/@luke-ui/react/src/theme/semantic-map.ts index e5d9d116..ffce7fac 100644 --- a/packages/@luke-ui/react/src/theme/semantic-map.ts +++ b/packages/@luke-ui/react/src/theme/semantic-map.ts @@ -58,7 +58,7 @@ export function mapSemanticColors(request: MapSemanticColorsRequest): SemanticCo values['color.surface.floating'] = formatOklch(surfaces.floating); values['color.surface.overlay'] = formatOklch(surfaces.overlay); values['color.scrim'] = scrim; - values['color.loadingSkeleton'] = formatOklch(neutral[7]); + values['color.loadingSkeleton'] = formatOklch(neutral[8]); // Global text / borders: neutral only. values['color.text.primary'] = formatOklch(neutral[12]);