- A: SWViewerIcon draws the SWHeader black outline as SVG stroke on a copy behind the icon (no CSS filter) - B: SWViewerPanel renders the common SWPopup frame (same DOM / CSS) and adds drag, corner resize, collapse, bounds and modal; SWPopup gains optional titleActions / footer; title art spans any width - C: shared Story canvas grid (--sw-story-canvas) behind the SW Header work area and dropdown room - D: SWViewerButton tone="green" for 전체 켜기 / 전체 끄기: default kept, hover = primary (확인) hover token, pressed = primary face - AI guide, verification scripts (76 checks, element regression) and evidence updated
50 lines
2.5 KiB
TypeScript
50 lines
2.5 KiB
TypeScript
import { useLayoutEffect, useRef, type ReactNode } from "react"
|
|
import "../../theme/sw-viewer.css"
|
|
|
|
const SHAPES = "path, line, polyline, polygon, circle, ellipse, rect"
|
|
|
|
export interface SWViewerIconProps {
|
|
/** The program's SVG icon (any size; stroke and / or fill, currentColor recommended). */
|
|
children: ReactNode
|
|
className?: string
|
|
}
|
|
|
|
/**
|
|
* Icon with the SWHeader black outline drawn as SVG stroke (SWHeader icon assets: 1px black OUTSIDE stroke).
|
|
* A copy of the icon sits behind it: every shape of the copy is stroked black with its own stroke width plus
|
|
* 2 x `--hm-sw-viewer-icon-outline-width` (1px each side, in screen pixels at the rendered size), and filled
|
|
* shapes are filled black. The icon on top is untouched, so its color, shape, size and line width stay the same.
|
|
*/
|
|
export function SWViewerIcon({ children, className }: SWViewerIconProps) {
|
|
const outlineRef = useRef<HTMLSpanElement>(null)
|
|
useLayoutEffect(() => {
|
|
const root = outlineRef.current
|
|
if (!root) return
|
|
const outline = parseFloat(getComputedStyle(root).getPropertyValue("--hm-sw-viewer-icon-outline-width")) || 1
|
|
for (const svg of Array.from(root.querySelectorAll("svg"))) {
|
|
const view = svg.viewBox?.baseVal
|
|
const rendered = svg.getBoundingClientRect().width
|
|
// User units per screen pixel, so the outline is 1px on screen whatever the viewBox (e.g. 24 drawn at 16px).
|
|
const unit = view && view.width && rendered ? view.width / rendered : 1
|
|
for (const shape of Array.from(svg.querySelectorAll<SVGElement>(SHAPES))) {
|
|
shape.style.removeProperty("stroke-width")
|
|
const style = getComputedStyle(shape)
|
|
const strokeWidth = style.stroke !== "none" ? parseFloat(style.strokeWidth) || 0 : 0
|
|
const filled = style.fill !== "none" && Number(style.fillOpacity) !== 0
|
|
if (!strokeWidth && !filled) { shape.style.visibility = "hidden"; continue }
|
|
shape.style.stroke = "var(--hm-sw-viewer-icon-outline-color)"
|
|
shape.style.strokeWidth = `${strokeWidth + 2 * outline * unit}`
|
|
shape.style.strokeLinejoin = "round"
|
|
if (!strokeWidth) shape.style.strokeLinecap = "round"
|
|
if (filled) shape.style.fill = "var(--hm-sw-viewer-icon-outline-color)"
|
|
}
|
|
}
|
|
})
|
|
return (
|
|
<span className={`hm-sw-viewer-icon${className ? ` ${className}` : ""}`} aria-hidden="true">
|
|
<span ref={outlineRef} className="hm-sw-viewer-icon__outline">{children}</span>
|
|
<span className="hm-sw-viewer-icon__glyph">{children}</span>
|
|
</span>
|
|
)
|
|
}
|