diff --git a/ivette/.gitignore b/ivette/.gitignore
index d77ab83c7bc84b3d2b1435f00e2ddb2042d5df65..0ad2d3aec30181df58b19a1867020acd77d3bc04 100644
--- a/ivette/.gitignore
+++ b/ivette/.gitignore
@@ -13,6 +13,7 @@ yarn-error.log
 /dist
 /doc/html
 /src/renderer/loader.ts
+/src/renderer/sandbox.ts
 /src/dome/doc/guides/icons.md
 /Makefile.plugins
 
diff --git a/ivette/CONTRIBUTING.md b/ivette/CONTRIBUTING.md
index 234508561fb240a0bd9647dae6ab67c4d639d702..d17eec67bd6ea0411c6e5ad9aeb18549787ca98b 100644
--- a/ivette/CONTRIBUTING.md
+++ b/ivette/CONTRIBUTING.md
@@ -54,6 +54,10 @@ Useful extensions:
 - `ESlint` provides support for lint errors and warnings;
 - `ES7 React/Redux/GraphQL/React-Native snippets` provides boilerplate snippets;
 
+# Sandboxing
+
+It is possible to add visual tests and playgrounds inside `src/sandbox` directory.
+Please read the associated [src/sandbox/README.md](src/sandbox/README.md) instructions.
 
 # Coding rules
 
diff --git a/ivette/Makefile b/ivette/Makefile
index 2fdd3467f40a35bb3f38e68c34ab9e0390436648..61a31e5f29c3d8212ae4b664bacabd92e72444d9 100644
--- a/ivette/Makefile
+++ b/ivette/Makefile
@@ -65,19 +65,29 @@ tsc: dome-pkg dome-templ
 # --------------------------------------------------------------------------
 
 LOADER=src/renderer/loader.ts
+SANDBOX=src/renderer/sandbox.ts
 PACKAGES=$(shell find src -name "pkg.json")
+SANDBOXES=$(shell find src/sandbox -name "*.tsx")
 
 lint: pkg
 dome-pkg: pkg
 dome-app: pkg
 dome-dev: pkg
 dome-dist: pkg
-pkg: $(LOADER)
+
+pkg: $(LOADER) $(SANDBOX)
+
 $(LOADER): $(PACKAGES) ./configure.js ./Makefile
-	@rm -f $(LOADER)
+	@rm -f $@
 	@echo "[Ivette] configure packages"
-	@node ./configure.js $(LOADER) $(PACKAGES)
-	@chmod -f a-w $(LOADER)
+	@node ./configure.js $@ $(PACKAGES)
+	@chmod -f a-w $@
+
+$(SANDBOX): $(SANDBOXES) ./sandboxer.js ./Makefile
+	@rm -f $@
+	@echo "[Ivette] configure sandboxes"
+	@node ./sandboxer.js $@ $(SANDBOXES)
+	@chmod -f a-w $@
 
 # --------------------------------------------------------------------------
 # --- Frama-C Source Distrib
diff --git a/ivette/Makefile.distrib b/ivette/Makefile.distrib
index dba6fa37ba930252cee8289297bda4fd21ecfefe..0f933db596ee0febd3157aa739797a507e871fee 100644
--- a/ivette/Makefile.distrib
+++ b/ivette/Makefile.distrib
@@ -18,6 +18,7 @@ DISTRIB_FILES += ivette/electron-builder.json
 DISTRIB_FILES += ivette/electron-webpack.json
 DISTRIB_FILES += ivette/ivette-macos.sh
 DISTRIB_FILES += ivette/package.json
+DISTRIB_FILES += ivette/sandboxer.js
 DISTRIB_FILES += ivette/src/dome/.gitignore
 DISTRIB_FILES += ivette/src/dome/CONTRIBUTING.md
 DISTRIB_FILES += ivette/src/dome/CONTRIBUTORS.md
@@ -112,6 +113,7 @@ DISTRIB_FILES += ivette/src/dome/renderer/layout/boxes.tsx
 DISTRIB_FILES += ivette/src/dome/renderer/layout/dispatch.tsx
 DISTRIB_FILES += ivette/src/dome/renderer/layout/forms.tsx
 DISTRIB_FILES += ivette/src/dome/renderer/layout/grids.js
+DISTRIB_FILES += ivette/src/dome/renderer/layout/qsplit.tsx
 DISTRIB_FILES += ivette/src/dome/renderer/layout/splitters.tsx
 DISTRIB_FILES += ivette/src/dome/renderer/layout/style.css
 DISTRIB_FILES += ivette/src/dome/renderer/light.css
@@ -194,7 +196,6 @@ DISTRIB_FILES += ivette/src/frama-c/server.ts
 DISTRIB_FILES += ivette/src/frama-c/states.ts
 DISTRIB_FILES += ivette/src/ivette/index.tsx
 DISTRIB_FILES += ivette/src/ivette/prefs.tsx
