feat(sw): viewer layer popup bulb icon and all-layers switch
User-approved 2026-10-22 change to the SW Viewer layer popup.
- New public export SWLayerVisibilityIcon({ on }) + SWLayerVisibilityIconProps:
the SW Header layer bulb art reused as is (header design and behavior unchanged).
- Layer rows: eye (Eye / EyeOff) icon replaced by the bulb, 24 x 24 button kept.
- All layers: the 전체 켜기 / 전체 끄기 buttons replaced by one SWViewerField +
SWViewerSwitch on top. ON only when every layer is shown; a partial state reads
OFF and a click shows all. Derived from the hidden-layer state, so it stays synced.
- AI docs (VIEWER.md 4.1, catalog, design rules, composition, AI_GUIDE sync) and
example H follow the new rule, replacing the 2026-10-21 eye-icon rule.
- viewer.browser.mjs: layer popup checks moved to the bulb / switch behavior.
This commit is contained in:
1 parent
1dd8fb3aad
commit
8cb98b373f
16 files changed
+79
-52
No files matched your search
@@ -2,10 +2,10 @@
|
||||
// UI state lives here (like a consuming program); no DWG/DXF engine — commands are logged, not executed.
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch,
|
||||
SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch,
|
||||
type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps,
|
||||
} from "@hm-design/sw"
|
||||
import { Eye, EyeOff, Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react"
|
||||
import { Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react"
|
||||
|
||||
export const SAMPLES = [
|
||||
{ id: "sample-basic", label: "기본 도면 · DWG", name: "BasicSample.dwg" },
|
||||
@@ -109,13 +109,15 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP
|
||||
const [query, setQuery] = useState("")
|
||||
const filtered = LAYERS.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase()))
|
||||
const set = (names: string[]) => demo.setHidden(new Set(names))
|
||||
// 2026-10-22: one switch for all layers. ON only when every layer is shown; a partial state reads OFF, so a click turns all on.
|
||||
const allVisible = demo.hidden.size === 0
|
||||
return (
|
||||
<SWViewerPanel title={`레이어 (${LAYERS.length}개)`} open={demo.panels.layers} onClose={() => demo.toggle("layers", false)} {...placement}>
|
||||
<SWViewerSection surface="plain" aria-label="레이어 목록">
|
||||
<div className="sw-viewer-story-toolbar">
|
||||
<SWViewerInput type="search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} />
|
||||
<SWViewerButton tone="green" onClick={() => set([])}>전체 켜기</SWViewerButton>
|
||||
<SWViewerButton tone="green" onClick={() => set(LAYERS.map(layer => layer.name))}>전체 끄기</SWViewerButton>
|
||||
<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible}
|
||||
onCheckedChange={next => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />}</SWViewerField>
|
||||
</div>
|
||||
{filtered.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : (
|
||||
<ul className="sw-viewer-story-list">
|
||||
@@ -126,13 +128,13 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP
|
||||
<span className="sw-viewer-story-swatch" style={{ background: layer.color }} aria-hidden="true" />
|
||||
<span className="sw-viewer-story-name" title={layer.name}>{layer.name}</span>
|
||||
<SWViewerBadge>{layer.count}</SWViewerBadge>
|
||||
<button type="button" className="sw-viewer-story-eye" aria-label={`${layer.name} ${visible ? "숨기기" : "표시하기"}`} title={visible ? "숨기기" : "표시하기"}
|
||||
<button type="button" className="sw-viewer-story-bulb" aria-label={`${layer.name} ${visible ? "숨기기" : "표시하기"}`} title={visible ? "숨기기" : "표시하기"}
|
||||
onClick={() => {
|
||||
const next = new Set(demo.hidden)
|
||||
if (visible) next.add(layer.name); else next.delete(layer.name)
|
||||
demo.setHidden(next)
|
||||
demo.record(`layer:${layer.name}=${!visible}`)
|
||||
}}>{visible ? <Eye size={16} /> : <EyeOff size={16} />}</button>
|
||||
}}><SWLayerVisibilityIcon on={visible} /></button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -8,16 +8,18 @@
|
||||
background: var(--sw-story-canvas);
|
||||
}
|
||||
.sw-viewer-story-room { padding-bottom: 320px; }
|
||||
.sw-viewer-story-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 12px 4px; }
|
||||
.sw-viewer-story-toolbar > input { flex: 1 0 100%; }
|
||||
/* Search, then the all-layers switch row (SWViewerField + SWViewerSwitch, 2026-10-22). */
|
||||
.sw-viewer-story-toolbar { display: flex; flex-wrap: wrap; gap: 12px 4px; }
|
||||
.sw-viewer-story-toolbar > * { flex: 1 0 100%; }
|
||||
.sw-viewer-story-list { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
||||
.sw-viewer-story-list li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 3px; }
|
||||
.sw-viewer-story-list li:hover { background: rgba(154, 146, 132, 0.25); }
|
||||
.sw-viewer-story-list li[data-visible="false"] { opacity: 0.45; }
|
||||
.sw-viewer-story-swatch { flex: 0 0 10px; width: 10px; height: 10px; border: 1px solid var(--hm-sw-viewer-panel-inner-border); border-radius: 9999px; }
|
||||
.sw-viewer-story-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, monospace; font-size: 12px; }
|
||||
.sw-viewer-story-eye { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; color: var(--hm-sw-viewer-panel-heading); cursor: pointer; }
|
||||
.sw-viewer-story-eye:hover { background: rgba(154, 146, 132, 0.3); }
|
||||
/* Visibility button around the public SWLayerVisibilityIcon (header bulb art, 20 x 20); 24 x 24 hit area kept. */
|
||||
.sw-viewer-story-bulb { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; }
|
||||
.sw-viewer-story-bulb:hover { background: rgba(154, 146, 132, 0.3); }
|
||||
.sw-viewer-story-props { margin: 0; }
|
||||
.sw-viewer-story-props > div { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--hm-sw-viewer-panel-inner-border); font-size: 12px; }
|
||||
.sw-viewer-story-props dt { color: var(--hm-sw-viewer-panel-label); }
|
||||
|
||||
Reference in new issue
Block a user