style(sw): viewer popup typography, 전체 레이어 title and options layout
Deploy Storybook / deploy (push) Successful in 2m10s

- Property popup: row labels 14px 500, read-only values 14px in the panel text
  color, monospace removed; 일반 정보 / 기하 속성 groups 20px apart
  (--hm-sw-viewer-panel-spacing on the second section).
- Layer popup: 전체 레이어 is a plain SWViewerSection title (existing
  .hm-sw-viewer-section__title) with the all-layers switch on the same row;
  layer names 14px 500, no monospace.
- Options popup follows the user image: 표시 (LWDISPLAY), 표시 배율 with the
  px/mm unit on the input row, updated lineweight hints.
- Remove stray monospace font-family from SW Story CSS.
- VIEWER.md §4.1 / new §4.2, DESIGN_RULES §11, Example H aligned.
This commit is contained in:
shkim4 committed 2026-10-22 16:56:22 +09:00
1 parent da0e647b49
commit 84d70f5cdf
9 files changed
+41 -22

No files matched your search

+2 -2
View File
@@ -160,8 +160,8 @@
| Dropdown 항목 | 면 `--hm-sw-viewer-menu-highlight`, 글자 `--hm-sw-viewer-menu-highlight-text`(Focus·Hover·서브메뉴 열림), Disabled 0.5 | — |
| 상태 표시 | `SWViewerStatus`: 글자 `--hm-sw-viewer-muted` 11px 400, `error`는 `--hm-sw-viewer-accent`, `loading` 아이콘 회전 | — |
- **Typography(뷰어):** Header 항목 14px 500, 캡션·개수·상태 11px 400, 패널 필드 라벨 14px 500, Section 제목 12px 700, 패널 버튼·Badge 12px 500, Input 12px. Font Weight는 400/500/700만 씁니다.
- **레이어 Popup 가시성(2026-10-22 필수, 10-21 눈 아이콘 규정 대체):** SW Viewer 레이어 Popup에서 레이어별 표시·숨김은 SW Header 레이어 메뉴와 같은 전구 아이콘(`SWLayerVisibilityIcon`, 20 × 20, 표시 = 색 있는 전구 / 숨김 = 회색) 버튼을 사용한다. 버튼 24 × 24 · radius 4 · Hover 면 `rgba(154, 146, 132, .3)` · `cursor: pointer`, 숨긴 행 불투명도 0.45. 전체 레이어 켜기·끄기는 Popup 상단 `SWViewerField` + `SWViewerSwitch` 하나(전체 표시일 때만 ON). 레이어별 표시·숨김은 Switch로 대체하지 않는다([VIEWER.md](./VIEWER.md) §4.1).
- **Typography(뷰어):** Header 항목 14px 500, 캡션·개수·상태 11px 400, 패널 필드 라벨 14px 500, Section 제목 12px 700, 패널 버튼·Badge 12px 500, Input 12px. 패널 콘텐츠(2026-10-22): 레이어 이름·속성 행 Label 14px 500, 읽기 전용 속성값 14px(패널 기본 Weight), 설명·Input·Select 값·Placeholder 12px, 별도 `font-family` 지정 없음([VIEWER.md](./VIEWER.md) §4.2). Font Weight는 400/500/700만 씁니다.
- **레이어 Popup 가시성(2026-10-22 필수, 10-21 눈 아이콘 규정 대체):** SW Viewer 레이어 Popup에서 레이어별 표시·숨김은 SW Header 레이어 메뉴와 같은 전구 아이콘(`SWLayerVisibilityIcon`, 20 × 20, 표시 = 색 있는 전구 / 숨김 = 회색) 버튼을 사용한다. 버튼 24 × 24 · radius 4 · Hover 면 `rgba(154, 146, 132, .3)` · `cursor: pointer`, 숨긴 행 불투명도 0.45. 전체 레이어 켜기·끄기는 Popup 상단 `SWViewerSection title="전체 레이어"`(기존 Section 제목) + 같은 줄의 `SWViewerSwitch` 하나(전체 표시일 때만 ON). 레이어별 표시·숨김은 Switch로 대체하지 않는다([VIEWER.md](./VIEWER.md) §4.1).
- **뷰어 신규 컴포넌트 체크리스트:** ① VIEWER.md·카탈로그 §9 컴포넌트로 해결되는지 먼저 본다 ② 위 토큰을 `var(--…)`로만 읽고 일반 모드 전용 모양(§8 `SWPopupRow`, §9 `SWToolButton`)을 섞지 않는다 ③ 상태는 위 표대로 ④ 토큰에 없는 값은 결과 보고에 "AI 구성 값 — 디자인 확인 필요"로 적는다 ⑤ `hm-sw-viewer-*` class를 쓰거나 덮어쓰지 않는다.
## 12. 로고 클릭 메뉴 (`SWHeader homeMenu` / `SWHomeMenu`, Figma 26:10591) — SW 일반 모드 전용
+8 -1
View File
@@ -105,9 +105,16 @@
- 전구 아이콘 버튼: 프로그램의 `<button type="button">` 24 × 24, radius 4, Hover 면 `rgba(154, 146, 132, .3)`, `cursor: pointer` 안에 `<SWLayerVisibilityIcon on={visible} />`(20 × 20, 장식용). 표시 중이면 색 있는 전구, 숨김이면 회색 전구(Header 레이어 메뉴와 같은 그림). 숨긴 행은 불투명도 0.45. 아이콘을 직접 그리거나 `hm-sw-header__*` class를 붙이지 않습니다.
- 접근성 이름에 다음 동작을 넣습니다: 표시 중 `"<레이어> 숨기기"`, 숨김 `"<레이어> 표시하기"`(`title`도 같은 동사).
- 클릭하면 그 레이어 하나의 가시성만 바꾸고 프로그램의 가시성 Callback·상태를 그대로 호출합니다. 아이콘만 그리고 기능을 빼지 않습니다.
- 전체 레이어 Switch: `<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible} onCheckedChange={setAll} />}</SWViewerField>`. `checked`는 모든 레이어가 표시 중일 때만 `true`이고, 일부만 표시 중이면 `false`로 보이며 누르면 전체 표시가 됩니다. ON → 전체 표시, OFF → 전체 숨김. 별도 상태를 두지 않고 레이어 가시성 상태에서 계산하므로 개별 전환 시 자동으로 맞춰집니다. 이전의 `전체 켜기`·`전체 끄기` 버튼 두 개는 쓰지 않습니다.
- 전체 레이어 Switch(2026-10-22 제목 형식): `<SWViewerSection surface="plain" title="전체 레이어" className="app-…"><SWViewerSwitch aria-label="전체 레이어" checked={allVisible} onCheckedChange={setAll} /></SWViewerSection>`. 제목은 기존 Section 제목(`.hm-sw-viewer-section__title`, 12px 700)을 그대로 쓰고 별도 제목 CSS·Prop을 만들지 않습니다. 제목과 Switch를 한 줄에 두는 것은 소비 앱 자기 class(예: `section.app-viewer-all-layers { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px }`, 예제 `example-layout.css`)로 하고 `hm-sw-viewer-*` class는 덮지 않습니다. `checked`는 모든 레이어가 표시 중일 때만 `true`이고, 일부만 표시 중이면 `false`로 보이며 누르면 전체 표시가 됩니다. ON → 전체 표시, OFF → 전체 숨김. 별도 상태를 두지 않고 레이어 가시성 상태에서 계산하므로 개별 전환 시 자동으로 맞춰집니다. 이전의 `전체 켜기`·`전체 끄기` 버튼 두 개는 쓰지 않습니다.
- 그 밖의 `SWViewerSwitch`는 옵션 패널의 ON/OFF 설정(그리드·2.5D 등)에 씁니다.
### 4.2 패널 콘텐츠 글자 기준 (2026-10-22)
- 일반 콘텐츠(레이어 이름, 속성 행 Label)·`SWViewerField` Label: 14px 500. 읽기 전용 속성값은 14px, Weight는 패널 기본(400), 색 `--hm-sw-viewer-panel-text`(검정 계열). 속성 행 Label 색은 `--hm-sw-viewer-panel-label`.
- 설명·안내(`SWViewerHint`, 빈 상태 문구)·Input 값·`SWSelect` 선택값·Placeholder·Badge: 12px(기존 Weight 유지). Section 제목 12px 700, 패널 제목 14px 700은 그대로입니다.
- 글꼴은 패널이 상속하는 `--hm-sw-header-font-family`(Noto Sans KR)만 씁니다. 목록·값에 `ui-monospace` 등 다른 `font-family`를 따로 지정하지 않습니다.
- 속성 패널의 그룹(예: 일반 정보 / 기하 속성)을 더 떨어뜨릴 때는 두 번째 Section에 자기 class로 `margin-top: var(--hm-sw-viewer-panel-spacing)`(12px, 본문 간격 8px과 합쳐 20px)을 줍니다. 패널 본문 공통 간격은 바꾸지 않습니다.
## 5. 올바른 사용
```tsx
@@ -77,7 +77,9 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
<SWViewerPanel title="레이어" open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
<SWViewerSection surface="plain" aria-label="레이어 목록">
{/* Top: all-layers SWViewerSwitch. Rows: swatch · name · count · bulb button (SWLayerVisibilityIcon), not a switch. */}
<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible} onCheckedChange={setAllLayers} />}</SWViewerField>
<SWViewerSection surface="plain" title="전체 레이어" className="app-viewer-all-layers">
<SWViewerSwitch aria-label="전체 레이어" checked={allVisible} onCheckedChange={setAllLayers} />
</SWViewerSection>
<ul className="app-viewer-layers">
{layers.map(layer => {
const visible = !hiddenLayers.has(layer.name)
+4 -2
View File
@@ -36,12 +36,14 @@
}
/* Viewer layer list (example H), the same look as Storybook SW/Viewer's layer popup. Panel tokens only.
Layer visibility is a 24 x 24 button around SWLayerVisibilityIcon (header bulb), never a switch or checkbox;
the all-layers control above the list is SWViewerField + SWViewerSwitch. */
the all-layers control above the list is SWViewerSection title="전체 레이어" + SWViewerSwitch on one row. */
.app-viewer-layers { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
.app-viewer-layers > li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 3px; }
.app-viewer-layers > li:hover { background: rgba(154, 146, 132, 0.25); }
.app-viewer-layers > li[data-visible="false"] { opacity: 0.45; }
/* 전체 레이어: the plain SWViewerSection laid out as one row — title (.hm-sw-viewer-section__title) left, switch right. */
section.app-viewer-all-layers { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px; }
.app-viewer-layer-swatch { flex: 0 0 10px; width: 10px; height: 10px; border: 1px solid var(--hm-sw-viewer-panel-inner-border); border-radius: 9999px; }
.app-viewer-layer-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, monospace; font-size: 12px; }
.app-viewer-layer-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 14px; font-weight: 500; }
.app-viewer-layer-bulb { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: none; cursor: pointer; }
.app-viewer-layer-bulb:hover { background: rgba(154, 146, 132, 0.3); }
+1 -1
View File
@@ -24,7 +24,7 @@
.sw-footer-story-guide-frame { display: block; width: 194px; height: 111px; background-repeat: no-repeat; background-size: 250px 1179px; }
.sw-footer-story-guide-text { display: grid; place-items: center; height: 100%; padding: 8px; font-size: 12px; color: #d4cbbd; text-align: center; }
.sw-footer-story-lucide { display: grid; place-items: center; }
.sw-footer-story-status { padding: 0 12px; font-size: 12px; color: #d4cbbd; font-family: ui-monospace, monospace; }
.sw-footer-story-status { padding: 0 12px; font-size: 12px; color: #d4cbbd; }
.sw-footer-story-row { display: flex; align-items: flex-start; gap: 16px; }
.sw-footer-story-row--tight { gap: 0; }
.sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; }
+1 -1
View File
@@ -7,7 +7,7 @@
.sw-home-menu-story__single { display: flex; height: 46px; }
.sw-home-menu-story__toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.sw-home-menu-story__toolbar > button { padding: 4px 8px; border: 1px solid #999; border-radius: 4px; background: #fff; font: inherit; font-size: 13px; cursor: pointer; }
.sw-home-menu-story__log { margin: 12px 0 0; padding-left: 20px; font-family: monospace; font-size: 12px; }
.sw-home-menu-story__log { margin: 12px 0 0; padding-left: 20px; font-size: 12px; }
.sw-home-menu-story__tall .sw-home-menu-story__frame { height: 860px; }
/* 2026-10-21 Storybook simplification (SW pages): no event log or long descriptions on screen; the log stays in the DOM
+1 -1
View File
@@ -26,7 +26,7 @@
.sw-story-stages { display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 16px; }
.sw-story-stage { margin: 0; }
.sw-story-stage figcaption { margin-top: 6px; font-size: 13px; color: #3d4a45; }
.sw-story-layer-state { margin: 0 0 8px; font-size: 12px; color: #3d4a45; font-family: ui-monospace, monospace; white-space: pre-wrap; }
.sw-story-layer-state { margin: 0 0 8px; font-size: 12px; color: #3d4a45; white-space: pre-wrap; }
.sw-story-dynamic { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.sw-story-property-room { min-height: 820px; }
/* Property panel states (2026-10-16): standalone panels at their natural size; the comparison wraps. */
+13 -9
View File
@@ -116,8 +116,11 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP
<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)} />
<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible}
onCheckedChange={next => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />}</SWViewerField>
{/* 2026-10-22: the section title (.hm-sw-viewer-section__title) names the switch row; the switch stays on that line. */}
<SWViewerSection surface="plain" title="전체 레이어" className="sw-viewer-story-all">
<SWViewerSwitch aria-label="전체 레이어" checked={allVisible}
onCheckedChange={next => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />
</SWViewerSection>
</div>
{filtered.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : (
<ul className="sw-viewer-story-list">
@@ -145,9 +148,9 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP
)
}
function SliderRow({ label, value, min, max, step, disabled, onChange }: { label: string; value: number; min: number; max: number; step: number; disabled?: boolean; onChange: (value: number) => void }) {
function SliderRow({ label, unit, value, min, max, step, disabled, onChange }: { label: string; unit?: string; value: number; min: number; max: number; step: number; disabled?: boolean; onChange: (value: number) => void }) {
return (
<SWViewerField label={label} layout="stacked" disabled={disabled}
<SWViewerField label={label} unit={unit} layout="stacked" disabled={disabled}
below={<SWViewerSlider aria-label={label} value={value} min={min} max={max} step={step} onValueChange={onChange} />}>
{id => <SWViewerInput id={id} type="number" min={min} max={max} step={step} value={value} onChange={event => { const next = Number(event.currentTarget.value); if (Number.isFinite(next)) onChange(next) }} />}
</SWViewerField>
@@ -162,10 +165,11 @@ export function OptionsPanel({ demo, footer, ...placement }: { demo: ViewerDemo;
headerActions={<SWViewerButton variant="ghost" aria-label="기본값으로" title="기본값으로" onClick={() => { setOptions(DEFAULT_OPTIONS); demo.record("options:reset") }}><RotateCcw size={16} /></SWViewerButton>}
footer={footer ? <><SWViewerButton onClick={() => demo.toggle("options", false)}>취소</SWViewerButton><SWViewerButton variant="primary" onClick={() => { demo.record("options:apply"); demo.toggle("options", false) }}>적용</SWViewerButton></> : undefined}>
<SWViewerSection title="선가중치">
<SWViewerField label="표시 사용">{id => <SWViewerSwitch id={id} checked={options.lineweightOn} onCheckedChange={lineweightOn => set({ lineweightOn })} />}</SWViewerField>
<SliderRow label="표시 배율 (px/mm)" value={options.lineweightScale} min={2} max={60} step={0.5} onChange={lineweightScale => set({ lineweightScale })} />
<SWViewerHint>이 도면에는 굵은 선이 없습니다 (0.50mm 였다면 {(0.5 * options.lineweightScale).toFixed(1)}px)</SWViewerHint>
<SWViewerHint>AutoCAD의 "선가중치 설정 → 표시 배율"과 동일합니다.</SWViewerHint>
{/* 2026-10-22 options image: LWDISPLAY toggle; scale input + px/mm unit on the label row, slider below. */}
<SWViewerField label="표시 (LWDISPLAY)">{id => <SWViewerSwitch id={id} checked={options.lineweightOn} onCheckedChange={lineweightOn => set({ lineweightOn })} />}</SWViewerField>
<SliderRow label="표시 배율" unit="px/mm" value={options.lineweightScale} min={2} max={60} step={0.5} onChange={lineweightScale => set({ lineweightScale })} />
<SWViewerHint>이 도면에는 굵은 선이 없습니다 (0.50mm였다면 {(0.5 * options.lineweightScale).toFixed(1)}px)</SWViewerHint>
<SWViewerHint>AutoCAD "선가중치 설정 → 표시 배율"과 같습니다. 모델 공간의 선가중치는 줌과 무관한 화면 픽셀로 그려지므로 화면 DPI에 맞춰 조정합니다.</SWViewerHint>
</SWViewerSection>
<SWViewerSection title="표시">
<SWViewerField label="그리드">{id => <SWViewerSwitch id={id} checked={options.grid} onCheckedChange={grid => set({ grid })} />}</SWViewerField>
@@ -192,7 +196,7 @@ export function PropertiesPanel({ demo, selected = true, ...placement }: { demo:
headerActions={selected ? <SWViewerButton variant="ghost" aria-label="선택 확대" title="선택 확대" onClick={() => demo.record("zoomToSelection (미연결)")}><ZoomIn size={16} /></SWViewerButton> : undefined}>
{selected ? <>
<SWViewerSection surface="plain" title="일반 정보 (General)"><SWViewerBadge>LINE · h=2F1A</SWViewerBadge>{rows(ENTITY_ROWS)}</SWViewerSection>
<SWViewerSection surface="plain" title="기하 속성 (Geometry)">{rows(GEOMETRY_ROWS)}</SWViewerSection>
<SWViewerSection surface="plain" title="기하 속성 (Geometry)" className="sw-viewer-story-props-next">{rows(GEOMETRY_ROWS)}</SWViewerSection>
</> : (
<div className="sw-viewer-story-empty"><MousePointerClick size={24} aria-hidden="true" />도면에서 객체(엔티티)를 클릭하면<br />상세 속성이 표시됩니다</div>
)}
+8 -4
View File
@@ -11,19 +11,23 @@
/* 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%; }
/* 전체 레이어: the plain SWViewerSection laid out as one row — its title on the left, the switch on the right. */
.sw-viewer-story-toolbar > .sw-viewer-story-all { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px; }
.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-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 14px; font-weight: 500; }
/* 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); }
.sw-viewer-story-props dd { margin: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, monospace; user-select: all; }
/* Property groups read as separate blocks: the panel spacing token on top of the shared 8px body gap (20px in all). */
.sw-viewer-story-props-next { margin-top: var(--hm-sw-viewer-panel-spacing); }
.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: 14px; }
.sw-viewer-story-props dt { color: var(--hm-sw-viewer-panel-label); font-weight: 500; }
.sw-viewer-story-props dd { margin: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--hm-sw-viewer-panel-text); user-select: all; }
.sw-viewer-story-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 40px 0; text-align: center; font-size: 12px; color: var(--hm-sw-viewer-panel-label); }
.sw-viewer-story-steps { margin: 0; padding-left: 20px; font-size: 14px; }
.sw-viewer-story-reopen { position: absolute; left: 712px; top: 16px; padding: 6px 10px; border-radius: 4px; background: #f5f0e5; font-size: 12px; }