@@ -14,7 +14,14 @@ import {
1414 useRef ,
1515 useState ,
1616} from 'react'
17- import { OverflowText , RowActions , rowActionsGroupClass } from '@sim/emcn'
17+ import {
18+ OverflowText ,
19+ RowActions ,
20+ rowActionsGroupClass ,
21+ SCROLL_FADE_BAND_PX ,
22+ scrollFadeAttributes ,
23+ scrollFadeXClass ,
24+ } from '@sim/emcn'
1825import { AnimatePresence , motion , useReducedMotion } from 'framer-motion'
1926import { Plus , X } from '../../icons'
2027import { cn } from '../../lib/cn'
@@ -24,36 +31,6 @@ import { TabStripAction } from './tab-strip-action'
2431
2532const DRAG_EDGE_ZONE = 40
2633const DRAG_SCROLL_SPEED = 8
27- /**
28- * Width of the scroll-edge fades, and so the margin a tab has to clear to be
29- * genuinely visible. Keep in step with the `w-4` on the gradients below: a tab
30- * revealed flush against the container edge lands under its gradient and reads
31- * as half-faded, which is indistinguishable from "there is more to scroll".
32- */
33- const EDGE_FADE_PX = 24
34-
35- /**
36- * Edge fades, as a mask rather than a tinted gradient laid over the tabs.
37- *
38- * Tabs paint their own fills, and an overlay tinted with the surface colour
39- * washes a pill's edge toward that colour instead of dissolving it — and it is
40- * only correct while whatever sits behind the strip is exactly that colour. A
41- * mask fades pill and label together to real transparency, over any background.
42- * This is how the command palette fades its results, and how every other
43- * horizontal fade in the app is drawn.
44- *
45- * The four combinations are spelled out because Tailwind scans for literal class
46- * strings; a template built at runtime would never be generated. Keep the 24px
47- * stops in step with {@link EDGE_FADE_PX}, which is how far `revealActiveTab`
48- * insets a tab so it lands clear of the fade rather than under it.
49- */
50- const SCROLL_FADE = {
51- none : '' ,
52- start :
53- '[-webkit-mask-image:linear-gradient(to_right,transparent_0px,black_24px)] [mask-image:linear-gradient(to_right,transparent_0px,black_24px)]' ,
54- end : '[-webkit-mask-image:linear-gradient(to_right,black_calc(100%_-_24px),transparent_100%)] [mask-image:linear-gradient(to_right,black_calc(100%_-_24px),transparent_100%)]' ,
55- both : '[-webkit-mask-image:linear-gradient(to_right,transparent_0px,black_24px,black_calc(100%_-_24px),transparent_100%)] [mask-image:linear-gradient(to_right,transparent_0px,black_24px,black_calc(100%_-_24px),transparent_100%)]' ,
56- } as const
5734const TAB_TRANSITION = { duration : 0.1 , ease : [ 0.2 , 0 , 0 , 1 ] as const }
5835
5936/**
@@ -62,13 +39,14 @@ const TAB_TRANSITION = { duration: 0.1, ease: [0.2, 0, 0, 1] as const }
6239 * the basis and left every tab sized by its own title.
6340 *
6441 * Floating tabs start at their content width, capped at 200px, then shrink with
65- * the available space. Floating tabs stop at a 64px control footprint; attached
66- * tabs retain their 96px label minimum. Crowded rows then scroll, and clipped
67- * titles remain available through tooltips.
42+ * the available space. Their 112px minimum leaves 50px for the title beside a
43+ * 16px icon and visible close button, including OverflowText's fade. Keep the
44+ * same minimum in every interaction state so revealing actions never shifts
45+ * tabs beneath the pointer. Crowded rows then scroll.
6846 */
6947const TAB_WIDTH : Record < TabStripVariant , string > = {
7048 attached : 'w-[156px] min-w-[96px] shrink' ,
71- floating : 'min-w-[64px] max-w-[var(--tab-strip-max-tab-width,200px)] shrink' ,
49+ floating : 'min-w-28 max-w-[var(--tab-strip-max-tab-width,200px)] shrink' ,
7250}
7351
7452/** The resting shape of a tab that is not the active one. */
@@ -427,9 +405,11 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab(
427405 className = { cn (
428406 'group relative select-none' ,
429407 rowActionsGroupClass ,
430- // `shrink` lets a crowded strip squeeze tabs to their floor before it
431- // starts scrolling.
432408 tab . pinned ? 'w-[34px] min-w-[34px] max-w-[34px] flex-none' : TAB_WIDTH [ variant ] ,
409+ variant === 'floating' &&
410+ closeable &&
411+ tab . attention &&
412+ '[@media(any-pointer:coarse)]:min-w-36 [@media(hover:none)]:min-w-36' ,
433413 dragging && 'opacity-30'
434414 ) }
435415 data-tab-strip-item = { tab . id }
@@ -584,16 +564,15 @@ export function TabStrip({
584564 const nodeRect = node . getBoundingClientRect ( )
585565 const tabLeft = tabRect . left - nodeRect . left + node . scrollLeft
586566 const tabRight = tabLeft + tabRect . width
587- // Inset by the fade on both sides so the tab comes to rest clear of the
588- // gradient rather than beneath it.
589- const viewLeft = node . scrollLeft + EDGE_FADE_PX
590- const viewRight = node . scrollLeft + node . clientWidth - EDGE_FADE_PX
567+ /** Keep the active tab clear of the canonical scroll fade. */
568+ const viewLeft = node . scrollLeft + SCROLL_FADE_BAND_PX
569+ const viewRight = node . scrollLeft + node . clientWidth - SCROLL_FADE_BAND_PX
591570 const maxScrollLeft = Math . max ( 0 , node . scrollWidth - node . clientWidth )
592571 const target =
593572 tabLeft < viewLeft
594- ? tabLeft - EDGE_FADE_PX
573+ ? tabLeft - SCROLL_FADE_BAND_PX
595574 : tabRight > viewRight
596- ? tabRight - node . clientWidth + EDGE_FADE_PX
575+ ? tabRight - node . clientWidth + SCROLL_FADE_BAND_PX
597576 : null
598577 if ( target === null ) return
599578 // The clamp is what lets the first and last tabs sit flush: there is no
@@ -926,18 +905,11 @@ export function TabStrip({
926905 < div className = 'flex min-w-0 shrink' >
927906 < div
928907 ref = { scrollNodeRef }
908+ { ...scrollFadeAttributes ( { left : canScrollLeft , right : canScrollRight } ) }
929909 className = { cn (
930910 'flex min-w-0 shrink select-none gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' ,
931911 variant === 'attached' ? 'items-end' : 'items-center gap-2' ,
932- SCROLL_FADE [
933- canScrollLeft
934- ? canScrollRight
935- ? 'both'
936- : 'start'
937- : canScrollRight
938- ? 'end'
939- : 'none'
940- ]
912+ scrollFadeXClass
941913 ) }
942914 >
943915 < AnimatePresence initial = { false } mode = 'popLayout' >
0 commit comments