feat(sw): viewer header logo menu and file list; simplify SW Storybook
SW Viewer Header (approved optional props, public export names unchanged at 170): - SWViewerHeader: logoMenu, documents, activeDocumentId, onDocumentSelect, onDocumentOpen, documentOpenLabel; measured responsive toggles (full -> icons -> More) with the SWHeader More / file-list styling. - SW AI guides, ExampleH, viewer/text-weight/theme-inheritance tests and the logo-document-menu tests with their QA record. SW Storybook cleanup (Story IDs and test fixtures kept): - Sidebar: Overview -> "SW Header" (first), 전체 Footer -> "SW Footer", Footer/Viewer responsive pages -> "Responsive More", Viewer content-variants -> "Settings Popup" (merges the settings popup page). - Hidden by the verification tag (direct URL kept): Header Dropdown, Document Open, Interactive States, Document Dropdown, Hidden Sub Controls, Home Menu standalone; Footer tool groups / custom content / parts; SW/Theme; Viewer menu groups / item states / variable size / settings panel / Header + Dropdown. - Removed demo UI: Overview size and callback output (demo data and selection only with &verification), Home Menu Figma node table and width demo, Header Responsive More reduced to 1440 / 768 / 390, Popup reopen button, property-panel comparison captions, Viewer context-menu demo (&verification only). - Tests: storybook-inventory records the hidden and renamed Stories; overview / range-cross-browser / viewer open the demo controls with &verification; integrated-popup reloads the Story instead of the reopen button; dropdown-consolidation asserts Dropdown is hidden from the sidebar and still opens by its direct URL.
This commit is contained in:
1 parent
46119eacb5
commit
23d522c136
57 files changed
+2623
-333
No files matched your search
@@ -1,5 +1,9 @@
|
||||
import { forwardRef, useId, useLayoutEffect, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type KeyboardEvent, type ReactNode } from "react"
|
||||
import { forwardRef, useEffect, useId, useLayoutEffect, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type KeyboardEvent, type ReactNode } from "react"
|
||||
import { moveRovingFocus } from "../shared/roving-focus"
|
||||
import { SWHeaderDocumentTitle } from "../sw-header/components/SWHeaderDocumentTitle"
|
||||
import { SWHeaderMore } from "../sw-header/components/SWHeaderMore"
|
||||
import type { SWHeaderDocument } from "../sw-header/types"
|
||||
import { SWToolButton } from "../sw-tool/SWTool"
|
||||
import { SWViewerMenu } from "./SWViewerMenu"
|
||||
import { SWViewerSwitch } from "./SWViewerControls"
|
||||
import { SWViewerIcon } from "./SWViewerIcon"
|
||||
@@ -25,8 +29,42 @@ export interface SWViewerHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "
|
||||
documentPlaceholder?: string
|
||||
/** Tooltip of the file name. Default: the name, or `파일 열기 또는 샘플을 선택하세요`. */
|
||||
documentTooltip?: string
|
||||
/** Replaces the file-name text (the dark document well stays). */
|
||||
/** Replaces the file-name text (the dark document well stays). Takes precedence over `documents`. */
|
||||
document?: ReactNode
|
||||
/**
|
||||
* Logo menu (2026-10-21): the logo cap opens a file menu flush under the header (SWHeaderMore behavior: Escape,
|
||||
* outside click, arrow keys; ArrowDown opens it on its first item and focus returns to the logo) with the face, colors,
|
||||
* size, spacing and type of the other viewer header dropdowns (the More face). `title` adds a
|
||||
* title row directly above the items (title + items are one group: no divider), shown only when an item follows;
|
||||
* without a title only the items are listed. `icon`: 16px icon left of the label (SWViewerMenu icon slot).
|
||||
* Each item runs its `onSelect`, then `onCommand` with `{ id, itemId: id }`. Takes precedence over `onHomeClick`;
|
||||
* with no visible item the cap keeps the plain logo / `onHomeClick`. Never open together with the document list.
|
||||
*/
|
||||
logoMenu?: {
|
||||
title?: string
|
||||
items: { id: string; label: string; icon?: ReactNode; value?: string; checked?: boolean; disabled?: boolean; hidden?: boolean; onSelect?: () => void }[]
|
||||
}
|
||||
/**
|
||||
* Document list below the file name (2026-10-21): the SWHeader file-name button and dropdown (SWHeaderDocumentTitle)
|
||||
* without + New and Open (file open is the header button, `onDocumentOpen`). The button shows `documentTitle` (or `documentPlaceholder`); the list
|
||||
* is at least as wide as the file-name button and the SWHeader list, wider for longer rows. Omit to keep the plain
|
||||
* file name.
|
||||
*/
|
||||
documents?: SWHeaderDocument[]
|
||||
/** Id of the current document in `documents` (its name in #FFC600; the list order never changes). */
|
||||
activeDocumentId?: string
|
||||
/** A document row was chosen. The host loads it and updates `documentTitle` / `activeDocumentId`. */
|
||||
onDocumentSelect?: (document: SWHeaderDocument) => void
|
||||
/**
|
||||
* File open button in the header (2026-10-21), like SWHeader `documentOpenPlacement="header"`: the SWHeader file-name
|
||||
* surface (SWToolButton `appearance="document"`) right after the logo, with a bordered folder icon and the label.
|
||||
* Shown only with this callback; the document list never shows Open or + New in the viewer. When the header has no
|
||||
* room it shows the icon only (label as tooltip and accessible name, measured like the other items), never in More.
|
||||
* Runs the callback, then `onCommand` with `{ id: "open", itemId: "document" }`.
|
||||
*/
|
||||
onDocumentOpen?: () => void
|
||||
/** Label of that button. Default `Open`. */
|
||||
documentOpenLabel?: string
|
||||
/** Menu data, in order. Groups with `placement="end"` form the trailing action area. */
|
||||
groups: SWViewerHeaderGroup[]
|
||||
/** Free content after the trailing groups (status, info button, ...). */
|
||||
@@ -42,20 +80,64 @@ export interface SWViewerHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "
|
||||
const MoreIcon = () => (
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="currentColor"><circle cx="3" cy="8" r="1.4" /><circle cx="8" cy="8" r="1.4" /><circle cx="13" cy="8" r="1.4" /></svg>
|
||||
)
|
||||
const FolderIcon = () => (
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></svg>
|
||||
)
|
||||
const ChevronDown = () => (
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
||||
)
|
||||
|
||||
const isShown = (item: { hidden?: boolean; collapseBelow?: number }, width: number) => !item.hidden && !(item.collapseBelow !== undefined && width < item.collapseBelow)
|
||||
/** Step id of the header Open button (icon only in stage 2). */
|
||||
const OPEN_ID = ":document-open"
|
||||
|
||||
const isShown = (item: { id: string; hidden?: boolean }, collapsed: ReadonlySet<string>) => !item.hidden && !collapsed.has(item.id)
|
||||
|
||||
/**
|
||||
* Measured responsive steps (2026-10-21), applied one at a time while the header has no room left and undone in reverse
|
||||
* order once the room a step freed is back: 1. every item with an `icon` and `iconOnlyBelow` shows its icon only (label
|
||||
* kept as tooltip and accessible name); 2. `collapseBelow` items move into More one by one, the higher value first and,
|
||||
* for the same value, the rightmost first. No fixed width decides a step: the values only give the order.
|
||||
*/
|
||||
type Step = { kind: "icons"; ids: string[] } | { kind: "collapse"; id: string }
|
||||
function responsiveSteps(groups: SWViewerHeaderGroup[], extraIcons: string[] = []): Step[] {
|
||||
const icons: string[] = [...extraIcons], collapse: { id: string; rank: number; order: number }[] = []
|
||||
groups.filter(group => !group.hidden).flatMap(group => group.items).forEach((item, order) => {
|
||||
if (item.hidden) return
|
||||
if (item.type !== "custom" && item.icon && !item.iconOnly && item.iconOnlyBelow !== undefined) icons.push(item.id)
|
||||
if (item.collapseBelow !== undefined) collapse.push({ id: item.id, rank: item.collapseBelow, order })
|
||||
})
|
||||
collapse.sort((a, b) => b.rank - a.rank || b.order - a.order)
|
||||
return [...(icons.length ? [{ kind: "icons" as const, ids: icons }] : []), ...collapse.map(item => ({ kind: "collapse" as const, id: item.id }))]
|
||||
}
|
||||
|
||||
/**
|
||||
* Room left in the header row (negative: missing) with every part at its natural width: the file name at its text
|
||||
* width up to its usual width (so a short name leaves room to the menus), the object surface without stretching and
|
||||
* the trailing area without its auto margin (`data-measuring`, sw-viewer.css). Read and reset in one layout effect,
|
||||
* never painted; positioned layers (menus, logo menu) never count. Same method as SWHeader's measured More.
|
||||
*/
|
||||
function measureRoom(root: HTMLElement) {
|
||||
root.setAttribute("data-measuring", "")
|
||||
const style = getComputedStyle(root), box = root.getBoundingClientRect()
|
||||
const end = box.right - parseFloat(style.borderRightWidth) - parseFloat(style.paddingRight)
|
||||
let used = box.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft)
|
||||
for (const child of Array.from(root.children)) {
|
||||
const childStyle = getComputedStyle(child)
|
||||
if (childStyle.display === "none" || childStyle.position === "absolute" || childStyle.position === "fixed") continue
|
||||
used = Math.max(used, child.getBoundingClientRect().right + parseFloat(childStyle.marginRight))
|
||||
}
|
||||
root.removeAttribute("data-measuring")
|
||||
return end - used
|
||||
}
|
||||
|
||||
/** More entries of the collapsed items, one section per group (menus and tabs get their own section). */
|
||||
function moreEntries(groups: SWViewerHeaderGroup[], width: number, command: (c: SWViewerCommand) => void, owners: Map<string, string>): SWViewerMenuEntry[] {
|
||||
function moreEntries(groups: SWViewerHeaderGroup[], collapsedIds: ReadonlySet<string>, command: (c: SWViewerCommand) => void, owners: Map<string, string>): SWViewerMenuEntry[] {
|
||||
const sections: SWViewerMenuEntry[][] = []
|
||||
// Dropdown entries keep their own callbacks; the More menu reports them to onCommand with their owner item.
|
||||
const own = (entries: SWViewerMenuEntry[], itemId: string) => entries.forEach(entry => { owners.set(entry.id, itemId); if (entry.type === "submenu") own(entry.entries, itemId) })
|
||||
for (const group of groups) {
|
||||
if (group.hidden) continue
|
||||
const collapsed = group.items.filter(item => !item.hidden && item.collapseBelow !== undefined && width < item.collapseBelow)
|
||||
const collapsed = group.items.filter(item => !item.hidden && collapsedIds.has(item.id))
|
||||
let plain: SWViewerMenuEntry[] = []
|
||||
const flush = () => { if (plain.length) sections.push([{ type: "label", id: `${group.id}:more`, label: group.moreLabel ?? group.label }, ...plain]); plain = [] }
|
||||
for (const item of collapsed) {
|
||||
@@ -103,7 +185,8 @@ function MenuButton({ item, iconOnly, onCommand }: { item: Extract<SWViewerHeade
|
||||
{!iconOnly && <span className="hm-sw-viewer-header__label">{item.label}</span>}
|
||||
<span className="hm-sw-viewer-header__chevron"><SWViewerIcon><ChevronDown /></SWViewerIcon></span>
|
||||
</button>
|
||||
<SWViewerMenu id={menuId} entries={item.entries} open={open} onOpenChange={setOpen} anchorRef={ref} align={item.menuAlign} width={item.menuWidth} label={item.label} initialFocus={focus}
|
||||
{/* 2026-10-21: header dropdowns use the More face (beige), like every menu of the viewer header. */}
|
||||
<SWViewerMenu id={menuId} entries={item.entries} open={open} onOpenChange={setOpen} anchorRef={ref} align={item.menuAlign} width={item.menuWidth} surface="beige" label={item.label} initialFocus={focus}
|
||||
onEntrySelect={entry => onCommand({ id: entry.id, itemId: item.id, value: entry.type === "checkbox" ? !entry.checked : undefined })} />
|
||||
</>
|
||||
)
|
||||
@@ -143,15 +226,15 @@ function SwitchField({ item, onCommand }: { item: Extract<SWViewerHeaderItem, {
|
||||
)
|
||||
}
|
||||
|
||||
function Item({ item, width, onCommand }: { item: SWViewerHeaderItem; width: number; onCommand: (c: SWViewerCommand) => void }) {
|
||||
function Item({ item, compact, onCommand }: { item: SWViewerHeaderItem; compact: boolean; onCommand: (c: SWViewerCommand) => void }) {
|
||||
if (item.type === "custom") return <>{item.render()}</>
|
||||
const iconOnly = !!item.icon && (item.iconOnly ? "always" : item.iconOnlyBelow !== undefined && width < item.iconOnlyBelow)
|
||||
const iconOnly = !!item.icon && (item.iconOnly ? "always" : compact)
|
||||
if (item.type === "menu") return <MenuButton item={item} iconOnly={iconOnly} onCommand={onCommand} />
|
||||
if (item.type === "tabs") return <Tabs item={item} onCommand={onCommand} />
|
||||
if (item.type === "switch") return <SwitchField item={item} onCommand={onCommand} />
|
||||
const toggle = item.type === "toggle"
|
||||
return (
|
||||
<SWViewerHeaderButton label={item.label} icon={item.icon} iconOnly={iconOnly === "always" ? true : iconOnly ? "compact" : false} pressed={toggle ? item.pressed : undefined} title={item.title} disabled={item.disabled}
|
||||
<SWViewerHeaderButton label={item.label} icon={item.icon} iconOnly={iconOnly === "always" ? true : iconOnly ? "compact" : false} pressed={toggle ? item.pressed : undefined} title={item.title ?? (iconOnly ? item.label : undefined)} disabled={item.disabled}
|
||||
onClick={() => {
|
||||
if (toggle) { item.onPressedChange?.(!item.pressed); onCommand({ id: item.id, itemId: item.id, value: !item.pressed }) }
|
||||
else { item.onSelect?.(); onCommand({ id: item.id, itemId: item.id }) }
|
||||
@@ -192,19 +275,23 @@ export const SWViewerHeaderButton = forwardRef<HTMLButtonElement, SWViewerHeader
|
||||
|
||||
/**
|
||||
* SW viewer-mode header (reference DWG/DXF viewer top menu): slanted brand cap → dark file-name well → one green
|
||||
* object surface holding data-driven menu groups, with a trailing action area. Items move into the More dropdown
|
||||
* by `collapseBelow` (measured header width, never horizontal scroll). Pure presentation: values are props, every
|
||||
* object surface holding data-driven menu groups, with a trailing action area. Responsive by the measured room of the
|
||||
* header row, not by fixed widths (2026-10-21): full items while they fit, then icons only (`iconOnlyBelow` items with
|
||||
* an icon), then `collapseBelow` items move into the More dropdown one by one; they come back in reverse order once
|
||||
* the room is there. The numbers only set the order. Never horizontal scroll. Pure presentation: values are props, every
|
||||
* action is a callback (`onCommand` plus each item's own). Use SWHeader for the normal SW mode.
|
||||
*/
|
||||
export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(function SWViewerHeader(
|
||||
{
|
||||
logo, logoSrc, logoAlt, onHomeClick, documentTitle, documentPlaceholder = "열린 도면 없음", documentTooltip, document: documentNode,
|
||||
logoMenu, documents, activeDocumentId, onDocumentSelect, onDocumentOpen, documentOpenLabel,
|
||||
groups, trailing, onCommand, moreLabel = "추가 도구", toolbarLabel = "도면 작업 도구", className, ...rest
|
||||
},
|
||||
forwardedRef,
|
||||
) {
|
||||
const rootRef = useRef<HTMLElement | null>(null)
|
||||
const [width, setWidth] = useState(1920)
|
||||
// Header width: a resize re-renders, and the layout pass below measures again.
|
||||
const [, setWidth] = useState(1920)
|
||||
const gradientId = `hm-sw-viewer-brand-${useId().replace(/:/g, "")}`
|
||||
useLayoutEffect(() => {
|
||||
const root = rootRef.current
|
||||
@@ -215,23 +302,73 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
|
||||
observer.observe(root)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
const start = groups.filter(group => group.placement !== "end")
|
||||
const end = groups.filter(group => group.placement === "end")
|
||||
// Measured responsive steps (see responsiveSteps): `level` steps are applied. Each layout pass measures the room; a
|
||||
// missing room applies the next step, and the last step is undone once the room it freed (measured right after it was
|
||||
// applied) is there again. The passes are synchronous layout effects, so only the settled layout is painted.
|
||||
const steps = responsiveSteps([...start, ...end], onDocumentOpen ? [OPEN_ID] : [])
|
||||
const [level, setLevel] = useState(0)
|
||||
const applied = steps.slice(0, level)
|
||||
const collapsed = new Set(applied.flatMap(step => (step.kind === "collapse" ? [step.id] : [])))
|
||||
const compact = new Set(applied.flatMap(step => (step.kind === "icons" ? step.ids : [])))
|
||||
const freedRef = useRef<{ before: number; room?: number }[]>([])
|
||||
// Steps or texts that change widths start the measurement over (file name, labels, items shown).
|
||||
const layoutKey = JSON.stringify([documentTitle, documentPlaceholder, documentOpenLabel, !!onDocumentOpen, steps, groups.map(group => [group.hidden, group.items.map(item => [item.id, item.hidden, "label" in item ? item.label : ""])])])
|
||||
const keyRef = useRef(layoutKey)
|
||||
// Web fonts change text widths without a resize: measure again once they are loaded.
|
||||
const [, setFontsLoaded] = useState(0)
|
||||
useEffect(() => {
|
||||
const fonts = typeof document === "undefined" ? undefined : document.fonts
|
||||
if (!fonts) return
|
||||
let live = true
|
||||
const loaded = () => { if (live) setFontsLoaded(count => count + 1) }
|
||||
fonts.addEventListener?.("loadingdone", loaded)
|
||||
fonts.ready.then(loaded)
|
||||
return () => { live = false; fonts.removeEventListener?.("loadingdone", loaded) }
|
||||
}, [])
|
||||
useLayoutEffect(() => {
|
||||
const root = rootRef.current
|
||||
if (!root) return
|
||||
if (keyRef.current !== layoutKey) {
|
||||
keyRef.current = layoutKey
|
||||
freedRef.current = []
|
||||
if (level !== 0) return setLevel(0)
|
||||
}
|
||||
if (level > steps.length) return setLevel(steps.length)
|
||||
const room = measureRoom(root)
|
||||
const freed = freedRef.current
|
||||
const last = level > 0 ? freed[level - 1] : undefined
|
||||
if (last && last.room === undefined) last.room = room - last.before
|
||||
if (room < -0.5) {
|
||||
if (level < steps.length) {
|
||||
freed[level] = { before: room }
|
||||
setLevel(level + 1)
|
||||
}
|
||||
} else if (level > 0 && (last?.room === undefined || room >= last.room)) {
|
||||
freed.length = level - 1
|
||||
setLevel(level - 1)
|
||||
}
|
||||
})
|
||||
const command = (value: SWViewerCommand) => onCommand?.(value)
|
||||
const owners = new Map<string, string>()
|
||||
const more = moreEntries(groups, width, command, owners)
|
||||
const more = moreEntries(groups, collapsed, command, owners)
|
||||
const moreRef = useRef<HTMLButtonElement>(null)
|
||||
const [moreOpen, setMoreOpen] = useState(false)
|
||||
const [moreFocus, setMoreFocus] = useState<"first" | "menu">("menu")
|
||||
// The logo menu and the document list are never open together (opening one closes the other).
|
||||
const [openMenu, setOpenMenu] = useState<"logo" | "document" | null>(null)
|
||||
const menuOpenChange = (menu: "logo" | "document") => (next: boolean) => setOpenMenu(current => (next ? menu : current === menu ? null : current))
|
||||
const logoItems = logoMenu?.items.filter(item => !item.hidden) ?? []
|
||||
const renderGroup = (group: SWViewerHeaderGroup) => {
|
||||
const items = group.items.filter(item => isShown(item, width))
|
||||
const items = group.items.filter(item => isShown(item, collapsed))
|
||||
if (group.hidden || !items.length) return null
|
||||
return (
|
||||
<div key={group.id} role="group" aria-label={group.label} className="hm-sw-viewer-header__group" data-surface={group.surface ?? "plain"} data-group={group.id}>
|
||||
{items.map(item => <Item key={item.id} item={item} width={width} onCommand={command} />)}
|
||||
{items.map(item => <Item key={item.id} item={item} compact={compact.has(item.id)} onCommand={command} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const start = groups.filter(group => group.placement !== "end")
|
||||
const end = groups.filter(group => group.placement === "end")
|
||||
const name = documentTitle || documentPlaceholder
|
||||
const mark = logo ?? (logoSrc ? <img src={logoSrc} alt="" /> : null)
|
||||
const brandContent = (
|
||||
@@ -253,11 +390,36 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
|
||||
return (
|
||||
<header ref={node => { rootRef.current = node; if (typeof forwardedRef === "function") forwardedRef(node); else if (forwardedRef) forwardedRef.current = node }}
|
||||
className={`hm-sw-theme hm-sw-viewer-header${className ? ` ${className}` : ""}`} {...rest}>
|
||||
{onHomeClick
|
||||
? <button type="button" className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} title={logoAlt} onClick={onHomeClick}>{brandContent}</button>
|
||||
: <div className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} role={logoAlt ? "img" : undefined} aria-label={logoAlt}>{brandContent}</div>}
|
||||
<div className="hm-sw-viewer-header__document" role="group" aria-label="현재 도면">
|
||||
{documentNode ?? <span className="hm-sw-viewer-header__document-name" data-empty={!documentTitle || undefined} title={documentTooltip ?? (documentTitle || "파일 열기 또는 샘플을 선택하세요")}>{name}</span>}
|
||||
{logoItems.length > 0
|
||||
? <SWHeaderMore className="hm-sw-viewer-header__logo-menu" align="start" title={logoMenu?.title} label={logoMenu?.title ?? (logoAlt ? `${logoAlt} 메뉴` : "메뉴")}
|
||||
open={openMenu === "logo"} onOpenChange={menuOpenChange("logo")}
|
||||
items={logoItems.map(item => ({
|
||||
id: item.id, label: item.label, icon: item.icon, value: item.value, checked: item.checked, disabled: item.disabled, tier: "action" as const, visible: true,
|
||||
onSelect: () => { item.onSelect?.(); command({ id: item.id, itemId: item.id }) },
|
||||
}))}
|
||||
renderTrigger={({ ref, ...trigger }) => (
|
||||
<button ref={ref} type="button" className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} data-state={trigger["aria-expanded"] ? "open" : undefined}
|
||||
aria-label={logoAlt ? `${logoAlt} 메뉴` : "메뉴"} title={logoAlt} {...trigger}>{brandContent}</button>
|
||||
)} />
|
||||
: onHomeClick
|
||||
? <button type="button" className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} title={logoAlt} onClick={onHomeClick}>{brandContent}</button>
|
||||
: <div className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} role={logoAlt ? "img" : undefined} aria-label={logoAlt}>{brandContent}</div>}
|
||||
{onDocumentOpen && (() => {
|
||||
const label = documentOpenLabel ?? "Open", iconOnly = compact.has(OPEN_ID)
|
||||
return (
|
||||
<SWToolButton appearance="document" className="hm-sw-viewer-header__open" data-document-open="" data-icon-only={iconOnly ? "" : undefined}
|
||||
aria-label={iconOnly ? label : undefined} title={iconOnly ? label : undefined}
|
||||
onClick={() => { onDocumentOpen(); command({ id: "open", itemId: "document" }) }}>
|
||||
<span className="hm-sw-viewer-header__open-icon" aria-hidden="true"><FolderIcon /></span>
|
||||
{!iconOnly && <span className="hm-sw-viewer-header__open-label">{label}</span>}
|
||||
</SWToolButton>
|
||||
)
|
||||
})()}
|
||||
<div className="hm-sw-viewer-header__document" role="group" aria-label="현재 도면" data-list={!documentNode && documents ? "" : undefined}>
|
||||
{documentNode ?? (documents
|
||||
? <SWHeaderDocumentTitle title={name} label="도면" documents={documents} activeDocumentId={activeDocumentId} onDocumentSelect={onDocumentSelect}
|
||||
showNew={false} showOpen={false} menuOpen={openMenu === "document"} onMenuOpenChange={menuOpenChange("document")} />
|
||||
: <span className="hm-sw-viewer-header__document-name" data-empty={!documentTitle || undefined} title={documentTooltip ?? (documentTitle || "파일 열기 또는 샘플을 선택하세요")}>{name}</span>)}
|
||||
</div>
|
||||
<div className="hm-sw-viewer-header__object" role="toolbar" aria-label={toolbarLabel}>
|
||||
{start.map(renderGroup)}
|
||||
|
||||
Reference in new issue
Block a user