From cdec37190cd72789743bf71c8d3d1f726a09ff92 Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Sat, 23 Mar 2024 22:00:08 +0100
Subject: [PATCH] feat: new split-screen #171

---
 playground/dialog/index.html                  |   42 +
 playground/dialog/main.js                     |   13 +
 playground/dialog/main.pcss                   |    8 +
 playground/issues/142.html                    |    9 +-
 playground/issues/144.html                    |    8 +-
 playground/issues/152.html                    |    7 +-
 playground/issues/154.html                    |    7 +-
 playground/issues/158.html                    |    9 +-
 playground/split-screen/index.html            |   75 ++
 playground/split-screen/main.js               |   13 +
 playground/split-screen/main.pcss             |    8 +
 playground/tab/index.html                     |   10 +-
 playground/tab/main.mjs                       |    2 +-
 playground/vite.config.js                     |   18 +-
 source/components/form/tabs.mjs               | 1026 +---------------
 source/components/host/overlay.mjs            |    2 +-
 source/components/host/viewer.mjs             |   16 +-
 source/components/layout/namespace.mjs        |   13 +
 source/components/layout/split-screen.mjs     |  341 ++++++
 .../components/layout/style/split-screen.pcss |   59 +
 .../{form => layout}/style/tabs.pcss          |    3 -
 .../layout/stylesheet/split-screen.mjs        |   27 +
 .../{form => layout}/stylesheet/tabs.mjs      |    0
 source/components/layout/tabs.mjs             | 1075 +++++++++++++++++
 24 files changed, 1728 insertions(+), 1063 deletions(-)
 create mode 100644 playground/dialog/index.html
 create mode 100644 playground/dialog/main.js
 create mode 100644 playground/dialog/main.pcss
 create mode 100644 playground/split-screen/index.html
 create mode 100644 playground/split-screen/main.js
 create mode 100644 playground/split-screen/main.pcss
 create mode 100644 source/components/layout/namespace.mjs
 create mode 100644 source/components/layout/split-screen.mjs
 create mode 100644 source/components/layout/style/split-screen.pcss
 rename source/components/{form => layout}/style/tabs.pcss (99%)
 create mode 100644 source/components/layout/stylesheet/split-screen.mjs
 rename source/components/{form => layout}/stylesheet/tabs.mjs (100%)
 create mode 100644 source/components/layout/tabs.mjs

diff --git a/playground/dialog/index.html b/playground/dialog/index.html
new file mode 100644
index 000000000..9c29fa671
--- /dev/null
+++ b/playground/dialog/index.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    <title>Form</title>
+    <script src="./main.js" type="module"></script>
+</head>
+<body>
+
+
+<main>
+
+    <div id="dialog">
+
+
+        <monster-split-screen data-monster-option-splittype="horizontal">
+
+            <div slot="start">
+                <h1>Start Panel</h1>
+                <p>Some content</p>
+                <p>Some content</p>
+            </div>
+
+            <div slot="end">
+                <h1>End Panel</h1>
+                <p>Some content</p>
+                <p>Some content</p>
+            </div>
+
+
+        </monster-split-screen>
+
+
+    </div>
+
+
+</main>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/playground/dialog/main.js b/playground/dialog/main.js
new file mode 100644
index 000000000..f18ca31b9
--- /dev/null
+++ b/playground/dialog/main.js
@@ -0,0 +1,13 @@
+import "../../source/components/style/property.pcss";
+import "../../source/components/style/normalize.pcss";
+import "../../source/components/style/color.pcss";
+import "../../source/components/style/theme.pcss";
+import "../../source/components/style/typography.pcss";
+import "../../source/components/style/form.pcss";
+import "../../source/components/style/link.pcss";
+import "../../source/components/style/button.pcss";
+import "../../source/components/style/ripple.pcss";
+import "../../source/components/layout/split-screen.mjs";
+import "./main.pcss";
+
+
diff --git a/playground/dialog/main.pcss b/playground/dialog/main.pcss
new file mode 100644
index 000000000..d62880653
--- /dev/null
+++ b/playground/dialog/main.pcss
@@ -0,0 +1,8 @@
+
+.dialog {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 1rem;
+    border: 1px solid #ccc; 
+    border-radius: 5px;
+}
\ No newline at end of file
diff --git a/playground/issues/142.html b/playground/issues/142.html
index f62f51629..6d2b7c54a 100644
--- a/playground/issues/142.html
+++ b/playground/issues/142.html
@@ -12,10 +12,11 @@
 
 <main>
 
-    <h1>Issue 142</h1>
-    <p>
-        <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/142" target="_blank">issues/142</a>
-    </p>
+    <h1>Issues 142</h1>
+    <ul>
+        <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/142" target="_blank">issues/142</a>
+        <li><a href="../../">Back to overview</a></li>
+    </ul>
 
     <div style="width: 450px; margin: 0 auto;">
 <!--        <monster-select value="value2,value1">-->
diff --git a/playground/issues/144.html b/playground/issues/144.html
index a3862bf8a..67316df76 100644
--- a/playground/issues/144.html
+++ b/playground/issues/144.html
@@ -28,9 +28,11 @@
 
 <main>
 
-    <h1>Issues</h1>
-
-    <h2>#144 Buttons in der button-bar nicht gleich hoch</h2>
+    <h1>Issues 144</h1>
+    <ul>
+        <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/144" target="_blank">issues/144</a>
+        <li><a href="../../">Back to overview</a></li>
+    </ul>
 
     <div style="width: 550px;margin: 0 auto;">
         <monster-button-bar style="width: 550px;" data-monster-option-popper-placement="right">
diff --git a/playground/issues/152.html b/playground/issues/152.html
index 03ac74455..4218e8bfd 100644
--- a/playground/issues/152.html
+++ b/playground/issues/152.html
@@ -13,9 +13,10 @@
 <main>
 
     <h1>Issues 152</h1>
-    <p>
-        <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a>
-    </p>
+    <ul>
+        <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a>
+        <li><a href="../../">Back to overview</a></li>
+    </ul>
 
 
     <div style="width: 250px;margin: 0 auto;">
diff --git a/playground/issues/154.html b/playground/issues/154.html
index 7c904826f..10e0d8024 100644
--- a/playground/issues/154.html
+++ b/playground/issues/154.html
@@ -13,9 +13,10 @@
 <main>
 
     <h1>Issues 152</h1>
-    <p>
-        <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a>
-    </p>
+    <ul>
+       <li> <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/152" target="_blank">issues/152</a></li>
+        <li><a href="../../">Back to overview</a></li>
+    </ul>
 
 
     <div style="width: 250px;margin: 0 auto;">
diff --git a/playground/issues/158.html b/playground/issues/158.html
index 8aef191a2..312733939 100644
--- a/playground/issues/158.html
+++ b/playground/issues/158.html
@@ -13,10 +13,11 @@
 <main>
 
     <h1>Issues 158</h1>
-    <p>
-        <a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/158"
-           target="_blank">issues/158</a>
-    </p>
+    <ul>
+        <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/158"
+           target="_blank">issues/158</a></li>
+        <li><a href="../../">Back to overview</a></li>
+    </ul>
 
 
     <monster-datasource-rest id="data1"
diff --git a/playground/split-screen/index.html b/playground/split-screen/index.html
new file mode 100644
index 000000000..7d0e6a1ae
--- /dev/null
+++ b/playground/split-screen/index.html
@@ -0,0 +1,75 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    <title>Form</title>
+    <script src="./main.js" type="module"></script>
+</head>
+<body>
+
+
+<main>
+
+    <h1>Split Screen</h1>
+    <ul>
+        <li><a href="../">Back to overview</a></li>
+    </ul>   
+
+    <div id="dialog" style="">
+
+
+        <monster-split-screen data-monster-option-splittype="horizontal" style="">
+
+            <div slot="start">
+                <h1>Start Panel</h1>
+                <p>Some content</p>
+                <p>Some content</p>
+            </div>
+
+            <div slot="end">
+                <monster-split-screen data-monster-option-splittype="vertical" style="">
+
+                    <div slot="start">
+                        <h1>Start Panel</h1>
+                        <p>Some content</p>
+                        <p>Some content</p>
+                    </div>
+
+                    <div slot="end">
+                        <monster-split-screen data-monster-option-splittype="horizontal" style="">
+
+                            <div slot="start">
+                                <h1>Start Panel</h1>
+                                <p>Some content</p>
+                                <p>Some content</p>
+                            </div>
+
+                            <div slot="end">
+                                <h1>End Panel</h1>
+                                <p>Some content</p>
+                                <p>Some content</p>
+                            </div>
+
+
+                        </monster-split-screen>
+
+                    </div>
+
+
+                </monster-split-screen>
+
+            </div>
+
+
+        </monster-split-screen>
+
+
+    </div>
+
+
+</main>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/playground/split-screen/main.js b/playground/split-screen/main.js
new file mode 100644
index 000000000..f18ca31b9
--- /dev/null
+++ b/playground/split-screen/main.js
@@ -0,0 +1,13 @@
+import "../../source/components/style/property.pcss";
+import "../../source/components/style/normalize.pcss";
+import "../../source/components/style/color.pcss";
+import "../../source/components/style/theme.pcss";
+import "../../source/components/style/typography.pcss";
+import "../../source/components/style/form.pcss";
+import "../../source/components/style/link.pcss";
+import "../../source/components/style/button.pcss";
+import "../../source/components/style/ripple.pcss";
+import "../../source/components/layout/split-screen.mjs";
+import "./main.pcss";
+
+
diff --git a/playground/split-screen/main.pcss b/playground/split-screen/main.pcss
new file mode 100644
index 000000000..d62880653
--- /dev/null
+++ b/playground/split-screen/main.pcss
@@ -0,0 +1,8 @@
+
+.dialog {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 1rem;
+    border: 1px solid #ccc; 
+    border-radius: 5px;
+}
\ No newline at end of file
diff --git a/playground/tab/index.html b/playground/tab/index.html
index 37c504143..619875f94 100644
--- a/playground/tab/index.html
+++ b/playground/tab/index.html
@@ -13,12 +13,14 @@
 <main style="width:98vw">
 
     <h1>Tabs</h1>
+    <ul>
+        <li><a href="../">Back to overview</a></li>
+    </ul>
 
-    
     <monster-tabs
-    
-    data-monster-option-classes-button="monster-theme-secondary-1"
-    data-monster-option-classes-popper="monster-theme-secondary-1"
+
+            data-monster-option-classes-button="monster-theme-secondary-1"
+            data-monster-option-classes-popper="monster-theme-secondary-1"
     >
         <div>Tab 1</div>
         <div>Tab 2</div>
