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 }) 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> </div>
</header> </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 /* 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). */ (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); 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 > :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; } .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 /* 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. */ 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 { .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. */ /* 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; 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 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 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. */ 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; } /* 2026-10-23: the first segment's left side is the strip's outer gradient line, painted inside it (a 1px layer in place
.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, 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) + *)::after { .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: ""; content: "";
position: absolute; position: absolute;
top: calc(-1 * var(--hm-sw-header-object-border-width, 1px)); 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)); left: calc(-1 * (var(--hm-sw-header-object-radius) + 2px));
pointer-events: none; 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); width: calc(var(--hm-sw-header-object-radius) + 2px);
background: var(--hm-sw-viewer-background); 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; box-sizing: border-box;
width: var(--hm-sw-header-object-radius); 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. */ /* 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__object { gap: 2px; padding-inline: 2px; }
.hm-sw-viewer-header__group { 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; } .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-status { padding-inline: 2px; }
.hm-sw-viewer-header__actions > .hm-sw-viewer-status { padding-inline: 8px; } .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; } .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); height: var(--hm-sw-header-bar-height);
background: var(--hm-sw-header-document-background); 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 { .hm-sw-header__window::before {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; 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; pointer-events: none;
} }
.hm-sw-header__actions { .hm-sw-header__actions {