-DISTRIB_FILES += ivette/src/ivette/sandbox.tsx
 DISTRIB_FILES += ivette/src/main/index.js
 DISTRIB_FILES += ivette/src/renderer/Application.tsx
 DISTRIB_FILES += ivette/src/renderer/Controller.tsx
@@ -203,6 +204,8 @@ DISTRIB_FILES += ivette/src/renderer/Laboratory.tsx
 DISTRIB_FILES += ivette/src/renderer/Preferences.tsx
 DISTRIB_FILES += ivette/src/renderer/index.js
 DISTRIB_FILES += ivette/src/renderer/style.css
+DISTRIB_FILES += ivette/src/sandbox/README.md
+DISTRIB_FILES += ivette/src/sandbox/qsplit.tsx
 DISTRIB_FILES += ivette/tests/eva-1.i
 DISTRIB_FILES += ivette/tests/eva-2.i
 DISTRIB_FILES += ivette/tsconfig.json
diff --git a/ivette/distrib.sh b/ivette/distrib.sh
index cb04515a5d49ff8171f697c142803af338512e42..38012edac621ff293d5ea0b1f3964f68d9adc5a4 100755
--- a/ivette/distrib.sh
+++ b/ivette/distrib.sh
@@ -26,7 +26,7 @@ Distribute() {
             *)
                 echo "DISTRIB_FILES += $src/$f" >> $Distrib
                 case $f in
