chore: remove old demo project

This commit is contained in:
mathuo 2022-07-23 16:31:23 +01:00
parent 9e6f0877f1
commit f8fca83166
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
83 changed files with 0 additions and 6740 deletions

View File

@ -1,8 +0,0 @@
{
"extends": [
"../../.eslintrc"
],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}

View File

@ -1,23 +0,0 @@
module.exports = {
stories: [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
{
name: '@storybook/addon-docs',
options: {
sourceLoaderOptions: {
injectStoryParameters: false,
},
},
},
"@storybook/addon-storysource",
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-controls"
],
core: {
builder: "webpack5"
}
}

View File

@ -1,5 +0,0 @@
import '!style-loader!css-loader!sass-loader!../node_modules/dockview/dist/styles/dockview.css';
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
}

View File

@ -1,51 +0,0 @@
{
"name": "dockview-demo",
"private": true,
"version": "1.5.1",
"description": "Demo project for https://github.com/mathuo/dockview",
"scripts": {
"build": "npm run build-webpack",
"build-webpack": "cross-env ../../node_modules/.bin/webpack --config webpack.config.js",
"start": "cross-env ../../node_modules/.bin/webpack serve --config webpack.config.js",
"clean": "rimraf dist/ .build/",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"author": "https://github.com/mathuo",
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^2.1.1",
"dockview": "^1.5.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"recoil": "^0.7.2"
},
"devDependencies": {
"@babel/core": "^7.17.10",
"@storybook/addon-actions": "^6.5.0-beta.0",
"@storybook/addon-controls": "^6.5.0-beta.0",
"@storybook/addon-docs": "^6.5.0-beta.0",
"@storybook/addon-essentials": "^6.5.0-beta.0",
"@storybook/addon-links": "^6.5.0-beta.0",
"@storybook/addon-storysource": "^6.5.0-beta.0",
"@storybook/builder-webpack5": "^6.5.0-beta.0",
"@storybook/manager-webpack5": "^6.5.0-beta.0",
"@storybook/react": "^6.5.0-beta.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"babel-loader": "^8.2.5",
"cross-env": "^7.0.3",
"rimraf": "^3.0.2",
"source-map-loader": "^3.0.1"
},
"resolutions": {
"webpack": "^5.0.0",
"css-loader": "^5.0.0",
"dotenv-webpack": "^6.0.0",
"html-webpack-plugin": "^5.0.0",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.0.0",
"webpack-dev-middleware": "^4.1.0",
"webpack-virtual-modules": "^0.4.2"
}
}

View File

@ -1,14 +0,0 @@
<!-- <!DOCTYPE html> -->
<html>
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<div id="anchor"></div>
<script src="/dist/bundle.js"></script>
</body>
</html>

View File

@ -1,13 +0,0 @@
export const toggleClass = (
element: HTMLElement,
className: string,
isToggled: boolean
) => {
const hasClass = element.classList.contains(className);
if (isToggled && !hasClass) {
element.classList.add(className);
}
if (!isToggled && hasClass) {
element.classList.remove(className);
}
};

View File

@ -1,122 +0,0 @@
import { IDisposable } from './lifecycle';
export interface Event<T> {
(listener: (e: T) => any): IDisposable;
}
export interface EmitterOptions {
replay?: boolean;
}
export namespace Event {
export const any = <T>(...children: Event<T>[]): Event<T> => {
return (listener: (e: T) => void) => {
const disposables = children.map((child) => child(listener));
return {
dispose: () => {
disposables.forEach((d) => {
d.dispose();
});
},
};
};
};
}
// dumb event emitter with better typings than nodes event emitter
// https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts
export class Emitter<T> implements IDisposable {
private _event?: Event<T>;
private _last?: T;
private _listeners: Array<(e: T) => any> = [];
private _disposed = false;
constructor(private readonly options?: EmitterOptions) {}
get event() {
if (!this._event) {
this._event = (listener: (e: T) => void): IDisposable => {
if (this.options?.replay && this._last !== undefined) {
listener(this._last);
}
this._listeners.push(listener);
return {
dispose: () => {
const index = this._listeners.indexOf(listener);
if (index > -1) {
this._listeners.splice(index, 1);
}
},
};
};
}
return this._event;
}
public fire(e: T) {
this._last = e;
for (const listener of this._listeners) {
listener(e);
}
}
public dispose() {
this._listeners = [];
this._disposed = true;
}
}
export function addDisposableWindowListener<K extends keyof WindowEventMap>(
element: Window,
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): IDisposable {
element.addEventListener(type, listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener);
},
};
}
export function addDisposableListener<K extends keyof HTMLElementEventMap>(
element: HTMLElement,
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): IDisposable {
element.addEventListener(type, listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener);
},
};
}
export class TickDelayedEvent implements IDisposable {
private timer: any;
private readonly _onFired = new Emitter<void>();
readonly onEvent = this._onFired.event;
fire(): void {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this._onFired.fire();
clearTimeout(this.timer);
});
}
dispose(): void {
this._onFired.dispose();
}
}

View File

@ -1,66 +0,0 @@
@import '~dockview/dist/styles/dockview.css';
body {
margin: 0;
font-family: 'Roboto', sans-serif;
overflow: hidden;
color: rgb(204, 204, 204);
font-size: 13px;
}
::-webkit-scrollbar {
height: 8px;
width: 8px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
// *,
// *::after,
// *::before {
// -webkit-user-drag: none;
// -webkit-app-region: no-drag;
// -webkit-user-select: none;
// }
.close-action {
&:active {
-webkit-mask-size: 100% 100% !important;
mask-size: 100% 100% !important;
}
background-color: white;
height: 16px;
width: 16px;
display: block;
-webkit-mask: var(--dv-tab-close-icon) 50% 50% / 90% 90% no-repeat;
mask: var(--dv-tab-close-icon) 50% 50% / 90% 90% no-repeat;
margin-right: '0.5em';
cursor: pointer;
}
button {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
border: none;
background-color: rgb(14, 99, 156);
color: white;
font-family: inherit;
outline: none;
padding: 2px 14px;
margin: 2px 0px;
&:hover {
background-color: rgb(17, 119, 187);
cursor: pointer;
}
}

View File

@ -1,15 +0,0 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { Application } from './layout-grid/application';
import { RecoilRoot } from 'recoil';
import './index.scss';
document.getElementById('app').classList.add('dockview-theme-dark');
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(
<RecoilRoot>
<Application />
</RecoilRoot>
);

View File

@ -1,34 +0,0 @@
.activity-bar {
height: 100%;
background-color: rgb(51, 51, 51);
.activity-bar-item {
width: 100%;
padding-bottom: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
position: relative;
&:hover {
background-color: rgba(90, 90, 90, 0.5);
}
.activity-bar-item-image {
-webkit-mask-size: 65% 65%;
mask-size: 65% 65%;
-webkit-mask-position: 50% 50%;
mask-position: 50% 50%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
height: 100%;
width: 100%;
display: block;
background-color: gray;
position: absolute;
top: 0;
left: 0;
}
}
}

View File

@ -1,71 +0,0 @@
import * as React from 'react';
import { GridviewApi, IGridviewPanelProps } from 'dockview';
import './activitybar.scss';
import { useLayoutRegistry } from './registry';
import { CompositeDisposable } from '../lifecycle';
const ActivitybarImage = (props: { url: string }) => (
<a
style={{
WebkitMaskImage: `url(${props.url})`,
maskImage: `url(${props.url})`,
}}
className="activity-bar-item-image"
/>
);
export const Activitybar = (props: IGridviewPanelProps) => {
const registry = useLayoutRegistry();
const [isActive, setActive] = React.useState<boolean>();
const onOpenSidebar = (event: React.MouseEvent<HTMLDivElement>) => {
const api = registry.get<GridviewApi>('gridview');
const sidebarPanel = api.getPanel('sidebar');
if (sidebarPanel.api.isVisible) {
sidebarPanel.api.setVisible(false);
} else {
event.preventDefault(); // prevent focus
sidebarPanel.api.setVisible(true);
sidebarPanel.focus();
}
};
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidActiveChange((event) => {
setActive(event.isActive);
})
);
return () => {
disposable.dispose();
};
}, []);
return (
<div className="activity-bar" onClick={onOpenSidebar}>
<div className="activity-bar-item">
<ActivitybarImage
url={
'https://fonts.gstatic.com/s/i/materialicons/search/v7/24px.svg'
}
/>
</div>
<div className="activity-bar-item">
<ActivitybarImage
url={
'https://fonts.gstatic.com/s/i/materialicons/search/v7/24px.svg'
}
/>
</div>
<div className="activity-bar-item">
<ActivitybarImage
url={
'https://fonts.gstatic.com/s/i/materialicons/search/v7/24px.svg'
}
/>
</div>
</div>
);
};

View File

@ -1,108 +0,0 @@
import * as React from 'react';
import {
Orientation,
GridviewReact,
LayoutPriority,
GridviewReadyEvent,
IGridviewPanelProps,
SerializedGridview,
GridviewApi,
} from 'dockview';
import { Activitybar } from '../services/widgets';
import { Footer } from './footer';
import { Panel } from './panel';
import { TestGrid } from './layoutGrid';
import { useLayoutRegistry } from './registry';
import { Sidebar } from '../services/widgets';
const rootcomponents: {
[index: string]: React.FunctionComponent<IGridviewPanelProps>;
} = {
sidebar: Sidebar,
activitybar: Activitybar,
editor: TestGrid,
footer: Footer,
panel: Panel,
};
export const Application = () => {
const api = React.useRef<GridviewApi>();
const registry = useLayoutRegistry();
const onReady = (event: GridviewReadyEvent) => {
api.current = event.api;
registry.register('gridview', event.api);
let success = false;
const state = localStorage.getItem('dockview-layout');
if (state) {
try {
event.api.fromJSON(JSON.parse(state));
success = true;
} catch (err) {
console.error('failed to load layout', err);
}
}
if (!success) {
event.api.addPanel({
id: 'i',
component: 'activitybar',
minimumWidth: 48,
maximumWidth: 48,
location: [0],
});
event.api.addPanel({
id: 'footer',
component: 'footer',
location: [1],
maximumHeight: 22,
minimumHeight: 22,
});
event.api.addPanel({
id: 'editor',
component: 'editor',
snap: true,
location: [0, 1],
priority: LayoutPriority.High,
});
event.api.addPanel({
id: 'sidebar',
component: 'sidebar',
snap: true,
position: { referencePanel: 'editor', direction: 'left' },
minimumWidth: 170,
size: 100,
});
event.api.addPanel({
id: 'panel',
component: 'panel',
position: { referencePanel: 'editor', direction: 'below' },
size: 200,
snap: true,
});
}
event.api.onDidLayoutChange(() => {
localStorage.setItem(
'dockview-layout',
JSON.stringify(event.api.toJSON())
);
});
};
return (
<GridviewReact
components={rootcomponents}
onReady={onReady}
orientation={Orientation.VERTICAL}
hideBorders={true}
proportionalLayout={true}
/>
);
};

View File

@ -1,30 +0,0 @@
.control-center {
display: flex;
flex-direction: column;
height: 100%;
padding: 4px 8px;
box-sizing: border-box;
.control-center-row {
height: 25px;
box-sizing: border-box;
button {
width: 175px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
border: none;
background-color: rgb(14, 99, 156);
color: white;
font-family: inherit;
outline: none;
padding: 2px 14px;
margin: 2px 0px;
&:hover {
background-color: rgb(17, 119, 187);
}
}
}
}

View File

@ -1,150 +0,0 @@
import * as React from 'react';
import { DockviewApi, GridviewApi, SplitviewApi } from 'dockview';
import { useLayoutRegistry } from './registry';
import './controlCenter.scss';
export const ControlCenter = () => {
const registry = useLayoutRegistry();
const onAdd = () => {
const api = registry.get<DockviewApi>('dockview');
const _id = Date.now();
const id = `${_id}`;
api.addPanel({
component: 'test_component',
id,
title: `Item ${id}`,
});
};
const onAddTheSamePanel = () => {
const api = registry.get<DockviewApi>('dockview');
const id = `duplicate_panel`;
const panel = api.getPanel(id);
if (panel) {
panel.api.setActive();
return;
}
api.addPanel({
component: 'test_component',
id,
title: `Item ${id}`,
});
};
const onAddEmpty = () => {
const api = registry.get<DockviewApi>('dockview');
api.addEmptyGroup();
};
const nextPanel = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const api = registry.get<DockviewApi>('dockview');
api.moveToNext({ includePanel: true });
};
const nextGroup = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const api = registry.get<DockviewApi>('dockview');
api.moveToNext();
};
const previousPanel = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const api = registry.get<DockviewApi>('dockview');
api.moveToPrevious({ includePanel: true });
};
const previousGroup = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const api = registry.get<DockviewApi>('dockview');
api.moveToNext();
};
const onConfig = () => {
const api = registry.get<DockviewApi>('dockview');
const data = api.toJSON();
const stringData = JSON.stringify(data, null, 4);
console.log(stringData);
localStorage.setItem('layout', stringData);
};
const onLoad = async () => {
const api = registry.get<DockviewApi>('dockview');
api.closeAllGroups();
const data = localStorage.getItem('layout');
if (data) {
const jsonData = JSON.parse(data);
api.fromJSON(jsonData);
}
};
const onClear = () => {
const api = registry.get<DockviewApi>('dockview');
api.closeAllGroups();
};
const saveApplicationLayout = () => {
const api = registry.get<GridviewApi>('dockview');
console.log(JSON.stringify(api.toJSON(), null, 4));
};
const onAddSettings = () => {
const api = registry.get<DockviewApi>('dockview');
const settingsPanel = api.getPanel('settings');
if (settingsPanel) {
settingsPanel.api.setActive();
return;
}
api.addPanel({
id: 'settings',
component: 'settings',
title: 'Settings',
});
};
return (
<div className="control-center">
<div className="control-center-row">
<button onMouseDown={onAdd}>Add new</button>
</div>
<div className="control-center-row">
<button onMouseDown={onAddTheSamePanel}>Add identical</button>
</div>
<div className="control-center-row">
<button onMouseDown={onAddSettings}>Settings</button>
</div>
<div className="control-center-row">
<button onMouseDown={onAddEmpty}>Add empty</button>
</div>
<div className="control-center-row">
<button onMouseDown={nextPanel}>Next panel</button>
</div>
<div className="control-center-row">
<button onMouseDown={nextGroup}>Next Group</button>
</div>
<div className="control-center-row">
<button onMouseDown={previousPanel}>Previous Panel</button>
</div>
<div className="control-center-row">
<button onMouseDown={previousGroup}>Previous Group</button>
</div>
<div className="control-center-row">
<button onClick={onConfig}>Save</button>
</div>
<div className="control-center-row">
<button onClick={onLoad}>Load</button>
</div>
<div className="control-center-row">
<button onClick={onClear}>Clear</button>
</div>
<div className="control-center-row">
<button onClick={saveApplicationLayout}>
Save application layout
</button>
</div>
</div>
);
};

View File

@ -1,6 +0,0 @@
import * as React from 'react';
import { IGroupPanelBaseProps } from 'dockview';
export const CustomTab = (props: IGroupPanelBaseProps) => {
return <div>hello</div>;
};

View File

@ -1,27 +0,0 @@
import * as React from 'react';
import { IGridviewPanelProps } from 'dockview';
import { atom, useRecoilValue } from 'recoil';
export const selectedPanelAtom = atom<string>({
key: 'selectedPanelAtom',
default: '',
});
export const Footer = (props: IGridviewPanelProps) => {
const selectedPanel = useRecoilValue(selectedPanelAtom);
return (
<div
style={{
height: '22px',
backgroundColor: 'dodgerblue',
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
}}
>
<span style={{ flexGrow: 1 }} />
<span>{selectedPanel}</span>
</div>
);
};

View File

@ -1,101 +0,0 @@
{
"grid": {
"root": {
"type": "branch",
"data": [
{
"type": "leaf",
"data": {
"id": "group_1",
"views": [
"split_panel"
],
"activeView": "split_panel"
},
"size": 371
},
{
"type": "branch",
"data": [
{
"type": "leaf",
"data": {
"id": "group_2",
"views": [
"item2"
],
"activeView": "item2"
},
"size": 792
},
{
"type": "leaf",
"data": {
"id": "group_3",
"views": [
"panel_2"
],
"activeView": "panel_2"
},
"size": 792
}
],
"size": 371
},
{
"type": "leaf",
"data": {
"id": "group_4",
"views": [
"panel_0"
],
"activeView": "panel_0"
},
"size": 372
}
],
"size": 1584
},
"height": 1114,
"width": 1584,
"orientation": "HORIZONTAL"
},
"panels": {
"panel_0": {
"id": "panel_0",
"contentId": "test_component",
"tabId": "__DEFAULT_TAB__",
"props": {
"text": "how low?"
},
"title": "Item 1",
"state": {}
},
"item2": {
"id": "item2",
"contentId": "test_component",
"tabId": "__DEFAULT_TAB__",
"props": {},
"title": "Item 2",
"state": {}
},
"split_panel": {
"id": "split_panel",
"contentId": "split_panel",
"tabId": "__DEFAULT_TAB__",
"props": {},
"title": "Item 3 with a long title",
"state": {}
},
"panel_2": {
"id": "panel_2",
"contentId": "test_component",
"tabId": "__DEFAULT_TAB__",
"props": {},
"title": "Item 3",
"suppressClosable": true,
"state": {}
}
},
"activeGroup": "group_2"
}

