Merge branch 'integration/sw-home-menu' into feature/sw-home-menu
Integrate the SW home (logo) menu into SWHeader for SW general mode only. SW viewer mode code and stories are unchanged. - Resolve conflicts: keep both sw-home-menu and sw-viewer exports; renumber AI docs (catalog viewer 8->9, patterns home menu 7->8, design rules home menu 11->12) - SWHeader: start the home menu after the file name when it sits inside controls, so the file name stays visible - AI guides: mark home menu as general-mode only; forbid it in viewer mode (AI_USAGE, catalog, design rules, patterns, VIEWER.md, AI_GUIDE) - Tests: ignore wildcard identifiers in ai-docs check; update expected Storybook SW titles (SW/Tool removed, SW/Viewer added) - Add merge QA report docs/sw/sw-home-menu-merge-2026-10-19.md
This commit is contained in:
commit
3a5e29f2fa
122 files changed
+6547
-41
No files matched your search
@@ -20,7 +20,8 @@ import { SWHeaderPropertyPanel } from "./components/SWHeaderPropertyPanel"
|
||||
import { SWHeaderDimensionStyleMenu, SWHeaderTextStyleMenu } from "./components/SWHeaderStyleListMenus"
|
||||
import { SWHeaderWindowControls } from "./components/SWHeaderWindowControls"
|
||||
import { SWToolButton } from "../sw-tool/SWTool"
|
||||
import type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId, SWHeaderMenuId, SWHeaderProps, SWHeaderSubControlId } from "./types"
|
||||
import { SWHomeMenu } from "../sw-home-menu/SWHomeMenu"
|
||||
import type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId, SWHeaderHomeMenuChangeReason, SWHeaderMenuId, SWHeaderProps, SWHeaderSubControlId } from "./types"
|
||||
|
||||
const defaults = {
|
||||
layer: { name: "0" },
|
||||
@@ -135,6 +136,10 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
onPropertyPanelOpenChange,
|
||||
disabled = false,
|
||||
tools,
|
||||
homeMenu,
|
||||
homeMenuOpen,
|
||||
defaultHomeMenuOpen = false,
|
||||
onHomeMenuOpenChange,
|
||||
logo,
|
||||
logoSrc,
|
||||
logoAlt,
|
||||
@@ -260,6 +265,41 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
setOpenCollapsed(false)
|
||||
}
|
||||
})
|
||||
|
||||
// Logo (home) menu: SWHeader owns its open state; a logo click toggles it instead of calling onHomeClick.
|
||||
const homeMenuId = useId()
|
||||
const homeButtonRef = useRef<HTMLElement | null>(null)
|
||||
const [internalHomeOpen, setInternalHomeOpen] = useState(defaultHomeMenuOpen)
|
||||
const homeOpen = !!homeMenu && shown("home") && (homeMenuOpen ?? internalHomeOpen)
|
||||
const [internalDocumentOpen, setInternalDocumentOpen] = useState(false)
|
||||
const documentOpenNow = documentMenuOpen ?? internalDocumentOpen
|
||||
const setDocumentOpen = (next: boolean) => {
|
||||
if (documentMenuOpen === undefined) setInternalDocumentOpen(next)
|
||||
onDocumentMenuOpenChange?.(next)
|
||||
}
|
||||
const setHomeOpen = (next: boolean, reason: SWHeaderHomeMenuChangeReason) => {
|
||||
if (homeMenuOpen === undefined) setInternalHomeOpen(next)
|
||||
if (next !== homeOpen) onHomeMenuOpenChange?.(next, reason)
|
||||
if (!next) return
|
||||
// One header layer at a time: the menu covers the fields, so their dropdown and the document menu close.
|
||||
if (openMenu) openers[openMenu].onOpenChange(false)
|
||||
if (documentOpenNow) setDocumentOpen(false)
|
||||
}
|
||||
// Placement from the DOM (never a fixed x): 2px before the object area (Figma 375 vs 377 at 1920) up to the
|
||||
// window area, so it follows the breakpoints, `leading`, the header Open and a narrowed file name.
|
||||
const [homeBox, setHomeBox] = useState({ left: 0, width: 0 })
|
||||
useLayoutEffect(() => {
|
||||
const header = fieldsRef.current?.closest<HTMLElement>(".hm-sw-header")
|
||||
homeButtonRef.current = header?.querySelector<HTMLElement>(":scope > .hm-sw-header__bar > .hm-sw-header__home") ?? null
|
||||
if (!homeOpen || !header || !panelAnchorRef.current) return
|
||||
const origin = header.getBoundingClientRect()
|
||||
// With `controls`, the file name sits inside the object area: start after it so it stays visible.
|
||||
const documentRight = fieldsRef.current?.querySelector<HTMLElement>(':scope > [data-control="document"]:not([data-hidden])')?.getBoundingClientRect().right
|
||||
const start = Math.max(panelAnchorRef.current.getBoundingClientRect().left, documentRight ?? -Infinity)
|
||||
const end = header.querySelector<HTMLElement>(":scope > .hm-sw-header__bar > .hm-sw-header__window")?.getBoundingClientRect().left ?? origin.right
|
||||
const next = { left: start - origin.left - 2, width: end - start + 2 }
|
||||
if (Math.abs(next.left - homeBox.left) > 0.1 || Math.abs(next.width - homeBox.width) > 0.1) setHomeBox(next)
|
||||
})
|
||||
const menuIds = configuration.map(item => item.id).filter((id): id is SWHeaderMenuId => id in menuSlot)
|
||||
const baseMenu = menuIds.includes("layer") ? "layer" : menuIds[0]
|
||||
const validPromoted = promoted && enabled(promoted) ? promoted : null
|
||||
@@ -440,8 +480,9 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
title={documentTitle}
|
||||
onClick={onDocumentClick}
|
||||
documents={documents}
|
||||
menuOpen={documentMenuOpen}
|
||||
onMenuOpenChange={onDocumentMenuOpenChange}
|
||||
// With a logo menu SWHeader holds the document menu state, so the two never stay open together.
|
||||
menuOpen={homeMenu ? documentOpenNow : documentMenuOpen}
|
||||
onMenuOpenChange={homeMenu ? next => { setDocumentOpen(next); if (next && homeOpen) setHomeOpen(false, "document") } : onDocumentMenuOpenChange}
|
||||
onDocumentSelect={onDocumentSelect}
|
||||
activeDocumentId={activeDocumentId}
|
||||
onDocumentClose={onDocumentClose}
|
||||
@@ -479,7 +520,8 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
return (
|
||||
<header ref={ref} className={`hm-sw-header${className ? ` ${className}` : ""}`} data-leading={openShown ? "open" : hasLeading ? "" : undefined} {...rest}>
|
||||
<div className="hm-sw-header__bar">
|
||||
<SWHeaderHome onClick={shown("home") ? onHomeClick : undefined} interactive={shown("home")} logo={logo} logoSrc={logoSrc} logoAlt={logoAlt} />
|
||||
<SWHeaderHome onClick={!shown("home") ? undefined : homeMenu ? () => setHomeOpen(!homeOpen, "home") : onHomeClick} interactive={shown("home")} logo={logo} logoSrc={logoSrc} logoAlt={logoAlt}
|
||||
aria-expanded={homeMenu && shown("home") ? homeOpen : undefined} aria-controls={homeOpen ? homeMenu?.id ?? homeMenuId : undefined} />
|
||||
{hasLeading && <div ref={leadingRef} className="hm-sw-header__leading">
|
||||
{openShown && <SWToolButton appearance="document" data-document-open="" disabled={disabled} onClick={onDocumentOpen}>{documentOpenLabel ?? "Open"}</SWToolButton>}
|
||||
{leading}
|
||||
@@ -505,6 +547,12 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
</div>}
|
||||
</div>
|
||||
{barOpen && barData && <div className="hm-sw-header__bar-anchor" style={{ left: barOffset, maxWidth: `calc(100% - ${barOffset}px)` }}><SWHeaderPropertyBar {...barData} disabled={disabled || propertyBar?.disabled} id={barId} /></div>}
|
||||
{homeMenu && shown("home") && (
|
||||
<div className="hm-sw-header__home-menu" style={{ left: homeBox.left, maxWidth: homeBox.width }}>
|
||||
<SWHomeMenu {...homeMenu} id={homeMenu.id ?? homeMenuId} open={homeOpen} returnFocusRef={homeButtonRef}
|
||||
onOpenChange={(next, reason) => setHomeOpen(next, reason)} />
|
||||
</div>
|
||||
)}
|
||||
{panelOpen && propertyPanel && <SWHeaderPropertyPanel {...propertyPanel} menus={{ showLayerFreeze: layerStates.showFreeze, showLayerLock: layerStates.showLock, ...propertyPanel.menus }} id={panelId} anchorRef={panelAnchorRef} onClose={closePanel} />}
|
||||
</header>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user