fix(sw): align header trailing border gradients
Deploy Storybook / deploy (push) Successful in 2m18s

This commit is contained in:
shkim4 committed 2026-10-23 11:05:42 +09:00
1 parent 1916146bd4
commit 3a4c63b5a4
3 files changed
+51 -8

No files matched your search

@@ -435,7 +435,9 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
if (owner) command({ id: entry.id, itemId: owner, value: entry.type === "checkbox" ? !entry.checked : undefined })
}} />
</>}
{(end.length > 0 || trailing) && <div className="hm-sw-viewer-header__actions">{end.map(renderGroup)}{trailing}</div>}
{/* 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) && <div className="hm-sw-viewer-header__actions">{end.map(renderGroup)}{trailing && <span className="hm-sw-viewer-header__strip-start" aria-hidden="true" />}{trailing}</div>}
</div>
</header>
)
+39 -6
View File
@@ -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; }
+9 -1
View File
@@ -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 {