View File

@ -1,22 +0,0 @@
.context-menu {
position: absolute;
height: 100px;
width: 170px;
font-size: 13px;
z-index: 999;
color: rgb(204, 204, 204);
background: rgb(37, 37, 38);
box-shadow: rgb(0, 0, 0) 0px 2px 4px;
padding: 10px 0px;
.context-action {
height: 20px;
line-height: 20px;
padding: 2px 10px;
cursor: pointer;
&:hover {
background-color: rgb(51, 51, 51);
}
}
}

View File

@ -1,334 +0,0 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {
IDockviewPanelProps,
IGridviewPanelProps,
TabContextMenuEvent,
DockviewReadyEvent,
DockviewReact,
DockviewApi,
IWatermarkPanelProps,
IDockviewPanel,
PanelCollection,
} from 'dockview';
import { CustomTab } from './customTab';
import { Settings } from './settingsPanel';
import { useLayoutRegistry } from './registry';
import { SplitPanel } from './splitPanel';
import './layoutGrid.scss';
import { WelcomePanel } from '../panels/welcome/welcome';
import { SplitviewPanel } from '../panels/splitview/splitview';
import { GridviewDemoPanel } from '../panels/gridview/gridview';
import { useRecoilCallback } from 'recoil';
import { selectedPanelAtom } from './footer';
import { ExampleFunctions } from './panels/exampleFunctions';
import { CompositeDisposable } from '../lifecycle';
const WatermarkComponent = () => {
return (
<div style={{ backgroundColor: 'black', color: 'white' }}>
Watermark component
</div>
);
};
const Test = (props: IDockviewPanelProps) => {
const [counter, setCounter] = React.useState<number>(0);
React.useEffect(() => {
const interval = setInterval(() => {
setCounter((_) => _ + 1);
}, 2000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div>
<div>{`custom body ${counter}`}</div>
<button>Toggle</button>
</div>
);
};
const components: PanelCollection<IDockviewPanelProps> = {
test: Test,
welcome: WelcomePanel,
splitview: SplitviewPanel,
gridview: GridviewDemoPanel,
inner_component: (props: IDockviewPanelProps) => {
const _api = React.useRef<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
_api.current = event.api;
event.api.addPanel({
component: 'test_component',
id: 'inner-1',
title: 'inner-1',
});
event.api.addPanel({
component: 'test_component',
id: 'inner-2',
title: 'inner-2',
});
event.api.addPanel({
component: 'test_component',
id: nextGuid(),
title: 'inner-3',
position: {
direction: 'within',
referencePanel: 'inner-1',
},
});
event.api.addPanel({
component: 'test_component',
id: nextGuid(),
title: 'inner-4',
position: {
direction: 'within',
referencePanel: 'inner-2',
},
});
};
return (
<div
style={{
boxSizing: 'border-box',
// borderTop: "1px solid var(--dv-separator-border)",
}}
>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={WatermarkComponent}
tabHeight={20}
/>
</div>
);
},
test_component: ExampleFunctions,
settings: Settings,
split_panel: SplitPanel,
};
const tabComponents = {
default: CustomTab,
};
export const nextGuid = (() => {
let counter = 0;
return () => 'panel_' + (counter++).toString();
})();
export const TestGrid = (props: IGridviewPanelProps) => {
const [api, setApi] = React.useState<DockviewApi>();
const registry = useLayoutRegistry();
const onReady = (event: DockviewReadyEvent) => {
const api = event.api;
setApi(api);
registry.register('dockview', api);
};
const setSelectedPanel = useRecoilCallback(
({ set }) =>
(value: string) =>
set(selectedPanelAtom, value),
[]
);
React.useEffect(() => {
if (!api) {
return () => {
//
};
}
props.api.setConstraints({
minimumWidth: () => api.minimumWidth,
minimumHeight: () => api.minimumHeight,
});
const disposable = new CompositeDisposable(
api.onDidLayoutChange(() => {
const state = api.toJSON();
localStorage.setItem('dockview', JSON.stringify(state));
}),
api.onDidActivePanelChange((e) => {
setSelectedPanel(e?.id || '');
})
);
const state = localStorage.getItem('dockview');
let success = false;
if (state) {
try {
api.fromJSON(JSON.parse(state));
success = true;
} catch (err) {
console.error('failed to load layout', err);
}
}
const welcomePanel = api.getPanel('welcome');
if (!welcomePanel) {
api.addPanel({
component: 'welcome',
id: 'welcome',
title: 'Welcome',
suppressClosable: true,
});
}
return () => {
disposable.dispose();
};
}, [api]);
const [coord, setCoord] =
React.useState<{
x: number;
y: number;
panel: IDockviewPanel;
}>(undefined);
const onTabContextMenu = React.useMemo(
() => (event: TabContextMenuEvent) => {
event.event.preventDefault();
console.log(event.panel);
const cb = (event: MouseEvent) => {
let element: HTMLElement = event.target as HTMLElement;
while (element) {
if (element.classList.contains('context-menu')) {
return;
}
element = element.parentElement;
}
window.removeEventListener('mousedown', cb);
setCoord(undefined);
};
window.addEventListener('mousedown', cb);
setCoord({
x: event.event.clientX,
y: event.event.clientY,
panel: event.panel,
});
},
[]
);
const onClose = () => {
setCoord(undefined);
coord.panel.api.close();
};
const onChangeName = () => {
setCoord(undefined);
coord.panel.api.setTitle('This looks new?');
};
return (
<>
{coord &&
ReactDOM.createPortal(
<div
className="context-menu"
style={{
left: `${coord.x}px`,
top: `${coord.y}px`,
}}
>
<div className="context-action" onClick={onClose}>
Close
</div>
<div className="context-action" onClick={onChangeName}>
Rename
</div>
</div>,
document.getElementById('anchor')
)}
<DockviewReact
onReady={onReady}
components={components}
tabComponents={tabComponents}
onTabContextMenu={onTabContextMenu}
watermarkComponent={Watermark}
showDndOverlay={(ev) => {
return true;
}}
onDidDrop={(ev) => {
console.log('onDidDrop', ev);
}}
/>
</>
);
};
const Watermark = (props: IWatermarkPanelProps) => {
const [groups, setGroups] = React.useState<number>(props.containerApi.size);
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.containerApi.onDidLayoutChange(() => {
console.log(`groups2 ${props.containerApi.size}`);
setGroups(props.containerApi.size);
})
);
return () => {
disposable.dispose();
};
}, []);
const onClick = () => {
props.close();
};
return (
<div
style={{
display: 'flex',
width: '100%',
flexGrow: 1,
height: '100%',
flexDirection: 'column',
}}
>
<div
style={{
height: '35px',
display: 'flex',
width: '100%',
}}
>
<span style={{ flexGrow: 1 }} />
{groups > 1 && (
<span
onClick={onClick}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<a className="close-action"></a>
</span>
)}
</div>
<div
style={{
flexGrow: 1,
display: 'flex',
justifyContent: 'center',
}}
>
<span>Watermark component</span>
</div>
</div>
);
};

View File

@ -1,67 +0,0 @@
import * as React from 'react';
import { IGridviewPanelProps } from 'dockview';
import { CompositeDisposable } from '../lifecycle';
export const Panel = (props: IGridviewPanelProps) => {
const [active, setActive] = React.useState<boolean>();
const [focused, setFocused] = React.useState<boolean>();
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidActiveChange((event) => {
setActive(event.isActive);
}),
props.api.onDidFocusChange((event) => {
setFocused(event.isFocused);
})
);
return () => {
disposable.dispose();
};
}, []);
const onToggle = () => {
const editorPanel = props.containerApi.getPanel('editor');
editorPanel.api.setVisible(!editorPanel.api.isVisible);
};
const onClose = () => {
const editorPanel = props.containerApi.getPanel('editor');
editorPanel.api.setVisible(true);
props.api.setVisible(false);
};
const onMove = () => {
const thisPanel = props.containerApi.getPanel('panel');
const editor = props.containerApi.getPanel('editor');
props.containerApi.movePanel(thisPanel, {
direction: 'left',
reference: editor.id,
});
};
return (
<div
style={{
borderTop: `1px solid var(--dv-separator-border)`,
boxSizing: 'border-box',
backgroundColor: 'rgb(30,30,30)',
height: '100%',
}}
>
<div style={{ display: 'flex', padding: '5px' }}>
<span>This panel is outside of the dockable layer</span>
<span style={{ flexGrow: 1 }} />
{/* <button onClick={onMove}>Move</button> */}
<button onClick={onToggle}>Resize</button>
<button onClick={onClose}>Close</button>
</div>
<div style={{ padding: '0px 5px' }}>
<div>{`isPanelActive: ${active} isPanelFocused: ${focused}`}</div>
</div>
</div>
);
};

View File

@ -1,14 +0,0 @@
.example-functions-panel {
padding: 5px;
display: flex;
flex-direction: column;
.example-functions-panel-header-bar {
display: flex;
justify-content: flex-end;
}
.example-functions-panel-section {
padding: 8px 0px;
}
}

View File

@ -1,102 +0,0 @@
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
import { CompositeDisposable } from '../../lifecycle';
import './exampleFunctions.scss';
export const ExampleFunctions = (
props: IDockviewPanelProps & { [key: string]: any }
) => {
const [panelState, setPanelState] = React.useState<{
isGroupActive: boolean;
isPanelVisible: boolean;
}>({
isGroupActive: props.api.isActive,
isPanelVisible: props.api.isVisible,
});
const [panelName, setPanelName] = React.useState<string>();
const input = React.useRef<HTMLInputElement>();
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidActiveChange((event) => {
setPanelState((_) => ({
..._,
isGroupActive: event.isActive,
}));
}),
props.api.onDidVisibilityChange((x) => {
setPanelState((_) => ({
..._,
isPanelVisible: x.isVisible,
}));
}),
props.api.onDidFocusChange(({ isFocused }) => {
if (isFocused) {
input.current.focus();
}
})
);
return () => {
disposable.dispose();
};
}, []);
const onRename = () => {
props.api.setTitle(panelName);
};
const onClose = () => {
props.api.close();
};
return (
<div className="example-functions-panel">
<div className="example-functions-panel-header-bar">
<span style={{ padding: '0px 8px' }}>
<span>{'isGroupActive: '}</span>
<span
style={{
color: panelState.isGroupActive
? '#23d16f'
: '#cd312b',
}}
>
{`${panelState.isGroupActive}`}
</span>
</span>
<span style={{ padding: '0px 8px' }}>
<span>{'isPanelVisible: '}</span>
<span
style={{
color: panelState.isPanelVisible
? '#23d16f'
: '#cd312b',
}}
>
{`${panelState.isPanelVisible}`}
</span>
</span>
</div>
<div className="example-functions-panel-section">
<input
value={panelName}
placeholder="New Panel Name"
type="text"
onChange={(event) => setPanelName(event.target.value)}
/>
<button onClick={onRename}>Rename</button>
</div>
<input
style={{ width: '175px' }}
ref={input}
placeholder="This is focused by the panel"
/>
<input
style={{ width: '175px' }}
placeholder="This is also focusable"
/>
</div>
);
};

View File

@ -1,25 +0,0 @@
import * as React from 'react';
export interface IRegistry {
register<T>(name: string, value: T): void;
get<T>(name: string): T;
}
class Registry implements IRegistry {
private cache = new Map<string, any>();
register<T>(name: string, value: T) {
this.cache.set(name, value);
}
get<T>(name: string) {
return this.cache.get(name) as T;
}
}
const RegistryInstance = new Registry();
export const useLayoutRegistry = (): IRegistry => {
const ref = React.useRef<IRegistry>(RegistryInstance);
return ref.current;
};

View File

@ -1,38 +0,0 @@
import * as React from 'react';
import { IDockviewPanelProps } from 'dockview';
export const Settings = (props: IDockviewPanelProps) => {
const [tabHeight, setTabHeight] = React.useState<number>(
props.containerApi.getTabHeight()
);
const onTabHeightChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
if (!Number.isNaN(value)) {
setTabHeight(value);
}
};
const onClick = () => {
props.containerApi.setTabHeight(tabHeight);
};
const onRemove = () => {
props.containerApi.setTabHeight(undefined);
};
return (
<div style={{ height: '100%', color: 'white' }}>
<label>
Tab height
<input
onChange={onTabHeightChange}
value={tabHeight}
type="number"
/>
<button onClick={onClick}>Apply</button>
<button onClick={onRemove}>Remove</button>
</label>
</div>
);
};

View File

@ -1,57 +0,0 @@
{
"views": [
{
"size": 22,
"data": {
"id": "1",
"component": "controlCenter",
"props": {},
"title": "Control Center",
"state": {}
},
"minimumSize": 120
},
{
"size": 508,
"data": {
"id": "2",
"component": "default",
"props": {},
"title": "Panel 1",
"state": {}
},
"minimumSize": 120,
"expanded": true
},
{
"size": 248,
"data": {
"id": "3",
"component": "default",
"props": {},
"title": "Panel 2",
"state": {}
},
"minimumSize": 120,
"expanded": true
},
{
"size": 22,
"data": {
"id": "4",
"component": "default",
"props": {},
"title": "Panel 3",
"state": {}
},
"minimumSize": 120,
"expanded": false
}
],
"size": 800,
"orientation": "VERTICAL"
}

View File

@ -1,18 +0,0 @@
.pane-header {
.my-header {
.actions {
display: none;
}
&.within {
.actions {
display: flex;
}
}
}
&:focus {
.actions {
display: flex;
}
}
}

View File

@ -1,97 +0,0 @@
import * as React from 'react';
import {
IGridviewPanelProps,
PaneviewReact,
PaneviewReadyEvent,
IPaneviewPanelProps,
PaneviewApi,
PaneviewDropEvent,
} from 'dockview';
import { ControlCenter } from './controlCenter';
import './sidebar.scss';
import { CompositeDisposable } from '../lifecycle';
const components = {
default: (props: IPaneviewPanelProps) => {
return <div style={{ height: '100%' }}>This is an example panel</div>;
},
controlCenter: ControlCenter,
};
export const Sidebar = (props: IGridviewPanelProps) => {
const api = React.useRef<PaneviewApi>();
const onReady = (event: PaneviewReadyEvent) => {
api.current = event.api;
console.log(props.api.width, props.api.height);
event.api.fromJSON(require('./sidebar.layout.json'));
return;
event.api.addPanel({
id: '1',
component: 'controlCenter',
title: 'Control Center',
});
event.api.addPanel({
id: '2',
component: 'default',
title: 'Panel 1',
});
event.api.addPanel({
id: '3',
component: 'default',
title: 'Panel 2',
});
event.api.addPanel({
id: '4',
component: 'default',
title: 'Panel 3',
});
setTimeout(() => {
console.log(JSON.stringify(event.api.toJSON(), null, 4));
}, 10000);
};
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidDimensionsChange((ev) => {
api.current.layout(ev.width, ev.height);
}),
props.api.onDidVisibilityChange((event) => {
console.log(event);
}),
props.api.onDidFocusChange(({ isFocused }) => {
if (isFocused) {
api.current.focus();
}
})
);
return () => {
disposable.dispose();
};
}, []);
const onDidDrop = React.useCallback((event: PaneviewDropEvent) => {
console.log('drop', event);
}, []);
return (
<div
style={{
backgroundColor: 'rgb(37,37,38)',
height: '100%',
}}
>
<PaneviewReact
components={components}
onReady={onReady}
onDidDrop={onDidDrop}
/>
</div>
);
};

View File

@ -1,145 +0,0 @@
import * as React from 'react';
import {
ISplitviewPanelProps,
Orientation,
SplitviewReadyEvent,
SplitviewReact,
SplitviewApi,
IDockviewPanelProps,
} from 'dockview';
import { useLayoutRegistry } from './registry';
import './splitPanel.scss';
import { CompositeDisposable } from '../lifecycle';
const components = {
default1: (props: ISplitviewPanelProps) => {
const ref = React.useRef<HTMLInputElement>();
const [focused, setFocused] = React.useState<boolean>(false);
const [active, setActive] = React.useState<boolean>(false);
const onClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
props.api.setSize({ size: 300 });
};
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidFocusChange((event) => {
setFocused(event.isFocused);
}),
props.api.onDidActiveChange((event) => {
setActive(event.isActive);
}),
props.api.onDidFocusChange(({ isFocused }) => {
if (isFocused) {
ref.current.focus();
}
})
);
return () => {
disposable.dispose();
};
}, []);
return (
<div style={{ height: '100%' }}>
<div style={{ display: 'flex', padding: '5px' }}>
<div>This is a splitview panel inside a dockable panel</div>
<span style={{ flexGrow: 1 }} />
<button onClick={onClick}>resize</button>
</div>
<div style={{ padding: '0px 5px' }}>
<div>{`isPanelActive: ${active} isPanelFocused: ${focused}`}</div>
<input ref={ref} type="text" placeholder="focus test" />
<span>{(props as any).text}</span>
</div>
</div>
);
},
};
export const SplitPanel = (props: IDockviewPanelProps) => {
const api = React.useRef<SplitviewApi>();
const registry = useLayoutRegistry();
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidDimensionsChange((event) => {
api.current?.layout(event.width, event.height - 25);
}),
props.api.onDidFocusChange(({ isFocused }) => {
if (isFocused) {
api.current.focus();
}
})
);
return () => {
disposable.dispose();
};
}, []);
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
registry.register('splitview', event.api);
event.api.fromJSON(require('./splitpanel.layout.json'));
return;
event.api.addPanel({
id: '1',
component: 'default1',
snap: true,
params: {
text: 'hiya',
},
});
event.api.addPanel({ id: '2', component: 'default1' });
};
const onUpdateProps = () => {
const panel = api.current.getPanel('1');
panel.update({ params: { text: Date.now().toString() } });
};
const onAdd = () => {
api.current.addPanel({
id: `${Date.now()}`,
component: 'default1',
snap: true,
params: {
text: 'hiya',
},
});
};
const onRemove = () => {
const panels = api.current.panels;
if (panels.length === 0) {
return;
}
api.current.removePanel(panels[panels.length - 1]);
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
color: 'white',
}}
>
<div style={{ height: '25px' }}>
<button onClick={onUpdateProps}>Update props</button>
<button onClick={onAdd}>Add</button>
<button onClick={onRemove}>Remove</button>
</div>
<SplitviewReact
components={components}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</div>
);
};

