feat(sw): integrate home menu into SWHeader (Task A + B)
Combine Task A (e8409ef, SW 0.2.0 integrated improvement) with Task B
(396539d, Figma-based SWHomeMenu / SWHomeMenuSection / SWHomeMenuItem /
SWRecentFiles). Task B's 84 new files are taken unchanged except for the
public-import Storybook update and joining the popup Escape stack.
- Export the home menu from @hm-design/sw
- SWHeader homeMenu / homeMenuOpen / defaultHomeMenuOpen /
onHomeMenuOpenChange: hidden by default, the logo toggles it (onHomeClick
is not called while homeMenu is set), placed from the measured header,
closes with / closes the document and field dropdowns
- Logo aria-expanded / aria-controls; menu layer CSS (z-index 20)
- AI catalog / patterns / design rules, example G, README, usage guide
- Storybook SW/Home Menu uses @hm-design/sw; integration story
Verified: SW typecheck, tsc -b, SW build, Storybook build, AI docs 22/22,
28/28 existing SW shots pixel-identical, regression 96 PASS / 0 FAIL,
home menu checks 22/22. Not run: tarball consumer install.
This commit is contained in:
1 parent
e8409eff44
commit
1472e51f47
110 files changed
+6463
-17
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,39 @@ 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()
|
||||
const area = panelAnchorRef.current.getBoundingClientRect()
|
||||
const end = header.querySelector<HTMLElement>(":scope > .hm-sw-header__bar > .hm-sw-header__window")?.getBoundingClientRect().left ?? origin.right
|
||||
const next = { left: area.left - origin.left - 2, width: end - area.left + 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 +478,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 +518,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 +545,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