JSFiddle - React, Tailwind, and code Playground

DPadComponentMockup

by Retsam19

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div class="main">
  <!-- ko template: {
    name: "layout",
    data: main
  } -->
  <!-- /ko -->
</div>

<template id="layout">
  <div class="layout" data-bind="
    css: directionStyle
  ">
    <!-- ko template: {
      name: 'layout-cell',
      foreach: cells
    } -->
    <!-- /ko -->
    <!--<div class="overlay"></div>-->
  </div>
</template>

<template id="layout-cell">
  <div class="layout-cell" data-bind="
    css: {
       'stretch': !isFixed
    },
    style: {
        flexBasis: flexBasis
    }
  ">
    <!-- ko if: !isFixed -->
      <!-- ko if: content -->
      
        <!-- ko template: {
          name: 'layout',
          data: content
        } -->
        <!-- /ko -->
      <!-- /ko -->
      
      <!-- ko ifnot: content -->
        <!-- ko template: {
          name: 'dpad',
          data: dpad
        } -->
        <!-- /ko -->
      <!-- /ko -->      
    <!-- /ko -->
  </div>
</template>

<template id="dpad">
  <div class="dpad">
    <div class="quarter quarter1" data-bind="
      click: clickHandler('up')
    "></div>
    <div class="quarter quarter2" data-bind="
      click: clickHandler('right')
    "></div> 
    <div class="quarter quarter3" data-bind="
      click: clickHandler('left')
    "></div>
    <div class="quarter quarter4" data-bind="
      click: clickHandler('down')
    "></div>
    <div class="cutout"></div>
  </div>
</template>

SCSS

html, body {
  height: 100%
}

.main {
  display: flex;
  height: 100%;
}

.layout {
  /* position: relative; */
  flex-grow: 1;
  border: 1px solid black;
  
  display: flex;
}

.layout-column {
  flex-direction: column; 
}
.layout-row {
  flex-direction: row;
}

.layout-cell {
  display: flex;
  
  background-color: rgba(0, 0, 255, 10%);
  outline: 1px solid green;
  
  flex-grow: 0;
  &.stretch {
    flex-grow: 1;
  }
}
/* 
.overlay {
  position: absolute;
  left: 0; right: 0; top: 0; bottom: 0;
  display: flex;
  align-items: center;
  background-color: rgba(255, 0, 0, 30%);
} */


/**
* Quarter Circles
*/

.dpad {
  align-self: center;
  
	position:relative;
	width:100px;
	height:100px;
	margin:0 auto;
}

.quarter {
	position:absolute;
	width:50%;
	height:50%;
	transform: rotate(45deg);
  cursor: pointer;
}
.quarter:hover {
	background-color:pink;
}
.quarter1 {
	top:0;
	left:0;
	background-color: #CCC;
	border-radius:100% 0 0 0;
	transform-origin: bottom right;
}
.quarter2 {
	top:0;
	right:0;
	background-color: #DDD;
	border-radius:0 100% 0 0;
	transform-origin: bottom left;
}
.quarter3 {
	bottom:0;
	left:0;
	background-color: #DDD;
	border-radius:0 0 0 100%;
	transform-origin: top right;
}
.quarter4 {
	bottom:0;
	right:0;
	background-color: #CCC;
	border-radius:0 0 100% 0 ;
	transform-origin: top left;
}
.cutout {
	width:50%;
	height:50%;
	background-color:white;
	position:absolute;
	top:25%;
	left:25%;
	border-radius:50%;
	/* pointer-events:none; */
  cursor: default;
}

Babel + JSX

const ROW = "row";
const COL = "column";

class LayoutContainer {
  constructor (data) {
    this.direction = data.direction;
    console.log(`Cosnstructed new ${this.direction} layout`)
    
    this.directionStyle = `layout-${this.direction}`;
  
  	const makeChild = (data) => new LayoutCell(data, addCell);
    
    const addCell = (sourceCell, direction) => {
      console.log(`${direction} clicked`);
      
      if(this.direction === ROW && (direction === "up"   || direction === "down") ||
         this.direction === COL && (direction === "left" || direction === "right")) {
         const fixedCellFirst = direction === "up" || direction === "left";
         const newLayoutData = {
            direction: this.direction === ROW ? COL : ROW,
            cells: [{
              isFixed: fixedCellFirst
            }, {
            	isFixed: !fixedCellFirst
            }]
         };
         
         sourceCell.content(new LayoutContainer(newLayoutData));
      } else {
        console.log(`Adding sibling ${direction}`)
        const sourceCellIndex = this.cells().indexOf(sourceCell);
        //Adding a sibling to the source cell
        const newCellIndex = (direction === "up" || direction === "left") ?
           sourceCellIndex :    //Add before
           sourceCellIndex + 1; //Add after
        this.cells.splice(newCellIndex, 0, makeChild({isFixed: true}))
      }
    }
  
    this.cells = ko.observableArray(data.cells.map(makeChild));
  }
}

class LayoutCell {
  constructor(data, addCell) {
    this.isFixed = data.isFixed;
    this.content = ko.observable(data.content && new LayoutContainer(data.content));
    
    this.flexBasis = this.isFixed ? "100px" : "fill";
    
    this.dpad = {
      clickHandler: (direction) => () => {
      	addCell(this, direction)
      }
    };
  }
}


const holyGrailData = {
  direction: COL,
  cells: [{
    isFixed: true
  }, {
    isFixed: false,
    content: {
      direction: ROW,
    	cells: [{
				isFixed: true
     ...