View File

@ -1,29 +0,0 @@
{
"views": [
{
"size": 200,
"data": {
"id": "1",
"component": "default1",
"props": {
"text": "hiya"
}
},
"minimumSize": 200,
"maximumSize": 9007199254740991,
"snap": true
},
{
"size": 409,
"data": {
"id": "2",
"component": "default1"
},
"minimumSize": 200,
"maximumSize": 9007199254740991
}
],
"activeView": "2",
"size": 609,
"orientation": "VERTICAL"
}

View File

@ -1,54 +0,0 @@
export interface IDisposable {
dispose: () => void;
}
export interface IValueDisposable<T> {
value: T;
disposable: IDisposable;
}
export namespace Disposable {
export const NONE: IDisposable = {
dispose: () => {
// noop
},
};
}
export class CompositeDisposable {
private readonly disposables: IDisposable[];
public static from(...args: IDisposable[]) {
return new CompositeDisposable(...args);
}
constructor(...args: IDisposable[]) {
this.disposables = args;
}
public addDisposables(...args: IDisposable[]) {
args.forEach((arg) => this.disposables.push(arg));
}
public dispose() {
this.disposables.forEach((arg) => arg.dispose());
}
}
export class MutableDisposable implements IDisposable {
private _disposable = Disposable.NONE;
set value(disposable: IDisposable) {
if (this._disposable) {
this._disposable.dispose();
}
this._disposable = disposable;
}
public dispose() {
if (this._disposable) {
this._disposable.dispose();
this._disposable = Disposable.NONE;
}
}
}

View File

@ -1,328 +0,0 @@
import {
ActiveEvent,
FocusEvent,
GridConstraintChangeEvent,
GridviewApi,
GridviewReact,
GridviewReadyEvent,
IDockviewPanelProps,
IGridviewPanelProps,
LayoutPriority,
Orientation,
orthogonal,
PanelCollection,
PanelDimensionChangeEvent,
VisibilityEvent,
} from 'dockview';
import * as React from 'react';
import { CompositeDisposable } from '../../lifecycle';
const components: PanelCollection<IGridviewPanelProps> = {
default: (props) => {
const [active, setActive] = React.useState<boolean>(false);
const [visible, setVisible] = React.useState<boolean>(false);
const [focused, setFocused] = React.useState<boolean>(false);
const [dimension, setDimension] = React.useState<{
width: number;
height: number;
}>({ width: 0, height: 0 });
const [
constraints,
setConstraints,
] = React.useState<GridConstraintChangeEvent>({
minimumHeight: undefined,
maximumHeight: undefined,
minimumWidth: undefined,
maximumWidth: undefined,
});
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidActiveChange((event: ActiveEvent) => {
setActive(event.isActive);
}),
props.api.onDidConstraintsChange(
(event: GridConstraintChangeEvent) => {
setConstraints(event);
}
),
props.api.onDidDimensionsChange(
(event: PanelDimensionChangeEvent) => {
setDimension(event);
}
),
props.api.onDidFocusChange((event: FocusEvent) => {
setFocused(event.isFocused);
}),
props.api.onDidVisibilityChange((event: VisibilityEvent) => {
setVisible(event.isVisible);
})
);
return () => {
disposable.dispose();
};
}, []);
const color = React.useMemo(
() =>
`rgb(${Math.floor(256 * Math.random())},${Math.floor(
256 * Math.random()
)},${Math.floor(256 * Math.random())})`,
[]
);
return (
<div
style={{
backgroundColor: color,
}}
className="splitview-demo-panel"
>
<div className="api-parameter">
<span>Width</span>
<span>{dimension.width}</span>
<span>Height</span>
<span>{dimension.height}</span>
<span>Min. height</span>
<span>{constraints.minimumHeight}</span>
<span>Max. height</span>
<span>{constraints.maximumHeight}</span>
<span>Min. width</span>
<span>{constraints.minimumWidth}</span>
<span>Max. width</span>
<span>{constraints.maximumWidth}</span>
<span>Active</span>
<span>{active.toString()}</span>
<span>Visible</span>
<span>{visible.toString()}</span>
<span>Focused</span>
<span>{focused.toString()}</span>
</div>
</div>
);
},
};
export const GridviewDemoPanel = (props: IDockviewPanelProps) => {
return (
<div
style={{
overflow: 'auto',
height: '100%',
padding: '10px 0px',
boxSizing: 'border-box',
}}
>
<div style={{ padding: '0px 20px' }}>
<h1>Gridview</h1>
</div>
<ul style={{ padding: '0px 20px 0px 40px' }}>
<li>
The gridview is a collection of nested splitviews which
forms a grid-based layout
</li>
</ul>
<GridviewDemo {...props} />
</div>
);
};
export const GridviewDemo = (props: IDockviewPanelProps) => {
const api = React.useRef<GridviewApi>();
const [orientation, setOrientation] = React.useState<Orientation>(
Orientation.VERTICAL
);
const onClick = () => {
api.current.orientation = orthogonal(api.current.orientation);
// load();
};
const load = () => {
api.current.fromJSON({
activePanel: '1',
grid: {
height: 3,
width: 2,
orientation: orientation,
root: {
type: 'branch',
size: 3,
data: [
{
type: 'branch',
size: 1,
data: [
{
type: 'leaf',
size: 1,
data: {
id: '1',
component: 'default',
minimumHeight: 50,
maximumHeight: Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth: Number.POSITIVE_INFINITY,
snap: false,
priority: LayoutPriority.Normal,
},
},
{
type: 'branch',
size: 1,
data: [
{
type: 'leaf',
size: 0.5,
data: {
id: '2',
component: 'default',
minimumHeight: 50,
maximumHeight:
Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth:
Number.POSITIVE_INFINITY,
snap: false,
priority: LayoutPriority.Normal,
},
},
{
type: 'branch',
size: 0.5,
data: [
{
type: 'leaf',
size: 0.5,
data: {
id: '2',
component: 'default',
minimumHeight: 50,
maximumHeight:
Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth:
Number.POSITIVE_INFINITY,
snap: false,
priority:
LayoutPriority.Normal,
},
},
{
type: 'leaf',
size: 0.5,
data: {
id: '3',
component: 'default',
minimumHeight: 50,
maximumHeight:
Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth:
Number.POSITIVE_INFINITY,
snap: false,
priority:
LayoutPriority.Normal,
},
},
],
},
],
},
],
},
{
type: 'leaf',
size: 1,
data: {
id: '4',
component: 'default',
minimumHeight: 50,
maximumHeight: Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth: Number.POSITIVE_INFINITY,
snap: false,
priority: LayoutPriority.Normal,
},
},
{
type: 'branch',
size: 1,
data: [
{
type: 'leaf',
size: 1,
data: {
id: '3',
component: 'default',
minimumHeight: 50,
maximumHeight: Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth: Number.POSITIVE_INFINITY,
snap: false,
priority: LayoutPriority.Normal,
},
},
{
type: 'leaf',
size: 1,
data: {
id: '4',
component: 'default',
minimumHeight: 50,
maximumHeight: Number.POSITIVE_INFINITY,
minimumWidth: 50,
maximumWidth: Number.POSITIVE_INFINITY,
snap: false,
priority: LayoutPriority.Normal,
},
},
],
},
],
},
},
});
};
const onReady = (event: GridviewReadyEvent) => {
api.current = event.api;
api.current?.layout(props.api.width - 80, 600);
load();
};
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidDimensionsChange((event) => {
api.current?.layout(event.width - 80, 600);
})
);
return () => {
disposable.dispose();
};
}, []);
return (
<div style={{ height: '100%', width: '100%' }}>
<button onClick={onClick}>Flip</button>
<div
style={{
height: '400px',
margin: '40px',
backgroundColor: 'grey',
}}
>
<GridviewReact
proportionalLayout={true}
components={components}
orientation={Orientation.VERTICAL}
onReady={onReady}
/>
</div>
</div>
);
};

View File

@ -1,76 +0,0 @@
.splitview-demo-container {
width: 100%;
padding: 0px 20px;
box-sizing: border-box;
.splitview-demo-content {
display: flex;
--dv-separator-border: white;
}
&.vertical {
.api-parameter {
padding-left: 10px;
}
.splitview-demo-view {
height: 500px;
width: 150px;
.api-parameter {
padding-left: 0px;
}
}
}
&.horizontal {
.splitview-demo-content {
flex-direction: column;
}
.api-parameter {
padding-top: 10px;
}
.splitview-demo-view {
height: 180px;
width: 500px;
.api-parameter {
padding-top: 0px;
}
}
}
}
.api-parameter {
display: grid;
grid-template-columns: 70px 50px;
grid-auto-rows: 18px;
span {
&:nth-child(2n) {
text-align: right;
}
}
.visibility-toggle {
height: 16px;
outline: 1px solid dodgerblue;
outline-offset: -1px;
padding: 0px 4px;
display: flex;
cursor: pointer;
&:not(:first-child) {
margin-left: 4px;
}
}
}
.splitview-demo-panel {
background-color: #2d2d2d;
height: 100%;
padding: 10px;
box-sizing: border-box;
overflow: hidden;
}

View File

@ -1,308 +0,0 @@
import {
ISplitviewPanelProps,
Orientation,
SplitviewApi,
SplitviewReact,
SplitviewReadyEvent,
PanelDimensionChangeEvent,
ActiveEvent,
FocusEvent,
VisibilityEvent,
PanelConstraintChangeEvent,
IDockviewPanelProps,
} from 'dockview';
import * as React from 'react';
import { CompositeDisposable } from '../../lifecycle';
import './splitview.scss';
const components = {
default: (props: ISplitviewPanelProps) => {
const [active, setActive] = React.useState<boolean>(false);
const [visible, setVisible] = React.useState<boolean>(false);
const [focused, setFocused] = React.useState<boolean>(false);
const [dimension, setDimension] = React.useState<{
width: number;
height: number;
}>({ width: 0, height: 0 });
const [constraints, setConstraints] = React.useState<{
maximumSize?: number;
minimumSize?: number;
}>({ maximumSize: undefined, minimumSize: undefined });
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidActiveChange((event: ActiveEvent) => {
setActive(event.isActive);
}),
props.api.onDidConstraintsChange(
(event: PanelConstraintChangeEvent) => {
setConstraints(event);
}
),
props.api.onDidDimensionsChange(
(event: PanelDimensionChangeEvent) => {
setDimension(event);
}
),
props.api.onDidFocusChange((event: FocusEvent) => {
setFocused(event.isFocused);
}),
props.api.onDidVisibilityChange((event: VisibilityEvent) => {
setVisible(event.isVisible);
})
);
return () => {
disposable.dispose();
};
}, []);
const color = React.useMemo(
() => 'rgba(14, 99, 156,0.4)',
// `rgb(${Math.floor(256 * Math.random())},${Math.floor(
// 256 * Math.random()
// )},${Math.floor(256 * Math.random())})`,
[]
);
return (
<div
style={{
backgroundColor: color,
color: '#cccccc',
}}
className="splitview-demo-panel"
>
<div className="api-parameter">
<span>Id</span>
<span>{props.api.id}</span>
<span>Width</span>
<span>{dimension.width}</span>
<span>Height</span>
<span>{dimension.height}</span>
<span>Min. size</span>
<span>{constraints.minimumSize}</span>
<span>Max. size</span>
<span>{constraints.maximumSize}</span>
<span>Active</span>
<span>{active.toString()}</span>
<span>Visible</span>
<span>{visible.toString()}</span>
<span>Focused</span>
<span>{focused.toString()}</span>
</div>
</div>
);
},
};
export const SplitviewPanel = (props: IDockviewPanelProps) => {
return (
<div
style={{
overflow: 'auto',
height: '100%',
padding: '10px 0px',
boxSizing: 'border-box',
}}
>
<div style={{ padding: '0px 20px', fontSize: '36px' }}>
Splitview
</div>
<ul style={{ padding: '0px 20px 0px 40px' }}>
<li>
The splitview component exposes an API object, a selection
of avaliable values are shown in the summary sections below
</li>
<li>
Each panel exposes it's own API, and has access to the
common API. A selector of panel API values are shown in each
panel of the splitview.
</li>
</ul>
<Common {...props} orientation={Orientation.HORIZONTAL} />
<Common {...props} orientation={Orientation.VERTICAL} />
</div>
);
};
export const Common = (
props: IDockviewPanelProps & { orientation: Orientation }
) => {
const api = React.useRef<SplitviewApi>();
const [dimensions, setDimensions] = React.useState<{
height: number;
width: number;
maximumSize: number;
minimumSize: number;
visibility: boolean[];
length: number;
}>({
height: undefined,
width: undefined,
maximumSize: undefined,
minimumSize: undefined,
visibility: [],
length: undefined,
});
React.useEffect(() => {
const disposable = new CompositeDisposable(
props.api.onDidDimensionsChange(
(event: PanelDimensionChangeEvent) => {
switch (props.orientation) {
case Orientation.HORIZONTAL:
api.current.layout(500, 100);
break;
case Orientation.VERTICAL:
api.current.layout(100, 500);
break;
}
const height = api.current.height;
const width = api.current.width;
const maximumSize = api.current.maximumSize;
const minimumSize = api.current.minimumSize;
const length = api.current.length;
setDimensions({
height,
width,
maximumSize,
minimumSize,
length,
visibility: api.current.panels.map(
(_) => _.api.isVisible
),
});
}
),
api.current.onDidLayoutChange(() => {
//
})
);
return () => {
disposable.dispose();
};
}, []);
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
event.api.fromJSON({
views: [
{
data: {
id: 'one',
component: 'default',
minimumSize: 10,
},
size: 1,
},
{
data: {
id: 'two',
component: 'default',
minimumSize: 10,
maximumSize: 200,
},
size: 2,
},
{
data: {
id: 'three',
component: 'default',
minimumSize: 50,
},
size: 3,
snap: true,
},
],
size: 6,
activeView: 'one',
orientation: props.orientation,
});
};
const toggleVisibility = (i: number) => () => {
const panel = api.current.panels[i];
panel.api.setVisible(panel.api.isVisible);
setDimensions((dimensions) => ({
...dimensions,
visibility: api.current.panels.map((_) => _.api.isVisible),
}));
};
const move = () => {
api.current.movePanel(api.current.panels.length - 1, 0);
setDimensions((dimensions) => ({
...dimensions,
visibility: api.current.panels.map((_) => _.api.isVisible),
}));
};
const text = React.useMemo(() => {
switch (props.orientation) {
case Orientation.VERTICAL:
return 'Vertical Splitview';
case Orientation.HORIZONTAL:
return 'Horizontal Splitview';
}
}, [props.orientation]);
return (
<div
className={`splitview-demo-container ${props.orientation.toLowerCase()}`}
>
<h3>{text}</h3>
<div className="splitview-demo-content">
<div
style={{
backgroundColor: 'rgb(60,60,60)',
padding: '10px',
overflow: 'auto',
}}
>
<div className="splitview-demo-view">
<SplitviewReact
orientation={props.orientation}
onReady={onReady}
components={components}
/>
</div>
</div>
<div className="api-parameter">
<span>Height</span>
<span>{dimensions.height}</span>
<span>Width</span>
<span>{dimensions.width}</span>
<span>Lenght</span>
<span>{dimensions.length}</span>
<span>Min. size</span>
<span>{dimensions.minimumSize}</span>
<span>Max. size</span>
<span>{dimensions.maximumSize}</span>
<span>Visible</span>
<span style={{ display: 'flex' }}>
{dimensions.visibility.map((_, i) => {
return (
<div
className="visibility-toggle"
onClick={toggleVisibility(i)}
key={i}
>
{_ ? 'Yes' : 'No'}
</div>
);
})}
</span>
<span>Move view</span>
<span className="visibility-toggle" onClick={move}>
Go
</span>
</div>
</div>
</div>
);
};

View File

