From 3a4c63b5a404a8c15a78bb97572426a2a381979f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Fri, 23 Oct 2026 11:05:42 +0900 Subject: [PATCH] fix(sw): align header trailing border gradients --- .../components/sw-viewer/SWViewerHeader.tsx | 4 +- packages/sw/src/theme/sw-viewer.css | 45 ++++++++++++++++--- packages/sw/src/theme/sw.css | 10 ++++- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx b/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx index a380c09..d065144 100644 --- a/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx +++ b/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx @@ -435,7 +435,9 @@ export const SWViewerHeader = forwardRef(funct if (owner) command({ id: entry.id, itemId: owner, value: entry.type === "checkbox" ? !entry.checked : undefined }) }} /> } - {(end.length > 0 || trailing) &&
{end.map(renderGroup)}{trailing}
} + {/* The empty strip-start mark (2026-10-23) holds the rounded object end drawn before the trailing strip + (sw-viewer.css), so the strip cells keep their own pseudo-elements for the strip outline. */} + {(end.length > 0 || trailing) &&
{end.map(renderGroup)}{trailing &&
} ) diff --git a/packages/sw/src/theme/sw-viewer.css b/packages/sw/src/theme/sw-viewer.css index 0b19439..5214b3b 100644 --- a/packages/sw/src/theme/sw-viewer.css +++ b/packages/sw/src/theme/sw-viewer.css @@ -439,13 +439,34 @@ button.hm-sw-viewer-header__brand { text-align: left; } /* Only the 1px line before a segment: top, bottom and right are the object surface border itself, so every segment (and the license button between them) has the same top and bottom edge (2026-10-21; no doubled lines). */ box-shadow: inset 1px 0 0 var(--hm-sw-header-document-border-color); + position: relative; +} +/* Strip outline (2026-10-23): the file-name gradient stroke (--hm-sw-header-document-border-gradient) over the object + border band above and below every segment, and right of the last one. Each segment's ::after is its own box with a + 1px transparent border whose border image is pushed out by exactly that border width (border-image-outset: 1), so it + lands on the object's 1px band at any zoom (both are rounded to device pixels the same way). The segments keep their + size and place; nothing is painted inside them, so content, hover, focus and clicks are unchanged. */ +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons)::after { + content: ""; + position: absolute; + inset: 0; + border: 0 solid transparent; + border-block-width: 1px; + border-image: var(--hm-sw-header-document-border-gradient) 1 / 1 / 1 0; + pointer-events: none; +} +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):last-child::after { + border-right-width: 1px; + border-image-outset: 1 1 1 0; } .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) { margin-left: calc(-1 * var(--hm-sw-viewer-gap)); } .hm-sw-viewer-header__actions > .hm-sw-viewer-status { padding-inline: 12px; } /* Icon buttons in the strip (e.g. 정보 / license, 2026-10-21 final): 32px wide, full strip height, the strip face but no line around the button in any state (no border, no side / top / bottom lines). Keyboard focus keeps its outline. */ .hm-sw-viewer-header .hm-sw-viewer-header__actions > .hm-sw-viewer-header__button { - border-color: transparent; + /* 2026-10-23: no border box at all (was a transparent 1px border): the strip outline (::after, inset 0) is placed from + the padding box, so a button border moved it inward and drew a line around the button. Same 32px box. */ + border-width: 0; /* No line at all on the button (2026-10-21 final): same 48px box and edges as minimize / close, the strip face only. */ box-shadow: none; } @@ -454,9 +475,19 @@ button.hm-sw-viewer-header__brand { text-align: left; } border band, the header background (gap + the corners outside the radius) and on top the surface end: the same object face (a vertical gradient, so the copy lines up) and border with the right radius. The 8px it covers are the empty group padding + gap; below 1180px (2px each) the segment keeps 4px more room. Heights are unchanged. */ -.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) { position: relative; } -.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::before, -.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::after { +/* 2026-10-23: the first segment's left side is the strip's outer gradient line, painted inside it (a 1px layer in place + of the flat divider, so its width is unchanged). */ +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) { + background: var(--hm-sw-header-document-border-gradient) left top / 1px 100% no-repeat border-box, var(--hm-sw-header-document-background); + box-shadow: none; +} +/* The empty strip-start mark (SWViewerHeader, right before `trailing`) carries the surface end, so the first segment + keeps its ::after for the strip outline. It is a 0-wide stretch item at the segment's left edge (its negative margin + takes back the actions gap it adds), so the end keeps the exact place, size and 1px-outset height it had as the + first segment's ::before / ::after. */ +.hm-sw-viewer-header__strip-start { position: relative; flex: 0 0 0; align-self: stretch; width: 0; margin-right: calc(-1 * var(--hm-sw-viewer-gap)); pointer-events: none; } +.hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::before, +.hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::after { content: ""; position: absolute; top: calc(-1 * var(--hm-sw-header-object-border-width, 1px)); @@ -464,11 +495,11 @@ button.hm-sw-viewer-header__brand { text-align: left; } left: calc(-1 * (var(--hm-sw-header-object-radius) + 2px)); pointer-events: none; } -.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::before { +.hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::before { width: calc(var(--hm-sw-header-object-radius) + 2px); background: var(--hm-sw-viewer-background); } -.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::after { +.hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::after { box-sizing: border-box; width: var(--hm-sw-header-object-radius); /* The object surface end: its face and gradient stroke, spanning the same 1px-outset height so both line up. */ @@ -485,6 +516,8 @@ button.hm-sw-viewer-header__brand { text-align: left; } .hm-sw-viewer-header__object { gap: 2px; padding-inline: 2px; } .hm-sw-viewer-header__group { gap: 2px; padding-inline: 2px; } .hm-sw-viewer-header__actions { padding-left: 2px; gap: 2px; } + /* The strip-start mark stays at the first segment's left edge: the 2px gap + its 4px margin below. */ + .hm-sw-viewer-header__strip-start { margin-right: -6px; } .hm-sw-viewer-status { padding-inline: 2px; } .hm-sw-viewer-header__actions > .hm-sw-viewer-status { padding-inline: 8px; } .hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) { margin-left: 4px; } diff --git a/packages/sw/src/theme/sw.css b/packages/sw/src/theme/sw.css index b7c56d0..bcc23d1 100644 --- a/packages/sw/src/theme/sw.css +++ b/packages/sw/src/theme/sw.css @@ -1398,11 +1398,19 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l height: var(--hm-sw-header-bar-height); background: var(--hm-sw-header-document-background); } +/* Outer line of the window area (2026-10-23): the file-name gradient stroke (--hm-sw-header-document-border-gradient, + as .hm-sw-header__document-button) instead of the flat border color, drawn as a 1px ring (mask) so the face and + the buttons inside are untouched. */ .hm-sw-header__window::before { content: ""; position: absolute; inset: 0; - border: 1px solid var(--hm-sw-header-document-border-color); + border: 1px solid transparent; + background: var(--hm-sw-header-document-border-gradient) border-box; + -webkit-mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0); + mask-composite: exclude; pointer-events: none; } .hm-sw-header__actions {