diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b71966..df31e8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,10 +28,10 @@ jobs: - name: Install dependencies run: pnpm install - - name: Run tests - run: pnpm test + - name: Run tests with coverage + run: pnpm test:coverage - - name: Upload coverage reports (optional) + - name: Upload coverage reports if: always() uses: actions/upload-artifact@v4 with: diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/favicon.png differ diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000..e4fe0da --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 4.28% + Statements + 93/2169 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 0% + Functions + 0/36 +
+ + +
+ 4.28% + Lines + 93/2169 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
trmnl-form-builder.js +
+
4.28%93/2169100%2/20%0/364.28%93/2169
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/lcov-report/favicon.png differ diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 0000000..e3dc2c9 --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 4.28% + Statements + 93/2169 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 0% + Functions + 0/36 +
+ + +
+ 4.28% + Lines + 93/2169 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
trmnl-form-builder.js +
+
4.28%93/2169100%2/20%0/364.28%93/2169
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/trmnl-form-builder.js.html b/coverage/lcov-report/trmnl-form-builder.js.html new file mode 100644 index 0000000..97aca15 --- /dev/null +++ b/coverage/lcov-report/trmnl-form-builder.js.html @@ -0,0 +1,6601 @@ + + + + + + Code coverage report for trmnl-form-builder.js + + + + + + + + + +
+
+

All files trmnl-form-builder.js