@ -1,33 +0,0 @@
.welcome-panel {
color: #cccccc;
margin: 40px;
.welcome-header {
margin-bottom: 40px;
h1 {
font-size: 30px;
}
h2 {
font-size: 20px;
}
}
.directory {
.directory-title {
font-size: 16px;
height: 25px;
line-height: 25px;
}
.directory-item {
font-size: 13px;
height: 20px;
line-height: 20px;
color: dodgerblue;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}

View File

@ -1,53 +0,0 @@
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
import './welcome.scss';
export const WelcomePanel = (props: IDockviewPanelProps) => {
const onAddSplitview = (event: React.MouseEvent<HTMLDivElement>) => {
const splitviewPanel = props.containerApi.getPanel('splitview');
if (splitviewPanel) {
splitviewPanel.api.setActive();
return;
}
props.containerApi.addPanel({
id: 'splitview',
component: 'splitview',
title: 'Splitview Docs',
});
};
const onAddGridview = (event: React.MouseEvent<HTMLDivElement>) => {
const splitviewPanel = props.containerApi.getPanel('gridview');
if (splitviewPanel) {
splitviewPanel.api.setActive();
return;
}
props.containerApi.addPanel({
id: 'gridview',
component: 'gridview',
title: 'Gridview Docs',
});
};
return (
<div className="welcome-panel">
<div className="welcome-header">
<h1>Dockview</h1>
<h2>Zero dependency layout manager</h2>
</div>
<div className="directory">
<div className="directory-title">Components</div>
<div className="directory-item">Dockview</div>
<div onClick={onAddSplitview} className="directory-item">
Splitview
</div>
<div onClick={onAddGridview} className="directory-item">
Gridview
</div>
<div className="directory-item">Paneview</div>
</div>
</div>
);
};

View File

@ -1,16 +0,0 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 30H45V70H5C2.23858 70 0 67.7614 0 65V30Z" fill="#1E90FF"/>
<path d="M45 30H70V65C70 67.7614 67.7614 70 65 70H45V30Z" fill="#FF1E90"/>
<rect x="45" y="30" width="25" height="5" fill="#AC1160"/>
<rect x="45" y="30" width="10" height="5" fill="#EB1D85"/>
<rect y="30" width="45" height="5" fill="#1863AD"/>
<rect y="30" width="10" height="5" fill="#1A84EC"/>
<rect x="10" y="30" width="10" height="5" fill="#1973CB"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H30V30H0V5Z" fill="#1EFF8D"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H30V5H0V5Z" fill="#1BCA71"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H10V5H0V5Z" fill="#1DF387"/>
<path d="M30 0H65C67.7614 0 70 2.23858 70 5V30H30V0Z" fill="#FF8D1E"/>
<path d="M30 0H65C67.7614 0 70 2.23858 70 5V5H30V0Z" fill="#BA6817"/>
<rect x="30" width="10" height="5" fill="#EF841B"/>
<rect x="40" width="10" height="5" fill="#DC7A1B"/>
</svg>

Before

Width:  |  Height:  |  Size: 974 B

View File

@ -1,8 +0,0 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 55H20V70H5C2.23858 70 0 67.7614 0 65V55Z" fill="#8D1EFF"/>
<path d="M45 55H70V65C70 67.7614 67.7614 70 65 70H45V55Z" fill="#FF1E90"/>
<path d="M20 55H45V70H20V55Z" fill="#90FF1E"/>
<path d="M0 30H70V55H0V30Z" fill="#1E90FF"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H30V30H0V5Z" fill="#1EFF8D"/>
<path d="M30 0H65C67.7614 0 70 2.23858 70 5V30H30V0Z" fill="#FF8D1E"/>
</svg>

Before

Width:  |  Height:  |  Size: 479 B

View File

@ -1,25 +0,0 @@
<svg width="200" height="70" viewBox="0 0 200 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 52.1429H60V60C60 62.7614 57.7614 65 55 65H5C2.23858 65 0 62.7614 0 60V52.1429Z" fill="#FF1E90"/>
<path d="M0 30.7143H60V52.1428H0V30.7143Z" fill="#1E90FF"/>
<path d="M0 10C0 7.23857 2.23858 5 5 5H55C57.7614 5 60 7.23858 60 10V30.7143H0V10Z" fill="#1EFF8D"/>
<path d="M65 55H85V70H70C67.2386 70 65 67.7614 65 65V55Z" fill="#8D1EFF"/>
<path d="M110 55H135V65C135 67.7614 132.761 70 130 70H110V55Z" fill="#FF1E90"/>
<path d="M85 55H110V70H85V55Z" fill="#90FF1E"/>
<path d="M65 30H135V55H65V30Z" fill="#1E90FF"/>
<path d="M65 5C65 2.23858 67.2386 0 70 0H95V30H65V5Z" fill="#1EFF8D"/>
<path d="M95 0H130C132.761 0 135 2.23858 135 5V30H95V0Z" fill="#FF8D1E"/>
<path d="M140 30.7143H178.571V65H145C142.239 65 140 62.7614 140 60V30.7143Z" fill="#1E90FF"/>
<path d="M178.571 30.7143H200V60C200 62.7614 197.761 65 195 65H178.571V30.7143Z" fill="#FF1E90"/>
<rect x="178.571" y="30.7143" width="21.4286" height="4.28572" fill="#AC1160"/>
<rect x="178.571" y="30.7143" width="8.57143" height="4.28572" fill="#EB1D85"/>
<rect x="140" y="30.7143" width="38.5714" height="4.28572" fill="#1863AD"/>
<rect x="140" y="30.7143" width="8.57143" height="4.28572" fill="#1A84EC"/>
<rect x="148.571" y="30.7143" width="8.57143" height="4.28572" fill="#1973CB"/>
<path d="M140 10C140 7.23858 142.239 5 145 5H165.714V30.7143H140V10Z" fill="#1EFF8D"/>
<path d="M140 9.28572C140 6.91878 141.919 5 144.286 5H165.714V9.28572H140V9.28572Z" fill="#1BCA71"/>
<path d="M140 9.28572C140 6.91878 141.919 5 144.286 5H148.571V9.28572H140V9.28572Z" fill="#1DF387"/>
<path d="M165.714 5H195C197.761 5 200 7.23858 200 10V30.7143H165.714V5Z" fill="#FF8D1E"/>
<path d="M165.714 5H195.714C198.081 5 200 6.91878 200 9.28572V9.28572H165.714V5Z" fill="#BA6817"/>
<rect x="165.714" y="5" width="8.57143" height="4.28572" fill="#EF841B"/>
<rect x="174.286" y="5" width="8.57143" height="4.28572" fill="#DC7A1B"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,8 +0,0 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 55H70V65C70 67.7614 67.7614 70 65 70H5C2.23858 70 0 67.7614 0 65V55Z" fill="#FF1E90"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H65C67.7614 0 70 2.23858 70 5V50H0V5Z" fill="#1EFF8D"/>
<path d="M0 40H70V65C70 67.7614 67.7614 70 65 70H5C2.23858 70 0 67.7614 0 65V40Z" fill="#FF1E90"/>
<rect y="35" width="70" height="5" fill="#AC1160"/>
<path d="M0 30H70V35H0V30Z" fill="#1863AD"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H65C67.7614 0 70 2.23858 70 5V5H0V5Z" fill="#1BCA71"/>
</svg>

Before

Width:  |  Height:  |  Size: 583 B

View File

@ -1,5 +0,0 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 55H70V65C70 67.7614 67.7614 70 65 70H5C2.23858 70 0 67.7614 0 65V55Z" fill="#FF1E90"/>
<path d="M0 30H70V55H0V30Z" fill="#1E90FF"/>
<path d="M0 5C0 2.23858 2.23858 0 5 0H65C67.7614 0 70 2.23858 70 5V30H0V5Z" fill="#1EFF8D"/>
</svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@ -1,144 +0,0 @@
import { ViewContainer } from './viewContainer';
import * as React from 'react';
export const Container = (props: {
container: ViewContainer;
isActive: boolean;
onDragOver: (e: React.DragEvent) => void;
onDrop: (e: React.DragEvent, direction: 'top' | 'bottom') => void;
onClick: (e: React.MouseEvent) => void;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
const [selection, setSelection] = React.useState<
'top' | 'bottom' | undefined
>(undefined);
const isDragging = React.useRef<boolean>(false);
const [dragEntered, setDragEntered] = React.useState<boolean>(false);
const timer = React.useRef<any>(null);
const onDragOver = (e: React.DragEvent) => {
if (!timer.current) {
timer.current = setTimeout(() => {
props.onDragOver(e);
}, 1000);
}
if (isDragging.current) {
return;
}
setDragEntered(true);
e.preventDefault();
const target = e.target as HTMLDivElement;
const width = target.clientWidth;
const height = target.clientHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
const x = e.nativeEvent.offsetX;
const y = e.nativeEvent.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
const isTop = yp < 50;
const isBottom = yp >= 50;
setSelection(isTop ? 'top' : 'bottom');
};
const onDragLeave = (e: React.DragEvent) => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
if (isDragging.current) {
return;
}
setDragEntered(false);
setSelection(undefined);
};
const onDrop = (e: React.DragEvent) => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
if (isDragging.current) {
return;
}
setDragEntered(false);
props.onDrop(e, selection);
setSelection(undefined);
};
const onDragEnter = (e: React.DragEvent) => {
e.preventDefault();
};
const onDragStart = (e: React.DragEvent) => {
isDragging.current = true;
e.dataTransfer.setData(
'application/json',
JSON.stringify({ container: props.container.id })
);
};
const onDragEnd = (e: React.DragEvent) => {
isDragging.current = false;
setDragEntered(false);
};
return (
<div
ref={ref}
draggable={true}
onClick={props.onClick}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragStart={onDragStart}
onDragLeave={onDragLeave}
onDragEnd={onDragEnd}
onDrop={onDrop}
style={{
borderLeft: props.isActive
? '2px solid white'
: '2px solid transparent',
}}
className={`activity-bar-item${props.isActive ? ' active' : ''}`}
>
{dragEntered && (
<div
style={{
position: 'absolute',
top: '0px',
left: '0px',
height: '100%',
width: '100%',
backgroundColor: 'transparent',
boxSizing: 'border-box',
borderTop: selection === 'top' ? '2px solid white' : '',
borderBottom:
selection === 'bottom' ? '2px solid white' : '',
pointerEvents: 'none',
}}
/>
)}
<a className="material-icons-outlined">{props.container.icon}</a>
</div>
);
};

View File

@ -1,64 +0,0 @@
export interface SerializedView {
id: string;
isExpanded: boolean;
}
export type ViewOptions = {
id: string;
title: string;
isExpanded: boolean;
isLocationEditable: boolean;
icon: string;
};
export interface View {
readonly id: string;
readonly isExpanded: boolean;
readonly title: string;
readonly isLocationEditable: boolean;
readonly icon: string;
toJSON(): SerializedView;
}
export class DefaultView implements View {
private readonly _id: string;
private readonly _title: string;
private readonly _isExpanded: boolean;
private readonly _isLocationEditable: boolean;
private readonly _icon: string;
get id(): string {
return this._id;
}
get title(): string {
return this._title;
}
get isExpanded(): boolean {
return this._isExpanded;
}
get isLocationEditable(): boolean {
return this._isLocationEditable;
}
get icon(): string {
return this._icon;
}
constructor(options: ViewOptions) {
this._id = options.id;
this._title = options.title;
this._isExpanded = options.isExpanded;
this._isLocationEditable = options.isLocationEditable;
this._icon = options.icon;
}
toJSON(): SerializedView {
return {
id: this.id,
isExpanded: this.isExpanded,
};
}
}

View File

@ -1,126 +0,0 @@
import { SerializedPaneview } from 'dockview';
import { Emitter, Event } from '../events';
import { DefaultView, View, SerializedView } from './view';
import { IViewRegistry } from './viewRegistry';
export interface SerializedViewContainer {
readonly id: string;
readonly views: SerializedView[];
}
export interface ViewContainer<T = any> {
readonly id: string;
readonly views: View[];
readonly schema: T | any;
readonly icon: string;
readonly onDidAddView: Event<{ view: View; index?: number }>;
readonly onDidRemoveView: Event<View>;
addView(view: View, location?: number): void;
layout(schema: T): void;
removeView(view: View): void;
clear(): void;
toJSON(): SerializedViewContainer;
}
export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
private readonly _id: string;
private readonly _views: View[] = [];
private readonly _onDidAddView = new Emitter<{
view: View;
index?: number;
}>();
readonly onDidAddView = this._onDidAddView.event;
private readonly _onDidRemoveView = new Emitter<View>();
readonly onDidRemoveView = this._onDidRemoveView.event;
private _schema: SerializedPaneview | undefined;
get id() {
return this._id;
}
get views() {
return [...this._views];
}
get schema(): SerializedPaneview | undefined {
if (!this._schema) {
this._schema = JSON.parse(
localStorage.getItem(`viewcontainer_${this.id}`)
);
}
return this._schema;
}
get icon(): string {
const defaultIcon = 'search';
if (this.views.length > 0) {
return this.views.find((v) => !!v.icon)?.icon || defaultIcon;
}
return defaultIcon;
}
constructor(
id: string,
viewRegistry: IViewRegistry,
views?: SerializedView[]
) {
this._id = id;
if (views) {
for (const view of views) {
const registeredView = viewRegistry.getRegisteredView(view.id);
this.addView(
new DefaultView({
id: view.id,
title: registeredView.title,
isExpanded: view.isExpanded,
isLocationEditable: registeredView.isLocationEditable,
icon: registeredView.icon,
})
);
}
}
// this.addDisposables(this._onDidAddView, this._onDidRemoveView);
}
layout(schema: SerializedPaneview): void {
this._schema = schema;
localStorage.setItem(
`viewcontainer_${this.id}`,
JSON.stringify(schema)
);
}
addView(view: View, index?: number): void {
this._views.splice(index, 0, view);
this._onDidAddView.fire({ view, index });
}
removeView(view: View): void {
const index = this._views.indexOf(view);
if (index < 0) {
throw new Error('invalid');
}
this._views.splice(index, 1);
if (this._schema) {
this._schema = { ...this._schema };
this._schema.views = this._schema.views.filter(
(v) => v.data.id !== view.id
);
this.layout(this._schema);
}
this._onDidRemoveView.fire(view);
}
clear() {
localStorage.removeItem(`viewcontainer_${this.id}`);
}
toJSON(): SerializedViewContainer {
return { id: this.id, views: this.views.map((v) => v.toJSON()) };
}
}

View File

@ -1,94 +0,0 @@
import React from 'react';
import { ControlCenter } from '../layout-grid/controlCenter';
export interface RegisteredView {
id: string;
icon: string;
title: string;
isLocationEditable: boolean;
component: React.FunctionComponent;
}
export interface IViewRegistry {
getRegisteredView(id: string): RegisteredView | undefined;
}
export class ViewRegistry {
private readonly _registry = new Map<string, RegisteredView>();
register(registeredView: RegisteredView): void {
this._registry.set(registeredView.id, registeredView);
}
getRegisteredView(id: string): RegisteredView | undefined {
return this._registry.get(id);
}
}
export const VIEW_REGISTRY = new ViewRegistry();
VIEW_REGISTRY.register({
id: 'search_widget',
title: 'search',
icon: 'search',
isLocationEditable: false,
component: () => {
return (
<div
style={{
backgroundColor: 'yellow',
color: 'black',
height: '100%',
}}
>
This is a search bar component
</div>
);
},
});
VIEW_REGISTRY.register({
id: 'home_widget',
title: 'Home',
icon: 'home',
isLocationEditable: true,
component: ControlCenter,
});
VIEW_REGISTRY.register({
id: 'account_widget',
title: 'Account',
icon: 'account_circle',
isLocationEditable: true,
component: () => {
return (
<div
style={{
backgroundColor: 'green',
color: 'black',
height: '100%',
}}
>
account_circle
</div>
);
},
});
VIEW_REGISTRY.register({
id: 'settings_widget',
title: 'Settings',
icon: 'settings',
isLocationEditable: true,
component: () => {
return (
<div
style={{
backgroundColor: 'orange',
color: 'black',
height: '100%',
}}
>
settings
</div>
);
},
});

View File

