diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabGroupIndicator.ts b/packages/dockview-core/src/dockview/components/titlebar/tabGroupIndicator.ts index 6ddb0350dc..17113f8d18 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabGroupIndicator.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabGroupIndicator.ts @@ -1,5 +1,5 @@ import { IValueDisposable } from '../../../lifecycle'; -import { DockviewHeaderDirection } from '../../options'; +import { DockviewHeaderDirection, DockviewHeaderPosition } from '../../options'; import { Tab } from '../tab/tab'; import { ITabGroup } from '../../tabGroup'; import { @@ -14,6 +14,7 @@ export interface TabGroupIndicatorContext { getTabMap(): Map>; getChipElement(groupId: string): HTMLElement | undefined; getDirection(): DockviewHeaderDirection; + getHeaderPosition(): DockviewHeaderPosition; getColorPalette(): TabGroupColorPalette | undefined; } @@ -410,6 +411,7 @@ export class WrapTabGroupIndicator extends BaseTabGroupIndicator { const r = 6; // corner radius const half = t / 2; + const headerPosition = this._ctx.getHeaderPosition(); if (isVertical) { const svgW = crossSize; @@ -419,20 +421,23 @@ export class WrapTabGroupIndicator extends BaseTabGroupIndicator { underline.style.width = `${svgW}px`; underline.style.height = `${svgH}px`; - const xLeft = half; - const xRight = svgW - half; + // right header: indicator on the left edge (invert x sides) + const isRightHeader = headerPosition === 'right'; + const xNear = isRightHeader ? svgW - half : half; + const xFar = isRightHeader ? half : svgW - half; + const cd = isRightHeader ? -1 : 1; // curve direction const d = [ - `M ${xLeft},0`, - `L ${xLeft},${aStart - r}`, - `Q ${xLeft},${aStart} ${xLeft + r},${aStart}`, - `L ${xRight - r},${aStart}`, - `Q ${xRight},${aStart} ${xRight},${aStart + r}`, - `L ${xRight},${aEnd - r}`, - `Q ${xRight},${aEnd} ${xRight - r},${aEnd}`, - `L ${xLeft + r},${aEnd}`, - `Q ${xLeft},${aEnd} ${xLeft},${aEnd + r}`, - `L ${xLeft},${svgH}`, + `M ${xNear},0`, + `L ${xNear},${aStart - r}`, + `Q ${xNear},${aStart} ${xNear + cd * r},${aStart}`, + `L ${xFar - cd * r},${aStart}`, + `Q ${xFar},${aStart} ${xFar},${aStart + r}`, + `L ${xFar},${aEnd - r}`, + `Q ${xFar},${aEnd} ${xFar - cd * r},${aEnd}`, + `L ${xNear + cd * r},${aEnd}`, + `Q ${xNear},${aEnd} ${xNear},${aEnd + r}`, + `L ${xNear},${svgH}`, ].join(' '); path.setAttribute('d', d); @@ -444,20 +449,23 @@ export class WrapTabGroupIndicator extends BaseTabGroupIndicator { underline.style.width = `${svgW}px`; underline.style.height = `${svgH}px`; - const yBot = svgH - half; - const yTop = half; + // bottom header: indicator on the top edge (invert y sides) + const isBottomHeader = headerPosition === 'bottom'; + const yNear = isBottomHeader ? half : svgH - half; + const yFar = isBottomHeader ? svgH - half : half; + const cd = isBottomHeader ? 1 : -1; // curve direction const d = [ - `M 0,${yBot}`, - `L ${aStart - r},${yBot}`, - `Q ${aStart},${yBot} ${aStart},${yBot - r}`, - `L ${aStart},${yTop + r}`, - `Q ${aStart},${yTop} ${aStart + r},${yTop}`, - `L ${aEnd - r},${yTop}`, - `Q ${aEnd},${yTop} ${aEnd},${yTop + r}`, - `L ${aEnd},${yBot - r}`, - `Q ${aEnd},${yBot} ${aEnd + r},${yBot}`, - `L ${svgW},${yBot}`, + `M 0,${yNear}`, + `L ${aStart - r},${yNear}`, + `Q ${aStart},${yNear} ${aStart},${yNear + cd * r}`, + `L ${aStart},${yFar - cd * r}`, + `Q ${aStart},${yFar} ${aStart + r},${yFar}`, + `L ${aEnd - r},${yFar}`, + `Q ${aEnd},${yFar} ${aEnd},${yFar - cd * r}`, + `L ${aEnd},${yNear + cd * r}`, + `Q ${aEnd},${yNear} ${aEnd + r},${yNear}`, + `L ${svgW},${yNear}`, ].join(' '); path.setAttribute('d', d); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabGroups.ts b/packages/dockview-core/src/dockview/components/titlebar/tabGroups.ts index 57b144a710..a37aef9baa 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabGroups.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabGroups.ts @@ -280,6 +280,7 @@ export class TabGroupManager { getChipElement: (id) => this._chipRenderers.get(id)?.chip.element, getDirection: () => this._ctx.getDirection(), + getHeaderPosition: () => this._ctx.group.model.headerPosition, getColorPalette: () => this._ctx.accessor.tabGroupColorPalette, }); } diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index a50aced9ed..bda93c8836 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -258,7 +258,7 @@ &::after { position: absolute; left: 0px; - top: 0px; + bottom: 0px; content: ''; width: 100%; height: 1px; @@ -478,7 +478,7 @@ &::after { position: absolute; left: 0px; - top: 0px; + bottom: 0px; content: ''; width: 100%; height: 2px; @@ -497,7 +497,7 @@ &::after { position: absolute; left: 0px; - top: 0px; + bottom: 0px; content: ''; width: 100%; height: 2px; @@ -1113,15 +1113,12 @@ } } -// Tab group indicator 'none' mode — uses a JS-positioned continuous bar -// (NoneTabGroupIndicator) instead of per-tab box-shadows, so the indicator -// spans the full tab group width without gaps in spaced themes. -// The flat bar always sits at the bottom (or left for vertical) edge of -// the tab bar, so we undo the header-bottom override that moves the -// wrap indicator's full-height SVG to top: 0. -.dv-tab-group-indicator-none { - .dv-groupview-header-bottom .dv-tab-group-underline { - top: auto; - bottom: 0; +// Header-bottom: flip the active-tab indicator from bottom (content side when +// tabs are on top) to top (content side when tabs are on bottom). +.dv-groupview-header-bottom { + .dv-tabs-container > .dv-tab::after { + top: 0px; + bottom: auto; } } + diff --git a/packages/docs/templates/dockview/tab-groups/react/src/app.tsx b/packages/docs/templates/dockview/tab-groups/react/src/app.tsx index 3eb6ca1a84..b6e29a4c43 100644 --- a/packages/docs/templates/dockview/tab-groups/react/src/app.tsx +++ b/packages/docs/templates/dockview/tab-groups/react/src/app.tsx @@ -6,6 +6,7 @@ import { GetTabContextMenuItemsParams, GetTabGroupChipContextMenuItemsParams, themeAbyss, + DockviewHeaderPosition, } from 'dockview-react'; import React from 'react'; import { @@ -27,6 +28,18 @@ const components = { export default () => { const [api, setApi] = React.useState(); + const [headerPosition, setHeaderPosition] = + React.useState('top'); + + const onHeaderPositionChange = ( + position: DockviewHeaderPosition + ) => { + setHeaderPosition(position); + if (!api) return; + for (const group of api.groups) { + group.api.setHeaderPosition(position); + } + }; const onReady = (event: DockviewReadyEvent) => { setApi(event.api); @@ -154,18 +167,33 @@ export default () => { ); return ( -
- +
+
+ Tab position: + {(['top', 'bottom'] as DockviewHeaderPosition[]).map((pos) => ( + + ))} +
+
+ +
); }; +