+
+ +
+ 4.28% + Statements + 93/2169 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 0% + Functions + 0/36 +
+ + +
+ 4.28% + Lines + 93/2169 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +1056 +1057 +1058 +1059 +1060 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1090 +1091 +1092 +1093 +1094 +1095 +1096 +1097 +1098 +1099 +1100 +1101 +1102 +1103 +1104 +1105 +1106 +1107 +1108 +1109 +1110 +1111 +1112 +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +1122 +1123 +1124 +1125 +1126 +1127 +1128 +1129 +1130 +1131 +1132 +1133 +1134 +1135 +1136 +1137 +1138 +1139 +1140 +1141 +1142 +1143 +1144 +1145 +1146 +1147 +1148 +1149 +1150 +1151 +1152 +1153 +1154 +1155 +1156 +1157 +1158 +1159 +1160 +1161 +1162 +1163 +1164 +1165 +1166 +1167 +1168 +1169 +1170 +1171 +1172 +1173 +1174 +1175 +1176 +1177 +1178 +1179 +1180 +1181 +1182 +1183 +1184 +1185 +1186 +1187 +1188 +1189 +1190 +1191 +1192 +1193 +1194 +1195 +1196 +1197 +1198 +1199 +1200 +1201 +1202 +1203 +1204 +1205 +1206 +1207 +1208 +1209 +1210 +1211 +1212 +1213 +1214 +1215 +1216 +1217 +1218 +1219 +1220 +1221 +1222 +1223 +1224 +1225 +1226 +1227 +1228 +1229 +1230 +1231 +1232 +1233 +1234 +1235 +1236 +1237 +1238 +1239 +1240 +1241 +1242 +1243 +1244 +1245 +1246 +1247 +1248 +1249 +1250 +1251 +1252 +1253 +1254 +1255 +1256 +1257 +1258 +1259 +1260 +1261 +1262 +1263 +1264 +1265 +1266 +1267 +1268 +1269 +1270 +1271 +1272 +1273 +1274 +1275 +1276 +1277 +1278 +1279 +1280 +1281 +1282 +1283 +1284 +1285 +1286 +1287 +1288 +1289 +1290 +1291 +1292 +1293 +1294 +1295 +1296 +1297 +1298 +1299 +1300 +1301 +1302 +1303 +1304 +1305 +1306 +1307 +1308 +1309 +1310 +1311 +1312 +1313 +1314 +1315 +1316 +1317 +1318 +1319 +1320 +1321 +1322 +1323 +1324 +1325 +1326 +1327 +1328 +1329 +1330 +1331 +1332 +1333 +1334 +1335 +1336 +1337 +1338 +1339 +1340 +1341 +1342 +1343 +1344 +1345 +1346 +1347 +1348 +1349 +1350 +1351 +1352 +1353 +1354 +1355 +1356 +1357 +1358 +1359 +1360 +1361 +1362 +1363 +1364 +1365 +1366 +1367 +1368 +1369 +1370 +1371 +1372 +1373 +1374 +1375 +1376 +1377 +1378 +1379 +1380 +1381 +1382 +1383 +1384 +1385 +1386 +1387 +1388 +1389 +1390 +1391 +1392 +1393 +1394 +1395 +1396 +1397 +1398 +1399 +1400 +1401 +1402 +1403 +1404 +1405 +1406 +1407 +1408 +1409 +1410 +1411 +1412 +1413 +1414 +1415 +1416 +1417 +1418 +1419 +1420 +1421 +1422 +1423 +1424 +1425 +1426 +1427 +1428 +1429 +1430 +1431 +1432 +1433 +1434 +1435 +1436 +1437 +1438 +1439 +1440 +1441 +1442 +1443 +1444 +1445 +1446 +1447 +1448 +1449 +1450 +1451 +1452 +1453 +1454 +1455 +1456 +1457 +1458 +1459 +1460 +1461 +1462 +1463 +1464 +1465 +1466 +1467 +1468 +1469 +1470 +1471 +1472 +1473 +1474 +1475 +1476 +1477 +1478 +1479 +1480 +1481 +1482 +1483 +1484 +1485 +1486 +1487 +1488 +1489 +1490 +1491 +1492 +1493 +1494 +1495 +1496 +1497 +1498 +1499 +1500 +1501 +1502 +1503 +1504 +1505 +1506 +1507 +1508 +1509 +1510 +1511 +1512 +1513 +1514 +1515 +1516 +1517 +1518 +1519 +1520 +1521 +1522 +1523 +1524 +1525 +1526 +1527 +1528 +1529 +1530 +1531 +1532 +1533 +1534 +1535 +1536 +1537 +1538 +1539 +1540 +1541 +1542 +1543 +1544 +1545 +1546 +1547 +1548 +1549 +1550 +1551 +1552 +1553 +1554 +1555 +1556 +1557 +1558 +1559 +1560 +1561 +1562 +1563 +1564 +1565 +1566 +1567 +1568 +1569 +1570 +1571 +1572 +1573 +1574 +1575 +1576 +1577 +1578 +1579 +1580 +1581 +1582 +1583 +1584 +1585 +1586 +1587 +1588 +1589 +1590 +1591 +1592 +1593 +1594 +1595 +1596 +1597 +1598 +1599 +1600 +1601 +1602 +1603 +1604 +1605 +1606 +1607 +1608 +1609 +1610 +1611 +1612 +1613 +1614 +1615 +1616 +1617 +1618 +1619 +1620 +1621 +1622 +1623 +1624 +1625 +1626 +1627 +1628 +1629 +1630 +1631 +1632 +1633 +1634 +1635 +1636 +1637 +1638 +1639 +1640 +1641 +1642 +1643 +1644 +1645 +1646 +1647 +1648 +1649 +1650 +1651 +1652 +1653 +1654 +1655 +1656 +1657 +1658 +1659 +1660 +1661 +1662 +1663 +1664 +1665 +1666 +1667 +1668 +1669 +1670 +1671 +1672 +1673 +1674 +1675 +1676 +1677 +1678 +1679 +1680 +1681 +1682 +1683 +1684 +1685 +1686 +1687 +1688 +1689 +1690 +1691 +1692 +1693 +1694 +1695 +1696 +1697 +1698 +1699 +1700 +1701 +1702 +1703 +1704 +1705 +1706 +1707 +1708 +1709 +1710 +1711 +1712 +1713 +1714 +1715 +1716 +1717 +1718 +1719 +1720 +1721 +1722 +1723 +1724 +1725 +1726 +1727 +1728 +1729 +1730 +1731 +1732 +1733 +1734 +1735 +1736 +1737 +1738 +1739 +1740 +1741 +1742 +1743 +1744 +1745 +1746 +1747 +1748 +1749 +1750 +1751 +1752 +1753 +1754 +1755 +1756 +1757 +1758 +1759 +1760 +1761 +1762 +1763 +1764 +1765 +1766 +1767 +1768 +1769 +1770 +1771 +1772 +1773 +1774 +1775 +1776 +1777 +1778 +1779 +1780 +1781 +1782 +1783 +1784 +1785 +1786 +1787 +1788 +1789 +1790 +1791 +1792 +1793 +1794 +1795 +1796 +1797 +1798 +1799 +1800 +1801 +1802 +1803 +1804 +1805 +1806 +1807 +1808 +1809 +1810 +1811 +1812 +1813 +1814 +1815 +1816 +1817 +1818 +1819 +1820 +1821 +1822 +1823 +1824 +1825 +1826 +1827 +1828 +1829 +1830 +1831 +1832 +1833 +1834 +1835 +1836 +1837 +1838 +1839 +1840 +1841 +1842 +1843 +1844 +1845 +1846 +1847 +1848 +1849 +1850 +1851 +1852 +1853 +1854 +1855 +1856 +1857 +1858 +1859 +1860 +1861 +1862 +1863 +1864 +1865 +1866 +1867 +1868 +1869 +1870 +1871 +1872 +1873 +1874 +1875 +1876 +1877 +1878 +1879 +1880 +1881 +1882 +1883 +1884 +1885 +1886 +1887 +1888 +1889 +1890 +1891 +1892 +1893 +1894 +1895 +1896 +1897 +1898 +1899 +1900 +1901 +1902 +1903 +1904 +1905 +1906 +1907 +1908 +1909 +1910 +1911 +1912 +1913 +1914 +1915 +1916 +1917 +1918 +1919 +1920 +1921 +1922 +1923 +1924 +1925 +1926 +1927 +1928 +1929 +1930 +1931 +1932 +1933 +1934 +1935 +1936 +1937 +1938 +1939 +1940 +1941 +1942 +1943 +1944 +1945 +1946 +1947 +1948 +1949 +1950 +1951 +1952 +1953 +1954 +1955 +1956 +1957 +1958 +1959 +1960 +1961 +1962 +1963 +1964 +1965 +1966 +1967 +1968 +1969 +1970 +1971 +1972 +1973 +1974 +1975 +1976 +1977 +1978 +1979 +1980 +1981 +1982 +1983 +1984 +1985 +1986 +1987 +1988 +1989 +1990 +1991 +1992 +1993 +1994 +1995 +1996 +1997 +1998 +1999 +2000 +2001 +2002 +2003 +2004 +2005 +2006 +2007 +2008 +2009 +2010 +2011 +2012 +2013 +2014 +2015 +2016 +2017 +2018 +2019 +2020 +2021 +2022 +2023 +2024 +2025 +2026 +2027 +2028 +2029 +2030 +2031 +2032 +2033 +2034 +2035 +2036 +2037 +2038 +2039 +2040 +2041 +2042 +2043 +2044 +2045 +2046 +2047 +2048 +2049 +2050 +2051 +2052 +2053 +2054 +2055 +2056 +2057 +2058 +2059 +2060 +2061 +2062 +2063 +2064 +2065 +2066 +2067 +2068 +2069 +2070 +2071 +2072 +2073 +2074 +2075 +2076 +2077 +2078 +2079 +2080 +2081 +2082 +2083 +2084 +2085 +2086 +2087 +2088 +2089 +2090 +2091 +2092 +2093 +2094 +2095 +2096 +2097 +2098 +2099 +2100 +2101 +2102 +2103 +2104 +2105 +2106 +2107 +2108 +2109 +2110 +2111 +2112 +2113 +2114 +2115 +2116 +2117 +2118 +2119 +2120 +2121 +2122 +2123 +2124 +2125 +2126 +2127 +2128 +2129 +2130 +2131 +2132 +2133 +2134 +2135 +2136 +2137 +2138 +2139 +2140 +2141 +2142 +2143 +2144 +2145 +2146 +2147 +2148 +2149 +2150 +2151 +2152 +2153 +2154 +2155 +2156 +2157 +2158 +2159 +2160 +2161 +2162 +2163 +2164 +2165 +2166 +2167 +2168 +2169 +2170 +2171 +2172 +21731x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  +1x +1x +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +1x +1x +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +  + 
/**
+ * TRMNL YML Form Builder Web Component
+ * 
+ * A standalone, dependency-free Web Component for building TRMNL custom plugin forms.
+ * Requires only Tailwind CSS for styling.
+ * 
+ * Usage:
+ *   <trmnl-form-builder></trmnl-form-builder>
+ * 
+ * Attributes:
+ *   - accent-color: Hex color for accent elements (default: #F8654B)
+ *   - theme: 'light' or 'dark' (default: 'light')
+ */
+ 
+class TRMLYamlForm extends HTMLElement {
+    constructor() {
+	  super();
+	  this.attachShadow({ mode: 'open' });
+
+	  // State management
+	  this.fields = [];
+	  this.selectedFieldId = null;
+	  this.draggedFieldId = null;
+	  this.draggedFromIndex = null;
+	  this.nextFieldId = 1;
+
+	  // Configuration
+	  this.accentColor = this.getAttribute('accent-color') || '#F8654B';
+	  this.theme = this.getAttribute('theme') || 'light';
+
+	  // Categories cache
+	  this.categories = [];
+	  this.loadCategories();
+
+	  // supported locales (based on trmnl-i18n)
+      this.locales = [
+		{ code: 'de', label: 'Deutsch' },
+		{ code: 'nl', label: 'Nederlands' },
+		{ code: 'fr', label: 'Français' },
+		{ code: 'en-GB', label: 'English: UK' },
+		{ code: 'es-ES', label: 'Español' },
+		{ code: 'it', label: 'Italiano' },
+		{ code: 'no', label: 'Norsk' },
+		{ code: 'da', label: 'Dansk' },
+		{ code: 'ja', label: '日本語' },
+		{ code: 'de-AT', label: 'Deutsch: Österreich' },
+		{ code: 'ko', label: '한국어' },
+		{ code: 'sv', label: 'Svenska' },
+		{ code: 'pt-BR', label: 'Português' },
+		{ code: 'zh-HK', label: '中文: 香港' },
+		{ code: 'pl', label: 'Polski' },
+		{ code: 'uk', label: 'Українська' }, 
+		{ code: 'zh-CN', label: '中文: 简体' },
+		{ code: 'hu', label: 'Magyar' },
+		{ code: 'he', label: 'עברית' },
+		{ code: 'sk', label: 'Slovenčina' },
+		{ code: 'ru', label: 'Русский' },
+		{ code: 'is', label: 'Íslenska' },
+		{ code: 'id', label: 'Bahasa Indonesia' }
+	  ];
+  
+	  // --- Property Definitions (How to render each setting) ---
+	  this.propertyDefinitions = {
+		// Common
+		keyname: { label: 'Keyname', type: 'text', help: 'Lowercase alphanumeric + underscores only' },
+		name: { label: 'Name', type: 'text', placeholder: 'About This Plugin' },
+		description: { label: 'Description', type: 'textarea', placeholder: 'Main description (HTML-friendly)' },
+		help_text: { label: 'Help Text', type: 'textarea', placeholder: 'Sub-label / hint (HTML-friendly)' },
+		optional: { label: 'Optional field', type: 'checkbox' },
+		
+		// Inputs
+		default: { label: 'Default Value', type: 'text', placeholder: 'Pre-selected value' },
+		placeholder: { label: 'Placeholder', type: 'text', placeholder: 'Example value' },
+		min: { label: 'Minimum Value', type: 'number', placeholder: '0' },
+		max: { label: 'Maximum Value', type: 'number', placeholder: '100' },
+		maxlength: { label: 'Maximum Length', type: 'number', placeholder: '12' },
+		rows: { label: 'Rows', type: 'number', placeholder: '10' },
+		
+		// Selection
+		options: { label: 'Options (one per line)', type: 'textarea', placeholder: 'Option 1\nOption 2', help: 'Enter one option per line' },
+		multiple: { label: 'Allow multiple selection', type: 'checkbox' },
+		endpoint: { label: 'Endpoint *', type: 'text', placeholder: 'https://api.example.com/options' },
+		http_verb: { 
+		  label: 'HTTP Method', 
+		  type: 'select', 
+		  default: 'GET', 
+		  options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT'],
+		  placeholder: 'GET'
+		},
+		
+		// Special
+		value: { label: 'Value', type: 'text', placeholder: 'sk_live_abc123xyz' },
+		
+		// Author Bio Specifics
+		categories: { label: 'Categories (Max 2)', type: 'custom_categories' },
+		github_url: { label: 'GitHub URL', type: 'url', placeholder: 'https://github.com/username' },
+		email_address: { label: 'Email Address', type: 'text', placeholder: 'user@example.com' },
+		learn_more_url: { label: 'Learn More URL', type: 'url', placeholder: 'https://thesatanictemple.com/' },
+		youtube_url: { label: 'YouTube URL', type: 'url', placeholder: 'https://www.youtube.com/@useTRMNL' },
+		
+		// The Translation Manager
+		i18n_descriptions: { label: 'Description Translations', type: 'i18n_map', help: 'Add translated descriptions for specific locales.' },
+		
+		// Conditional Logic
+		conditional_validation: { label: 'Conditional Logic', type: 'conditional_builder', help: 'Hide other fields based on this field\'s value' }
+	  };
+
+	  // --- Field Types (Added 'properties' array to each) ---
+	  this.fieldTypes = {
+		// Text Input
+		author_bio: { 
+		  category: 'TEXT INPUT', name: 'Author Bio', description: 'Plugin information', 
+		  properties: [{ 
+			  key: 'name', 
+			  default: 'About This Plugin'
+			}, 'categories', 'github_url', 'email_address', 'learn_more_url', 'youtube_url'] // Note: Common props are handled automatically
+		},
+		url: { 
+		  category: 'TEXT INPUT', name: 'Url', description: 'URL input', 
+		  properties: ['default', 'placeholder', 'optional'] 
+		},
+		string: { 
+		  category: 'TEXT INPUT', name: 'String', description: 'Single-line text', 
+		  properties: ['default', 'placeholder', 'maxlength', 'optional', 'conditional_validation'] 
+		},
+		multi_string: { 
+		  category: 'TEXT INPUT', name: 'Multi String', description: 'Comma-separated', 
+		  properties: ['default', 'placeholder', 'optional'] 
+		},
+		text: { 
+		  category: 'TEXT INPUT', name: 'Text', description: 'Multi-line textarea', 
+		  properties: ['default', 'placeholder', 'rows', 'optional', 'conditional_validation'] 
+		},
+		code: { 
+		  category: 'TEXT INPUT', name: 'Code', description: 'Monospace code', 
+		  properties: ['default', 'placeholder', 'rows', 'optional'] 
+		},
+		password: { 
+		  category: 'TEXT INPUT', name: 'Password', description: 'Masked input', 
+		  properties: ['default', 'placeholder', 'optional'] 
+		},
+		
+		// Numeric
+		number: { 
+		  category: 'NUMERIC', name: 'Number', description: 'Numeric input', 
+		  properties: ['default', 'placeholder', 'min', 'max', 'optional', 'conditional_validation'] 
+		},
+		
+		// Date & Time
+		date: { 
+		  category: 'DATE & TIME', name: 'Date', description: 'Date picker', 
+		  // We use an object to override the generic 'default' definition
+		  properties: [
+			{ 
+			  key: 'default', // The property key we are targeting
+			  placeholder: 'YYYY-MM-DD', 
+			  help: 'Can also be set to "today" to use the current date automatically.' 
+			},
+			'optional'
+		  ] 
+		},
+		time: { category: 'DATE & TIME', name: 'Time', description: 'Time picker', properties: ['default', 'optional'] },
+		timezone: { category: 'DATE & TIME', name: 'Time Zone', description: 'Timezone selector', properties: ['default', 'optional'] },
+		
+		// Selection
+		select: { 
+		  category: 'SELECTION', name: 'Select', description: 'Dropdown', 
+		  properties: ['options', 'default', 'multiple', 'optional', 'conditional_validation'] 
+		},
+		xhrSelect: { 
+		  category: 'SELECTION', name: 'XhrSelect', description: 'Dynamic dropdown', 
+		  properties: ['endpoint', 'http_verb', 'multiple', 'optional'] 
+		},
+		xhrSelectSearch: { 
+		  category: 'SELECTION', name: 'XhrSelectSearch', description: 'Searchable dynamic', 
+		  properties: ['endpoint', 'http_verb', 'multiple', 'optional'] 
+		},
+		
+		// Special
+		copyable: { category: 'SPECIAL', name: 'Copyable', description: 'Click-to-copy', properties: ['value'] },
+		copyable_webhook_url: { category: 'SPECIAL', name: 'Copyable Webhook', description: 'Click-to-copy webhook', properties: ['value'] },
+	  };
+	}
+ 
+  connectedCallback() {
+    this.render();
+    this.setupEventListeners();
+  }
+ 
+  async loadCategories() {
+    try {
+      const response = await fetch('https://usetrmnl.com/api/categories');
+      if (response.ok) {
+        const data = await response.json();
+        // Handle both array and object with data property
+        this.categories = Array.isArray(data) ? data : (data.data || this.getFallbackCategories());
+        // Extract just the names if they're objects
+        if (this.categories.length > 0 && typeof this.categories[0] === 'object') {
+          this.categories = this.categories.map(cat => cat.name || cat.title || cat);
+        }
+      } else {
+        this.categories = this.getFallbackCategories();
+      }
+    } catch (error) {
+      console.warn('Failed to load categories from API, using fallback:', error);
+      this.categories = this.getFallbackCategories();
+    }
+  }
+ 
+  getFallbackCategories() {
+    return [
+        "analytics",
+		"art",
+		"calendar",
+		"comics",
+		"crm",
+		"custom",
+		"discovery",
+		"ecommerce",
+		"education",
+		"email",
+		"entertainment",
+		"environment",
+		"finance",
+		"games",
+		"humor",
+		"images",
+		"kpi",
+		"life",
+		"marketing",
+		"nature",
+		"news",
+		"personal",
+		"productivity",
+		"programming",
+		"sales",
+		"sports",
+		"travel"
+    ];
+  }
+ 
+  render() {
+    const html = `
+      <style>
+        * {
+          box-sizing: border-box;
+        }
+        
+        :host {
+          --accent-color: ${this.accentColor};
+          --bg-primary: ${this.theme === 'dark' ? '#1a1a1a' : '#ffffff'};
+          --bg-secondary: ${this.theme === 'dark' ? '#2a2a2a' : '#f5f5f5'};
+		  --bg-primary-inverted: ${this.theme === 'dark' ? '#ffffff' : '#1a1a1a'};
+          --text-primary: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+          --text-secondary: ${this.theme === 'dark' ? '#a0a0a0' : '#666666'};
+          --text-primary-inverted: ${this.theme === 'dark' ? '#1a1a1a' : '#e0e0e0'};
+          --border-color: ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+          
+          display: block;
+          /* font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif; */
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          height: 100vh;
+          overflow: auto;
+        }
+        
+        .container {
+          display: grid;
+          grid-template-columns: 280px 1fr 1fr;
+          grid-template-rows: auto 1fr;
+          height: 100%;
+          gap: 0;
+        }
+        
+        .header {
+          grid-column: 1 / -1;
+          padding: 20px 24px;
+          border-bottom: 1px solid var(--border-color);
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+        }
+        
+        .header h1 {
+          margin: 0;
+          font-size: 24px;
+          font-weight: 600;
+          letter-spacing: -0.5px;
+        }
+        
+        .header p {
+          margin: 4px 0 0 0;
+          font-size: 13px;
+          color: var(--text-secondary);
+        }
+        
+        .field-count {
+          font-size: 13px;
+          color: var(--text-secondary);
+          font-weight: 500;
+        }
+        
+        .sidebar-left {
+          border-right: 1px solid var(--border-color);
+          overflow-y: auto;
+          background: var(--bg-secondary);
+        }
+        
+        .sidebar-right {
+          border-left: 1px solid var(--border-color);
+          display: flex;
+          flex-direction: column;
+          background: var(--bg-secondary);
+        }
+        
+        .canvas {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          padding: 40px;
+          overflow-y: auto;
+          background: var(--bg-primary);
+        }
+        
+        .canvas.has-fields {
+          align-items: stretch;
+          justify-content: flex-start;
+        }
+        
+        .field-types-section {
+          padding: 16px;
+        }
+        
+        .field-types-label {
+          font-size: 11px;
+          font-weight: 700;
+          letter-spacing: 0.5px;
+          text-transform: uppercase;
+          color: var(--text-secondary);
+          margin: 16px 0 8px 0;
+          padding: 0 8px;
+        }
+        
+        .field-type-button {
+          width: 100%;
+          padding: 12px;
+          margin-bottom: 8px;
+          border: 1px dashed var(--accent-color);
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          border-radius: 6px;
+          cursor: grab;
+          transition: all 0.2s;
+          text-align: left;
+          font-size: 13px;
+          font-weight: 500;
+          position: relative;
+        }
+        
+        .field-type-button:hover {
+          background: var(--bg-secondary);
+          border-style: solid;
+        }
+        
+        .field-type-button:active {
+          cursor: grabbing;
+        }
+		
+		.field-type-button[disabled] {
+		  opacity: 0.5;
+		  cursor: not-allowed;
+		  background: var(--bg-secondary);
+		  border-color: var(--border-color);
+		  pointer-events: none; /* Prevents clicking and dragging */
+		}
+        
+        .field-type-name {
+          font-weight: 600;
+          margin-bottom: 2px;
+        }
+        
+        .field-type-desc {
+          font-size: 12px;
+          color: var(--text-secondary);
+        }
+        
+        .field-type-icon {
+          position: absolute;
+          right: 8px;
+          top: 50%;
+          transform: translateY(-50%);
+          width: 20px;
+          height: 20px;
+          background: var(--accent-color);
+          color: white;
+          border-radius: 3px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 11px;
+          font-weight: 600;
+        }
+        
+        .empty-state {
+          text-align: center;
+          color: var(--text-secondary);
+        }
+        
+        .empty-state-icon {
+          font-size: 48px;
+          margin-bottom: 16px;
+          opacity: 0.5;
+        }
+        
+        .empty-state h3 {
+          margin: 0 0 8px 0;
+          font-size: 16px;
+          font-weight: 600;
+          color: var(--text-primary);
+        }
+        
+        .empty-state p {
+          margin: 0;
+          font-size: 13px;
+          line-height: 1.5;
+          max-width: 300px;
+        }
+        
+        .fields-list {
+          display: flex;
+          flex-direction: column;
+          gap: 12px;
+          width: 100%;
+          max-width: 600px;
+        }
+        
+        .field-card {
+          padding: 12px;
+          border: 2px solid var(--border-color);
+          border-radius: 6px;
+          background: var(--bg-primary);
+          cursor: move;
+          transition: all 0.2s;
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          user-select: none;
+        }
+        
+        .field-card:hover {
+          border-color: var(--accent-color);
+          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+        }
+        
+        .field-card.selected {
+          border-color: var(--accent-color);
+          background: var(--bg-secondary);
+        }
+        
+        .field-card-info {
+          flex: 1;
+          min-width: 0;
+        }
+        
+        .field-card-keyname {
+          font-size: 12px;
+          color: var(--text-secondary);
+          margin-bottom: 2px;
+		  text-transform: lowercase;
+        }
+        
+        .field-card-type {
+          font-size: 11px;
+          color: var(--text-secondary);
+          text-transform: uppercase;
+          letter-spacing: 0.5px;
+          margin-left: 4px;
+        }
+        
+        .field-card-name {
+          font-size: 14px;
+          font-weight: 600;
+          color: var(--text-primary);
+        }
+        
+        .field-card-actions {
+          display: flex;
+          gap: 4px;
+          margin-left: 8px;
+        }
+        
+        .icon-button {
+          width: 28px;
+          height: 28px;
+          padding: 0;
+          border: 1px solid var(--border-color);
+          background: var(--bg-secondary);
+          color: var(--text-primary);
+          border-radius: 4px;
+          cursor: pointer;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 13px;
+          transition: all 0.2s;
+        }
+        
+        .icon-button:hover {
+          border-color: var(--accent-color);
+          color: var(--accent-color);
+        }
+        
+        .icon-button.delete:hover {
+          border-color: #ef4444;
+          color: #ef4444;
+        }
+        
+        .tabs {
+          display: flex;
+          border-bottom: 1px solid var(--border-color);
+          padding: 0 16px;
+        }
+        
+        .tab {
+          padding: 12px 16px;
+          border: none;
+          background: transparent;
+          color: var(--text-secondary);
+          cursor: pointer;
+          font-size: 13px;
+          font-weight: 500;
+          border-bottom: 2px solid transparent;
+          transition: all 0.2s;
+        }
+        
+        .tab:hover {
+          color: var(--text-primary);
+        }
+        
+        .tab.active {
+          color: var(--accent-color);
+          border-bottom-color: var(--accent-color);
+        }
+        
+        .tab-content {
+          flex: 1;
+          overflow-y: auto;
+          padding: 16px;
+          display: none;
+        }
+        
+        .tab-content.active {
+          display: block;
+        }
+        
+        .form-group {
+          margin-bottom: 16px;
+        }
+        
+        .form-label {
+          display: block;
+          font-size: 12px;
+          font-weight: 600;
+          margin-bottom: 6px;
+          color: var(--text-primary);
+          text-transform: uppercase;
+          letter-spacing: 0.5px;
+        }
+        
+        .form-input,
+        .form-textarea {
+          width: 100%;
+          padding: 8px;
+          border: 1px solid var(--border-color);
+          border-radius: 4px;
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          font-size: 13px;
+          font-family: inherit;
+          transition: border-color 0.2s;
+        }
+        
+        .form-input:focus,
+        .form-textarea:focus {
+          outline: none;
+          border-color: var(--accent-color);
+          box-shadow: 0 0 0 2px rgba(248, 101, 75, 0.1);
+        }
+        
+        .form-textarea {
+          resize: vertical;
+          min-height: 60px;
+        }
+        
+        .form-checkbox {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          cursor: pointer;
+        }
+        
+        .form-checkbox input {
+          cursor: pointer;
+          accent-color: var(--accent-color);
+        }
+        
+        .form-checkbox label {
+          cursor: pointer;
+          font-size: 13px;
+        }
+        
+        .yaml-output {
+          font-family: monospace;
+          font-size: 12px;
+          line-height: 1.6;
+          white-space: pre-wrap;
+          word-break: break-word;
+          background: var(--bg-primary-inverted);
+          padding: 12px;
+          border-radius: 4px;
+          border: 1px solid var(--border-color);
+          color: var(--text-primary-inverted);
+          max-height: 400px;
+          overflow-y: auto;
+        }
+        
+        .yaml-empty {
+          color: var(--text-secondary);
+          font-style: italic;
+        }
+        
+        .button-group {
+          display: flex;
+          gap: 8px;
+          margin-bottom: 16px;
+        }
+        
+        .button {
+          flex: 1;
+          padding: 8px 12px;
+          border: none;
+          border-radius: 4px;
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+          transition: all 0.2s;
+          text-transform: uppercase;
+          letter-spacing: 0.5px;
+        }
+        
+        .button-primary {
+          background: var(--accent-color);
+          color: white;
+        }
+        
+        .button-primary:hover {
+          opacity: 0.9;
+          transform: translateY(-1px);
+          box-shadow: 0 2px 8px rgba(248, 101, 75, 0.3);
+        }
+        
+        .button-secondary {
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          border: 1px solid var(--border-color);
+        }
+        
+        .button-secondary:hover {
+          border-color: var(--accent-color);
+          color: var(--accent-color);
+        }
+        
+        .no-field-selected {
+          color: var(--text-secondary);
+          /* text-align: center; */
+          padding: 40px 20px;
+          font-size: 13px;
+        }
+        
+        @keyframes slideIn {
+          from {
+            transform: translateX(400px);
+            opacity: 0;
+          }
+          to {
+            transform: translateX(0);
+            opacity: 1;
+          }
+        }
+        
+        .drag-over {
+          background: var(--bg-secondary);
+          border-color: var(--accent-color);
+        }
+		
+		.reset-button {
+		  margin-left: auto; /* Pushes button to the far right */
+		  background: transparent;
+		  border: none;
+		  color: var(--accent-color);
+		  cursor: pointer;
+		  font-size: 11px;
+		  font-weight: 600;
+		  text-transform: uppercase;
+		  letter-spacing: 0.5px;
+		  padding: 0 4px;
+		  transition: color 0.2s;
+		}
+
+		.reset-button:hover {
+		  color: var(--text-secondary);
+		}
+      </style>
+      
+      <div class="container">
+        <div class="header">
+          <div>
+            <h1>TRMNL YML Form Builder</h1>
+            <p>Create custom plugin configuration forms with a no-code interface</p>
+          </div>
+          <div class="field-count"><span id="fieldCount">0</span> fields defined</div>
+        </div>
+        
+        <div class="sidebar-left">
+          <div id="fieldTypesList"></div>
+        </div>
+        
+        <div class="canvas" id="canvas">
+          <div class="empty-state">
+            <div class="empty-state-icon">+</div>
+            <h3>No fields yet</h3>
+            <p>Select a field type from the left panel to start building your form.</p>
+            <p style="margin-top: 8px; font-size: 12px;">You can drag fields to reorder them, or click to edit properties.</p>
+          </div>
+        </div>
+        
+        <div class="sidebar-right">
+          <div class="tabs">
+            <button class="tab active" data-tab="config">Config</button>
+            <button class="tab" data-tab="yaml">YAML</button>
+			<button class="reset-button" id="resetBtn" title="Clear all fields">Reset</button>
+          </div>
+          
+          <div id="configTab" class="tab-content active">
+            <div id="configContent" class="no-field-selected">
+              Select a field to edit its properties
+            </div>
+          </div>
+          
+          <div id="yamlTab" class="tab-content">
+            <div class="button-group">
+              <button class="button button-primary" id="copyBtn">Copy</button>
+			  <button class="button button-secondary" id="importBtn">Import</button>
+            </div>
+            <div id="yamlOutput" class="yaml-output">
+              <span class="yaml-empty"># No fields defined yet</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    `;
+    
+    this.shadowRoot.innerHTML = html;
+    this.renderFieldTypes();
+  }
+ 
+  renderFieldTypes() {
+    const container = this.shadowRoot.getElementById('fieldTypesList');
+    const grouped = {};
+    
+    // Group field types by category
+    Object.entries(this.fieldTypes).forEach(([key, config]) => {
+      if (!grouped[config.category]) {
+        grouped[config.category] = [];
+      }
+      grouped[config.category].push({ key, ...config });
+    });
+    
+    let html = '';
+    Object.entries(grouped).forEach(([category, fields]) => {
+      html += `<div class="field-types-section">
+        <div class="field-types-label">${category}</div>`;
+      
+      fields.forEach(field => {
+        html += `
+          <button class="field-type-button" draggable="true" data-field-type="${field.key}">
+            <div class="field-type-name">${field.name}</div>
+            <div class="field-type-desc">${field.description}</div>
+            <div class="field-type-icon">+</div>
+          </button>
+        `;
+      });
+      
+      html += '</div>';
+    });
+    
+    container.innerHTML = html;
+	this.updateAuthorBioState(); // Check immediately on render
+  }
+ 
+  renderCategoryPills(selectedCategories = []) {
+    if (!this.categories || this.categories.length === 0) {
+      return '<div style="color: var(--text-secondary); font-size: 12px;">Loading categories...</div>';
+    }
+
+    let html = '<div style="display: flex; flex-wrap: wrap; gap: 8px;">';
+    
+    this.categories.forEach(category => {
+      const isSelected = selectedCategories.includes(category);
+      html += `
+        <button class="category-pill" data-category="${category}" style="
+          padding: 6px 12px;
+          border: 1px solid ${isSelected ? 'var(--accent-color)' : 'var(--border-color)'};
+          background: ${isSelected ? 'var(--accent-color)' : 'var(--bg-primary)'};
+          color: ${isSelected ? 'white' : 'var(--text-primary)'};
+          border-radius: 20px;
+          cursor: pointer;
+          font-size: 12px;
+          font-weight: 500;
+          transition: all 0.2s;
+          white-space: nowrap;
+        ">${category}</button>
+      `;
+    });
+    
+    html += '</div>';
+    return html;
+  }
+ 
+  setupEventListeners() {
+    // Field type buttons
+    this.shadowRoot.querySelectorAll('.field-type-button').forEach(btn => {
+      btn.addEventListener('dragstart', (e) => this.handleFieldTypeDragStart(e));
+      btn.addEventListener('click', (e) => this.addField(e.currentTarget.dataset.fieldType));
+    });
+    
+    // Canvas drop zone
+    const canvas = this.shadowRoot.getElementById('canvas');
+    canvas.addEventListener('dragover', (e) => this.handleCanvasDragOver(e));
+    canvas.addEventListener('drop', (e) => this.handleCanvasDrop(e));
+    canvas.addEventListener('dragleave', (e) => this.handleCanvasDragLeave(e));
+    
+    // Tabs
+    this.shadowRoot.querySelectorAll('.tab').forEach(tab => {
+      tab.addEventListener('click', (e) => this.switchTab(e.currentTarget.dataset.tab));
+    });
+    
+    // YAML buttons
+    this.shadowRoot.getElementById('copyBtn').addEventListener('click', () => this.copyYaml());
+	this.shadowRoot.getElementById('importBtn').addEventListener('click', () => this.importYaml());
+	
+	// Reset button listener
+    const resetBtn = this.shadowRoot.getElementById('resetBtn');
+    if (resetBtn) {
+      resetBtn.addEventListener('click', () => {
+        if (confirm('Are you sure you want to reset the form? All fields will be lost.')) {
+          this.clear();
+        }
+      });
+    }
+  }
+ 
+  handleFieldTypeDragStart(e) {
+    e.dataTransfer.effectAllowed = 'copy';
+    e.dataTransfer.setData('fieldType', e.currentTarget.dataset.fieldType);
+  }
+ 
+  handleCanvasDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+    this.shadowRoot.getElementById('canvas').classList.add('drag-over');
+  }
+ 
+  handleCanvasDragLeave(e) {
+    if (e.target === this.shadowRoot.getElementById('canvas')) {
+      this.shadowRoot.getElementById('canvas').classList.remove('drag-over');
+    }
+  }
+ 
+  handleCanvasDrop(e) {
+    e.preventDefault();
+    this.shadowRoot.getElementById('canvas').classList.remove('drag-over');
+    const fieldType = e.dataTransfer.getData('fieldType');
+    if (fieldType) {
+      this.addField(fieldType);
+    }
+  }
+ 
+   addField(fieldType) {
+	  if (fieldType === 'author_bio' && this.fields.some(f => f.field_type === 'author_bio')) {
+		this.showToast('Only one Author Bio field is allowed');
+		return;
+	  }
+	  
+	  const fieldKeyname = fieldType === 'author_bio'
+		? fieldType
+		: `${fieldType}_${Date.now().toString().slice(-4)}`;
+
+	  const typeConfig = this.fieldTypes[fieldType];
+	  
+	  // Create new field object with minimal defaults
+	  const field = {
+		id: `field_${this.nextFieldId++}`,
+		keyname: fieldKeyname,
+		field_type: fieldType,
+	  };
+	  
+	  // Only set 'name' if there's a default specified in the type config
+	  let hasNameDefault = false;
+	  if (typeConfig && Array.isArray(typeConfig.properties)) {
+		typeConfig.properties.forEach(prop => {
+		  if (typeof prop === 'object' && prop.key === 'name' && prop.default) {
+			field.name = prop.default;
+			hasNameDefault = true;
+		  }
+		});
+	  }
+	  
+	  // Fallback to type name if no custom default was set
+	  if (!hasNameDefault) {
+		field.name = this.fieldTypes[fieldType]?.name || fieldType;
+	  }
+	  
+	  // Update state
+	  this.fields.push(field);
+	  this.selectedFieldId = field.id;
+	  
+	  this.switchTab('config');
+	  this.updateConfigPanel(); 
+	  this.updateCanvasView();
+	  this.updateYamlOutput();
+	  this.updateAuthorBioState();
+  }
+ 
+  updateCanvasView() {
+    const canvas = this.shadowRoot.getElementById('canvas');
+    
+    if (this.fields.length === 0) {
+      canvas.innerHTML = `
+        <div class="empty-state">
+          <div class="empty-state-icon">+</div>
+          <h3>No fields yet</h3>
+          <p>Select a field type from the left panel to start building your form.</p>
+          <p style="margin-top: 8px; font-size: 12px;">You can drag fields to reorder them, or click to edit properties.</p>
+        </div>
+      `;
+      canvas.classList.remove('has-fields');
+      return;
+    }
+    
+    canvas.classList.add('has-fields');
+    let html = `<div class="fields-list">`;
+    
+    this.fields.forEach((field, index) => {
+      const isSelected = field.id === this.selectedFieldId;
+      html += `
+        <div class="field-card ${isSelected ? 'selected' : ''}" data-field-id="${field.id}" draggable="true">
+          <div class="field-card-info">
+            <div class="field-card-keyname">${field.keyname}<span class="field-card-type">${field.field_type}</span></div>
+            <div class="field-card-name">${field.name}</div>
+          </div>
+          <div class="field-card-actions">
+	  `;
+	  if (field.field_type != 'author_bio'){
+            html += `<button class="icon-button duplicate" data-field-id="${field.id}" title="Duplicate">⎘</button>`;
+	  }
+      html += `
+            <button class="icon-button delete" data-field-id="${field.id}" title="Delete">✕</button>
+          </div>
+        </div>
+      `;
+    });
+    
+    html += '</div>';
+    canvas.innerHTML = html;
+    
+    // Setup field card event listeners
+    this.shadowRoot.querySelectorAll('.field-card').forEach(card => {
+      card.addEventListener('click', (e) => {
+        if (!e.target.closest('.icon-button')) {
+          this.selectedFieldId = card.dataset.fieldId;
+		  this.switchTab('config');
+          this.updateConfigPanel();
+          this.updateCanvasView();
+        }
+      });
+      
+      card.addEventListener('dragstart', (e) => {
+        // Explicitly set data to satisfy browser validation
+        e.dataTransfer.setData('text/plain', card.dataset.fieldId); 
+        e.dataTransfer.effectAllowed = 'move';
+        
+        this.draggedFieldId = card.dataset.fieldId;
+        this.draggedFromIndex = this.fields.findIndex(f => f.id === this.draggedFieldId);
+        card.style.opacity = '0.5';
+      });
+      
+      card.addEventListener('dragend', () => {
+        card.style.opacity = '1';
+        this.draggedFieldId = null;
+        this.draggedFromIndex = null;
+      });
+      
+      card.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        // Stop propagation to prevent Canvas "copy" effect from overriding "move"
+        e.stopPropagation(); 
+        e.dataTransfer.dropEffect = 'move';
+        
+        // Add visual cue
+        card.style.borderTop = '2px solid var(--accent-color)';
+      });
+      
+      card.addEventListener('dragleave', (e) => {
+        // Ensure we don't remove style when entering child elements
+        // Only remove if we are genuinely leaving the card rect
+        const rect = card.getBoundingClientRect();
+        const x = e.clientX;
+        const y = e.clientY;
+        
+        // Check if cursor is actually outside the element bounds
+        if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
+             card.style.borderTop = '';
+        }
+      });
+      
+      card.addEventListener('drop', (e) => {
+        e.preventDefault();
+        // Stop propagation so the canvas handler doesn't trigger
+        e.stopPropagation(); 
+        
+        card.style.borderTop = '';
+        
+        if (this.draggedFieldId && this.draggedFieldId !== card.dataset.fieldId) {
+          const toIndex = this.fields.findIndex(f => f.id === card.dataset.fieldId);
+          
+          if (this.draggedFromIndex !== -1 && toIndex !== -1) {
+            // Perform the move
+            const [draggedField] = this.fields.splice(this.draggedFromIndex, 1);
+            this.fields.splice(toIndex, 0, draggedField);
+            
+            // Re-render
+            this.updateCanvasView();
+            this.updateYamlOutput();
+          }
+        }
+      });
+    });
+    
+    // Setup action buttons
+    this.shadowRoot.querySelectorAll('.icon-button.duplicate').forEach(btn => {
+      btn.addEventListener('click', (e) => {
+        e.stopPropagation();
+        this.duplicateField(btn.dataset.fieldId);
+      });
+    });
+    
+    this.shadowRoot.querySelectorAll('.icon-button.delete').forEach(btn => {
+      btn.addEventListener('click', (e) => {
+        e.stopPropagation();
+        this.deleteField(btn.dataset.fieldId);
+      });
+    });
+    
+    this.updateFieldCount();
+  }
+ 
+  updateAuthorBioState() {
+    const hasAuthorBio = this.fields.some(f => f.field_type === 'author_bio');
+    const btn = this.shadowRoot.querySelector('button[data-field-type="author_bio"]');
+    
+    if (btn) {
+      if (hasAuthorBio) {
+        btn.setAttribute('disabled', 'true');
+        btn.removeAttribute('draggable');
+        btn.title = "Only one Author Bio allowed";
+      } else {
+        btn.removeAttribute('disabled');
+        btn.setAttribute('draggable', 'true');
+        btn.removeAttribute('title');
+      }
+    }
+  }
+ 
+  updateConfigPanel() {
+	  const field = this.fields.find(f => f.id === this.selectedFieldId);
+	  if (!field) {
+		this.shadowRoot.getElementById('configContent').innerHTML = '<div class="no-field-selected">Select a field to edit its properties</div>';
+		return;
+	  }
+	  
+	  const typeConfig = this.fieldTypes[field.field_type];
+	  
+	  // 1. Define Standard Properties
+	  let visibleProps = ['keyname', 'field_type_display', 'name', 'description', 'i18n_descriptions', 'help_text'];
+	  
+	  // 2. Add Type-Specific Properties (These can now be strings OR objects)
+	  if (typeConfig && Array.isArray(typeConfig.properties)) {
+		visibleProps = [...visibleProps, ...typeConfig.properties];
+	  }
+
+	  // 3. Generate HTML
+	  let html = '';
+	  visibleProps.forEach(propEntry => { // propEntry is now string or object
+		
+		// Determine the base key (string or object.key)
+		const propKey = typeof propEntry === 'string' ? propEntry : propEntry.key;
+		
+		// Special case for static display of field type
+		if (propKey === 'field_type_display') {
+		  html += `
+			<div class="form-group">
+			  <label class="form-label">Field Type</label>
+			  <div style="font-size: 13px; color: var(--text-primary); padding: 8px; background: var(--bg-secondary); border-radius: 4px;">${field.field_type}</div>
+			</div>`;
+		  return;
+		}
+
+		let propDef = this.propertyDefinitions[propKey];
+		
+		// --- NEW LOGIC: Merge Overrides ---
+		if (typeof propEntry === 'object' && propDef) {
+			// Create a new definition by merging the base definition with the specific overrides
+			propDef = {...propDef, ...propEntry}; 
+		}
+
+		if (propDef) {
+		  // Pass the key and the potentially merged definition to the renderer
+		  html += this.renderPropertyInput(propKey, propDef, field);
+		}
+	  });
+	  
+	  this.shadowRoot.getElementById('configContent').innerHTML = html;
+	  
+	  // 4. Re-attach Event Listeners
+	  this.attachConfigListeners(field);
+  }
+ 
+  renderPropertyInput(key, def, field) {
+	  const value = field[key] !== undefined ? field[key] : (def.default || '');
+	  
+	  if (def.type === 'custom_categories') {
+		const selectedCategories = field.category ? field.category.split(',').map(c => c.trim()).filter(c => c) : [];
+		return `
+		  <div class="form-group" id="prop-${key}">
+			<label class="form-label">${def.label}</label>
+			<div id="categoryPills" style="margin-bottom: 8px;">
+			  ${this.renderCategoryPills(selectedCategories)}
+			</div>
+			<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">Selected: ${selectedCategories.length}/2</div>
+		  </div>
+		`;
+	  }
+	  
+	  if (def.type === 'checkbox') {
+		return `
+		  <div class="form-group" id="prop-${key}">
+		  <label class="form-checkbox">
+		  <input type="checkbox" data-prop="${key}" ${value ? 'checked' : ''}>
+			${def.label}  </label>
+		  </div>`;
+	  }
+	  
+	  if (def.type === 'textarea') {
+		  // Convert array options to string for display
+		  let displayValue = value;
+		  let useLabelValue = false;
+		  
+		  if (key === 'options' && Array.isArray(value)) {
+			// Check if any option is an object (label:value pair)
+			useLabelValue = value.some(opt => typeof opt === 'object');
+			
+			if (useLabelValue) {
+			  // Convert objects to "Label: value" format
+			  displayValue = value.map(opt => {
+				if (typeof opt === 'object') {
+				  const [label, val] = Object.entries(opt)[0];
+				  return `${label}: ${val}`;
+				}
+				return opt;
+			  }).join('\n');
+			} else {
+			  // Simple string array
+			  displayValue = value.join('\n');
+			}
+		  }
+		  
+		  // Add toggle for options field
+		  const optionsToggle = key === 'options' ? `
+			<label class="form-checkbox" style="margin-bottom: 8px;">
+			  <input type="checkbox" id="use-label-value" ${useLabelValue ? 'checked' : ''}>
+			  Use Label:Value pairs
+			</label>
+			<div id="options-help" style="font-size: 11px; color: var(--text-secondary); margin-bottom: 6px;">
+			  ${useLabelValue ? 'Enter one per line in format "Label: value"' : 'Enter one option per line'}
+			</div>
+		  ` : '';
+		  
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  ${optionsToggle}
+			  <textarea class="form-textarea" data-prop="${key}" placeholder="${def.placeholder || ''}">${displayValue}</textarea>
+			  ${!optionsToggle && def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>`;
+	  }
+	  
+	  // --- i18n_map Renderer ---
+	  if (def.type === 'i18n_map') {
+		// 1. Find existing translations in the field object
+		const existingTranslations = Object.keys(field)
+		  .filter(k => k.startsWith('description-'))
+		  .map(k => {
+			const code = k.replace('description-', '');
+			return { code, value: field[k] };
+		  });
+
+		// 2. Build the list of existing translation inputs
+		let rowsHtml = existingTranslations.map(item => `
+		  <div class="i18n-row" style="margin-top: 8px; border-left: 2px solid var(--accent-color); padding-left: 8px;">
+			<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
+			  <span style="font-weight: 600;">${item.code}</span>
+			  <button class="remove-i18n-btn" data-code="${item.code}" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px;">&times;</button>
+			</div>
+			<textarea class="form-textarea i18n-input" data-code="${item.code}" rows="2">${item.value}</textarea>
+		  </div>
+		`).join('');
+
+		// 3. Build the "Add New" dropdown
+		// Filter out locales that are already added
+		const availableLocales = this.locales.filter(l => !field[`description-${l.code}`]);
+		
+		let addHtml = '';
+		if (availableLocales.length > 0) {
+		  addHtml = `
+			<div style="display: flex; gap: 8px; margin-top: 8px;">
+			  <select id="new-locale-select" class="form-input" style="padding: 4px;">
+				<option value="">Select language...</option>
+				${availableLocales.map(l => `<option value="${l.code}">${l.label} (${l.code})</option>`).join('')}
+			  </select>
+			  <button id="add-locale-btn" class="btn-secondary" style="padding: 4px 8px; font-size: 12px;">Add</button>
+			</div>
+		  `;
+		}
+
+		return `
+		  <div class="form-group" id="prop-${key}">
+			<label class="form-label">${def.label}</label>
+			<div id="i18n-container">
+			  ${rowsHtml}
+			</div>
+			${addHtml}
+			${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+		  </div>`;
+	  }
+	  
+	  // --- Handle generic Select inputs (including http_verb) ---
+	  if (def.options && Array.isArray(def.options)) {
+		  // Determine the current value, falling back to default if not set
+		  const currentValue = field[key] !== undefined ? field[key] : (def.default || '');
+
+		  const optionsHtml = def.options.map(option => {
+			// The option value is the method name itself (GET, POST, etc.)
+			const selected = currentValue === option ? 'selected' : '';
+			return `<option value="${option}" ${selected}>${option}</option>`;
+		  }).join('');
+
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  <select class="form-input" data-prop="${key}">
+				${optionsHtml}
+			  </select>
+			  ${def.help ? `<div style="color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>`;
+	  }
+
+	  // --- Conditional Builder Renderer ---
+	  if (def.type === 'conditional_builder') {
+		  const conditions = field.conditional_validation || [];
+		  
+		  // Get list of other fields for checkboxes
+		  const otherFields = this.fields.filter(f => f.id !== field.id);
+		  
+		  let conditionsHtml = conditions.map((condition, idx) => {
+			const hiddenSet = new Set(condition.hidden || []);
+			
+			const checkboxesHtml = otherFields.map(f => {
+			  const checked = hiddenSet.has(f.keyname) ? 'checked' : '';
+			  return `
+				<label style="display: block; margin: 4px 0; cursor: pointer;">
+				  <input type="checkbox" class="condition-hidden-cb" data-condition-idx="${idx}" data-field-keyname="${f.keyname}" ${checked} style="margin-right: 6px;">
+				  <span style="font-size: 12px;">${f.keyname} (${f.name})</span>
+				</label>
+			  `;
+			}).join('');
+			
+			return `
+			  <div class="condition-block" data-condition-idx="${idx}" style="border-left: 2px solid var(--accent-color); padding-left: 12px; margin-top: 12px;">
+				<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
+				  <div style="display: flex; align-items: center; gap: 8px;">
+					<span style="font-size: 12px; font-weight: 600;">When equals:</span>
+					<input type="text" class="form-input condition-when-input" data-condition-idx="${idx}" value="${condition.when || ''}" style="width: 150px; padding: 4px 8px;">
+				  </div>
+				  <button class="remove-condition-btn" data-condition-idx="${idx}" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 18px; font-weight: bold;">&times;</button>
+				</div>
+				<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; margin-bottom: 6px; color: var(--text-secondary);">Hide these fields:</div>
+				<div style="max-height: 150px; overflow-y: auto; padding: 4px 0;">
+				  ${checkboxesHtml || '<div style="color: var(--text-secondary); font-size: 12px; font-style: italic;">No other fields available</div>'}
+				</div>
+			  </div>
+			`;
+		  }).join('');
+		  
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  <div id="conditional-container">
+				${conditionsHtml}
+			  </div>
+			  <button id="add-condition-btn" style="margin-top: 12px; padding: 6px 12px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; font-size: 12px; width: 100%;">⊕ Add Condition</button>
+			  ${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>
+		  `;
+	  }
+
+	  // --- Special handling for 'default' field ---
+	  if (key === 'default') {
+		  const hasDefault = field.hasOwnProperty('default');
+		  const defaultValue = hasDefault ? (field.default || '') : '';
+		  
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  <label class="form-checkbox" style="margin-bottom: 8px;">
+				<input type="checkbox" id="enable-default" ${hasDefault ? 'checked' : ''}>
+				Set default value
+			  </label>
+			  <input type="${def.type}" class="form-input" id="default-input" data-prop="${key}" value="${defaultValue}" placeholder="${def.placeholder || ''}" ${!hasDefault ? 'disabled' : ''} style="opacity: ${hasDefault ? '1' : '0.5'}">
+			  ${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>`;
+	  }
+
+	  // Default: Text, Number, URL inputs
+	  return `
+		  <div class="form-group" id="prop-${key}">
+			<label class="form-label">${def.label}</label>
+			<input type="${def.type}" class="form-input" data-prop="${key}" value="${value}" placeholder="${def.placeholder || ''}">
+			${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+		  </div>`;
+  }
+ 
+ 
+  attachConfigListeners(field) {
+	  const container = this.shadowRoot.getElementById('configContent');
+
+	  // Generic Inputs
+	  container.querySelectorAll('[data-prop]').forEach(input => {
+		input.addEventListener('input', (e) => this.handleConfigInput(e, field));
+		input.addEventListener('change', (e) => this.handleConfigInput(e, field));
+	  });
+
+	  // Handle Label:Value toggle for options
+	  const labelValueToggle = container.querySelector('#use-label-value');
+	  if (labelValueToggle) {
+	    labelValueToggle.addEventListener('change', (e) => {
+		  const helpDiv = container.querySelector('#options-help');
+		  if (helpDiv) {
+		    helpDiv.textContent = e.target.checked 
+			  ? 'Enter one per line in format "Label: value"'
+			  : 'Enter one option per line';
+		  }
+	    });
+ 	  }
+	  
+	  // Handle "Set default value" checkbox
+	  const enableDefaultCheckbox = container.querySelector('#enable-default');
+		const defaultInput = container.querySelector('#default-input');
+		if (enableDefaultCheckbox && defaultInput) {
+		  enableDefaultCheckbox.addEventListener('change', (e) => {
+			if (e.target.checked) {
+			  // Enable the input and set default to empty string if not already set
+			  defaultInput.disabled = false;
+			  defaultInput.style.opacity = '1';
+			  if (!field.hasOwnProperty('default')) {
+				field.default = '';
+			  }
+			} else {
+			  // Disable the input and remove the default property
+			  defaultInput.disabled = true;
+			  defaultInput.style.opacity = '0.5';
+			  delete field.default;
+			}
+			this.updateYamlOutput();
+		  });
+	  }
+	  
+	  // Special Case: Categories (Pills)
+	  const pills = container.querySelectorAll('.category-pill');
+	  if (pills.length > 0) {
+		pills.forEach(pill => {
+		  pill.addEventListener('click', (e) => {
+			e.preventDefault();
+			e.stopPropagation();
+			
+			const category = pill.dataset.category;
+			// Parse current selection
+			let selected = field.category ? field.category.split(',').map(c => c.trim()).filter(c => c) : [];
+			
+			const index = selected.indexOf(category);
+			
+			if (index > -1) {
+			  // Deselect
+			  selected.splice(index, 1);
+			} else if (selected.length < 2) {
+			  // Select (if under limit)
+			  selected.push(category);
+			} else {
+			  // Limit Reached Feedback
+			  this.showToast('You can only select up to 2 categories');
+			  return; // Stop here so we don't re-render unnecessarily
+			}
+			
+			field.category = selected.join(',');
+			this.updateConfigPanel(); // Re-render to update pill styling
+			this.updateYamlOutput();
+		  });
+		});
+	  }
+	  
+	  // --- i18n Listeners ---
+	  // Handle Input Changes (typing in the translation box)
+	  container.querySelectorAll('.i18n-input').forEach(input => {
+		input.addEventListener('input', (e) => {
+		  const code = e.target.dataset.code;
+		  field[`description-${code}`] = e.target.value;
+		  this.updateYamlOutput();
+		});
+	  });
+
+	  // Handle "Add" Button
+	  const addBtn = container.querySelector('#add-locale-btn');
+	  if (addBtn) {
+		addBtn.addEventListener('click', (e) => {
+		  e.preventDefault();
+		  const select = container.querySelector('#new-locale-select');
+		  const code = select.value;
+		  if (code) {
+			// Initialize the new key
+			field[`description-${code}`] = '';
+			this.updateConfigPanel(); // Re-render to show the new input
+			this.updateYamlOutput();
+		  }
+		});
+	  }
+
+	  // Handle "Remove" Buttons
+	  container.querySelectorAll('.remove-i18n-btn').forEach(btn => {
+		btn.addEventListener('click', (e) => {
+		  e.preventDefault();
+		  const code = e.target.dataset.code;
+		  delete field[`description-${code}`]; // Remove the key entirely
+		  this.updateConfigPanel(); // Re-render to remove the input
+		  this.updateYamlOutput();
+		});
+	  });
+	  
+	  // --- Conditional Logic Listeners ---
+		// Add Condition Button
+		const addConditionBtn = container.querySelector('#add-condition-btn');
+		if (addConditionBtn) {
+		  addConditionBtn.addEventListener('click', (e) => {
+			e.preventDefault();
+			if (!field.conditional_validation) {
+			  field.conditional_validation = [];
+			}
+			field.conditional_validation.push({
+			  when: '',
+			  hidden: []
+			});
+			this.updateConfigPanel();
+			this.updateYamlOutput();
+		  });
+		}
+
+		// Remove Condition Buttons
+		container.querySelectorAll('.remove-condition-btn').forEach(btn => {
+		  btn.addEventListener('click', (e) => {
+			e.preventDefault();
+			const idx = parseInt(e.target.dataset.conditionIdx);
+			field.conditional_validation.splice(idx, 1);
+			if (field.conditional_validation.length === 0) {
+			  delete field.conditional_validation;
+			}
+			this.updateConfigPanel();
+			this.updateYamlOutput();
+		  });
+		});
+
+		// When Value Inputs
+		container.querySelectorAll('.condition-when-input').forEach(input => {
+		  input.addEventListener('input', (e) => {
+			const idx = parseInt(e.target.dataset.conditionIdx);
+			field.conditional_validation[idx].when = e.target.value;
+			this.updateYamlOutput();
+		  });
+		});
+
+		// Hidden Field Checkboxes
+		container.querySelectorAll('.condition-hidden-cb').forEach(cb => {
+		  cb.addEventListener('change', (e) => {
+			const idx = parseInt(e.target.dataset.conditionIdx);
+			const fieldKeyname = e.target.dataset.fieldKeyname;
+			
+			if (!field.conditional_validation[idx].hidden) {
+			  field.conditional_validation[idx].hidden = [];
+			}
+			
+			if (e.target.checked) {
+			  if (!field.conditional_validation[idx].hidden.includes(fieldKeyname)) {
+				field.conditional_validation[idx].hidden.push(fieldKeyname);
+			  }
+			} else {
+			  field.conditional_validation[idx].hidden = field.conditional_validation[idx].hidden.filter(k => k !== fieldKeyname);
+			}
+			
+			this.updateYamlOutput();
+		  });
+		});
+  }
+ 
+  handleConfigInput(e, field) {
+	  const input = e.target;
+	  const prop = input.dataset.prop;
+	  let value;
+	  const container = this.shadowRoot.getElementById('configContent');
+
+	  // 1. Determine Value
+	  if (input.type === 'checkbox') {
+		value = input.checked;
+	  } else if (input.type === 'number') {
+		value = input.value === '' ? undefined : Number(input.value);
+	  } else if (prop === 'options') {
+		  const lines = input.value.split('\n').filter(o => o.trim());
+		  
+		  // Check if we're in label:value mode
+		  const useLabelValue = container.querySelector('#use-label-value')?.checked;
+		  
+		  if (useLabelValue) {
+			// Parse as label:value pairs
+			value = lines.map(line => {
+			  const colonIndex = line.indexOf(':');
+			  if (colonIndex > -1) {
+				const label = line.substring(0, colonIndex).trim();
+				const val = line.substring(colonIndex + 1).trim();
+				return { [label]: val };
+			  }
+			  // Fallback to simple string if no colon found
+			  return line.trim();
+			});
+		  } else {
+			// Simple string array
+			value = lines;
+		  }
+	  } else {
+		value = input.value;
+		// Special handling for 'default' - allow empty string but only if checkbox is checked
+		if (prop === 'default' && value === '') {
+			// If checkbox is checked, explicitly set to empty string
+			// If unchecked, this won't be called because input is disabled
+			field[prop] = '';
+			this.updateYamlOutput();
+			return;
+		}
+	  }
+
+	  // 2. Save Value
+	  field[prop] = value;
+	  
+	  // 3. --- NEW: Auto-populate help text for multiple select ---
+	  if (prop === 'multiple' && value === true && field.field_type === 'select') {
+		// Only populate if currently empty to avoid overwriting user content
+		if (!field.help_text || !field.help_text.trim()) {
+		  field.help_text = 'Use <kbd>⌘</kbd>+<kbd>click</kbd> or <kbd>ctrl</kbd>+<kbd>click</kbd> to select and deselect multiple options.';
+		  
+		  // Force a re-render so the user sees the text appear immediately in the specific field
+		  this.updateConfigPanel(); 
+		}
+	  }
+
+	  // 4. Specific UI updates
+	  if (prop === 'keyname' || prop === 'name') {
+		this.updateCanvasView(); // Refresh the sidebar list items
+	  }
+	  
+	  this.updateYamlOutput();
+  }
+ 
+ 
+  updateYamlOutput() {
+    const yaml = this.generateYaml();
+    this.shadowRoot.getElementById('yamlOutput').textContent = yaml;
+  }
+ 
+  generateYaml() {
+    if (this.fields.length === 0) {
+      return '# No fields defined yet';
+    }
+    
+    const lines = [];
+    
+    this.fields.forEach(field => {
+      lines.push(`- keyname: ${field.keyname}`);
+      lines.push(`  field_type: ${field.field_type}`);
+      lines.push(`  name: ${this.escapeYaml(field.name)}`);
+      
+      if (field.description) {
+        lines.push(`  description: ${this.escapeYaml(field.description)}`);
+      }
+      
+      if (field.help_text) {
+        lines.push(`  help_text: ${this.escapeYaml(field.help_text)}`);
+      }
+      
+      if (field.placeholder) {
+        lines.push(`  placeholder: ${this.escapeYaml(field.placeholder)}`);
+      }
+      
+      if (field.hasOwnProperty('default')) {
+		  lines.push(`  default: ${this.escapeYaml(field.default)}`);
+	  }
+      
+      if (field.optional) {
+        lines.push(`  optional: true`);
+      }
+      
+      if (field.min !== undefined) {
+        lines.push(`  min: ${field.min}`);
+      }
+      
+      if (field.max !== undefined) {
+        lines.push(`  max: ${field.max}`);
+      }
+      
+      if (field.maxlength !== undefined) {
+        lines.push(`  maxlength: ${field.maxlength}`);
+      }
+
+      if (field.rows !== undefined) {
+        lines.push(`  rows: ${field.rows}`);
+      }
+      
+      if (field.options && field.options.length > 0) {
+		  // console.log('Field options:', field.keyname, field.options);
+		  lines.push(`  options:`);
+		  field.options.forEach(opt => {
+			if (typeof opt === 'object') {
+			  // Label:Value pair - format as YAML key:value
+			  const [label, value] = Object.entries(opt)[0];
+			  // Don't escape the label in the key position, only the value
+			  lines.push(`  - ${label}: ${this.escapeYaml(value)}`);
+			} else {
+			  // Simple string
+			  lines.push(`  - ${this.escapeYaml(opt)}`);
+			}
+		  });
+	  }
+      
+      if (field.multiple) {
+        lines.push(`  multiple: true`);
+      }
+      
+      if (field.endpoint) {
+        lines.push(`  endpoint: ${this.escapeYaml(field.endpoint)}`);
+      }
+	  
+      const isXhrField = field.field_type === 'xhrSelect' || field.field_type === 'xhrSelectSearch';
+      if (isXhrField) {
+          // Determine the verb: use the field property value, or fall back to the default 'GET'
+          const verb = field.http_verb || this.propertyDefinitions.http_verb.default;
+          
+          // Always print the http_verb for XHR fields, even if it's the default 'GET'.
+          lines.push(`  http_verb: ${verb}`);
+      } else if (field.http_verb && field.http_verb !== 'GET') {
+          // Keep this secondary check for non-XHR fields that might use a non-default verb 
+          // (though unlikely with current definitions)
+          lines.push(`  http_verb: ${field.http_verb}`);
+      }
+	       
+      if (field.value) {
+        lines.push(`  value: ${this.escapeYaml(field.value)}`);
+      }
+      
+      if (field.category) {
+        lines.push(`  category: ${this.escapeYaml(field.category)}`);
+      }
+      
+      if (field.github_url) {
+        lines.push(`  github_url: ${this.escapeYaml(field.github_url)}`);
+      }
+      if (field.email_address) {
+        lines.push(`  email_address: ${this.escapeYaml(field.email_address)}`);
+      }
+      if (field.learn_more_url) {
+        lines.push(`  learn_more_url: ${this.escapeYaml(field.learn_more_url)}`);
+      }
+      if (field.youtube_url) {
+        lines.push(`  youtube_url: ${this.escapeYaml(field.youtube_url)}`);
+      }
+	  
+	  // Conditional validation
+	  if (field.conditional_validation && field.conditional_validation.length > 0) {
+		lines.push(`  conditional_validation:`);
+		field.conditional_validation.forEach(condition => {
+			lines.push(`  - when: ${this.escapeYaml(condition.when)}`);
+			if (condition.hidden && condition.hidden.length > 0) {
+			  lines.push(`    hidden:`);
+			  condition.hidden.forEach(keyname => {
+				lines.push(`    - ${keyname}`);
+			  });
+			}
+		});
+	  }
+
+	  // Capture any i18n description keys
+	  Object.keys(field).forEach(k => {
+	    if (k.startsWith('description-')) {
+		  lines.push( `  ${k}: "${field[k]}"`);
+	    }
+	  });
+      
+    });
+    
+    return lines.join('\n');
+  }
+ 
+  escapeYaml(value) {
+	  if (!value && value !== 0) return '""';
+	  
+	  // Convert to string if it's a number
+	  if (typeof value === 'number') { return value.toString(); }
+	  
+	  // If it's not a string at this point, return empty
+	  if (typeof value !== 'string') return '""';
+	  
+	  // Check if value needs quoting
+	  if (
+        /[:#\[\]\{\},&\*\|>!'"%@`\n\t]/.test(value) || // Existing special chars
+        /^\d+$/.test(value) ||                       // Existing numeric string check
+        value.startsWith(' ') || value.endsWith(' ') || // Existing whitespace check
+        /^(true|false|yes|no|on|off)$/i.test(value)      // NEW check for YAML literals (case-insensitive)
+      ){
+		  return `"${value.replace(/"/g, '\\"')}"`;
+	  };
+	  
+	  return value;  
+  }
+ 
+  reorderFields(draggedId, targetId) {
+    const draggedIndex = this.fields.findIndex(f => f.id === draggedId);
+    const targetIndex = this.fields.findIndex(f => f.id === targetId);
+    
+    if (draggedIndex !== -1 && targetIndex !== -1) {
+      const [draggedField] = this.fields.splice(draggedIndex, 1);
+      this.fields.splice(targetIndex, 0, draggedField);
+      this.updateCanvasView();
+      this.updateYamlOutput();
+    }
+  }
+ 
+  duplicateField(fieldId) {
+    const field = this.fields.find(f => f.id === fieldId);
+    
+    // Prevent duplicating author_bio (existing logic)
+    if (field && field.field_type === 'author_bio') {
+      this.showToast('Author Bio field cannot be duplicated');
+      return;
+    }
+
+    if (field) {
+      // 1. Create a deep copy of the original field object
+      const newField = JSON.parse(JSON.stringify(field));
+      
+      // 2. Assign a new unique ID
+      const newFieldId = `field_${this.nextFieldId++}`;
+      newField.id = newFieldId;
+      
+      // 3. FIX: Generate a clean, unique keyname based on the field type
+      // Example: 'string' -> 'string_2'
+      newField.keyname = `${field.field_type}_${this.nextFieldId - 1}`;
+      
+      // 4. Insert the new field and update state
+      this.fields.push(newField);
+      this.selectedFieldId = newField.id;
+      
+      // Ensure the config panel opens for the newly duplicated field
+      this.updateConfigPanel();
+      this.updateCanvasView();
+      this.updateYamlOutput();
+      this.updateAuthorBioState();
+    }
+  }
+ 
+  deleteField(fieldId) {
+    this.fields = this.fields.filter(f => f.id !== fieldId);
+    if (this.selectedFieldId === fieldId) {
+      this.selectedFieldId = this.fields[0]?.id || null;
+    }
+    this.updateCanvasView();
+    this.updateConfigPanel();
+    this.updateYamlOutput();
+	this.updateAuthorBioState();
+  }
+ 
+  updateFieldCount() {
+    this.shadowRoot.querySelector('#fieldCount').textContent = this.fields.length;
+  }
+ 
+  switchTab(tab) {
+    this.shadowRoot.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+    this.shadowRoot.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
+    
+    this.shadowRoot.querySelector(`[data-tab="${tab}"]`).classList.add('active');
+    this.shadowRoot.getElementById(`${tab}Tab`).classList.add('active');
+  }
+ 
+  copyYaml() {
+    const yaml = this.generateYaml();
+    navigator.clipboard.writeText(yaml).then(() => {
+      this.showToast('YAML copied to clipboard');
+    });
+  }
+  
+  importYaml() {
+	  // Create modal overlay
+	  const modal = document.createElement('div');
+	  modal.style.cssText = `
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.5);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		z-index: 9999;
+	  `;
+	  
+	  const modalContent = document.createElement('div');
+	  modalContent.style.cssText = `
+		background: ${this.theme === 'dark' ? '#1a1a1a' : '#ffffff'};
+		border: 1px solid ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+		border-radius: 8px;
+		padding: 24px;
+		width: 600px;
+		max-width: 90vw;
+		max-height: 80vh;
+		display: flex;
+		flex-direction: column;
+	  `;
+	  
+	  modalContent.innerHTML = `
+		<h3 style="margin: 0 0 16px 0; color: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'}; font-size: 18px; font-weight: 600;">Import YAML</h3>
+		<textarea id="yamlImportTextarea" placeholder="Paste your YAML here..." style="
+		  flex: 1;
+		  min-height: 300px;
+		  padding: 12px;
+		  border: 1px solid ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+		  border-radius: 4px;
+		  background: ${this.theme === 'dark' ? '#2a2a2a' : '#ffffff'};
+		  color: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+		  font-family: monospace;
+		  font-size: 12px;
+		  resize: vertical;
+		  margin-bottom: 16px;
+		"></textarea>
+		<div style="display: flex; gap: 8px; justify-content: flex-end;">
+		  <button id="cancelImportBtn" style="
+			padding: 8px 16px;
+			border: 1px solid ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+			background: ${this.theme === 'dark' ? '#2a2a2a' : '#f5f5f5'};
+			color: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+			border-radius: 4px;
+			cursor: pointer;
+			font-size: 13px;
+			font-weight: 600;
+		  ">Cancel</button>
+		  <button id="confirmImportBtn" style="
+			padding: 8px 16px;
+			border: none;
+			background: ${this.accentColor};
+			color: white;
+			border-radius: 4px;
+			cursor: pointer;
+			font-size: 13px;
+			font-weight: 600;
+		  ">Import</button>
+		</div>
+	  `;
+	  
+	  modal.appendChild(modalContent);
+	  document.body.appendChild(modal);
+	  
+	  const textarea = modalContent.querySelector('#yamlImportTextarea');
+	  const cancelBtn = modalContent.querySelector('#cancelImportBtn');
+	  const confirmBtn = modalContent.querySelector('#confirmImportBtn');
+	  
+	  // Focus textarea
+	  textarea.focus();
+	  
+	  // Cancel handler
+	  const closeModal = () => {
+		document.body.removeChild(modal);
+	  };
+	  
+	  cancelBtn.addEventListener('click', closeModal);
+	  modal.addEventListener('click', (e) => {
+		if (e.target === modal) closeModal();
+	  });
+	  
+	  // Import handler
+	  confirmBtn.addEventListener('click', () => {
+		const yamlText = textarea.value;
+		
+		if (!yamlText || !yamlText.trim()) {
+		  closeModal();
+		  return;
+		}
+		
+		try {
+		  const fields = this.parseYaml(yamlText);
+		  // console.log('Parsed fields:', fields);
+		  if (fields.length === 0) {
+			this.showToast('No fields found in YAML');
+			closeModal();
+			return;
+		  }
+		  
+		  // Clear existing fields and import new ones
+		  this.fields = fields.map((fieldData, index) => {
+			const field = {
+			  id: `field_${this.nextFieldId++}`,
+			  keyname: fieldData.keyname || `field_${index}`,
+			  field_type: fieldData.field_type || 'string',
+			  name: fieldData.name || '',
+			};
+			
+			// Copy over all other properties
+			Object.keys(fieldData).forEach(key => {
+			  if (key !== 'id') {
+				field[key] = fieldData[key];
+			  }
+			});
+			
+			return field;
+		  });
+		  
+		  this.selectedFieldId = this.fields[0]?.id || null;
+		  this.updateCanvasView();
+		  this.updateConfigPanel();
+		  this.updateYamlOutput();
+		  this.updateAuthorBioState();
+		  this.switchTab('config');
+		  this.showToast(`Imported ${fields.length} field(s)`);
+		  closeModal();
+		} catch (error) {
+		  alert(`Error parsing YAML: ${error.message}`);
+		}
+	  });
+	}
+  
+	parseYaml(yamlText) {
+		const fields = [];
+		const lines = yamlText.split('\n');
+		let currentField = null;
+		let currentArray = null;
+		let currentArrayKey = null;
+		let currentCondition = null;
+		let currentNestedKey = null; // Helper to track if we are in hidden/required
+
+		lines.forEach(line => {
+		  // Skip comments and empty lines
+		  if (line.trim().startsWith('#') || !line.trim()) {
+			return;
+		  }
+
+		  // 1. New field (starts with - keyname:)
+		  if (line.match(/^-\s+keyname:/)) {
+			if (currentField) {
+			  fields.push(currentField);
+			}
+			currentField = {};
+			currentArray = null;
+			currentArrayKey = null;
+			currentCondition = null;
+			currentNestedKey = null;
+			const value = line.split('keyname:')[1].trim();
+			currentField.keyname = this.unescapeYaml(value);
+		  }
+		  
+		  // 2. Field properties (2 spaces indentation)
+		  else if (currentField && line.match(/^\s{2}[a-z_-]+:/)) {
+			const match = line.match(/^\s{2}([a-z_-]+):\s*(.*)$/);
+			if (match) {
+			  const key = match[1];
+			  const value = match[2].trim();
+
+			  // Handle array start (value is empty)
+			  if (!value || value === '') {
+				currentArrayKey = key;
+				currentArray = [];
+
+				if (key === 'conditional_validation') {
+				  currentField[key] = [];
+				} else {
+				  currentField[key] = currentArray;
+				}
+			  } else {
+				// Scalar values
+				currentArrayKey = null;
+				currentArray = null;
+				currentField[key] = this.unescapeYaml(value);
+
+				// Convert string booleans
+				if (value === 'true') currentField[key] = true;
+				if (value === 'false') currentField[key] = false;
+
+				// Convert numbers (but not empty strings!)
+				if (!isNaN(value) && value !== '' && value !== '""') {
+				  currentField[key] = Number(value);
+				}
+			  }
+			}
+		  }
+
+		  // 3. Nested properties in conditions (e.g., "    hidden:")
+		  // We check this BEFORE array items to set context
+		  else if (currentCondition && line.match(/^\s{4}[a-z_-]+:/)) {
+			const match = line.match(/^\s{4}([a-z_]+):\s*(.*)$/);
+			if (match) {
+			  const key = match[1];
+			  // Initialize the nested array
+			  currentCondition[key] = [];
+			  currentNestedKey = key; // Track that we are currently filling this key
+			}
+		  }
+
+		  // 4. Nested Array Items (Deep indentation: 4 or more spaces + dash)
+		  // IMPORTANT: This must come BEFORE the generic array check
+		  else if (currentCondition && line.match(/^\s{4,}-\s+/)) {
+			const value = line.replace(/^\s{4,}-\s+/, '').trim();
+			
+			// Add to the currently active nested array (hidden or required)
+			if (currentNestedKey && Array.isArray(currentCondition[currentNestedKey])) {
+			   currentCondition[currentNestedKey].push(value);
+			}
+		  }
+
+		  // 5. Top-Level Array items (2 or more spaces + dash)
+		  // But we've already handled conditional nested items above with a more specific check
+		  else if (line.match(/^\s{2,}-\s+/)) {
+			const value = line.replace(/^\s{2,}-\s+/, '').trim();
+
+			// If we're in conditional_validation, this is a NEW condition object
+			if (currentArrayKey === 'conditional_validation') {
+			  currentCondition = {};
+			  currentField.conditional_validation.push(currentCondition);
+			  currentNestedKey = null; // Reset nested context for new object
+
+			  // Parse the "when" value
+			  if (value.startsWith('when:')) {
+				currentCondition.when = this.unescapeYaml(value.split('when:')[1].trim());
+			  }
+			}
+			// Otherwise it's a regular array item (like options)
+			else if (currentArray) {
+			  const colonIndex = value.indexOf(':');
+			  if (currentArrayKey === 'options' && colonIndex > -1) {
+				const label = value.substring(0, colonIndex).trim();
+				const val = value.substring(colonIndex + 1).trim();
+				currentArray.push({ [this.unescapeYaml(label)]: this.unescapeYaml(val) });
+			  } else {
+				currentArray.push(this.unescapeYaml(value));
+			  }
+			}
+		  }
+		});
+
+		// Don't forget the last field
+		if (currentField) {
+		  fields.push(currentField);
+		}
+
+		return fields;
+	}
+ 
+  unescapeYaml(value) {
+	  if (!value && value !== 0) return '';
+
+	  // If it's not a string, just return it as-is
+	  if (typeof value !== 'string') return value;
+	  
+	  // Remove surrounding quotes
+	  value = value.replace(/^["'](.*)["']$/, '$1');
+	  
+	  // Unescape escaped quotes
+	  value = value.replace(/\\"/g, '"');
+	  
+	  return value;
+  }
+ 
+  showToast(message) {
+    const toast = document.createElement('div');
+    toast.className = 'toast';
+    toast.textContent = message;
+    toast.style.cssText = `
+      position: fixed;
+      bottom: 20px;
+      right: 20px;
+      padding: 12px 16px;
+      background: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+      color: ${this.theme === 'dark' ? '#1a1a1a' : '#e0e0e0'};
+      border-radius: 4px;
+      font-size: 150%;
+      font-weight: 500;
+      z-index: 1000;
+      animation: slideIn 0.3s ease-out;
+    `;
+    document.body.appendChild(toast);
+    setTimeout(() => toast.remove(), 2000);
+  }
+ 
+  // Public API methods
+  getYaml() {
+    return this.generateYaml();
+  }
+ 
+  getFields() {
+    return JSON.parse(JSON.stringify(this.fields));
+  }
+ 
+  setFields(fields) {
+    this.fields = JSON.parse(JSON.stringify(fields));
+    this.selectedFieldId = this.fields[0]?.id || null;
+    this.updateCanvasView();
+    this.updateConfigPanel();
+    this.updateYamlOutput();
+	this.updateAuthorBioState(); // Check immediately on render
+  }
+ 
+  clear() {
+    this.fields = [];
+    this.selectedFieldId = null;
+	this.draggedFieldId = null;
+    this.draggedFromIndex = null;
+	this.nextFieldId = 1;
+    this.render();
+    this.setupEventListeners();
+    this.updateCanvasView();
+	this.updateConfigPanel();
+    this.updateYamlOutput();
+	this.updateAuthorBioState(); // Check immediately on render
+  }
+}
+ 
+// Register the custom element
+customElements.define('trmnl-form-builder', TRMLYamlForm);
+ 
+ 
+ 
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..3c32439 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,2252 @@ +TN: +SF:trmnl-form-builder.js +FN:16,TRMLYamlForm +FN:185,connectedCallback +FN:190,loadCategories +FN:210,getFallbackCategories +FN:242,render +FN:768,renderFieldTypes +FN:802,renderCategoryPills +FN:831,setupEventListeners +FN:864,handleFieldTypeDragStart +FN:869,handleCanvasDragOver +FN:875,handleCanvasDragLeave +FN:881,handleCanvasDrop +FN:890,addField +FN:936,updateCanvasView +FN:1069,updateAuthorBioState +FN:1086,updateConfigPanel +FN:1140,renderPropertyInput +FN:1355,attachConfigListeners +FN:1533,handleConfigInput +FN:1601,updateYamlOutput +FN:1606,generateYaml +FN:1738,escapeYaml +FN:1760,reorderFields +FN:1772,duplicateField +FN:1805,deleteField +FN:1816,updateFieldCount +FN:1820,switchTab +FN:1828,copyYaml +FN:1835,importYaml +FN:1974,parseYaml +FN:2099,unescapeYaml +FN:2114,showToast +FN:2136,getYaml +FN:2140,getFields +FN:2144,setFields +FN:2153,clear +FNF:36 +FNH:0 +FNDA:0,TRMLYamlForm +FNDA:0,connectedCallback +FNDA:0,loadCategories +FNDA:0,getFallbackCategories +FNDA:0,render +FNDA:0,renderFieldTypes +FNDA:0,renderCategoryPills +FNDA:0,setupEventListeners +FNDA:0,handleFieldTypeDragStart +FNDA:0,handleCanvasDragOver +FNDA:0,handleCanvasDragLeave +FNDA:0,handleCanvasDrop +FNDA:0,addField +FNDA:0,updateCanvasView +FNDA:0,updateAuthorBioState +FNDA:0,updateConfigPanel +FNDA:0,renderPropertyInput +FNDA:0,attachConfigListeners +FNDA:0,handleConfigInput +FNDA:0,updateYamlOutput +FNDA:0,generateYaml +FNDA:0,escapeYaml +FNDA:0,reorderFields +FNDA:0,duplicateField +FNDA:0,deleteField +FNDA:0,updateFieldCount +FNDA:0,switchTab +FNDA:0,copyYaml +FNDA:0,importYaml +FNDA:0,parseYaml +FNDA:0,unescapeYaml +FNDA:0,showToast +FNDA:0,getYaml +FNDA:0,getFields +FNDA:0,setFields +FNDA:0,clear +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,1 +DA:15,1 +DA:16,1 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,1 +DA:185,1 +DA:186,0 +DA:187,0 +DA:188,0 +DA:189,1 +DA:190,1 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:209,1 +DA:210,1 +DA:211,0 +DA:212,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:218,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,1 +DA:242,1 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:261,0 +DA:262,0 +DA:263,0 +DA:264,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:281,0 +DA:282,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:286,0 +DA:287,0 +DA:288,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:293,0 +DA:294,0 +DA:295,0 +DA:296,0 +DA:297,0 +DA:298,0 +DA:299,0 +DA:300,0 +DA:301,0 +DA:302,0 +DA:303,0 +DA:304,0 +DA:305,0 +DA:306,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:316,0 +DA:317,0 +DA:318,0 +DA:319,0 +DA:320,0 +DA:321,0 +DA:322,0 +DA:323,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:328,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:332,0 +DA:333,0 +DA:334,0 +DA:335,0 +DA:336,0 +DA:337,0 +DA:338,0 +DA:339,0 +DA:340,0 +DA:341,0 +DA:342,0 +DA:343,0 +DA:344,0 +DA:345,0 +DA:346,0 +DA:347,0 +DA:348,0 +DA:349,0 +DA:350,0 +DA:351,0 +DA:352,0 +DA:353,0 +DA:354,0 +DA:355,0 +DA:356,0 +DA:357,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:361,0 +DA:362,0 +DA:363,0 +DA:364,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:369,0 +DA:370,0 +DA:371,0 +DA:372,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:376,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:380,0 +DA:381,0 +DA:382,0 +DA:383,0 +DA:384,0 +DA:385,0 +DA:386,0 +DA:387,0 +DA:388,0 +DA:389,0 +DA:390,0 +DA:391,0 +DA:392,0 +DA:393,0 +DA:394,0 +DA:395,0 +DA:396,0 +DA:397,0 +DA:398,0 +DA:399,0 +DA:400,0 +DA:401,0 +DA:402,0 +DA:403,0 +DA:404,0 +DA:405,0 +DA:406,0 +DA:407,0 +DA:408,0 +DA:409,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:413,0 +DA:414,0 +DA:415,0 +DA:416,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:420,0 +DA:421,0 +DA:422,0 +DA:423,0 +DA:424,0 +DA:425,0 +DA:426,0 +DA:427,0 +DA:428,0 +DA:429,0 +DA:430,0 +DA:431,0 +DA:432,0 +DA:433,0 +DA:434,0 +DA:435,0 +DA:436,0 +DA:437,0 +DA:438,0 +DA:439,0 +DA:440,0 +DA:441,0 +DA:442,0 +DA:443,0 +DA:444,0 +DA:445,0 +DA:446,0 +DA:447,0 +DA:448,0 +DA:449,0 +DA:450,0 +DA:451,0 +DA:452,0 +DA:453,0 +DA:454,0 +DA:455,0 +DA:456,0 +DA:457,0 +DA:458,0 +DA:459,0 +DA:460,0 +DA:461,0 +DA:462,0 +DA:463,0 +DA:464,0 +DA:465,0 +DA:466,0 +DA:467,0 +DA:468,0 +DA:469,0 +DA:470,0 +DA:471,0 +DA:472,0 +DA:473,0 +DA:474,0 +DA:475,0 +DA:476,0 +DA:477,0 +DA:478,0 +DA:479,0 +DA:480,0 +DA:481,0 +DA:482,0 +DA:483,0 +DA:484,0 +DA:485,0 +DA:486,0 +DA:487,0 +DA:488,0 +DA:489,0 +DA:490,0 +DA:491,0 +DA:492,0 +DA:493,0 +DA:494,0 +DA:495,0 +DA:496,0 +DA:497,0 +DA:498,0 +DA:499,0 +DA:500,0 +DA:501,0 +DA:502,0 +DA:503,0 +DA:504,0 +DA:505,0 +DA:506,0 +DA:507,0 +DA:508,0 +DA:509,0 +DA:510,0 +DA:511,0 +DA:512,0 +DA:513,0 +DA:514,0 +DA:515,0 +DA:516,0 +DA:517,0 +DA:518,0 +DA:519,0 +DA:520,0 +DA:521,0 +DA:522,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:526,0 +DA:527,0 +DA:528,0 +DA:529,0 +DA:530,0 +DA:531,0 +DA:532,0 +DA:533,0 +DA:534,0 +DA:535,0 +DA:536,0 +DA:537,0 +DA:538,0 +DA:539,0 +DA:540,0 +DA:541,0 +DA:542,0 +DA:543,0 +DA:544,0 +DA:545,0 +DA:546,0 +DA:547,0 +DA:548,0 +DA:549,0 +DA:550,0 +DA:551,0 +DA:552,0 +DA:553,0 +DA:554,0 +DA:555,0 +DA:556,0 +DA:557,0 +DA:558,0 +DA:559,0 +DA:560,0 +DA:561,0 +DA:562,0 +DA:563,0 +DA:564,0 +DA:565,0 +DA:566,0 +DA:567,0 +DA:568,0 +DA:569,0 +DA:570,0 +DA:571,0 +DA:572,0 +DA:573,0 +DA:574,0 +DA:575,0 +DA:576,0 +DA:577,0 +DA:578,0 +DA:579,0 +DA:580,0 +DA:581,0 +DA:582,0 +DA:583,0 +DA:584,0 +DA:585,0 +DA:586,0 +DA:587,0 +DA:588,0 +DA:589,0 +DA:590,0 +DA:591,0 +DA:592,0 +DA:593,0 +DA:594,0 +DA:595,0 +DA:596,0 +DA:597,0 +DA:598,0 +DA:599,0 +DA:600,0 +DA:601,0 +DA:602,0 +DA:603,0 +DA:604,0 +DA:605,0 +DA:606,0 +DA:607,0 +DA:608,0 +DA:609,0 +DA:610,0 +DA:611,0 +DA:612,0 +DA:613,0 +DA:614,0 +DA:615,0 +DA:616,0 +DA:617,0 +DA:618,0 +DA:619,0 +DA:620,0 +DA:621,0 +DA:622,0 +DA:623,0 +DA:624,0 +DA:625,0 +DA:626,0 +DA:627,0 +DA:628,0 +DA:629,0 +DA:630,0 +DA:631,0 +DA:632,0 +DA:633,0 +DA:634,0 +DA:635,0 +DA:636,0 +DA:637,0 +DA:638,0 +DA:639,0 +DA:640,0 +DA:641,0 +DA:642,0 +DA:643,0 +DA:644,0 +DA:645,0 +DA:646,0 +DA:647,0 +DA:648,0 +DA:649,0 +DA:650,0 +DA:651,0 +DA:652,0 +DA:653,0 +DA:654,0 +DA:655,0 +DA:656,0 +DA:657,0 +DA:658,0 +DA:659,0 +DA:660,0 +DA:661,0 +DA:662,0 +DA:663,0 +DA:664,0 +DA:665,0 +DA:666,0 +DA:667,0 +DA:668,0 +DA:669,0 +DA:670,0 +DA:671,0 +DA:672,0 +DA:673,0 +DA:674,0 +DA:675,0 +DA:676,0 +DA:677,0 +DA:678,0 +DA:679,0 +DA:680,0 +DA:681,0 +DA:682,0 +DA:683,0 +DA:684,0 +DA:685,0 +DA:686,0 +DA:687,0 +DA:688,0 +DA:689,0 +DA:690,0 +DA:691,0 +DA:692,0 +DA:693,0 +DA:694,0 +DA:695,0 +DA:696,0 +DA:697,0 +DA:698,0 +DA:699,0 +DA:700,0 +DA:701,0 +DA:702,0 +DA:703,0 +DA:704,0 +DA:705,0 +DA:706,0 +DA:707,0 +DA:708,0 +DA:709,0 +DA:710,0 +DA:711,0 +DA:712,0 +DA:713,0 +DA:714,0 +DA:715,0 +DA:716,0 +DA:717,0 +DA:718,0 +DA:719,0 +DA:720,0 +DA:721,0 +DA:722,0 +DA:723,0 +DA:724,0 +DA:725,0 +DA:726,0 +DA:727,0 +DA:728,0 +DA:729,0 +DA:730,0 +DA:731,0 +DA:732,0 +DA:733,0 +DA:734,0 +DA:735,0 +DA:736,0 +DA:737,0 +DA:738,0 +DA:739,0 +DA:740,0 +DA:741,0 +DA:742,0 +DA:743,0 +DA:744,0 +DA:745,0 +DA:746,0 +DA:747,0 +DA:748,0 +DA:749,0 +DA:750,0 +DA:751,0 +DA:752,0 +DA:753,0 +DA:754,0 +DA:755,0 +DA:756,0 +DA:757,0 +DA:758,0 +DA:759,0 +DA:760,0 +DA:761,0 +DA:762,0 +DA:763,0 +DA:764,0 +DA:765,0 +DA:766,0 +DA:767,1 +DA:768,1 +DA:769,0 +DA:770,0 +DA:771,0 +DA:772,0 +DA:773,0 +DA:774,0 +DA:775,0 +DA:776,0 +DA:777,0 +DA:778,0 +DA:779,0 +DA:780,0 +DA:781,0 +DA:782,0 +DA:783,0 +DA:784,0 +DA:785,0 +DA:786,0 +DA:787,0 +DA:788,0 +DA:789,0 +DA:790,0 +DA:791,0 +DA:792,0 +DA:793,0 +DA:794,0 +DA:795,0 +DA:796,0 +DA:797,0 +DA:798,0 +DA:799,0 +DA:800,0 +DA:801,1 +DA:802,1 +DA:803,0 +DA:804,0 +DA:805,0 +DA:806,0 +DA:807,0 +DA:808,0 +DA:809,0 +DA:810,0 +DA:811,0 +DA:812,0 +DA:813,0 +DA:814,0 +DA:815,0 +DA:816,0 +DA:817,0 +DA:818,0 +DA:819,0 +DA:820,0 +DA:821,0 +DA:822,0 +DA:823,0 +DA:824,0 +DA:825,0 +DA:826,0 +DA:827,0 +DA:828,0 +DA:829,0 +DA:830,1 +DA:831,1 +DA:832,0 +DA:833,0 +DA:834,0 +DA:835,0 +DA:836,0 +DA:837,0 +DA:838,0 +DA:839,0 +DA:840,0 +DA:841,0 +DA:842,0 +DA:843,0 +DA:844,0 +DA:845,0 +DA:846,0 +DA:847,0 +DA:848,0 +DA:849,0 +DA:850,0 +DA:851,0 +DA:852,0 +DA:853,0 +DA:854,0 +DA:855,0 +DA:856,0 +DA:857,0 +DA:858,0 +DA:859,0 +DA:860,0 +DA:861,0 +DA:862,0 +DA:863,1 +DA:864,1 +DA:865,0 +DA:866,0 +DA:867,0 +DA:868,1 +DA:869,1 +DA:870,0 +DA:871,0 +DA:872,0 +DA:873,0 +DA:874,1 +DA:875,1 +DA:876,0 +DA:877,0 +DA:878,0 +DA:879,0 +DA:880,1 +DA:881,1 +DA:882,0 +DA:883,0 +DA:884,0 +DA:885,0 +DA:886,0 +DA:887,0 +DA:888,0 +DA:889,1 +DA:890,1 +DA:891,0 +DA:892,0 +DA:893,0 +DA:894,0 +DA:895,0 +DA:896,0 +DA:897,0 +DA:898,0 +DA:899,0 +DA:900,0 +DA:901,0 +DA:902,0 +DA:903,0 +DA:904,0 +DA:905,0 +DA:906,0 +DA:907,0 +DA:908,0 +DA:909,0 +DA:910,0 +DA:911,0 +DA:912,0 +DA:913,0 +DA:914,0 +DA:915,0 +DA:916,0 +DA:917,0 +DA:918,0 +DA:919,0 +DA:920,0 +DA:921,0 +DA:922,0 +DA:923,0 +DA:924,0 +DA:925,0 +DA:926,0 +DA:927,0 +DA:928,0 +DA:929,0 +DA:930,0 +DA:931,0 +DA:932,0 +DA:933,0 +DA:934,0 +DA:935,1 +DA:936,1 +DA:937,0 +DA:938,0 +DA:939,0 +DA:940,0 +DA:941,0 +DA:942,0 +DA:943,0 +DA:944,0 +DA:945,0 +DA:946,0 +DA:947,0 +DA:948,0 +DA:949,0 +DA:950,0 +DA:951,0 +DA:952,0 +DA:953,0 +DA:954,0 +DA:955,0 +DA:956,0 +DA:957,0 +DA:958,0 +DA:959,0 +DA:960,0 +DA:961,0 +DA:962,0 +DA:963,0 +DA:964,0 +DA:965,0 +DA:966,0 +DA:967,0 +DA:968,0 +DA:969,0 +DA:970,0 +DA:971,0 +DA:972,0 +DA:973,0 +DA:974,0 +DA:975,0 +DA:976,0 +DA:977,0 +DA:978,0 +DA:979,0 +DA:980,0 +DA:981,0 +DA:982,0 +DA:983,0 +DA:984,0 +DA:985,0 +DA:986,0 +DA:987,0 +DA:988,0 +DA:989,0 +DA:990,0 +DA:991,0 +DA:992,0 +DA:993,0 +DA:994,0 +DA:995,0 +DA:996,0 +DA:997,0 +DA:998,0 +DA:999,0 +DA:1000,0 +DA:1001,0 +DA:1002,0 +DA:1003,0 +DA:1004,0 +DA:1005,0 +DA:1006,0 +DA:1007,0 +DA:1008,0 +DA:1009,0 +DA:1010,0 +DA:1011,0 +DA:1012,0 +DA:1013,0 +DA:1014,0 +DA:1015,0 +DA:1016,0 +DA:1017,0 +DA:1018,0 +DA:1019,0 +DA:1020,0 +DA:1021,0 +DA:1022,0 +DA:1023,0 +DA:1024,0 +DA:1025,0 +DA:1026,0 +DA:1027,0 +DA:1028,0 +DA:1029,0 +DA:1030,0 +DA:1031,0 +DA:1032,0 +DA:1033,0 +DA:1034,0 +DA:1035,0 +DA:1036,0 +DA:1037,0 +DA:1038,0 +DA:1039,0 +DA:1040,0 +DA:1041,0 +DA:1042,0 +DA:1043,0 +DA:1044,0 +DA:1045,0 +DA:1046,0 +DA:1047,0 +DA:1048,0 +DA:1049,0 +DA:1050,0 +DA:1051,0 +DA:1052,0 +DA:1053,0 +DA:1054,0 +DA:1055,0 +DA:1056,0 +DA:1057,0 +DA:1058,0 +DA:1059,0 +DA:1060,0 +DA:1061,0 +DA:1062,0 +DA:1063,0 +DA:1064,0 +DA:1065,0 +DA:1066,0 +DA:1067,0 +DA:1068,1 +DA:1069,1 +DA:1070,0 +DA:1071,0 +DA:1072,0 +DA:1073,0 +DA:1074,0 +DA:1075,0 +DA:1076,0 +DA:1077,0 +DA:1078,0 +DA:1079,0 +DA:1080,0 +DA:1081,0 +DA:1082,0 +DA:1083,0 +DA:1084,0 +DA:1085,1 +DA:1086,1 +DA:1087,0 +DA:1088,0 +DA:1089,0 +DA:1090,0 +DA:1091,0 +DA:1092,0 +DA:1093,0 +DA:1094,0 +DA:1095,0 +DA:1096,0 +DA:1097,0 +DA:1098,0 +DA:1099,0 +DA:1100,0 +DA:1101,0 +DA:1102,0 +DA:1103,0 +DA:1104,0 +DA:1105,0 +DA:1106,0 +DA:1107,0 +DA:1108,0 +DA:1109,0 +DA:1110,0 +DA:1111,0 +DA:1112,0 +DA:1113,0 +DA:1114,0 +DA:1115,0 +DA:1116,0 +DA:1117,0 +DA:1118,0 +DA:1119,0 +DA:1120,0 +DA:1121,0 +DA:1122,0 +DA:1123,0 +DA:1124,0 +DA:1125,0 +DA:1126,0 +DA:1127,0 +DA:1128,0 +DA:1129,0 +DA:1130,0 +DA:1131,0 +DA:1132,0 +DA:1133,0 +DA:1134,0 +DA:1135,0 +DA:1136,0 +DA:1137,0 +DA:1138,0 +DA:1139,1 +DA:1140,1 +DA:1141,0 +DA:1142,0 +DA:1143,0 +DA:1144,0 +DA:1145,0 +DA:1146,0 +DA:1147,0 +DA:1148,0 +DA:1149,0 +DA:1150,0 +DA:1151,0 +DA:1152,0 +DA:1153,0 +DA:1154,0 +DA:1155,0 +DA:1156,0 +DA:1157,0 +DA:1158,0 +DA:1159,0 +DA:1160,0 +DA:1161,0 +DA:1162,0 +DA:1163,0 +DA:1164,0 +DA:1165,0 +DA:1166,0 +DA:1167,0 +DA:1168,0 +DA:1169,0 +DA:1170,0 +DA:1171,0 +DA:1172,0 +DA:1173,0 +DA:1174,0 +DA:1175,0 +DA:1176,0 +DA:1177,0 +DA:1178,0 +DA:1179,0 +DA:1180,0 +DA:1181,0 +DA:1182,0 +DA:1183,0 +DA:1184,0 +DA:1185,0 +DA:1186,0 +DA:1187,0 +DA:1188,0 +DA:1189,0 +DA:1190,0 +DA:1191,0 +DA:1192,0 +DA:1193,0 +DA:1194,0 +DA:1195,0 +DA:1196,0 +DA:1197,0 +DA:1198,0 +DA:1199,0 +DA:1200,0 +DA:1201,0 +DA:1202,0 +DA:1203,0 +DA:1204,0 +DA:1205,0 +DA:1206,0 +DA:1207,0 +DA:1208,0 +DA:1209,0 +DA:1210,0 +DA:1211,0 +DA:1212,0 +DA:1213,0 +DA:1214,0 +DA:1215,0 +DA:1216,0 +DA:1217,0 +DA:1218,0 +DA:1219,0 +DA:1220,0 +DA:1221,0 +DA:1222,0 +DA:1223,0 +DA:1224,0 +DA:1225,0 +DA:1226,0 +DA:1227,0 +DA:1228,0 +DA:1229,0 +DA:1230,0 +DA:1231,0 +DA:1232,0 +DA:1233,0 +DA:1234,0 +DA:1235,0 +DA:1236,0 +DA:1237,0 +DA:1238,0 +DA:1239,0 +DA:1240,0 +DA:1241,0 +DA:1242,0 +DA:1243,0 +DA:1244,0 +DA:1245,0 +DA:1246,0 +DA:1247,0 +DA:1248,0 +DA:1249,0 +DA:1250,0 +DA:1251,0 +DA:1252,0 +DA:1253,0 +DA:1254,0 +DA:1255,0 +DA:1256,0 +DA:1257,0 +DA:1258,0 +DA:1259,0 +DA:1260,0 +DA:1261,0 +DA:1262,0 +DA:1263,0 +DA:1264,0 +DA:1265,0 +DA:1266,0 +DA:1267,0 +DA:1268,0 +DA:1269,0 +DA:1270,0 +DA:1271,0 +DA:1272,0 +DA:1273,0 +DA:1274,0 +DA:1275,0 +DA:1276,0 +DA:1277,0 +DA:1278,0 +DA:1279,0 +DA:1280,0 +DA:1281,0 +DA:1282,0 +DA:1283,0 +DA:1284,0 +DA:1285,0 +DA:1286,0 +DA:1287,0 +DA:1288,0 +DA:1289,0 +DA:1290,0 +DA:1291,0 +DA:1292,0 +DA:1293,0 +DA:1294,0 +DA:1295,0 +DA:1296,0 +DA:1297,0 +DA:1298,0 +DA:1299,0 +DA:1300,0 +DA:1301,0 +DA:1302,0 +DA:1303,0 +DA:1304,0 +DA:1305,0 +DA:1306,0 +DA:1307,0 +DA:1308,0 +DA:1309,0 +DA:1310,0 +DA:1311,0 +DA:1312,0 +DA:1313,0 +DA:1314,0 +DA:1315,0 +DA:1316,0 +DA:1317,0 +DA:1318,0 +DA:1319,0 +DA:1320,0 +DA:1321,0 +DA:1322,0 +DA:1323,0 +DA:1324,0 +DA:1325,0 +DA:1326,0 +DA:1327,0 +DA:1328,0 +DA:1329,0 +DA:1330,0 +DA:1331,0 +DA:1332,0 +DA:1333,0 +DA:1334,0 +DA:1335,0 +DA:1336,0 +DA:1337,0 +DA:1338,0 +DA:1339,0 +DA:1340,0 +DA:1341,0 +DA:1342,0 +DA:1343,0 +DA:1344,0 +DA:1345,0 +DA:1346,0 +DA:1347,0 +DA:1348,0 +DA:1349,0 +DA:1350,0 +DA:1351,0 +DA:1352,0 +DA:1353,1 +DA:1354,1 +DA:1355,1 +DA:1356,0 +DA:1357,0 +DA:1358,0 +DA:1359,0 +DA:1360,0 +DA:1361,0 +DA:1362,0 +DA:1363,0 +DA:1364,0 +DA:1365,0 +DA:1366,0 +DA:1367,0 +DA:1368,0 +DA:1369,0 +DA:1370,0 +DA:1371,0 +DA:1372,0 +DA:1373,0 +DA:1374,0 +DA:1375,0 +DA:1376,0 +DA:1377,0 +DA:1378,0 +DA:1379,0 +DA:1380,0 +DA:1381,0 +DA:1382,0 +DA:1383,0 +DA:1384,0 +DA:1385,0 +DA:1386,0 +DA:1387,0 +DA:1388,0 +DA:1389,0 +DA:1390,0 +DA:1391,0 +DA:1392,0 +DA:1393,0 +DA:1394,0 +DA:1395,0 +DA:1396,0 +DA:1397,0 +DA:1398,0 +DA:1399,0 +DA:1400,0 +DA:1401,0 +DA:1402,0 +DA:1403,0 +DA:1404,0 +DA:1405,0 +DA:1406,0 +DA:1407,0 +DA:1408,0 +DA:1409,0 +DA:1410,0 +DA:1411,0 +DA:1412,0 +DA:1413,0 +DA:1414,0 +DA:1415,0 +DA:1416,0 +DA:1417,0 +DA:1418,0 +DA:1419,0 +DA:1420,0 +DA:1421,0 +DA:1422,0 +DA:1423,0 +DA:1424,0 +DA:1425,0 +DA:1426,0 +DA:1427,0 +DA:1428,0 +DA:1429,0 +DA:1430,0 +DA:1431,0 +DA:1432,0 +DA:1433,0 +DA:1434,0 +DA:1435,0 +DA:1436,0 +DA:1437,0 +DA:1438,0 +DA:1439,0 +DA:1440,0 +DA:1441,0 +DA:1442,0 +DA:1443,0 +DA:1444,0 +DA:1445,0 +DA:1446,0 +DA:1447,0 +DA:1448,0 +DA:1449,0 +DA:1450,0 +DA:1451,0 +DA:1452,0 +DA:1453,0 +DA:1454,0 +DA:1455,0 +DA:1456,0 +DA:1457,0 +DA:1458,0 +DA:1459,0 +DA:1460,0 +DA:1461,0 +DA:1462,0 +DA:1463,0 +DA:1464,0 +DA:1465,0 +DA:1466,0 +DA:1467,0 +DA:1468,0 +DA:1469,0 +DA:1470,0 +DA:1471,0 +DA:1472,0 +DA:1473,0 +DA:1474,0 +DA:1475,0 +DA:1476,0 +DA:1477,0 +DA:1478,0 +DA:1479,0 +DA:1480,0 +DA:1481,0 +DA:1482,0 +DA:1483,0 +DA:1484,0 +DA:1485,0 +DA:1486,0 +DA:1487,0 +DA:1488,0 +DA:1489,0 +DA:1490,0 +DA:1491,0 +DA:1492,0 +DA:1493,0 +DA:1494,0 +DA:1495,0 +DA:1496,0 +DA:1497,0 +DA:1498,0 +DA:1499,0 +DA:1500,0 +DA:1501,0 +DA:1502,0 +DA:1503,0 +DA:1504,0 +DA:1505,0 +DA:1506,0 +DA:1507,0 +DA:1508,0 +DA:1509,0 +DA:1510,0 +DA:1511,0 +DA:1512,0 +DA:1513,0 +DA:1514,0 +DA:1515,0 +DA:1516,0 +DA:1517,0 +DA:1518,0 +DA:1519,0 +DA:1520,0 +DA:1521,0 +DA:1522,0 +DA:1523,0 +DA:1524,0 +DA:1525,0 +DA:1526,0 +DA:1527,0 +DA:1528,0 +DA:1529,0 +DA:1530,0 +DA:1531,0 +DA:1532,1 +DA:1533,1 +DA:1534,0 +DA:1535,0 +DA:1536,0 +DA:1537,0 +DA:1538,0 +DA:1539,0 +DA:1540,0 +DA:1541,0 +DA:1542,0 +DA:1543,0 +DA:1544,0 +DA:1545,0 +DA:1546,0 +DA:1547,0 +DA:1548,0 +DA:1549,0 +DA:1550,0 +DA:1551,0 +DA:1552,0 +DA:1553,0 +DA:1554,0 +DA:1555,0 +DA:1556,0 +DA:1557,0 +DA:1558,0 +DA:1559,0 +DA:1560,0 +DA:1561,0 +DA:1562,0 +DA:1563,0 +DA:1564,0 +DA:1565,0 +DA:1566,0 +DA:1567,0 +DA:1568,0 +DA:1569,0 +DA:1570,0 +DA:1571,0 +DA:1572,0 +DA:1573,0 +DA:1574,0 +DA:1575,0 +DA:1576,0 +DA:1577,0 +DA:1578,0 +DA:1579,0 +DA:1580,0 +DA:1581,0 +DA:1582,0 +DA:1583,0 +DA:1584,0 +DA:1585,0 +DA:1586,0 +DA:1587,0 +DA:1588,0 +DA:1589,0 +DA:1590,0 +DA:1591,0 +DA:1592,0 +DA:1593,0 +DA:1594,0 +DA:1595,0 +DA:1596,0 +DA:1597,0 +DA:1598,0 +DA:1599,1 +DA:1600,1 +DA:1601,1 +DA:1602,0 +DA:1603,0 +DA:1604,0 +DA:1605,1 +DA:1606,1 +DA:1607,0 +DA:1608,0 +DA:1609,0 +DA:1610,0 +DA:1611,0 +DA:1612,0 +DA:1613,0 +DA:1614,0 +DA:1615,0 +DA:1616,0 +DA:1617,0 +DA:1618,0 +DA:1619,0 +DA:1620,0 +DA:1621,0 +DA:1622,0 +DA:1623,0 +DA:1624,0 +DA:1625,0 +DA:1626,0 +DA:1627,0 +DA:1628,0 +DA:1629,0 +DA:1630,0 +DA:1631,0 +DA:1632,0 +DA:1633,0 +DA:1634,0 +DA:1635,0 +DA:1636,0 +DA:1637,0 +DA:1638,0 +DA:1639,0 +DA:1640,0 +DA:1641,0 +DA:1642,0 +DA:1643,0 +DA:1644,0 +DA:1645,0 +DA:1646,0 +DA:1647,0 +DA:1648,0 +DA:1649,0 +DA:1650,0 +DA:1651,0 +DA:1652,0 +DA:1653,0 +DA:1654,0 +DA:1655,0 +DA:1656,0 +DA:1657,0 +DA:1658,0 +DA:1659,0 +DA:1660,0 +DA:1661,0 +DA:1662,0 +DA:1663,0 +DA:1664,0 +DA:1665,0 +DA:1666,0 +DA:1667,0 +DA:1668,0 +DA:1669,0 +DA:1670,0 +DA:1671,0 +DA:1672,0 +DA:1673,0 +DA:1674,0 +DA:1675,0 +DA:1676,0 +DA:1677,0 +DA:1678,0 +DA:1679,0 +DA:1680,0 +DA:1681,0 +DA:1682,0 +DA:1683,0 +DA:1684,0 +DA:1685,0 +DA:1686,0 +DA:1687,0 +DA:1688,0 +DA:1689,0 +DA:1690,0 +DA:1691,0 +DA:1692,0 +DA:1693,0 +DA:1694,0 +DA:1695,0 +DA:1696,0 +DA:1697,0 +DA:1698,0 +DA:1699,0 +DA:1700,0 +DA:1701,0 +DA:1702,0 +DA:1703,0 +DA:1704,0 +DA:1705,0 +DA:1706,0 +DA:1707,0 +DA:1708,0 +DA:1709,0 +DA:1710,0 +DA:1711,0 +DA:1712,0 +DA:1713,0 +DA:1714,0 +DA:1715,0 +DA:1716,0 +DA:1717,0 +DA:1718,0 +DA:1719,0 +DA:1720,0 +DA:1721,0 +DA:1722,0 +DA:1723,0 +DA:1724,0 +DA:1725,0 +DA:1726,0 +DA:1727,0 +DA:1728,0 +DA:1729,0 +DA:1730,0 +DA:1731,0 +DA:1732,0 +DA:1733,0 +DA:1734,0 +DA:1735,0 +DA:1736,0 +DA:1737,1 +DA:1738,1 +DA:1739,0 +DA:1740,0 +DA:1741,0 +DA:1742,0 +DA:1743,0 +DA:1744,0 +DA:1745,0 +DA:1746,0 +DA:1747,0 +DA:1748,0 +DA:1749,0 +DA:1750,0 +DA:1751,0 +DA:1752,0 +DA:1753,0 +DA:1754,0 +DA:1755,0 +DA:1756,0 +DA:1757,0 +DA:1758,0 +DA:1759,1 +DA:1760,1 +DA:1761,0 +DA:1762,0 +DA:1763,0 +DA:1764,0 +DA:1765,0 +DA:1766,0 +DA:1767,0 +DA:1768,0 +DA:1769,0 +DA:1770,0 +DA:1771,1 +DA:1772,1 +DA:1773,0 +DA:1774,0 +DA:1775,0 +DA:1776,0 +DA:1777,0 +DA:1778,0 +DA:1779,0 +DA:1780,0 +DA:1781,0 +DA:1782,0 +DA:1783,0 +DA:1784,0 +DA:1785,0 +DA:1786,0 +DA:1787,0 +DA:1788,0 +DA:1789,0 +DA:1790,0 +DA:1791,0 +DA:1792,0 +DA:1793,0 +DA:1794,0 +DA:1795,0 +DA:1796,0 +DA:1797,0 +DA:1798,0 +DA:1799,0 +DA:1800,0 +DA:1801,0 +DA:1802,0 +DA:1803,0 +DA:1804,1 +DA:1805,1 +DA:1806,0 +DA:1807,0 +DA:1808,0 +DA:1809,0 +DA:1810,0 +DA:1811,0 +DA:1812,0 +DA:1813,0 +DA:1814,0 +DA:1815,1 +DA:1816,1 +DA:1817,0 +DA:1818,0 +DA:1819,1 +DA:1820,1 +DA:1821,0 +DA:1822,0 +DA:1823,0 +DA:1824,0 +DA:1825,0 +DA:1826,0 +DA:1827,1 +DA:1828,1 +DA:1829,0 +DA:1830,0 +DA:1831,0 +DA:1832,0 +DA:1833,0 +DA:1834,1 +DA:1835,1 +DA:1836,0 +DA:1837,0 +DA:1838,0 +DA:1839,0 +DA:1840,0 +DA:1841,0 +DA:1842,0 +DA:1843,0 +DA:1844,0 +DA:1845,0 +DA:1846,0 +DA:1847,0 +DA:1848,0 +DA:1849,0 +DA:1850,0 +DA:1851,0 +DA:1852,0 +DA:1853,0 +DA:1854,0 +DA:1855,0 +DA:1856,0 +DA:1857,0 +DA:1858,0 +DA:1859,0 +DA:1860,0 +DA:1861,0 +DA:1862,0 +DA:1863,0 +DA:1864,0 +DA:1865,0 +DA:1866,0 +DA:1867,0 +DA:1868,0 +DA:1869,0 +DA:1870,0 +DA:1871,0 +DA:1872,0 +DA:1873,0 +DA:1874,0 +DA:1875,0 +DA:1876,0 +DA:1877,0 +DA:1878,0 +DA:1879,0 +DA:1880,0 +DA:1881,0 +DA:1882,0 +DA:1883,0 +DA:1884,0 +DA:1885,0 +DA:1886,0 +DA:1887,0 +DA:1888,0 +DA:1889,0 +DA:1890,0 +DA:1891,0 +DA:1892,0 +DA:1893,0 +DA:1894,0 +DA:1895,0 +DA:1896,0 +DA:1897,0 +DA:1898,0 +DA:1899,0 +DA:1900,0 +DA:1901,0 +DA:1902,0 +DA:1903,0 +DA:1904,0 +DA:1905,0 +DA:1906,0 +DA:1907,0 +DA:1908,0 +DA:1909,0 +DA:1910,0 +DA:1911,0 +DA:1912,0 +DA:1913,0 +DA:1914,0 +DA:1915,0 +DA:1916,0 +DA:1917,0 +DA:1918,0 +DA:1919,0 +DA:1920,0 +DA:1921,0 +DA:1922,0 +DA:1923,0 +DA:1924,0 +DA:1925,0 +DA:1926,0 +DA:1927,0 +DA:1928,0 +DA:1929,0 +DA:1930,0 +DA:1931,0 +DA:1932,0 +DA:1933,0 +DA:1934,0 +DA:1935,0 +DA:1936,0 +DA:1937,0 +DA:1938,0 +DA:1939,0 +DA:1940,0 +DA:1941,0 +DA:1942,0 +DA:1943,0 +DA:1944,0 +DA:1945,0 +DA:1946,0 +DA:1947,0 +DA:1948,0 +DA:1949,0 +DA:1950,0 +DA:1951,0 +DA:1952,0 +DA:1953,0 +DA:1954,0 +DA:1955,0 +DA:1956,0 +DA:1957,0 +DA:1958,0 +DA:1959,0 +DA:1960,0 +DA:1961,0 +DA:1962,0 +DA:1963,0 +DA:1964,0 +DA:1965,0 +DA:1966,0 +DA:1967,0 +DA:1968,0 +DA:1969,0 +DA:1970,0 +DA:1971,0 +DA:1972,0 +DA:1973,1 +DA:1974,1 +DA:1975,0 +DA:1976,0 +DA:1977,0 +DA:1978,0 +DA:1979,0 +DA:1980,0 +DA:1981,0 +DA:1982,0 +DA:1983,0 +DA:1984,0 +DA:1985,0 +DA:1986,0 +DA:1987,0 +DA:1988,0 +DA:1989,0 +DA:1990,0 +DA:1991,0 +DA:1992,0 +DA:1993,0 +DA:1994,0 +DA:1995,0 +DA:1996,0 +DA:1997,0 +DA:1998,0 +DA:1999,0 +DA:2000,0 +DA:2001,0 +DA:2002,0 +DA:2003,0 +DA:2004,0 +DA:2005,0 +DA:2006,0 +DA:2007,0 +DA:2008,0 +DA:2009,0 +DA:2010,0 +DA:2011,0 +DA:2012,0 +DA:2013,0 +DA:2014,0 +DA:2015,0 +DA:2016,0 +DA:2017,0 +DA:2018,0 +DA:2019,0 +DA:2020,0 +DA:2021,0 +DA:2022,0 +DA:2023,0 +DA:2024,0 +DA:2025,0 +DA:2026,0 +DA:2027,0 +DA:2028,0 +DA:2029,0 +DA:2030,0 +DA:2031,0 +DA:2032,0 +DA:2033,0 +DA:2034,0 +DA:2035,0 +DA:2036,0 +DA:2037,0 +DA:2038,0 +DA:2039,0 +DA:2040,0 +DA:2041,0 +DA:2042,0 +DA:2043,0 +DA:2044,0 +DA:2045,0 +DA:2046,0 +DA:2047,0 +DA:2048,0 +DA:2049,0 +DA:2050,0 +DA:2051,0 +DA:2052,0 +DA:2053,0 +DA:2054,0 +DA:2055,0 +DA:2056,0 +DA:2057,0 +DA:2058,0 +DA:2059,0 +DA:2060,0 +DA:2061,0 +DA:2062,0 +DA:2063,0 +DA:2064,0 +DA:2065,0 +DA:2066,0 +DA:2067,0 +DA:2068,0 +DA:2069,0 +DA:2070,0 +DA:2071,0 +DA:2072,0 +DA:2073,0 +DA:2074,0 +DA:2075,0 +DA:2076,0 +DA:2077,0 +DA:2078,0 +DA:2079,0 +DA:2080,0 +DA:2081,0 +DA:2082,0 +DA:2083,0 +DA:2084,0 +DA:2085,0 +DA:2086,0 +DA:2087,0 +DA:2088,0 +DA:2089,0 +DA:2090,0 +DA:2091,0 +DA:2092,0 +DA:2093,0 +DA:2094,0 +DA:2095,0 +DA:2096,0 +DA:2097,0 +DA:2098,1 +DA:2099,1 +DA:2100,0 +DA:2101,0 +DA:2102,0 +DA:2103,0 +DA:2104,0 +DA:2105,0 +DA:2106,0 +DA:2107,0 +DA:2108,0 +DA:2109,0 +DA:2110,0 +DA:2111,0 +DA:2112,0 +DA:2113,1 +DA:2114,1 +DA:2115,0 +DA:2116,0 +DA:2117,0 +DA:2118,0 +DA:2119,0 +DA:2120,0 +DA:2121,0 +DA:2122,0 +DA:2123,0 +DA:2124,0 +DA:2125,0 +DA:2126,0 +DA:2127,0 +DA:2128,0 +DA:2129,0 +DA:2130,0 +DA:2131,0 +DA:2132,0 +DA:2133,0 +DA:2134,1 +DA:2135,1 +DA:2136,1 +DA:2137,0 +DA:2138,0 +DA:2139,1 +DA:2140,1 +DA:2141,0 +DA:2142,0 +DA:2143,1 +DA:2144,1 +DA:2145,0 +DA:2146,0 +DA:2147,0 +DA:2148,0 +DA:2149,0 +DA:2150,0 +DA:2151,0 +DA:2152,1 +DA:2153,1 +DA:2154,0 +DA:2155,0 +DA:2156,0 +DA:2157,0 +DA:2158,0 +DA:2159,0 +DA:2160,0 +DA:2161,0 +DA:2162,0 +DA:2163,0 +DA:2164,0 +DA:2165,0 +DA:2166,1 +DA:2167,1 +DA:2168,1 +DA:2169,1 +LF:2169 +LH:93 +BRDA:1,0,0,1 +BRDA:1,1,0,1 +BRF:2 +BRH:2 +end_of_record diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/coverage/sort-arrow-sprite.png differ diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/trmnl-form-builder.js.html b/coverage/trmnl-form-builder.js.html new file mode 100644 index 0000000..793c47b --- /dev/null +++ b/coverage/trmnl-form-builder.js.html @@ -0,0 +1,6601 @@ + + + + + + Code coverage report for trmnl-form-builder.js + + + + + + + + + +
+
+

All files trmnl-form-builder.js

+
+ +
+ 4.28% + Statements + 93/2169 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 0% + Functions + 0/36 +
+ + +
+ 4.28% + Lines + 93/2169 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +1056 +1057 +1058 +1059 +1060 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1090 +1091 +1092 +1093 +1094 +1095 +1096 +1097 +1098 +1099 +1100 +1101 +1102 +1103 +1104 +1105 +1106 +1107 +1108 +1109 +1110 +1111 +1112 +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +1122 +1123 +1124 +1125 +1126 +1127 +1128 +1129 +1130 +1131 +1132 +1133 +1134 +1135 +1136 +1137 +1138 +1139 +1140 +1141 +1142 +1143 +1144 +1145 +1146 +1147 +1148 +1149 +1150 +1151 +1152 +1153 +1154 +1155 +1156 +1157 +1158 +1159 +1160 +1161 +1162 +1163 +1164 +1165 +1166 +1167 +1168 +1169 +1170 +1171 +1172 +1173 +1174 +1175 +1176 +1177 +1178 +1179 +1180 +1181 +1182 +1183 +1184 +1185 +1186 +1187 +1188 +1189 +1190 +1191 +1192 +1193 +1194 +1195 +1196 +1197 +1198 +1199 +1200 +1201 +1202 +1203 +1204 +1205 +1206 +1207 +1208 +1209 +1210 +1211 +1212 +1213 +1214 +1215 +1216 +1217 +1218 +1219 +1220 +1221 +1222 +1223 +1224 +1225 +1226 +1227 +1228 +1229 +1230 +1231 +1232 +1233 +1234 +1235 +1236 +1237 +1238 +1239 +1240 +1241 +1242 +1243 +1244 +1245 +1246 +1247 +1248 +1249 +1250 +1251 +1252 +1253 +1254 +1255 +1256 +1257 +1258 +1259 +1260 +1261 +1262 +1263 +1264 +1265 +1266 +1267 +1268 +1269 +1270 +1271 +1272 +1273 +1274 +1275 +1276 +1277 +1278 +1279 +1280 +1281 +1282 +1283 +1284 +1285 +1286 +1287 +1288 +1289 +1290 +1291 +1292 +1293 +1294 +1295 +1296 +1297 +1298 +1299 +1300 +1301 +1302 +1303 +1304 +1305 +1306 +1307 +1308 +1309 +1310 +1311 +1312 +1313 +1314 +1315 +1316 +1317 +1318 +1319 +1320 +1321 +1322 +1323 +1324 +1325 +1326 +1327 +1328 +1329 +1330 +1331 +1332 +1333 +1334 +1335 +1336 +1337 +1338 +1339 +1340 +1341 +1342 +1343 +1344 +1345 +1346 +1347 +1348 +1349 +1350 +1351 +1352 +1353 +1354 +1355 +1356 +1357 +1358 +1359 +1360 +1361 +1362 +1363 +1364 +1365 +1366 +1367 +1368 +1369 +1370 +1371 +1372 +1373 +1374 +1375 +1376 +1377 +1378 +1379 +1380 +1381 +1382 +1383 +1384 +1385 +1386 +1387 +1388 +1389 +1390 +1391 +1392 +1393 +1394 +1395 +1396 +1397 +1398 +1399 +1400 +1401 +1402 +1403 +1404 +1405 +1406 +1407 +1408 +1409 +1410 +1411 +1412 +1413 +1414 +1415 +1416 +1417 +1418 +1419 +1420 +1421 +1422 +1423 +1424 +1425 +1426 +1427 +1428 +1429 +1430 +1431 +1432 +1433 +1434 +1435 +1436 +1437 +1438 +1439 +1440 +1441 +1442 +1443 +1444 +1445 +1446 +1447 +1448 +1449 +1450 +1451 +1452 +1453 +1454 +1455 +1456 +1457 +1458 +1459 +1460 +1461 +1462 +1463 +1464 +1465 +1466 +1467 +1468 +1469 +1470 +1471 +1472 +1473 +1474 +1475 +1476 +1477 +1478 +1479 +1480 +1481 +1482 +1483 +1484 +1485 +1486 +1487 +1488 +1489 +1490 +1491 +1492 +1493 +1494 +1495 +1496 +1497 +1498 +1499 +1500 +1501 +1502 +1503 +1504 +1505 +1506 +1507 +1508 +1509 +1510 +1511 +1512 +1513 +1514 +1515 +1516 +1517 +1518 +1519 +1520 +1521 +1522 +1523 +1524 +1525 +1526 +1527 +1528 +1529 +1530 +1531 +1532 +1533 +1534 +1535 +1536 +1537 +1538 +1539 +1540 +1541 +1542 +1543 +1544 +1545 +1546 +1547 +1548 +1549 +1550 +1551 +1552 +1553 +1554 +1555 +1556 +1557 +1558 +1559 +1560 +1561 +1562 +1563 +1564 +1565 +1566 +1567 +1568 +1569 +1570 +1571 +1572 +1573 +1574 +1575 +1576 +1577 +1578 +1579 +1580 +1581 +1582 +1583 +1584 +1585 +1586 +1587 +1588 +1589 +1590 +1591 +1592 +1593 +1594 +1595 +1596 +1597 +1598 +1599 +1600 +1601 +1602 +1603 +1604 +1605 +1606 +1607 +1608 +1609 +1610 +1611 +1612 +1613 +1614 +1615 +1616 +1617 +1618 +1619 +1620 +1621 +1622 +1623 +1624 +1625 +1626 +1627 +1628 +1629 +1630 +1631 +1632 +1633 +1634 +1635 +1636 +1637 +1638 +1639 +1640 +1641 +1642 +1643 +1644 +1645 +1646 +1647 +1648 +1649 +1650 +1651 +1652 +1653 +1654 +1655 +1656 +1657 +1658 +1659 +1660 +1661 +1662 +1663 +1664 +1665 +1666 +1667 +1668 +1669 +1670 +1671 +1672 +1673 +1674 +1675 +1676 +1677 +1678 +1679 +1680 +1681 +1682 +1683 +1684 +1685 +1686 +1687 +1688 +1689 +1690 +1691 +1692 +1693 +1694 +1695 +1696 +1697 +1698 +1699 +1700 +1701 +1702 +1703 +1704 +1705 +1706 +1707 +1708 +1709 +1710 +1711 +1712 +1713 +1714 +1715 +1716 +1717 +1718 +1719 +1720 +1721 +1722 +1723 +1724 +1725 +1726 +1727 +1728 +1729 +1730 +1731 +1732 +1733 +1734 +1735 +1736 +1737 +1738 +1739 +1740 +1741 +1742 +1743 +1744 +1745 +1746 +1747 +1748 +1749 +1750 +1751 +1752 +1753 +1754 +1755 +1756 +1757 +1758 +1759 +1760 +1761 +1762 +1763 +1764 +1765 +1766 +1767 +1768 +1769 +1770 +1771 +1772 +1773 +1774 +1775 +1776 +1777 +1778 +1779 +1780 +1781 +1782 +1783 +1784 +1785 +1786 +1787 +1788 +1789 +1790 +1791 +1792 +1793 +1794 +1795 +1796 +1797 +1798 +1799 +1800 +1801 +1802 +1803 +1804 +1805 +1806 +1807 +1808 +1809 +1810 +1811 +1812 +1813 +1814 +1815 +1816 +1817 +1818 +1819 +1820 +1821 +1822 +1823 +1824 +1825 +1826 +1827 +1828 +1829 +1830 +1831 +1832 +1833 +1834 +1835 +1836 +1837 +1838 +1839 +1840 +1841 +1842 +1843 +1844 +1845 +1846 +1847 +1848 +1849 +1850 +1851 +1852 +1853 +1854 +1855 +1856 +1857 +1858 +1859 +1860 +1861 +1862 +1863 +1864 +1865 +1866 +1867 +1868 +1869 +1870 +1871 +1872 +1873 +1874 +1875 +1876 +1877 +1878 +1879 +1880 +1881 +1882 +1883 +1884 +1885 +1886 +1887 +1888 +1889 +1890 +1891 +1892 +1893 +1894 +1895 +1896 +1897 +1898 +1899 +1900 +1901 +1902 +1903 +1904 +1905 +1906 +1907 +1908 +1909 +1910 +1911 +1912 +1913 +1914 +1915 +1916 +1917 +1918 +1919 +1920 +1921 +1922 +1923 +1924 +1925 +1926 +1927 +1928 +1929 +1930 +1931 +1932 +1933 +1934 +1935 +1936 +1937 +1938 +1939 +1940 +1941 +1942 +1943 +1944 +1945 +1946 +1947 +1948 +1949 +1950 +1951 +1952 +1953 +1954 +1955 +1956 +1957 +1958 +1959 +1960 +1961 +1962 +1963 +1964 +1965 +1966 +1967 +1968 +1969 +1970 +1971 +1972 +1973 +1974 +1975 +1976 +1977 +1978 +1979 +1980 +1981 +1982 +1983 +1984 +1985 +1986 +1987 +1988 +1989 +1990 +1991 +1992 +1993 +1994 +1995 +1996 +1997 +1998 +1999 +2000 +2001 +2002 +2003 +2004 +2005 +2006 +2007 +2008 +2009 +2010 +2011 +2012 +2013 +2014 +2015 +2016 +2017 +2018 +2019 +2020 +2021 +2022 +2023 +2024 +2025 +2026 +2027 +2028 +2029 +2030 +2031 +2032 +2033 +2034 +2035 +2036 +2037 +2038 +2039 +2040 +2041 +2042 +2043 +2044 +2045 +2046 +2047 +2048 +2049 +2050 +2051 +2052 +2053 +2054 +2055 +2056 +2057 +2058 +2059 +2060 +2061 +2062 +2063 +2064 +2065 +2066 +2067 +2068 +2069 +2070 +2071 +2072 +2073 +2074 +2075 +2076 +2077 +2078 +2079 +2080 +2081 +2082 +2083 +2084 +2085 +2086 +2087 +2088 +2089 +2090 +2091 +2092 +2093 +2094 +2095 +2096 +2097 +2098 +2099 +2100 +2101 +2102 +2103 +2104 +2105 +2106 +2107 +2108 +2109 +2110 +2111 +2112 +2113 +2114 +2115 +2116 +2117 +2118 +2119 +2120 +2121 +2122 +2123 +2124 +2125 +2126 +2127 +2128 +2129 +2130 +2131 +2132 +2133 +2134 +2135 +2136 +2137 +2138 +2139 +2140 +2141 +2142 +2143 +2144 +2145 +2146 +2147 +2148 +2149 +2150 +2151 +2152 +2153 +2154 +2155 +2156 +2157 +2158 +2159 +2160 +2161 +2162 +2163 +2164 +2165 +2166 +2167 +2168 +2169 +2170 +2171 +2172 +21731x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  +1x +1x +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +1x +1x +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +  + 
/**
+ * TRMNL YML Form Builder Web Component
+ * 
+ * A standalone, dependency-free Web Component for building TRMNL custom plugin forms.
+ * Requires only Tailwind CSS for styling.
+ * 
+ * Usage:
+ *   <trmnl-form-builder></trmnl-form-builder>
+ * 
+ * Attributes:
+ *   - accent-color: Hex color for accent elements (default: #F8654B)
+ *   - theme: 'light' or 'dark' (default: 'light')
+ */
+ 
+class TRMLYamlForm extends HTMLElement {
+    constructor() {
+	  super();
+	  this.attachShadow({ mode: 'open' });
+
+	  // State management
+	  this.fields = [];
+	  this.selectedFieldId = null;
+	  this.draggedFieldId = null;
+	  this.draggedFromIndex = null;
+	  this.nextFieldId = 1;
+
+	  // Configuration
+	  this.accentColor = this.getAttribute('accent-color') || '#F8654B';
+	  this.theme = this.getAttribute('theme') || 'light';
+
+	  // Categories cache
+	  this.categories = [];
+	  this.loadCategories();
+
+	  // supported locales (based on trmnl-i18n)
+      this.locales = [
+		{ code: 'de', label: 'Deutsch' },
+		{ code: 'nl', label: 'Nederlands' },
+		{ code: 'fr', label: 'Français' },
+		{ code: 'en-GB', label: 'English: UK' },
+		{ code: 'es-ES', label: 'Español' },
+		{ code: 'it', label: 'Italiano' },
+		{ code: 'no', label: 'Norsk' },
+		{ code: 'da', label: 'Dansk' },
+		{ code: 'ja', label: '日本語' },
+		{ code: 'de-AT', label: 'Deutsch: Österreich' },
+		{ code: 'ko', label: '한국어' },
+		{ code: 'sv', label: 'Svenska' },
+		{ code: 'pt-BR', label: 'Português' },
+		{ code: 'zh-HK', label: '中文: 香港' },
+		{ code: 'pl', label: 'Polski' },
+		{ code: 'uk', label: 'Українська' }, 
+		{ code: 'zh-CN', label: '中文: 简体' },
+		{ code: 'hu', label: 'Magyar' },
+		{ code: 'he', label: 'עברית' },
+		{ code: 'sk', label: 'Slovenčina' },
+		{ code: 'ru', label: 'Русский' },
+		{ code: 'is', label: 'Íslenska' },
+		{ code: 'id', label: 'Bahasa Indonesia' }
+	  ];
+  
+	  // --- Property Definitions (How to render each setting) ---
+	  this.propertyDefinitions = {
+		// Common
+		keyname: { label: 'Keyname', type: 'text', help: 'Lowercase alphanumeric + underscores only' },
+		name: { label: 'Name', type: 'text', placeholder: 'About This Plugin' },
+		description: { label: 'Description', type: 'textarea', placeholder: 'Main description (HTML-friendly)' },
+		help_text: { label: 'Help Text', type: 'textarea', placeholder: 'Sub-label / hint (HTML-friendly)' },
+		optional: { label: 'Optional field', type: 'checkbox' },
+		
+		// Inputs
+		default: { label: 'Default Value', type: 'text', placeholder: 'Pre-selected value' },
+		placeholder: { label: 'Placeholder', type: 'text', placeholder: 'Example value' },
+		min: { label: 'Minimum Value', type: 'number', placeholder: '0' },
+		max: { label: 'Maximum Value', type: 'number', placeholder: '100' },
+		maxlength: { label: 'Maximum Length', type: 'number', placeholder: '12' },
+		rows: { label: 'Rows', type: 'number', placeholder: '10' },
+		
+		// Selection
+		options: { label: 'Options (one per line)', type: 'textarea', placeholder: 'Option 1\nOption 2', help: 'Enter one option per line' },
+		multiple: { label: 'Allow multiple selection', type: 'checkbox' },
+		endpoint: { label: 'Endpoint *', type: 'text', placeholder: 'https://api.example.com/options' },
+		http_verb: { 
+		  label: 'HTTP Method', 
+		  type: 'select', 
+		  default: 'GET', 
+		  options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT'],
+		  placeholder: 'GET'
+		},
+		
+		// Special
+		value: { label: 'Value', type: 'text', placeholder: 'sk_live_abc123xyz' },
+		
+		// Author Bio Specifics
+		categories: { label: 'Categories (Max 2)', type: 'custom_categories' },
+		github_url: { label: 'GitHub URL', type: 'url', placeholder: 'https://github.com/username' },
+		email_address: { label: 'Email Address', type: 'text', placeholder: 'user@example.com' },
+		learn_more_url: { label: 'Learn More URL', type: 'url', placeholder: 'https://thesatanictemple.com/' },
+		youtube_url: { label: 'YouTube URL', type: 'url', placeholder: 'https://www.youtube.com/@useTRMNL' },
+		
+		// The Translation Manager
+		i18n_descriptions: { label: 'Description Translations', type: 'i18n_map', help: 'Add translated descriptions for specific locales.' },
+		
+		// Conditional Logic
+		conditional_validation: { label: 'Conditional Logic', type: 'conditional_builder', help: 'Hide other fields based on this field\'s value' }
+	  };
+
+	  // --- Field Types (Added 'properties' array to each) ---
+	  this.fieldTypes = {
+		// Text Input
+		author_bio: { 
+		  category: 'TEXT INPUT', name: 'Author Bio', description: 'Plugin information', 
+		  properties: [{ 
+			  key: 'name', 
+			  default: 'About This Plugin'
+			}, 'categories', 'github_url', 'email_address', 'learn_more_url', 'youtube_url'] // Note: Common props are handled automatically
+		},
+		url: { 
+		  category: 'TEXT INPUT', name: 'Url', description: 'URL input', 
+		  properties: ['default', 'placeholder', 'optional'] 
+		},
+		string: { 
+		  category: 'TEXT INPUT', name: 'String', description: 'Single-line text', 
+		  properties: ['default', 'placeholder', 'maxlength', 'optional', 'conditional_validation'] 
+		},
+		multi_string: { 
+		  category: 'TEXT INPUT', name: 'Multi String', description: 'Comma-separated', 
+		  properties: ['default', 'placeholder', 'optional'] 
+		},
+		text: { 
+		  category: 'TEXT INPUT', name: 'Text', description: 'Multi-line textarea', 
+		  properties: ['default', 'placeholder', 'rows', 'optional', 'conditional_validation'] 
+		},
+		code: { 
+		  category: 'TEXT INPUT', name: 'Code', description: 'Monospace code', 
+		  properties: ['default', 'placeholder', 'rows', 'optional'] 
+		},
+		password: { 
+		  category: 'TEXT INPUT', name: 'Password', description: 'Masked input', 
+		  properties: ['default', 'placeholder', 'optional'] 
+		},
+		
+		// Numeric
+		number: { 
+		  category: 'NUMERIC', name: 'Number', description: 'Numeric input', 
+		  properties: ['default', 'placeholder', 'min', 'max', 'optional', 'conditional_validation'] 
+		},
+		
+		// Date & Time
+		date: { 
+		  category: 'DATE & TIME', name: 'Date', description: 'Date picker', 
+		  // We use an object to override the generic 'default' definition
+		  properties: [
+			{ 
+			  key: 'default', // The property key we are targeting
+			  placeholder: 'YYYY-MM-DD', 
+			  help: 'Can also be set to "today" to use the current date automatically.' 
+			},
+			'optional'
+		  ] 
+		},
+		time: { category: 'DATE & TIME', name: 'Time', description: 'Time picker', properties: ['default', 'optional'] },
+		timezone: { category: 'DATE & TIME', name: 'Time Zone', description: 'Timezone selector', properties: ['default', 'optional'] },
+		
+		// Selection
+		select: { 
+		  category: 'SELECTION', name: 'Select', description: 'Dropdown', 
+		  properties: ['options', 'default', 'multiple', 'optional', 'conditional_validation'] 
+		},
+		xhrSelect: { 
+		  category: 'SELECTION', name: 'XhrSelect', description: 'Dynamic dropdown', 
+		  properties: ['endpoint', 'http_verb', 'multiple', 'optional'] 
+		},
+		xhrSelectSearch: { 
+		  category: 'SELECTION', name: 'XhrSelectSearch', description: 'Searchable dynamic', 
+		  properties: ['endpoint', 'http_verb', 'multiple', 'optional'] 
+		},
+		
+		// Special
+		copyable: { category: 'SPECIAL', name: 'Copyable', description: 'Click-to-copy', properties: ['value'] },
+		copyable_webhook_url: { category: 'SPECIAL', name: 'Copyable Webhook', description: 'Click-to-copy webhook', properties: ['value'] },
+	  };
+	}
+ 
+  connectedCallback() {
+    this.render();
+    this.setupEventListeners();
+  }
+ 
+  async loadCategories() {
+    try {
+      const response = await fetch('https://usetrmnl.com/api/categories');
+      if (response.ok) {
+        const data = await response.json();
+        // Handle both array and object with data property
+        this.categories = Array.isArray(data) ? data : (data.data || this.getFallbackCategories());
+        // Extract just the names if they're objects
+        if (this.categories.length > 0 && typeof this.categories[0] === 'object') {
+          this.categories = this.categories.map(cat => cat.name || cat.title || cat);
+        }
+      } else {
+        this.categories = this.getFallbackCategories();
+      }
+    } catch (error) {
+      console.warn('Failed to load categories from API, using fallback:', error);
+      this.categories = this.getFallbackCategories();
+    }
+  }
+ 
+  getFallbackCategories() {
+    return [
+        "analytics",
+		"art",
+		"calendar",
+		"comics",
+		"crm",
+		"custom",
+		"discovery",
+		"ecommerce",
+		"education",
+		"email",
+		"entertainment",
+		"environment",
+		"finance",
+		"games",
+		"humor",
+		"images",
+		"kpi",
+		"life",
+		"marketing",
+		"nature",
+		"news",
+		"personal",
+		"productivity",
+		"programming",
+		"sales",
+		"sports",
+		"travel"
+    ];
+  }
+ 
+  render() {
+    const html = `
+      <style>
+        * {
+          box-sizing: border-box;
+        }
+        
+        :host {
+          --accent-color: ${this.accentColor};
+          --bg-primary: ${this.theme === 'dark' ? '#1a1a1a' : '#ffffff'};
+          --bg-secondary: ${this.theme === 'dark' ? '#2a2a2a' : '#f5f5f5'};
+		  --bg-primary-inverted: ${this.theme === 'dark' ? '#ffffff' : '#1a1a1a'};
+          --text-primary: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+          --text-secondary: ${this.theme === 'dark' ? '#a0a0a0' : '#666666'};
+          --text-primary-inverted: ${this.theme === 'dark' ? '#1a1a1a' : '#e0e0e0'};
+          --border-color: ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+          
+          display: block;
+          /* font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif; */
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          height: 100vh;
+          overflow: auto;
+        }
+        
+        .container {
+          display: grid;
+          grid-template-columns: 280px 1fr 1fr;
+          grid-template-rows: auto 1fr;
+          height: 100%;
+          gap: 0;
+        }
+        
+        .header {
+          grid-column: 1 / -1;
+          padding: 20px 24px;
+          border-bottom: 1px solid var(--border-color);
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+        }
+        
+        .header h1 {
+          margin: 0;
+          font-size: 24px;
+          font-weight: 600;
+          letter-spacing: -0.5px;
+        }
+        
+        .header p {
+          margin: 4px 0 0 0;
+          font-size: 13px;
+          color: var(--text-secondary);
+        }
+        
+        .field-count {
+          font-size: 13px;
+          color: var(--text-secondary);
+          font-weight: 500;
+        }
+        
+        .sidebar-left {
+          border-right: 1px solid var(--border-color);
+          overflow-y: auto;
+          background: var(--bg-secondary);
+        }
+        
+        .sidebar-right {
+          border-left: 1px solid var(--border-color);
+          display: flex;
+          flex-direction: column;
+          background: var(--bg-secondary);
+        }
+        
+        .canvas {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          padding: 40px;
+          overflow-y: auto;
+          background: var(--bg-primary);
+        }
+        
+        .canvas.has-fields {
+          align-items: stretch;
+          justify-content: flex-start;
+        }
+        
+        .field-types-section {
+          padding: 16px;
+        }
+        
+        .field-types-label {
+          font-size: 11px;
+          font-weight: 700;
+          letter-spacing: 0.5px;
+          text-transform: uppercase;
+          color: var(--text-secondary);
+          margin: 16px 0 8px 0;
+          padding: 0 8px;
+        }
+        
+        .field-type-button {
+          width: 100%;
+          padding: 12px;
+          margin-bottom: 8px;
+          border: 1px dashed var(--accent-color);
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          border-radius: 6px;
+          cursor: grab;
+          transition: all 0.2s;
+          text-align: left;
+          font-size: 13px;
+          font-weight: 500;
+          position: relative;
+        }
+        
+        .field-type-button:hover {
+          background: var(--bg-secondary);
+          border-style: solid;
+        }
+        
+        .field-type-button:active {
+          cursor: grabbing;
+        }
+		
+		.field-type-button[disabled] {
+		  opacity: 0.5;
+		  cursor: not-allowed;
+		  background: var(--bg-secondary);
+		  border-color: var(--border-color);
+		  pointer-events: none; /* Prevents clicking and dragging */
+		}
+        
+        .field-type-name {
+          font-weight: 600;
+          margin-bottom: 2px;
+        }
+        
+        .field-type-desc {
+          font-size: 12px;
+          color: var(--text-secondary);
+        }
+        
+        .field-type-icon {
+          position: absolute;
+          right: 8px;
+          top: 50%;
+          transform: translateY(-50%);
+          width: 20px;
+          height: 20px;
+          background: var(--accent-color);
+          color: white;
+          border-radius: 3px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 11px;
+          font-weight: 600;
+        }
+        
+        .empty-state {
+          text-align: center;
+          color: var(--text-secondary);
+        }
+        
+        .empty-state-icon {
+          font-size: 48px;
+          margin-bottom: 16px;
+          opacity: 0.5;
+        }
+        
+        .empty-state h3 {
+          margin: 0 0 8px 0;
+          font-size: 16px;
+          font-weight: 600;
+          color: var(--text-primary);
+        }
+        
+        .empty-state p {
+          margin: 0;
+          font-size: 13px;
+          line-height: 1.5;
+          max-width: 300px;
+        }
+        
+        .fields-list {
+          display: flex;
+          flex-direction: column;
+          gap: 12px;
+          width: 100%;
+          max-width: 600px;
+        }
+        
+        .field-card {
+          padding: 12px;
+          border: 2px solid var(--border-color);
+          border-radius: 6px;
+          background: var(--bg-primary);
+          cursor: move;
+          transition: all 0.2s;
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          user-select: none;
+        }
+        
+        .field-card:hover {
+          border-color: var(--accent-color);
+          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+        }
+        
+        .field-card.selected {
+          border-color: var(--accent-color);
+          background: var(--bg-secondary);
+        }
+        
+        .field-card-info {
+          flex: 1;
+          min-width: 0;
+        }
+        
+        .field-card-keyname {
+          font-size: 12px;
+          color: var(--text-secondary);
+          margin-bottom: 2px;
+		  text-transform: lowercase;
+        }
+        
+        .field-card-type {
+          font-size: 11px;
+          color: var(--text-secondary);
+          text-transform: uppercase;
+          letter-spacing: 0.5px;
+          margin-left: 4px;
+        }
+        
+        .field-card-name {
+          font-size: 14px;
+          font-weight: 600;
+          color: var(--text-primary);
+        }
+        
+        .field-card-actions {
+          display: flex;
+          gap: 4px;
+          margin-left: 8px;
+        }
+        
+        .icon-button {
+          width: 28px;
+          height: 28px;
+          padding: 0;
+          border: 1px solid var(--border-color);
+          background: var(--bg-secondary);
+          color: var(--text-primary);
+          border-radius: 4px;
+          cursor: pointer;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 13px;
+          transition: all 0.2s;
+        }
+        
+        .icon-button:hover {
+          border-color: var(--accent-color);
+          color: var(--accent-color);
+        }
+        
+        .icon-button.delete:hover {
+          border-color: #ef4444;
+          color: #ef4444;
+        }
+        
+        .tabs {
+          display: flex;
+          border-bottom: 1px solid var(--border-color);
+          padding: 0 16px;
+        }
+        
+        .tab {
+          padding: 12px 16px;
+          border: none;
+          background: transparent;
+          color: var(--text-secondary);
+          cursor: pointer;
+          font-size: 13px;
+          font-weight: 500;
+          border-bottom: 2px solid transparent;
+          transition: all 0.2s;
+        }
+        
+        .tab:hover {
+          color: var(--text-primary);
+        }
+        
+        .tab.active {
+          color: var(--accent-color);
+          border-bottom-color: var(--accent-color);
+        }
+        
+        .tab-content {
+          flex: 1;
+          overflow-y: auto;
+          padding: 16px;
+          display: none;
+        }
+        
+        .tab-content.active {
+          display: block;
+        }
+        
+        .form-group {
+          margin-bottom: 16px;
+        }
+        
+        .form-label {
+          display: block;
+          font-size: 12px;
+          font-weight: 600;
+          margin-bottom: 6px;
+          color: var(--text-primary);
+          text-transform: uppercase;
+          letter-spacing: 0.5px;
+        }
+        
+        .form-input,
+        .form-textarea {
+          width: 100%;
+          padding: 8px;
+          border: 1px solid var(--border-color);
+          border-radius: 4px;
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          font-size: 13px;
+          font-family: inherit;
+          transition: border-color 0.2s;
+        }
+        
+        .form-input:focus,
+        .form-textarea:focus {
+          outline: none;
+          border-color: var(--accent-color);
+          box-shadow: 0 0 0 2px rgba(248, 101, 75, 0.1);
+        }
+        
+        .form-textarea {
+          resize: vertical;
+          min-height: 60px;
+        }
+        
+        .form-checkbox {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          cursor: pointer;
+        }
+        
+        .form-checkbox input {
+          cursor: pointer;
+          accent-color: var(--accent-color);
+        }
+        
+        .form-checkbox label {
+          cursor: pointer;
+          font-size: 13px;
+        }
+        
+        .yaml-output {
+          font-family: monospace;
+          font-size: 12px;
+          line-height: 1.6;
+          white-space: pre-wrap;
+          word-break: break-word;
+          background: var(--bg-primary-inverted);
+          padding: 12px;
+          border-radius: 4px;
+          border: 1px solid var(--border-color);
+          color: var(--text-primary-inverted);
+          max-height: 400px;
+          overflow-y: auto;
+        }
+        
+        .yaml-empty {
+          color: var(--text-secondary);
+          font-style: italic;
+        }
+        
+        .button-group {
+          display: flex;
+          gap: 8px;
+          margin-bottom: 16px;
+        }
+        
+        .button {
+          flex: 1;
+          padding: 8px 12px;
+          border: none;
+          border-radius: 4px;
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+          transition: all 0.2s;
+          text-transform: uppercase;
+          letter-spacing: 0.5px;
+        }
+        
+        .button-primary {
+          background: var(--accent-color);
+          color: white;
+        }
+        
+        .button-primary:hover {
+          opacity: 0.9;
+          transform: translateY(-1px);
+          box-shadow: 0 2px 8px rgba(248, 101, 75, 0.3);
+        }
+        
+        .button-secondary {
+          background: var(--bg-primary);
+          color: var(--text-primary);
+          border: 1px solid var(--border-color);
+        }
+        
+        .button-secondary:hover {
+          border-color: var(--accent-color);
+          color: var(--accent-color);
+        }
+        
+        .no-field-selected {
+          color: var(--text-secondary);
+          /* text-align: center; */
+          padding: 40px 20px;
+          font-size: 13px;
+        }
+        
+        @keyframes slideIn {
+          from {
+            transform: translateX(400px);
+            opacity: 0;
+          }
+          to {
+            transform: translateX(0);
+            opacity: 1;
+          }
+        }
+        
+        .drag-over {
+          background: var(--bg-secondary);
+          border-color: var(--accent-color);
+        }
+		
+		.reset-button {
+		  margin-left: auto; /* Pushes button to the far right */
+		  background: transparent;
+		  border: none;
+		  color: var(--accent-color);
+		  cursor: pointer;
+		  font-size: 11px;
+		  font-weight: 600;
+		  text-transform: uppercase;
+		  letter-spacing: 0.5px;
+		  padding: 0 4px;
+		  transition: color 0.2s;
+		}
+
+		.reset-button:hover {
+		  color: var(--text-secondary);
+		}
+      </style>
+      
+      <div class="container">
+        <div class="header">
+          <div>
+            <h1>TRMNL YML Form Builder</h1>
+            <p>Create custom plugin configuration forms with a no-code interface</p>
+          </div>
+          <div class="field-count"><span id="fieldCount">0</span> fields defined</div>
+        </div>
+        
+        <div class="sidebar-left">
+          <div id="fieldTypesList"></div>
+        </div>
+        
+        <div class="canvas" id="canvas">
+          <div class="empty-state">
+            <div class="empty-state-icon">+</div>
+            <h3>No fields yet</h3>
+            <p>Select a field type from the left panel to start building your form.</p>
+            <p style="margin-top: 8px; font-size: 12px;">You can drag fields to reorder them, or click to edit properties.</p>
+          </div>
+        </div>
+        
+        <div class="sidebar-right">
+          <div class="tabs">
+            <button class="tab active" data-tab="config">Config</button>
+            <button class="tab" data-tab="yaml">YAML</button>
+			<button class="reset-button" id="resetBtn" title="Clear all fields">Reset</button>
+          </div>
+          
+          <div id="configTab" class="tab-content active">
+            <div id="configContent" class="no-field-selected">
+              Select a field to edit its properties
+            </div>
+          </div>
+          
+          <div id="yamlTab" class="tab-content">
+            <div class="button-group">
+              <button class="button button-primary" id="copyBtn">Copy</button>
+			  <button class="button button-secondary" id="importBtn">Import</button>
+            </div>
+            <div id="yamlOutput" class="yaml-output">
+              <span class="yaml-empty"># No fields defined yet</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    `;
+    
+    this.shadowRoot.innerHTML = html;
+    this.renderFieldTypes();
+  }
+ 
+  renderFieldTypes() {
+    const container = this.shadowRoot.getElementById('fieldTypesList');
+    const grouped = {};
+    
+    // Group field types by category
+    Object.entries(this.fieldTypes).forEach(([key, config]) => {
+      if (!grouped[config.category]) {
+        grouped[config.category] = [];
+      }
+      grouped[config.category].push({ key, ...config });
+    });
+    
+    let html = '';
+    Object.entries(grouped).forEach(([category, fields]) => {
+      html += `<div class="field-types-section">
+        <div class="field-types-label">${category}</div>`;
+      
+      fields.forEach(field => {
+        html += `
+          <button class="field-type-button" draggable="true" data-field-type="${field.key}">
+            <div class="field-type-name">${field.name}</div>
+            <div class="field-type-desc">${field.description}</div>
+            <div class="field-type-icon">+</div>
+          </button>
+        `;
+      });
+      
+      html += '</div>';
+    });
+    
+    container.innerHTML = html;
+	this.updateAuthorBioState(); // Check immediately on render
+  }
+ 
+  renderCategoryPills(selectedCategories = []) {
+    if (!this.categories || this.categories.length === 0) {
+      return '<div style="color: var(--text-secondary); font-size: 12px;">Loading categories...</div>';
+    }
+
+    let html = '<div style="display: flex; flex-wrap: wrap; gap: 8px;">';
+    
+    this.categories.forEach(category => {
+      const isSelected = selectedCategories.includes(category);
+      html += `
+        <button class="category-pill" data-category="${category}" style="
+          padding: 6px 12px;
+          border: 1px solid ${isSelected ? 'var(--accent-color)' : 'var(--border-color)'};
+          background: ${isSelected ? 'var(--accent-color)' : 'var(--bg-primary)'};
+          color: ${isSelected ? 'white' : 'var(--text-primary)'};
+          border-radius: 20px;
+          cursor: pointer;
+          font-size: 12px;
+          font-weight: 500;
+          transition: all 0.2s;
+          white-space: nowrap;
+        ">${category}</button>
+      `;
+    });
+    
+    html += '</div>';
+    return html;
+  }
+ 
+  setupEventListeners() {
+    // Field type buttons
+    this.shadowRoot.querySelectorAll('.field-type-button').forEach(btn => {
+      btn.addEventListener('dragstart', (e) => this.handleFieldTypeDragStart(e));
+      btn.addEventListener('click', (e) => this.addField(e.currentTarget.dataset.fieldType));
+    });
+    
+    // Canvas drop zone
+    const canvas = this.shadowRoot.getElementById('canvas');
+    canvas.addEventListener('dragover', (e) => this.handleCanvasDragOver(e));
+    canvas.addEventListener('drop', (e) => this.handleCanvasDrop(e));
+    canvas.addEventListener('dragleave', (e) => this.handleCanvasDragLeave(e));
+    
+    // Tabs
+    this.shadowRoot.querySelectorAll('.tab').forEach(tab => {
+      tab.addEventListener('click', (e) => this.switchTab(e.currentTarget.dataset.tab));
+    });
+    
+    // YAML buttons
+    this.shadowRoot.getElementById('copyBtn').addEventListener('click', () => this.copyYaml());
+	this.shadowRoot.getElementById('importBtn').addEventListener('click', () => this.importYaml());
+	
+	// Reset button listener
+    const resetBtn = this.shadowRoot.getElementById('resetBtn');
+    if (resetBtn) {
+      resetBtn.addEventListener('click', () => {
+        if (confirm('Are you sure you want to reset the form? All fields will be lost.')) {
+          this.clear();
+        }
+      });
+    }
+  }
+ 
+  handleFieldTypeDragStart(e) {
+    e.dataTransfer.effectAllowed = 'copy';
+    e.dataTransfer.setData('fieldType', e.currentTarget.dataset.fieldType);
+  }
+ 
+  handleCanvasDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+    this.shadowRoot.getElementById('canvas').classList.add('drag-over');
+  }
+ 
+  handleCanvasDragLeave(e) {
+    if (e.target === this.shadowRoot.getElementById('canvas')) {
+      this.shadowRoot.getElementById('canvas').classList.remove('drag-over');
+    }
+  }
+ 
+  handleCanvasDrop(e) {
+    e.preventDefault();
+    this.shadowRoot.getElementById('canvas').classList.remove('drag-over');
+    const fieldType = e.dataTransfer.getData('fieldType');
+    if (fieldType) {
+      this.addField(fieldType);
+    }
+  }
+ 
+   addField(fieldType) {
+	  if (fieldType === 'author_bio' && this.fields.some(f => f.field_type === 'author_bio')) {
+		this.showToast('Only one Author Bio field is allowed');
+		return;
+	  }
+	  
+	  const fieldKeyname = fieldType === 'author_bio'
+		? fieldType
+		: `${fieldType}_${Date.now().toString().slice(-4)}`;
+
+	  const typeConfig = this.fieldTypes[fieldType];
+	  
+	  // Create new field object with minimal defaults
+	  const field = {
+		id: `field_${this.nextFieldId++}`,
+		keyname: fieldKeyname,
+		field_type: fieldType,
+	  };
+	  
+	  // Only set 'name' if there's a default specified in the type config
+	  let hasNameDefault = false;
+	  if (typeConfig && Array.isArray(typeConfig.properties)) {
+		typeConfig.properties.forEach(prop => {
+		  if (typeof prop === 'object' && prop.key === 'name' && prop.default) {
+			field.name = prop.default;
+			hasNameDefault = true;
+		  }
+		});
+	  }
+	  
+	  // Fallback to type name if no custom default was set
+	  if (!hasNameDefault) {
+		field.name = this.fieldTypes[fieldType]?.name || fieldType;
+	  }
+	  
+	  // Update state
+	  this.fields.push(field);
+	  this.selectedFieldId = field.id;
+	  
+	  this.switchTab('config');
+	  this.updateConfigPanel(); 
+	  this.updateCanvasView();
+	  this.updateYamlOutput();
+	  this.updateAuthorBioState();
+  }
+ 
+  updateCanvasView() {
+    const canvas = this.shadowRoot.getElementById('canvas');
+    
+    if (this.fields.length === 0) {
+      canvas.innerHTML = `
+        <div class="empty-state">
+          <div class="empty-state-icon">+</div>
+          <h3>No fields yet</h3>
+          <p>Select a field type from the left panel to start building your form.</p>
+          <p style="margin-top: 8px; font-size: 12px;">You can drag fields to reorder them, or click to edit properties.</p>
+        </div>
+      `;
+      canvas.classList.remove('has-fields');
+      return;
+    }
+    
+    canvas.classList.add('has-fields');
+    let html = `<div class="fields-list">`;
+    
+    this.fields.forEach((field, index) => {
+      const isSelected = field.id === this.selectedFieldId;
+      html += `
+        <div class="field-card ${isSelected ? 'selected' : ''}" data-field-id="${field.id}" draggable="true">
+          <div class="field-card-info">
+            <div class="field-card-keyname">${field.keyname}<span class="field-card-type">${field.field_type}</span></div>
+            <div class="field-card-name">${field.name}</div>
+          </div>
+          <div class="field-card-actions">
+	  `;
+	  if (field.field_type != 'author_bio'){
+            html += `<button class="icon-button duplicate" data-field-id="${field.id}" title="Duplicate">⎘</button>`;
+	  }
+      html += `
+            <button class="icon-button delete" data-field-id="${field.id}" title="Delete">✕</button>
+          </div>
+        </div>
+      `;
+    });
+    
+    html += '</div>';
+    canvas.innerHTML = html;
+    
+    // Setup field card event listeners
+    this.shadowRoot.querySelectorAll('.field-card').forEach(card => {
+      card.addEventListener('click', (e) => {
+        if (!e.target.closest('.icon-button')) {
+          this.selectedFieldId = card.dataset.fieldId;
+		  this.switchTab('config');
+          this.updateConfigPanel();
+          this.updateCanvasView();
+        }
+      });
+      
+      card.addEventListener('dragstart', (e) => {
+        // Explicitly set data to satisfy browser validation
+        e.dataTransfer.setData('text/plain', card.dataset.fieldId); 
+        e.dataTransfer.effectAllowed = 'move';
+        
+        this.draggedFieldId = card.dataset.fieldId;
+        this.draggedFromIndex = this.fields.findIndex(f => f.id === this.draggedFieldId);
+        card.style.opacity = '0.5';
+      });
+      
+      card.addEventListener('dragend', () => {
+        card.style.opacity = '1';
+        this.draggedFieldId = null;
+        this.draggedFromIndex = null;
+      });
+      
+      card.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        // Stop propagation to prevent Canvas "copy" effect from overriding "move"
+        e.stopPropagation(); 
+        e.dataTransfer.dropEffect = 'move';
+        
+        // Add visual cue
+        card.style.borderTop = '2px solid var(--accent-color)';
+      });
+      
+      card.addEventListener('dragleave', (e) => {
+        // Ensure we don't remove style when entering child elements
+        // Only remove if we are genuinely leaving the card rect
+        const rect = card.getBoundingClientRect();
+        const x = e.clientX;
+        const y = e.clientY;
+        
+        // Check if cursor is actually outside the element bounds
+        if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
+             card.style.borderTop = '';
+        }
+      });
+      
+      card.addEventListener('drop', (e) => {
+        e.preventDefault();
+        // Stop propagation so the canvas handler doesn't trigger
+        e.stopPropagation(); 
+        
+        card.style.borderTop = '';
+        
+        if (this.draggedFieldId && this.draggedFieldId !== card.dataset.fieldId) {
+          const toIndex = this.fields.findIndex(f => f.id === card.dataset.fieldId);
+          
+          if (this.draggedFromIndex !== -1 && toIndex !== -1) {
+            // Perform the move
+            const [draggedField] = this.fields.splice(this.draggedFromIndex, 1);
+            this.fields.splice(toIndex, 0, draggedField);
+            
+            // Re-render
+            this.updateCanvasView();
+            this.updateYamlOutput();
+          }
+        }
+      });
+    });
+    
+    // Setup action buttons
+    this.shadowRoot.querySelectorAll('.icon-button.duplicate').forEach(btn => {
+      btn.addEventListener('click', (e) => {
+        e.stopPropagation();
+        this.duplicateField(btn.dataset.fieldId);
+      });
+    });
+    
+    this.shadowRoot.querySelectorAll('.icon-button.delete').forEach(btn => {
+      btn.addEventListener('click', (e) => {
+        e.stopPropagation();
+        this.deleteField(btn.dataset.fieldId);
+      });
+    });
+    
+    this.updateFieldCount();
+  }
+ 
+  updateAuthorBioState() {
+    const hasAuthorBio = this.fields.some(f => f.field_type === 'author_bio');
+    const btn = this.shadowRoot.querySelector('button[data-field-type="author_bio"]');
+    
+    if (btn) {
+      if (hasAuthorBio) {
+        btn.setAttribute('disabled', 'true');
+        btn.removeAttribute('draggable');
+        btn.title = "Only one Author Bio allowed";
+      } else {
+        btn.removeAttribute('disabled');
+        btn.setAttribute('draggable', 'true');
+        btn.removeAttribute('title');
+      }
+    }
+  }
+ 
+  updateConfigPanel() {
+	  const field = this.fields.find(f => f.id === this.selectedFieldId);
+	  if (!field) {
+		this.shadowRoot.getElementById('configContent').innerHTML = '<div class="no-field-selected">Select a field to edit its properties</div>';
+		return;
+	  }
+	  
+	  const typeConfig = this.fieldTypes[field.field_type];
+	  
+	  // 1. Define Standard Properties
+	  let visibleProps = ['keyname', 'field_type_display', 'name', 'description', 'i18n_descriptions', 'help_text'];
+	  
+	  // 2. Add Type-Specific Properties (These can now be strings OR objects)
+	  if (typeConfig && Array.isArray(typeConfig.properties)) {
+		visibleProps = [...visibleProps, ...typeConfig.properties];
+	  }
+
+	  // 3. Generate HTML
+	  let html = '';
+	  visibleProps.forEach(propEntry => { // propEntry is now string or object
+		
+		// Determine the base key (string or object.key)
+		const propKey = typeof propEntry === 'string' ? propEntry : propEntry.key;
+		
+		// Special case for static display of field type
+		if (propKey === 'field_type_display') {
+		  html += `
+			<div class="form-group">
+			  <label class="form-label">Field Type</label>
+			  <div style="font-size: 13px; color: var(--text-primary); padding: 8px; background: var(--bg-secondary); border-radius: 4px;">${field.field_type}</div>
+			</div>`;
+		  return;
+		}
+
+		let propDef = this.propertyDefinitions[propKey];
+		
+		// --- NEW LOGIC: Merge Overrides ---
+		if (typeof propEntry === 'object' && propDef) {
+			// Create a new definition by merging the base definition with the specific overrides
+			propDef = {...propDef, ...propEntry}; 
+		}
+
+		if (propDef) {
+		  // Pass the key and the potentially merged definition to the renderer
+		  html += this.renderPropertyInput(propKey, propDef, field);
+		}
+	  });
+	  
+	  this.shadowRoot.getElementById('configContent').innerHTML = html;
+	  
+	  // 4. Re-attach Event Listeners
+	  this.attachConfigListeners(field);
+  }
+ 
+  renderPropertyInput(key, def, field) {
+	  const value = field[key] !== undefined ? field[key] : (def.default || '');
+	  
+	  if (def.type === 'custom_categories') {
+		const selectedCategories = field.category ? field.category.split(',').map(c => c.trim()).filter(c => c) : [];
+		return `
+		  <div class="form-group" id="prop-${key}">
+			<label class="form-label">${def.label}</label>
+			<div id="categoryPills" style="margin-bottom: 8px;">
+			  ${this.renderCategoryPills(selectedCategories)}
+			</div>
+			<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">Selected: ${selectedCategories.length}/2</div>
+		  </div>
+		`;
+	  }
+	  
+	  if (def.type === 'checkbox') {
+		return `
+		  <div class="form-group" id="prop-${key}">
+		  <label class="form-checkbox">
+		  <input type="checkbox" data-prop="${key}" ${value ? 'checked' : ''}>
+			${def.label}  </label>
+		  </div>`;
+	  }
+	  
+	  if (def.type === 'textarea') {
+		  // Convert array options to string for display
+		  let displayValue = value;
+		  let useLabelValue = false;
+		  
+		  if (key === 'options' && Array.isArray(value)) {
+			// Check if any option is an object (label:value pair)
+			useLabelValue = value.some(opt => typeof opt === 'object');
+			
+			if (useLabelValue) {
+			  // Convert objects to "Label: value" format
+			  displayValue = value.map(opt => {
+				if (typeof opt === 'object') {
+				  const [label, val] = Object.entries(opt)[0];
+				  return `${label}: ${val}`;
+				}
+				return opt;
+			  }).join('\n');
+			} else {
+			  // Simple string array
+			  displayValue = value.join('\n');
+			}
+		  }
+		  
+		  // Add toggle for options field
+		  const optionsToggle = key === 'options' ? `
+			<label class="form-checkbox" style="margin-bottom: 8px;">
+			  <input type="checkbox" id="use-label-value" ${useLabelValue ? 'checked' : ''}>
+			  Use Label:Value pairs
+			</label>
+			<div id="options-help" style="font-size: 11px; color: var(--text-secondary); margin-bottom: 6px;">
+			  ${useLabelValue ? 'Enter one per line in format "Label: value"' : 'Enter one option per line'}
+			</div>
+		  ` : '';
+		  
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  ${optionsToggle}
+			  <textarea class="form-textarea" data-prop="${key}" placeholder="${def.placeholder || ''}">${displayValue}</textarea>
+			  ${!optionsToggle && def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>`;
+	  }
+	  
+	  // --- i18n_map Renderer ---
+	  if (def.type === 'i18n_map') {
+		// 1. Find existing translations in the field object
+		const existingTranslations = Object.keys(field)
+		  .filter(k => k.startsWith('description-'))
+		  .map(k => {
+			const code = k.replace('description-', '');
+			return { code, value: field[k] };
+		  });
+
+		// 2. Build the list of existing translation inputs
+		let rowsHtml = existingTranslations.map(item => `
+		  <div class="i18n-row" style="margin-top: 8px; border-left: 2px solid var(--accent-color); padding-left: 8px;">
+			<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
+			  <span style="font-weight: 600;">${item.code}</span>
+			  <button class="remove-i18n-btn" data-code="${item.code}" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px;">&times;</button>
+			</div>
+			<textarea class="form-textarea i18n-input" data-code="${item.code}" rows="2">${item.value}</textarea>
+		  </div>
+		`).join('');
+
+		// 3. Build the "Add New" dropdown
+		// Filter out locales that are already added
+		const availableLocales = this.locales.filter(l => !field[`description-${l.code}`]);
+		
+		let addHtml = '';
+		if (availableLocales.length > 0) {
+		  addHtml = `
+			<div style="display: flex; gap: 8px; margin-top: 8px;">
+			  <select id="new-locale-select" class="form-input" style="padding: 4px;">
+				<option value="">Select language...</option>
+				${availableLocales.map(l => `<option value="${l.code}">${l.label} (${l.code})</option>`).join('')}
+			  </select>
+			  <button id="add-locale-btn" class="btn-secondary" style="padding: 4px 8px; font-size: 12px;">Add</button>
+			</div>
+		  `;
+		}
+
+		return `
+		  <div class="form-group" id="prop-${key}">
+			<label class="form-label">${def.label}</label>
+			<div id="i18n-container">
+			  ${rowsHtml}
+			</div>
+			${addHtml}
+			${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+		  </div>`;
+	  }
+	  
+	  // --- Handle generic Select inputs (including http_verb) ---
+	  if (def.options && Array.isArray(def.options)) {
+		  // Determine the current value, falling back to default if not set
+		  const currentValue = field[key] !== undefined ? field[key] : (def.default || '');
+
+		  const optionsHtml = def.options.map(option => {
+			// The option value is the method name itself (GET, POST, etc.)
+			const selected = currentValue === option ? 'selected' : '';
+			return `<option value="${option}" ${selected}>${option}</option>`;
+		  }).join('');
+
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  <select class="form-input" data-prop="${key}">
+				${optionsHtml}
+			  </select>
+			  ${def.help ? `<div style="color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>`;
+	  }
+
+	  // --- Conditional Builder Renderer ---
+	  if (def.type === 'conditional_builder') {
+		  const conditions = field.conditional_validation || [];
+		  
+		  // Get list of other fields for checkboxes
+		  const otherFields = this.fields.filter(f => f.id !== field.id);
+		  
+		  let conditionsHtml = conditions.map((condition, idx) => {
+			const hiddenSet = new Set(condition.hidden || []);
+			
+			const checkboxesHtml = otherFields.map(f => {
+			  const checked = hiddenSet.has(f.keyname) ? 'checked' : '';
+			  return `
+				<label style="display: block; margin: 4px 0; cursor: pointer;">
+				  <input type="checkbox" class="condition-hidden-cb" data-condition-idx="${idx}" data-field-keyname="${f.keyname}" ${checked} style="margin-right: 6px;">
+				  <span style="font-size: 12px;">${f.keyname} (${f.name})</span>
+				</label>
+			  `;
+			}).join('');
+			
+			return `
+			  <div class="condition-block" data-condition-idx="${idx}" style="border-left: 2px solid var(--accent-color); padding-left: 12px; margin-top: 12px;">
+				<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
+				  <div style="display: flex; align-items: center; gap: 8px;">
+					<span style="font-size: 12px; font-weight: 600;">When equals:</span>
+					<input type="text" class="form-input condition-when-input" data-condition-idx="${idx}" value="${condition.when || ''}" style="width: 150px; padding: 4px 8px;">
+				  </div>
+				  <button class="remove-condition-btn" data-condition-idx="${idx}" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 18px; font-weight: bold;">&times;</button>
+				</div>
+				<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; margin-bottom: 6px; color: var(--text-secondary);">Hide these fields:</div>
+				<div style="max-height: 150px; overflow-y: auto; padding: 4px 0;">
+				  ${checkboxesHtml || '<div style="color: var(--text-secondary); font-size: 12px; font-style: italic;">No other fields available</div>'}
+				</div>
+			  </div>
+			`;
+		  }).join('');
+		  
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  <div id="conditional-container">
+				${conditionsHtml}
+			  </div>
+			  <button id="add-condition-btn" style="margin-top: 12px; padding: 6px 12px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; font-size: 12px; width: 100%;">⊕ Add Condition</button>
+			  ${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>
+		  `;
+	  }
+
+	  // --- Special handling for 'default' field ---
+	  if (key === 'default') {
+		  const hasDefault = field.hasOwnProperty('default');
+		  const defaultValue = hasDefault ? (field.default || '') : '';
+		  
+		  return `
+			<div class="form-group" id="prop-${key}">
+			  <label class="form-label">${def.label}</label>
+			  <label class="form-checkbox" style="margin-bottom: 8px;">
+				<input type="checkbox" id="enable-default" ${hasDefault ? 'checked' : ''}>
+				Set default value
+			  </label>
+			  <input type="${def.type}" class="form-input" id="default-input" data-prop="${key}" value="${defaultValue}" placeholder="${def.placeholder || ''}" ${!hasDefault ? 'disabled' : ''} style="opacity: ${hasDefault ? '1' : '0.5'}">
+			  ${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+			</div>`;
+	  }
+
+	  // Default: Text, Number, URL inputs
+	  return `
+		  <div class="form-group" id="prop-${key}">
+			<label class="form-label">${def.label}</label>
+			<input type="${def.type}" class="form-input" data-prop="${key}" value="${value}" placeholder="${def.placeholder || ''}">
+			${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
+		  </div>`;
+  }
+ 
+ 
+  attachConfigListeners(field) {
+	  const container = this.shadowRoot.getElementById('configContent');
+
+	  // Generic Inputs
+	  container.querySelectorAll('[data-prop]').forEach(input => {
+		input.addEventListener('input', (e) => this.handleConfigInput(e, field));
+		input.addEventListener('change', (e) => this.handleConfigInput(e, field));
+	  });
+
+	  // Handle Label:Value toggle for options
+	  const labelValueToggle = container.querySelector('#use-label-value');
+	  if (labelValueToggle) {
+	    labelValueToggle.addEventListener('change', (e) => {
+		  const helpDiv = container.querySelector('#options-help');
+		  if (helpDiv) {
+		    helpDiv.textContent = e.target.checked 
+			  ? 'Enter one per line in format "Label: value"'
+			  : 'Enter one option per line';
+		  }
+	    });
+ 	  }
+	  
+	  // Handle "Set default value" checkbox
+	  const enableDefaultCheckbox = container.querySelector('#enable-default');
+		const defaultInput = container.querySelector('#default-input');
+		if (enableDefaultCheckbox && defaultInput) {
+		  enableDefaultCheckbox.addEventListener('change', (e) => {
+			if (e.target.checked) {
+			  // Enable the input and set default to empty string if not already set
+			  defaultInput.disabled = false;
+			  defaultInput.style.opacity = '1';
+			  if (!field.hasOwnProperty('default')) {
+				field.default = '';
+			  }
+			} else {
+			  // Disable the input and remove the default property
+			  defaultInput.disabled = true;
+			  defaultInput.style.opacity = '0.5';
+			  delete field.default;
+			}
+			this.updateYamlOutput();
+		  });
+	  }
+	  
+	  // Special Case: Categories (Pills)
+	  const pills = container.querySelectorAll('.category-pill');
+	  if (pills.length > 0) {
+		pills.forEach(pill => {
+		  pill.addEventListener('click', (e) => {
+			e.preventDefault();
+			e.stopPropagation();
+			
+			const category = pill.dataset.category;
+			// Parse current selection
+			let selected = field.category ? field.category.split(',').map(c => c.trim()).filter(c => c) : [];
+			
+			const index = selected.indexOf(category);
+			
+			if (index > -1) {
+			  // Deselect
+			  selected.splice(index, 1);
+			} else if (selected.length < 2) {
+			  // Select (if under limit)
+			  selected.push(category);
+			} else {
+			  // Limit Reached Feedback
+			  this.showToast('You can only select up to 2 categories');
+			  return; // Stop here so we don't re-render unnecessarily
+			}
+			
+			field.category = selected.join(',');
+			this.updateConfigPanel(); // Re-render to update pill styling
+			this.updateYamlOutput();
+		  });
+		});
+	  }
+	  
+	  // --- i18n Listeners ---
+	  // Handle Input Changes (typing in the translation box)
+	  container.querySelectorAll('.i18n-input').forEach(input => {
+		input.addEventListener('input', (e) => {
+		  const code = e.target.dataset.code;
+		  field[`description-${code}`] = e.target.value;
+		  this.updateYamlOutput();
+		});
+	  });
+
+	  // Handle "Add" Button
+	  const addBtn = container.querySelector('#add-locale-btn');
+	  if (addBtn) {
+		addBtn.addEventListener('click', (e) => {
+		  e.preventDefault();
+		  const select = container.querySelector('#new-locale-select');
+		  const code = select.value;
+		  if (code) {
+			// Initialize the new key
+			field[`description-${code}`] = '';
+			this.updateConfigPanel(); // Re-render to show the new input
+			this.updateYamlOutput();
+		  }
+		});
+	  }
+
+	  // Handle "Remove" Buttons
+	  container.querySelectorAll('.remove-i18n-btn').forEach(btn => {
+		btn.addEventListener('click', (e) => {
+		  e.preventDefault();
+		  const code = e.target.dataset.code;
+		  delete field[`description-${code}`]; // Remove the key entirely
+		  this.updateConfigPanel(); // Re-render to remove the input
+		  this.updateYamlOutput();
+		});
+	  });
+	  
+	  // --- Conditional Logic Listeners ---
+		// Add Condition Button
+		const addConditionBtn = container.querySelector('#add-condition-btn');
+		if (addConditionBtn) {
+		  addConditionBtn.addEventListener('click', (e) => {
+			e.preventDefault();
+			if (!field.conditional_validation) {
+			  field.conditional_validation = [];
+			}
+			field.conditional_validation.push({
+			  when: '',
+			  hidden: []
+			});
+			this.updateConfigPanel();
+			this.updateYamlOutput();
+		  });
+		}
+
+		// Remove Condition Buttons
+		container.querySelectorAll('.remove-condition-btn').forEach(btn => {
+		  btn.addEventListener('click', (e) => {
+			e.preventDefault();
+			const idx = parseInt(e.target.dataset.conditionIdx);
+			field.conditional_validation.splice(idx, 1);
+			if (field.conditional_validation.length === 0) {
+			  delete field.conditional_validation;
+			}
+			this.updateConfigPanel();
+			this.updateYamlOutput();
+		  });
+		});
+
+		// When Value Inputs
+		container.querySelectorAll('.condition-when-input').forEach(input => {
+		  input.addEventListener('input', (e) => {
+			const idx = parseInt(e.target.dataset.conditionIdx);
+			field.conditional_validation[idx].when = e.target.value;
+			this.updateYamlOutput();
+		  });
+		});
+
+		// Hidden Field Checkboxes
+		container.querySelectorAll('.condition-hidden-cb').forEach(cb => {
+		  cb.addEventListener('change', (e) => {
+			const idx = parseInt(e.target.dataset.conditionIdx);
+			const fieldKeyname = e.target.dataset.fieldKeyname;
+			
+			if (!field.conditional_validation[idx].hidden) {
+			  field.conditional_validation[idx].hidden = [];
+			}
+			
+			if (e.target.checked) {
+			  if (!field.conditional_validation[idx].hidden.includes(fieldKeyname)) {
+				field.conditional_validation[idx].hidden.push(fieldKeyname);
+			  }
+			} else {
+			  field.conditional_validation[idx].hidden = field.conditional_validation[idx].hidden.filter(k => k !== fieldKeyname);
+			}
+			
+			this.updateYamlOutput();
+		  });
+		});
+  }
+ 
+  handleConfigInput(e, field) {
+	  const input = e.target;
+	  const prop = input.dataset.prop;
+	  let value;
+	  const container = this.shadowRoot.getElementById('configContent');
+
+	  // 1. Determine Value
+	  if (input.type === 'checkbox') {
+		value = input.checked;
+	  } else if (input.type === 'number') {
+		value = input.value === '' ? undefined : Number(input.value);
+	  } else if (prop === 'options') {
+		  const lines = input.value.split('\n').filter(o => o.trim());
+		  
+		  // Check if we're in label:value mode
+		  const useLabelValue = container.querySelector('#use-label-value')?.checked;
+		  
+		  if (useLabelValue) {
+			// Parse as label:value pairs
+			value = lines.map(line => {
+			  const colonIndex = line.indexOf(':');
+			  if (colonIndex > -1) {
+				const label = line.substring(0, colonIndex).trim();
+				const val = line.substring(colonIndex + 1).trim();
+				return { [label]: val };
+			  }
+			  // Fallback to simple string if no colon found
+			  return line.trim();
+			});
+		  } else {
+			// Simple string array
+			value = lines;
+		  }
+	  } else {
+		value = input.value;
+		// Special handling for 'default' - allow empty string but only if checkbox is checked
+		if (prop === 'default' && value === '') {
+			// If checkbox is checked, explicitly set to empty string
+			// If unchecked, this won't be called because input is disabled
+			field[prop] = '';
+			this.updateYamlOutput();
+			return;
+		}
+	  }
+
+	  // 2. Save Value
+	  field[prop] = value;
+	  
+	  // 3. --- NEW: Auto-populate help text for multiple select ---
+	  if (prop === 'multiple' && value === true && field.field_type === 'select') {
+		// Only populate if currently empty to avoid overwriting user content
+		if (!field.help_text || !field.help_text.trim()) {
+		  field.help_text = 'Use <kbd>⌘</kbd>+<kbd>click</kbd> or <kbd>ctrl</kbd>+<kbd>click</kbd> to select and deselect multiple options.';
+		  
+		  // Force a re-render so the user sees the text appear immediately in the specific field
+		  this.updateConfigPanel(); 
+		}
+	  }
+
+	  // 4. Specific UI updates
+	  if (prop === 'keyname' || prop === 'name') {
+		this.updateCanvasView(); // Refresh the sidebar list items
+	  }
+	  
+	  this.updateYamlOutput();
+  }
+ 
+ 
+  updateYamlOutput() {
+    const yaml = this.generateYaml();
+    this.shadowRoot.getElementById('yamlOutput').textContent = yaml;
+  }
+ 
+  generateYaml() {
+    if (this.fields.length === 0) {
+      return '# No fields defined yet';
+    }
+    
+    const lines = [];
+    
+    this.fields.forEach(field => {
+      lines.push(`- keyname: ${field.keyname}`);
+      lines.push(`  field_type: ${field.field_type}`);
+      lines.push(`  name: ${this.escapeYaml(field.name)}`);
+      
+      if (field.description) {
+        lines.push(`  description: ${this.escapeYaml(field.description)}`);
+      }
+      
+      if (field.help_text) {
+        lines.push(`  help_text: ${this.escapeYaml(field.help_text)}`);
+      }
+      
+      if (field.placeholder) {
+        lines.push(`  placeholder: ${this.escapeYaml(field.placeholder)}`);
+      }
+      
+      if (field.hasOwnProperty('default')) {
+		  lines.push(`  default: ${this.escapeYaml(field.default)}`);
+	  }
+      
+      if (field.optional) {
+        lines.push(`  optional: true`);
+      }
+      
+      if (field.min !== undefined) {
+        lines.push(`  min: ${field.min}`);
+      }
+      
+      if (field.max !== undefined) {
+        lines.push(`  max: ${field.max}`);
+      }
+      
+      if (field.maxlength !== undefined) {
+        lines.push(`  maxlength: ${field.maxlength}`);
+      }
+
+      if (field.rows !== undefined) {
+        lines.push(`  rows: ${field.rows}`);
+      }
+      
+      if (field.options && field.options.length > 0) {
+		  // console.log('Field options:', field.keyname, field.options);
+		  lines.push(`  options:`);
+		  field.options.forEach(opt => {
+			if (typeof opt === 'object') {
+			  // Label:Value pair - format as YAML key:value
+			  const [label, value] = Object.entries(opt)[0];
+			  // Don't escape the label in the key position, only the value
+			  lines.push(`  - ${label}: ${this.escapeYaml(value)}`);
+			} else {
+			  // Simple string
+			  lines.push(`  - ${this.escapeYaml(opt)}`);
+			}
+		  });
+	  }
+      
+      if (field.multiple) {
+        lines.push(`  multiple: true`);
+      }
+      
+      if (field.endpoint) {
+        lines.push(`  endpoint: ${this.escapeYaml(field.endpoint)}`);
+      }
+	  
+      const isXhrField = field.field_type === 'xhrSelect' || field.field_type === 'xhrSelectSearch';
+      if (isXhrField) {
+          // Determine the verb: use the field property value, or fall back to the default 'GET'
+          const verb = field.http_verb || this.propertyDefinitions.http_verb.default;
+          
+          // Always print the http_verb for XHR fields, even if it's the default 'GET'.
+          lines.push(`  http_verb: ${verb}`);
+      } else if (field.http_verb && field.http_verb !== 'GET') {
+          // Keep this secondary check for non-XHR fields that might use a non-default verb 
+          // (though unlikely with current definitions)
+          lines.push(`  http_verb: ${field.http_verb}`);
+      }
+	       
+      if (field.value) {
+        lines.push(`  value: ${this.escapeYaml(field.value)}`);
+      }
+      
+      if (field.category) {
+        lines.push(`  category: ${this.escapeYaml(field.category)}`);
+      }
+      
+      if (field.github_url) {
+        lines.push(`  github_url: ${this.escapeYaml(field.github_url)}`);
+      }
+      if (field.email_address) {
+        lines.push(`  email_address: ${this.escapeYaml(field.email_address)}`);
+      }
+      if (field.learn_more_url) {
+        lines.push(`  learn_more_url: ${this.escapeYaml(field.learn_more_url)}`);
+      }
+      if (field.youtube_url) {
+        lines.push(`  youtube_url: ${this.escapeYaml(field.youtube_url)}`);
+      }
+	  
+	  // Conditional validation
+	  if (field.conditional_validation && field.conditional_validation.length > 0) {
+		lines.push(`  conditional_validation:`);
+		field.conditional_validation.forEach(condition => {
+			lines.push(`  - when: ${this.escapeYaml(condition.when)}`);
+			if (condition.hidden && condition.hidden.length > 0) {
+			  lines.push(`    hidden:`);
+			  condition.hidden.forEach(keyname => {
+				lines.push(`    - ${keyname}`);
+			  });
+			}
+		});
+	  }
+
+	  // Capture any i18n description keys
+	  Object.keys(field).forEach(k => {
+	    if (k.startsWith('description-')) {
+		  lines.push( `  ${k}: "${field[k]}"`);
+	    }
+	  });
+      
+    });
+    
+    return lines.join('\n');
+  }
+ 
+  escapeYaml(value) {
+	  if (!value && value !== 0) return '""';
+	  
+	  // Convert to string if it's a number
+	  if (typeof value === 'number') { return value.toString(); }
+	  
+	  // If it's not a string at this point, return empty
+	  if (typeof value !== 'string') return '""';
+	  
+	  // Check if value needs quoting
+	  if (
+        /[:#\[\]\{\},&\*\|>!'"%@`\n\t]/.test(value) || // Existing special chars
+        /^\d+$/.test(value) ||                       // Existing numeric string check
+        value.startsWith(' ') || value.endsWith(' ') || // Existing whitespace check
+        /^(true|false|yes|no|on|off)$/i.test(value)      // NEW check for YAML literals (case-insensitive)
+      ){
+		  return `"${value.replace(/"/g, '\\"')}"`;
+	  };
+	  
+	  return value;  
+  }
+ 
+  reorderFields(draggedId, targetId) {
+    const draggedIndex = this.fields.findIndex(f => f.id === draggedId);
+    const targetIndex = this.fields.findIndex(f => f.id === targetId);
+    
+    if (draggedIndex !== -1 && targetIndex !== -1) {
+      const [draggedField] = this.fields.splice(draggedIndex, 1);
+      this.fields.splice(targetIndex, 0, draggedField);
+      this.updateCanvasView();
+      this.updateYamlOutput();
+    }
+  }
+ 
+  duplicateField(fieldId) {
+    const field = this.fields.find(f => f.id === fieldId);
+    
+    // Prevent duplicating author_bio (existing logic)
+    if (field && field.field_type === 'author_bio') {
+      this.showToast('Author Bio field cannot be duplicated');
+      return;
+    }
+
+    if (field) {
+      // 1. Create a deep copy of the original field object
+      const newField = JSON.parse(JSON.stringify(field));
+      
+      // 2. Assign a new unique ID
+      const newFieldId = `field_${this.nextFieldId++}`;
+      newField.id = newFieldId;
+      
+      // 3. FIX: Generate a clean, unique keyname based on the field type
+      // Example: 'string' -> 'string_2'
+      newField.keyname = `${field.field_type}_${this.nextFieldId - 1}`;
+      
+      // 4. Insert the new field and update state
+      this.fields.push(newField);
+      this.selectedFieldId = newField.id;
+      
+      // Ensure the config panel opens for the newly duplicated field
+      this.updateConfigPanel();
+      this.updateCanvasView();
+      this.updateYamlOutput();
+      this.updateAuthorBioState();
+    }
+  }
+ 
+  deleteField(fieldId) {
+    this.fields = this.fields.filter(f => f.id !== fieldId);
+    if (this.selectedFieldId === fieldId) {
+      this.selectedFieldId = this.fields[0]?.id || null;
+    }
+    this.updateCanvasView();
+    this.updateConfigPanel();
+    this.updateYamlOutput();
+	this.updateAuthorBioState();
+  }
+ 
+  updateFieldCount() {
+    this.shadowRoot.querySelector('#fieldCount').textContent = this.fields.length;
+  }
+ 
+  switchTab(tab) {
+    this.shadowRoot.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+    this.shadowRoot.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
+    
+    this.shadowRoot.querySelector(`[data-tab="${tab}"]`).classList.add('active');
+    this.shadowRoot.getElementById(`${tab}Tab`).classList.add('active');
+  }
+ 
+  copyYaml() {
+    const yaml = this.generateYaml();
+    navigator.clipboard.writeText(yaml).then(() => {
+      this.showToast('YAML copied to clipboard');
+    });
+  }
+  
+  importYaml() {
+	  // Create modal overlay
+	  const modal = document.createElement('div');
+	  modal.style.cssText = `
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.5);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		z-index: 9999;
+	  `;
+	  
+	  const modalContent = document.createElement('div');
+	  modalContent.style.cssText = `
+		background: ${this.theme === 'dark' ? '#1a1a1a' : '#ffffff'};
+		border: 1px solid ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+		border-radius: 8px;
+		padding: 24px;
+		width: 600px;
+		max-width: 90vw;
+		max-height: 80vh;
+		display: flex;
+		flex-direction: column;
+	  `;
+	  
+	  modalContent.innerHTML = `
+		<h3 style="margin: 0 0 16px 0; color: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'}; font-size: 18px; font-weight: 600;">Import YAML</h3>
+		<textarea id="yamlImportTextarea" placeholder="Paste your YAML here..." style="
+		  flex: 1;
+		  min-height: 300px;
+		  padding: 12px;
+		  border: 1px solid ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+		  border-radius: 4px;
+		  background: ${this.theme === 'dark' ? '#2a2a2a' : '#ffffff'};
+		  color: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+		  font-family: monospace;
+		  font-size: 12px;
+		  resize: vertical;
+		  margin-bottom: 16px;
+		"></textarea>
+		<div style="display: flex; gap: 8px; justify-content: flex-end;">
+		  <button id="cancelImportBtn" style="
+			padding: 8px 16px;
+			border: 1px solid ${this.theme === 'dark' ? '#404040' : '#e0e0e0'};
+			background: ${this.theme === 'dark' ? '#2a2a2a' : '#f5f5f5'};
+			color: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+			border-radius: 4px;
+			cursor: pointer;
+			font-size: 13px;
+			font-weight: 600;
+		  ">Cancel</button>
+		  <button id="confirmImportBtn" style="
+			padding: 8px 16px;
+			border: none;
+			background: ${this.accentColor};
+			color: white;
+			border-radius: 4px;
+			cursor: pointer;
+			font-size: 13px;
+			font-weight: 600;
+		  ">Import</button>
+		</div>
+	  `;
+	  
+	  modal.appendChild(modalContent);
+	  document.body.appendChild(modal);
+	  
+	  const textarea = modalContent.querySelector('#yamlImportTextarea');
+	  const cancelBtn = modalContent.querySelector('#cancelImportBtn');
+	  const confirmBtn = modalContent.querySelector('#confirmImportBtn');
+	  
+	  // Focus textarea
+	  textarea.focus();
+	  
+	  // Cancel handler
+	  const closeModal = () => {
+		document.body.removeChild(modal);
+	  };
+	  
+	  cancelBtn.addEventListener('click', closeModal);
+	  modal.addEventListener('click', (e) => {
+		if (e.target === modal) closeModal();
+	  });
+	  
+	  // Import handler
+	  confirmBtn.addEventListener('click', () => {
+		const yamlText = textarea.value;
+		
+		if (!yamlText || !yamlText.trim()) {
+		  closeModal();
+		  return;
+		}
+		
+		try {
+		  const fields = this.parseYaml(yamlText);
+		  // console.log('Parsed fields:', fields);
+		  if (fields.length === 0) {
+			this.showToast('No fields found in YAML');
+			closeModal();
+			return;
+		  }
+		  
+		  // Clear existing fields and import new ones
+		  this.fields = fields.map((fieldData, index) => {
+			const field = {
+			  id: `field_${this.nextFieldId++}`,
+			  keyname: fieldData.keyname || `field_${index}`,
+			  field_type: fieldData.field_type || 'string',
+			  name: fieldData.name || '',
+			};
+			
+			// Copy over all other properties
+			Object.keys(fieldData).forEach(key => {
+			  if (key !== 'id') {
+				field[key] = fieldData[key];
+			  }
+			});
+			
+			return field;
+		  });
+		  
+		  this.selectedFieldId = this.fields[0]?.id || null;
+		  this.updateCanvasView();
+		  this.updateConfigPanel();
+		  this.updateYamlOutput();
+		  this.updateAuthorBioState();
+		  this.switchTab('config');
+		  this.showToast(`Imported ${fields.length} field(s)`);
+		  closeModal();
+		} catch (error) {
+		  alert(`Error parsing YAML: ${error.message}`);
+		}
+	  });
+	}
+  
+	parseYaml(yamlText) {
+		const fields = [];
+		const lines = yamlText.split('\n');
+		let currentField = null;
+		let currentArray = null;
+		let currentArrayKey = null;
+		let currentCondition = null;
+		let currentNestedKey = null; // Helper to track if we are in hidden/required
+
+		lines.forEach(line => {
+		  // Skip comments and empty lines
+		  if (line.trim().startsWith('#') || !line.trim()) {
+			return;
+		  }
+
+		  // 1. New field (starts with - keyname:)
+		  if (line.match(/^-\s+keyname:/)) {
+			if (currentField) {
+			  fields.push(currentField);
+			}
+			currentField = {};
+			currentArray = null;
+			currentArrayKey = null;
+			currentCondition = null;
+			currentNestedKey = null;
+			const value = line.split('keyname:')[1].trim();
+			currentField.keyname = this.unescapeYaml(value);
+		  }
+		  
+		  // 2. Field properties (2 spaces indentation)
+		  else if (currentField && line.match(/^\s{2}[a-z_-]+:/)) {
+			const match = line.match(/^\s{2}([a-z_-]+):\s*(.*)$/);
+			if (match) {
+			  const key = match[1];
+			  const value = match[2].trim();
+
+			  // Handle array start (value is empty)
+			  if (!value || value === '') {
+				currentArrayKey = key;
+				currentArray = [];
+
+				if (key === 'conditional_validation') {
+				  currentField[key] = [];
+				} else {
+				  currentField[key] = currentArray;
+				}
+			  } else {
+				// Scalar values
+				currentArrayKey = null;
+				currentArray = null;
+				currentField[key] = this.unescapeYaml(value);
+
+				// Convert string booleans
+				if (value === 'true') currentField[key] = true;
+				if (value === 'false') currentField[key] = false;
+
+				// Convert numbers (but not empty strings!)
+				if (!isNaN(value) && value !== '' && value !== '""') {
+				  currentField[key] = Number(value);
+				}
+			  }
+			}
+		  }
+
+		  // 3. Nested properties in conditions (e.g., "    hidden:")
+		  // We check this BEFORE array items to set context
+		  else if (currentCondition && line.match(/^\s{4}[a-z_-]+:/)) {
+			const match = line.match(/^\s{4}([a-z_]+):\s*(.*)$/);
+			if (match) {
+			  const key = match[1];
+			  // Initialize the nested array
+			  currentCondition[key] = [];
+			  currentNestedKey = key; // Track that we are currently filling this key
+			}
+		  }
+
+		  // 4. Nested Array Items (Deep indentation: 4 or more spaces + dash)
+		  // IMPORTANT: This must come BEFORE the generic array check
+		  else if (currentCondition && line.match(/^\s{4,}-\s+/)) {
+			const value = line.replace(/^\s{4,}-\s+/, '').trim();
+			
+			// Add to the currently active nested array (hidden or required)
+			if (currentNestedKey && Array.isArray(currentCondition[currentNestedKey])) {
+			   currentCondition[currentNestedKey].push(value);
+			}
+		  }
+
+		  // 5. Top-Level Array items (2 or more spaces + dash)
+		  // But we've already handled conditional nested items above with a more specific check
+		  else if (line.match(/^\s{2,}-\s+/)) {
+			const value = line.replace(/^\s{2,}-\s+/, '').trim();
+
+			// If we're in conditional_validation, this is a NEW condition object
+			if (currentArrayKey === 'conditional_validation') {
+			  currentCondition = {};
+			  currentField.conditional_validation.push(currentCondition);
+			  currentNestedKey = null; // Reset nested context for new object
+
+			  // Parse the "when" value
+			  if (value.startsWith('when:')) {
+				currentCondition.when = this.unescapeYaml(value.split('when:')[1].trim());
+			  }
+			}
+			// Otherwise it's a regular array item (like options)
+			else if (currentArray) {
+			  const colonIndex = value.indexOf(':');
+			  if (currentArrayKey === 'options' && colonIndex > -1) {
+				const label = value.substring(0, colonIndex).trim();
+				const val = value.substring(colonIndex + 1).trim();
+				currentArray.push({ [this.unescapeYaml(label)]: this.unescapeYaml(val) });
+			  } else {
+				currentArray.push(this.unescapeYaml(value));
+			  }
+			}
+		  }
+		});
+
+		// Don't forget the last field
+		if (currentField) {
+		  fields.push(currentField);
+		}
+
+		return fields;
+	}
+ 
+  unescapeYaml(value) {
+	  if (!value && value !== 0) return '';
+
+	  // If it's not a string, just return it as-is
+	  if (typeof value !== 'string') return value;
+	  
+	  // Remove surrounding quotes
+	  value = value.replace(/^["'](.*)["']$/, '$1');
+	  
+	  // Unescape escaped quotes
+	  value = value.replace(/\\"/g, '"');
+	  
+	  return value;
+  }
+ 
+  showToast(message) {
+    const toast = document.createElement('div');
+    toast.className = 'toast';
+    toast.textContent = message;
+    toast.style.cssText = `
+      position: fixed;
+      bottom: 20px;
+      right: 20px;
+      padding: 12px 16px;
+      background: ${this.theme === 'dark' ? '#e0e0e0' : '#1a1a1a'};
+      color: ${this.theme === 'dark' ? '#1a1a1a' : '#e0e0e0'};
+      border-radius: 4px;
+      font-size: 150%;
+      font-weight: 500;
+      z-index: 1000;
+      animation: slideIn 0.3s ease-out;
+    `;
+    document.body.appendChild(toast);
+    setTimeout(() => toast.remove(), 2000);
+  }
+ 
+  // Public API methods
+  getYaml() {
+    return this.generateYaml();
+  }
+ 
+  getFields() {
+    return JSON.parse(JSON.stringify(this.fields));
+  }
+ 
+  setFields(fields) {
+    this.fields = JSON.parse(JSON.stringify(fields));
+    this.selectedFieldId = this.fields[0]?.id || null;
+    this.updateCanvasView();
+    this.updateConfigPanel();
+    this.updateYamlOutput();
+	this.updateAuthorBioState(); // Check immediately on render
+  }
+ 
+  clear() {
+    this.fields = [];
+    this.selectedFieldId = null;
+	this.draggedFieldId = null;
+    this.draggedFromIndex = null;
+	this.nextFieldId = 1;
+    this.render();
+    this.setupEventListeners();
+    this.updateCanvasView();
+	this.updateConfigPanel();
+    this.updateYamlOutput();
+	this.updateAuthorBioState(); // Check immediately on render
+  }
+}
+ 
+// Register the custom element
+customElements.define('trmnl-form-builder', TRMLYamlForm);
+ 
+ 
+ 
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/node_modules/.bin/vitest b/node_modules/.bin/vitest index 6449d0e..af2f22f 100755 --- a/node_modules/.bin/vitest +++ b/node_modules/.bin/vitest @@ -11,7 +11,7 @@ else export NODE_PATH="/home/ucffool/trmnl-form-builder/trmnl-form-builder/node_modules/.pnpm/vitest@1.6.1_@vitest+ui@1.6.1_jsdom@23.2.0/node_modules/vitest/node_modules:/home/ucffool/trmnl-form-builder/trmnl-form-builder/node_modules/.pnpm/vitest@1.6.1_@vitest+ui@1.6.1_jsdom@23.2.0/node_modules:/home/ucffool/trmnl-form-builder/trmnl-form-builder/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then - exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@" + exec "$basedir/node" "$basedir/../.pnpm/vitest@1.6.1_@vitest+ui@1.6.1_jsdom@23.2.0/node_modules/vitest/vitest.mjs" "$@" else - exec node "$basedir/../vitest/vitest.mjs" "$@" + exec node "$basedir/../.pnpm/vitest@1.6.1_@vitest+ui@1.6.1_jsdom@23.2.0/node_modules/vitest/vitest.mjs" "$@" fi diff --git a/node_modules/.modules.yaml b/node_modules/.modules.yaml index 9810243..244ff0c 100644 --- a/node_modules/.modules.yaml +++ b/node_modules/.modules.yaml @@ -469,8 +469,6 @@ hoistedDependencies: xmlchars: private yocto-queue@1.2.2: yocto-queue: private -ignoredBuilds: - - esbuild included: dependencies: true devDependencies: true diff --git a/node_modules/.pnpm-workspace-state.json b/node_modules/.pnpm-workspace-state.json index ac5b804..b109ae2 100644 --- a/node_modules/.pnpm-workspace-state.json +++ b/node_modules/.pnpm-workspace-state.json @@ -1,5 +1,5 @@ { - "lastValidatedTimestamp": 1765429046997, + "lastValidatedTimestamp": 1765429531292, "projects": {}, "pnpmfileExists": false, "settings": { diff --git a/node_modules/.pnpm/lock.yaml b/node_modules/.pnpm/lock.yaml index b39158e..4bc6b92 100644 --- a/node_modules/.pnpm/lock.yaml +++ b/node_modules/.pnpm/lock.yaml @@ -9,7 +9,7 @@ importers: .: devDependencies: '@vitest/coverage-v8': - specifier: ^1.0.0 + specifier: ^1.6.1 version: 1.6.1(vitest@1.6.1) '@vitest/ui': specifier: ^1.0.0 diff --git a/node_modules/.vite/vitest/results.json b/node_modules/.vite/vitest/results.json index a74ff50..8219f2c 100644 --- a/node_modules/.vite/vitest/results.json +++ b/node_modules/.vite/vitest/results.json @@ -1 +1 @@ -{"version":"1.6.1","results":[[":trmnl-form-builder.test.js",{"duration":3,"failed":false}]]} \ No newline at end of file +{"version":"1.6.1","results":[[":trmnl-form-builder.test.js",{"duration":4,"failed":false}]]} \ No newline at end of file diff --git a/package.json b/package.json index 6274f6a..8bec5af 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "test:coverage": "vitest run --coverage" }, "devDependencies": { - "vitest": "^1.0.0", + "@vitest/coverage-v8": "^1.6.1", "@vitest/ui": "^1.0.0", - "@vitest/coverage-v8": "^1.0.0", - "jsdom": "^23.0.0" + "jsdom": "^23.0.0", + "vitest": "^1.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b39158e..4bc6b92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: .: devDependencies: '@vitest/coverage-v8': - specifier: ^1.0.0 + specifier: ^1.6.1 version: 1.6.1(vitest@1.6.1) '@vitest/ui': specifier: ^1.0.0