From d811ca655406e03065e6d68f16253195bcb48eba Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:42:57 +0000 Subject: [PATCH] feat: improved dnd model --- .../src/__tests__/dnd/droptarget.spec.ts | 4 +- .../__tests__/dockview/components/tab.spec.ts | 51 ++-- .../components/titlebar/tabsContainer.spec.ts | 49 ++-- .../dockview/dockviewComponent.spec.ts | 40 +--- .../dockview/dockviewGroupPanelModel.spec.ts | 50 ++-- .../dockview-core/src/api/component.api.ts | 14 +- .../src/dnd/abstractDragHandler.ts | 6 +- .../src/dnd/dropTargetAnchorContainer.scss | 23 ++ .../src/dnd/dropTargetAnchorContainer.ts | 102 ++++++++ .../dockview-core/src/dnd/droptarget.scss | 10 +- packages/dockview-core/src/dnd/droptarget.ts | 204 +++++++++++++++- packages/dockview-core/src/dnd/ghost.ts | 5 +- .../dockview-core/src/dnd/groupDragHandler.ts | 4 +- .../src/dockview/components/panel/content.ts | 26 +- .../dockview/components/tab/defaultTab.scss | 4 +- .../src/dockview/components/tab/tab.ts | 32 ++- .../components/titlebar/tabsContainer.scss | 30 ++- .../components/titlebar/tabsContainer.ts | 5 +- .../components/titlebar/voidContainer.ts | 14 +- .../src/dockview/dockviewComponent.ts | 117 +++++++-- .../src/dockview/dockviewGroupPanelModel.ts | 37 +++ .../dockview-core/src/dockview/options.ts | 24 +- packages/dockview-core/src/dockview/theme.ts | 54 +++++ packages/dockview-core/src/index.ts | 1 + .../src/splitview/splitview.scss | 1 + .../dockview-core/src/splitview/splitview.ts | 2 + packages/dockview-core/src/theme.scss | 226 +++++++++++++----- .../src/theme/_drop-target-static-mixin.scss | 10 + .../src/theme/_sash-handle-mixin.scss | 53 ++++ .../dockview-core/src/theme/_space-mixin.scss | 52 ++++ packages/dockview/src/svg.tsx | 4 +- .../docs/docs/overview/getStarted/theme.mdx | 6 +- .../react/dockview/demo-dockview/src/app.scss | 1 + .../react/dockview/demo-dockview/src/app.tsx | 36 +-- .../dockview/demo-dockview/src/controls.tsx | 2 +- .../demo-dockview/src/gridActions.tsx | 40 +++- .../docs/src/components/frameworkSpecific.css | 27 ++- .../docs/src/components/frameworkSpecific.tsx | 5 +- .../src/components/ui/codeSandboxButton.scss | 2 +- .../src/components/ui/codeSandboxButton.tsx | 4 +- packages/docs/src/components/ui/container.tsx | 6 +- .../docs/src/components/ui/exampleFrame.tsx | 3 +- packages/docs/src/config/theme.config.ts | 33 ++- packages/docs/src/css/custom.scss | 28 ++- packages/docs/src/pages/demo.tsx | 135 ++++++++++- 45 files changed, 1232 insertions(+), 350 deletions(-) create mode 100644 packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss create mode 100644 packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts create mode 100644 packages/dockview-core/src/dockview/theme.ts create mode 100644 packages/dockview-core/src/theme/_drop-target-static-mixin.scss create mode 100644 packages/dockview-core/src/theme/_sash-handle-mixin.scss create mode 100644 packages/dockview-core/src/theme/_space-mixin.scss diff --git a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts index 2d82095ab..b150ba896 100644 --- a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts @@ -16,10 +16,10 @@ describe('droptarget', () => { beforeEach(() => { element = document.createElement('div'); - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200); }); test('that dragover events are marked', () => { diff --git a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts index cb246b598..4a2f0c72d 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts @@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { Tab } from '../../../dockview/components/tab/tab'; import { IDockviewPanel } from '../../../dockview/dockviewPanel'; +import { fromPartial } from '@total-typescript/shoehorn'; describe('tab', () => { test('that empty tab has inactive-tab class', () => { @@ -46,15 +47,10 @@ describe('tab', () => { id: 'testcomponentid', }; }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -72,38 +68,33 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); fireEvent.dragEnter(cut.element); fireEvent.dragOver(cut.element); - expect(groupView.canDisplayOverlay).toBeCalled(); + expect(groupView.canDisplayOverlay).toHaveBeenCalled(); expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length ).toBe(0); }); - test('that if you drag over yourself no drop target is shown', () => { + test('that if you drag over yourself a drop target is shown', () => { const accessorMock = jest.fn, []>(() => { return { id: 'testcomponentid', }; }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -121,10 +112,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -136,11 +127,11 @@ describe('tab', () => { fireEvent.dragEnter(cut.element); fireEvent.dragOver(cut.element); - expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); test('that if you drag over another tab a drop target is shown', () => { @@ -175,10 +166,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -229,10 +220,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -289,10 +280,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index eee78a588..bae8daa8d 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -42,16 +42,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -73,15 +73,14 @@ describe('tabsContainer', () => { options: {}, }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); + const dropTargetContainer = document.createElement('div'); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + // dropTargetContainer: new DropTargetAnchorContainer( + // dropTargetContainer + // ), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -97,16 +96,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -129,6 +128,10 @@ describe('tabsContainer', () => { expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length ).toBe(1); + // expect( + // dropTargetContainer.getElementsByClassName('dv-drop-target-anchor') + // .length + // ).toBe(1); }); test('that dropping over the empty space should render a drop target', () => { @@ -166,16 +169,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -229,16 +232,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -291,16 +294,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 0d00d1a31..8c0dab657 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -133,11 +133,11 @@ describe('dockviewComponent', () => { }, className: 'test-a test-b', }); - expect(dockview.element.className).toBe('test-a test-b'); + expect(dockview.element.className).toBe('test-a test-b dockview-theme-abyss'); dockview.updateOptions({ className: 'test-b test-c' }); - expect(dockview.element.className).toBe('test-b test-c'); + expect(dockview.element.className).toBe('dockview-theme-abyss test-b test-c'); }); describe('memory leakage', () => { @@ -3339,10 +3339,10 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - Object.defineProperty(dockview.element, 'clientWidth', { + Object.defineProperty(dockview.element, 'offsetWidth', { get: () => 100, }); - Object.defineProperty(dockview.element, 'clientHeight', { + Object.defineProperty(dockview.element, 'offsetHeight', { get: () => 100, }); @@ -6652,36 +6652,4 @@ describe('dockviewComponent', () => { expect(api.panels.length).toBe(3); expect(api.groups.length).toBe(3); }); - - describe('updateOptions', () => { - test('gap', () => { - const container = document.createElement('div'); - - const dockview = new DockviewComponent(container, { - createComponent(options) { - switch (options.name) { - case 'default': - return new PanelContentPartTest( - options.id, - options.name - ); - default: - throw new Error(`unsupported`); - } - }, - gap: 6, - }); - - expect(dockview.gap).toBe(6); - - dockview.updateOptions({ gap: 10 }); - expect(dockview.gap).toBe(10); - - dockview.updateOptions({}); - expect(dockview.gap).toBe(10); - - dockview.updateOptions({ gap: 15 }); - expect(dockview.gap).toBe(15); - }); - }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 55aed39ec..19b811c4f 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -684,12 +684,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0)! as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); fireEvent.dragEnter(element); fireEvent.dragOver(element); @@ -744,12 +744,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0)! as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); function run(value: number) { fireEvent.dragEnter(element); @@ -792,7 +792,7 @@ describe('dockviewGroupPanelModel', () => { fireEvent.dragEnd(element); }); - test('that should not show drop target if dropping on self', () => { + test('that should show drop target if dropping on self', () => { const accessor = fromPartial({ id: 'testcomponentid', options: {}, @@ -806,15 +806,9 @@ describe('dockviewGroupPanelModel', () => { ), }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -842,12 +836,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0)! as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); LocalSelectionTransfer.getInstance().setData( [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], @@ -861,10 +855,10 @@ describe('dockviewGroupPanelModel', () => { expect( element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); - test('that should not allow drop when dropping on self for same component id', () => { + test('that should allow drop when dropping on self for same component id', () => { const accessor = fromPartial({ id: 'testcomponentid', options: {}, @@ -915,12 +909,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0) as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); LocalSelectionTransfer.getInstance().setData( [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], @@ -934,7 +928,7 @@ describe('dockviewGroupPanelModel', () => { expect( element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); test('that should not allow drop when not dropping for different component id', () => { @@ -988,12 +982,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0) as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); LocalSelectionTransfer.getInstance().setData( [new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')], diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 6884c93aa..0b0850ed0 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -629,10 +629,20 @@ export class DockviewApi implements CommonApi { return this.component.totalPanels; } + /** + * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ get gap(): number { return this.component.gap; } + /** + * @deprecated dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ + setGap(gap: number | undefined): void { + this.component.updateOptions({ gap: gap }); + } + /** * Invoked when the active group changes. May be undefined if no group is active. */ @@ -914,10 +924,6 @@ export class DockviewApi implements CommonApi { return this.component.addPopoutGroup(item, options); } - setGap(gap: number | undefined): void { - this.component.updateOptions({ gap }); - } - updateOptions(options: Partial) { this.component.updateOptions(options); } diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 84345c160..7ba701034 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -67,7 +67,7 @@ export abstract class DragHandler extends CompositeDisposable { * For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews * dnd logic. You can see the code at - * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 + P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 */ event.dataTransfer.setData('text/plain', ''); } @@ -75,7 +75,9 @@ export abstract class DragHandler extends CompositeDisposable { }), addDisposableListener(this.el, 'dragend', () => { this.pointerEventsDisposable.dispose(); - this.dataDisposable.dispose(); + setTimeout(() => { + this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing + }, 0); }) ); } diff --git a/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss new file mode 100644 index 000000000..0fd3dc5a5 --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss @@ -0,0 +1,23 @@ +.dv-drop-target-container { + position: absolute; + z-index: 9999; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + pointer-events: none; + overflow: hidden; + --dv-transition-duration: 300ms; + + .dv-drop-target-anchor { + position: relative; + border: var(--dv-drag-over-border); + transition: opacity var(--dv-transition-duration) ease-in, + top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out; + background-color: var(--dv-drag-over-background-color); + opacity: 1; + } +} diff --git a/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts new file mode 100644 index 000000000..e43989c9e --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts @@ -0,0 +1,102 @@ +import { CompositeDisposable, Disposable } from '../lifecycle'; +import { DropTargetTargetModel } from './droptarget'; + +export class DropTargetAnchorContainer extends CompositeDisposable { + private _model: + | { root: HTMLElement; overlay: HTMLElement; changed: boolean } + | undefined; + + private _outline: HTMLElement | undefined; + + private _disabled = false; + + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + if (this.disabled === value) { + return; + } + + this._disabled = value; + + if (value) { + this.model?.clear(); + } + } + + get model(): DropTargetTargetModel | undefined { + if (this.disabled) { + return undefined; + } + + return { + clear: () => { + if (this._model) { + this._model.root.parentElement?.removeChild( + this._model.root + ); + } + this._model = undefined; + }, + exists: () => { + return !!this._model; + }, + getElements: (event?: DragEvent, outline?: HTMLElement) => { + const changed = this._outline !== outline; + this._outline = outline; + + if (this._model) { + this._model.changed = changed; + return this._model; + } + + const container = this.createContainer(); + const anchor = this.createAnchor(); + + this._model = { root: container, overlay: anchor, changed }; + + container.appendChild(anchor); + this.element.appendChild(container); + + if (event?.target instanceof HTMLElement) { + const targetBox = event.target.getBoundingClientRect(); + const box = this.element.getBoundingClientRect(); + + anchor.style.left = `${targetBox.left - box.left}px`; + anchor.style.top = `${targetBox.top - box.top}px`; + } + + return this._model; + }, + }; + } + + constructor(readonly element: HTMLElement, options: { disabled: boolean }) { + super(); + + this._disabled = options.disabled; + + this.addDisposables( + Disposable.from(() => { + this.model?.clear(); + }) + ); + } + + private createContainer(): HTMLElement { + const el = document.createElement('div'); + el.className = 'dv-drop-target-container'; + + return el; + } + + private createAnchor(): HTMLElement { + const el = document.createElement('div'); + el.className = 'dv-drop-target-anchor'; + el.style.visibility = 'hidden'; + + return el; + } +} diff --git a/packages/dockview-core/src/dnd/droptarget.scss b/packages/dockview-core/src/dnd/droptarget.scss index f23f318f7..7f2c8cb8b 100644 --- a/packages/dockview-core/src/dnd/droptarget.scss +++ b/packages/dockview-core/src/dnd/droptarget.scss @@ -1,5 +1,6 @@ .dv-drop-target { position: relative; + --dv-transition-duration: 70ms; > .dv-drop-target-dropzone { position: absolute; @@ -15,10 +16,13 @@ box-sizing: border-box; height: 100%; width: 100%; + border: var(--dv-drag-over-border); background-color: var(--dv-drag-over-background-color); - transition: top 70ms ease-out, left 70ms ease-out, - width 70ms ease-out, height 70ms ease-out, - opacity 0.15s ease-out; + transition: top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out, + opacity var(--dv-transition-duration) ease-out; will-change: transform; pointer-events: none; diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 702fed867..c611f890b 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = { const SMALL_WIDTH_BOUNDARY = 100; const SMALL_HEIGHT_BOUNDARY = 100; +export interface DropTargetTargetModel { + getElements( + event?: DragEvent, + outline?: HTMLElement + ): { + root: HTMLElement; + overlay: HTMLElement; + changed: boolean; + }; + exists(): boolean; + clear(): void; +} + export interface DroptargetOptions { canDisplayOverlay: CanDisplayOverlay; acceptedTargetZones: Position[]; overlayModel?: DroptargetOverlayModel; + getOverrideTarget?: () => DropTargetTargetModel | undefined; + className?: string; + getOverlayOutline?: () => HTMLElement | null; } export class Droptarget extends CompositeDisposable { @@ -116,6 +132,18 @@ export class Droptarget extends CompositeDisposable { private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; + private static ACTUAL_TARGET: Droptarget | undefined; + + private _disabled: boolean; + + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = value; + } + get state(): Position | undefined { return this._state; } @@ -126,21 +154,35 @@ export class Droptarget extends CompositeDisposable { ) { super(); + this._disabled = false; + // use a set to take advantage of #.has this._acceptedTargetZonesSet = new Set( this.options.acceptedTargetZones ); this.dnd = new DragAndDropObserver(this.element, { - onDragEnter: () => undefined, + onDragEnter: () => { + this.options.getOverrideTarget?.()?.getElements(); + }, onDragOver: (e) => { + Droptarget.ACTUAL_TARGET = this; + + const overrideTraget = this.options.getOverrideTarget?.(); + if (this._acceptedTargetZonesSet.size === 0) { + if (overrideTraget) { + return; + } this.removeDropTarget(); return; } - const width = this.element.clientWidth; - const height = this.element.clientHeight; + const target = + this.options.getOverlayOutline?.() ?? this.element; + + const width = target.offsetWidth; + const height = target.offsetHeight; if (width === 0 || height === 0) { return; // avoid div!0 @@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable { const rect = ( e.currentTarget as HTMLElement ).getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const x = (e.clientX ?? 0) - rect.left; + const y = (e.clientY ?? 0) - rect.top; const quadrant = this.calculateQuadrant( this._acceptedTargetZonesSet, @@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable { } if (!this.options.canDisplayOverlay(e, quadrant)) { + if (overrideTraget) { + return; + } this.removeDropTarget(); return; } @@ -194,7 +239,9 @@ export class Droptarget extends CompositeDisposable { this.markAsUsed(e); - if (!this.targetElement) { + if (overrideTraget) { + // + } else if (!this.targetElement) { this.targetElement = document.createElement('div'); this.targetElement.className = 'dv-drop-target-dropzone'; this.overlayElement = document.createElement('div'); @@ -202,8 +249,16 @@ export class Droptarget extends CompositeDisposable { this._state = 'center'; this.targetElement.appendChild(this.overlayElement); - this.element.classList.add('dv-drop-target'); - this.element.append(this.targetElement); + target.classList.add('dv-drop-target'); + target.append(this.targetElement); + + // this.overlayElement.style.opacity = '0'; + + // requestAnimationFrame(() => { + // if (this.overlayElement) { + // this.overlayElement.style.opacity = ''; + // } + // }); } this.toggleClasses(quadrant, width, height); @@ -211,10 +266,32 @@ export class Droptarget extends CompositeDisposable { this._state = quadrant; }, onDragLeave: () => { + const target = this.options.getOverrideTarget?.(); + + if (target) { + return; + } + this.removeDropTarget(); }, - onDragEnd: () => { + onDragEnd: (e) => { + const target = this.options.getOverrideTarget?.(); + + if (target && Droptarget.ACTUAL_TARGET === this) { + if (this._state) { + // only stop the propagation of the event if we are dealing with it + // which is only when the target has state + e.stopPropagation(); + this._onDrop.fire({ + position: this._state, + nativeEvent: e, + }); + } + } + this.removeDropTarget(); + + target?.clear(); }, onDrop: (e) => { e.preventDefault(); @@ -223,6 +300,8 @@ export class Droptarget extends CompositeDisposable { this.removeDropTarget(); + this.options.getOverrideTarget?.()?.clear(); + if (state) { // only stop the propagation of the event if we are dealing with it // which is only when the target has state @@ -268,7 +347,9 @@ export class Droptarget extends CompositeDisposable { width: number, height: number ): void { - if (!this.overlayElement) { + const target = this.options.getOverrideTarget?.(); + + if (!target && !this.overlayElement) { return; } @@ -300,6 +381,103 @@ export class Droptarget extends CompositeDisposable { } } + if (target) { + const outlineEl = + this.options.getOverlayOutline?.() ?? this.element; + const elBox = outlineEl.getBoundingClientRect(); + + const ta = target.getElements(undefined, outlineEl); + const el = ta.root; + const overlay = ta.overlay; + + const bigbox = el.getBoundingClientRect(); + + const rootTop = elBox.top - bigbox.top; + const rootLeft = elBox.left - bigbox.left; + + const box = { + top: rootTop, + left: rootLeft, + width: width, + height: height, + }; + + if (rightClass) { + box.left = rootLeft + width * (1 - size); + box.width = width * size; + } else if (leftClass) { + box.width = width * size; + } else if (topClass) { + box.height = height * size; + } else if (bottomClass) { + box.top = rootTop + height * (1 - size); + box.height = height * size; + } + + if (isSmallX && isLeft) { + box.width = 4; + } + if (isSmallX && isRight) { + box.left = rootLeft + width - 4; + box.width = 4; + } + + const topPx = `${Math.round(box.top)}px`; + const leftPx = `${Math.round(box.left)}px`; + const widthPx = `${Math.round(box.width)}px`; + const heightPx = `${Math.round(box.height)}px`; + + if ( + overlay.style.top === topPx && + overlay.style.left === leftPx && + overlay.style.width === widthPx && + overlay.style.height === heightPx + ) { + return; + } + + overlay.style.top = topPx; + overlay.style.left = leftPx; + overlay.style.width = widthPx; + overlay.style.height = heightPx; + overlay.style.visibility = 'visible'; + + overlay.className = `dv-drop-target-anchor${ + this.options.className ? ` ${this.options.className}` : '' + }`; + + toggleClass(overlay, 'dv-drop-target-left', isLeft); + toggleClass(overlay, 'dv-drop-target-right', isRight); + toggleClass(overlay, 'dv-drop-target-top', isTop); + toggleClass(overlay, 'dv-drop-target-bottom', isBottom); + toggleClass( + overlay, + 'dv-drop-target-center', + quadrant === 'center' + ); + + if (ta.changed) { + toggleClass( + overlay, + 'dv-drop-target-anchor-container-changed', + true + ); + setTimeout(() => { + toggleClass( + overlay, + 'dv-drop-target-anchor-container-changed', + false + ); + }, 10); + } + + return; + } + + if (!this.overlayElement) { + return; + } + const box = { top: '0px', left: '0px', width: '100%', height: '100%' }; /** @@ -396,10 +574,12 @@ export class Droptarget extends CompositeDisposable { private removeDropTarget(): void { if (this.targetElement) { this._state = undefined; - this.element.removeChild(this.targetElement); + this.targetElement.parentElement?.classList.remove( + 'dv-drop-target' + ); + this.targetElement.remove(); this.targetElement = undefined; this.overlayElement = undefined; - this.element.classList.remove('dv-drop-target'); } } } diff --git a/packages/dockview-core/src/dnd/ghost.ts b/packages/dockview-core/src/dnd/ghost.ts index 2ff9c569f..df976c7cf 100644 --- a/packages/dockview-core/src/dnd/ghost.ts +++ b/packages/dockview-core/src/dnd/ghost.ts @@ -2,13 +2,14 @@ import { addClasses, removeClasses } from '../dom'; export function addGhostImage( dataTransfer: DataTransfer, - ghostElement: HTMLElement + ghostElement: HTMLElement, + options?: { x?: number; y?: number } ): void { // class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues addClasses(ghostElement, 'dv-dragged'); document.body.appendChild(ghostElement); - dataTransfer.setDragImage(ghostElement, 0, 0); + dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0); setTimeout(() => { removeClasses(ghostElement, 'dv-dragged'); diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index bdda2be3b..2e3c9d281 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -72,9 +72,11 @@ export class GroupDragHandler extends DragHandler { ghostElement.style.lineHeight = '20px'; ghostElement.style.borderRadius = '12px'; ghostElement.style.position = 'absolute'; + ghostElement.style.pointerEvents = 'none'; + ghostElement.style.top = '-9999px'; ghostElement.textContent = `Multiple Panels (${this.group.size})`; - addGhostImage(dataTransfer, ghostElement); + addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 }); } return { diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 4f66b03d3..08703179d 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -55,7 +55,15 @@ export class ContentContainer this.addDisposables(this._onDidFocus, this._onDidBlur); + const target = group.dropTargetContainer; + this.dropTarget = new Droptarget(this.element, { + getOverlayOutline: () => { + return accessor.options.theme?.includeHeaderWhenHoverOverContent + ? this.element.parentElement + : null; + }, + className: 'dv-drop-target-content', acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], canDisplayOverlay: (event, position) => { if ( @@ -76,26 +84,12 @@ export class ContentContainer } if (data && data.viewId === this.accessor.id) { - if (data.groupId === this.group.id) { - if (position === 'center') { - // don't allow to drop on self for center position - return false; - } - if (data.panelId === null) { - // don't allow group move to drop anywhere on self - return false; - } - } - - const groupHasOnePanelAndIsActiveDragElement = - this.group.panels.length === 1 && - data.groupId === this.group.id; - - return !groupHasOnePanelAndIsActiveDragElement; + return true; } return this.group.canDisplayOverlay(event, position, 'content'); }, + getOverrideTarget: target ? () => target.model : undefined, }); this.addDisposables(this.dropTarget); diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss index 0fdf53d78..3d2865583 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss @@ -58,15 +58,13 @@ position: relative; height: 100%; display: flex; - min-width: 80px; align-items: center; - padding: 0px 8px; white-space: nowrap; text-overflow: ellipsis; .dv-default-tab-content { - padding: 0px 8px; flex-grow: 1; + margin-right: 4px; } .dv-default-tab-action { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 1eb1174d8..9b1975d4a 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -16,6 +16,7 @@ import { } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; import { IDockviewPanel } from '../../dockviewPanel'; +import { addGhostImage } from '../../../dnd/ghost'; class TabDragHandler extends DragHandler { private readonly panelTransfer = @@ -86,7 +87,8 @@ export class Tab extends CompositeDisposable { ); this.dropTarget = new Droptarget(this._element, { - acceptedTargetZones: ['center'], + acceptedTargetZones: ['left', 'right'], + overlayModel: { activationSize: { value: 50, type: 'percentage' } }, canDisplayOverlay: (event, position) => { if (this.group.locked) { return false; @@ -95,15 +97,7 @@ export class Tab extends CompositeDisposable { const data = getPanelData(); if (data && this.accessor.id === data.viewId) { - if ( - data.panelId === null && - data.groupId === this.group.id - ) { - // don't allow group move to drop on self - return false; - } - - return this.panel.id !== data.panelId; + return true; } return this.group.model.canDisplayOverlay( @@ -112,6 +106,7 @@ export class Tab extends CompositeDisposable { 'tab' ); }, + getOverrideTarget: () => group.model.dropTargetContainer?.model, }); this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; @@ -121,6 +116,23 @@ export class Tab extends CompositeDisposable { this._onDropped, this._onDragStart, dragHandler.onDragStart((event) => { + if (event.dataTransfer) { + const style = getComputedStyle(this.element); + const newNode = this.element.cloneNode(true) as HTMLElement; + Array.from(style).forEach((key) => + newNode.style.setProperty( + key, + style.getPropertyValue(key), + style.getPropertyPriority(key) + ) + ); + newNode.style.position = 'absolute'; + + addGhostImage(event.dataTransfer, newNode, { + y: -10, + x: 30, + }); + } this._onDragStart.fire(event); }), dragHandler, diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss index fef520e03..86777ec9e 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss @@ -7,17 +7,17 @@ font-size: var(--dv-tabs-and-actions-container-font-size); &.dv-single-tab.dv-full-width-single-tab { - .dv-tabs-container { - flex-grow: 1; - - .dv-tab { + .dv-tabs-container { flex-grow: 1; - } - } - .dv-void-container { - flex-grow: 0; - } + .dv-tab { + flex-grow: 1; + } + } + + .dv-void-container { + flex-grow: 0; + } } .dv-void-container { @@ -50,10 +50,20 @@ .dv-tab { -webkit-user-drag: element; outline: none; - min-width: 75px; + padding: 0.25rem 0.5rem; cursor: pointer; position: relative; box-sizing: border-box; + font-size: var(-dv-tab-font-size); + margin: var(--dv-tab-margin); + + &:first-child { + margin-right: 0; + } + + &:not(:nth-last-child(1)) { + margin-left: 0; + } &:not(:first-child)::before { content: ' '; diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index d3bd0568b..c6af57973 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -10,7 +10,10 @@ import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; -import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; +import { + DockviewGroupPanelModel, + WillShowOverlayLocationEvent, +} from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; export interface TabDropIndexEvent { diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 6e9ea0c47..29e31b9b6 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -1,4 +1,3 @@ -import { last } from '../../../array'; import { getPanelData } from '../../../dnd/dataTransfer'; import { Droptarget, @@ -10,6 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable } from '../../../lifecycle'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; +import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; export class VoidContainer extends CompositeDisposable { private readonly _element: HTMLElement; @@ -54,16 +54,7 @@ export class VoidContainer extends CompositeDisposable { const data = getPanelData(); if (data && this.accessor.id === data.viewId) { - if ( - data.panelId === null && - data.groupId === this.group.id - ) { - // don't allow group move to drop on self - return false; - } - - // don't show the overlay if the tab being dragged is the last panel of this group - return last(this.group.panels)?.id !== data.panelId; + return true; } return group.model.canDisplayOverlay( @@ -72,6 +63,7 @@ export class VoidContainer extends CompositeDisposable { 'header_space' ); }, + getOverrideTarget: () => group.model.dropTargetContainer?.model, }); this.onWillShowOverlay = this.dropTraget.onWillShowOverlay; diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 0dde93454..86465c015 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -54,6 +54,7 @@ import { Parameters } from '../panel/types'; import { Overlay } from '../overlay/overlay'; import { addTestId, + Classnames, getDockviewTheme, toggleClass, watchElementResize, @@ -74,6 +75,8 @@ import { } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; import { StrictEventsSequencing } from './strictEventsSequencing'; +import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; +import { DockviewTheme, themeAbyss } from './theme'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -191,6 +194,9 @@ export interface IDockviewComponent extends IBaseGrid { readonly totalPanels: number; readonly panels: IDockviewPanel[]; readonly orientation: Orientation; + /** + * @deprecated use `theme` instead. This will be removed in a future version + */ readonly gap: number; readonly onDidDrop: Event; readonly onWillDrop: Event; @@ -253,9 +259,11 @@ export class DockviewComponent private readonly _deserializer = new DefaultDockviewDeserialzier(this); private readonly _api: DockviewApi; private _options: Exclude; - private watermark: IWatermarkRenderer | null = null; + private _watermark: IWatermarkRenderer | null = null; + private readonly _themeClassnames: Classnames; readonly overlayRenderContainer: OverlayRenderContainer; + readonly rootDropTargetContainer: DropTargetAnchorContainer; private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -361,6 +369,9 @@ export class DockviewComponent } get gap(): number { + console.warn( + 'dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.' + ); return this.gridview.margin; } @@ -377,10 +388,18 @@ export class DockviewComponent : undefined, disableAutoResizing: options.disableAutoResizing, locked: options.locked, - margin: options.gap, + margin: options.theme?.gap ?? 0, className: options.className, }); + this.updateDropTargetModel(options); + + this._themeClassnames = new Classnames(this.element); + + this.rootDropTargetContainer = new DropTargetAnchorContainer( + this.element, + { disabled: true } + ); this.overlayRenderContainer = new OverlayRenderContainer( this.gridview.element, this @@ -394,6 +413,7 @@ export class DockviewComponent } this.addDisposables( + this.rootDropTargetContainer, this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, @@ -464,8 +484,10 @@ export class DockviewComponent ); this._options = options; + this.updateTheme(); this._rootDropTarget = new Droptarget(this.element, { + className: 'dv-drop-target-edge', canDisplayOverlay: (event, position) => { const data = getPanelData(); @@ -506,6 +528,7 @@ export class DockviewComponent acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], overlayModel: this.options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL, + getOverrideTarget: () => this.rootDropTargetContainer?.model, }); this.addDisposables( @@ -756,6 +779,15 @@ export class DockviewComponent popoutContainer.appendChild(group.element); + const anchor = document.createElement('div'); + const dropTargetContainer = new DropTargetAnchorContainer( + anchor, + { disabled: this.rootDropTargetContainer.disabled } + ); + popoutContainer.appendChild(anchor); + + group.model.dropTargetContainer = dropTargetContainer; + group.model.location = { type: 'popout', getWindow: () => _window.window!, @@ -844,6 +876,8 @@ export class DockviewComponent } else if (this.getPanel(group.id)) { group.model.renderContainer = this.overlayRenderContainer; + group.model.dropTargetContainer = + this.rootDropTargetContainer; returnedGroup = group; const alreadyRemoved = !this._popoutGroups.find( @@ -1134,6 +1168,13 @@ export class DockviewComponent override updateOptions(options: Partial): void { super.updateOptions(options); + if ('gap' in options) { + console.warn( + 'dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version.' + ); + this.gridview.margin = options.gap ?? 0; + } + if ('floatingGroupBounds' in options) { for (const group of this._floatingGroups) { switch (options.floatingGroupBounds) { @@ -1158,18 +1199,14 @@ export class DockviewComponent } } - if ('rootOverlayModel' in options) { - this._rootDropTarget.setOverlayModel( - options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL - ); - } - - if ('gap' in options) { - this.gridview.margin = options.gap ?? 0; - } + this.updateDropTargetModel(options); this._options = { ...this.options, ...options }; + if ('theme' in options) { + this.updateTheme(); + } + this.layout(this.gridview.width, this.gridview.height, true); } @@ -1745,24 +1782,24 @@ export class DockviewComponent (x) => x.api.location.type === 'grid' && x.api.isVisible ).length === 0 ) { - if (!this.watermark) { - this.watermark = this.createWatermarkComponent(); + if (!this._watermark) { + this._watermark = this.createWatermarkComponent(); - this.watermark.init({ + this._watermark.init({ containerApi: new DockviewApi(this), }); const watermarkContainer = document.createElement('div'); watermarkContainer.className = 'dv-watermark-container'; addTestId(watermarkContainer, 'watermark-component'); - watermarkContainer.appendChild(this.watermark.element); + watermarkContainer.appendChild(this._watermark.element); this.gridview.element.appendChild(watermarkContainer); } - } else if (this.watermark) { - this.watermark.element.parentElement!.remove(); - this.watermark.dispose?.(); - this.watermark = null; + } else if (this._watermark) { + this._watermark.element.parentElement!.remove(); + this._watermark.dispose?.(); + this._watermark = null; } } @@ -2404,9 +2441,11 @@ export class DockviewComponent if (this._moving) { return; } + if (event.panel !== this.activePanel) { return; } + if (this._onDidActivePanelChange.value !== event.panel) { this._onDidActivePanelChange.fire(event.panel); } @@ -2489,4 +2528,44 @@ export class DockviewComponent ? rootOrientation : orthogonal(rootOrientation); } + + private updateDropTargetModel(options: Partial) { + if ('dndEdges' in options) { + this._rootDropTarget.disabled = + typeof options.dndEdges === 'boolean' && + options.dndEdges === false; + + if ( + typeof options.dndEdges === 'object' && + options.dndEdges !== null + ) { + this._rootDropTarget.setOverlayModel(options.dndEdges); + } else { + this._rootDropTarget.setOverlayModel( + DEFAULT_ROOT_OVERLAY_MODEL + ); + } + } + + if ('rootOverlayModel' in options) { + this.updateDropTargetModel({ dndEdges: options.dndEdges }); + } + } + + private updateTheme(): void { + const theme = this._options.theme ?? themeAbyss; + this._themeClassnames.setClassNames(theme.className); + + this.gridview.margin = theme.gap ?? 0; + + switch (theme.dndOverlayMounting) { + case 'absolute': + this.rootDropTargetContainer.disabled = false; + break; + case 'relative': + default: + this.rootDropTargetContainer.disabled = true; + break; + } + } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index a34d5ef10..c56babf91 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -39,6 +39,7 @@ import { import { OverlayRenderContainer } from '../overlay/overlayRenderContainer'; import { TitleEvent } from '../api/dockviewPanelApi'; import { Contraints } from '../gridview/gridviewPanel'; +import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; interface GroupMoveEvent { groupId: string; @@ -265,6 +266,8 @@ export class DockviewGroupPanelModel private mostRecentlyUsed: IDockviewPanel[] = []; private _overwriteRenderContainer: OverlayRenderContainer | null = null; + private _overwriteDropTargetContainer: DropTargetAnchorContainer | null = + null; private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = @@ -535,6 +538,17 @@ export class DockviewGroupPanelModel ); } + set dropTargetContainer(value: DropTargetAnchorContainer | null) { + this._overwriteDropTargetContainer = value; + } + + get dropTargetContainer(): DropTargetAnchorContainer | null { + return ( + this._overwriteDropTargetContainer ?? + this.accessor.rootDropTargetContainer + ); + } + initialize(): void { if (this.options.panels) { this.options.panels.forEach((panel) => { @@ -1049,6 +1063,29 @@ export class DockviewGroupPanelModel const data = getPanelData(); if (data && data.viewId === this.accessor.id) { + if (type === 'content') { + if (data.groupId === this.id) { + // don't allow to drop on self for center position + + if (position === 'center') { + return; + } + + if (data.panelId === null) { + // don't allow group move to drop anywhere on self + return; + } + } + } + + if (type === 'header') { + if (data.groupId === this.id) { + if (data.panelId === null) { + return; + } + } + } + if (data.panelId === null) { // this is a group move dnd event const { groupId } = data; diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 3f7b94367..4b096c941 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -17,6 +17,7 @@ import { IGroupHeaderProps } from './framework'; import { FloatingGroupOptions } from './dockviewComponent'; import { Contraints } from '../gridview/gridviewPanel'; import { AcceptableEvent, IAcceptableEvent } from '../events'; +import { DockviewTheme } from './theme'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -51,19 +52,26 @@ export interface DockviewOptions { }; popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; - debug?: boolean; - rootOverlayModel?: DroptargetOverlayModel; - locked?: boolean; - disableDnd?: boolean; - className?: string; /** - * Pixel gap between groups + * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. */ gap?: number; + debug?: boolean; + // #start dnd + dndEdges?: false | DroptargetOverlayModel; + /** + * @deprecated use `dndEdges` instead. To be removed in a future version. + * */ + rootOverlayModel?: DroptargetOverlayModel; + disableDnd?: boolean; + // #end dnd + locked?: boolean; + className?: string; /** * Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`. */ noPanelsOverlay?: 'emptyGroup' | 'watermark'; + theme?: DockviewTheme; } export interface DockviewDndOverlayEvent extends IAcceptableEvent { @@ -106,9 +114,11 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { rootOverlayModel: undefined, locked: undefined, disableDnd: undefined, - gap: undefined, className: undefined, noPanelsOverlay: undefined, + dndEdges: undefined, + theme: undefined, + gap: undefined, }; return Object.keys(properties) as (keyof DockviewOptions)[]; diff --git a/packages/dockview-core/src/dockview/theme.ts b/packages/dockview-core/src/dockview/theme.ts new file mode 100644 index 000000000..4a921e2db --- /dev/null +++ b/packages/dockview-core/src/dockview/theme.ts @@ -0,0 +1,54 @@ +export interface DockviewTheme { + name: string; + className: string; + gap?: number; + dndOverlayMounting?: 'absolute' | 'relative'; + includeHeaderWhenHoverOverContent?: boolean; +} + +export const themeDark: DockviewTheme = { + name: 'dark', + className: 'dockview-theme-dark', +}; + +export const themeLight: DockviewTheme = { + name: 'light', + className: 'dockview-theme-light', +}; + +export const themeVisualStudio: DockviewTheme = { + name: 'visualStudio', + className: 'dockview-theme-vs', +}; + +export const themeAbyss: DockviewTheme = { + name: 'abyss', + className: 'dockview-theme-abyss', +}; + +export const themeDracula: DockviewTheme = { + name: 'dracula', + className: 'dockview-theme-dracula', +}; + +export const themeReplit: DockviewTheme = { + name: 'replit', + className: 'dockview-theme-replit', + gap: 10, +}; + +export const themeAbyssSpaced: DockviewTheme = { + name: 'abyssSpaced', + className: 'dockview-theme-abyss-spaced', + gap: 10, + dndOverlayMounting: 'absolute', + includeHeaderWhenHoverOverContent: true, +}; + +export const themeLightSpaced: DockviewTheme = { + name: 'lightSpaced', + className: 'dockview-theme-light-spaced', + gap: 10, + dndOverlayMounting: 'absolute', + includeHeaderWhenHoverOverContent: true, +}; diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 9764f658a..d9f66a787 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -64,6 +64,7 @@ export { } from './dockview/framework'; export * from './dockview/options'; +export * from './dockview/theme'; export * from './dockview/dockviewPanel'; export { DefaultTab } from './dockview/components/tab/defaultTab'; export { diff --git a/packages/dockview-core/src/splitview/splitview.scss b/packages/dockview-core/src/splitview/splitview.scss index adf09b368..047b382cf 100644 --- a/packages/dockview-core/src/splitview/splitview.scss +++ b/packages/dockview-core/src/splitview/splitview.scss @@ -116,6 +116,7 @@ -moz-user-select: none; // Firefox -ms-user-select: none; // IE 10 and IE 11 touch-action: none; + background-color: var(--dv-sash-color, transparent); &:not(.disabled):active { transition: background-color 0.1s ease-in-out; diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 77fbb9cf5..c511d54f6 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -219,6 +219,8 @@ export class Splitview { set margin(value: number) { this._margin = value; + + toggleClass(this.element, 'dv-splitview-has-margin', value !== 0); } constructor( diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 5eb3f7442..ee28b9565 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -1,17 +1,29 @@ +@import 'theme/_sash-handle-mixin'; +@import 'theme/_drop-target-static-mixin'; +@import 'theme/_space-mixin'; + @mixin dockview-theme-core-mixin { --dv-paneview-active-outline-color: dodgerblue; --dv-tabs-and-actions-container-font-size: 13px; --dv-tabs-and-actions-container-height: 35px; --dv-drag-over-background-color: rgba(83, 89, 93, 0.5); - --dv-drag-over-border-color: white; + --dv-drag-over-border-color: transparent; --dv-tabs-container-scrollbar-color: #888; --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); --dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5); --dv-overlay-z-index: 999; + // + + --dv-tab-font-size: inherit; + --dv-border-radius: 0px; + --dv-tab-margin: 0; + --dv-sash-color: transparent; + --dv-active-sash-color: transparent; } @mixin dockview-theme-dark-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); // --dv-group-view-background-color: #1e1e1e; @@ -35,6 +47,8 @@ @mixin dockview-theme-light-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: white; // @@ -131,30 +145,49 @@ @mixin dockview-theme-abyss-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + + --dv-color-abyss-dark: #000c18; + --dv-color-abyss: #10192c; + --dv-color-abyss-light: #1c1c2a; + --dv-color-abyss-lighter: #2b2b4a; + --dv-color-abyss-accent: rgb(91, 30, 207); + + --dv-color-abyss-primary-text: white; + --dv-color-abyss-secondary-text: rgb(148, 151, 169); + // - --dv-group-view-background-color: #000c18; + --dv-group-view-background-color: var(--dv-color-abyss-dark); // - --dv-tabs-and-actions-container-background-color: #1c1c2a; + --dv-tabs-and-actions-container-background-color: var( + --dv-color-abyss-light + ); // - --dv-activegroup-visiblepanel-tab-background-color: #000c18; - --dv-activegroup-hiddenpanel-tab-background-color: #10192c; - --dv-inactivegroup-visiblepanel-tab-background-color: #000c18; - --dv-inactivegroup-hiddenpanel-tab-background-color: #10192c; - --dv-tab-divider-color: #2b2b4a; + --dv-activegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-dark + ); + --dv-activegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss); + --dv-inactivegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-dark + ); + --dv-inactivegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss); + --dv-tab-divider-color: var(--dv-color-abyss-lighter); // --dv-activegroup-visiblepanel-tab-color: white; --dv-activegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.5); --dv-inactivegroup-visiblepanel-tab-color: rgba(255, 255, 255, 0.5); --dv-inactivegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.25); // - --dv-separator-border: #2b2b4a; - --dv-paneview-header-border-color: #2b2b4a; + --dv-separator-border: var(--dv-color-abyss-lighter); + --dv-paneview-header-border-color: var(--dv-color-abyss-lighter); --dv-paneview-active-outline-color: #596f99; } @mixin dockview-theme-dracula-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: #282a36; // @@ -229,10 +262,17 @@ } @mixin dockview-design-replit-mixin { + @include dockview-drop-target-no-travel(); + .dv-resize-container:has(> .dv-groupview) { border-radius: 8px; } + .dv-resize-container { + border-radius: 10px !important; + border: none; + } + .dv-groupview { overflow: hidden; border-radius: 10px; @@ -266,59 +306,16 @@ border: 1px solid transparent; } } - - .dv-vertical > .dv-sash-container > .dv-sash { - &:not(.disabled) { - &::after { - content: ''; - height: 4px; - width: 40px; - border-radius: 2px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: var(--dv-separator-handle-background-color); - position: absolute; - } - - &:hover { - &::after { - background-color: var( - --dv-separator-handle-hover-background-color - ); - } - } - } - } - - .dv-horizontal > .dv-sash-container > .dv-sash { - &:not(.disabled) { - &::after { - content: ''; - height: 40px; - width: 4px; - border-radius: 2px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: var(--dv-separator-handle-background-color); - position: absolute; - } - - &:hover { - &::after { - background-color: var( - --dv-separator-handle-hover-background-color - ); - } - } - } - } } .dockview-theme-replit { @include dockview-theme-core-mixin(); @include dockview-design-replit-mixin(); + @include dockview-design-handle-mixin(); + + padding: 10px; + background-color: #ebeced; + // --dv-group-view-background-color: #ebeced; // @@ -339,6 +336,115 @@ --dv-paneview-header-border-color: rgb(51, 51, 51); ///// - --dv-separator-handle-background-color: #cfd1d3; - --dv-separator-handle-hover-background-color: #babbbb; + --dv-sash-color: #cfd1d3; + --dv-active-sash-color: #babbbb; +} + +.dockview-theme-abyss-spaced { + @include dockview-theme-core-mixin(); + @include dockview-design-space-mixin(); + + // stylesheet + --dv-color-abyss-dark: rgb(11, 6, 17); + --dv-color-abyss: #16121f; + --dv-color-abyss-light: #201d2b; + --dv-color-abyss-lighter: #2a2837; + --dv-color-abyss-accent: rgb(91, 30, 207); + --dv-color-abyss-primary-text: white; + --dv-color-abyss-secondary-text: rgb(148, 151, 169); + + // + --dv-drag-over-border: 2px solid var(--dv-color-abyss-accent); + --dv-drag-over-background-color: ''; + // + + // + --dv-group-view-background-color: var(--dv-color-abyss-dark); + // + --dv-tabs-and-actions-container-background-color: var(--dv-color-abyss); + // + --dv-activegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-lighter + ); + --dv-activegroup-hiddenpanel-tab-background-color: var( + --dv-color-abyss-light + ); + --dv-inactivegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-lighter + ); + --dv-inactivegroup-hiddenpanel-tab-background-color: var( + --dv-color-abyss-light + ); + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: var(--dv-color-abyss-primary-text); + --dv-activegroup-hiddenpanel-tab-color: var( + --dv-color-abyss-secondary-text + ); + --dv-inactivegroup-visiblepanel-tab-color: var( + --dv-color-abyss-primary-text + ); + --dv-inactivegroup-hiddenpanel-tab-color: var( + --dv-color-abyss-secondary-text + ); + // + --dv-separator-border: transparent; + --dv-paneview-header-border-color: rgb(51, 51, 51); + + ///// + --dv-active-sash-color: var(--dv-color-abyss-accent); + // + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.5); + + padding: 10px; + background-color: var(--dv-color-abyss-dark); + + .dv-resize-container { + .dv-groupview { + border: 2px solid var(--dv-color-abyss-dark); + } + } +} + +.dockview-theme-light-spaced { + @include dockview-theme-core-mixin(); + @include dockview-design-space-mixin(); + + // + --dv-drag-over-border: 2px solid rgb(91, 30, 207); + --dv-drag-over-background-color: ''; + // + + // + --dv-group-view-background-color: #f6f5f9; + // + --dv-tabs-and-actions-container-background-color: white; + // + --dv-activegroup-visiblepanel-tab-background-color: #ededf0; + --dv-activegroup-hiddenpanel-tab-background-color: #f9f9fa; + --dv-inactivegroup-visiblepanel-tab-background-color: #ededf0; + --dv-inactivegroup-hiddenpanel-tab-background-color: #f9f9fa; + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: rgb(104, 107, 130); + --dv-activegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + --dv-inactivegroup-visiblepanel-tab-color: rgb(104, 107, 130); + --dv-inactivegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + // + --dv-separator-border: transparent; + --dv-paneview-header-border-color: rgb(51, 51, 51); + + ///// + --dv-active-sash-color: rgb(91, 30, 207); + // + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.1); + + padding: 10px; + background-color: #f6f5f9; + + .dv-resize-container { + .dv-groupview { + border: 2px solid rgb(255, 255, 255, 0.1); + } + } } diff --git a/packages/dockview-core/src/theme/_drop-target-static-mixin.scss b/packages/dockview-core/src/theme/_drop-target-static-mixin.scss new file mode 100644 index 000000000..b2e0e4ba7 --- /dev/null +++ b/packages/dockview-core/src/theme/_drop-target-static-mixin.scss @@ -0,0 +1,10 @@ +@mixin dockview-drop-target-no-travel { + .dv-drop-target-container { + .dv-drop-target-anchor { + &.dv-drop-target-anchor-container-changed { + opacity: 0; + transition: none; + } + } + } +} diff --git a/packages/dockview-core/src/theme/_sash-handle-mixin.scss b/packages/dockview-core/src/theme/_sash-handle-mixin.scss new file mode 100644 index 000000000..31e5822da --- /dev/null +++ b/packages/dockview-core/src/theme/_sash-handle-mixin.scss @@ -0,0 +1,53 @@ +@mixin dockview-design-handle-mixin { + .dv-vertical > .dv-sash-container > .dv-sash { + background-color: transparent; + + &:not(.disabled) { + &::after { + content: ''; + height: 4px; + width: 40px; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--dv-sash-color); + position: absolute; + } + + &:hover, + &:active { + background-color: transparent; + &::after { + background-color: var(--dv-active-sash-color); + } + } + } + } + + .dv-horizontal > .dv-sash-container > .dv-sash { + background-color: transparent; + + &:not(.disabled) { + &::after { + content: ''; + height: 40px; + width: 4px; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--dv-sash-color); + position: absolute; + } + + &:hover, + &:active { + background-color: transparent; + &::after { + background-color: var(--dv-active-sash-color); + } + } + } + } +} diff --git a/packages/dockview-core/src/theme/_space-mixin.scss b/packages/dockview-core/src/theme/_space-mixin.scss new file mode 100644 index 000000000..b2e84cdbd --- /dev/null +++ b/packages/dockview-core/src/theme/_space-mixin.scss @@ -0,0 +1,52 @@ +@mixin dockview-design-space-mixin { + --dv-tab-font-size: 12px; + --dv-border-radius: 20px; + --dv-tab-margin: 0.5rem 0.25rem; + --dv-tabs-and-actions-container-height: 44px; + + + --dv-border-radius + + .dv-resize-container:has(> .dv-groupview) { + border-radius: 8px; + } + + .dv-sash { + border-radius: 4px; + } + + .dv-drop-target-anchor { + border-radius: calc(var(--dv-border-radius) / 4); + &.dv-drop-target-content { + border-radius: var(--dv-border-radius); + } + } + + .dv-resize-container { + border-radius: var(--dv-border-radius) !important; + border: none; + } + + .dv-groupview { + border-radius: var(--dv-border-radius); + + .dv-tabs-and-actions-container { + padding: 0px calc(var(--dv-border-radius) / 2); + + .dv-tab { + border-radius: 8px; + + .dv-svg { + height: 8px; + width: 8px; + } + } + } + + .dv-content-container { + background-color: var( + --dv-tabs-and-actions-container-background-color + ); + } + } +} diff --git a/packages/dockview/src/svg.tsx b/packages/dockview/src/svg.tsx index 76143411a..eccf52bb6 100644 --- a/packages/dockview/src/svg.tsx +++ b/packages/dockview/src/svg.tsx @@ -7,7 +7,7 @@ export const CloseButton = () => ( viewBox="0 0 28 28" aria-hidden={'false'} focusable={false} - className="dockview-svg" + className="dv-svg" > @@ -21,7 +21,7 @@ export const ExpandMore = () => { viewBox="0 0 24 15" aria-hidden={'false'} focusable={false} - className="dockview-svg" + className="dv-svg" > diff --git a/packages/docs/docs/overview/getStarted/theme.mdx b/packages/docs/docs/overview/getStarted/theme.mdx index 2e89eefe6..6240340b9 100644 --- a/packages/docs/docs/overview/getStarted/theme.mdx +++ b/packages/docs/docs/overview/getStarted/theme.mdx @@ -7,7 +7,9 @@ title: Theme import { CSSVariablesTable, ThemeTable } from '@site/src/components/cssVariables'; -Theming is controlled through CSS and is highly customizable. + +Dockview components accept a `theme` property which is highly customizable, the theme is largly controlled through CSS however some properties can only be adjusted +by direct editing variables of the `theme` object. Firstly, you should import `dockview.css`: @@ -38,7 +40,7 @@ To use a `dockview` theme the CSS must encapsulate the component. The current li :::info -The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss). +The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss) and the associated CSS [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss). ::: ## Customizing Theme diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss index 57549c075..2f7a940fc 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss @@ -11,6 +11,7 @@ &:hover { border-radius: 2px; + color: var(--dv-activegroup-visiblepanel-tab-color); background-color: var(--dv-icon-hover-background-color); } } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx index e1ccaef0f..a69632ebf 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -5,6 +5,7 @@ import { IDockviewPanelHeaderProps, IDockviewPanelProps, DockviewApi, + DockviewTheme, } from 'dockview'; import * as React from 'react'; import './app.scss'; @@ -80,6 +81,7 @@ const components = { ); }, nested: (props: IDockviewPanelProps) => { + const theme = React.useContext(ThemeContext); return ( ); }, @@ -141,7 +143,9 @@ const WatermarkComponent = () => { return
custom watermark
; }; -const DockviewDemo = (props: { theme?: string }) => { +const ThemeContext = React.createContext(undefined); + +const DockviewDemo = (props: { theme?: DockviewTheme }) => { const [logLines, setLogLines] = React.useState< { text: string; timestamp?: Date; backgroundColor?: string }[] >([]); @@ -380,18 +384,22 @@ const DockviewDemo = (props: { theme?: string }) => { }} > - + + + diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx index 63032b5f4..c9fd5e19f 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx @@ -81,7 +81,7 @@ export const RightControls = (props: IDockviewHeaderActionsProps) => { alignItems: 'center', padding: '0px 8px', height: '100%', - color: 'var(--dv-activegroup-visiblepanel-tab-color)', + color: 'var(--dv-activegroup-hiddenpanel-tab-color)', }} > {props.isGroupActive && } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx index 40e57b2fa..ec16135ed 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx @@ -151,11 +151,20 @@ export const GridActions = (props: { props.api?.addGroup(); }; - const [gap, setGap] = React.useState(0); + // const [gap, setGap] = React.useState(undefined); - React.useEffect(() => { - props.api?.setGap(gap); - }, [gap, props.api]); + const [overlayMode, setOverlayMode] = React.useState(false); + + // React.useEffect(() => { + // if (!props.api) { + // return; + // } + // if (typeof gap === 'number') { + // props.api.setGap(gap); + // } else { + // setGap(props.api.gap); + // } + // }, [gap, props.api]); return (
@@ -191,6 +200,23 @@ export const GridActions = (props: { Use Custom Watermark + {/* + + */} @@ -204,7 +230,7 @@ export const GridActions = (props: { Reset -
+ {/*
Grid Gap setGap(Number(event.target.value))} /> -
+
*/}
); }; diff --git a/packages/docs/src/components/frameworkSpecific.css b/packages/docs/src/components/frameworkSpecific.css index a92d63d29..9a03811a8 100644 --- a/packages/docs/src/components/frameworkSpecific.css +++ b/packages/docs/src/components/frameworkSpecific.css @@ -1,13 +1,16 @@ .DropdownMenuContent { /* min-width: 220px; */ - background-color: rgba(255, 255, 255, 0.1); + background-color: var(--ifm-dropdown-background-color); + color: var(--ifm-color-primary); + border: var(--ifm-dropdown-border); border-radius: 6px; padding: 5px; box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2); animation-duration: 400ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); will-change: transform, opacity; + z-index: 99; } .DropdownMenuContent[data-side='top'], .DropdownMenuSubContent[data-side='top'] { @@ -39,25 +42,33 @@ display: flex; align-items: center; justify-content: space-between; - width: 100px; - height: 25px; + width: 120px; + /* height: 25px; */ padding: 4px 8px; - font-size: 13px; + font-size: 1rem; + font-weight: normal; cursor: pointer; + + color: var(--ifm-menu-color); + + &:hover { + background-color: var(--ifm-hover-overlay); + } } .framework-menu-item-select { display: flex; align-items: center; justify-content: space-between; - width: 120px; + width: 130px; height: 35px; padding: 4px 8px; border-radius: 6px; - font-size: 13px; - background-color: rgba(255, 255, 255, 0.1); + font-size: 1rem; + font-weight: normal; cursor: pointer; - border: 1px solid rgba(0,0,0, 0.1); + + border: 1px solid rgba(60, 60, 66,0.5); } @keyframes slideUpAndFade { diff --git a/packages/docs/src/components/frameworkSpecific.tsx b/packages/docs/src/components/frameworkSpecific.tsx index 65ae1d549..14d8ea830 100644 --- a/packages/docs/src/components/frameworkSpecific.tsx +++ b/packages/docs/src/components/frameworkSpecific.tsx @@ -1,6 +1,8 @@ import BrowserOnly from '@docusaurus/BrowserOnly'; import { DockviewEmitter } from 'dockview'; import * as React from 'react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import useBaseUrl from '@docusaurus/useBaseUrl'; import './frameworkSpecific.css'; export interface FrameworkDescriptor { @@ -51,8 +53,7 @@ export function useActiveFramework(): [ return [option, setter]; } -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; -import useBaseUrl from '@docusaurus/useBaseUrl'; + const FrameworkSelector1 = () => { const [activeFramework, setActiveFramework] = useActiveFramework(); diff --git a/packages/docs/src/components/ui/codeSandboxButton.scss b/packages/docs/src/components/ui/codeSandboxButton.scss index 8c31be3b8..5fda1291b 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.scss +++ b/packages/docs/src/components/ui/codeSandboxButton.scss @@ -28,7 +28,7 @@ } } -.dockview-svg { +.dv-svg { display: inline-block; fill: currentcolor; line-height: 1; diff --git a/packages/docs/src/components/ui/codeSandboxButton.tsx b/packages/docs/src/components/ui/codeSandboxButton.tsx index a68aa34d7..c2c6e02ff 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.tsx +++ b/packages/docs/src/components/ui/codeSandboxButton.tsx @@ -17,7 +17,7 @@ const createSvgElementFromPath = (params: { width={params.width} viewBox={params.viewbox} focusable={false} - className={'dockview-svg'} + className={'dv-svg'} > @@ -54,7 +54,7 @@ export const CodeSandboxButton = (props: { { return ( { const JavascriptIcon = (props: { height: number; width: number }) => { return ( { return (
{ diff --git a/packages/docs/src/config/theme.config.ts b/packages/docs/src/config/theme.config.ts index 2e8b9741f..0946720df 100644 --- a/packages/docs/src/config/theme.config.ts +++ b/packages/docs/src/config/theme.config.ts @@ -1,33 +1,54 @@ +import { + themeAbyss, + themeDark, + themeDracula, + themeAbyssSpaced, + themeLightSpaced, + themeLight, + themeReplit, + themeVisualStudio, +} from 'dockview'; + export const themeConfig = [ { - id: 'dockview-theme-dark', + id: themeDark, key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**', text: '', }, { - id: 'dockview-theme-light', + id: themeLight, key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**', text: '', }, { - id: 'dockview-theme-vs', + id: themeVisualStudio, key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**', text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)', }, { - id: 'dockview-theme-abyss', + id: themeAbyss, key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme', }, { - id: 'dockview-theme-dracula', + id: themeDracula, key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme', }, { - id: 'dockview-theme-replit', + id: themeReplit, key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**', text: 'Based on [Replit](https://replit.com)', }, + { + id: themeLightSpaced, + key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + text: '', + }, + { + id: themeAbyssSpaced, + key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + text: '', + }, ]; diff --git a/packages/docs/src/css/custom.scss b/packages/docs/src/css/custom.scss index 9d446add3..0ab665892 100644 --- a/packages/docs/src/css/custom.scss +++ b/packages/docs/src/css/custom.scss @@ -11,10 +11,10 @@ /* You can override the default Infima variables here. */ :root { - --ifm-font-family-base: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, - BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, - sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, - Noto Color Emoji; + --ifm-font-family-base: 'IBM Plex Sans', ui-sans-serif, system-ui, + -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, + Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, + Segoe UI Symbol, Noto Color Emoji; --ifm-font-weight-bold: 600; @@ -36,6 +36,9 @@ --ifm-color-primary: black; + --ifm-dropdown-background-color: white; + --ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest); + --ifm-navbar-link-color: white; --ifm-navbar-link-hover-color: white; @@ -54,15 +57,18 @@ } /* --ifm-color-primary: #0c111d; */ - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; + --ifm-color-primary: #98a2b3; + --ifm-color-primary-dark: #828a99; + --ifm-color-primary-darker: #6a707c; + --ifm-color-primary-darkest: #474b53; + --ifm-color-primary-light: #acb7ca; + --ifm-color-primary-lighter: #bcc9df; + --ifm-color-primary-lightest: #d2e1fa; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --ifm-dropdown-background-color: #373d4b; + --ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest); + --dv-docs-markdown-text-color: #cdced8; } diff --git a/packages/docs/src/pages/demo.tsx b/packages/docs/src/pages/demo.tsx index cab33d7ef..d04d76f60 100644 --- a/packages/docs/src/pages/demo.tsx +++ b/packages/docs/src/pages/demo.tsx @@ -3,11 +3,29 @@ import Layout from '@theme/Layout'; import { themeConfig } from '../config/theme.config'; import ExampleFrame from '../components/ui/exampleFrame'; import BrowserOnly from '@docusaurus/BrowserOnly'; +import { DockviewTheme, themeAbyss } from 'dockview'; + +const updateTheme = (theme: DockviewTheme) => { + const urlParams = new URLSearchParams(window.location.search); + + urlParams.set('theme', theme.name); + + const newUrl = window.location.pathname + '?' + urlParams.toString(); + + window.history.pushState({ path: newUrl }, '', newUrl); +}; const ThemeToggle: React.FC = () => { - const [theme, setTheme] = React.useState( - new URLSearchParams(location.search).get('theme') ?? themeConfig[3].id - ); + const [theme, setTheme] = React.useState(themeAbyss); + + React.useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const themeName = urlParams.get('theme'); + const newTheme = + themeConfig.find((c) => c.id.name === themeName)?.id ?? themeAbyss; + setTheme(newTheme); + updateTheme(newTheme); + }, []); return ( <> @@ -16,20 +34,48 @@ const ThemeToggle: React.FC = () => { height: '40px', display: 'flex', alignItems: 'center', + padding: '0px 15px', }} > - { - const url = new URL(window.location.href); - url.searchParams.set('theme', event.target.value); - window.location.href = url.toString(); + const theme = themeConfig.find( + (theme) => theme.id.name === event.target.value + ).id; + setTheme(theme); + updateTheme(theme); }} - value={theme} + value={theme.name} > {themeConfig.map((theme) => { - return ; + return ( + + ); })} - + */}
); } + +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@radix-ui/react-dropdown-menu'; + +const ThemeSelector = (props: { + options: string[]; + value: string; + onChanged: (value: string) => void; +}) => { + const ref = React.useRef(null); + + return ( +
+ { + if (!open) { + return; + } + + if (!ref.current) { + return; + } + + requestAnimationFrame(() => { + const el = ref.current!.querySelector( + `[data-dropdown-menu-value="${props.value}"]` + ); + if (el) { + (el as HTMLElement).focus(); + } + }); + }} + > + +
+ {props.value} +
+
+ + {props.options.map((option) => { + return ( + props.onChanged(option)} + className="DropdownMenuItem" + > +
+ {option} + + {option === props.value ? '✓' : ''} + +
+
+ ); + })} +
+
+
+ ); +};