Skip to content

Commit dc7d650

Browse files
committed
UI: tab-bar redesign — distinct inactive tabs + Chrome-style active flange
- Active tab bg switched to `--color-bg-secondary` (matches the path bar / col header row below, so the active tab visually merges with the chrome) - Inactive tabs get their own `--color-bg-tab-inactive` bg: slightly darker than `bg-secondary` in light mode, slightly lighter in dark mode — reads as "recessed". `.tab-bar` bg also uses this same token so the empty area around tabs (and any gaps between adjacent tabs) blend with the inactive tabs - New `--color-tab-separator` token + `::before` hairline between adjacent inactive tabs (~70% of tab height, 2 px margin on each side; tab-list gap bumped to 5 px to leave room) - Chrome-style "shoulders": two `<span class="tab-shoulder">` elements rendered inside the active tab. Each is an 8 × 8 box sticking out past the tab's bottom corners, `bg-secondary`-filled, masked with a `radial-gradient` so the visible shape is a concave curve flaring outward from the tab. `.tab-bar` + `.tab-list` switched to `overflow: visible` so the shoulders can leave the tab box - `--color-bg-secondary` opacity bumped 0.5 → 0.78 (light) / 0.7 → 0.86 (dark) so the chrome row reads at the same translucency density as the file pane — strip-vs-pane distinction is now in tint, not in opacity
1 parent 91c31f3 commit dc7d650

2 files changed

Lines changed: 130 additions & 24 deletions

File tree

apps/desktop/src/app.css

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,22 @@
1919
contrast against the underlying material still clears WCAG AA in both
2020
light and dark wallpapers; verified by `a11y-contrast`. */
2121
--color-bg-primary: rgba(255, 255, 255, 0.85);
22-
--color-bg-secondary: rgba(245, 245, 245, 0.5);
22+
--color-bg-secondary: rgba(245, 245, 245, 0.78);
2323
--color-bg-tertiary: #e8e8e8;
2424
/* Status-bar surface that contrasts gently with `--color-bg-secondary`.
2525
Light: a touch darker than secondary. Dark: a touch lighter. Used by
2626
`SelectionInfo` so the status band reads as distinct from the
2727
function-key bar below (which keeps `--color-bg-secondary`). */
28-
--color-bg-info-bar: rgba(236, 236, 236, 0.5);
28+
--color-bg-info-bar: rgba(236, 236, 236, 0.78);
29+
/* Inactive tab surface. Reads as "inactive" — slightly darker than
30+
`--color-bg-secondary` (which the active tab now adopts) in light
31+
mode, slightly lighter in dark mode. The contrast against
32+
`--color-bg-secondary` is what conveys "this tab isn't selected"
33+
without needing a hard border. */
34+
--color-bg-tab-inactive: rgba(225, 225, 225, 0.78);
35+
/* Faint vertical hairline between adjacent inactive tabs. Light enough
36+
to read as "subdivider" rather than "this is its own thing". */
37+
--color-tab-separator: rgba(0, 0, 0, 0.12);
2938
/* Settings-window sidebar (the "section selector"). In light mode the
3039
sidebar reads as a near-white "card" that's slightly **lighter** than
3140
the rest of the translucent window (`--color-bg-primary` ≈ 0.85α). A
@@ -416,6 +425,7 @@
416425
--color-bg-sidebar-to: #ffffff;
417426
--color-sidebar-border: #ffffff;
418427
--color-bg-settings-primary: #ffffff;
428+
--color-bg-tab-inactive: #e1e1e1;
419429
}
420430

421431
/* selection-fg fallback: dark mode + pane tint + focused cursor on a selected
@@ -523,11 +533,18 @@
523533
Slightly heavier alpha than light mode because dark backgrounds need
524534
more body to keep text contrast against bright wallpapers. */
525535
--color-bg-primary: rgba(30, 30, 30, 0.93);
526-
--color-bg-secondary: rgba(42, 42, 42, 0.7);
536+
--color-bg-secondary: rgba(42, 42, 42, 0.86);
527537
--color-bg-tertiary: #333333;
528538
/* Dark mode: lighter than secondary (opposite of light mode) so the
529539
status band lifts away from the function-key bar below. */
530-
--color-bg-info-bar: rgba(50, 50, 50, 0.7);
540+
--color-bg-info-bar: rgba(50, 50, 50, 0.86);
541+
/* Inactive tab surface (dark): slightly lighter than the active-tab
542+
bg (`--color-bg-secondary`) so unselected tabs read as recessed.
543+
Light mode flips the relationship (darker than secondary). */
544+
--color-bg-tab-inactive: rgba(30, 30, 30, 0.86);
545+
/* Tab separator (dark): light hairline visible against the tab
546+
surfaces around it. */
547+
--color-tab-separator: rgba(255, 255, 255, 0.12);
531548
/* Settings-window sidebar (dark). Same logic as light mode but flipped:
532549
both stops sit **darker** than `--color-bg-primary`, with a slight
533550
diagonal brightening so the card reads as one panel. */
@@ -677,6 +694,7 @@
677694
--color-bg-sidebar-to: #262626;
678695
--color-sidebar-border: var(--color-border);
679696
--color-bg-settings-primary: #1e1e1e;
697+
--color-bg-tab-inactive: #1e1e1e;
680698
}
681699

