|
106 | 106 | handleContextMenu(e, tab.id) |
107 | 107 | }} |
108 | 108 | > |
| 109 | + {#if isActive} |
| 110 | + <!-- Chrome-style "shoulders": small concave quarter- |
| 111 | + circle wedges that stick out past the active tab's |
| 112 | + bottom corners, carving a smooth rounded notch into |
| 113 | + the adjacent inactive tabs. They share the active |
| 114 | + tab's bg color so the tab reads as "flowing into" |
| 115 | + the path bar surface below. --> |
| 116 | + <span class="tab-shoulder tab-shoulder-left" aria-hidden="true"></span> |
| 117 | + <span class="tab-shoulder tab-shoulder-right" aria-hidden="true"></span> |
| 118 | + {/if} |
109 | 119 | {#if tab.unreachable} |
110 | 120 | <span class="warning-icon" use:tooltip={'Unreachable'} aria-label="Unreachable"> |
111 | 121 | <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> |
|
161 | 171 | height: var(--spacing-tab-bar-height); |
162 | 172 | min-height: var(--spacing-tab-bar-height); |
163 | 173 | max-height: var(--spacing-tab-bar-height); |
164 | | - background-color: var(--color-bg-secondary); |
| 174 | + /* Bar bg matches the inactive tabs so adjacent inactive tabs blend |
| 175 | + into the bar around them. The active tab (`bg-secondary`, same |
| 176 | + as the col header / path bar below) is the only contrasting |
| 177 | + surface in the strip. Side-effect: the active tab's |
| 178 | + `bg-secondary` no longer stacks on a second `bg-secondary` layer |
| 179 | + from the bar, so its effective opacity now matches the path bar |
| 180 | + and col header exactly. */ |
| 181 | + background-color: var(--color-bg-tab-inactive); |
165 | 182 | padding: 0 var(--spacing-xxs); |
166 | | - overflow: hidden; |
| 183 | + /* Need `overflow: visible` so the active-tab shoulders can extend |
| 184 | + outside the tab into the surrounding gap + inactive-tab area. */ |
| 185 | + overflow: visible; |
167 | 186 | } |
168 | 187 |
|
169 | 188 | .tab-list { |
170 | 189 | display: flex; |
171 | 190 | flex: 1; |
172 | 191 | min-width: 0; |
173 | 192 | align-items: end; |
174 | | - overflow: hidden; |
| 193 | + /* `overflow: visible` so the active-tab shoulders can extend |
| 194 | + outside the list into the surrounding (gap + inactive-tab) |
| 195 | + area without being clipped. */ |
| 196 | + overflow: visible; |
| 197 | + /* 5 px gap = 2 px margin + 1 px separator + 2 px margin between |
| 198 | + adjacent tabs. The `.tab::before` separator (above) sits inside |
| 199 | + this gap at `left: -3px`. */ |
175 | 200 | /* stylelint-disable-next-line declaration-property-value-disallowed-list */ |
176 | | - gap: 1px; |
| 201 | + gap: 5px; |
177 | 202 | } |
178 | 203 |
|
179 | 204 | .tab { |
|
192 | 217 | padding: 0 var(--spacing-sm); |
193 | 218 | border: none; |
194 | 219 | border-radius: var(--radius-sm) var(--radius-sm) 0 0; |
195 | | - background-color: transparent; |
| 220 | + /* Inactive tabs get a distinct "recessed" bg: slightly darker than |
| 221 | + `--color-bg-secondary` in light mode, slightly lighter in dark |
| 222 | + mode. `.tab.active` below overrides this with `bg-secondary` so |
| 223 | + the selected tab merges with the path bar. */ |
| 224 | + background-color: var(--color-bg-tab-inactive); |
196 | 225 | color: var(--color-text-secondary); |
197 | 226 | font-size: var(--font-size-sm); |
198 | 227 | font-family: var(--font-system); |
|
205 | 234 | color var(--transition-fast); |
206 | 235 | } |
207 | 236 |
|
208 | | - /* Subtle separator between inactive tabs, hidden next to the active tab */ |
| 237 | + /* Faint hairline separator between adjacent inactive tabs (skipped |
| 238 | + next to the active tab and at the leftmost edge of the list). |
| 239 | + Sits at the tab's left edge, ~70 % of tab height, with ~2 px of |
| 240 | + breathing room above and below thanks to top/bottom 15 %. The |
| 241 | + `tab-list { gap: 5px }` rule below leaves enough room around the |
| 242 | + separator (1 px wide line + 2 px margin on each side). */ |
209 | 243 | .tab:not(.active, .before-active, .after-active, :first-child)::before { |
210 | 244 | content: ''; |
211 | 245 | position: absolute; |
212 | | - left: -1px; |
213 | | - top: 5px; |
214 | | - bottom: 5px; |
| 246 | + left: -3px; |
| 247 | + top: 15%; |
| 248 | + bottom: 15%; |
215 | 249 | width: 1px; |
216 | | - background-color: var(--color-border-subtle); |
| 250 | + background-color: var(--color-tab-separator); |
217 | 251 | } |
218 | 252 |
|
219 | 253 | .tab.active { |
220 | | - background-color: color-mix(in oklch, var(--color-bg-primary), var(--color-accent) 4%); |
| 254 | + /* Same bg as the path bar below (`--color-bg-secondary`), so the |
| 255 | + active tab visually merges with the chrome row underneath. The |
| 256 | + accent line at the top + the rounded "shoulder" elements at the |
| 257 | + bottom are what make it read as "tab, not gap". */ |
| 258 | + background-color: var(--color-bg-secondary); |
221 | 259 | color: var(--color-text-primary); |
222 | 260 | font-weight: 500; |
223 | | - /* Bar height + 1px so the active tab covers the tab-bar bottom border; |
224 | | - * the extra px hangs below via `margin-bottom: -1px`. */ |
| 261 | + /* Bar height + 1px so the active tab covers any seam with the |
| 262 | + path bar; the extra px hangs below via `margin-bottom: -1px`. */ |
225 | 263 | height: calc(var(--spacing-tab-bar-height) + 1px); |
226 | 264 | /* stylelint-disable-next-line declaration-property-value-disallowed-list */ |
227 | 265 | margin-bottom: -1px; |
228 | 266 | z-index: 1; |
229 | | - box-shadow: 0 0 4px color-mix(in srgb, black, transparent 96%); |
| 267 | + /* Let the Chrome-style shoulders extend past the tab's left/right |
| 268 | + edges into the surrounding inactive-tab area. */ |
| 269 | + overflow: visible; |
230 | 270 | } |
231 | 271 |
|
232 | 272 | /* Accent top border on active tab */ |
|
242 | 282 | border-radius: 1px 1px 0 0; |
243 | 283 | } |
244 | 284 |
|
245 | | - @media (prefers-color-scheme: dark) { |
246 | | - .tab.active { |
247 | | - background-color: color-mix(in oklch, var(--color-bg-primary), var(--color-accent) 7%); |
248 | | - box-shadow: 0 0 4px color-mix(in srgb, black, transparent 85%); |
249 | | - } |
| 285 | + /* Chrome-style "shoulders": pseudo-elements that stick out past the |
| 286 | + active tab's bottom-left and bottom-right corners with the same bg |
| 287 | + as the tab, then mask out a quarter-circle so the visible shape is |
| 288 | + a concave curve. This carves a smooth rounded notch out of the |
| 289 | + adjacent inactive tab's surface — the active tab reads as "flowing |
| 290 | + into" the path bar below while the inactive tabs around it bend |
| 291 | + away. Won't be visible when there's no adjacent inactive tab |
| 292 | + (`:first-child` / `:last-child` ends of the tab list), which is |
| 293 | + fine — the bar's own bg matches the active tab anyway. */ |
| 294 | + .tab-shoulder { |
| 295 | + position: absolute; |
| 296 | + /* Explicit `top: auto` so `bottom: 0` is the only vertical anchor — |
| 297 | + inherited cascade (or a future stacking-context tweak) can't shove |
| 298 | + these to the top accidentally. */ |
| 299 | + top: auto; |
| 300 | + bottom: 0; |
| 301 | + width: 8px; |
| 302 | + height: 8px; |
| 303 | + background-color: var(--color-bg-secondary); |
| 304 | + pointer-events: none; |
| 305 | + } |
| 306 | +
|
| 307 | + /* Left shoulder: 8 × 8 box that sticks out 8 px to the left of the |
| 308 | + active tab's bottom-left corner. The mask keeps only the |
| 309 | + *bottom-right* curved triangle of the box visible (= the area |
| 310 | + closest to the tab); the rest is transparent. That visible chunk |
| 311 | + forms a smooth convex bulge extending the active tab's |
| 312 | + bottom-left corner outward into the adjacent inactive tab's |
| 313 | + surface. */ |
| 314 | + .tab-shoulder-left { |
| 315 | + left: -8px; |
| 316 | + /* `transparent` inside the top-left quarter-disc (away from tab), |
| 317 | + `black` outside (= near tab → opaque, visible). */ |
| 318 | + /* stylelint-disable-next-line declaration-property-value-disallowed-list -- mask uses raw px in radial-gradient args */ |
| 319 | + mask-image: radial-gradient(circle at top left, transparent 8px, black 8px); |
| 320 | + /* stylelint-disable-next-line declaration-property-value-disallowed-list -- vendor-prefixed mask, WKWebView fallback */ |
| 321 | + -webkit-mask-image: radial-gradient(circle at top left, transparent 8px, black 8px); |
| 322 | + } |
| 323 | +
|
| 324 | + /* Right shoulder: mirror image of the left — visible bottom-left |
| 325 | + curved triangle near the tab's bottom-right corner. */ |
| 326 | + .tab-shoulder-right { |
| 327 | + right: -8px; |
| 328 | + /* stylelint-disable-next-line declaration-property-value-disallowed-list -- mask uses raw px in radial-gradient args */ |
| 329 | + mask-image: radial-gradient(circle at top right, transparent 8px, black 8px); |
| 330 | + /* stylelint-disable-next-line declaration-property-value-disallowed-list -- vendor-prefixed mask, WKWebView fallback */ |
| 331 | + -webkit-mask-image: radial-gradient(circle at top right, transparent 8px, black 8px); |
250 | 332 | } |
251 | 333 |
|
252 | 334 | .tab:hover:not(.active) { |
253 | | - background-color: color-mix(in srgb, var(--color-bg-tertiary), transparent 40%); |
| 335 | + background-color: color-mix(in srgb, var(--color-bg-secondary), white 8%); |
254 | 336 | color: var(--color-text-secondary); |
255 | 337 | } |
256 | 338 |
|
| 339 | + @media (prefers-color-scheme: dark) { |
| 340 | + .tab:hover:not(.active) { |
| 341 | + background-color: color-mix(in srgb, var(--color-bg-tab-inactive), white 6%); |
| 342 | + } |
| 343 | + } |
| 344 | +
|
257 | 345 | /* Hide separator when hovering an inactive tab */ |
258 | 346 | .tab:hover:not(.active)::before { |
259 | 347 | background-color: transparent; |
|
0 commit comments