diff --git a/playground/tab/main.mjs b/playground/tab/main.mjs
index c6752bab0..14045ed94 100644
--- a/playground/tab/main.mjs
+++ b/playground/tab/main.mjs
@@ -11,6 +11,6 @@ import "./main.pcss";
 
 
 
-import "../../source/components/form/tabs.mjs";
+import "../../source/components/layout/tabs.mjs";
 
 
diff --git a/playground/vite.config.js b/playground/vite.config.js
index 8608de8e4..16c0e8b57 100644
--- a/playground/vite.config.js
+++ b/playground/vite.config.js
@@ -35,6 +35,12 @@ const playgroundDir = join(rootDir, 'playground')
 
 console.log(rootDir)
 
+function buildStylesheets() {
+    debugger
+    execSync('build-stylesheets', {cwd: rootDir, stdio: 'inherit'})
+}
+
+
 export default defineConfig({
     clearScreen: false,
 
@@ -61,8 +67,16 @@ export default defineConfig({
         directoryIndex({    }),
 
     viteMockServe({
-        mockPath:playgroundDir+ "/mock", // Der Pfad zu Ihren Mock-Dateien
-    })
+        mockPath:playgroundDir+ "/mock", 
+    }),
+
+        {
+            name: 'postbuild-commands', 
+            closeBundle: async () => {
+                await buildStylesheets() 
+            },
+            
+        },
 
     ],
     css: {
diff --git a/source/components/form/tabs.mjs b/source/components/form/tabs.mjs
index 1c7faaaf8..6ed295922 100644
--- a/source/components/form/tabs.mjs
+++ b/source/components/form/tabs.mjs
@@ -4,134 +4,14 @@
  * This file is licensed under the AGPLv3 License.
  * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
  */
-import { instanceSymbol } from "../../constants.mjs";
-import { createPopper } from "@popperjs/core";
-import { extend } from "../../data/extend.mjs";
-import { Pathfinder } from "../../data/pathfinder.mjs";
+import { Tabs as NewTabs } from "../layout/tabs.mjs";
 import {
-	addAttributeToken,
-	addToObjectLink,
-	hasObjectLink,
-} from "../../dom/attributes.mjs";
-import {
-	ATTRIBUTE_ERRORMESSAGE,
-	ATTRIBUTE_PREFIX,
-	ATTRIBUTE_ROLE,
-} from "../../dom/constants.mjs";
-import {
-	assembleMethodSymbol,
-	CustomElement,
-	getSlottedElements,
 	registerCustomElement,
 } from "../../dom/customelement.mjs";
-import {
-	findTargetElementFromEvent,
-	fireCustomEvent,
-} from "../../dom/events.mjs";
-import { getDocument } from "../../dom/util.mjs";
-import { random } from "../../math/random.mjs";
-import { getGlobal } from "../../types/global.mjs";
-import { ID } from "../../types/id.mjs";
-import { isArray, isString } from "../../types/is.mjs";
-import { TokenList } from "../../types/tokenlist.mjs";
-import { clone } from "../../util/clone.mjs";
-import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
-import { Processing } from "../../util/processing.mjs";
-import {
-	ATTRIBUTE_BUTTON_LABEL,
-	ATTRIBUTE_FORM_RELOAD,
-	ATTRIBUTE_FORM_URL,
-	STYLE_DISPLAY_MODE_BLOCK,
-} from "./constants.mjs";
 
-import { TabsStyleSheet } from "./stylesheet/tabs.mjs";
-import { loadAndAssignContent } from "./util/fetch.mjs";
-import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
-import {
-	popperInstanceSymbol,
-	setEventListenersModifiers,
-} from "./util/popper.mjs";
 
 export { Tabs };
 
-/**
- * @private
- * @type {symbol}
- */
-const popperElementSymbol = Symbol("popperElement");
-
-/**
- * @private
- * @type {symbol}
- */
-const popperNavElementSymbol = Symbol("popperNavElement");
-
-/**
- * @private
- * @type {symbol}
- */
-const controlElementSymbol = Symbol("controlElement");
-
-/**
- * @private
- * @type {symbol}
- */
-const navElementSymbol = Symbol("navElement");
-/**
- * @private
- * @type {symbol}
- */
-const switchElementSymbol = Symbol("switchElement");
-
-/**
- * @private
- * @type {symbol}
- */
-const changeTabEventHandler = Symbol("changeTabEventHandler");
-/**
- * @private
- * @type {symbol}
- */
-const removeTabEventHandler = Symbol("removeTabEventHandler");
-
-/**
- * @private
- * @type {symbol}
- */
-const popperSwitchEventHandler = Symbol("popperSwitchEventHandler");
-
-/**
- * local symbol
- * @private
- * @type {symbol}
- */
-const closeEventHandler = Symbol("closeEventHandler");
-
-/**
- * @private
- * @type {symbol}
- */
-const mutationObserverSymbol = Symbol("mutationObserver");
-
-/**
- * @private
- * @type {symbol}
- */
-const dimensionsSymbol = Symbol("dimensions");
-
-/**
- * @private
- * @type {symbol}
- */
-const timerCallbackSymbol = Symbol("timerCallback");
-
-/**
- * local symbol
- * @private
- * @type {symbol}
- */
-const resizeObserverSymbol = Symbol("resizeObserver");
-
 /**
  * This CustomControl creates a tab element with a variety of options.
  *
@@ -162,7 +42,8 @@ const resizeObserverSymbol = Symbol("resizeObserver");
  * skinparam shadowing false
  * HTMLElement <|-- CustomElement
  * CustomElement <|-- CustomControl
- * CustomControl <|-- Tabs
+ * CustomControl <|-- NewTabs
+ * NewTabs <|-- Tabs
  * @enduml
  *
  * @since 1.10.0
@@ -170,907 +51,10 @@ const resizeObserverSymbol = Symbol("resizeObserver");
  * @memberOf Monster.Components.Form
  * @summary A configurable tab control
  * @fires Monster.Components.Form.event:monster-fetched
+ * @deprecated since 3.59.0 use {@link Monster.Components.Layout.Tabs}
  */
-class Tabs extends CustomElement {
-	/**
-	 * This method is called by the `instanceof` operator.
-	 * @returns {symbol}
-	 * @since 2.1.0
-	 */
-	static get [instanceSymbol]() {
-		return Symbol.for("@schukai/monster/components/form/tabs");
-	}
-
-	/**
-	 * To set the options via the html tag the attribute `data-monster-options` must be used.
-	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
-	 *
-	 * The individual configuration values can be found in the table.
-	 *
-	 * @property {Object} templates Template definitions
-	 * @property {string} templates.main Main template
-	 * @property {Object} labels
-	 * @property {string} labels.new-tab-label="New Tab"
-	 * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
-	 * @property {String} fetch.redirect=error
-	 * @property {String} fetch.method=GET
-	 * @property {String} fetch.mode=same-origin
-	 * @property {String} fetch.credentials=same-origin
-	 * @property {Object} fetch.headers={"accept":"text/html"}}
-	 * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/)
-	 * @property {string} popper.placement=bottom PopperJS placement
-	 * @property {Object[]} modifiers={name:offset} PopperJS placement
-	 */
-	get defaults() {
-		return Object.assign({}, super.defaults, {
-			templates: {
-				main: getTemplate(),
-			},
-			labels: {
-				"new-tab-label": "New Tab",
-			},
-			buttons: {
-				standard: [],
-				popper: [],
-			},
-			fetch: {
-				redirect: "error",
-				method: "GET",
-				mode: "same-origin",
-				credentials: "same-origin",
-				headers: {
-					accept: "text/html",
-				},
-			},
-
-			classes: {
-				button: "monster-theme-primary-1",
-				popper: "monster-theme-primary-1",
-			},
-
-			popper: {
-				placement: "bottom",
-				modifiers: [
-					{
-						name: "offset",
-						options: {
-							offset: [0, 2],
-						},
-					},
-
-					{
-						name: "eventListeners",
-						enabled: false,
-					},
-				],
-			},
-		});
-	}
-
-	/**
-	 * This method is called internal and should not be called directly.
-	 */
-	[assembleMethodSymbol]() {
-		super[assembleMethodSymbol]();
-
-		initControlReferences.call(this);
-
-		this[dimensionsSymbol] = new Pathfinder({ data: {} });
-
-		initEventHandler.call(this);
-
-		// setup structure
-		initTabButtons.call(this).then(() => {
-			initPopperSwitch.call(this);
-			initPopper.call(this);
-			attachResizeObserver.call(this);
-			attachTabChangeObserver.call(this);
-		});
-	}
-
-	/**
-	 * This method is called internal and should not be called directly.
-	 *
-	 * @return {CSSStyleSheet[]}
-	 */
-	static getCSSStyleSheet() {
-		return [TabsStyleSheet, ThemeStyleSheet];
-	}
-
-	/**
-	 * This method is called internal and should not be called directly.
-	 *
-	 * @return {string}
-	 */
-	static getTag() {
-		return "monster-tabs";
-	}
-
-	/**
-	 * This method is called by the dom and should not be called directly.
-	 *
-	 * @return {void}
-	 */
-	connectedCallback() {
-		super.connectedCallback();
-
-		const document = getDocument();
-
-		for (const [, type] of Object.entries(["click", "touch"])) {
-			// close on outside ui-events
-			document.addEventListener(type, this[closeEventHandler]);
-		}
-	}
-
-	/**
-	 * This method is called by the dom and should not be called directly.
-	 *
-	 * @return {void}
-	 */
-	disconnectedCallback() {
-		super.disconnectedCallback();
-
-		const document = getDocument();
-
-		// close on outside ui-events
-		for (const [, type] of Object.entries(["click", "touch"])) {
-			document.removeEventListener(type, this[closeEventHandler]);
-		}
-	}
-}
-
-/**
- * @private
- */
-function initPopperSwitch() {
-	const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots
-	let switchButton;
-	if (nodes.size === 0) {
-		switchButton = document.createElement("button");
-		switchButton.setAttribute(ATTRIBUTE_ROLE, "switch");
-		switchButton.setAttribute("part", "switch");
-		switchButton.classList.add("hidden");
-		const classList = this.getOption("classes.button");
-		if (classList) {
-			switchButton.classList.add(classList);
-		}
-		switchButton.innerHTML =
-			'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>';
-		this[navElementSymbol].prepend(switchButton);
-	} else {
-		switchButton = nodes.next();
-	}
-
-	/**
-	 * @param {Event} event
-	 */
-	this[popperSwitchEventHandler] = (event) => {
-		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch");
-
-		if (element instanceof HTMLButtonElement) {
-			togglePopper.call(this);
-		}
-	};
-
-	for (const type of ["click", "touch"]) {
-		switchButton.addEventListener(type, this[popperSwitchEventHandler]);
-	}
-
-	this[switchElementSymbol] = switchButton;
-}
-
-/**
- * @private
- */
-function hidePopper() {
-	if (!this[popperInstanceSymbol]) {
-		return;
-	}
-
-	this[popperElementSymbol].style.display = "none";
-	// performance https://popper.js.org/docs/v2/tutorial/#performance
-	setEventListenersModifiers.call(this, false);
-}
-
-/**
- * @private
- */
-function showPopper() {
-	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
-		return;
-	}
-
-	this[popperElementSymbol].style.visibility = "hidden";
-	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;
-	// performance https://popper.js.org/docs/v2/tutorial/#performance
-	setEventListenersModifiers.call(this, true);
-
-	this[popperInstanceSymbol].update();
-
-	new Processing(() => {
-		this[popperElementSymbol].style.removeProperty("visibility");
-	})
-		.run(undefined)
-		.then(() => {})
-		.catch((e) => {
-			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
-		});
-}
-
-/**
- * @private
- */
-function togglePopper() {
-	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
-		hidePopper.call(this);
-	} else {
-		showPopper.call(this);
-	}
-}
-
-/**
- * @private
- */
-function attachResizeObserver() {
-	// against flickering
-	this[resizeObserverSymbol] = new ResizeObserver((entries) => {
-		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
-			try {
-				this[timerCallbackSymbol].touch();
-				return;
-			} catch (e) {
-				delete this[timerCallbackSymbol];
-			}
-		}
-
-		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
-			this[dimensionsSymbol].setVia("data.calculated", false);
-			checkAndRearrangeButtons.call(this);
-		});
-	});
-
-	this[resizeObserverSymbol].observe(this[navElementSymbol]);
-}
-
-/**
- * @private
- */
-function attachTabChangeObserver() {
-	// against flickering
-	new MutationObserver((mutations) => {
-		let runUpdate = false;
-
-		for (const mutation of mutations) {
-			if (mutation.type === "childList") {
-				if (
-					mutation.addedNodes.length > 0 ||
-					mutation.removedNodes.length > 0
-				) {
-					runUpdate = true;
-					break;
-				}
-			}
-		}
-
-		if (runUpdate === true) {
-			this[dimensionsSymbol].setVia("data.calculated", false);
-			initTabButtons.call(this);
-		}
-	}).observe(this, {
-		childList: true,
-	});
-}
-
-/**
- * @private
- * @return {Select}
- * @external "external:createPopper"
- * @see {@link  Plugins}
- */
-function initPopper() {
-	const self = this;
-
-	const options = extend({}, self.getOption("popper"));
-
-	self[popperInstanceSymbol] = createPopper(
-		self[switchElementSymbol],
-		self[popperElementSymbol],
-		options,
-	);
-
-	const observer1 = new MutationObserver(function (mutations) {
-		let runUpdate = false;
-
-		for (const mutation of mutations) {
-			if (mutation.type === "childList") {
-				if (
-					mutation.addedNodes.length > 0 ||
-					mutation.removedNodes.length > 0
-				) {
-					runUpdate = true;
-					break;
-				}
-			}
-		}
-
-		if (runUpdate === true) {
-			self[popperInstanceSymbol].update();
-		}
-	});
-
-	observer1.observe(self[popperNavElementSymbol], {
-		childList: true,
-		subtree: true,
-	});
-
-	return self;
-}
-
-/**
- * @private
- * @param {HTMLElement} element
- */
-function show(element) {
-	if (!this.shadowRoot) {
-		throw new Error("no shadow-root is defined");
-	}
-
-	const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`);
-
-	const nodes = getSlottedElements.call(this);
-	for (const node of nodes) {
-		const id = node.getAttribute("id");
-
-		if (id === reference) {
-			node.classList.add("active");
-
-			// get all data- from button and filter out data-monster-attributes and data-monster-insert
-			const data = {};
-			const mask = [
-				"data-monster-attributes",
-				"data-monster-insert-reference",
-				"data-monster-state",
-				"data-monster-button-label",
-				"data-monster-objectlink",
-				"data-monster-role",
-			];
-
-			for (const [, attr] of Object.entries(node.attributes)) {
-				if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) {
-					data[attr.name] = attr.value;
-				}
-			}
-
-			if (node.hasAttribute(ATTRIBUTE_FORM_URL)) {
-				const url = node.getAttribute(ATTRIBUTE_FORM_URL);
-
-				if (
-					!node.hasAttribute(ATTRIBUTE_FORM_RELOAD) ||
-					node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow"
-				) {
-					node.removeAttribute(ATTRIBUTE_FORM_URL);
-				}
-
-				const options = this.getOption("fetch", {});
-				const filter = undefined;
-				loadAndAssignContent(node, url, options, filter)
-					.then(() => {
-						fireCustomEvent(this, "monster-tab-changed", {
-							reference,
-						});
-					})
-					.catch((e) => {
-						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
-					});
-			} else {
-				fireCustomEvent(this, "monster-tab-changed", {
-					reference,
-					data,
-				});
-			}
-		} else {
-			node.classList.remove("active");
-		}
-	}
-
-	const standardButtons = this.getOption("buttons.standard");
-	for (const index in standardButtons) {
-		const button = standardButtons[index];
-		const state = button["reference"] === reference ? "active" : "inactive";
-		this.setOption(`buttons.standard.${index}.state`, state);
-	}
-
-	const popperButton = this.getOption("buttons.popper");
-	for (const index in popperButton) {
-		const button = popperButton[index];
-		const state = button["reference"] === reference ? "active" : "inactive";
-		this.setOption(`buttons.popper.${index}.state`, state);
-	}
-
-	hidePopper.call(this);
-}
-
-/**
- * @private
- */
-function initEventHandler() {
-	if (!this.shadowRoot) {
-		throw new Error("no shadow-root is defined");
-	}
-
-	/**
-	 * @param {Event} event
-	 */
-	this[changeTabEventHandler] = (event) => {
-		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");
-
-		if (element instanceof HTMLButtonElement && element.disabled !== true) {
-			show.call(this, element);
-		}
-	};
-
-	/**
-	 * event:monster-tab-remove
-	 * @event Monster.Components.Form.event:monster-tab-remove
-	 */
-
-	/**
-	 * @param {Event} event
-	 * @fires Monster.Components.Form.event:monster-tab-remove
-	 */
-	this[removeTabEventHandler] = (event) => {
-		const element = findTargetElementFromEvent(
-			event,
-			ATTRIBUTE_ROLE,
-			"remove-tab",
-		);
-
-		if (element instanceof HTMLElement) {
-			const button = findTargetElementFromEvent(
-				event,
-				ATTRIBUTE_ROLE,
-				"button",
-			);
-
-			if (button instanceof HTMLButtonElement && button.disabled !== true) {
-				const reference = button.getAttribute(
-					`${ATTRIBUTE_PREFIX}tab-reference`,
-				);
-				if (reference) {
-					const container = this.querySelector(`[id=${reference}]`);
-					if (container instanceof HTMLElement) {
-						container.remove();
-						initTabButtons.call(this);
-						fireCustomEvent(this, "monster-tab-remove", {
-							reference,
-						});
-					}
-				}
-			}
-		}
-	};
-
-	this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]);
-	this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]);
-
-	this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]);
-	this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]);
-
-	/**
-	 * @param {Event} event
-	 */
-	this[closeEventHandler] = (event) => {
-		const path = event.composedPath();
-
-		for (const [, element] of Object.entries(path)) {
-			if (element === this) {
-				return;
-			}
-		}
-
-		hidePopper.call(this);
-	};
-
-	return this;
-}
-
-/**
- * @private
- * @param observedNode
- */
-function attachTabMutationObserver(observedNode) {
-	const self = this;
-
-	if (hasObjectLink(observedNode, mutationObserverSymbol)) {
-		return;
-	}
-
-	/**
-	 * this construct monitors a node whether it is disabled or modified
-	 * @type {MutationObserver}
-	 */
-	const observer = new MutationObserver(function (mutations) {
-		if (isArray(mutations)) {
-			const mutation = mutations.pop();
-			if (mutation instanceof MutationRecord) {
-				initTabButtons.call(self);
-			}
-		}
-	});
-
-	observer.observe(observedNode, {
-		childList: false,
-		attributes: true,
-		subtree: false,
-		attributeFilter: [
-			"disabled",
-			ATTRIBUTE_BUTTON_LABEL,
-			`${ATTRIBUTE_PREFIX}button-icon`,
-		],
-	});
-
-	addToObjectLink(observedNode, mutationObserverSymbol, observer);
-}
-
-/**
- * @private
- * @return {Select}
- * @throws {Error} no shadow-root is defined
- */
-function initControlReferences() {
-	if (!this.shadowRoot) {
-		throw new Error("no shadow-root is defined");
-	}
-
-	this[controlElementSymbol] = this.shadowRoot.querySelector(
-		`[${ATTRIBUTE_ROLE}=control]`,
-	);
-	this[navElementSymbol] = this.shadowRoot.querySelector(
-		`nav[${ATTRIBUTE_ROLE}=nav]`,
-	);
-	this[popperElementSymbol] = this.shadowRoot.querySelector(
-		`[${ATTRIBUTE_ROLE}=popper]`,
-	);
-	this[popperNavElementSymbol] = this.shadowRoot.querySelector(
-		`[${ATTRIBUTE_ROLE}=popper-nav]`,
-	);
-}
-
-/**
- * @private
- * @return {Promise<unknown>}
- * @throws {Error} no shadow-root is defined
- *
- */
-function initTabButtons() {
-	if (!this.shadowRoot) {
-		throw new Error("no shadow-root is defined");
-	}
-
-	let activeReference;
-
-	const dimensionsCalculated = this[dimensionsSymbol].getVia(
-		"data.calculated",
-		false,
-	);
-
-	const buttons = [];
-	const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots
-
-	for (const node of nodes) {
-		if (!(node instanceof HTMLElement)) continue;
-		let label = getButtonLabel.call(this, node);
-
-		let reference;
-		if (node.hasAttribute("id")) {
-			reference = node.getAttribute("id");
-		}
-
-		let disabled;
-		if (node.hasAttribute("disabled") || node.disabled === true) {
-			disabled = true;
-		}
-
-		if (!reference) {
-			reference = new ID("tab").toString();
-			node.setAttribute("id", reference);
-		}
-
-		if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) {
-			label = `<span part="label">${label}</span><img part="icon" src="${node.getAttribute(
-				`${ATTRIBUTE_PREFIX}button-icon`,
-			)}">`;
-		}
-
-		let remove = false;
-		if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) {
-			remove = true;
-		}
-
-		if (node.matches(".active") === true && disabled !== true) {
-			node.classList.remove("active");
-			activeReference = reference;
-		}
-
-		const state = "";
-		const classes = dimensionsCalculated ? "" : "invisible";
-
-		buttons.push({
-			reference,
-			label,
-			state,
-			class: classes,
-			disabled,
-			remove,
-		});
-
-		attachTabMutationObserver.call(this, node);
-	}
-
-	this.setOption("buttons.standard", clone(buttons));
-	this.setOption("buttons.popper", []);
-	this.setOption("marker", random());
-
-	return adjustButtonVisibility.call(this).then(() => {
-		if (activeReference) {
-			return new Processing(() => {
-				const button = this.shadowRoot.querySelector(
-					`[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`,
-				);
-				if (button instanceof HTMLButtonElement && button.disabled !== true) {
-					show.call(this, button);
-				}
-			})
-				.run(undefined)
-				.then(() => {})
-				.catch((e) => {
-					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
-				});
-		}
-
-		return Promise.resolve();
-	});
-}
-
-function checkAndRearrangeButtons() {
-	if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) {
-		calculateNavigationButtonsDimensions.call(this);
-	}
-
-	rearrangeButtons.call(this);
-}
-
-/**
- * @private
- * @return {Promise<unknown>}
- */
-function adjustButtonVisibility() {
-	const self = this;
-
-	return new Promise((resolve) => {
-		const observer = new MutationObserver(function (mutations) {
-			const defCount = self.getOption("buttons.standard").length;
-			const domCount = self[navElementSymbol].querySelectorAll(
-				'button[data-monster-role="button"]',
-			).length;
-
-			// in drawing
-			if (defCount !== domCount) return;
-
-			observer.disconnect();
-
-			checkAndRearrangeButtons.call(self);
-
-			resolve();
-		});
-
-		observer.observe(self[navElementSymbol], {
-			attributes: true,
-		});
-	});
-}
-
-/**
- * @private
- * @param {string} value
- * @return {number}
- */
-function getDimValue(value) {
-	if ([undefined, null].indexOf(value) !== -1) {
-		return 0;
-	}
-
-	const valueAsInt = parseInt(value, 10);
-
-	if (isNaN(valueAsInt)) {
-		return 0;
-	}
-
-	return valueAsInt;
-}
-
-/**
- * @private
- * @param {HTMLElement} node
- * @return {number}
- */
-function calcBoxWidth(node) {
-	const dim = getGlobal("window").getComputedStyle(node);
-	const bounding = node.getBoundingClientRect();
-
-	return (
-		getDimValue(dim["border-left-width"]) +
-		getDimValue(dim["padding-left"]) +
-		getDimValue(dim["margin-left"]) +
-		getDimValue(bounding["width"]) +
-		getDimValue(dim["border-right-width"]) +
-		getDimValue(dim["margin-right"]) +
-		getDimValue(dim["padding-left"])
-	);
-}
-
-/**
- * @private
- * @return {Object}
- */
-function rearrangeButtons() {
-	const standardButtons = [];
-	const popperButtons = [];
-
-	let sum = 0;
-	const space = this[dimensionsSymbol].getVia("data.space");
-
-	const buttons = this.getOption("buttons.standard");
-	for (const [, button] of buttons.entries()) {
-		const ref = button?.reference;
-
-		sum += this[dimensionsSymbol].getVia(`data.button.${ref}`);
-
-		if (sum > space) {
-			popperButtons.push(clone(button));
-		} else {
-			standardButtons.push(clone(button));
-		}
-	}
-
-	this.setOption("buttons.standard", standardButtons);
-	this.setOption("buttons.popper", popperButtons);
-
-	if (this[switchElementSymbol]) {
-		if (popperButtons.length > 0) {
-			this[switchElementSymbol].classList.remove("hidden");
-		} else {
-			this[switchElementSymbol].classList.add("hidden");
-		}
-	}
-}
-
-/**
- * @private
- * @return {Object}
- */
-function calculateNavigationButtonsDimensions() {
-	const width = this[navElementSymbol].getBoundingClientRect().width;
-
-	let startEndWidth = 0;
-
-	getSlottedElements.call(this, undefined, "start").forEach((node) => {
-		startEndWidth += calcBoxWidth.call(this, node);
-	});
-
-	getSlottedElements.call(this, undefined, "end").forEach((node) => {
-		startEndWidth += calcBoxWidth.call(this, node);
-	});
-
-	this[dimensionsSymbol].setVia("data.space", width - startEndWidth - 2);
-	this[dimensionsSymbol].setVia("data.visible", !(width === 0));
-
-	const buttons = this.getOption("buttons.standard").concat(
-		this.getOption("buttons.popper"),
-	);
-
-	for (const [i, button] of buttons.entries()) {
-		const ref = button?.reference;
-		const element = this[navElementSymbol].querySelector(
-			`:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`,
-		);
-		if (!(element instanceof HTMLButtonElement)) continue;
-
-		this[dimensionsSymbol].setVia(
-			`data.button.${ref}`,
-			calcBoxWidth.call(this, element),
-		);
-		button["class"] = new TokenList(button["class"])
-			.remove("invisible")
-			.toString();
-	}
-
-	const slots = this[controlElementSymbol].querySelectorAll(
-		`nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`,
-	);
-	for (const [, slot] of slots.entries()) {
-		slot.classList.remove("invisible");
-	}
-
-	this[dimensionsSymbol].setVia("data.calculated", true);
-	this.setOption("buttons.standard", clone(buttons));
-}
-
-/**
- * @private
- * @param {HTMLElement} node
- * @return {string}
- */
-function getButtonLabel(node) {
-	let label;
-	let setLabel = false;
-	if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) {
-		label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL);
-	} else {
-		label = node.innerText;
-		setLabel = true;
-	}
-
-	if (!isString(label)) {
-		label = "";
-	}
-
-	label = label.trim();
-
-	if (label === "") {
-		label = this.getOption("labels.new-tab-label", "New Tab");
-	}
-
-	if (label.length > 100) {
-		label = `${label.substring(0, 99)}…`;
-	}
-
-	if (setLabel === true) {
-		node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label);
-	}
-
-	return label;
-}
-
-/**
- * @private
- * @return {string}
- */
-function getTemplate() {
-	// language=HTML
-	return `
-        <template id="buttons">
-            <button part="button"
-                    data-monster-role="button"
-                    data-monster-attributes="
-                    class path:classes.button,
-                    data-monster-state path:buttons.state,
-                    disabled path:buttons.disabled | if:true,                    
-                    data-monster-tab-reference path:buttons.reference"><span
-                    data-monster-replace="path:buttons.label"></span><span part="remove-tab"
-                                                                           data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden "
-                                                                           data-monster-role="remove-tab"
-                                                                           tabindex="-1"></span></button>
-        </template>
-        <div data-monster-role="control" part="control">
-            <nav data-monster-role="nav" part="nav"
-                 data-monster-attributes="data-monster-marker path:marker"
-                 data-monster-insert="buttons path:buttons.standard">
-                <slot name="start" class="invisible"></slot>
-                <div data-monster-role="popper" part="popper" tabindex="-1"
-					 data-monster-attributes="class path:classes.popper">
-                    <div data-popper-arrow></div>
-
+class Tabs extends NewTabs {
 
-                    <div part="popper-nav" data-monster-role="popper-nav"
-                         data-monster-insert="buttons path:buttons.popper"
-                         tabindex="-1"></div>
-                </div>
-                <slot name="popper" class="invisible"></slot>
-                <slot name="end" class="invisible"></slot>
-            </nav>
-            <slot data-monster-role="slot" class="invisible"></slot>
-        </div>`;
 }
 
 registerCustomElement(Tabs);
