feat(sw): implement SW header design system
This commit is contained in:
1 parent
9407c8567f
commit
fc26afd440
107 files changed
+40063
-1
No files matched your search
@@ -0,0 +1,461 @@
|
||||
import { forwardRef, useEffect, useLayoutEffect, Fragment, type ReactNode, useId, useRef, useState } from "react"
|
||||
import { SWHeaderActions } from "./components/SWHeaderActions"
|
||||
import { SWHeaderColorMenu } from "./components/SWHeaderColorMenu"
|
||||
import { SWHeaderDocumentTitle } from "./components/SWHeaderDocumentTitle"
|
||||
import {
|
||||
SWHeaderColorControl, SWHeaderDimensionStyle, SWHeaderLayerControl,
|
||||
SWHeaderLineStyle, SWHeaderTextStyle,
|
||||
} from "./components/SWHeaderFields"
|
||||
import { SWHeaderHome } from "./components/SWHeaderHome"
|
||||
import { SWHeaderLayerMenu } from "./components/SWHeaderLayerMenu"
|
||||
import { SWHeaderLineStyleMenu } from "./components/SWHeaderLineStyleMenu"
|
||||
import { SWHeaderMore, type SWHeaderMoreItem } from "./components/SWHeaderMore"
|
||||
import { SWHeaderProperties } from "./components/SWHeaderProperties"
|
||||
import { SWHeaderPropertyBar } from "./components/SWHeaderPropertyBar"
|
||||
import { SWHeaderCommitPresence } from "./components/SWHeaderInput"
|
||||
import { SWHeaderKGuide } from "./components/SWHeaderKGuide"
|
||||
import printIcon from "../../assets/print.svg"
|
||||
import printHover from "../../assets/print-hover.svg"
|
||||
import { SWHeaderPropertyPanel } from "./components/SWHeaderPropertyPanel"
|
||||
import { SWHeaderDimensionStyleMenu, SWHeaderTextStyleMenu } from "./components/SWHeaderStyleListMenus"
|
||||
import { SWHeaderWindowControls } from "./components/SWHeaderWindowControls"
|
||||
import type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId, SWHeaderMenuId, SWHeaderProps } from "./types"
|
||||
|
||||
const defaults = {
|
||||
layer: { name: "0" },
|
||||
color: { value: "ByLayer", swatch: "#ffffff" },
|
||||
lineStyle: { name: "ByLayer", weight: "ByLayer", scale: "1.00" },
|
||||
textStyle: { font: "Arial", name: "Standard" },
|
||||
dimensionStyle: { name: "Standard" },
|
||||
}
|
||||
|
||||
/** `data-menu` of each field (its Figma variant name). */
|
||||
const menuSlot: Record<SWHeaderMenuId, string> = {
|
||||
layer: "layer", color: "color", lineStyle: "line-style", textStyle: "text-style", dimensionStyle: "dimension-style",
|
||||
}
|
||||
|
||||
const defaultMenuOrder = Object.keys(menuSlot) as SWHeaderMenuId[]
|
||||
const defaultControls: SWHeaderControlConfig[] = (["document", "drawingMode", ...defaultMenuOrder, "details", "propertyBar", "propertyPanel", "print", "popupGuide"] as const).map(id => ({ id }))
|
||||
const frameControls: readonly SWHeaderFrameControlId[] = ["home", "windowMove", "minimize", "close"]
|
||||
const isMenu = (id: string): id is SWHeaderMenuId => id in menuSlot
|
||||
|
||||
/**
|
||||
* Menu order: listed known ids first (duplicates and unknown ids ignored), missing ids after them in
|
||||
* the default order. Menus only swap places among the slots menus already take; other parts never move.
|
||||
*/
|
||||
function orderMenus(items: SWHeaderControlConfig[], menuOrder?: readonly SWHeaderMenuId[]) {
|
||||
if (!menuOrder) return items
|
||||
const listed = menuOrder.filter((id, index) => isMenu(id) && menuOrder.indexOf(id) === index)
|
||||
const rank = [...listed, ...defaultMenuOrder.filter(id => !listed.includes(id))]
|
||||
const menus = items.filter(item => isMenu(item.id)).sort((a, b) => rank.indexOf(a.id as SWHeaderMenuId) - rank.indexOf(b.id as SWHeaderMenuId))
|
||||
let next = 0
|
||||
return items.map(item => (isMenu(item.id) ? menus[next++] : item))
|
||||
}
|
||||
|
||||
/**
|
||||
* SW Header (Figma node 26:8944, 1920 x 49). Pure presentation: values are props,
|
||||
* user actions are callbacks, window behavior belongs to the host program.
|
||||
*/
|
||||
export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader(
|
||||
{
|
||||
documentTitle,
|
||||
controls,
|
||||
hiddenControls,
|
||||
menuOrder,
|
||||
documents,
|
||||
documentMenuOpen,
|
||||
onDocumentMenuOpenChange,
|
||||
onDocumentSelect,
|
||||
activeDocumentId,
|
||||
onDocumentClose,
|
||||
onDocumentNew,
|
||||
onDocumentOpen,
|
||||
documentIcon,
|
||||
documentNewLabel,
|
||||
documentOpenLabel,
|
||||
drawingModeLabel = "그리기모드",
|
||||
layer = defaults.layer,
|
||||
layers,
|
||||
activeLayerId,
|
||||
layerMenuOpen,
|
||||
onLayerMenuOpenChange,
|
||||
onLayerSelect,
|
||||
onLayerVisibilityChange,
|
||||
onLayerFreezeChange,
|
||||
onLayerLockChange,
|
||||
onLayerDetail,
|
||||
color = defaults.color,
|
||||
lineStyle = defaults.lineStyle,
|
||||
textStyle = defaults.textStyle,
|
||||
dimensionStyle = defaults.dimensionStyle,
|
||||
colors,
|
||||
userColors,
|
||||
colorMode,
|
||||
colorMenuOpen,
|
||||
onColorMenuOpenChange,
|
||||
onColorSelect,
|
||||
onColorModeChange,
|
||||
onColorInput,
|
||||
onColorPick,
|
||||
onColorDetail,
|
||||
lineTypes,
|
||||
lineWeights,
|
||||
activeLineTypeId,
|
||||
activeLineWeightId,
|
||||
lineStyleMenuOpen,
|
||||
onLineStyleMenuOpenChange,
|
||||
onLineTypeSelect,
|
||||
onLineWeightSelect,
|
||||
onLineWeightValueChange,
|
||||
onLineScaleChange,
|
||||
onLineStyleDetail,
|
||||
textStyles,
|
||||
activeTextStyleId,
|
||||
textStyleMenuOpen,
|
||||
onTextStyleMenuOpenChange,
|
||||
onTextStyleSelect,
|
||||
onTextStyleDetail,
|
||||
dimensionStyles,
|
||||
activeDimensionStyleId,
|
||||
dimensionStyleMenuOpen,
|
||||
onDimensionStyleMenuOpenChange,
|
||||
onDimensionStyleSelect,
|
||||
onDimensionStyleDetail,
|
||||
activeMenu = null,
|
||||
propertyBarOpen,
|
||||
defaultPropertyBarOpen = false,
|
||||
propertyBar,
|
||||
onPropertyBarOpenChange,
|
||||
propertyPanelOpen,
|
||||
propertyPanel,
|
||||
onPropertyPanelOpenChange,
|
||||
disabled = false,
|
||||
tools,
|
||||
logo,
|
||||
logoSrc,
|
||||
logoAlt,
|
||||
onHomeClick,
|
||||
onDocumentClick,
|
||||
onLayerOpen,
|
||||
onColorOpen,
|
||||
onLineStyleOpen,
|
||||
onTextStyleOpen,
|
||||
onDimensionStyleOpen,
|
||||
onPropertyBarToggle,
|
||||
onPropertyPanelToggle,
|
||||
onPrint,
|
||||
onPopupGuide,
|
||||
onWindowMoveStart,
|
||||
onMinimize,
|
||||
onClose,
|
||||
className,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
// The logo is never in this list: it is always shown. Hidden parts are unmounted (no space, no popup, no callback).
|
||||
const removed = new Set<string>(hiddenControls ?? [])
|
||||
const shown = (id: SWHeaderHideableControlId) => !removed.has(id)
|
||||
const seen = new Set<SWHeaderControlId>()
|
||||
const configuration = orderMenus((controls ?? defaultControls).filter(item => item.visible !== false && shown(item.id) && !seen.has(item.id) && !!seen.add(item.id)), menuOrder)
|
||||
const enabled = (id: SWHeaderControlId) => configuration.some(item => item.id === id)
|
||||
// Default design with removed object parts: same structure and breakpoints, but the script decides what
|
||||
// moves into More (the CSS alone assumes every field is present). Untouched defaults stay CSS-only.
|
||||
const scripted = controls !== undefined || [...removed].some(id => !frameControls.includes(id as SWHeaderFrameControlId))
|
||||
const presentRef = useRef(new Set(configuration.map(item => item.id)))
|
||||
presentRef.current = new Set(configuration.map(item => item.id))
|
||||
const label = (id: SWHeaderControlId, fallback: string) => configuration.find(item => item.id === id)?.label ?? fallback
|
||||
const [internalBarOpen, setInternalBarOpen] = useState(defaultPropertyBarOpen)
|
||||
const barOpen = !disabled && enabled("propertyBar") && (propertyBarOpen ?? internalBarOpen)
|
||||
const toggleBar = () => {
|
||||
if (!enabled("propertyBar") || disabled) return
|
||||
const next = !barOpen
|
||||
if (propertyBarOpen === undefined) setInternalBarOpen(next)
|
||||
onPropertyBarToggle?.()
|
||||
onPropertyBarOpenChange?.(next)
|
||||
}
|
||||
// Uncontrolled open dropdown (one at a time). A controlled `*MenuOpen` prop takes precedence.
|
||||
const [openMenu, setOpenMenu] = useState<SWHeaderMenuId | null>(null)
|
||||
// Representative slot (UI state only, never program data): a field chosen in More takes the layer
|
||||
// field's place while it is in More at the current width (CSS decides, see sw.css "Representative slot").
|
||||
const [promoted, setPromoted] = useState<SWHeaderMenuId | null>(null)
|
||||
const focusRef = useRef<SWHeaderMenuId | null>(null)
|
||||
const fieldsRef = useRef<HTMLDivElement>(null)
|
||||
const panelAnchorRef = useRef<HTMLDivElement>(null)
|
||||
const panelId = useId()
|
||||
const barId = useId()
|
||||
const [internalPanelOpen, setInternalPanelOpen] = useState(false)
|
||||
const panelOpen = !disabled && enabled("propertyPanel") && (propertyPanelOpen ?? internalPanelOpen)
|
||||
const setPanelOpen = (next: boolean) => {
|
||||
if (propertyPanelOpen === undefined) setInternalPanelOpen(next)
|
||||
if (next !== panelOpen) onPropertyPanelOpenChange?.(next)
|
||||
if (next) setOpenMenu(null)
|
||||
}
|
||||
const togglePanel = () => { onPropertyPanelToggle?.(); setPanelOpen(!panelOpen) }
|
||||
const closePanel = () => {
|
||||
if (propertyPanelOpen !== undefined && !onPropertyPanelOpenChange) onPropertyPanelToggle?.()
|
||||
setPanelOpen(false)
|
||||
const header = panelAnchorRef.current?.closest(".hm-sw-header")
|
||||
const trigger = header?.querySelector<HTMLButtonElement>('[data-property-panel-trigger]')
|
||||
const more = header?.querySelector<HTMLButtonElement>('.hm-sw-header__more-trigger')
|
||||
;(trigger?.getClientRects().length ? trigger : more)?.focus()
|
||||
}
|
||||
|
||||
|
||||
const [headerWidth, setHeaderWidth] = useState(1920)
|
||||
const [barOffset, setBarOffset] = useState(0)
|
||||
useLayoutEffect(() => {
|
||||
const header = fieldsRef.current?.closest(".hm-sw-header")
|
||||
if (!header) return
|
||||
const update = () => {
|
||||
const bounds = header.getBoundingClientRect()
|
||||
setHeaderWidth(bounds.width)
|
||||
setBarOffset((panelAnchorRef.current?.getBoundingClientRect().left ?? bounds.left) - bounds.left)
|
||||
}
|
||||
const observer = new ResizeObserver(update)
|
||||
update()
|
||||
observer.observe(header)
|
||||
if (panelAnchorRef.current) observer.observe(panelAnchorRef.current)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
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
|
||||
const isOverflow = (id: SWHeaderControlId) => id === "color" ? headerWidth < 900 : ["lineStyle", "textStyle", "dimensionStyle"].includes(id) ? headerWidth < 1800 : ["propertyBar", "propertyPanel", "details"].includes(id) ? headerWidth < 1080 : ["print", "popupGuide"].includes(id) ? headerWidth < 640 : false
|
||||
const representative = validPromoted && isOverflow(validPromoted) ? validPromoted : baseMenu
|
||||
const fieldHidden = (id: SWHeaderMenuId) => id !== representative && (isOverflow(id) || (id === baseMenu && representative !== baseMenu))
|
||||
// Removal is structural: close stale internal dropdown state silently, never send data callbacks.
|
||||
const controlKey = configuration.map(item => item.id).join(",")
|
||||
useEffect(() => {
|
||||
if (openMenu && !enabled(openMenu)) setOpenMenu(null)
|
||||
if (promoted && !enabled(promoted)) setPromoted(null)
|
||||
if (!enabled("propertyPanel")) setInternalPanelOpen(false)
|
||||
if (!enabled("propertyBar")) setInternalBarOpen(false)
|
||||
}, [controlKey, openMenu, promoted])
|
||||
|
||||
const hasMenu: Record<SWHeaderMenuId, boolean> = {
|
||||
layer: layers !== undefined,
|
||||
color: colors !== undefined,
|
||||
lineStyle: lineTypes !== undefined || lineWeights !== undefined,
|
||||
textStyle: textStyles !== undefined,
|
||||
dimensionStyle: dimensionStyles !== undefined,
|
||||
}
|
||||
const openChange = (id: SWHeaderMenuId, callback?: (open: boolean) => void) => (next: boolean) => {
|
||||
if (next && panelOpen) setPanelOpen(false)
|
||||
setOpenMenu(current => (next ? id : current === id ? null : current))
|
||||
callback?.(next)
|
||||
}
|
||||
const menuOpenProps = (id: SWHeaderMenuId, controlled: boolean | undefined, callback?: (open: boolean) => void) => ({
|
||||
open: enabled(id) && (controlled ?? openMenu === id),
|
||||
onOpenChange: openChange(id, callback),
|
||||
})
|
||||
const layerOpen = menuOpenProps("layer", layerMenuOpen, onLayerMenuOpenChange)
|
||||
const colorOpen = menuOpenProps("color", colorMenuOpen, onColorMenuOpenChange)
|
||||
const lineStyleOpen = menuOpenProps("lineStyle", lineStyleMenuOpen, onLineStyleMenuOpenChange)
|
||||
const textStyleOpen = menuOpenProps("textStyle", textStyleMenuOpen, onTextStyleMenuOpenChange)
|
||||
const dimensionStyleOpen = menuOpenProps("dimensionStyle", dimensionStyleMenuOpen, onDimensionStyleMenuOpenChange)
|
||||
const openers: Record<SWHeaderMenuId, { onOpenChange: (open: boolean) => void; onOpen?: () => void }> = {
|
||||
layer: { onOpenChange: layerOpen.onOpenChange, onOpen: onLayerOpen },
|
||||
color: { onOpenChange: colorOpen.onOpenChange, onOpen: onColorOpen },
|
||||
lineStyle: { onOpenChange: lineStyleOpen.onOpenChange, onOpen: onLineStyleOpen },
|
||||
textStyle: { onOpenChange: textStyleOpen.onOpenChange, onOpen: onTextStyleOpen },
|
||||
dimensionStyle: { onOpenChange: dimensionStyleOpen.onOpenChange, onOpen: onDimensionStyleOpen },
|
||||
}
|
||||
|
||||
/**
|
||||
* More → field: show the field in the representative slot (choosing the layer gives the slot back),
|
||||
* open its dropdown right below and move focus to the field. Calls the field's open callback once,
|
||||
* exactly like a click on the field; no data callback is called.
|
||||
*/
|
||||
const promote = (id: SWHeaderMenuId) => () => {
|
||||
setPromoted(id === baseMenu ? null : id)
|
||||
focusRef.current = id
|
||||
if (hasMenu[id]) openers[id].onOpenChange(true)
|
||||
openers[id].onOpen?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const id = focusRef.current
|
||||
if (!id) return
|
||||
focusRef.current = null
|
||||
// The slot is the field itself, or the wrapper of a field with a dropdown (layer: .hm-sw-header__layer).
|
||||
const slot = fieldsRef.current?.querySelector<HTMLElement>(scripted ? `[data-control="${id}"]` : id === "layer" ? ":scope > :is(.hm-sw-header__layer, [data-menu='layer'])" : `:scope > [data-menu="${menuSlot[id]}"]`)
|
||||
const field = slot?.matches(".hm-sw-header__field") ? slot : slot?.querySelector<HTMLElement>(".hm-sw-header__field")
|
||||
field?.focus()
|
||||
})
|
||||
|
||||
const currentLayerName = layers?.find(item => item.id === activeLayerId)?.name ?? layer.name
|
||||
const allMoreItems: SWHeaderMoreItem[] = [
|
||||
{ id: "layer", menu: "layer", label: "레이어", value: currentLayerName, tier: "slot", selected: activeMenu === "layer", disabled, onSelect: promote("layer") },
|
||||
{ id: "color", menu: "color", label: "색상", value: color.value, tier: "narrow", selected: activeMenu === "color", disabled, onSelect: promote("color") },
|
||||
{ id: "lineStyle", menu: "line-style", label: "선스타일", value: lineStyle.name, tier: "aux", selected: activeMenu === "lineStyle", disabled, onSelect: promote("lineStyle") },
|
||||
{ id: "textStyle", menu: "text-style", label: "문자스타일", value: textStyle.name, tier: "aux", selected: activeMenu === "textStyle", disabled, onSelect: promote("textStyle") },
|
||||
{ id: "dimensionStyle", menu: "dimension-style", label: "치수스타일", value: dimensionStyle.name, tier: "aux", selected: activeMenu === "dimensionStyle", disabled, onSelect: promote("dimensionStyle") },
|
||||
{ id: "propertyBar", label: "속성Bar", tier: "panel", checked: barOpen, disabled, onSelect: toggleBar },
|
||||
{ id: "propertyPanel", label: "속성창", tier: "panel", checked: panelOpen, disabled, onSelect: togglePanel },
|
||||
{ id: "print", label: "인쇄", tier: "action", disabled, onSelect: onPrint },
|
||||
{ id: "popupGuide", label: "팝업 가이드", tier: "action", disabled, guide: true, onSelect: onPopupGuide },
|
||||
]
|
||||
|
||||
const moreItems = configuration.flatMap(config => {
|
||||
const item = allMoreItems.find(item => item.id === config.id)
|
||||
if (!item) return []
|
||||
const hidden = config.id in menuSlot ? fieldHidden(config.id as SWHeaderMenuId) : isOverflow(config.id)
|
||||
return [{ ...item, label: config.label ?? item.label, visible: scripted ? hidden : undefined }]
|
||||
})
|
||||
const fieldNodes: Record<SWHeaderMenuId, ReactNode> = {
|
||||
layer: (layers ? (
|
||||
<SWHeaderLayerMenu label={label("layer", "레이어")}
|
||||
value={layer}
|
||||
layers={layers}
|
||||
activeLayerId={activeLayerId}
|
||||
{...layerOpen}
|
||||
onClick={onLayerOpen}
|
||||
selected={activeMenu === "layer"}
|
||||
disabled={disabled}
|
||||
onLayerSelect={onLayerSelect}
|
||||
onVisibilityChange={onLayerVisibilityChange}
|
||||
onFreezeChange={onLayerFreezeChange}
|
||||
onLockChange={onLayerLockChange}
|
||||
onDetail={onLayerDetail}
|
||||
/>
|
||||
) : (
|
||||
<SWHeaderLayerControl label={label("layer", "레이어")} value={layer} selected={activeMenu === "layer"} disabled={disabled} onClick={onLayerOpen} />
|
||||
)),
|
||||
color: (colors ? (
|
||||
<SWHeaderColorMenu label={label("color", "색상")}
|
||||
value={color}
|
||||
colors={colors}
|
||||
userColors={userColors}
|
||||
mode={colorMode}
|
||||
{...colorOpen}
|
||||
onClick={onColorOpen}
|
||||
selected={activeMenu === "color"}
|
||||
disabled={disabled}
|
||||
onColorSelect={onColorSelect}
|
||||
onModeChange={onColorModeChange}
|
||||
onColorInput={onColorInput}
|
||||
onPick={onColorPick}
|
||||
onDetail={onColorDetail}
|
||||
/>
|
||||
) : (
|
||||
<SWHeaderColorControl label={label("color", "색상")} value={color} selected={activeMenu === "color"} disabled={disabled} onClick={onColorOpen} />
|
||||
)),
|
||||
lineStyle: (hasMenu.lineStyle ? (
|
||||
<SWHeaderLineStyleMenu label={label("lineStyle", "선스타일")}
|
||||
value={lineStyle}
|
||||
lineTypes={lineTypes}
|
||||
lineWeights={lineWeights}
|
||||
activeLineTypeId={activeLineTypeId}
|
||||
activeLineWeightId={activeLineWeightId}
|
||||
{...lineStyleOpen}
|
||||
onClick={onLineStyleOpen}
|
||||
selected={activeMenu === "lineStyle"}
|
||||
disabled={disabled}
|
||||
onLineTypeSelect={onLineTypeSelect}
|
||||
onLineWeightSelect={onLineWeightSelect}
|
||||
onLineWeightValueChange={onLineWeightValueChange}
|
||||
onScaleChange={onLineScaleChange}
|
||||
onDetail={onLineStyleDetail}
|
||||
/>
|
||||
) : (
|
||||
<SWHeaderLineStyle label={label("lineStyle", "선스타일")} value={lineStyle} selected={activeMenu === "lineStyle"} disabled={disabled} onClick={onLineStyleOpen} />
|
||||
)),
|
||||
textStyle: (textStyles ? (
|
||||
<SWHeaderTextStyleMenu label={label("textStyle", "문자스타일")}
|
||||
value={textStyle}
|
||||
textStyles={textStyles}
|
||||
activeTextStyleId={activeTextStyleId}
|
||||
{...textStyleOpen}
|
||||
onClick={onTextStyleOpen}
|
||||
selected={activeMenu === "textStyle"}
|
||||
disabled={disabled}
|
||||
onTextStyleSelect={onTextStyleSelect}
|
||||
onDetail={onTextStyleDetail}
|
||||
/>
|
||||
) : (
|
||||
<SWHeaderTextStyle label={label("textStyle", "문자스타일")} value={textStyle} selected={activeMenu === "textStyle"} disabled={disabled} onClick={onTextStyleOpen} />
|
||||
)),
|
||||
dimensionStyle: (dimensionStyles ? (
|
||||
<SWHeaderDimensionStyleMenu label={label("dimensionStyle", "치수스타일")}
|
||||
value={dimensionStyle}
|
||||
dimensionStyles={dimensionStyles}
|
||||
activeDimensionStyleId={activeDimensionStyleId}
|
||||
{...dimensionStyleOpen}
|
||||
onClick={onDimensionStyleOpen}
|
||||
selected={activeMenu === "dimensionStyle"}
|
||||
disabled={disabled}
|
||||
onDimensionStyleSelect={onDimensionStyleSelect}
|
||||
onDetail={onDimensionStyleDetail}
|
||||
/>
|
||||
) : (
|
||||
<SWHeaderDimensionStyle label={label("dimensionStyle", "치수스타일")} value={dimensionStyle} selected={activeMenu === "dimensionStyle"} disabled={disabled} onClick={onDimensionStyleOpen} />
|
||||
)),
|
||||
}
|
||||
const documentNode = (<SWHeaderDocumentTitle
|
||||
label={label("document", "문서")}
|
||||
title={documentTitle}
|
||||
onClick={onDocumentClick}
|
||||
documents={documents}
|
||||
menuOpen={documentMenuOpen}
|
||||
onMenuOpenChange={onDocumentMenuOpenChange}
|
||||
onDocumentSelect={onDocumentSelect}
|
||||
activeDocumentId={activeDocumentId}
|
||||
onDocumentClose={onDocumentClose}
|
||||
onNew={onDocumentNew}
|
||||
onOpen={onDocumentOpen}
|
||||
documentIcon={documentIcon}
|
||||
newLabel={documentNewLabel}
|
||||
openLabel={documentOpenLabel}
|
||||
/>)
|
||||
const configuredNodes: Partial<Record<SWHeaderControlId, ReactNode>> = {
|
||||
...fieldNodes,
|
||||
document: documentNode,
|
||||
drawingMode: <span className="hm-sw-header__mode">{label("drawingMode", drawingModeLabel)}</span>,
|
||||
details: <span className="hm-sw-header__properties-caption">{label("details", "상세보기")}</span>,
|
||||
propertyBar: <button type="button" className="hm-sw-header__toggle" aria-pressed={barOpen} aria-controls={barOpen ? barId : undefined} disabled={disabled} onClick={toggleBar}>{label("propertyBar", "속성Bar")}</button>,
|
||||
propertyPanel: <button type="button" className="hm-sw-header__toggle" data-property-panel-trigger="" aria-pressed={panelOpen} aria-haspopup={propertyPanel ? "dialog" : undefined} aria-expanded={panelOpen} aria-controls={panelOpen ? panelId : undefined} disabled={disabled} onClick={togglePanel}>{label("propertyPanel", "속성창")}</button>,
|
||||
print: <button type="button" className="hm-sw-header__action hm-sw-header__action--print" aria-label={label("print", "인쇄")} disabled={disabled} onClick={onPrint}><img className="hm-sw-header__icon" src={printIcon} alt="" /><img className="hm-sw-header__icon-hover" src={printHover} alt="" /></button>,
|
||||
popupGuide: <SWHeaderKGuide label={label("popupGuide", "팝업 가이드")} disabled={disabled} onClick={onPopupGuide} />,
|
||||
}
|
||||
const presence = (id: SWHeaderControlId, node: ReactNode) => <SWHeaderCommitPresence.Provider value={() => presentRef.current.has(id)}>{node}</SWHeaderCommitPresence.Provider>
|
||||
const actionsShown = controls === undefined && (enabled("print") || enabled("popupGuide"))
|
||||
const windowShown = actionsShown || frameControls.slice(1).some(shown)
|
||||
const windowReduced = removed.size > 0 && (frameControls.some(id => removed.has(id)) || (controls === undefined && !(enabled("print") && enabled("popupGuide"))))
|
||||
// CSS-only default layout: a field holding the first slot instead of the removed layer is styled as promoted.
|
||||
const promotedSlot = validPromoted ?? (!scripted && baseMenu && baseMenu !== "layer" ? baseMenu : null)
|
||||
const barData = propertyBar ?? (propertyPanel && {
|
||||
selection: propertyPanel.selection,
|
||||
properties: propertyPanel.properties,
|
||||
onPropertyChange: propertyPanel.onPropertyChange,
|
||||
})
|
||||
|
||||
return (
|
||||
<header ref={ref} className={`hm-sw-header${className ? ` ${className}` : ""}`} {...rest}>
|
||||
<div className="hm-sw-header__bar">
|
||||
<SWHeaderHome onClick={shown("home") ? onHomeClick : undefined} interactive={shown("home")} logo={logo} logoSrc={logoSrc} logoAlt={logoAlt} />
|
||||
{controls === undefined && enabled("document") && documentNode}
|
||||
<div ref={panelAnchorRef} className="hm-sw-header__object-area" role="toolbar" aria-label="객체 수정" data-configured={controls !== undefined || undefined} data-promoted={promotedSlot ? menuSlot[promotedSlot] : undefined}>
|
||||
{controls === undefined && enabled("drawingMode") && <span className="hm-sw-header__mode">{drawingModeLabel}</span>}
|
||||
<div ref={fieldsRef} className="hm-sw-header__fields" data-configured={controls !== undefined || undefined}>
|
||||
{controls ? configuration.map(config => {
|
||||
const hidden = config.id in menuSlot ? fieldHidden(config.id as SWHeaderMenuId) : isOverflow(config.id) || (config.id === "drawingMode" && headerWidth < 1180)
|
||||
return <div key={config.id} className="hm-sw-header__control" data-control={config.id} data-hidden={hidden || undefined}>{presence(config.id, configuredNodes[config.id])}</div>
|
||||
}) : scripted ? menuIds.map(id => (
|
||||
<div key={id} className="hm-sw-header__control" data-control={id} data-hidden={fieldHidden(id) || undefined} data-representative={(id === representative && id !== baseMenu) || undefined}>{presence(id, fieldNodes[id])}</div>
|
||||
)) : menuIds.map(id => <Fragment key={id}>{fieldNodes[id]}</Fragment>)}
|
||||
{tools}
|
||||
</div>
|
||||
{moreItems.some(item => item.visible !== false) && <SWHeaderMore items={moreItems} />}
|
||||
{controls === undefined && <SWHeaderProperties barOpen={barOpen} panelOpen={panelOpen} panelId={propertyPanel ? panelId : undefined} disabled={disabled} onBarToggle={toggleBar} onPanelToggle={togglePanel} showCaption={enabled("details")} showBar={enabled("propertyBar")} showPanel={enabled("propertyPanel")} />}
|
||||
</div>
|
||||
{windowShown && <div className="hm-sw-header__window" data-configured={controls !== undefined || undefined} data-reduced={windowReduced || undefined}>
|
||||
{actionsShown && <SWHeaderActions disabled={disabled} onPrint={onPrint} onPopupGuide={onPopupGuide} showPrint={enabled("print")} showPopupGuide={enabled("popupGuide")} />}
|
||||
<SWHeaderWindowControls onWindowMoveStart={onWindowMoveStart} onMinimize={onMinimize} onClose={onClose} showWindowMove={shown("windowMove")} showMinimize={shown("minimize")} showClose={shown("close")} />
|
||||
</div>}
|
||||
</div>
|
||||
{barOpen && barData && <div className="hm-sw-header__bar-anchor" style={{ left: barOffset, width: `calc(100% - ${barOffset}px)` }}><SWHeaderPropertyBar {...barData} disabled={disabled || propertyBar?.disabled} id={barId} /></div>}
|
||||
{panelOpen && propertyPanel && <SWHeaderPropertyPanel {...propertyPanel} id={panelId} anchorRef={panelAnchorRef} onClose={closePanel} />}
|
||||
</header>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user