@ -1,213 +0,0 @@
import { Emitter, Event } from '../events';
import { IDisposable } from '../lifecycle';
import { View } from './view';
import {
PaneviewContainer,
ViewContainer,
SerializedViewContainer,
} from './viewContainer';
import { IViewRegistry } from './viewRegistry';
export interface SerializedViewService {
containers: SerializedViewContainer[];
activeContainer?: string;
}
export interface IViewService extends IDisposable {
readonly containers: ViewContainer[];
readonly onDidActiveContainerChange: Event<void>;
readonly onDidRemoveContainer: Event<void>;
readonly onDidAddContainer: Event<void>;
readonly onDidContainersChange: Event<void>;
readonly activeContainer: ViewContainer | undefined;
addContainer(container: ViewContainer): void;
setActiveViewContainer(id: string): void;
getView(id: string): View | undefined;
moveViewToLocation(
view: View,
targetViewContainer: ViewContainer,
targetLocation: number
): void;
insertContainerAfter(source: ViewContainer, target: ViewContainer): void;
insertContainerBefore(source: ViewContainer, target: ViewContainer): void;
addViews(view: View, viewContainer: ViewContainer, location?: number): void;
removeViews(removeViews: View[], viewContainer: ViewContainer): void;
getViewContainer(id: string): ViewContainer | undefined;
getViewContainer2(view: View): ViewContainer | undefined;
toJSON(): SerializedViewService;
load(layout: SerializedViewService): void;
}
export class ViewService implements IViewService {
private _viewContainers: ViewContainer[] = [];
private readonly _onDidActiveContainerChange = new Emitter<void>();
readonly onDidActiveContainerChange = this._onDidActiveContainerChange
.event;
private readonly _onDidRemoveContainer = new Emitter<void>();
readonly onDidRemoveContainer = this._onDidRemoveContainer.event;
private readonly _onDidAddContainer = new Emitter<void>();
readonly onDidAddContainer = this._onDidAddContainer.event;
private readonly _onDidContainersChange = new Emitter<void>();
readonly onDidContainersChange = this._onDidContainersChange.event;
private _activeViewContainerId: string;
get containers(): ViewContainer[] {
return this._viewContainers;
}
get activeContainer(): ViewContainer | undefined {
return this._viewContainers.find(
(c) => c.id === this._activeViewContainerId
);
}
constructor(private readonly viewRegistry: IViewRegistry) {
//
}
load(layout: SerializedViewService): void {
const { containers, activeContainer } = layout;
for (const container of containers) {
const { id, views } = container;
const viewContainer = new PaneviewContainer(
id,
this.viewRegistry,
views
);
this.addContainer(viewContainer);
}
this.setActiveViewContainer(activeContainer);
}
insertContainerAfter(source: ViewContainer, target: ViewContainer): void {
const sourceIndex = this._viewContainers.findIndex(
(c) => c.id === source.id
);
const view = this._viewContainers.splice(sourceIndex, 1)[0];
const targetIndex = this._viewContainers.findIndex(
(c) => c.id === target.id
);
this._viewContainers.splice(targetIndex + 1, 0, view);
this._viewContainers = [...this._viewContainers];
this._onDidContainersChange.fire();
}
insertContainerBefore(source: ViewContainer, target: ViewContainer): void {
const sourceIndex = this._viewContainers.findIndex(
(c) => c.id === source.id
);
const view = this._viewContainers.splice(sourceIndex, 1)[0];
const targetIndex = this._viewContainers.findIndex(
(c) => c.id === target.id
);
this._viewContainers.splice(Math.max(targetIndex, 0), 0, view);
this._viewContainers = [...this._viewContainers];
this._onDidContainersChange.fire();
}
addContainer(container: ViewContainer): void {
this._viewContainers = [...this._viewContainers, container];
this._activeViewContainerId = container.id;
this._onDidAddContainer.fire();
}
removeContainer(container: ViewContainer): void {
this._viewContainers = this._viewContainers.filter(
(c) => c.id !== container.id
);
if (this._activeViewContainerId === container.id) {
this._activeViewContainerId =
this._viewContainers.length > 0
? this._viewContainers[0].id
: undefined;
}
this._onDidRemoveContainer.fire();
}
setActiveViewContainer(id: string): void {
if (!this._viewContainers.find((c) => c.id === id)) {
throw new Error(`invalid container ${id}`);
}
this._activeViewContainerId = id;
this._onDidActiveContainerChange.fire();
}
getView(id: string): View | undefined {
for (const container of Array.from(this._viewContainers.values())) {
const view = container.views.find((v) => v.id === id);
if (view) {
return view;
}
}
return undefined;
}
moveViewToLocation(
view: View,
targetViewContainer: ViewContainer,
targetLocation: number
): void {
const sourceViewContainer = this.getViewContainer2(view);
this.removeViews([view], sourceViewContainer);
this.addViews(view, targetViewContainer, targetLocation);
}
addViews(
view: View,
viewContainer: ViewContainer,
location?: number
): void {
viewContainer.addView(view, location);
}
removeViews(removeViews: View[], viewContainer: ViewContainer): void {
for (const view of removeViews) {
viewContainer.removeView(view);
if (viewContainer.views.length === 0) {
viewContainer.clear();
this.removeContainer(viewContainer);
}
}
}
getViewContainer(id: string): ViewContainer | undefined {
return this._viewContainers.find((c) => c.id === id);
}
getViewContainer2(view: View): ViewContainer | undefined {
for (const container of Array.from(this._viewContainers.values())) {
const v = container.views.find((v) => v.id === view.id);
if (v) {
return container;
}
}
return undefined;
}
toJSON(): SerializedViewService {
return {
containers: this.containers.map((c) => c.toJSON()),
activeContainer: this.activeContainer.id,
};
}
dispose(): void {
this._onDidActiveContainerChange.dispose();
this._onDidAddContainer.dispose();
this._onDidRemoveContainer.dispose();
}
}

View File

@ -1,32 +0,0 @@
.sidebar-part {
background-color: #252526;
}
.activity-bar-part {
background-color: #333333;
.activity-bar-item {
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
height: 48px;
box-sizing: border-box;
position: relative;
color: grey;
font-size: 24px;
&:hover,
&.active {
color: white;
}
}
.activity-bar-space {
height: 100%;
&.activity-bar-space-dragover {
border-top: 2px solid white;
}
}
}

View File

@ -1,474 +0,0 @@
import {
getPaneData,
GridviewApi,
IGridviewPanelProps,
IPaneviewPanelProps,
PanelCollection,
PaneviewApi,
PaneviewDropEvent,
PaneviewReact,
PaneviewReadyEvent,
Position,
} from 'dockview';
import * as React from 'react';
import { useLayoutRegistry } from '../layout-grid/registry';
import { PaneviewContainer, ViewContainer } from './viewContainer';
import { IViewService, ViewService } from './viewService';
import { DefaultView } from './view';
import { RegisteredView, VIEW_REGISTRY } from './viewRegistry';
import { toggleClass } from '../dom';
import { Container } from './sidebarItem';
import './widgets.scss';
import { CompositeDisposable } from '../lifecycle';
class ViewServiceModel {
private readonly viewService: IViewService;
get model() {
return this.viewService;
}
constructor() {
this.viewService = new ViewService(VIEW_REGISTRY);
this.init();
}
init(): void {
const layout = localStorage.getItem('viewservice');
if (layout) {
this.viewService.load(JSON.parse(layout));
} else {
const container1 = new PaneviewContainer(
'default_container_1',
VIEW_REGISTRY
);
if (!container1.schema) {
this.addView(
container1,
VIEW_REGISTRY.getRegisteredView('search_widget')
);
this.addView(
container1,
VIEW_REGISTRY.getRegisteredView('home_widget')
);
}
const container2 = new PaneviewContainer(
'default_container_2',
VIEW_REGISTRY
);
if (!container2.schema) {
this.addView(
container2,
VIEW_REGISTRY.getRegisteredView('account_widget')
);
this.addView(
container2,
VIEW_REGISTRY.getRegisteredView('settings_widget')
);
}
this.viewService.addContainer(container1);
this.viewService.addContainer(container2);
}
const save = () => {
localStorage.setItem(
'viewservice',
JSON.stringify(this.viewService.toJSON())
);
};
this.viewService.onDidActiveContainerChange(save);
this.viewService.onDidRemoveContainer(save);
this.viewService.onDidAddContainer(save);
}
private addView(
container: ViewContainer,
registedView: RegisteredView
): void {
container.addView(
new DefaultView({
id: registedView.id,
title: registedView.title,
isExpanded: true,
isLocationEditable: registedView.isLocationEditable,
icon: registedView.icon,
})
);
}
}
const viewService = new ViewServiceModel();
const colors = {
home_widget: 'red',
account_widget: 'green',
settings_widget: 'yellow',
search_widget: 'orange',
};
const components: PanelCollection<IPaneviewPanelProps<any>> = {
default: (props: IPaneviewPanelProps<{ viewId: string }>) => {
const Component = React.useMemo(() => {
const registeredView = VIEW_REGISTRY.getRegisteredView(
props.params.viewId
);
return registeredView?.component;
}, [props.params.viewId]);
return Component ? <Component /> : null;
},
};
export const Activitybar = (props: IGridviewPanelProps) => {
const [activeContainerid, setActiveContainerId] = React.useState<string>(
viewService.model.activeContainer.id
);
const [containers, setContainers] = React.useState<ViewContainer[]>(
viewService.model.containers
);
const registry = useLayoutRegistry();
React.useEffect(() => {
const disposable = new CompositeDisposable(
viewService.model.onDidActiveContainerChange(() => {
setActiveContainerId(viewService.model.activeContainer.id);
}),
viewService.model.onDidAddContainer(() => {
setContainers(viewService.model.containers);
}),
viewService.model.onDidRemoveContainer(() => {
setContainers(viewService.model.containers);
}),
viewService.model.onDidContainersChange(() => {
setContainers(viewService.model.containers);
})
);
return () => {
disposable.dispose();
};
}, []);
const onClick =
(container: ViewContainer, alwaysOpen = false) =>
(event: React.MouseEvent) => {
const api = registry.get<GridviewApi>('gridview');
const selectedActive = container.id === activeContainerid;
const sidebarPanel = api.getPanel('sidebar');
if (sidebarPanel.api.isVisible) {
if (!alwaysOpen && selectedActive) {
sidebarPanel.api.setVisible(false);
}
} else {
event.preventDefault(); // prevent focus
sidebarPanel.api.setVisible(true);
sidebarPanel.focus();
}
viewService.model.setActiveViewContainer(container.id);
};
const onContainerDrop =
(targetContainer: ViewContainer) =>
(event: React.DragEvent, direction: 'top' | 'bottom') => {
const data = event.dataTransfer.getData('application/json');
if (data) {
const { container } = JSON.parse(data);
const sourceContainer =
viewService.model.getViewContainer(container);
switch (direction) {
case 'bottom':
viewService.model.insertContainerAfter(
sourceContainer,
targetContainer
);
break;
case 'top':
viewService.model.insertContainerBefore(
sourceContainer,
targetContainer
);
break;
}
viewService.model.setActiveViewContainer(sourceContainer.id);
}
};
const onNewContainer = (event: React.DragEvent) => {
const data = getPaneData();
if (data) {
const { paneId } = data;
const view = viewService.model.getView(paneId);
if (!view) {
console.log(`view ${paneId} doesn't exist`);
return;
}
const viewContainer = viewService.model.getViewContainer2(view);
if (!viewContainer) {
console.log(`viewContainer for view ${view.id} doesn't exist`);
return;
}
viewService.model.removeViews([view], viewContainer);
const newContainer = new PaneviewContainer(
`t_${Date.now().toString().substr(5)}`,
VIEW_REGISTRY
);
newContainer.addView(view);
viewService.model.addContainer(newContainer);
}
};
const onDragOver = (container: ViewContainer) => (e: React.DragEvent) => {
const api = registry.get<GridviewApi>('gridview');
const sidebarPanel = api.getPanel('sidebar');
if (!sidebarPanel.api.isVisible) {
sidebarPanel.api.setVisible(true);
sidebarPanel.focus();
}
viewService.model.setActiveViewContainer(container.id);
};
return (
<div className="activity-bar-part">
{containers.map((container, i) => {
const isActive = activeContainerid === container.id;
return (
<Container
key={i}
container={container}
isActive={isActive}
onDragOver={onDragOver(container)}
onClick={onClick(container)}
onDrop={onContainerDrop(container)}
/>
);
})}
<ExtraSpace onNewContainer={onNewContainer} />
</div>
);
};
const ExtraSpace = (props: {
onNewContainer: (event: React.DragEvent) => void;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
const onDrop = (event: React.DragEvent) => {
toggleClass(ref.current, 'activity-bar-space-dragover', false);
props.onNewContainer(event);
};
return (
<div
ref={ref}
className="activity-bar-space"
onDragOver={(e) => {
e.preventDefault();
}}
onDragEnter={(e) => {
toggleClass(ref.current, 'activity-bar-space-dragover', true);
e.preventDefault();
}}
onDragLeave={(e) => {
toggleClass(ref.current, 'activity-bar-space-dragover', false);
}}
onDrop={onDrop}
></div>
);
};
export const Sidebar = () => {
const [sidebarId, setSidebarId] = React.useState<string>(
viewService.model.activeContainer.id
);
React.useEffect(() => {
const disposable = viewService.model.onDidActiveContainerChange(() => {
setSidebarId(viewService.model.activeContainer.id);
});
return () => {
disposable.dispose();
};
}, []);
return <SidebarPart id={sidebarId} />;
};
const headerComponents: PanelCollection<IPaneviewPanelProps> = {
default: (props) => {
const onClick = () => props.api.setExpanded(!props.api.isExpanded);
return (
<div
onClick={onClick}
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
cursor: 'pointer',
}}
>
<a className="material-icons-outlined">
{props.api.isExpanded ? 'expand_more' : 'chevron_right'}
</a>
<span>{props.title}</span>
</div>
);
},
};
export const SidebarPart = (props: { id: string }) => {
const [api, setApi] = React.useState<PaneviewApi>();
React.useEffect(() => {
if (!api) {
return () => {
//
};
}
const viewContainer = viewService.model.getViewContainer(props.id);
const disposables = new CompositeDisposable(
api.onDidLayoutChange(() => {
viewContainer.layout(api.toJSON());
}),
viewContainer.onDidAddView(({ view, index }) => {
api.addPanel({
id: view.id,
isExpanded: view.isExpanded,
title: view.title,
component: 'default',
headerComponent: 'default',
params: {
viewId: view.id,
},
index,
});
}),
viewContainer.onDidRemoveView((view) => {
const panel = api.getPanel(view.id);
api.removePanel(panel);
})
);
const schema = viewContainer.schema;
if (schema) {
api.fromJSON(schema);
} else {
api.panels.forEach((p) => {
api.removePanel(p);
});
viewContainer.views.forEach((view) => {
api.addPanel({
id: view.id,
isExpanded: view.isExpanded,
title: view.title,
component: 'default',
headerComponent: 'default',
params: {
viewId: view.id,
},
});
});
}
return () => {
disposables.dispose();
};
}, [api, props.id]);
const onReady = (event: PaneviewReadyEvent) => {
setApi(event.api);
};
const onDidDrop = (event: PaneviewDropEvent) => {
const data = event.getData();
const containerData =
event.nativeEvent.dataTransfer.getData('application/json');
if (containerData) {
const { container } = JSON.parse(containerData);
if (container === props.id) {
return;
}
const sourceContainer =
viewService.model.getViewContainer(container);
const targetContainer = viewService.model.getViewContainer(
props.id
);
if (!sourceContainer) {
console.log(`sourceContainer ${props.id} doesn't exist`);
return;
}
if (!targetContainer) {
console.log(`targetContainer ${props.id} doesn't exist`);
return;
}
sourceContainer.views.forEach((v) => {
viewService.model.moveViewToLocation(v, targetContainer, 0);
});
return;
}
if (!data) {
return;
}
const targetPanel = event.panel;
const allPanels = event.api.panels;
let toIndex = allPanels.indexOf(targetPanel);
if (
event.position === Position.Right ||
event.position === Position.Bottom
) {
toIndex = Math.min(allPanels.length, toIndex + 1);
}
const viewId = data.paneId;
const viewContainer = viewService.model.getViewContainer(props.id);
if (!viewContainer) {
console.log(`viewContainer ${props.id} doesn't exist`);
return;
}
const view = viewService.model.getView(viewId);
viewService.model.moveViewToLocation(view, viewContainer, toIndex);
};
if (!props.id) {
return null;
}
return (
<PaneviewReact
className="sidebar-part"
onDidDrop={onDidDrop}
components={components}
headerComponents={headerComponents}
onReady={onReady}
/>
);
};

View File

@ -1,64 +0,0 @@
import { Meta } from '@storybook/addon-docs/blocks';
import Code from './assets/code-brackets.svg';
import Colors from './assets/colors.svg';
import Comments from './assets/comments.svg';
import Direction from './assets/direction.svg';
import Flow from './assets/flow.svg';
import Plugin from './assets/plugin.svg';
import Repo from './assets/repo.svg';
import StackAlt from './assets/stackalt.svg';
import Splitview from '../resources/splitview-logo.svg';
import Gridview from '../resources/gridview-logo.svg';
import Dockview from '../resources/dockview-logo.svg';
import Paneview from '../resources/paneview-logo.svg';
import './introduction.css';
<Meta title="Introduction" />
# Dockview
<div className="subheading">Components</div>
<div className="link-list">
<a
className="link-item"
href="/?path=/story/library-splitview-documentation--page"
>
<img src={Splitview} alt="splitview" />
<span>
<strong>Splitview</strong>
Vertical or horiziontal splitview panels
</span>
</a>
<a
className="link-item"
href="/?path=/story/library-gridview-documentation--page"
>
<img src={Gridview} alt="gridview" />
<span>
<strong>Gridview</strong>
Splitviews within splitviews
</span>
</a>
<a
className="link-item"
href="/?path=/story/library-dockview-documentation--page"
>
<img src={Dockview} alt="dockview" />
<span>
<strong>Dockview</strong>
Dockable and tabular panels within Gridviews
</span>
</a>
<a
className="link-item"
href="/?path=/story/library-paneview-documentation--page"
>
<img src={Paneview} alt="paneview" />
<span>
<strong>Paneview</strong>
An extension to splitviews allowing collapsable panels with a persistant
header section
</span>
</a>
</div>

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,7 +0,0 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Library/Dockview/Documentation" />
# Dockview documentation
<div className="subheading">Components</div>

View File