diff --git a/source/components/host/overlay.mjs b/source/components/host/overlay.mjs
index 08e38ba23..ab6e618bc 100644
--- a/source/components/host/overlay.mjs
+++ b/source/components/host/overlay.mjs
@@ -106,7 +106,7 @@ class Overlay extends CustomElement {
 	 * @returns {symbol}
 	 */
 	static get [instanceSymbol]() {
-		return Symbol.for("@schukai/component-host/overlay@@instance");
+		return Symbol.for("@schukai/monster/components/host/overlay@@instance");
 	}
 
 	/**
diff --git a/source/components/host/viewer.mjs b/source/components/host/viewer.mjs
index 8581aef06..7139b567c 100644
--- a/source/components/host/viewer.mjs
+++ b/source/components/host/viewer.mjs
@@ -39,18 +39,10 @@ const viewerElementSymbol = Symbol("viewerElement");
  * Or you can create this CustomControl directly in Javascript:
  *
  * ```js
- * import '@schukai/component-state/source/viewer.mjs';
+ * import '@schukai/monster/components/host/viewer.mjs';
  * document.createElement('monster-viewer');
  * ```
  *
- * The Body should have a class "hidden" to ensure that the styles are applied correctly.
- *
- * ```css
- * body.hidden {
- *    visibility: hidden;
- * }
- * ```
- *
  * @startuml viewer.png
  * skinparam monochrome true
  * skinparam shadowing false
@@ -62,10 +54,6 @@ const viewerElementSymbol = Symbol("viewerElement");
  * @copyright schukai GmbH
  * @memberOf Monster.Components.Host
  * @summary A simple viewer component
- * @fires Monster.Components.Host.Viewer.event:monster-viewer-before-open
- * @fires Monster.Components.Host.Viewer.event:monster-viewer-open
- * @fires Monster.Components.Host.Viewer.event:monster-viewer-before-close
- * @fires Monster.Components.Host.Viewer.event:monster-viewer-closed
  */
 class Viewer extends CustomElement {
 	/**
@@ -73,7 +61,7 @@ class Viewer extends CustomElement {
 	 * @returns {symbol}
 	 */
 	static get [instanceSymbol]() {
-		return Symbol.for("@schukai/component-host/viewer@@instance");
+		return Symbol.for("@schukai/monster/components/host/viewer@@instance");
 	}
 
 	/**
diff --git a/source/components/layout/namespace.mjs b/source/components/layout/namespace.mjs
new file mode 100644
index 000000000..23450cb6d
--- /dev/null
+++ b/source/components/layout/namespace.mjs
@@ -0,0 +1,13 @@
+/**
+ * Copyright 2023 schukai GmbH
+ * SPDX-License-Identifier: AGPL-3.0
+ */
+
+/**
+ * Namespace for all layout related functions.
+ *
+ * @namespace Monster.Components.Layout
+ * @memberOf Monster
+ * @author schukai GmbH
+ */
+const ns = {};
diff --git a/source/components/layout/split-screen.mjs b/source/components/layout/split-screen.mjs
new file mode 100644
index 000000000..3375a9f5c
--- /dev/null
+++ b/source/components/layout/split-screen.mjs
@@ -0,0 +1,341 @@
+/**
+ * Copyright 2023 schukai GmbH
+ * SPDX-License-Identifier: AGPL-3.0
+ */
+
+import {
+    assembleMethodSymbol,
+    CustomElement,
+    registerCustomElement,
+} from "../../dom/customelement.mjs";
+import "../notify/notify.mjs";
+import {Observer} from "../../types/observer.mjs";
+import {SplitScreenStyleSheet} from "./stylesheet/split-screen.mjs";
+import {instanceSymbol} from "../../constants.mjs";
+import {internalSymbol} from "../../constants.mjs";
+
+export {SplitScreen, TYPE_VERTICAL, TYPE_HORIZONTAL};
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const splitScreenElementSymbol = Symbol("splitScreenElement");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const draggerElementSymbol = Symbol("draggerElement");
+/**
+ * @private
+ * @type {symbol}
+ */
+const startPanelElementSymbol = Symbol("startPanelElement");
+/**
+ * @private
+ * @type {symbol}
+ */
+const endPanelElementSymbol = Symbol("endPanelElement");
+/**
+ * @private
+ * @type {symbol}
+ */
+const handleElementSymbol = Symbol("handleElement");
+
+/**
+ * 
+ * @type {string}
+ */
+const TYPE_VERTICAL = "vertical";
+/**
+ * 
+ * @type {string}
+ */
+const TYPE_HORIZONTAL = "horizontal";
+
+
+/**
+ * The Viewer component is used to show a PDF, HTML or Image.
+ *
+ * <img src="./images/splitscreen.png">
+ *
+ * You can create this control either by specifying the HTML tag <monster-splitscreen />` directly in the HTML or using
+ * Javascript via the `document.createElement('monster-split-screen');` method.
+ *
+ * ```html
+ * <monster-split-screen></monster-split-screen>
+ * ```
+ *
+ * Or you can create this CustomControl directly in Javascript:
+ *
+ * ```js
+ * import '@schukai/monster/components/layout/split-screen.mjs';
+ * document.createElement('monster-split-screen');
+ * ```
+ *
+ * @startuml splitscreen.png
+ * skinparam monochrome true
+ * skinparam shadowing false
+ * HTMLElement <|-- CustomElement
+ * CustomElement <|-- CustomControl
+ * CustomControl <|-- SplitScreen
+ * @enduml
+ *
+ * @copyright schukai GmbH
+ * @memberOf Monster.Components.Layout
+ * @summary A simple split screen layout
+ */
+class SplitScreen extends CustomElement {
+
+    /**
+     * This method is called by the `instanceof` operator.
+     * @returns {symbol}
+     */
+    static get [instanceSymbol]() {
+        return Symbol.for("@schukai/monster/components/layout/splitscreen");
+    }
+
+    /**
+     * To set the options via the html tag the attribute `data-monster-options` must be used.
+     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
+     *
+     * The individual configuration values can be found in the table.
+     *
+     * @property {Object} templates Template definitions
+     * @property {string} templates.main Main template
+     * @property {Object} classes Css classes
+     * @property {Object} features Feature definitions
+     */
+    get defaults() {
+        return Object.assign({}, super.defaults, {
+            templates: {
+                main: getTemplate(),
+            },
+            splitType: TYPE_VERTICAL,
+            dimension: "20%",
+            classes: {},
+            features: {},
+        });
+    }
+
+    setContent(html) {
+        this.setOption("content", html);
+        return this;
+    }
+
+    /**
+     *
+     * @returns {Monster.Components.Host.Viewer}
+     */
+    [assembleMethodSymbol]() {
+        super[assembleMethodSymbol]();
+
+        initControlReferences.call(this);
+        initEventHandler.call(this);
+        applyPanelDimensions.call(this);
+    }
+
+    setDimension(dimension) {
+        // check if percent and greater than100
+        if (dimension.includes("%")) {
+            if (parseInt(dimension) > 100) {
+                throw new Error("dimension must be less than 100%");
+            } else if (parseInt(dimension) < 0) {
+                throw new Error("dimension must be greater than 0%");
+            }
+        }
+
+        this.setOption("dimension", dimension);
+        return this;
+    }
+
+
+    /**
+     *
+     * @return {string}
+     */
+    static getTag() {
+        return "monster-split-screen";
+    }
+
+    /**
+     * @return {CSSStyleSheet[]}
+     */
+    static getCSSStyleSheet() {
+        return [SplitScreenStyleSheet];
+    }
+}
+
+
+/**
+ * Set the dimensions of the panel based on the split type.
+ */
+function applyPanelDimensions() {
+
+    const splitType = this.getOption("splitType");
+    const dimension = this.getOption("dimension");
+
+    if (splitType === TYPE_VERTICAL) {
+        this[startPanelElementSymbol].style.width = dimension;
+        this[endPanelElementSymbol].style.width = `calc(100% - ${dimension} - 5px)`;
+        this[draggerElementSymbol].style.cursor = "ew-resize";
+        this[splitScreenElementSymbol].classList.add("vertical");
+        this[splitScreenElementSymbol].classList.remove("horizontal");
+
+    } else {
+        this[startPanelElementSymbol].style.height = dimension;
+        this[endPanelElementSymbol].style.height = `calc(100% - ${dimension} - 5px)`;
+        this[draggerElementSymbol].style.cursor = "ns-resize";
+        this[splitScreenElementSymbol].classList.add("horizontal");
+        this[splitScreenElementSymbol].classList.remove("vertical");
+
+    }
+}
+
+
+/**
+ * @private
+ * @return {Select}
+ * @throws {Error} no shadow-root is defined
+ */
+function initControlReferences() {
+    if (!this.shadowRoot) {
+        throw new Error("no shadow-root is defined");
+    }
+
+    this[splitScreenElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=split-screen]");
+    this[draggerElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=dragger]");
+    this[handleElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=handle]");
+
+    this[startPanelElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=startPanel]");
+    this[endPanelElementSymbol] = this.shadowRoot.querySelector("[data-monster-role=endPanel]");
+
+}
+
+/**
+ * @private
+ */
+function initEventHandler() {
+    const self = this;
+
+    this[internalSymbol].getSubject().isDragging = false;
+
+    this[draggerElementSymbol].addEventListener('dblclick', () => {
+        self[internalSymbol].getSubject().isDragging = false;
+        applyPanelDimensions.call(this);
+    });
+
+    const resizeObserver = new ResizeObserver((entries) => {
+        for (let entry of entries) {
+            console.log(entry.contentRect.width);
+        }
+    });
+
+    resizeObserver.observe(this[splitScreenElementSymbol]);
+
+
+    this[draggerElementSymbol].addEventListener('mousedown', () => {
+        self[internalSymbol].getSubject().isDragging = true;
+
+        document.addEventListener('mousemove', (e) => {
+            e.preventDefault();
+            if (!self[internalSymbol].getSubject().isDragging) {
+                return;
+            }
+
+            if (self.getOption("splitType") === TYPE_HORIZONTAL) {
+                const containerOffsetTop = self[splitScreenElementSymbol].offsetTop;
+                const topPanel = self[startPanelElementSymbol];
+                const bottomPanel = self[endPanelElementSymbol];
+                const newTopHeight = e.clientY - containerOffsetTop;
+                topPanel.style.height = `${newTopHeight}px`;
+                bottomPanel.style.height = `calc(100% - ${newTopHeight}px - 5px)`; // 5px is dragger height
+
+
+            } else {
+
+                const containerOffsetLeft = self[splitScreenElementSymbol].offsetLeft;
+                const leftPanel = self[startPanelElementSymbol];
+                const rightPanel = self[endPanelElementSymbol];
+                const newLeftWidth = e.clientX - containerOffsetLeft;
+                leftPanel.style.width = `${newLeftWidth}px`;
+                rightPanel.style.width = `calc(100% - ${newLeftWidth}px - 5px)`; // 5px is dragger width
+            }
+
+        });
+
+        document.addEventListener('mouseup', (e) => {
+            self[internalSymbol].getSubject().isDragging = false;
+            document.removeEventListener('mousemove', (e) => {
+                if (!self[internalSymbol].getSubject().isDragging) {
+                    return;
+                }
+
+                if (self.getOption("splitType") === TYPE_VERTICAL) {
+                    const containerOffsetTop = self[splitScreenElementSymbol].offsetTop;
+                    const topPanel = self[startPanelElementSymbol];
+                    const bottomPanel = self[endPanelElementSymbol];
+                    const newTopHeight = e.clientY - containerOffsetTop;
+                    topPanel.style.height = `${newTopHeight}px`;
+                    bottomPanel.style.height = `calc(100% - ${newTopHeight}px - 5px)`; // 5px is dragger height
+
+                } else {
+                    const containerOffsetLeft = self[splitScreenElementSymbol].offsetLeft;
+                    const newLeftWidth = e.clientX - containerOffsetLeft;
+                    const leftPanel = self[startPanelElementSymbol];
+                    const rightPanel = self[endPanelElementSymbol];
+                    leftPanel.style.width = `${newLeftWidth}px`;
+                    rightPanel.style.width = `calc(100% - ${newLeftWidth}px - 5px)`; // 5px is dragger width
+                }
+            });
+            document.removeEventListener('mouseup', (e) => {
+                self[internalSymbol].getSubject().isDragging = false;
+            });
+        });
+    });
+
+    let lastDimension = this.getOption("dimension");
+    let lastType = this.getOption("splitType");
+    this[internalSymbol].attachObserver(
+        new Observer(() => {
+            if (lastDimension !== this.getOption("dimension")) {
+                lastDimension = this.getOption("dimension");
+                applyPanelDimensions.call(this);
+            }
+
+            if (lastType !== this.getOption("splitType")) {
+                lastType = this.getOption("splitType");
+                applyPanelDimensions.call(this);
+            }
+
+        }));
+
+
+    return this;
+
+}
+
+/**
+ * @private
+ * @return {string}
+ */
+function getTemplate() {
+    // language=HTML
+    return `
+        <div data-monster-role="split-screen" part="container">
+            <div data-monster-role="startPanel" class="panel" part="startPanel">
+                <slot name="start"></slot>
+            </div>
+            <div data-monster-role="dragger" part="dragger">
+                <div data-monster-role="handle"></div>
+            </div>
+            <div data-monster-role="endPanel" class="panel" part="endPanel">
+                <slot name="end"></slot>
+            </div>
+
+
+        </div>`;
+}
+
+registerCustomElement(SplitScreen);
diff --git a/source/components/layout/style/split-screen.pcss b/source/components/layout/style/split-screen.pcss
new file mode 100644
index 000000000..786812cf4
--- /dev/null
+++ b/source/components/layout/style/split-screen.pcss
@@ -0,0 +1,59 @@
+
+
+[data-monster-role="split-screen"] {
+
+    box-sizing: border-box;
+    display: flex;
+    width: 100%;
+    height: auto;
+    flex-direction: row;
+    margin: 0;
+    padding: 0;
+
+    & .panel {
+        flex-grow: 1;
+        overflow: auto;
+    }
+
+    [data-monster-role="dragger"] {
+        background-color: var(--monster-bg-color-primary-4);
+        color: var(--monster-color-primary-4);
+        width: var(--monster-border-width);
+        height: auto;
+        
+        position: relative;
+        
+        & [data-monster-role=handle] {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            cursor: pointer;
+            width: 5px;
+            height: 120px;
+            background-color: var(--monster-bg-color-primary-3);
+            color: var(--monster-color-primary-3);
+            z-index: var(--monster-z-index-outline);
+        }
+    }
+
+    &.horizontal {
+        flex-direction: column;
+
+        [data-monster-role="dragger"] {
+            width: 100%;
+            height: var(--monster-border-width);
+            
+            & [data-monster-role=handle] {
+                width: 120px;
+                height: 5px;
+            }
+            
+        }
+
+
+    }
+
+}
+
+
diff --git a/source/components/form/style/tabs.pcss b/source/components/layout/style/tabs.pcss
similarity index 99%
rename from source/components/form/style/tabs.pcss
rename to source/components/layout/style/tabs.pcss
index de838d398..6c3b5b096 100644
--- a/source/components/form/style/tabs.pcss
+++ b/source/components/layout/style/tabs.pcss
@@ -27,9 +27,6 @@ nav[data-monster-role=nav] {
     border-bottom-style: var(--monster-border-style);
     box-shadow: var(--monster-box-shadow-1);
     border-color: var(--monster-bg-color-primary-2);    
-    
-    flex-wrap: nowrap;
-    
 }
 
 [data-monster-role=nav] button .remove-tab {
diff --git a/source/components/layout/stylesheet/split-screen.mjs b/source/components/layout/stylesheet/split-screen.mjs
new file mode 100644
index 000000000..516f7ae58
--- /dev/null
+++ b/source/components/layout/stylesheet/split-screen.mjs
@@ -0,0 +1,27 @@
+
+/**
+ * Copyright schukai GmbH and contributors 2024. All Rights Reserved.
+ * Node module: @schukai/monster
+ * This file is licensed under the AGPLv3 License.
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
+ */
+
+import {addAttributeToken} from "../../../dom/attributes.mjs";
+import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
+
+export {SplitScreenStyleSheet}
+
+/**
+ * @private
+ * @type {CSSStyleSheet}
+ */
+const SplitScreenStyleSheet = new CSSStyleSheet();
+
+try {
+  SplitScreenStyleSheet.insertRule(`
+@layer splitscreen { 
+[data-monster-role=split-screen]{box-sizing:border-box;display:flex;flex-direction:row;height:auto;margin:0;padding:0;width:100%}[data-monster-role=split-screen] .panel{flex-grow:1;overflow:auto}[data-monster-role=split-screen] [data-monster-role=dragger]{background-color:var(--monster-bg-color-primary-4);color:var(--monster-color-primary-4);height:auto;position:relative;width:var(--monster-border-width)}[data-monster-role=split-screen] [data-monster-role=dragger] [data-monster-role=handle]{background-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-3);cursor:pointer;height:120px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px;z-index:var(--monster-z-index-outline)}.horizontal[data-monster-role=split-screen]{flex-direction:column}.horizontal[data-monster-role=split-screen] [data-monster-role=dragger]{height:var(--monster-border-width);width:100%}.horizontal[data-monster-role=split-screen] [data-monster-role=dragger] [data-monster-role=handle]{height:5px;width:120px} 
+}`, 0);
+} catch (e) {
+  addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
+}
diff --git a/source/components/form/stylesheet/tabs.mjs b/source/components/layout/stylesheet/tabs.mjs
similarity index 100%
rename from source/components/form/stylesheet/tabs.mjs
rename to source/components/layout/stylesheet/tabs.mjs
diff --git a/source/components/layout/tabs.mjs b/source/components/layout/tabs.mjs
new file mode 100644
index 000000000..75d9d07f6
--- /dev/null
+++ b/source/components/layout/tabs.mjs
@@ -0,0 +1,1075 @@
+/**
+ * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
+ * Node module: @schukai/monster
+ * This file is licensed under the AGPLv3 License.
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
+ */
+import { instanceSymbol } from "../../constants.mjs";
+import { createPopper } from "@popperjs/core";
+import { extend } from "../../data/extend.mjs";
+import { Pathfinder } from "../../data/pathfinder.mjs";
+import {
+	addAttributeToken,
+	addToObjectLink,
+	hasObjectLink,
+} from "../../dom/attributes.mjs";
+import {
+	ATTRIBUTE_ERRORMESSAGE,
+	ATTRIBUTE_PREFIX,
+	ATTRIBUTE_ROLE,
+} from "../../dom/constants.mjs";
+import {
+	assembleMethodSymbol,
+	CustomElement,
+	getSlottedElements,
+	registerCustomElement,
+} from "../../dom/customelement.mjs";
+import {
+	findTargetElementFromEvent,
+	fireCustomEvent,
+} from "../../dom/events.mjs";
+import { getDocument } from "../../dom/util.mjs";
+import { random } from "../../math/random.mjs";
+import { getGlobal } from "../../types/global.mjs";
+import { ID } from "../../types/id.mjs";
+import { isArray, isString } from "../../types/is.mjs";
+import { TokenList } from "../../types/tokenlist.mjs";
+import { clone } from "../../util/clone.mjs";
+import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
+import { Processing } from "../../util/processing.mjs";
+import {
+	ATTRIBUTE_BUTTON_LABEL,
+	ATTRIBUTE_FORM_RELOAD,
+	ATTRIBUTE_FORM_URL,
+	STYLE_DISPLAY_MODE_BLOCK,
+} from "../form/constants.mjs";
+
+import { TabsStyleSheet } from "./stylesheet/tabs.mjs";
+import { loadAndAssignContent } from "../form/util/fetch.mjs";
+import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
+import {
+	popperInstanceSymbol,
+	setEventListenersModifiers,
+} from "../form/util/popper.mjs";
+
+export { Tabs };
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const popperElementSymbol = Symbol("popperElement");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const popperNavElementSymbol = Symbol("popperNavElement");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const controlElementSymbol = Symbol("controlElement");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const navElementSymbol = Symbol("navElement");
+/**
+ * @private
+ * @type {symbol}
+ */
+const switchElementSymbol = Symbol("switchElement");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const changeTabEventHandler = Symbol("changeTabEventHandler");
+/**
+ * @private
+ * @type {symbol}
+ */
+const removeTabEventHandler = Symbol("removeTabEventHandler");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const popperSwitchEventHandler = Symbol("popperSwitchEventHandler");
+
+/**
+ * local symbol
+ * @private
+ * @type {symbol}
+ */
+const closeEventHandler = Symbol("closeEventHandler");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const mutationObserverSymbol = Symbol("mutationObserver");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const dimensionsSymbol = Symbol("dimensions");
+
+/**
+ * @private
+ * @type {symbol}
+ */
+const timerCallbackSymbol = Symbol("timerCallback");
+
+/**
+ * local symbol
+ * @private
+ * @type {symbol}
+ */
+const resizeObserverSymbol = Symbol("resizeObserver");
+
+/**
+ * This CustomControl creates a tab element with a variety of options.
+ *
+ * <img src="./images/tabs.png">
+ *
+ * You can create this control either by specifying the HTML tag `<monster-tabs />` directly in the HTML or using
+ * Javascript via the `document.createElement('monster-tabs');` method.
+ *
+ * ```html
+ * <monster-tabs></monster-tabs>
+ * ```
+ *
+ * Or you can create this CustomControl directly in Javascript:
+ *
+ * ```js
+ * import {Tabs} from '@schukai/monster/components/layout/tabs.mjs';
+ * document.createElement('monster-tabs');
+ * ```
+ *
+ * @example <caption>Create a simple tab control</caption>
+ * <monster-tabs>
+ *     <div id="tab1">Tab 1</div>
+ *     <div id="tab2">Tab 2</div>
+ * </monster-tabs>
+ *
+ * @startuml tabs.png
+ * skinparam monochrome true
+ * skinparam shadowing false
+ * HTMLElement <|-- CustomElement
+ * CustomElement <|-- Tabs
+ * @enduml
+ *
+ * @since 1.10.0
+ * @copyright schukai GmbH
+ * @memberOf Monster.Components.Layout
+ * @summary A configurable tab control
+ * @fires Monster.Components.Layout.event:monster-fetched
+ */
+class Tabs extends CustomElement {
+	/**
+	 * This method is called by the `instanceof` operator.
+	 * @returns {symbol}
+	 * @since 2.1.0
+	 */
+	static get [instanceSymbol]() {
+		return Symbol.for("@schukai/monster/components/layout/tabs");
+	}
+
+	/**
+	 * To set the options via the html tag the attribute `data-monster-options` must be used.
+	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
+	 *
+	 * The individual configuration values can be found in the table.
+	 *
+	 * @property {Object} templates Template definitions
+	 * @property {string} templates.main Main template
+	 * @property {Object} labels
+	 * @property {string} labels.new-tab-label="New Tab"
+	 * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
+	 * @property {String} fetch.redirect=error
+	 * @property {String} fetch.method=GET
+	 * @property {String} fetch.mode=same-origin
+	 * @property {String} fetch.credentials=same-origin
+	 * @property {Object} fetch.headers={"accept":"text/html"}}
+	 * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/)
+	 * @property {string} popper.placement=bottom PopperJS placement
+	 * @property {Object[]} modifiers={name:offset} PopperJS placement
+	 */
+	get defaults() {
+		return Object.assign({}, super.defaults, {
+			templates: {
+				main: getTemplate(),
+			},
+			labels: {
+				"new-tab-label": "New Tab",
+			},
+			buttons: {
+				standard: [],
+				popper: [],
+			},
+			fetch: {
+				redirect: "error",
+				method: "GET",
+				mode: "same-origin",
+				credentials: "same-origin",
+				headers: {
+					accept: "text/html",
+				},
+			},
+
+			classes: {
+				button: "monster-theme-primary-1",
+				popper: "monster-theme-primary-1",
+			},
+
+			popper: {
+				placement: "bottom",
+				modifiers: [
+					{
+						name: "offset",
+						options: {
+							offset: [0, 2],
+						},
+					},
+
+					{
+						name: "eventListeners",
+						enabled: false,
+					},
+				],
+			},
+		});
+	}
+
+	/**
+	 * This method is called internal and should not be called directly.
+	 */
+	[assembleMethodSymbol]() {
+		super[assembleMethodSymbol]();
+
+		initControlReferences.call(this);
+
+		this[dimensionsSymbol] = new Pathfinder({ data: {} });
+
+		initEventHandler.call(this);
+
+		// setup structure
+		initTabButtons.call(this).then(() => {
+			initPopperSwitch.call(this);
+			initPopper.call(this);
+			attachResizeObserver.call(this);
+			attachTabChangeObserver.call(this);
+		});
+	}
+
+	/**
+	 * This method is called internal and should not be called directly.
+	 *
+	 * @return {CSSStyleSheet[]}
+	 */
+	static getCSSStyleSheet() {
+		return [TabsStyleSheet, ThemeStyleSheet];
+	}
+
+	/**
+	 * This method is called internal and should not be called directly.
+	 *
+	 * @return {string}
+	 */
+	static getTag() {
+		return "monster-tabs";
+	}
+
+	/**
+	 * This method is called by the dom and should not be called directly.
+	 *
+	 * @return {void}
+	 */
+	connectedCallback() {
+		super.connectedCallback();
+
+		const document = getDocument();
+
+		for (const [, type] of Object.entries(["click", "touch"])) {
+			// close on outside ui-events
+			document.addEventListener(type, this[closeEventHandler]);
+		}
+	}
+
+	/**
+	 * This method is called by the dom and should not be called directly.
+	 *
+	 * @return {void}
+	 */
+	disconnectedCallback() {
+		super.disconnectedCallback();
+
+		const document = getDocument();
+
+		// close on outside ui-events
+		for (const [, type] of Object.entries(["click", "touch"])) {
+			document.removeEventListener(type, this[closeEventHandler]);
+		}
+	}
+}
+
+/**
+ * @private
+ */
+function initPopperSwitch() {
+	const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots
+	let switchButton;
+	if (nodes.size === 0) {
+		switchButton = document.createElement("button");
+		switchButton.setAttribute(ATTRIBUTE_ROLE, "switch");
+		switchButton.setAttribute("part", "switch");
+		switchButton.classList.add("hidden");
+		const classList = this.getOption("classes.button");
+		if (classList) {
+			switchButton.classList.add(classList);
+		}
+		switchButton.innerHTML =
+			'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>';
+		this[navElementSymbol].prepend(switchButton);
+	} else {
+		switchButton = nodes.next();
+	}
+
+	/**
+	 * @param {Event} event
+	 */
+	this[popperSwitchEventHandler] = (event) => {
+		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch");
+
+		if (element instanceof HTMLButtonElement) {
+			togglePopper.call(this);
+		}
+	};
+
+	for (const type of ["click", "touch"]) {
+		switchButton.addEventListener(type, this[popperSwitchEventHandler]);
+	}
+
+	this[switchElementSymbol] = switchButton;
+}
+
+/**
+ * @private
+ */
+function hidePopper() {
+	if (!this[popperInstanceSymbol]) {
+		return;
+	}
+
+	this[popperElementSymbol].style.display = "none";
+	// performance https://popper.js.org/docs/v2/tutorial/#performance
+	setEventListenersModifiers.call(this, false);
+}
+
+/**
+ * @private
+ */
+function showPopper() {
+	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
+		return;
+	}
+
+	this[popperElementSymbol].style.visibility = "hidden";
+	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;
+	// performance https://popper.js.org/docs/v2/tutorial/#performance
+	setEventListenersModifiers.call(this, true);
+
+	this[popperInstanceSymbol].update();
+
+	new Processing(() => {
+		this[popperElementSymbol].style.removeProperty("visibility");
+	})
+		.run(undefined)
+		.then(() => {})
+		.catch((e) => {
+			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
+		});
+}
+
+/**
+ * @private
+ */
+function togglePopper() {
+	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
+		hidePopper.call(this);
+	} else {
+		showPopper.call(this);
+	}
+}
+
+/**
+ * @private
+ */
+function attachResizeObserver() {
+	// against flickering
+	this[resizeObserverSymbol] = new ResizeObserver((entries) => {
+		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
+			try {
+				this[timerCallbackSymbol].touch();
+				return;
+			} catch (e) {
+				delete this[timerCallbackSymbol];
+			}
+		}
+
+		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
+			this[dimensionsSymbol].setVia("data.calculated", false);
+			checkAndRearrangeButtons.call(this);
+		});
+	});
+
+	this[resizeObserverSymbol].observe(this[navElementSymbol]);
+}
+
+/**
+ * @private
+ */
+function attachTabChangeObserver() {
+	// against flickering
+	new MutationObserver((mutations) => {
+		let runUpdate = false;
+
+		for (const mutation of mutations) {
+			if (mutation.type === "childList") {
+				if (
+					mutation.addedNodes.length > 0 ||
+					mutation.removedNodes.length > 0
+				) {
+					runUpdate = true;
+					break;
+				}
+			}
+		}
+
+		if (runUpdate === true) {
+			this[dimensionsSymbol].setVia("data.calculated", false);
+			initTabButtons.call(this);
+		}
+	}).observe(this, {
+		childList: true,
+	});
+}
+
+/**
+ * @private
+ * @return {Select}
+ * @external "external:createPopper"
+ * @see {@link  Plugins}
+ */
+function initPopper() {
+	const self = this;
+
+	const options = extend({}, self.getOption("popper"));
+
+	self[popperInstanceSymbol] = createPopper(
+		self[switchElementSymbol],
+		self[popperElementSymbol],
+		options,
+	);
+
+	const observer1 = new MutationObserver(function (mutations) {
+		let runUpdate = false;
+
+		for (const mutation of mutations) {
+			if (mutation.type === "childList") {
+				if (
+					mutation.addedNodes.length > 0 ||
+					mutation.removedNodes.length > 0
+				) {
+					runUpdate = true;
+					break;
+				}
+			}
+		}
+
+		if (runUpdate === true) {
+			self[popperInstanceSymbol].update();
+		}
+	});
+
+	observer1.observe(self[popperNavElementSymbol], {
+		childList: true,
+		subtree: true,
+	});
+
+	return self;
+}
+
+/**
+ * @private
+ * @param {HTMLElement} element
+ */
+function show(element) {
+	if (!this.shadowRoot) {
+		throw new Error("no shadow-root is defined");
+	}
+
+	const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`);
+
+	const nodes = getSlottedElements.call(this);
+	for (const node of nodes) {
+		const id = node.getAttribute("id");
+
+		if (id === reference) {
+			node.classList.add("active");
+
+			// get all data- from button and filter out data-monster-attributes and data-monster-insert
+			const data = {};
+			const mask = [
+				"data-monster-attributes",
+				"data-monster-insert-reference",
+				"data-monster-state",
+				"data-monster-button-label",
+				"data-monster-objectlink",
+				"data-monster-role",
+			];
+
+			for (const [, attr] of Object.entries(node.attributes)) {
+				if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) {
+					data[attr.name] = attr.value;
+				}
+			}
+
+			if (node.hasAttribute(ATTRIBUTE_FORM_URL)) {
+				const url = node.getAttribute(ATTRIBUTE_FORM_URL);
+
+				if (
+					!node.hasAttribute(ATTRIBUTE_FORM_RELOAD) ||
+					node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow"
+				) {
+					node.removeAttribute(ATTRIBUTE_FORM_URL);
+				}
+
+				const options = this.getOption("fetch", {});
+				const filter = undefined;
+				loadAndAssignContent(node, url, options, filter)
+					.then(() => {
+						fireCustomEvent(this, "monster-tab-changed", {
+							reference,
+						});
+					})
+					.catch((e) => {
+						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
+					});
+			} else {
+				fireCustomEvent(this, "monster-tab-changed", {
+					reference,
+					data,
+				});
+			}
+		} else {
+			node.classList.remove("active");
+		}
+	}
+
+	const standardButtons = this.getOption("buttons.standard");
+	for (const index in standardButtons) {
+		const button = standardButtons[index];
+		const state = button["reference"] === reference ? "active" : "inactive";
+		this.setOption(`buttons.standard.${index}.state`, state);
+	}
+
+	const popperButton = this.getOption("buttons.popper");
+	for (const index in popperButton) {
+		const button = popperButton[index];
+		const state = button["reference"] === reference ? "active" : "inactive";
+		this.setOption(`buttons.popper.${index}.state`, state);
+	}
+
+	hidePopper.call(this);
+}
+
+/**
+ * @private
+ */
+function initEventHandler() {
+	if (!this.shadowRoot) {
+		throw new Error("no shadow-root is defined");
+	}
+
+	/**
+	 * @param {Event} event
+	 */
+	this[changeTabEventHandler] = (event) => {
+		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");
+
+		if (element instanceof HTMLButtonElement && element.disabled !== true) {
+			show.call(this, element);
+		}
+	};
+
+	/**
+	 * event:monster-tab-remove
+	 * @event Monster.Components.Layout.event:monster-tab-remove
+	 */
+
+	/**
+	 * @param {Event} event
+	 * @fires Monster.Components.Layout.event:monster-tab-remove
+	 */
+	this[removeTabEventHandler] = (event) => {
+		const element = findTargetElementFromEvent(
+			event,
+			ATTRIBUTE_ROLE,
+			"remove-tab",
+		);
+
+		if (element instanceof HTMLElement) {
+			const button = findTargetElementFromEvent(
+				event,
+				ATTRIBUTE_ROLE,
+				"button",
+			);
+
+			if (button instanceof HTMLButtonElement && button.disabled !== true) {
+				const reference = button.getAttribute(
+					`${ATTRIBUTE_PREFIX}tab-reference`,
+				);
+				if (reference) {
+					const container = this.querySelector(`[id=${reference}]`);
+					if (container instanceof HTMLElement) {
+						container.remove();
+						initTabButtons.call(this);
+						fireCustomEvent(this, "monster-tab-remove", {
+							reference,
+						});
+					}
+				}
+			}
+		}
+	};
+
+	this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]);
+	this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]);
+
+	this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]);
+	this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]);
+
+	/**
+	 * @param {Event} event
+	 */
+	this[closeEventHandler] = (event) => {
+		const path = event.composedPath();
+
+		for (const [, element] of Object.entries(path)) {
+			if (element === this) {
+				return;
+			}
+		}
+
+		hidePopper.call(this);
+	};
+
+	return this;
+}
+
+/**
+ * @private
+ * @param observedNode
+ */
+function attachTabMutationObserver(observedNode) {
+	const self = this;
+
+	if (hasObjectLink(observedNode, mutationObserverSymbol)) {
+		return;
+	}
+
+	/**
+	 * this construct monitors a node whether it is disabled or modified
+	 * @type {MutationObserver}
+	 */
+	const observer = new MutationObserver(function (mutations) {
+		if (isArray(mutations)) {
+			const mutation = mutations.pop();
+			if (mutation instanceof MutationRecord) {
+				initTabButtons.call(self);
+			}
+		}
+	});
+
+	observer.observe(observedNode, {
+		childList: false,
+		attributes: true,
+		subtree: false,
+		attributeFilter: [
+			"disabled",
+			ATTRIBUTE_BUTTON_LABEL,
+			`${ATTRIBUTE_PREFIX}button-icon`,
+		],
+	});
+
+	addToObjectLink(observedNode, mutationObserverSymbol, observer);
+}
+
+/**
+ * @private
+ * @return {Select}
+ * @throws {Error} no shadow-root is defined
+ */
+function initControlReferences() {
+	if (!this.shadowRoot) {
+		throw new Error("no shadow-root is defined");
+	}
+
+	this[controlElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=control]`,
+	);
+	this[navElementSymbol] = this.shadowRoot.querySelector(
+		`nav[${ATTRIBUTE_ROLE}=nav]`,
+	);
+	this[popperElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=popper]`,
+	);
+	this[popperNavElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=popper-nav]`,
+	);
+}
+
+/**
+ * @private
+ * @return {Promise<unknown>}
+ * @throws {Error} no shadow-root is defined
+ *
+ */
+function initTabButtons() {
+	if (!this.shadowRoot) {
+		throw new Error("no shadow-root is defined");
+	}
+
+	let activeReference;
+
+	const dimensionsCalculated = this[dimensionsSymbol].getVia(
+		"data.calculated",
+		false,
+	);
+
+	const buttons = [];
+	const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots
+
+	for (const node of nodes) {
+		if (!(node instanceof HTMLElement)) continue;
+		let label = getButtonLabel.call(this, node);
+
+		let reference;
+		if (node.hasAttribute("id")) {
+			reference = node.getAttribute("id");
+		}
+
+		let disabled;
+		if (node.hasAttribute("disabled") || node.disabled === true) {
+			disabled = true;
+		}
+
+		if (!reference) {
+			reference = new ID("tab").toString();
+			node.setAttribute("id", reference);
+		}
+
+		if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) {
+			label = `<span part="label">${label}</span><img part="icon" src="${node.getAttribute(
+				`${ATTRIBUTE_PREFIX}button-icon`,
+			)}">`;
+		}
+
+		let remove = false;
+		if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) {
+			remove = true;
+		}
+
+		if (node.matches(".active") === true && disabled !== true) {
+			node.classList.remove("active");
+			activeReference = reference;
+		}
+
+		const state = "";
+		const classes = dimensionsCalculated ? "" : "invisible";
+
+		buttons.push({
+			reference,
+			label,
+			state,
+			class: classes,
+			disabled,
+			remove,
+		});
+
+		attachTabMutationObserver.call(this, node);
+	}
+
+	this.setOption("buttons.standard", clone(buttons));
+	this.setOption("buttons.popper", []);
+	this.setOption("marker", random());
+
+	return adjustButtonVisibility.call(this).then(() => {
+		if (activeReference) {
+			return new Processing(() => {
+				const button = this.shadowRoot.querySelector(
+					`[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`,
+				);
+				if (button instanceof HTMLButtonElement && button.disabled !== true) {
+					show.call(this, button);
+				}
+			})
+				.run(undefined)
+				.then(() => {})
+				.catch((e) => {
+					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
+				});
+		}
+
+		return Promise.resolve();
+	});
+}
+
+function checkAndRearrangeButtons() {
+	if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) {
+		calculateNavigationButtonsDimensions.call(this);
+	}
+
+	rearrangeButtons.call(this);
+}
+
+/**
+ * @private
+ * @return {Promise<unknown>}
+ */
+function adjustButtonVisibility() {
+	const self = this;
+
+	return new Promise((resolve) => {
+		const observer = new MutationObserver(function (mutations) {
+			const defCount = self.getOption("buttons.standard").length;
+			const domCount = self[navElementSymbol].querySelectorAll(
+				'button[data-monster-role="button"]',
+			).length;
+
+			// in drawing
+			if (defCount !== domCount) return;
+
+			observer.disconnect();
+
+			checkAndRearrangeButtons.call(self);
+
+			resolve();
+		});
+
+		observer.observe(self[navElementSymbol], {
+			attributes: true,
+		});
+	});
+}
+
+/**
+ * @private
+ * @param {string} value
+ * @return {number}
+ */
+function getDimValue(value) {
+	if ([undefined, null].indexOf(value) !== -1) {
+		return 0;
+	}
+
+	const valueAsInt = parseInt(value, 10);
+
+	if (isNaN(valueAsInt)) {
+		return 0;
+	}
+
+	return valueAsInt;
+}
+
+/**
+ * @private
+ * @param {HTMLElement} node
+ * @return {number}
+ */
+function calcBoxWidth(node) {
+	const dim = getGlobal("window").getComputedStyle(node);
+	const bounding = node.getBoundingClientRect();
+
+	return (
+		getDimValue(dim["border-left-width"]) +
+		getDimValue(dim["padding-left"]) +
+		getDimValue(dim["margin-left"]) +
+		getDimValue(bounding["width"]) +
+		getDimValue(dim["border-right-width"]) +
+		getDimValue(dim["margin-right"]) +
+		getDimValue(dim["padding-left"])
+	);
+}
+
+/**
+ * @private
+ * @return {Object}
+ */
+function rearrangeButtons() {
+	const standardButtons = [];
+	const popperButtons = [];
+
+	let sum = 0;
+	const space = this[dimensionsSymbol].getVia("data.space");
+
+	const buttons = this.getOption("buttons.standard");
+	for (const [, button] of buttons.entries()) {
+		const ref = button?.reference;
+
+		sum += this[dimensionsSymbol].getVia(`data.button.${ref}`);
+
+		if (sum > space) {
+			popperButtons.push(clone(button));
+		} else {
+			standardButtons.push(clone(button));
+		}
+	}
+
+	this.setOption("buttons.standard", standardButtons);
+	this.setOption("buttons.popper", popperButtons);
+
+	if (this[switchElementSymbol]) {
+		if (popperButtons.length > 0) {
+			this[switchElementSymbol].classList.remove("hidden");
+		} else {
+			this[switchElementSymbol].classList.add("hidden");
+		}
+	}
+}
+
+/**
+ * @private
+ * @return {Object}
+ */
+function calculateNavigationButtonsDimensions() {
+	const width = this[navElementSymbol].getBoundingClientRect().width;
+
+	let startEndWidth = 0;
+
+	getSlottedElements.call(this, undefined, "start").forEach((node) => {
+		startEndWidth += calcBoxWidth.call(this, node);
+	});
+
+	getSlottedElements.call(this, undefined, "end").forEach((node) => {
+		startEndWidth += calcBoxWidth.call(this, node);
+	});
+
+	this[dimensionsSymbol].setVia("data.space", width - startEndWidth - 2);
+	this[dimensionsSymbol].setVia("data.visible", !(width === 0));
+
+	const buttons = this.getOption("buttons.standard").concat(
+		this.getOption("buttons.popper"),
+	);
+
+	for (const [i, button] of buttons.entries()) {
+		const ref = button?.reference;
+		const element = this[navElementSymbol].querySelector(
+			`:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`,
+		);
+		if (!(element instanceof HTMLButtonElement)) continue;
+
+		this[dimensionsSymbol].setVia(
+			`data.button.${ref}`,
+			calcBoxWidth.call(this, element),
+		);
+		button["class"] = new TokenList(button["class"])
+			.remove("invisible")
+			.toString();
+	}
+
+	const slots = this[controlElementSymbol].querySelectorAll(
+		`nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`,
+	);
+	for (const [, slot] of slots.entries()) {
+		slot.classList.remove("invisible");
+	}
+
+	this[dimensionsSymbol].setVia("data.calculated", true);
+	this.setOption("buttons.standard", clone(buttons));
+}
+
+/**
+ * @private
+ * @param {HTMLElement} node
+ * @return {string}
+ */
+function getButtonLabel(node) {
+	let label;
+	let setLabel = false;
+	if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) {
+		label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL);
+	} else {
+		label = node.innerText;
+		setLabel = true;
+	}
+
+	if (!isString(label)) {
+		label = "";
+	}
+
+	label = label.trim();
+
+	if (label === "") {
+		label = this.getOption("labels.new-tab-label", "New Tab");
+	}
+
+	if (label.length > 100) {
+		label = `${label.substring(0, 99)}…`;
+	}
+
+	if (setLabel === true) {
+		node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label);
+	}
+
+	return label;
+}
+
+/**
+ * @private
+ * @return {string}
+ */
+function getTemplate() {
+	// language=HTML
+	return `
+        <template id="buttons">
+            <button part="button"
+                    data-monster-role="button"
+                    data-monster-attributes="
+                    class path:classes.button,
+                    data-monster-state path:buttons.state,
+                    disabled path:buttons.disabled | if:true,                    
+                    data-monster-tab-reference path:buttons.reference"><span
+                    data-monster-replace="path:buttons.label"></span><span part="remove-tab"
+                                                                           data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden "
+                                                                           data-monster-role="remove-tab"
+                                                                           tabindex="-1"></span></button>
+        </template>
+        <div data-monster-role="control" part="control">
+            <nav data-monster-role="nav" part="nav"
+                 data-monster-attributes="data-monster-marker path:marker"
+                 data-monster-insert="buttons path:buttons.standard">
+                <slot name="start" class="invisible"></slot>
+                <div data-monster-role="popper" part="popper" tabindex="-1"
+					 data-monster-attributes="class path:classes.popper">
+                    <div data-popper-arrow></div>
+
+
+                    <div part="popper-nav" data-monster-role="popper-nav"
+                         data-monster-insert="buttons path:buttons.popper"
+                         tabindex="-1"></div>
+                </div>
+                <slot name="popper" class="invisible"></slot>
+                <slot name="end" class="invisible"></slot>
+            </nav>
+            <slot data-monster-role="slot" class="invisible"></slot>
+        </div>`;
+}
+
+registerCustomElement(Tabs);
-- 
GitLab