-                    *.sh | *.json | */dome/doc/* | configure.js | .* | webpack*.js )
+                    *.sh | *.json | */dome/doc/* | configure.js | sandboxer.js | .* | webpack*.js )
                         echo "$f: .ignore" >> $Headers
                         ;;
                     *Make* | *.js* | *.ts* | *.ml*)
diff --git a/ivette/headers/header_spec.txt b/ivette/headers/header_spec.txt
index be8c7cf2f9ef17c8ccf162b16983bca82a71848d..361c89dfb300142bb207d7bdcf5f4010d130f83b 100644
--- a/ivette/headers/header_spec.txt
+++ b/ivette/headers/header_spec.txt
@@ -17,6 +17,7 @@ electron-builder.json: .ignore
 electron-webpack.json: .ignore
 ivette-macos.sh: .ignore
 package.json: .ignore
+sandboxer.js: .ignore
 src/dome/.gitignore: .ignore
 src/dome/CONTRIBUTING.md: .ignore
 src/dome/CONTRIBUTORS.md: .ignore
@@ -111,6 +112,7 @@ src/dome/renderer/layout/boxes.tsx: CEA_LGPL
 src/dome/renderer/layout/dispatch.tsx: CEA_LGPL
 src/dome/renderer/layout/forms.tsx: CEA_LGPL
 src/dome/renderer/layout/grids.js: CEA_LGPL
+src/dome/renderer/layout/qsplit.tsx: CEA_LGPL
 src/dome/renderer/layout/splitters.tsx: CEA_LGPL
 src/dome/renderer/layout/style.css: .ignore
 src/dome/renderer/light.css: .ignore
@@ -193,7 +195,6 @@ src/frama-c/server.ts: CEA_LGPL
 src/frama-c/states.ts: CEA_LGPL
 src/ivette/index.tsx: CEA_LGPL
 src/ivette/prefs.tsx: CEA_LGPL
-src/ivette/sandbox.tsx: CEA_LGPL
 src/main/index.js: CEA_LGPL
 src/renderer/Application.tsx: CEA_LGPL
 src/renderer/Controller.tsx: CEA_LGPL
@@ -202,6 +203,8 @@ src/renderer/Laboratory.tsx: CEA_LGPL
 src/renderer/Preferences.tsx: CEA_LGPL
 src/renderer/index.js: CEA_LGPL
 src/renderer/style.css: .ignore
+src/sandbox/README.md: .ignore
+src/sandbox/qsplit.tsx: CEA_LGPL
 tests/eva-1.i: .ignore
 tests/eva-2.i: .ignore
 tsconfig.json: .ignore
diff --git a/ivette/sandboxer.js b/ivette/sandboxer.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc1af7ba1f9cc582e4ab18bfc4f848ba0d16747f
--- /dev/null
+++ b/ivette/sandboxer.js
@@ -0,0 +1,24 @@
+// --------------------------------------------------------------------------
+// --- Configure Sandboxes
+// --- Called by [make pkg]
+// --------------------------------------------------------------------------
+
+const path = require('path');
+const fs = require('fs');
+
+const loader = process.argv[2];
+const inputFiles = process.argv.slice(3);
+let buffer = '// Ivette Sandboxes Loader (generated)\n';
+
+inputFiles.forEach((file) => {
+  try {
+    const box = path.relative('./src',file);
+    console.log(`[Ivette] sandbox ${box}`);
+    buffer += `import '../${box}';\n`;
+  } catch(err) {
+    console.error(`[Dome] Error ${file}: ${err}`);
+    process.exit(1);
+  }
+});
+
+fs.writeFileSync(loader, buffer);
diff --git a/ivette/src/dome/renderer/layout/qsplit.tsx b/ivette/src/dome/renderer/layout/qsplit.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5a21b802c46968a89371e53156778fc8065b4410
--- /dev/null
+++ b/ivette/src/dome/renderer/layout/qsplit.tsx
@@ -0,0 +1,489 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2022                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/* -------------------------------------------------------------------------- */
+/* --- Quarter-based Splitter                                             --- */
+/* -------------------------------------------------------------------------- */
+
+/**
+    @packageDocumentation
+    @module dome/layout/qsplit
+*/
+
+import * as React from 'react';
+import * as Utils from 'dome/misc/utils';
+import { DraggableCore, DraggableEventHandler } from 'react-draggable';
+import { AutoSizer, Size } from 'react-virtualized';
+
+/* -------------------------------------------------------------------------- */
+/* --- Q-Split Properties                                                 --- */
+/* -------------------------------------------------------------------------- */
+
+export interface QSplitProps {
+  /** Q-Split additional class. */
+  className?: string;
+  /** Q-Split additional style. */
+  style?: React.CSSProperties;
+  /** Q-Pane to layout in A-quarter. */
+  A?: string;
+  /** Q-Pane to layout in B-quarter. */
+  B?: string;
+  /** Q-Pane to layout in C-quarter. */
+  C?: string;
+  /** Q-Pane to layout in D-quarter. */
+  D?: string;
+  /** Horizontal panes ratio (range `0..1`, default `0.5`). */
+  H?: number;
+  /** Vertical panes ratio (range `0..1`, default `0.5`). */
+  V?: number;
+  /** Dragging ratios callback. */
+  setPosition?: (H: number, V: number) => void;
+  /** Q-Split contents. Shall be (possibly packed) Q-Panes.
+     Other components would be layout as they are in the
+     positionned `<div/>` of the Q-Split. */
+  children?: React.ReactNode;
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Split Bars                                                         --- */
+/* -------------------------------------------------------------------------- */
+
+type DragPos = { position: number, anchor: number, offset: number };
+type Dragging = undefined | DragPos;
+
+const getDragPosition =
+  (d: DragPos): number => d.position + d.offset - d.anchor;
+
+interface BSplitterProps {
+  hsplit: boolean;
+  style: React.CSSProperties;
+  dragging: Dragging;
+  setDragging: (dragging: Dragging) => void;
+  setPosition: (P: number) => void;
+  resetPosition: () => void;
+}
+
+const HPOS = 'dome-xSplitter-hpos-R';
+const VPOS = 'dome-xSplitter-vpos-R';
+const HVPOS = 'dome-xSplitter-hvpos';
+const HANDLE = '.dome-xSplitter-grab';
+const HGRAB = 'dome-xSplitter-grab dome-xSplitter-hgrab';
+const VGRAB = 'dome-xSplitter-grab dome-xSplitter-vgrab';
+const HVGRAB = 'dome-xSplitter-grab dome-xSplitter-hvgrab';
+const DRAGGING = 'dome-color-dragging';
+const DRAGZONE = 'dome-color-dragzone';
+
+function BSplitter(props: BSplitterProps): JSX.Element {
+  const { hsplit, style, dragging } = props;
+
+  const onStart: DraggableEventHandler =
+    (_evt, data) => {
+      const startPos = hsplit ? data.node.offsetLeft : data.node.offsetTop;
+      const anchor = hsplit ? data.x : data.y;
+      props.setDragging({ position: startPos, offset: anchor, anchor });
+    };
+
+  const onDrag: DraggableEventHandler =
+    (_evt, data) => {
+      if (dragging) {
+        const offset = hsplit ? data.x : data.y;
+        props.setDragging({ ...dragging, offset });
+      }
+    };
+
+  const onStop: DraggableEventHandler =
+    (evt, _data) => {
+      if (evt.metaKey || evt.altKey || evt.ctrlKey) {
+        props.resetPosition();
+      } else if (dragging) {
+        props.setPosition(getDragPosition(dragging));
+      }
+      props.setDragging(undefined);
+    };
+
+  const dragger = Utils.classes(
+    hsplit ? HGRAB : VGRAB,
+    dragging ? DRAGGING : DRAGZONE,
+  );
+
+  const css = hsplit ? HPOS : VPOS;
+
+  return (
+    <DraggableCore
+      handle={HANDLE}
+      onStart={onStart}
+      onDrag={onDrag}
+      onStop={onStop}
+    >
+      <div
+        className={css}
+        style={style}
+      >
+        <div className={dragger} />
+      </div>
+    </DraggableCore>
+  );
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Split Node                                                         --- */
+/* -------------------------------------------------------------------------- */
+
+interface CSplitterProps {
+  style: React.CSSProperties;
+  dragX: Dragging;
+  dragY: Dragging;
+  setDragX: (dx: Dragging) => void;
+  setDragY: (dy: Dragging) => void;
+  resetPosition: () => void;
+  setPosition: (X: number, Y: number) => void;
+}
+
+function CSplitter(props: CSplitterProps): JSX.Element {
+  const { style, dragX, dragY } = props;
+
+  const onStart: DraggableEventHandler =
+    (_evt, data) => {
+      const startX = data.node.offsetLeft;
+      const startY = data.node.offsetTop;
+      const anchorX = data.x;
+      const anchorY = data.y;
+      props.setDragX({ position: startX, offset: anchorX, anchor: anchorX });
+      props.setDragY({ position: startY, offset: anchorY, anchor: anchorY });
+    };
+
+  const onDrag: DraggableEventHandler =
+    (_evt, data) => {
+      if (dragX) props.setDragX({ ...dragX, offset: data.x });
+      if (dragY) props.setDragY({ ...dragY, offset: data.y });
+    };
+
+  const onStop: DraggableEventHandler =
+    (evt, _data) => {
+      if (evt.metaKey || evt.altKey || evt.ctrlKey) {
+        props.resetPosition();
+      } else if (dragX && dragY) {
+        const X = getDragPosition(dragX);
+        const Y = getDragPosition(dragY);
+        props.setPosition(X, Y);
+      }
+      props.setDragX(undefined);
+      props.setDragY(undefined);
+    };
+
+  const dragging = dragX !== undefined && dragY !== undefined;
+  const dragger = Utils.classes(HVGRAB, dragging ? DRAGGING : DRAGZONE);
+  return (
+    <DraggableCore
+      handle={HANDLE}
+      onStart={onStart}
+      onDrag={onDrag}
+      onStop={onStop}
+    >
+      <div
+        className={HVPOS}
+        style={style}
+      >
+        <div className={dragger} />
+      </div>
+    </DraggableCore>
+  );
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Q-Split Engine                                                     --- */
+/* -------------------------------------------------------------------------- */
+
+type QSplitLayout = Map<string, React.CSSProperties>;
+const QSplitContext = React.createContext<QSplitLayout>(new Map());
+const NODISPLAY: React.CSSProperties = { display: 'none' };
+
+const HSPLIT = (
+  left: number,
+  top: number,
+  height: number,
+): React.CSSProperties => ({ display: 'block', left, top, height });
+
+const VSPLIT = (
+  left: number,
+  top: number,
+  width: number,
+): React.CSSProperties => ({ display: 'block', left, top, width });
+
+const DISPLAY = (
+  layout: QSplitLayout,
+  id: string | undefined,
+  left: number,
+  width: number,
+  top: number,
+  height: number,
+): void => {
+  if (id) layout.set(id, { display: 'block', left, width, top, height });
+};
+
+interface QSplitEngineProps extends QSplitProps { size: Size }
+
+const inRange = (P: number, D: number): number => Math.max(0, Math.min(P, D));
+
+const getRatio = (P: number, D: number): number => inRange(P, D) / D;
+
+const getPosition = (d: Dragging, D: number, R: number): number =>
+  d ? inRange(getDragPosition(d), D) : Math.round(D * R);
+
+type Pid = string | undefined;
+type Sid = string | undefined | null; // null means Top
+
+const sameOf = (P: Pid, Q: Pid): Pid => {
+  if (P === Q) return P;
+  if (!P) return Q;
+  if (!Q) return P;
+  return undefined;
+};
+
+const merge = (U: Sid, V: Sid): Sid => {
+  if (U === V) return U;
+  if (U === undefined) return V;
+  if (V === undefined) return U;
+  return null;
+};
+
+const fullOf = (A: Pid, B: Pid, C: Pid, D: Pid): Pid => {
+  const S = merge(A, merge(B, merge(C, D)));
+  return (S === null ? undefined : S);
+};
+
+function QSplitEngine(props: QSplitEngineProps): JSX.Element {
+  const [dragX, setDragX] = React.useState<Dragging>();
+  const [dragY, setDragY] = React.useState<Dragging>();
+  const layout: QSplitLayout = new Map();
+  let hsplit: React.CSSProperties = NODISPLAY;
+  let vsplit: React.CSSProperties = NODISPLAY;
+  let hvsplit: React.CSSProperties = NODISPLAY;
+  const { A, B, C, D, H = 0.5, V = 0.5, size, setPosition } = props;
+  const { width, height } = size;
+  const setX = React.useCallback((X: number) => {
+    if (setPosition) setPosition(getRatio(X, width), V);
+  }, [setPosition, width, V]);
+  const setY = React.useCallback((Y: number) => {
+    if (setPosition) setPosition(H, getRatio(Y, height));
+  }, [setPosition, height, H]);
+  const setXY = React.useCallback((X: number, Y: number) => {
+    if (setPosition) setPosition(getRatio(X, width), getRatio(Y, height));
+  }, [setPosition, width, height]);
+  const resetX = React.useCallback(() => {
+    if (setPosition) setPosition(0.5, V);
+  }, [setPosition, V]);
+  const resetY = React.useCallback(() => {
+    if (setPosition) setPosition(H, 0.5);
+  }, [setPosition, H]);
+  const resetXY = React.useCallback(() => {
+    if (setPosition) setPosition(0.5, 0.5);
+  }, [setPosition]);
+  const X = getPosition(dragX, width, H);
+  const Y = getPosition(dragY, height, V);
+  const RX = width - X - 1;
+  const RY = height - Y - 1;
+  const AB = sameOf(A, B);
+  const AC = sameOf(A, C);
+  const BD = sameOf(B, D);
+  const CD = sameOf(C, D);
+  const ABCD = fullOf(A, B, C, D);
+  //----------------------------------------
+  // [ A ]
+  //---------------------------------------
+  if (ABCD) {
+    DISPLAY(layout, ABCD, 0, width, 0, height);
+  }
+  //----------------------------------------
+  // [ A - C ]
+  //---------------------------------------
+  else if (AB && CD) {
+    vsplit = VSPLIT(0, Y, width);
+    DISPLAY(layout, AB, 0, width, 0, Y);
+    DISPLAY(layout, CD, 0, width, Y + 1, RY);
+  }
+  //----------------------------------------
+  // [ A | B ]
+  //---------------------------------------
+  else if (AC && BD) {
+    hsplit = HSPLIT(X, 0, height);
+    DISPLAY(layout, AC, 0, X, 0, height);
+    DISPLAY(layout, BD, X + 1, RX, 0, height);
+  }
+  //----------------------------------------
+  // [ A – C|D ]
+  //----------------------------------------
+  else if (AB) {
+    hsplit = HSPLIT(X, Y, RY);
+    vsplit = VSPLIT(0, Y, width);
+    DISPLAY(layout, AB, 0, width, 0, Y);
+    DISPLAY(layout, C, 0, X, Y + 1, RY);
+    DISPLAY(layout, D, X + 1, RX, Y + 1, RY);
+  }
+  //----------------------------------------
+  // [ A | B-D ]
+  //----------------------------------------
+  else if (AC) {
+    hsplit = HSPLIT(X, 0, height);
+    vsplit = VSPLIT(X, Y, RY);
+    DISPLAY(layout, AC, 0, X, 0, height);
+    DISPLAY(layout, B, X + 1, RX, 0, Y);
+    DISPLAY(layout, D, X + 1, RX, Y + 1, RY);
+  }
+  //----------------------------------------
+  // [ A-C | B ]
+  //----------------------------------------
+  else if (BD) {
+    hsplit = HSPLIT(X, 0, height);
+    vsplit = VSPLIT(0, Y, X);
+    DISPLAY(layout, A, 0, X, 0, Y);
+    DISPLAY(layout, BD, X + 1, RX, 0, height);
+    DISPLAY(layout, C, 0, X, Y + 1, RY);
+  }
+  //----------------------------------------
+  // [ A|B - C ]
+  //----------------------------------------
+  else if (CD) {
+    hsplit = HSPLIT(X, 0, Y);
+    vsplit = VSPLIT(0, Y, width);
+    DISPLAY(layout, A, 0, X, 0, Y);
+    DISPLAY(layout, B, X + 1, RX, 0, Y);
+    DISPLAY(layout, CD, 0, width, Y + 1, RY);
+  }
+  //----------------------------------------
+  // [ A, B, C, D ]
+  //----------------------------------------
+  else {
+    hsplit = HSPLIT(X, 0, height);
+    vsplit = VSPLIT(0, Y, width);
+    DISPLAY(layout, A, 0, X, 0, Y);
+    DISPLAY(layout, B, X + 1, RX, 0, Y);
+    DISPLAY(layout, C, 0, X, Y + 1, RY);
+    DISPLAY(layout, D, X + 1, RX, Y + 1, RY);
+  }
+  //----------------------------------------
+  if (hsplit !== NODISPLAY && vsplit !== NODISPLAY)
+    hvsplit = { display: 'block', left: X, top: Y };
+  //----------------------------------------
+  // Rendering
+  //----------------------------------------
+  return (
+    <QSplitContext.Provider value={layout}>
+      <BSplitter
+        key='HSPLIT'
+        hsplit={true}
+        dragging={dragX}
+        setDragging={setDragX}
+        setPosition={setX}
+        resetPosition={resetX}
+        style={hsplit}
+      />
+      <BSplitter
+        key='VSPLIT'
+        hsplit={false}
+        dragging={dragY}
+        setDragging={setDragY}
+        setPosition={setY}
+        resetPosition={resetY}
+        style={vsplit}
+      />
+      <CSplitter
+        key='HVSPLIT'
+        dragX={dragX}
+        dragY={dragY}
+        setDragX={setDragX}
+        setDragY={setDragY}
+        setPosition={setXY}
+        resetPosition={resetXY}
+        style={hvsplit}
+      />
+      {props.children}
+    </QSplitContext.Provider>
+  );
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Q-Split                                                            --- */
+/* -------------------------------------------------------------------------- */
+
+/** Q-Spliiter Container.
+
+   The contained is divided into four quarters named `A`, `B`, `C` and `D`
+   with the following layout:
+
+   ```
+     A | B
+     -----
+     C | D
+   ```
+
+   The horizontal and vertical split bars can be dragged to adjust the ratios.
+   The central node can also be dragged to adust both ratios.
+
+   Any adjacent quarters collapse when they contain either the same component
+   or one component and `undefined`. The split bars are erased accordingly.
+
+   When all quarters contain the same component or `undefined`, they all
+   collapse and the only component extends to the full container size.
+
+   Other cases are a bit degenerated and lead to « incomplete » layout.
+   For instance, when a given component is positionned into two diagonal
+   corners but the adjacent quarters can not collapse,
+   it will be positionned into only one quarter.
+ */
+export function QSplit(props: QSplitProps): JSX.Element {
+  const CONTAINER = Utils.classes('dome-xSplitter-container', props.className);
+  return (
+    <div className={CONTAINER} style={props.style}>
+      <AutoSizer>
+        {(size: Size) => (
+          <QSplitEngine size={size} {...props} />
+        )}
+      </AutoSizer>
+    </div>
+  );
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Q-Pane                                                             --- */
+/* -------------------------------------------------------------------------- */
+
+export interface QPaneProps {
+  id: string; /** Q-Pane Identifer. */
+  className?: string; /** Additional class of the Q-Pane div. */
+  style?: React.CSSProperties; /** Additional style of the Q-Pane div. */
+  children?: React.ReactNode; /** Q-Pane contents. */
+}
+
+/**
+   Q-Splitter Components.
+
+   Childrens are rendered in a positionned `<div/>` with absolute coordinates.
+ */
+export function QPane(props: QPaneProps): JSX.Element {
+  const layout = React.useContext(QSplitContext);
+  const QPANE = Utils.classes('dome-xSplitter-pane', props.className);
+  const QSTYLE = Utils.styles(props.style, layout?.get(props.id) ?? NODISPLAY);
+  return <div className={QPANE} style={QSTYLE}>{props.children}</div>;
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/renderer/layout/splitters.tsx b/ivette/src/dome/renderer/layout/splitters.tsx
index e56f47dd345caba272617b45e6cb2071ffaf9edc..032766f6f2e931d3f38cafc2b0849289bd38350c 100644
--- a/ivette/src/dome/renderer/layout/splitters.tsx
+++ b/ivette/src/dome/renderer/layout/splitters.tsx
@@ -274,13 +274,13 @@ function SplitterEngine(props: SplitterEngineProps): JSX.Element {
         {A}
       </div>
       <DraggableCore
+        key="split"
         handle={HANDLE}
         onStart={onStart}
         onDrag={onDrag}
         onStop={onStop}
       >
         <div
-          key="split"
           className={css.split}
           style={styleR}
         >
diff --git a/ivette/src/dome/renderer/layout/style.css b/ivette/src/dome/renderer/layout/style.css
index 4d6125bb7a614057aae47918d3f1ec2fde06cd41..a2e0fca13db2095fde266199646103b6c8264367 100644
--- a/ivette/src/dome/renderer/layout/style.css
+++ b/ivette/src/dome/renderer/layout/style.css
@@ -177,6 +177,17 @@
     cursor: row-resize ;
 }
 
+.dome-xSplitter-hvgrab {
+    position: relative;
+    z-index: 2 ;
+    top: -4px ;
+    left: -4px ;
+    height: 9px;
+    width: 9px;
+    border-radius: 4px;
+    cursor: move;
+}
+
 .dome-xSplitter-hpos-A { position: absolute; left: 0px; height: 100% }
 .dome-xSplitter-hpos-R { position: absolute; width: 1px; height: 100% }
 .dome-xSplitter-hpos-B { position: absolute; right: 0px; height: 100% }
@@ -184,6 +195,7 @@
 .dome-xSplitter-vpos-A { position: absolute; top: 0px; width: 100% }
 .dome-xSplitter-vpos-R { position: absolute; height: 1px; width: 100% }
 .dome-xSplitter-vpos-B { position: absolute; bottom: 0px; width: 100% }
+.dome-xSplitter-hvpos { position: absolute; width: 1px; height: 1px }
 
 .dome-xSplitter-hline,
 .dome-xSplitter-vline
@@ -191,6 +203,12 @@
     background: var(--splitter) ;
 }
 
+.dome-xSplitter-pane
+{
+    position: absolute;
+    overflow: hidden;
+}
+
 /* -------------------------------------------------------------------------- */
 /* --- GridLayout Styles                                                  --- */
 /* -------------------------------------------------------------------------- */
diff --git a/ivette/src/ivette/index.tsx b/ivette/src/ivette/index.tsx
index 6916d0be581172c2799d10c5964d4648934bef7d..9e4a8535bd6f2b56ed3768c854cd75beec1107e4 100644
--- a/ivette/src/ivette/index.tsx
+++ b/ivette/src/ivette/index.tsx
@@ -36,7 +36,6 @@ import { DefineElement } from 'dome/layout/dispatch';
 import { GridItem, GridHbox, GridVbox } from 'dome/layout/grids';
 import * as Lab from 'ivette@lab';
 import * as Ext from 'ivette@ext';
-import Sandbox from './sandbox';
 
 /* --------------------------------------------------------------------------*/
 /* --- Items                                                              ---*/
@@ -205,12 +204,22 @@ export function registerStatusbar(status: ToolProps): void {
 /* --------------------------------------------------------------------------*/
 
 if (DEVEL) {
-  registerComponent({
-    id: 'ivette.sandbox',
+  registerGroup({
+    id: 'sandbox',
     label: 'Sandbox',
-    title: 'Ivette Sandbox Component (only in DEVEL mode)',
-    children: <Sandbox />,
+    title: 'Ivette Sandbox Components (only in DEVEL mode)',
   });
+  registerView({
+    id: 'sandbox',
+    rank: -2,
+    label: 'Sandbox',
+    title: 'Sandbox Playground (only in DEVEL mode)',
+    layout: [],
+  });
+}
+
+export function registerSandbox(props: ComponentProps): void {
+  if (DEVEL) registerComponent({ ...props, group: 'sandbox' });
 }
 
 // --------------------------------------------------------------------------
diff --git a/ivette/src/ivette/sandbox.tsx b/ivette/src/ivette/sandbox.tsx
deleted file mode 100644
index 23d89ccb4c5446d88ed9f262eddd8f47c72c74a9..0000000000000000000000000000000000000000
--- a/ivette/src/ivette/sandbox.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/* ************************************************************************ */
-/*                                                                          */
-/*   This file is part of Frama-C.                                          */
-/*                                                                          */
-/*   Copyright (C) 2007-2022                                                */
-/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
-/*          alternatives)                                                   */
-/*                                                                          */
-/*   you can redistribute it and/or modify it under the terms of the GNU    */
-/*   Lesser General Public License as published by the Free Software        */
-/*   Foundation, version 2.1.                                               */
-/*                                                                          */
-/*   It is distributed in the hope that it will be useful,                  */
-/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
-/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
-/*   GNU Lesser General Public License for more details.                    */
-/*                                                                          */
-/*   See the GNU Lesser General Public License version 2.1                  */
-/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
-/*                                                                          */
-/* ************************************************************************ */
-
-/* -------------------------------------------------------------------------- */
-/* --- Sandbox Ivette Component.                                          --- */
-/* --- Only appears in DEVEL mode.                                        --- */
-/* --- Please, keep it empty.                                             --- */
-/* -------------------------------------------------------------------------- */
-
-import React from 'react';
-import { Label } from 'dome/controls/labels';
-
-export default function Sandbox(): JSX.Element {
-  return <Label>Hello World!</Label>;
-}
diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx
index 42bfd85a9f252bb9d1ef723bcf49450eadd3d7f0..95835862a8eeb3dbb2c3caa42dcfe8ff35f6ddff 100644
--- a/ivette/src/renderer/Application.tsx
+++ b/ivette/src/renderer/Application.tsx
@@ -37,6 +37,7 @@ import * as Extensions from './Extensions';
 import * as Laboratory from './Laboratory';
 import * as IvettePrefs from 'ivette/prefs';
 import './loader';
+import './sandbox';
 
 // --------------------------------------------------------------------------
 // --- Main View
diff --git a/ivette/src/sandbox/README.md b/ivette/src/sandbox/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f9dcad96ad7f92f27f19b0022cb3065a0ddc34db
--- /dev/null
+++ b/ivette/src/sandbox/README.md
@@ -0,0 +1,19 @@
+# Sandboxed Components
+
+This directory is for Sandboxed components.
+Sandboxed components are only visible in DEV mode (make dev).
+
+The playground view « Sandbox » is also only visible in DEV mode and can be used to
+play with sandboxed components _or_ any other component of the platform.
+
+All files with `*.tsx` extension inside this directory will be automatically loaded
+and shall register sandboxe(s) by using typically:
+
+    Ivette.registerSandbox({
+        id: 'sandbox.<ident>',
+        label: 'My New Feature',
+        title: 'Testing this new amazing feature',
+        chidren: <MyNewFeature />,
+    })
+
+Enjoy Sandboxing !
diff --git a/ivette/src/sandbox/qsplit.tsx b/ivette/src/sandbox/qsplit.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3aaae9b5ef12a25cfa8d18f704b174eef5d838ab
--- /dev/null
+++ b/ivette/src/sandbox/qsplit.tsx
@@ -0,0 +1,128 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2022                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/* -------------------------------------------------------------------------- */
+/* --- Sandbox Ivette Component.                                          --- */
+/* --- Only appears in DEVEL mode.                                        --- */
+/* --- Please, keep it empty.                                             --- */
+/* -------------------------------------------------------------------------- */
+
+import React from 'react';
+import * as Ctrl from 'dome/controls/buttons';
+import * as Disp from 'dome/controls/displays';
+import * as Box from 'dome/layout/boxes';
+import { QSplit, QPane } from 'dome/layout/qsplit';
+import { registerSandbox } from 'ivette';
+
+function Quarter(props: {
+  value?: string,
+  setValue: (v: string | undefined) => void,
+}): JSX.Element {
+  const onChange = (s?: string): void => props.setValue(s ? s : undefined);
+  return (
+    <Ctrl.Select value={props.value ?? ''} onChange={onChange}>
+      <option value=''>-</option>
+      <option value='A'>A</option>
+      <option value='B'>B</option>
+      <option value='C'>C</option>
+      <option value='D'>D</option>
+      <option value='E'>E</option>
+    </Ctrl.Select>
+  );
+}
+
+function Pane(props: { id: string, background: string }): JSX.Element {
+  const { id, background } = props;
+  const css: React.CSSProperties = {
+    width: '100%',
+    height: '100%',
+    textAlign: 'center',
+    background,
+  };
+  return (
+    <QPane id={id}><div style={css}>{id}</div></QPane>
+  );
+}
+
+const round = (r: number): number => Math.round(r * 100) / 100;
+
+function QSplitSandbox(): JSX.Element {
+  const [H, setH] = React.useState(0.5);
+  const [V, setV] = React.useState(0.5);
+  const [A, setA] = React.useState<string | undefined>('A');
+  const [B, setB] = React.useState<string | undefined>('B');
+  const [C, setC] = React.useState<string | undefined>('C');
+  const [D, setD] = React.useState<string | undefined>('D');
+  const setPosition = React.useCallback((h, v) => {
+    setH(h);
+    setV(v);
+  }, [setH, setV]);
+  const reset = (): void => {
+    setPosition(0.5, 0.5);
+    setA('A');
+    setB('B');
+    setC('C');
+    setD('D');
+  };
+  const clear = (): void => {
+    setPosition(0.5, 0.5);
+    setA(undefined);
+    setB(undefined);
+    setC(undefined);
+    setD(undefined);
+  };
+  return (
+    <Box.Vfill>
+      <Box.Hfill>
+        <Ctrl.Button icon='RELOAD' label='Reset' onClick={reset} />
+        <Ctrl.Button icon='TRASH' label='Clear' onClick={clear} />
+        <Box.Space />
+        <Disp.LCD>H={round(H)} V={round(V)}</Disp.LCD>
+        <Box.Space />
+        <Quarter value={A} setValue={setA} />
+        <Quarter value={B} setValue={setB} />
+        <Quarter value={C} setValue={setC} />
+        <Quarter value={D} setValue={setD} />
+      </Box.Hfill>
+      <QSplit A={A} B={B} C={C} D={D} H={H} V={V}
+        setPosition={setPosition}>
+        <Pane id='A' background='lightblue' />
+        <Pane id='B' background='lightgreen' />
+        <Pane id='C' background='#8282db' />
+        <Pane id='D' background='coral' />
+        <Pane id='E' background='red' />
+      </QSplit>
+    </Box.Vfill >
+  );
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Sandbox                                                            --- */
+/* -------------------------------------------------------------------------- */
+
+registerSandbox({
+  id: 'sandbox.qsplit',
+  label: 'Q-Splitters',
+  children: <QSplitSandbox />,
+});
+
+/* -------------------------------------------------------------------------- */