@ -1,108 +0,0 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
PanelCollection,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IDockviewPanelProps> = {
default: (props) => {
return (
<div style={{ padding: '10px', height: '100%' }}>
This content is not within an IFrame
</div>
);
},
iframe: (props) => {
return (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
height: '25px',
display: 'flex',
alignItems: 'center',
margin: '0px 4px',
}}
>
The contents below is within an iFrame
</div>
<iframe
src="./"
style={{
flexGrow: 1,
boxSizing: 'border-box',
border: '1px solid red',
width: '100%',
}}
/>
</div>
);
},
};
export const Iframe = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
title: 'Standard Panel',
component: 'default',
});
event.api.addPanel({
id: 'panel2',
title: 'IFrame Panel',
component: 'iframe',
});
};
return (
<DockviewReact
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Dockview/Iframe',
component: Iframe,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,126 +0,0 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
PanelCollection,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IDockviewPanelProps<any>> = {
default: (props) => {
return (
<div style={{ padding: '10px', height: '100%' }}>hello world</div>
);
},
ticker: (props: IDockviewPanelProps<{ ticker: number }>) => {
const close = () => props.api.close();
return (
<div style={{ padding: '10px', height: '100%' }}>
{`The current ticker value is ${props.params.ticker}`}
</div>
);
},
iframe: (props) => {
return (
<div style={{ height: '100%', width: '100%' }}>
<iframe src="./" style={{ height: '100%', width: '100%' }}>
Hello world
</iframe>
</div>
);
},
};
export const Params = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<DockviewApi>();
React.useEffect(() => {
if (!api.current) {
return () => {
// noop
};
}
const gridApi = api.current;
const interval = setInterval(() => {
const panel1 = gridApi.getPanel('panel1');
const panel2 = gridApi.getPanel('panel2');
panel1.update({ params: { params: { ticker: Date.now() } } });
panel2.api.setTitle(`Panel2 ${Date.now()}`);
}, 1000);
return () => {
clearInterval(interval);
};
}, [api]);
const onReady = (event: DockviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'ticker',
params: {
ticker: 0,
},
});
event.api.addPanel({
id: 'panel2',
component: 'default',
});
event.api.addPanel({
id: 'panel3',
component: 'default',
position: { referencePanel: 'panel1', direction: 'right' },
});
event.api.addPanel({
id: 'panel4',
component: 'default',
position: { referencePanel: 'panel3', direction: 'below' },
});
// event.api.getPanel('panel1').api;
};
return (
<DockviewReact
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Dockview/Params',
component: Params,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,93 +0,0 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
PanelCollection,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IDockviewPanelProps> = {
default: (props) => {
return (
<div style={{ padding: '10px', height: '100%' }}>hello world</div>
);
},
iframe: (props) => {
return (
<div style={{ height: '100%', width: '100%' }}>
<iframe src="./" style={{ height: '100%', width: '100%' }}>
Hello world
</iframe>
</div>
);
},
};
export const Simple = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'default',
});
event.api.addPanel({
id: 'panel2',
component: 'default',
});
event.api.addPanel({
id: 'panel3',
component: 'default',
position: { referencePanel: 'panel1', direction: 'right' },
});
event.api.addPanel({
id: 'panel4',
component: 'default',
position: { referencePanel: 'panel3', direction: 'below' },
});
// event.api.getPanel('panel1').api;
};
return (
<DockviewReact
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Dockview/Simple',
component: Simple,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,85 +0,0 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
PanelCollection,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IDockviewPanelProps> = {
default: (props) => {
const close = () => props.api.close();
return (
<div style={{ padding: '10px', height: '100%' }}>hello world</div>
);
},
};
export const Tab = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'default',
});
event.api.addPanel({
id: 'panel2',
component: 'default',
});
event.api.addPanel({
id: 'panel3',
component: 'default',
position: { referencePanel: 'panel1', direction: 'right' },
});
event.api.addPanel({
id: 'panel4',
component: 'default',
position: { referencePanel: 'panel3', direction: 'below' },
});
// event.api.getPanel('panel1').api;
};
return (
<DockviewReact
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Dockview/Tab',
component: Tab,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,145 +0,0 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
IWatermarkPanelProps,
PanelCollection,
} from 'dockview';
import { CompositeDisposable } from '../../lifecycle';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IDockviewPanelProps> = {
default: (props) => {
return (
<div style={{ padding: '10px', height: '100%' }}>hello world</div>
);
},
iframe: (props) => {
return (
<div style={{ height: '100%', width: '100%' }}>
<iframe src="./" style={{ height: '100%', width: '100%' }}>
Hello world
</iframe>
</div>
);
},
};
const WatermarkPanel = (props: IWatermarkPanelProps) => {
const [size, setSize] = React.useState<number>(props.containerApi.size);
const [panels, setPanels] = React.useState<number>(
props.containerApi.totalPanels
);
React.useEffect(() => {
const disposables = new CompositeDisposable(
props.containerApi.onDidAddGroup((event) => {
setSize(props.containerApi.size);
}),
props.containerApi.onDidRemoveGroup((event) => {
setSize(props.containerApi.size);
}),
props.containerApi.onDidAddPanel((event) => {
setPanels(props.containerApi.totalPanels);
}),
props.containerApi.onDidRemovePanel((event) => {
setPanels(props.containerApi.totalPanels);
})
);
return () => {
disposables.dispose();
};
}, []);
const onClick = () => {
props.containerApi.addPanel({
id: Date.now().toString(),
component: 'default',
position: { direction: 'right' },
});
};
const onReplace = () => {
props.containerApi.addPanel({
id: Date.now().toString(),
component: 'default',
});
};
const onAddEmptyGroup = () => {
props.containerApi.addEmptyGroup();
};
return (
<div style={{ margin: '10px' }}>
<ul>
<li>
This is a custom watermark for when a group has no panels to
display.
</li>
<li>
The watermark is only automatically shown when there are no
panels to display. You can otherwise add a watermark
programatically.
</li>
</ul>
<div>{`Total groups: ${size}`}</div>
<div>{`Total panels: ${panels}`}</div>
<button onClick={onClick}>Add</button>
<button onClick={onReplace}>Replace</button>
<button onClick={onAddEmptyGroup}>Empty</button>
{size > 1 && <button onClick={props.close}>Close</button>}
</div>
);
};
export const Watermark = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
api.current = event.api;
event.api.addEmptyGroup();
};
return (
<DockviewReact
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
watermarkComponent={WatermarkPanel}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Dockview/Watermark',
component: Watermark,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,7 +0,0 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Library/Gridview/Documentation" />
# Gridview documentation
<div className="subheading">Components</div>

View File

@ -1,107 +0,0 @@
import {
GridviewApi,
GridviewReact,
GridviewReadyEvent,
IGridviewPanelProps,
Orientation,
PanelCollection,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IGridviewPanelProps<any>> = {
default: (props) => {
return (
<div style={{ padding: '10px', height: '100%' }}>hello world</div>
);
},
ticker: (props: IGridviewPanelProps<{ ticker: number }>) => {
return (
<div style={{ padding: '10px', height: '100%' }}>
{`The current ticker value is ${props.params.ticker}`}
</div>
);
},
};
export const Params = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<GridviewApi>();
React.useEffect(() => {
if (!api.current) {
return () => {
// noop
};
}
const gridApi = api.current;
const interval = setInterval(() => {
const panel1 = gridApi.getPanel('panel1');
panel1.update({ params: { ticker: Date.now() } });
}, 1000);
return () => {
clearInterval(interval);
};
}, [api]);
const onReady = (event: GridviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'ticker',
params: {
ticker: 0,
},
});
event.api.addPanel({
id: 'panel2',
component: 'default',
});
event.api.addPanel({
id: 'panel3',
component: 'default',
});
};
return (
<GridviewReact
orientation={Orientation.HORIZONTAL}
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Gridview/Params',
component: Params,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,137 +0,0 @@
import {
GridviewApi,
GridviewReact,
GridviewReadyEvent,
IGridviewPanelProps,
Orientation,
orthogonal,
PanelCollection,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IGridviewPanelProps<any>> = {
default: (props: IGridviewPanelProps<{ color: string }>) => {
const transpose = () => {
props.containerApi.orientation = orthogonal(
props.containerApi.orientation
);
};
const resizeWidth = () => {
props.api.setSize({ width: 300 });
};
const resizeHeight = () => {
props.api.setSize({ height: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
height: '100%',
}}
>
<div>{'hello world'}</div>
<button onClick={transpose}>Transpose</button>
<button onClick={resizeWidth}>Resize width</button>
<button onClick={resizeHeight}>Resize height</button>
</div>
);
},
};
export const Simple = (props: {
orientation: Orientation;
hideBorders: boolean;
proportionalLayout: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<GridviewApi>();
const onReady = (event: GridviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel_1',
component: 'default',
params: { color: 'red' },
minimumHeight: 50,
minimumWidth: 50,
location: [0],
});
event.api.addPanel({
id: 'panel_2',
component: 'default',
params: { color: 'green' },
minimumHeight: 50,
minimumWidth: 50,
location: [0, 0],
});
event.api.addPanel({
id: 'panel_3',
component: 'default',
params: { color: 'purple' },
minimumHeight: 50,
minimumWidth: 50,
location: [0, 0, 0],
});
event.api.addPanel({
id: 'panel_4',
component: 'default',
params: { color: 'yellow' },
minimumHeight: 50,
minimumWidth: 50,
location: [0, 0, 0, 0],
});
event.api.addPanel({
id: 'panel_5',
component: 'default',
params: { color: 'dodgerblue' },
minimumHeight: 50,
minimumWidth: 50,
location: [0, 0, 0, 0, 0],
});
};
return (
<GridviewReact
onReady={onReady}
orientation={props.orientation}
components={components}
hideBorders={props.hideBorders}
proportionalLayout={props.proportionalLayout}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Gridview/Simple',
component: Simple,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: {
orientation: Orientation.VERTICAL,
proportionalLayout: true,
disableAutoResizing: false,
},
argTypes: {
orientation: {
control: {
type: 'inline-radio',
options: [Orientation.HORIZONTAL, Orientation.VERTICAL],
},
},
},
} as Meta;

View File

@ -1,100 +0,0 @@
.subheading {
--mediumdark: '#999999';
font-weight: 900;
font-size: 13px;
color: #999;
letter-spacing: 6px;
line-height: 24px;
text-transform: uppercase;
margin-bottom: 12px;
margin-top: 40px;
}
.link-list {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
row-gap: 10px;
}
@media (min-width: 620px) {
.link-list {
row-gap: 20px;
column-gap: 20px;
grid-template-columns: 1fr 1fr;
}
}
@media all and (-ms-high-contrast: none) {
.link-list {
display: -ms-grid;
-ms-grid-columns: 1fr 1fr;
-ms-grid-rows: 1fr 1fr;
}
}
.link-item {
display: block;
padding: 20px 30px 20px 15px;
border: 1px solid #00000010;
border-radius: 5px;
transition: background 150ms ease-out, border 150ms ease-out,
transform 150ms ease-out;
color: #333333;
display: flex;
align-items: flex-start;
}
.link-item:hover {
border-color: #1ea7fd50;
transform: translate3d(0, -3px, 0);
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
}
.link-item:active {
border-color: #1ea7fd;
transform: translate3d(0, 0, 0);
}
.link-item strong {
font-weight: 700;
display: block;
margin-bottom: 2px;
}
.link-item img {
height: 40px;
width: 40px;
margin-right: 15px;
flex: none;
}
.link-item span {
font-size: 14px;
line-height: 20px;
}
.tip {
display: inline-block;
border-radius: 1em;
font-size: 11px;
line-height: 12px;
font-weight: 700;
background: #e7fdd8;
color: #66bf3c;
padding: 4px 12px;
margin-right: 10px;
vertical-align: top;
}
.tip-wrapper {
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
}
.tip-wrapper code {
font-size: 12px;
display: inline-block;
}

View File

@ -1,161 +0,0 @@
import {
PanelCollection,
PaneviewReact,
PaneviewApi,
PaneviewReadyEvent,
IPaneviewPanelProps,
PanelConstraintChangeEvent,
PanelDimensionChangeEvent,
ExpansionEvent,
FocusEvent,
ActiveEvent,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
import { CompositeDisposable } from '../../lifecycle';
const components: PanelCollection<IPaneviewPanelProps<any>> = {
default: (props: IPaneviewPanelProps<{ color: string }>) => {
const [
constraints,
setConstraints,
] = React.useState<PanelConstraintChangeEvent>();
const [
dimensions,
setDimensions,
] = React.useState<PanelDimensionChangeEvent>();
const [
expansionState,
setExpansionState,
] = React.useState<ExpansionEvent>();
const [active, setActive] = React.useState<ActiveEvent>();
const [focus, setFocus] = React.useState<FocusEvent>();
React.useEffect(() => {
const disposables = new CompositeDisposable(
props.api.onDidConstraintsChange(setConstraints),
props.api.onDidDimensionsChange(setDimensions),
props.api.onDidExpansionChange(setExpansionState),
props.api.onDidActiveChange(setActive),
props.api.onDidFocusChange(setFocus)
);
return () => {
disposables.dispose();
};
}, []);
const resize = () => {
props.api.setSize({ size: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
boxSizing: 'border-box',
height: '100%',
}}
>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
>
<div>Contraints:</div>
<div>{`maximumSize: ${constraints?.maximumSize} minimumSize: ${constraints?.minimumSize}`}</div>
<div>Dimesions:</div>
<div>{`width: ${dimensions?.width} height: ${dimensions?.height}`}</div>
<div>Expansion:</div>
<div>{`expanded: ${expansionState?.isExpanded}`}</div>
<div>Active:</div>
<div>{`active: ${active?.isActive}`}</div>
<div>Focus:</div>
<div>{`focused: ${focus?.isFocused}`}</div>
</div>
<button onClick={resize}>Resize</button>
</div>
);
},
};
export const Deserialization = (props: {
theme: string;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<PaneviewApi>();
const onReady = (event: PaneviewReadyEvent) => {
api.current = event.api;
event.api.fromJSON({
size: 100,
views: [
{
size: 80,
expanded: true,
minimumSize: 100,
data: {
id: 'panel1',
component: 'default',
title: 'Panel 1',
},
},
{
size: 20,
expanded: true,
minimumSize: 100,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
},
{
size: 20,
expanded: false,
minimumSize: 100,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
},
],
});
event.api.getPanel('panel2')?.api.setSize({ size: 60 });
};
return (
<PaneviewReact
className={props.theme}
onReady={onReady}
components={components}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Paneview/Deserialization',
component: Deserialization,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light', disableAutoResizing: false },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,7 +0,0 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Library/Paneview/Documentation" />
# Paneview documentation
<div className="subheading">Components</div>

View File

@ -1,174 +0,0 @@
import {
PanelCollection,
PaneviewReact,
PaneviewApi,
PaneviewReadyEvent,
IPaneviewPanelProps,
SerializedPaneview,
PanelConstraintChangeEvent,
PanelDimensionChangeEvent,
ExpansionEvent,
FocusEvent,
ActiveEvent,
} from 'dockview';
import { CompositeDisposable } from '../../lifecycle';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<IPaneviewPanelProps<any>> = {
default: (props: IPaneviewPanelProps<{ color: string }>) => {
const [
constraints,
setConstraints,
] = React.useState<PanelConstraintChangeEvent>();
const [
dimensions,
setDimensions,
] = React.useState<PanelDimensionChangeEvent>();
const [
expansionState,
setExpansionState,
] = React.useState<ExpansionEvent>();
const [active, setActive] = React.useState<ActiveEvent>();
const [focus, setFocus] = React.useState<FocusEvent>();
React.useEffect(() => {
const disposables = new CompositeDisposable(
props.api.onDidConstraintsChange(setConstraints),
props.api.onDidDimensionsChange(setDimensions),
props.api.onDidExpansionChange(setExpansionState),
props.api.onDidActiveChange(setActive),
props.api.onDidFocusChange(setFocus)
);
return () => {
disposables.dispose();
};
}, []);
const resize = () => {
props.api.setSize({ size: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
boxSizing: 'border-box',
height: '100%',
}}
>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
>
<div>Contraints:</div>
<div>{`maximumSize: ${constraints?.maximumSize} minimumSize: ${constraints?.minimumSize}`}</div>
<div>Dimesions:</div>
<div>{`width: ${dimensions?.width} height: ${dimensions?.height}`}</div>
<div>Expansion:</div>
<div>{`expanded: ${expansionState?.isExpanded}`}</div>
<div>Active:</div>
<div>{`active: ${active?.isActive}`}</div>
<div>Focus:</div>
<div>{`focused: ${focus?.isFocused}`}</div>
</div>
<button onClick={resize}>Resize</button>
</div>
);
},
};
export const Persistance = (props: {
theme: string;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<PaneviewApi>();
const onReady = (event: PaneviewReadyEvent) => {
api.current = event.api;
event.api.onDidLayoutChange(() => {
const state = event.api.toJSON();
localStorage.setItem('paneview.test.layout', JSON.stringify(state));
console.log(JSON.stringify(state, null, 4));
});
const state = localStorage.getItem('paneview.test.layout');
if (state) {
event.api.fromJSON(JSON.parse(state) as SerializedPaneview);
return;
}
event.api.fromJSON({
size: 100,
views: [
{
size: 80,
expanded: true,
minimumSize: 100,
data: {
id: 'panel1',
component: 'default',
title: 'Panel 1',
},
},
{
size: 20,
expanded: true,
minimumSize: 100,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
},
{
size: 20,
expanded: false,
minimumSize: 100,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
},
],
});
event.api.getPanel('panel2')?.api.setSize({ size: 60 });
};
return (
<PaneviewReact
className={props.theme}
onReady={onReady}
components={components}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Paneview/Persistance',
component: Persistance,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light', disableAutoResizing: false },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,145 +0,0 @@
import {
PanelCollection,
PaneviewReact,
PaneviewApi,
PaneviewReadyEvent,
IPaneviewPanelProps,
PanelConstraintChangeEvent,
PanelDimensionChangeEvent,
ExpansionEvent,
FocusEvent,
ActiveEvent,
} from 'dockview';
import { CompositeDisposable } from '../../lifecycle';
import * as React from 'react';
import { Story, Meta } from '@storybook/react';
const components: PanelCollection<IPaneviewPanelProps<any>> = {
default: (props: IPaneviewPanelProps<{ color: string }>) => {
const [
constraints,
setConstraints,
] = React.useState<PanelConstraintChangeEvent>();
const [
dimensions,
setDimensions,
] = React.useState<PanelDimensionChangeEvent>();
const [
expansionState,
setExpansionState,
] = React.useState<ExpansionEvent>();
const [active, setActive] = React.useState<ActiveEvent>();
const [focus, setFocus] = React.useState<FocusEvent>();
React.useEffect(() => {
const disposables = new CompositeDisposable(
props.api.onDidConstraintsChange(setConstraints),
props.api.onDidDimensionsChange(setDimensions),
props.api.onDidExpansionChange(setExpansionState),
props.api.onDidActiveChange(setActive),
props.api.onDidFocusChange(setFocus)
);
return () => {
disposables.dispose();
};
}, []);
const resize = () => {
props.api.setSize({ size: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
boxSizing: 'border-box',
height: '100%',
}}
>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
>
<div>Contraints:</div>
<div>{`maximumSize: ${constraints?.maximumSize} minimumSize: ${constraints?.minimumSize}`}</div>
<div>Dimesions:</div>
<div>{`width: ${dimensions?.width} height: ${dimensions?.height}`}</div>
<div>Expansion:</div>
<div>{`expanded: ${expansionState?.isExpanded}`}</div>
<div>Active:</div>
<div>{`active: ${active?.isActive}`}</div>
<div>Focus:</div>
<div>{`focused: ${focus?.isFocused}`}</div>
</div>
<button onClick={resize}>Resize</button>
</div>
);
},
};
export const Simple = (props: {
theme: string;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<PaneviewApi>();
const onReady = (event: PaneviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'default',
params: { color: 'red' },
title: 'Panel1',
minimumBodySize: 100,
});
event.api.addPanel({
id: 'panel2',
component: 'default',
params: { color: 'green' },
title: 'Panel 2',
minimumBodySize: 100,
});
event.api.addPanel({
id: 'panel3',
component: 'default',
params: { color: 'purple' },
title: 'Panel 3',
minimumBodySize: 100,
});
};
return (
<PaneviewReact
className={props.theme}
onReady={onReady}
components={components}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Paneview/Simple',
component: Simple,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light', disableAutoResizing: false },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

View File

@ -1,74 +0,0 @@
.markdown-line {
padding-left: 20px;
}
.indent-1 {
padding-left: 40px;
}
.markdown-highlight {
background-color: rgba(30, 144, 255, 0.1);
}
.sash {
background-color: orange;
position: absolute;
top: 0px;
width: 4px;
height: 100%;
z-index: 2;
cursor: ew-resize;
user-select: none;
}
.debug-sash-max {
height: 10px;
width: 1px;
position: absolute;
z-index: 999;
top: -10px;
}
.debug-sash-min {
height: 10px;
width: 1px;
position: absolute;
z-index: 999;
top: 100%;
}
.debug-sash-text {
height: 20px;
line-height: 20px;
width: 80px;
display: flex;
justify-content: center;
position: absolute;
z-index: 999;
font-size: 14px;
}
.sash-container {
position: absolute;
height: 100%;
}
.view-container {
position: relative;
height: 100%;
}
.view {
position: absolute;
height: 100%;
background-color: dodgerblue;
z-index: 1;
top: 0px;
padding: 10px;
color: white;
box-sizing: border-box;
}
.sash.drag-sash {
background-color: red;
}

View File

@ -1,122 +0,0 @@
import {
ISplitviewPanelProps,
Orientation,
PanelCollection,
SplitviewApi,
SplitviewReact,
SplitviewReadyEvent,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<ISplitviewPanelProps> = {
default: (props: ISplitviewPanelProps<{ color: string }>) => {
const resize = () => {
props.api.setSize({ size: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
height: '100%',
boxSizing: 'border-box',
}}
>
<div>hello world</div>
<button onClick={resize}>Resize</button>
</div>
);
},
};
export const Deserialization = (props: {
orientation: Orientation;
hideBorders: boolean;
proportionalLayout: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<SplitviewApi>();
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
event.api.fromJSON({
size: 100,
views: [
{
size: 20,
data: {
id: 'panel1',
component: 'default',
params: {
color: 'red',
},
},
},
{
size: 40,
data: {
id: 'panel2',
component: 'default',
params: {
color: 'green',
},
},
},
{
size: 60,
data: {
id: 'panel3',
component: 'default',
params: {
color: 'purple',
},
},
},
],
orientation: props.orientation,
activeView: 'panel1',
});
};
return (
<SplitviewReact
onReady={onReady}
orientation={props.orientation}
components={components}
disableAutoResizing={props.disableAutoResizing}
hideBorders={props.hideBorders}
proportionalLayout={props.proportionalLayout}
/>
);
};
export default {
title: 'Library/Splitview/Deserialization',
component: Deserialization,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: {
orientation: Orientation.VERTICAL,
proportionalLayout: true,
disableAutoResizing: false,
},
argTypes: {
orientation: {
control: {
type: 'inline-radio',
options: [Orientation.HORIZONTAL, Orientation.VERTICAL],
},
},
},
} as Meta;

View File

@ -1,349 +0,0 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Library/Splitview/Documentation" />
import { Splitview } from './splitview';
import VisualOne from './visual_1.jpg';
import Contraints from './constraints.jpg';
# Splitview
The splitview component is one of the libraries key components.
The paneview is an extension of thesplitview, the gridview is a collection of nested splitviews and the dockview adds tabular panels to the gridview.
You can view several interactive examples of the Splitview component in action here, as well as read a description as to how this key component actually works.
# The math behind the split view layouting
The below is a language independant walkthrough of the math used within to layout these panels. This is the end result and below will explain the steps taken to reach this point.
The origins of this logic stem from the VSCode source code found [here](https://github.com/microsoft/vscode/tree/main/src/vs/base/browser/ui/splitview).
<Splitview mode={3} debug={false} />
## What is a Splitview?
> A Splitview control is a collection of _views_ stacked either _horizontally_ or _vertically_ where each view can be indepedantly resized by dragging on the edge of a particular view.
To explain how this works will require some better definitions so lets start with the two fundamental components of this control, the <b>View</b> and the <b>Sash</b>.
Assume the Splitview control has <b>n</b> views where n is positive number (i.e if n=4 then our split view controls has 4 views). A single view can then be defined as
<div>
<div>View</div>
<ul style={{ marginLeft: '40px' }}>
<li>
The size of the n<sup>th</sup> view will be known as V<sub>n</sub>
</li>
<li>
The minimum size of the n<sup>th</sup> view will be known as V
<sup>min</sup>
<sub>n</sub>
</li>
<li>
The maximum size of the n<sup>th</sup> view will be known as V
<sup>max</sup>
<sub>n</sub>
</li>
</ul>
</div>
Additionally by definition we can known V<sup>min</sup><sub>n</sub> <= V<sub>n</sub> <= V<sup>max</sup><sub>n</sub>
To be able to resize a view you need to be able to drag on the edge of a view to increase or decrease it's size.
This can be achieved by introducing a narrow component that sits between each view acting as a _drag handle_.
Lets call this component a <b>Sash</b> (see [link](https://en.wikipedia.org/wiki/Sash_window)) and we can define this as
<div>
<div>Sash</div>
<ul style={{marginLeft: "40px"}}>
<li>If we have n views then we will have n-1 sashes. There is no sash before V<sub>0</sub> nor after V<sub>n</sub></li>
<li>The sash between V<sub>n</sub> and V<sub>n+1</sub> is known as S<sub>n</sub></li>
<li>The sash is of fixed width, and it's sole purpose is to act a drag-handle for resizing views</li>
</ul>
</div>
To calculate the new view sizes after a sash is dragged we need to know which sash is being dragged.
Lets denote the sash S<sub>i</sub> as the sash to drag, which will give us a set of definitions to work with inline with the below diagram.
<img src={VisualOne} />
If we are to drag the sash S<sub>i</sub> then it's also needed to know how far along the x-axis, or the y-axis (in the case of vertically stacked views) you have travelled.
Lets denote this as the delta, using the symbol Δ. Delta is only limited by the width (or height) of the control so in it's most general form we say it ranges from negative to positive infinity, that is -∞ < Δ < ∞ .
In reality as you will see we will apply a set of constraints on the value of Δ reducing it's overall set of valid values.
This defines everything we need to describe the definition of a Splitview. The first approach to show will be the most native building the complexity after each iteration.
## Iteration #1 - The naive approach (aka. the accordian)
The most basic form of resizing may be to say as I add delta increase view sizes and as I remove delta I decrease view sizes. This could be further described with the following statements:
- As the sash moves left shrink each view to the left and as the sash moves right expand each view to left, from right-most to left-most in both cases.
- If there is enough delta to shrink a view to it's mimimum size then progress onto the next view, and if we have enough delta to expand a view to it's maximum size then again progress onto the next view.
- Shrink no more once everything to the left is at minimums and expand no more once everything to the left is at maximums
- We don't manipulate any views to the right of the active sash S<sub>i</sub>
You should be able to show each of the four points above hold true for the below interactive example.
You'll see that changes to the right will always remain at zero because we are not manipulating views to the right of the active sash.
<Splitview mode={1} debug={true} />
Putting this implemenation in psuedocode using the definitions from above where we drag sash S<sub>i</sub> by an amount Δ
<div
style={{
display: 'inline-block',
marginLeft: '20px',
marginBottom: '20px',
borderLeft: '2px solid black',
}}
>
<div className="markdown-line">
Δ<sub>remaining</sub> = Δ
</div>
<div className="markdown-line">
<span style={{ fontWeight: 'bold' }}>for</span>
<span>
(<span style={{ fontStyle: 'italic' }}>j = i; j >= 0; i--</span>)
</span>
<span style={{ fontWeight: 'bold' }}> do</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>next</sup>
<sub>j</sub> = <span style={{ fontWeight: 'bold' }}>Min</span>(V<sup>
max
</sup>
<sub>j</sub>, <span style={{ fontWeight: 'bold' }}>Max</span>(V
<sup>min</sup>
<sub>j</sub>, V<sub>j</sub> + Δ<sub>remaining</sub>))
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>Δ</sup>
<sub>j</sub> = V<sup>next</sup>
<sub>j</sub> - V<sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
Δ<sub>remaining</sub> = Δ<sub>remaining</sub> - V<sup>Δ</sup>
<sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sub>j</sub> = V<sup>next</sup>
<sub>j</sub>
</span>
</div>
</div>
and as instructions
<div
style={{
borderLeft: '2px solid black',
margin: '0px 20px 20px 20px',
display: 'flex',
flexDirection: 'row',
}}
>
<div
style={{ borderRight: '2px solid black', flexShrink: 0, width: '20px' }}
>
<div style={{ display: 'flex', justifyContent: 'center' }}>1</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>2</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>3</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>4</div>
</div>
<div style={{ flexGrow: 1, marginLeft: '20px' }}>
<div>
For each view j to the left of the sash we have dragged, from
right-most to left-most
</div>
<div>
Add the delta to the view j (clamped at either the maximum or
minimum value)
</div>
<div>
Subtract the different between the new and old size (the used delta)
from the remaining delta
</div>
<div>repeat</div>
</div>
</div>
There are some obvious flaws with this approach. Nothing to the right of the active sash is resizes which also related to the fact that the width of the control does not remain constant.
## Iteration #2 - When Δ is added an equal Δ must be removed
For the width of the control to remain constant it would make sense that if I add Δ to the left then I should add -Δ (or remove Δ) on the right, and vice-versa, which is the approach of the below interactive example.
As you may see it is right, but there are still some edge cases that fail.
<Splitview mode={2} debug={true} />
To write this approach in pseudocode lets define another variable to track the delta we've added on the left, Δ<sub>used</sub>.
After we've applied changes to the left side we'll substract this Δ<sub>used</sub> from the right side with the aim to keep the width of the control constant.
<div style={{display: "inline-block", marginLeft: "20px", marginBottom: "20px", borderLeft: "2px solid black"}}>
<div className="markdown-line">
Δ<sub>remaining</sub> = Δ
</div>
<div className="markdown-line markdown-highlight">
Δ<sub>used</sub> = 0
</div>
<div className="markdown-line">
<span style={{fontWeight: "bold"}}>for</span><span>(<span style={{fontStyle: "italic"}}>j = i; j >= 0; i--</span>)</span>
<span style={{fontWeight: "bold"}}> do</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>next</sup><sub>j</sub> = <span style={{fontWeight: "bold"}}>Min</span>(V<sup>max</sup><sub>j</sub>, <span style={{fontWeight: "bold"}}>Max</span>(V<sup>min</sup><sub>j</sub>, V<sub>j</sub> + Δ<sub>remaining</sub>))
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>Δ</sup><sub>j</sub> = V<sup>next</sup><sub>j</sub> - V<sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
Δ<sub>remaining</sub> = Δ<sub>remaining</sub> - V<sup>Δ</sup><sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1 markdown-highlight">
<span>
Δ<sub>used</sub> = Δ<sub>used</sub> + V<sup>Δ</sup><sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sub>j</sub> = V<sup>next</sup><sub>j</sub>
</span>
</div>
<div style={{height: "0px", width: "100%", marginBottom: "20px"}}/>
<div className="markdown-line markdown-highlight">
<span style={{fontWeight: "bold"}}>for</span><span>(<span style={{fontStyle: "italic"}}>{"j = i+1; j < n; i++"}</span>)</span>
<span style={{fontWeight: "bold"}}> do</span>
</div>
<div className="markdown-line markdown-highlight indent-1">
<span>
V<sup>next</sup><sub>j</sub> = <span style={{fontWeight: "bold"}}>Min</span>(V<sup>max</sup><sub>j</sub>, <span style={{fontWeight: "bold"}}>Max</span>(V<sup>min</sup><sub>j</sub>,V<sub>j</sub> - Δ<sub>used</sub>))
</span>
</div>
<div className="markdown-line markdown-highlight indent-1">
<span>
V<sup>Δ</sup><sub>j</sub> = V<sup>next</sup><sub>j</sub> - V<sub>j</sub>
</span>
</div>
<div className="markdown-line markdown-highlight indent-1">
<span>
Δ<sub>used</sub> = Δ<sub>used</sub> + V<sup>Δ</sup><sub>j</sub>
</span>
</div>
<div style={{marginBottom: "20px"}} className="markdown-line markdown-highlight indent-1">
<span>
V<sub>j</sub> = V<sup>next</sup><sub>j</sub>
</span>
</div>
</div>
Go back and try to minimise or maximise every view in the container. The width is no longer preserved, you can see at some point the change to the left
is not longer eqaul to the change on the right, which causes the container to once again flex.
## Iteration #3 - Constraining the values of Δ
The failure of iteration #2 can be explained as the addition or removal of too much delta which means we need to look at what constraints we can apply to the problem.
For a sash S<sub>i</sub> think about the minimum and maximum amount of delta that can be both added and removed.
Minimized view constraints
- S<sub>i</sub> can go no further left that the sum of the minimum sizes of the views to the left because you would then have a view smaller than it's minimum size
- S<sub>i</sub> can go no further further right than the sum of the minimum sizes of the views to the right because you would then have a view smaller than it's minimum size
Maximised view constraints
- S<sub>i</sub> can go no further left that the sum of the maximum sizes of the views to the right because otherwise you would have a viewer larger than it's maximum size
- S<sub>i</sub> can go no further right that the sum of the maximum sizes of the views to the left because otherwise you would have a viewer larger than it's maximum size
Since Δ is relative to S<sub>i</sub> we need these to define these constraints relative to S<sub>i</sub>.
When the views to the left of S<sub>i</sub> are all at minimum size define the distance between here and Δ to be Δ<sup>min</sup><sub>left</sub>.
This distance would be the sum of the differences between V<sup>min</sup><sub>j</sub> and V<sub>j</sub> for each view:
<div style={{ margin: '20px' }}>
<span>
Δ<sup>min</sup>
<sub>left</sub> = Σ V<sup>min</sup>
<sub>j</sub> - V<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i,...0</span>
</div>
Similarly we can work out the distance between S<sub>i</sub> and the point at each every view to the left is at its
maximum size as the sum of differences between V<sup>max</sup><sub>j</sub> an V<sub>j</sub>
<div style={{ margin: '20px' }}>
<span>
Δ<sup>max</sup>
<sub>left</sub> = Σ V<sup>max</sup>
<sub>j</sub> - V<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i,...0</span>
</div>
The same logic can be applied to work out those values for Δ<sup>min</sup><sub>right</sub> and Δ<sup>max</sup><sub>right</sub>
<div style={{ margin: '20px' }}>
<div style={{ marginBottom: '10px' }}>
<span>
Δ<sup>min</sup>
<sub>right</sub> = Σ V<sub>j</sub> - V<sup>min</sup>
<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i+1...n</span>
</div>
<div>
<span>
Δ<sup>max</sup>
<sub>right</sub> = Σ V<sub>j</sub> - V<sup>min</sup>
<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i+1...n</span>
</div>
</div>
This leaves us with two minimum constraints which are V<sup>min</sup><sub>left</sub> and V<sup>max</sup><sub>right</sub> and two maximum
constraints V<sup>max</sup><sub>left</sub> and V<sup>min</sup><sub>right</sub>.
We can reduce these down to a single minimum and maximum contraint by taking the maximum of the two minimums and the minimum of the two maximums leaving us with the following constraints:
<div style={{ margin: '20px' }}>
<div style={{ marginBottom: '10px' }}>
Δ<sub>min</sub> = Max ( V<sup>min</sup>
<sub>left</sub> , V<sup>max</sup>
<sub>right</sub> )
</div>
<div>
Δ<sub>max</sub> = Min ( V<sup>max</sup>
<sub>left</sub> , V<sup>min</sup>
<sub>right</sub> )
</div>
</div>
Given these constraints we can clamp the value of Δ to be within this minimum and maxium boundary.
This clamped delta can be used in place of delta in the pseudocode from Iteration #2.
<div style={{ margin: '20px' }}>
Δ<sub>clamped</sub> = MIN ( V<sub>max</sub> , MAX ( V<sub>min</sub> , Δ ) )
</div>
You can see how this works in this interactive example which also visually indicates this boundary conditions.
<Splitview mode={3} debug={true} />
For a more visual explaination it's worth studying the below diagram:
<img src={Contraints} />

View File

@ -1,107 +0,0 @@
import {
ISplitviewPanelProps,
Orientation,
PanelCollection,
SplitviewApi,
SplitviewReact,
SplitviewReadyEvent,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<ISplitviewPanelProps<any>> = {
default: (props) => {
return (
<div style={{ padding: '10px', height: '100%' }}>hello world</div>
);
},
ticker: (props: ISplitviewPanelProps<{ ticker: number }>) => {
return (
<div style={{ padding: '10px', height: '100%' }}>
{`The current ticker value is ${props.params.ticker}`}
</div>
);
},
};
export const Params = (props: {
theme: string;
hideBorders: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<SplitviewApi>();
React.useEffect(() => {
if (!api.current) {
return () => {
// noop
};
}
const gridApi = api.current;
const interval = setInterval(() => {
const panel1 = gridApi.getPanel('panel1');
panel1.update({ params: { ticker: Date.now() } });
}, 1000);
return () => {
clearInterval(interval);
};
}, [api]);
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'ticker',
params: {
ticker: 0,
},
});
event.api.addPanel({
id: 'panel2',
component: 'default',
});
event.api.addPanel({
id: 'panel3',
component: 'default',
});
};
return (
<SplitviewReact
orientation={Orientation.HORIZONTAL}
className={props.theme}
onReady={onReady}
components={components}
hideBorders={props.hideBorders}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Splitview/Params',
component: Params,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: { theme: 'dockview-theme-light' },
argTypes: {
theme: {
control: {
type: 'select',
options: ['dockview-theme-dark', 'dockview-theme-light'],
},
},
},
} as Meta;

View File

@ -1,138 +0,0 @@
import {
ISplitviewPanelProps,
Orientation,
PanelCollection,
SerializedSplitview,
SplitviewApi,
SplitviewReact,
SplitviewReadyEvent,
} from 'dockview';
import * as React from 'react';
import { Story, Meta } from '@storybook/react';
const components: PanelCollection<ISplitviewPanelProps<any>> = {
default: (props: ISplitviewPanelProps<{ color: string }>) => {
const resize = () => {
props.api.setSize({ size: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
height: '100%',
boxSizing: 'border-box',
}}
>
<div>hello world</div>
<button onClick={resize}>Resize</button>
</div>
);
},
};
export const Persistance = (props: {
orientation: Orientation;
hideBorders: boolean;
proportionalLayout: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<SplitviewApi>();
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
event.api.onDidLayoutChange(() => {
const state = event.api.toJSON();
localStorage.setItem(
'splitview.test.layout',
JSON.stringify(state)
);
console.log(JSON.stringify(state, null, 4));
});
const state = localStorage.getItem('splitview.test.layout');
if (state) {
event.api.fromJSON(JSON.parse(state) as SerializedSplitview);
return;
}
event.api.fromJSON({
size: 100,
views: [
{
size: 20,
data: {
id: 'panel1',
component: 'default',
params: {
color: 'red',
},
},
},
{
size: 40,
data: {
id: 'panel2',
component: 'default',
params: {
color: 'green',
},
},
},
{
size: 60,
data: {
id: 'panel3',
component: 'default',
params: {
color: 'purple',
},
},
},
],
orientation: props.orientation,
activeView: 'panel1',
});
};
return (
<SplitviewReact
onReady={onReady}
orientation={props.orientation}
components={components}
disableAutoResizing={props.disableAutoResizing}
hideBorders={props.hideBorders}
proportionalLayout={props.proportionalLayout}
/>
);
};
export default {
title: 'Library/Splitview/Persistance',
component: Persistance,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: {
orientation: Orientation.VERTICAL,
proportionalLayout: true,
disableAutoResizing: false,
},
argTypes: {
orientation: {
control: {
type: 'inline-radio',
options: [Orientation.HORIZONTAL, Orientation.VERTICAL],
},
},
},
} as Meta;

View File

@ -1,110 +0,0 @@
import {
ISplitviewPanelProps,
Orientation,
orthogonal,
PanelCollection,
SplitviewApi,
SplitviewReact,
SplitviewReadyEvent,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<ISplitviewPanelProps<any>> = {
default: (props: ISplitviewPanelProps<{ color: string }>) => {
const resize = () => {
props.api.setSize({ size: 300 });
};
const transpose = () => {
props.containerApi.updateOptions({
orientation: orthogonal(props.containerApi.orientation),
});
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
height: '100%',
boxSizing: 'border-box',
}}
>
<div>hello world</div>
<button onClick={resize}>Resize</button>
<button onClick={transpose}>Transpose</button>
</div>
);
},
};
export const Simple = (props: {
orientation: Orientation;
hideBorders: boolean;
proportionalLayout: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<SplitviewApi>();
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'default',
params: { color: 'red' },
minimumSize: 50,
});
event.api.addPanel({
id: 'panel2',
component: 'default',
params: { color: 'green' },
minimumSize: 50,
});
event.api.addPanel({
id: 'panel3',
component: 'default',
params: { color: 'purple' },
minimumSize: 50,
});
};
return (
<SplitviewReact
onReady={onReady}
orientation={props.orientation}
components={components}
hideBorders={props.hideBorders}
proportionalLayout={props.proportionalLayout}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Splitview/Simple',
component: Simple,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: {
orientation: Orientation.VERTICAL,
proportionalLayout: true,
disableAutoResizing: false,
},
argTypes: {
orientation: {
control: {
type: 'inline-radio',
options: [Orientation.HORIZONTAL, Orientation.VERTICAL],
},
},
},
} as Meta;

View File

@ -1,104 +0,0 @@
import {
ISplitviewPanelProps,
Orientation,
PanelCollection,
SplitviewApi,
SplitviewReact,
SplitviewReadyEvent,
} from 'dockview';
import * as React from 'react';
import { Meta } from '@storybook/react';
const components: PanelCollection<ISplitviewPanelProps<any>> = {
default: (props: ISplitviewPanelProps<{ color: string }>) => {
const resize = () => {
props.api.setSize({ size: 300 });
};
return (
<div
style={{
padding: '10px',
backgroundColor: props.params.color,
height: '100%',
boxSizing: 'border-box',
}}
>
<div>hello world</div>
<button onClick={resize}>Resize</button>
</div>
);
},
};
export const Snap = (props: {
orientation: Orientation;
hideBorders: boolean;
proportionalLayout: boolean;
disableAutoResizing: boolean;
}) => {
const api = React.useRef<SplitviewApi>();
const onReady = (event: SplitviewReadyEvent) => {
api.current = event.api;
event.api.addPanel({
id: 'panel1',
component: 'default',
params: { color: 'red' },
minimumSize: 50,
snap: true,
});
event.api.addPanel({
id: 'panel2',
component: 'default',
params: { color: 'green' },
minimumSize: 50,
});
event.api.addPanel({
id: 'panel3',
component: 'default',
params: { color: 'purple' },
minimumSize: 50,
snap: true,
});
};
return (
<SplitviewReact
onReady={onReady}
orientation={props.orientation}
components={components}
hideBorders={props.hideBorders}
proportionalLayout={props.proportionalLayout}
disableAutoResizing={props.disableAutoResizing}
/>
);
};
export default {
title: 'Library/Splitview/Snap',
component: Snap,
decorators: [
(Component) => {
document.body.style.padding = '0px';
return (
<div style={{ height: '100vh', fontFamily: 'Arial' }}>
<Component />
</div>
);
},
],
args: {
orientation: Orientation.VERTICAL,
proportionalLayout: true,
disableAutoResizing: false,
},
argTypes: {
orientation: {
control: {
type: 'inline-radio',
options: [Orientation.HORIZONTAL, Orientation.VERTICAL],
},
},
},
} as Meta;

View File

@ -1,341 +0,0 @@
import * as React from 'react';
import './splitview.css';
const min = 100;
const max = 300;
interface IDebugResize {
leftmin: number;
leftmax: number;
rightmin: number;
rightmax: number;
min: number;
max: number;
}
const resize = (
index: number,
delta: number,
sizes: number[],
mode: number
) => {
const nextSizes = [...sizes];
const left = nextSizes.filter((_, i) => i <= index);
const right = nextSizes.filter((_, i) => i > index);
let result: IDebugResize = {
leftmin: undefined,
leftmax: undefined,
rightmin: undefined,
rightmax: undefined,
max: undefined,
min: undefined,
};
// step 3
if (mode > 2) {
const leftMinimumsDelta = left
.map((x) => min - x)
.reduce((x, y) => x + y, 0);
const leftMaximumsDelta = left
.map((x) => max - x)
.reduce((x, y) => x + y, 0);
const rightMinimumsDelta = right
.map((x) => x - min)
.reduce((x, y) => x + y, 0);
const rightMaximumsDelta = right
.map((x) => x - max)
.reduce((x, y) => x + y, 0);
const _min = Math.max(leftMinimumsDelta, rightMaximumsDelta);
const _max = Math.min(leftMaximumsDelta, rightMinimumsDelta);
const clamp = Math.max(_min, Math.min(_max, delta));
result = {
leftmin: leftMinimumsDelta,
leftmax: leftMaximumsDelta,
rightmin: rightMinimumsDelta,
rightmax: rightMaximumsDelta,
max: _max,
min: _min,
};
delta = clamp;
}
let usedDelta = 0;
let remainingDelta = delta;
// Step 1
for (let i = left.length - 1; i > -1; i--) {
const x = Math.max(min, Math.min(max, left[i] + remainingDelta));
const viewDelta = x - left[i];
usedDelta += viewDelta;
remainingDelta -= viewDelta;
left[i] = x;
}
// Step 2
if (mode > 1) {
for (let i = 0; i < right.length; i++) {
const x = Math.max(min, Math.min(max, right[i] - usedDelta));
const viewDelta = x - right[i];
usedDelta += viewDelta;
right[i] = x;
}
}
return { ...result, sizes: [...left, ...right] };
};
interface ILayoutState {
sashes: number[];
views: number[];
deltas: number[];
left: number;
right: number;
debug: IDebugResize;
drag: number;
}
export const Splitview = (props: { mode: number; debug: boolean }) => {
// keep the sashes and views in one state to prevent weird out-of-sync-ness
const [layout, setLayout] = React.useState<ILayoutState>({
sashes: [200, 400, 600],
views: [200, 200, 200, 200],
deltas: [0, 0, 0, 0],
left: 0,
right: 0,
debug: undefined,
drag: -1,
});
const ref = React.useRef<HTMLDivElement>();
const onMouseDown = (index: number) => (ev: React.MouseEvent) => {
const start = ev.clientX;
const sizes = [...layout.views];
const mousemove = (ev: MouseEvent) => {
const current = ev.clientX;
const delta = current - start;
const {
sizes: nextLayout,
rightmin,
rightmax,
leftmin,
leftmax,
max,
min,
} = resize(index, delta, sizes, props.mode);
const sashes = nextLayout.reduce(
(x, y) => [...x, y + (x.length === 0 ? 0 : x[x.length - 1])],
[]
);
sashes.splice(sashes.length - 1, 1);
const deltas = sizes.map((x, i) => nextLayout[i] - x);
const offset = start - ref.current?.getBoundingClientRect().left;
setLayout({
views: nextLayout,
sashes,
deltas,
left: deltas
.filter((_, i) => i <= index)
.reduce((x, y) => x + y, 0),
right: deltas
.filter((_, i) => i > index)
.reduce((x, y) => x + y, 0),
debug: {
leftmax: leftmax + offset,
leftmin: leftmin + offset,
rightmax: rightmax + offset,
rightmin: rightmin + offset,
min: min + offset,
max: max + offset,
},
drag: index,
});
};
const end = (ev: MouseEvent) => {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', end);
setLayout((_) => ({
..._,
deltas: _.deltas.map((_) => 0),
left: 0,
right: 0,
drag: -1,
}));
};
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', end);
};
const extras = React.useMemo(() => {
if (!props.debug || !layout.debug || props.mode < 3) {
return null;
}
return (
<>
<div
style={{
left: `${layout.debug.leftmax - 40}px`,
top: '-30px',
}}
className="debug-sash-text"
>
left-max
</div>
<div
style={{
left: `${layout.debug.leftmin - 40}px`,
top: '-30px',
}}
className="debug-sash-text"
>
left-min
</div>
<div
style={{
left: `${layout.debug.rightmax - 40}px`,
bottom: '-30px',
}}
className="debug-sash-text"
>
right-max
</div>
<div
style={{
left: `${layout.debug.rightmin - 40}px`,
bottom: '-30px',
}}
className="debug-sash-text"
>
right-min
</div>
<div
className="debug-sash-max"
style={{
left: `${layout.debug.leftmax - 1}px`,
border: '2px solid purple',
}}
/>
<div
className="debug-sash-max"
style={{
left: `${layout.debug.leftmin - 1}px`,
border: '2px solid green',
}}
/>
<div
className="debug-sash-min"
style={{
left: `${layout.debug.rightmax - 1}px`,
border: '2px solid cyan',
}}
/>
<div
className="debug-sash-min"
style={{
left: `${layout.debug.rightmin - 1}px`,
border: '2px solid pink',
}}
/>
</>
);
}, [layout.debug]);
return (
<div
style={{
marginBottom: '40px',
marginTop: '30px',
backgroundColor: 'gray',
}}
>
{props.debug && (
<div style={{ marginBottom: extras ? '25px' : '0px' }}>
<span>{`Change to left ${layout?.left}`}</span>
<span
style={{
marginLeft: '10px',
backgroundColor:
-layout?.right !== layout?.left ? 'red' : '',
}}
>{`Change to right ${layout?.right}`}</span>
<span
style={{ marginLeft: '10px' }}
>{`Total size ${layout?.views.reduce(
(x, y) => x + y,
0
)}`}</span>
</div>
)}
<div
ref={ref}
style={{
height: '100px',
width: '100%',
position: 'relative',
backgroundColor: 'dimgray',
}}
>
<div className="sash-container">
{layout.sashes.map((x, i) => {
const className =
layout.drag === i ? 'sash drag-sash' : 'sash';
return (
<div
key={i}
onMouseDown={onMouseDown(i)}
style={{
left: `${x - 2}px`,
}}
className={className}
></div>
);
})}
{extras}
</div>
<div className="view-container">
{layout.views.map((x, i) => {
const isMax = x >= max;
const isMin = x <= min;
return (
<div
key={i}
style={{
left: `${
i === 0 ? 0 : layout.sashes[i - 1]
}px`,
width: `${x}px`,
}}
className="view"
>
{props.debug && (
<>
<div>
{`${layout.views[i]} (${
layout.deltas[i] > -1 ? '+' : ''
}${layout.deltas[i]})`}
</div>
<div
style={{ fontSize: '12px' }}
>{`isMin = ${isMin}`}</div>
<div
style={{ fontSize: '12px' }}
>{`isMax = ${isMax}`}</div>
</>
)}
</div>
);
})}
</div>
</div>
</div>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,16 +0,0 @@
{
"extends": "../../module-build/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"tsBuildInfoFile": ".build/tsconfig.tsbuildinfo.cjs",
"jsx": "react",
"rootDir": "src",
"allowUnreachableCode": true,
"noImplicitAny": false,
"sourceMap": true,
"declaration": false,
"strictNullChecks":false
},
"include": ["src"],
"exclude": ["**/node_modules", "src/__tests__"]
}

View File

@ -1,52 +0,0 @@
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'src/index.tsx'),
devtool: 'source-map',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist',
},
mode: 'development',
resolve: {
extensions: ['.ts', '.js', '.tsx', 'jsx'],
// alias: {
// react: path.resolve(__dirname, 'node_modules/react'),
// 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
// },
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
},
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader',
],
},
{
test: /\.css$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
],
},
],
},
devServer: {
port: 9000,
compress: true,
static: path.resolve(__dirname, 'public'),
},
};