682700
/* Old-WebKit fallback (dark mode). See the light-mode counterpart above for

apps/desktop/src/lib/file-explorer/tabs/TabBar.svelte

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@
106106
handleContextMenu(e, tab.id)
107107
}}
108108
>
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}
109119
{#if tab.unreachable}
110120
<span class="warning-icon" use:tooltip={'Unreachable'} aria-label="Unreachable">
111121
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
@@ -161,19 +171,34 @@
161171
height: var(--spacing-tab-bar-height);
162172
min-height: var(--spacing-tab-bar-height);
163173
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);
165182
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;
167186
}
168187
169188
.tab-list {
170189
display: flex;
171190
flex: 1;
172191
min-width: 0;
173192
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`. */
175200
/* stylelint-disable-next-line declaration-property-value-disallowed-list */
176-
gap: 1px;
201+
gap: 5px;
177202
}
178203
179204
.tab {
@@ -192,7 +217,11 @@
192217
padding: 0 var(--spacing-sm);
193218
border: none;
194219
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);
196225
color: var(--color-text-secondary);
197226
font-size: var(--font-size-sm);
198227
font-family: var(--font-system);
@@ -205,28 +234,39 @@
205234
color var(--transition-fast);
206235
}
207236
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). */
209243
.tab:not(.active, .before-active, .after-active, :first-child)::before {
210244
content: '';
211245
position: absolute;
212-
left: -1px;
213-
top: 5px;
214-
bottom: 5px;
246+
left: -3px;
247+
top: 15%;
248+
bottom: 15%;
215249
width: 1px;
216-
background-color: var(--color-border-subtle);
250+
background-color: var(--color-tab-separator);
217251
}
218252
219253
.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);
221259
color: var(--color-text-primary);
222260
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`. */
225263
height: calc(var(--spacing-tab-bar-height) + 1px);
226264
/* stylelint-disable-next-line declaration-property-value-disallowed-list */
227265
margin-bottom: -1px;
228266
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;
230270
}
231271
232272
/* Accent top border on active tab */
@@ -242,18 +282,66 @@
242282
border-radius: 1px 1px 0 0;
243283
}
244284
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);
250332
}
251333
252334
.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%);
254336
color: var(--color-text-secondary);
255337
}
256338
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+
257345
/* Hide separator when hovering an inactive tab */
258346
.tab:hover:not(.active)::before {
259347
background-color: transparent;

0 commit comments

Comments
 (0)