Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/ui/src/components/preview-split-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { clsx } from 'clsx';
import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
import { ResizeHandle, ResizeOverlay } from '@/components/resize-handle';
import { usePreviewSplit } from '@/hooks/use-preview-split';
import { useSidebarCollapsed } from '@/hooks/use-sidebar-collapsed';
import styles from './style.module.css';

// Keep in sync with the content-column transition duration in style.module.css.
Expand All @@ -28,6 +29,7 @@ export function PreviewSplitFrame( {
}: PreviewSplitFrameProps ) {
const showPreview = previewOpen && preview != null;
const { rootRef, contentWidthVar, isResizing, handleProps } = usePreviewSplit( { showPreview } );
const isSidebarCollapsed = useSidebarCollapsed();

// Animate only open/close toggles of an already-mounted preview — never the
// initial layout, so a route loading with the preview visible doesn't replay
Expand Down Expand Up @@ -67,6 +69,7 @@ export function PreviewSplitFrame( {
ref={ rootRef }
className={ clsx(
styles.root,
isSidebarCollapsed && styles.rootFrameless,
showPreview && styles.rootPreviewOpen,
isResizing && styles.rootPreviewResizing,
animatingPreviewToggle && styles.rootPreviewAnimating
Expand Down
36 changes: 35 additions & 1 deletion apps/ui/src/components/preview-split-frame/style.module.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
.root {
height: 100%;
/* Inset frame around the whole content+preview container: margin on the
top, right, and bottom (the left edge stays against the sidebar), with
the border and radius on this container — the panels inside stay
plain. */
--panel-frame-gap: 12px;
--panel-frame-radius: 10px;

box-sizing: border-box;
height: calc(100% - 2 * var(--panel-frame-gap));
margin: var(--panel-frame-gap) var(--panel-frame-gap) var(--panel-frame-gap) 0;
min-height: 0;
position: relative;
overflow: hidden;
background-color: var(--wpds-color-bg-surface-neutral);
/* The chrome behind (sidebar-layout's .main) is a window drag region;
the content frame itself must stay interactive. */
-webkit-app-region: no-drag;
/* Without this, the container's own (light) background paints under the
border and shows through any transparent border color, so the edge
always reads as a solid light line no matter the border alpha. */
background-clip: padding-box;
/* The frame always sits on the dark window chrome (both OS schemes), so
a soft translucent white reads as a gentle edge highlight instead of a
hard stroke — especially against dark page content in the preview. */
border: 1px solid rgb(255 255 255 / 18%);
border-radius: var(--panel-frame-radius);
transition: height 150ms ease, margin 150ms ease, border-radius 150ms ease;
}

/* Full-bleed when the sidebar is hidden — the frame reads as chrome that
belongs with the sidebar. The border must drop to zero WIDTH (not just
transparent): the chrome background shows through a transparent border
ring via background-clip, leaving stray dark lines at the edges. */
.rootFrameless {
height: 100%;
margin: 0;
border-width: 0;
border-radius: 0;
}

/* Opaque and above the preview slot so it fully covers the (still mounted)
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/sidebar-header/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
position: sticky;
top: 0;
z-index: 1;
background-color: var(--wpds-color-bg-surface-neutral-weak);
background-color: var(--app-chrome-bg);
}

/* When there are no traffic lights to clear — non-macOS, the browser
Expand Down
4 changes: 4 additions & 0 deletions apps/ui/src/components/sidebar-layout/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ vi.mock( '@/hooks/use-fullscreen', () => ( {
useFullscreen: () => false,
} ) );

vi.mock( '@/hooks/use-color-scheme', () => ( {
useColorScheme: () => 'light',
} ) );

vi.mock( '@wordpress/ui', async () => {
const actual = await vi.importActual< typeof import('@wordpress/ui') >( '@wordpress/ui' );
return {
Expand Down
59 changes: 42 additions & 17 deletions apps/ui/src/components/sidebar-layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { __ } from '@wordpress/i18n';
import { privateApis } from '@wordpress/theme';
import { IconButton } from '@wordpress/ui';
import { clsx } from 'clsx';
import { useCallback, useEffect, useState } from 'react';
Expand All @@ -8,18 +9,31 @@ import { SidebarHeader } from '@/components/sidebar-header';
import { SiteList } from '@/components/site-list';
import { UserMenu } from '@/components/user-menu';
import { useConnector } from '@/data/core';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useResizablePanel } from '@/hooks/use-resizable-panel';
import { SidebarCollapsedContext } from '@/hooks/use-sidebar-collapsed';
import { useTrafficLightSpace } from '@/hooks/use-traffic-light-space';
import { drawerIcon } from '@/lib/icons';
import { SIDEBAR_PANEL_CONFIG, SIDEBAR_PANEL_STORAGE_KEY } from '@/lib/resizable-panels';
import { unlock } from '@/lock-unlock';
import styles from './style.module.css';
import type { CSSProperties, ReactNode } from 'react';

const { ThemeProvider } = unlock( privateApis );

// Dark window chrome behind the sidebar and the content frame, mimicking the
// legacy renderer's `bg-chrome` (rgba(30,30,30,1)) and the wp-admin dark
// chrome. Dark mode goes a step deeper so the chrome still contrasts with
// #1e1e1e content surfaces.
const CHROME_BG_LIGHT = '#1e1e1e';
const CHROME_BG_DARK = '#161616';

export function SidebarLayout( { children }: { children: ReactNode } ) {
const [ collapsed, setCollapsed ] = useState( false );
const connector = useConnector();
const reserveTrafficLightSpace = useTrafficLightSpace();
const colorScheme = useColorScheme();
const chromeBg = colorScheme === 'dark' ? CHROME_BG_DARK : CHROME_BG_LIGHT;
const sidebarResize = useResizablePanel( {
config: SIDEBAR_PANEL_CONFIG,
edge: 'right',
Expand All @@ -36,7 +50,7 @@ export function SidebarLayout( { children }: { children: ReactNode } ) {

return (
<SidebarCollapsedContext.Provider value={ collapsed }>
<div className={ styles.root }>
<div className={ styles.root } style={ { '--app-chrome-bg': chromeBg } as CSSProperties }>
<aside
className={ clsx(
styles.sidebar,
Expand All @@ -45,24 +59,35 @@ export function SidebarLayout( { children }: { children: ReactNode } ) {
) }
style={ sidebarStyle }
>
<SidebarHeader onToggleSidebar={ toggleSidebar } />
<SiteList />
<div className={ styles.sidebarFooter }>
{ ! collapsed ? <AppToasts className={ styles.sidebarToasts } /> : null }
<UserMenu />
</div>
{ /* The sidebar sits on the dark window chrome in both color
schemes, so its wpds tokens come from a nested dark theme
scope. */ }
<ThemeProvider color={ { bg: chromeBg } }>
<div className={ styles.sidebarThemeScope }>
<SidebarHeader onToggleSidebar={ toggleSidebar } />
<SiteList />
<div className={ styles.sidebarFooter }>
{ ! collapsed ? <AppToasts className={ styles.sidebarToasts } /> : null }
<UserMenu />
</div>
</div>
</ThemeProvider>
</aside>
{ ! collapsed ? (
<ResizeHandle
className={ styles.resizeHandle }
label={ __( 'Resize sidebar' ) }
minWidth={ sidebarResize.minWidth }
maxWidth={ sidebarResize.maxWidth }
width={ sidebarResize.width }
isResizing={ sidebarResize.isResizing }
onResizeStart={ sidebarResize.handleResizeStart }
onKeyDown={ sidebarResize.handleKeyDown }
/>
// Same dark theme scope as the sidebar so the indicator's
// brand token resolves against the dark ramp.
<ThemeProvider color={ { bg: chromeBg } }>
<ResizeHandle
className={ styles.resizeHandle }
label={ __( 'Resize sidebar' ) }
minWidth={ sidebarResize.minWidth }
maxWidth={ sidebarResize.maxWidth }
width={ sidebarResize.width }
isResizing={ sidebarResize.isResizing }
onResizeStart={ sidebarResize.handleResizeStart }
onKeyDown={ sidebarResize.handleKeyDown }
/>
</ThemeProvider>
) : null }
<main className={ styles.main }>
{ collapsed ? (
Expand Down
30 changes: 25 additions & 5 deletions apps/ui/src/components/sidebar-layout/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
}

.sidebar {
--sidebar-row-background: var(--wpds-color-bg-interactive-neutral-weak-active);

width: var(--sidebar-width, 320px);
/* The 240px floor is owned by useResizablePanel, which clamps
--sidebar-width on every path. A CSS min-width here would be redundant
Expand All @@ -15,8 +13,7 @@
min-width: 0;
max-width: 25vw;
flex-shrink: 0;
background-color: var(--wpds-color-bg-surface-neutral-weak);
border-right: 1px solid var(--wpds-color-stroke-surface-neutral);
background-color: var(--app-chrome-bg);
display: flex;
flex-direction: column;
/* The sidebar itself scrolls so the scrollbar gutter affects the
Expand All @@ -27,6 +24,14 @@
transition: width 150ms ease;
}

/* Inside the sidebar's dark ThemeProvider; generates no box, but declares
the row background here so it resolves against the dark ramp (declared on
.sidebar it would resolve against the outer light theme). */
.sidebarThemeScope {
display: contents;
--sidebar-row-background: var(--wpds-color-bg-interactive-neutral-weak-active);
}

/* While dragging the handle the width updates every frame; the collapse
transition above would animate toward each step and lag behind the cursor.
Drop it for the duration of the drag (mirrors `.rootPreviewResizing` in
Expand All @@ -43,7 +48,7 @@
position: sticky;
bottom: 0;
z-index: 1;
background-color: var(--wpds-color-bg-surface-neutral-weak);
background-color: var(--app-chrome-bg);
}

.sidebarCollapsed {
Expand All @@ -56,6 +61,8 @@
overflow: auto;
position: relative;
isolation: isolate;
background-color: var(--app-chrome-bg);
-webkit-app-region: drag;
}

/* The negative margin cancels the handle's width so it adds no space to the
Expand All @@ -70,6 +77,19 @@
justify-content: flex-start;
}

/* Sidebar-specific indicator treatment: match the content card's vertical
extent instead of running through the frame gaps above/below it (keep the
12px in sync with --panel-frame-gap in preview-split-frame). The handle is
wrapped in the sidebar's dark ThemeProvider, so the brand token resolves
to the on-dark blue. Nudged left off the content card's edge onto the
chrome, with rounded caps to match the frame's soft corners. */
.resizeHandle span {
margin-block: 12px;
margin-inline-start: -3px;
border-radius: 999px;
background-color: var(--wpds-color-fg-interactive-brand);
}

/* Pinned to the bottom-left of the main area, mirroring the sidebar footer.
The button carries its own `padding-sm` top/bottom for its hit area; the
matching negative `bottom` cancels that lower padding so the glyph sits flush
Expand Down
14 changes: 3 additions & 11 deletions apps/ui/src/components/site-list/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@
--site-list-chat-status-start-offset: 4px;
--site-list-chat-status-gap: 3px;
--site-list-label-inline-offset: 4px;
--site-list-pending-indicator-bg: color-mix(
in srgb,
var(--wpds-color-bg-interactive-error-strong) 34%,
var(--wpds-color-bg-surface-caution)
);
/* The sidebar sits on the dark window chrome in both color schemes, so
the pending indicator is always the vivid on-dark value. */
--site-list-pending-indicator-bg: var(--studio-color-status-attention);
--site-list-sync-indicator-bg: var(--studio-color-status-sync);

/* Pull the row shape slightly left, then add the inset back inside rows so
Expand All @@ -25,12 +23,6 @@
user-select: none;
}

@media (prefers-color-scheme: dark) {
.root {
--site-list-pending-indicator-bg: var(--studio-color-status-attention);
}
}

.sites {
display: flex;
flex-direction: column;
Expand Down