feat(sw): viewer follow-up — SVG icon outline, shared SW popup frame, story grid, green layer buttons
- 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
This commit is contained in:
1 parent
7f42923ccf
commit
68fbcf6e15
45 files changed
+1463
-377
No files matched your search
@@ -0,0 +1,49 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user