From e50b9466b5c40fde45fe1435dd0245fad2ea2228 Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Tue, 19 May 2026 11:45:21 -0400 Subject: [PATCH 1/8] ui fixes --- .../compositions/compositions.module.scss | 76 ++++--- .../compositions/compositions.tsx | 202 ++++++++++++------ .../compositions-panel.module.scss | 13 ++ .../compositions-panel/compositions-panel.tsx | 28 +++ .../live-control-input.module.scss | 13 +- scopes/ui-foundation/ui/rspack/html.ts | 25 ++- .../ui/ui/client-context.module.scss | 8 + .../ui/ui/stable-theme-switcher.tsx | 37 +++- .../hope-component-card.module.scss | 10 + 9 files changed, 311 insertions(+), 101 deletions(-) diff --git a/scopes/compositions/compositions/compositions.module.scss b/scopes/compositions/compositions/compositions.module.scss index a16105bfeedb..2b42b8fb3e77 100644 --- a/scopes/compositions/compositions/compositions.module.scss +++ b/scopes/compositions/compositions/compositions.module.scss @@ -146,20 +146,30 @@ border-top: 1px solid var(--bit-border-color-lightest, #eaeaec); background: var(--bit-bg-color, #ffffff); overflow: hidden; + min-height: 0; +} + +.controlsTrayCollapsed { + // when collapsed, only show the header bar + height: auto; } -.trayDragHandle { +// ─── Two-zone header: resize strip on top, click-to-collapse below ───── +// The two zones are visually separated by a divider so the boundary is +// obvious: top = "grab to resize", bottom = "click to collapse". + +.trayResizeStrip { + position: relative; flex-shrink: 0; - height: 8px; - display: flex; - align-items: center; - justify-content: center; + height: 12px; cursor: ns-resize; - background: transparent; - transition: background 120ms ease; + user-select: none; + background: var(--surface01-color, rgba(0, 0, 0, 0.02)); + border-bottom: 1px solid var(--bit-border-color-lightest, #eaeaec); + transition: background 140ms ease; &:hover { - background: rgba(0, 0, 0, 0.03); + background: var(--surface-hover-color, rgba(0, 0, 0, 0.05)); } &:hover .trayDragBar { @@ -169,19 +179,36 @@ } .trayDragBar { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); width: 32px; height: 3px; border-radius: 2px; background: var(--bit-border-color-lightest, #d0d0d3); transition: all 140ms ease; + pointer-events: none; } -.trayHeader { - flex-shrink: 0; +.trayHeaderInner { display: flex; align-items: center; justify-content: space-between; - padding: 4px 14px 8px; + padding: 8px 14px; + cursor: pointer; + user-select: none; + outline: none; + transition: background 140ms ease; + + &:hover { + background: var(--surface-hover-color, rgba(0, 0, 0, 0.03)); + } +} + +// give the collapsed bar a bit more breathing room +.controlsTrayCollapsed .trayHeaderInner { + padding: 10px 14px; } .trayTitleRow { @@ -218,25 +245,22 @@ line-height: 1; } -.trayClose { - display: flex; +.trayCollapseIcon { + display: inline-flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; - border-radius: 4px; - border: none; - background: transparent; + width: 20px; + height: 20px; + font-size: 10px; color: var(--bit-text-color-light, #8b8d98); - cursor: pointer; - font-size: 12px; - padding: 0; - transition: all 120ms ease; + transform: rotate(90deg); + transform-origin: center; + transition: transform 200ms ease-in-out; + pointer-events: none; +} - &:hover { - background: rgba(0, 0, 0, 0.06); - color: var(--bit-text-color-heavy, #1c2024); - } +.trayCollapseIconCollapsed { + transform: rotate(-90deg); } .trayBody { diff --git a/scopes/compositions/compositions/compositions.tsx b/scopes/compositions/compositions/compositions.tsx index 97731409385a..8377bc9e0425 100644 --- a/scopes/compositions/compositions/compositions.tsx +++ b/scopes/compositions/compositions/compositions.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react'; import React, { useContext, useEffect, useState, useMemo, useRef, useCallback } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; import head from 'lodash.head'; import queryString from 'query-string'; import { ThemeContext } from '@teambit/documenter.theme.theme-context'; @@ -97,36 +98,41 @@ export function Compositions({ const queryParams = useMemo(() => queryString.stringify(compositionParams), [compositionParams]); - const [controlsTrayOpen, setControlsTrayOpen] = useState(false); + // Tracks compositions the user has explicitly collapsed. Anything not in + // this set defaults to expanded, which gives us "auto-open on load" as a + // pure derivation rather than an effect. + const [collapsedCompositions, setCollapsedCompositions] = useState>(() => new Set()); const [isDraggingTray, setIsDraggingTray] = useState(false); const trayRef = useRef(null); - const trayHeightRef = useRef(260); + const [trayHeight, setTrayHeight] = useState(260); const { ready, defs, values, onChange } = useLiveControls(); - const onTrayDragStart = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - const startY = e.clientY; - const startHeight = trayHeightRef.current; - setIsDraggingTray(true); - document.body.style.cursor = 'ns-resize'; - document.body.style.userSelect = 'none'; - const onMove = (ev: MouseEvent) => { - const next = Math.max(120, Math.min(600, startHeight + (startY - ev.clientY))); - trayHeightRef.current = next; - if (trayRef.current) { - trayRef.current.style.maxHeight = `${next}px`; - } - }; - const onUp = () => { - setIsDraggingTray(false); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }, []); + const onResizeStripMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button !== 0) return; + e.preventDefault(); + const startY = e.clientY; + const startHeight = trayHeight; + setIsDraggingTray(true); + document.body.style.cursor = 'ns-resize'; + document.body.style.userSelect = 'none'; + const onMove = (ev: MouseEvent) => { + const delta = startY - ev.clientY; + const next = Math.max(120, Math.min(window.innerHeight - 120, startHeight + delta)); + setTrayHeight(next); + }; + const onUp = () => { + setIsDraggingTray(false); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, + [trayHeight] + ); // collapse sidebar when empty, reopen when not useEffect(() => setSidebarOpenness(showSidebar), [showSidebar]); @@ -148,7 +154,21 @@ export function Compositions({ }, [enableLiveControls]); const currentCompositionHasControls = ready && defs.length > 0; - const showControlsTray = currentCompositionHasControls && controlsTrayOpen; + const currentCompositionIdentifier = currentComposition?.identifier; + const isTrayCollapsedForCurrent = + !!currentCompositionIdentifier && collapsedCompositions.has(currentCompositionIdentifier); + const showControlsTray = currentCompositionHasControls; + const isTrayCollapsed = showControlsTray && isTrayCollapsedForCurrent; + + const toggleTrayCollapsed = useCallback(() => { + if (!currentCompositionIdentifier) return; + setCollapsedCompositions((prev) => { + const next = new Set(prev); + if (next.has(currentCompositionIdentifier)) next.delete(currentCompositionIdentifier); + else next.add(currentCompositionIdentifier); + return next; + }); + }, [currentCompositionIdentifier]); return ( @@ -160,16 +180,6 @@ export function Compositions({ - {currentCompositionHasControls && ( - - - - )} {showControlsTray && ( -
-
-
-
-
-
- - Live Controls - {ready && defs.length > 0 && {defs.length}} -
- -
-
- {ready ? ( - - ) : ( -
No live controls available for this composition
- )} -
-
+ )}
@@ -234,6 +233,9 @@ export function Compositions({ isScaling={isScaling} useNameParam={useNameParam} includesEnvTemplate={component.preview?.includesEnvTemplate} + hasLiveControls={currentCompositionHasControls} + liveControlsActive={currentCompositionHasControls && !isTrayCollapsedForCurrent} + onToggleLiveControls={toggleTrayCollapsed} onSelectComposition={(composition) => { if (!currentComposition || !location) return; const selectedCompositionFromUrl = params['*']; @@ -252,9 +254,6 @@ export function Compositions({ } const newPath = pathSegments.join('/'); navigate(`/${newPath}?${urlParams.toString()}`); - // open the tray only on explicit user selection, so the - // tray stays closed on navigation/tab return. - setControlsTrayOpen(true); }} url={compositionUrl} compositions={component.compositions} @@ -272,6 +271,85 @@ export function Compositions({ ); } +type LiveControlsTrayProps = { + trayRef: React.RefObject; + collapsed: boolean; + height: number; + ready: boolean; + defs: any[]; + values: Record; + onChange: (key: string, value: any) => void; + onResizeStripMouseDown: (e: React.MouseEvent) => void; + onToggleExpanded: () => void; +}; + +function LiveControlsTray({ + trayRef, + collapsed, + height, + ready, + defs, + values, + onChange, + onResizeStripMouseDown, + onToggleExpanded, +}: LiveControlsTrayProps) { + return ( +
+ {!collapsed && ( +
+
+
+ )} +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onToggleExpanded(); + } + }} + title={collapsed ? 'Click to expand' : 'Click to collapse'} + > +
+ + Live Controls + {ready && defs.length > 0 && {defs.length}} +
+ + + +
+ {!collapsed && ( +
+ {ready ? ( + + ) : ( +
No live controls available for this composition
+ )} +
+ )} +
+ ); +} + export type CompositionContentProps = { component: ComponentModel; selected?: Composition; diff --git a/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.module.scss b/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.module.scss index 893b814366c0..fc9611dbf1d7 100644 --- a/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.module.scss +++ b/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.module.scss @@ -121,6 +121,19 @@ text-decoration: none; } +.iconButton { + background: transparent; + border: none; + padding: 0; + color: inherit; + font: inherit; +} + +.actionIconActive { + background: rgba(255, 255, 255, 0.15); + color: #ffffff !important; +} + // ─── Legacy compat ────────────────────────────────────────────────────── .liveControlsSkeleton { padding: 12px 16px; diff --git a/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.tsx b/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.tsx index 46354dc0abfe..a1947a09bbc7 100644 --- a/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.tsx +++ b/scopes/compositions/compositions/ui/compositions-panel/compositions-panel.tsx @@ -12,6 +12,9 @@ import type { Composition } from '../../composition'; export type CompositionsPanelProps = { compositions: Composition[]; onSelectComposition: (composition: Composition) => void; + onToggleLiveControls?: (composition: Composition) => void; + hasLiveControls?: boolean; + liveControlsActive?: boolean; active?: Composition; url: string; isScaling?: boolean; @@ -24,6 +27,9 @@ export function CompositionsPanel({ compositions, isScaling, onSelectComposition: onSelect, + onToggleLiveControls, + hasLiveControls, + liveControlsActive, active, includesEnvTemplate, useNameParam, @@ -77,6 +83,28 @@ export function CompositionsPanel({ {composition.displayName}
+ {hasLiveControls && onToggleLiveControls && composition === active && ( + + + + )} * { - width: 100%; + // Let the Toggle keep its native dimensions. The component sizes its + // knob travel in `em`, so stretching the wrapper width without scaling + // the font-size leaves the knob short of the right edge in the "on" + // state. + display: inline-flex; + flex: 0 0 auto; } .rangeWrapper { diff --git a/scopes/ui-foundation/ui/rspack/html.ts b/scopes/ui-foundation/ui/rspack/html.ts index f95d57a22a20..88afa1d17292 100644 --- a/scopes/ui-foundation/ui/rspack/html.ts +++ b/scopes/ui-foundation/ui/rspack/html.ts @@ -8,6 +8,29 @@ export function html(title: string, withDevTools?: boolean) { ${title}