<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Protoserve</title>
        <link rel="icon" type="image/png" href="static/favicon-512.png">
        <link rel="apple-touch-icon" href="static/favicon-512.png">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
    --u-display-metric: default;
    --u-display-us: none;
}

html, body {
    margin: 0;
    width: 100%;
    height: 100%;
    font-family: Helvetica, Segoe UI, Sans-Serif;
}

body {
    display: grid;
    grid-template-columns: clamp(200px, 500px, 50vw) 1fr;
    grid-template-rows: 1fr 0fr;
    grid-template-areas: "controls main"
                         "links main";

}

#controls {
    grid-area: controls;
    user-select: none;
    display: grid;
    grid-template-columns: 10fr 1fr 1fr;
    align-content: start;
    overflow-y: scroll;
}

label {
    grid-column-start: 1;
    grid-column-end: span 3;
    display: grid;
    grid-template-columns: subgrid;
    text-align: right;
}

input[type="checkbox"] {
    justify-self: start;
}

input {
    align-self: start;
}

.group > h4, .group > h5 {
    text-align: center;
    margin: 5px;
    grid-column-start: 1;
    grid-column-end: span 3;
}

.expand > :first-child {
    grid-column-start: 1;
    grid-column-end: span 3;
}

.group, .field, .expand {
    grid-column-start: 1;
    grid-column-end: span 3;
    display: grid;
    grid-template-columns: subgrid;
    align-content: start;
}

.group {
    background-color: hsl(.0turn 50% 10% / 4%);
    padding: 10px;
    border-radius: 10px;
    box-shadow: 0 0 8px 0px hsl(0 0% 0% / 20%);
    margin: 10px 0 10px 0;
}

.expand > .field:first-child {
    display: grid;
    grid-template-columns: 1fr 100fr 1fr;
    align-items: center;
    text-align: left;
    width: 100%;
    position: relative;
}

.group > .content {
    grid-column-start: 1;
    grid-column-end: span 3;
    display: flex;
    flex-direction: column;
    text-align: center;
}

.group > .attribution, .group > .usage {
    grid-column-start: 1;
    grid-column-end: span 3;
}

.group > div > .proportion {
    display: none;
}

.proportional > div > .proportion {
    display: grid;
}

.split-sides .double-sided-only {
    display: none;
}

.split-sides > .placeholder .area-controls {
    display: none;
}

.board > .placeholder > .area-controls {
    display: none;
}

.area-controls .area-move {
    display: none;
}

.area-controls .area-move::before {
    content: "/";
    padding: 0 5px 0 5px;
}

.group.proportional > .group > .area-controls .area-move {
    display: block;
}

.content.area-controls {
    flex-direction: row;
    justify-content: center;
}

.field > input, .field > select { max-width: 5em;
    text-align: right;
    margin: 0 5px 0 5px;
}

input[type="text"]:invalid {
    background: rgba(255 0 0 / 30%);
}

input[type="text"]:focus:valid {
    background: rgba(0 192 64 / 30%);
}

.group.expand {
    border-radius: 0;
}

.expand > :first-child {
    font-weight: bold;
}

.expand.collapsed > :nth-child(n+2) {
    display: none;
}

.unit.metric {
    display: var(--u-display-metric);
}

.unit.us {
    display: var(--u-display-us);
}

#preview {
    position: relative;
    grid-area: main;
    padding: 20px;
}

#preview-image {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

#preview-message {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: hsla(0 0% 50% / 30%);
    display: none;
    justify-content: center;
    align-items: center;
    font-size: 18pt;
    font-weight: bold;
    color: white;
}

#preview-message.loading {
    display: flex;
}

#links {
    grid-area: links;
    display: flex;
    justify-content: center;
    padding: 5px;
}

#link-gerbers {
    background-color: #0d6efd;
    color: white;
    font-weight: bold;
    box-shadow: 0px 0px 1px 1px hsl(0, 0%, 0% / 20%);
    border-radius: .5em;
    padding: 1em 2em 1em 2em;
}

.layout-area {
    grid-column-start: 1;
    grid-column-end: span 3;
}

.drop-target {
    grid-column-start: 1;
    grid-column-end: span 3;
    text-align: center;
    display: none;
}

