From 2f1edd47ebc7beb2babd2b80f79ab54bda191fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Thu, 22 Oct 2026 21:08:38 +0900 Subject: [PATCH] fix(sw): viewer popup inputs match the property window, header labels hide by group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viewer popup value inputs (number, text) take the property window field: transparent 1px border, radius 2, 7px inset, 14px 500 #E60403; hover / editing border, #0F0D0A editing face with the outlined value. A field unit (px/mm) sits inside the box and turns white on the editing face. Layer search, sizes and layout unchanged. Viewer header labels hide per iconOnlyBelow value instead of one by one: 레이어 / 설정 / 속성 together, 전체 보기 on its own; More collapse unchanged. Verified in Storybook: Settings Popup editing / unit / gauge sync; Responsive More at 1920 / 1440 / 1280 / ~1100 / 950, shrink-and-grow restore, no overflow. Not re-run: viewer browser tests. --- .../components/sw-viewer/SWViewerHeader.tsx | 21 +++++----- packages/sw/src/theme/sw-viewer.css | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx b/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx index f91344a..dc5c282 100644 --- a/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx +++ b/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx @@ -94,25 +94,24 @@ const isShown = (item: { id: string; hidden?: boolean }, collapsed: ReadonlySet< /** * 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. items with an `icon` and `iconOnlyBelow` show their icon only one by one - * (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. + * order once the room a step freed is back: 1. items with an `icon` and `iconOnlyBelow` show their icon only, the items + * sharing one value together (label kept as tooltip and accessible name), the higher value first; 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 and the sets. */ type Step = { kind: "icons"; ids: string[] } | { kind: "collapse"; id: string } -// 2026-10-22 (user request): labels hide one item at a time (not all at once), so a label with room stays (e.g. 전체 보기 -// at 1440 / 1280): the higher `iconOnlyBelow` first, the rightmost first for the same value; extra icons (Open) first. +// 2026-10-22 (user request, replaces the one-by-one labels of 83405c4): one icon step per `iconOnlyBelow` value, so e.g. +// 레이어 / 설정 / 속성 (900) hide and come back together and 전체 보기 (560) on its own; extra icons (Open) first. function responsiveSteps(groups: SWViewerHeaderGroup[], extraIcons: string[] = []): Step[] { - const icons: { id: string; rank: number; order: number }[] = extraIcons.map((id, order) => ({ id, rank: Infinity, order })) + const icons = new Map(extraIcons.length ? [[Infinity, [...extraIcons]]] : []) const 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({ id: item.id, rank: item.iconOnlyBelow, order: order + extraIcons.length }) + if (item.type !== "custom" && item.icon && !item.iconOnly && item.iconOnlyBelow !== undefined) icons.set(item.iconOnlyBelow, [...(icons.get(item.iconOnlyBelow) ?? []), item.id]) if (item.collapseBelow !== undefined) collapse.push({ id: item.id, rank: item.collapseBelow, order }) }) - const byRank = (a: { rank: number; order: number }, b: { rank: number; order: number }) => b.rank - a.rank || b.order - a.order - icons.sort(byRank) - collapse.sort(byRank) - return [...icons.map(item => ({ kind: "icons" as const, ids: [item.id] })), ...collapse.map(item => ({ kind: "collapse" as const, id: item.id }))] + collapse.sort((a, b) => b.rank - a.rank || b.order - a.order) + return [...[...icons].sort(([a], [b]) => b - a).map(([, ids]) => ({ kind: "icons" as const, ids })), ...collapse.map(item => ({ kind: "collapse" as const, id: item.id }))] } /** diff --git a/packages/sw/src/theme/sw-viewer.css b/packages/sw/src/theme/sw-viewer.css index 06ae8e9..1f5de05 100644 --- a/packages/sw/src/theme/sw-viewer.css +++ b/packages/sw/src/theme/sw-viewer.css @@ -634,6 +634,48 @@ button.hm-sw-viewer-header__brand { text-align: left; } } .hm-sw-theme .hm-sw-viewer-input:focus-visible { outline: 2px solid var(--hm-sw-viewer-panel-switch-on); outline-offset: -1px; } .hm-sw-theme .hm-sw-viewer-input:disabled { opacity: 0.5; } +/* 2026-10-22 (user request): value inputs (number, text such as 하이퍼링크) take the property window field + (SW/Popup › Property Panel): transparent 1px border, radius 2, 7px inset, 14px 500 #E60403 value; Hover / + editing rgba(33,29,19,.4) border, editing #0F0D0A with the black text outline. Width / height and the layer + search box are unchanged. Read-only values keep the label color and the keyboard focus ring. */ +.hm-sw-theme .hm-sw-viewer-input:not([type="search"]) { + padding: 0 7px; + border-color: transparent; + border-radius: 2px; + background: transparent; + color: var(--hm-sw-header-input-color); + caret-color: var(--hm-sw-header-input-color); + font-size: 14px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.02em; + appearance: textfield; +} +.hm-sw-theme .hm-sw-viewer-input:not([type="search"])::-webkit-inner-spin-button, +.hm-sw-theme .hm-sw-viewer-input:not([type="search"])::-webkit-outer-spin-button { appearance: none; margin: 0; } +.hm-sw-theme .hm-sw-viewer-input:not([type="search"]):is(:hover:not(:disabled, :read-only), :focus:not(:read-only)) { border-color: var(--hm-sw-property-field-hover-border-color); } +.hm-sw-theme .hm-sw-viewer-input:not([type="search"]):focus:not(:read-only) { outline: none; background: var(--hm-sw-property-field-editing-background); text-shadow: var(--hm-sw-header-text-outline); } +/* A field unit (px/mm) sits inside the value box like the property window unit: the control row is the box (same + 80px for the input alone) and the editing face covers the unit, which turns #FFF on it. */ +.hm-sw-theme .hm-sw-viewer-field__control:has(> .hm-sw-viewer-input:not([type="search"]) + .hm-sw-viewer-field__unit) { + box-sizing: border-box; + height: 28px; + padding: 0 7px; + border: 1px solid transparent; + border-radius: 2px; +} +.hm-sw-theme .hm-sw-viewer-field__control:has(> .hm-sw-viewer-field__unit) > .hm-sw-viewer-input:not([type="search"]) { + width: 64px; + height: 26px; + padding: 0; + border: 0; + background: transparent; +} +.hm-sw-theme .hm-sw-viewer-field__control:has(> .hm-sw-viewer-input:not([type="search"], :disabled, :read-only)):is(:hover, :focus-within):has(> .hm-sw-viewer-field__unit) { border-color: var(--hm-sw-property-field-hover-border-color); } +.hm-sw-theme .hm-sw-viewer-field__control:has(> .hm-sw-viewer-input:not([type="search"], :read-only):focus):has(> .hm-sw-viewer-field__unit) { background: var(--hm-sw-property-field-editing-background); } +.hm-sw-theme .hm-sw-viewer-field__control:has(> .hm-sw-viewer-input:not([type="search"], :read-only):focus) > .hm-sw-viewer-field__unit { color: var(--hm-sw-property-field-editing-unit-color); } +.hm-sw-theme .hm-sw-viewer-input:not([type="search"]):read-only { color: var(--hm-sw-property-label); } +.hm-sw-theme .hm-sw-viewer-input:not([type="search"]):read-only:focus-visible { outline: 2px solid var(--hm-sw-property-border); outline-offset: -2px; } /* SWViewerSlider = SWSlider appearance="panel" (sw.css, 2026-10-20): the shared gauge rules painted with the viewer panel tokens, so the viewer look and its theme overrides are unchanged. */