.group.drop-enabled > .drop-target {
    display: block;
}

.placeholder hr {
    width: 3em;
    border: none;
    border-top: 1px solid hsl(0 0% 60%);
}

#controls.move-in-progress input {
    background-color: hsl(0 0% 85%);
}

#controls.move-in-progress {
    color: hsl(0 0% 60%);
}

.narrow-only {
    display: none;
}

</style>
    </head>
    <body>
        <div id="controls">
            <form>
                <div class="group board">
                    <h4>Board settings</h4>
                    <label>Units
                        <select name='units' value="metric">
                            <option value="metric">Metric</option>
                            <option value="us">US Customary</option>
                        </select>
                    </label>

                    <label>Board width
                        <input name="width" type="text" placeholder="width" value="100" pattern="[0-9]+\.?[0-9]*"/>
                        <span class="unit metric">mm</span>
                        <span class="unit us">inch</span>
                    </label>

                    <label>Board height
                        <input name="height" type="text" placeholder="height" value="80" pattern="[0-9]+\.?[0-9]*"/>
                        <span class="unit metric">mm</span>
                        <span class="unit us">inch</span>
                    </label>

                    <div class="group expand" data-group="round_corners">
                        <label>Round corners
                            <input name="enabled" type="checkbox" checked/>
                        </label>

                        <label>Radius
                            <input name="radius" type="text" placeholder="radius" value="1.5" pattern="[0-9]+\.?[0-9]*"/>
                            <span class="unit metric">mm</span> 
                            <span class="unit us">inch</span> 
                        </label>
                    </div>

                    <div class="group expand" data-group="mounting_holes">
                        <label>Mounting holes
                            <input name="enabled" type="checkbox" name="has_holes" checked/>
                        </label>

                        <label>Diameter
                            <input type="text" placeholder="diameter" name="diameter" value="3.2" pattern="[0-9]+\.?[0-9]*"/>
                            <span class="unit metric">mm</span> 
                            <span class="unit us">inch</span> 
                        </label>

                        <label>Board edge to hole center
                            <input type="text" placeholder="distance" name="offset" value="5" pattern="[0-9]+\.?[0-9]*"/>
                            <span class="unit metric">mm</span> 
                            <span class="unit us">inch</span> 
                        </label>
                    </div>

                    <h4>Content</h4>
                    <div class="group placeholder"></div>
                </div>
            </form>
        </div>
        <div id="preview">
            <img id="preview-image" alt="Automatically generated preview image"/>
            <div id="preview-message"></div>
        </div>
        <div id="links">
            <a class="narrow-only" href="#controls">Settings</a>
            <a class="narrow-only" href="#preview">Preview</a>
            <a id="link-gerbers" href='#'>
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em">
                    <title>Download</title>
                    <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/>
                </svg>
                Gerbers
            </a>
        </div>

        <template id="tpl-drop-target">
            <a class="drop-target" href="#">
                <svg viewBox="0 0 532 532" width="1em" xmlns="http://www.w3.org/2000/svg">
                    <title>Move here</title>
                    <path id="path2" d="m 424.025,300.075 c 17.7,0 32,-14.3 32,-32 0,-17.7 -14.3,-32 -32,-32 h -82.7 l 181.3,-181.4 c 12.5,-12.5 12.5,-32.8 0,-45.3 -12.5,-12.5 -32.8,-12.5 -45.3,0 l -181.3,181.4 v -82.7 c 0,-17.7 -14.3,-32 -32,-32 -17.7,0 -32,14.3 -32,32 v 160 c 0,17.7 14.3,32 32,32 z M 80,52 C 35.8,52 0,87.8 0,132 v 320 c 0,44.2 35.8,80 80,80 h 320 c 44.2,0 80,-35.8 80,-80 v -72 c 0,-17.7 -14.3,-32 -32,-32 -17.7,0 -32,14.3 -32,32 v 72 c 0,8.8 -7.2,16 -16,16 H 80 c -8.8,0 -16,-7.2 -16,-16 V 132 c 0,-8.8 7.2,-16 16,-16 h 72 c 17.7,0 32,-14.3 32,-32 0,-17.7 -14.3,-32 -32,-32 z" />
                </svg>
            </a>
        </template>

        <template id="tpl-g-layout">
            <div data-type="layout" class="group proportional">
                <h4>Proportional Layout</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Layout settings</h4>
                <label>Direction
                    <select name="direction" value="horizontal">
                        <option value="h">horizontal</option>
                        <option value="v">vertical</option>
                    </select>
                </label>

                <h5>Content</h4>
                <div class="drop-target"></div>
                <div class="placeholder"></div>
                <div class="drop-target"></div>
                <div class="placeholder"></div>
                <div class="drop-target"></div>
                <a class="content add-element" href="#">Add element</a>
            </div>
        </template>

        <template id="tpl-g-twoside">
            <div data-type='twoside' class="group split-sides">
                <h4>Split front and back</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Front</h5>
                <div class="placeholder"></div>
                <h5>Back</h5>
                <div class="placeholder"></div>
            </div>
        </template> 

        <template id="tpl-g-placeholder">
            <div data-type="placeholder" class="group placeholder">
                <h4>Empty area</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <div class="content">
                    <a href="#" data-placeholder="layout">Create Layout</a>
                    <a href="#" data-placeholder="twoside" class="double-sided-only">Split front and back</a>
                    <hr/>
                    <a href="#" data-placeholder="smd">SMD area</a>
                    <a href="#" data-placeholder="tht" class="double-sided-only">THT area</a>
                    <a href="#" data-placeholder="manhattan">Manhattan area</a>
                    <a href="#" data-placeholder="flower" class="double-sided-only">THT Flower area</a>
                    <a href="#" data-placeholder="powered" class="double-sided-only">Powered THT area</a>
                    <a href="#" data-placeholder="rf" class="double-sided-only">RF THT area</a>
                    <a href="#" data-placeholder="spiky" class="double-sided-only">Spiky hybrid area</a>
                    <a href="#" data-placeholder="alio" class="double-sided-only">ALio hybrid area</a>
                </div>
            </div>
        </template>

        <template id="tpl-g-smd">
            <div data-type="smd" class="group smd">
                <h4>SMD area</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch X
                    <input type="text" name="pitch_x" placeholder="length" value="1.27" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Pitch Y
                    <input type="text" name="pitch_y" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.3" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Pad shape
                    <select name="pad_shape" value="rect">
                        <option value="rect">(Rounded) Rectangle</option>
                        <option value="circle">Circle</option>
                    </select>
                </label>
                <label class="only-shape rect">Corner radius
                    <input type="text" name="pad_h" placeholder="length" value="0" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <template id="tpl-g-tht">
            <div data-type="tht" class="group tht">
                <h4>THT area</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch X
                    <input type="text" name="pitch_x" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Pitch Y
                    <input type="text" name="pitch_y" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Plating
                    <select name="plating" value="through">
                        <option value="plated">Double-sided, through-plated</option>
                        <option value="nonplated">Double-sided, non-plated</option>
                        <option value="singleside">Single-sided, non-plated</option>
                    </select>
                </label>
                <label>Hole diameter
                    <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Pad shape
                    <select name="pad_shape" value="circle">
                        <option value="circle">Circle</option>
                        <option value="rect">(Rounded) Rectangle</option>
                        <option value="obround">Obround</option>
                    </select>
                </label>
                <label class="only-shape rect">Corner radius
                    <input type="text" name="pad_h" placeholder="length" value="0" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <template id="tpl-g-manhattan">
            <div data-type="manhattan" class="group manhattan">
                <h4>Manhattan area</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch X
                    <input type="text" name="pitch_x" placeholder="length" value="5.08" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Pitch Y
                    <input type="text" name="pitch_y" placeholder="length" value="5.08" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <template id="tpl-g-flower">
            <div data-type="flower" class="group flower">
                <h4>THT flower area</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch
                    <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Pattern diameter
                    <input type="text" name="pattern_dia" placeholder="length" value="2.0" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Hole diameter
                    <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <template id="tpl-g-powered">
            <div data-type="powered" class="group powered">
                <h4>Powered THT area</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch
                    <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Hole diameter
                    <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Via drill
                    <input type="text" name="via_hole_dia" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Via diameter
                    <input type="text" name="via_dia" placeholder="length" value="1.1" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Trace width
                    <input type="text" name="trace_width" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.2" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <template id="tpl-g-rf">
            <div data-type="rf" class="group rf">
                <h4>THT area with RF ground</h4>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch
                    <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Hole diameter
                    <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Trace width
                    <input type="text" name="trace_width" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Via diameter
                    <input type="text" name="via_dia" placeholder="length" value="0.8" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Via drill
                    <input type="text" name="via_hole_dia" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.3" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <template id="tpl-g-spiky">
            <div data-type="spiky" class="group spiky">
                <h4>Spiky hybrid area</h4>
                <div class="attribution">
                    Layout by <a href="https://social.treehouse.systems/@electronic_eel">electroniceel</a> (<a href="https://github.com/electroniceel/protoboard">github</a>)
                </div>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <div class="usage">This area has a fixed 100 mil / 2.54 mm pitch.</div>
            </div>
        </template>

        <template id="tpl-g-alio">
            <div data-type="alio" class="group alio">
                <h4>ALio hybrid area</h4>
                <div class="attribution">
                    Layout by arief ibrahim adha (<a href="https://hackaday.io/project/28570-alio-new-hardware-prototyping-platform">hackaday.io</a>).
                    Top and bottom have opposed orientation of the SMD pads. 
                </div>
                <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
                <label class="proportion">Proportion
                    <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
                </label>

                <h5>Area Settings</h5>
                <label>Pitch
                    <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Hole diameter
                    <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Link trace width
                    <input type="text" name="link_trace_width" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Via pad width
                    <input type="text" name="link_pad_width" placeholder="length" value="0.8" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Via drill
                    <input type="text" name="via_hole_dia" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
                <label>Clearance
                    <input type="text" name="clearance" placeholder="length" value="0.3" pattern="[0-9]+\.?[0-9]*"/>
                    <span class="unit metric">mm</span>
                    <span class="unit us">mil</span>
                </label>
            </div>
        </template>

        <script>
            document.querySelectorAll('.expand').forEach((elem) => {
                const checkbox = elem.querySelector(':first-child > input');
                checkbox.addEventListener("change", (evt) => {
                    if (evt.currentTarget.checked) {
                        elem.classList.remove('collapsed');
                    } else {
                        elem.classList.add('collapsed');
                    }
                });
                if (checkbox.checked) {
                    elem.classList.remove('collapsed');
                } else {
                    elem.classList.add('collapsed');
                }
            });

            let g_dropElement = null;

            function hookupAreaRemove(node) {
                for (const bt of node.querySelectorAll('a.area-remove')) {
                    bt.addEventListener('click', (evt) => {
                        let elem = evt.target.closest('.group');
                        if (elem.parentElement && elem.parentElement.matches('.proportional')) {
                            let sibling = elem.previousElementSibling;
                            if (sibling.matches('.drop-target')) {
                                sibling.remove();
                            }
                            elem.remove();

                        } else {
                            elem.replaceWith(createPlaceholder());
                        }

                        previewReloader.scheduleCall();
                    });
                }
            }

            function createDropTarget() {
                const node = document.querySelector('#tpl-drop-target').content.cloneNode(true);
                node.querySelector('a').addEventListener('click', (evt) => {
                    if (g_dropElement != null) {
                        const target = evt.target.closest('a');

                        let sibling = g_dropElement.previousElementSibling;
                        if (sibling.matches('.drop-target')) {
                            if (sibling == target) {
                                return;
                            }
                            sibling.remove();
                        }
                        g_dropElement.remove();
                        g_dropElement.querySelector('a.area-move').innerText = "Move";

                        target.before(sibling);
                        target.before(g_dropElement);

                        document.querySelector('#controls').classList.remove('move-in-progress');
                        document.querySelector('.group.drop-enabled').classList.remove('drop-enabled');
                        g_dropElement = null;

                        previewReloader.scheduleCall();
                    }
                });
                return node;
            }

            function hookupAreaMove(node) {
                for (const bt of node.querySelectorAll('a.area-move')) {
                    bt.addEventListener('click', (evt) => {
                        const controls = document.querySelector('#controls');
                        const group = evt.target.closest('.group');

                        if (g_dropElement == null) {
                            controls.classList.add('move-in-progress');
                            group.parentElement.classList.add('drop-enabled');
                            g_dropElement = group;
                            evt.target.innerText = "Cancel move";

                        } else {
                            controls.classList.remove('move-in-progress');
                            group.parentElement.classList.remove('drop-enabled');
                            g_dropElement = null;
                            evt.target.innerText = "Move";
                        }
                    });
                }
            }

            function hookupPreviewUpdate(node) {
                for (const elem of node.querySelectorAll('select, input')) {
                    elem.addEventListener('change', previewReloader.scheduleCall.bind(previewReloader));
                }
            }

            function createLayoutItem(type) {
                if (type == 'placeholder') {
                    return createPlaceholder();
                }

                const template = document.querySelector(`#tpl-g-${type}`);
                const node = template.content.cloneNode(true).firstElementChild;

                hookupPreviewUpdate(node);
                hookupAreaRemove(node);
                hookupAreaMove(node);

                for (const bt of node.querySelectorAll(':scope > a.add-element')) {
                    bt.addEventListener('click', (evt) => {
                        evt.target.before(createPlaceholder());
                        evt.target.before(createDropTarget());
                        previewReloader.scheduleCall();
                    });
                }

                function updateShapeFilter(filterNode) {
                    console.log(filterNode);
                    for (elem of filterNode.closest('.group').querySelectorAll('.only-shape')) {
                        if (elem.classList.contains(filterNode.value)) {
                            elem.style.removeProperty('display');
                        } else {
                            elem.style.setProperty('display', 'none');
                        }
                    }
                }

                if (type == 'tht' || type == 'smd') {
                    const filterNode = node.querySelector('select[name="pad_shape"]');
                    updateShapeFilter(filterNode);
                    filterNode.addEventListener('change', (evt) => {
                        updateShapeFilter(evt.target);
                    });
                }

                return node;
            }

            function createPlaceholder() {
                const node = document.querySelector('#tpl-g-placeholder').content.cloneNode(true).firstElementChild;

                hookupAreaRemove(node);
                hookupAreaMove(node);

                for (const bt of node.querySelectorAll('.placeholder a[data-placeholder]')) {
                    bt.addEventListener('click', (evt) => {
                        const item = createLayoutItem(evt.target.getAttribute('data-placeholder'));

                        for (const elem of item.querySelectorAll('div.placeholder')) {
                            elem.replaceWith(createPlaceholder());
                        }

                        for (const elem of item.querySelectorAll('div.drop-target')) {
                            elem.replaceWith(createDropTarget());
                        }

                        evt.target.closest('.group').replaceWith(item);
                        previewReloader.scheduleCall();
                    });
                }

                return node;
            }

            function serializeNode(node) {
                function serializeProperties(node) {
                    let obj = {};
                    for (const input of node.querySelectorAll(':scope > label > input, :scope > label > select')) {
                        if (input.type == 'checkbox') {
                            obj[input.name] = input.checked;

                        } else {
                            obj[input.name] = input.value;
                        }
                    }
                    return obj;
                }

                const obj = serializeProperties(node);
                
                for (const expand of node.querySelectorAll(':scope > .group.expand')) {
                    obj[expand.getAttribute('data-group')] = serializeProperties(expand);
                }

                const children = [];
                for (const elem of node.querySelectorAll(':scope > .group:not(.expand)')) {
                    const child = serializeNode(elem);
                    child['type'] = elem.getAttribute('data-type');
                    children.push(child);
                }
                obj['children'] = children;

                return obj;
            }

            function serialize() {
                const board = document.querySelector('.group.board');
                return JSON.stringify(serializeNode(board));
            }


            function deserializeNode(node, obj) {
                function deserializeProperties(node, obj) {
                    for (const input of node.querySelectorAll(':scope > label > input, :scope > label > select')) {
                        if (input.type == 'checkbox') {
                            input.checked = obj[input.name];

                        } else {
                            input.value = obj[input.name];
                        }
                    }
                }

                deserializeProperties(node, obj);
                
                for (const expand of node.querySelectorAll(':scope > .group.expand')) {
                    deserializeProperties(expand, obj[expand.getAttribute('data-group')]);
                }

                for (const child of obj['children']) {
                    const type = child['type'];
                    if (type) {
                        const item = createLayoutItem(type);
                        deserializeNode(item, child);

                        if (type == 'layout') {
                            for (const elem of item.querySelectorAll('div.placeholder, div.drop-target')) {
                                elem.remove();
                            }

                        } else {
                            for (const elem of item.querySelectorAll('div.drop-target')) {
                                elem.replaceWith(createDropTarget());
                            }
                        }

                        if (obj['type'] == 'layout') {
                            const addLink = node.querySelector(':scope > a.add-element');
                            addLink.before(item);
                            addLink.before(createDropTarget());

                        } else {
                            const placeholder = node.querySelector('div.placeholder');
                            placeholder.replaceWith(item);
                        }
                    }
                }
            }

            function deserialize(json) {
                const board = document.querySelector('.group.board');
                const data = JSON.parse(json);
                deserializeNode(board, data);
                previewReloader.scheduleCall();
            }

            class RateLimiter {
                constructor(callback, interval_ms) {
                    this.callback = callback;
                    this.interval_ms = interval_ms;
                    this.lastRan = -1e99;
                    this.timerId = null;
                }

                callNow() {
                    const now = performance.timeOrigin + performance.now();
                    this.lastRan = now;
                    this.timerId = null;
                    this.callback();
                }

                scheduleCall() {
                    const now = performance.timeOrigin + performance.now();
                    const timeRemaining = this.interval_ms - (now - this.lastRan);
                    console.log('scheduling', timeRemaining);
                    if (!this.timerId) {
                        if (timeRemaining <= 0) {
                            this.callNow();
                        } else {
                            const callback = this.callback;
                            this.timerId = setTimeout(this.callNow.bind(this), timeRemaining);
                        }
                    }
                }
            }

            let previewBlobURL = null;
            previewReloader = new RateLimiter(async () => {
                if (document.querySelector('form').checkValidity()) {
                    document.querySelector('#preview-message').textContent = 'Reloading...';
                    document.querySelector('#preview-message').classList.add('loading');
                    const response = await fetch('preview.svg', {
                        method: 'POST',
                        mode: 'same-origin',
                        cache: 'no-cache',
                        headers: {'Content-Type': 'application/json'},
                        body: serialize(),
                    });
                    const data = await response.blob();
                    if (previewBlobURL) {
                        URL.revokeObjectURL(previewBlobURL);
                    }
                    previewBlobURL = URL.createObjectURL(data);
                    document.querySelector('#preview-image').src = previewBlobURL;
                    document.querySelector('#preview-message').textContent = '';
                    document.querySelector('#preview-message').classList.remove('loading');
                } else {
                    document.querySelector('#preview-message').classList.add('loading');
                    document.querySelector('#preview-message').textContent = 'Please correct any invalid fields.';
                }
            }, 1000);

            document.querySelector('div.placeholder').replaceWith(createPlaceholder());

            for (elem of document.querySelectorAll('select[name="units"]')) {
                elem.addEventListener('change', (evt) => {
                    const style = evt.target.closest('.group').style;
                    for (const unit of ['metric', 'us']) {
                        const value = (unit == evt.target.value) ? 'default' : 'none';
                        style.setProperty(`--u-display-${unit}`, value);
                    }
                });
            }

            let downloadObjectURL = null;
            document.querySelector('#link-gerbers').addEventListener('click', async () => {
                const response = await fetch('gerbers.zip', {
                    method: 'POST',
                    mode: 'same-origin',
                    cache: 'no-cache',
                    headers: {'Content-Type': 'application/json'},
                    body: serialize(),
                });
                const data = await response.blob();
                /* cf. https://gist.github.com/devloco/5f779216c988438777b76e7db113d05c */
                const zipBlob = new Blob([data], { type: 'application/zip' });
                    
                if (downloadObjectURL) {
                    URL.revokeObjectURL(downloadObjectURL);
                }
                
                downloadObjectURL = URL.createObjectURL(zipBlob);
                let link = document.createElement('a');
                link.href = downloadObjectURL;
                link.download = 'gerbers.zip';
                link.click();
            });

            hookupPreviewUpdate(document.querySelector('.group.board'));
            previewReloader.scheduleCall();
        </script>
    </body>
</html>