diff --git a/src/assets/css/comments.css b/src/assets/css/comments.css
new file mode 100644
index 00000000..69a40176
--- /dev/null
+++ b/src/assets/css/comments.css
@@ -0,0 +1,254 @@
+/**
+* Author: Vera Konigin
+* Site: https://groundedwren.neocities.org
+* Contact: vera@groundedwren.com
+*
+* File Description: Styles for the guestbook control
+* CSS variables come from https://groundedwren.neocities.org/styles/gwBoilerPlatePersonalization.css
+*/
+:root {
+ --icon-color: var(--clr-body-txt);
+}
+
+gw-comment-form, gw-comment-list {
+ width: 100%;
+}
+
+.comments-region {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.8rem;
+}
+
+.comment-form input {
+ width: 135px;
+}
+
+.comments-container {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ max-width: 100%;
+ gap: 0.4em;
+}
+
+.comment-box-container {
+ margin-top: 0.4em;
+ margin-left: auto;
+ margin-right: auto;
+ width: fit-content;
+}
+
+.comment-form-title {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 0.4em;
+ font-size: 1.25em;
+}
+
+.comment-article {
+ border-left: 2px solid var(--border-color);
+ padding: 0.5em 0;
+ background-color: var(--content-color);
+ display: flex;
+ flex-direction: column;
+}
+
+.comment-article blockquote {
+ max-width: unset !important;
+ overflow-wrap: break-word;
+}
+
+.comment-article > button {
+ max-width: fit-content;
+ height: 30px;
+}
+
+.comment-header {
+ display: grid;
+ grid-template-columns: 0fr 1fr 1fr;
+ gap: 5px;
+ align-items: baseline;
+}
+
+.comment-article .comment-article {
+ margin-left: 0.9em;
+ margin-top: 0.9em;
+ padding-right: 0;
+}
+
+.comment-id, .comment-timestamp {
+ font-size: 0.85em;
+ font-weight: lighter;
+ font-style: italic;
+ word-break: keep-all;
+}
+
+.comment-timestamp {
+ text-align: end;
+}
+
+.commenter-name {
+ font-size: 1.1em;
+ font-weight: bold;
+}
+
+form {
+ border: 1px solid;
+ padding: 0.4em;
+ border-color: var(--general-border-color);
+ width: 100%;
+ background-color: var(--content-color);
+}
+
+form.transparent-form {
+ border: none;
+ padding: 0;
+ width: auto;
+ background-color: inherit;
+}
+
+.transparent-fieldset {
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+form > h1, form > h2, form > h3, form > h4, form > h5, form > h6 {
+ margin: 0;
+ text-align: center;
+}
+
+.transparent-fieldset > legend {
+ display: none !important;
+}
+
+.form-footer {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+}
+
+.form-footer > * {
+ margin: 3px;
+ min-height: 30px;
+}
+
+.input-line > label {
+ text-align: left;
+ width: 150px;
+ min-width: 100px;
+}
+
+.input-line * {
+ display: inline-block;
+}
+
+.input-line {
+ margin-bottom: 5px;
+}
+
+.input-flex {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+}
+
+.input-flex-line {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ justify-content: flex-start;
+ align-items: center;
+ margin-bottom: 5px;
+}
+
+.input-horizontal-flex {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ align-items: center;
+ row-gap: 4px;
+}
+
+.input-horizontal-flex > .input-flex-line, .input-horizontal-flex > .input-vertical-line {
+ width: auto;
+ margin-right: 10px;
+}
+
+.input-vertical {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.input-vertical-line {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 6px;
+}
+
+.input-vertical-line > label, .input-vertical > label {
+ padding-bottom: 2px;
+}
+
+.input-grid {
+ display: grid;
+ grid-template-columns: minmax(75px, 1fr) minmax(75px, auto);
+ grid-auto-rows: minmax(25px, auto);
+ grid-column-gap: 5px;
+ grid-row-gap: 2px;
+ align-items: baseline;
+}
+
+.input-grid > .full-line {
+ grid-column: 1 / -1;
+}
+
+.input-line label, .input-flex-line label, .input-vertical-line label, .input-grid label {
+ cursor: pointer;
+}
+
+.input-line > button, .input-flex-line > button, .input-vertical-line > button, .input-grid button {
+ height: 30px;
+}
+
+.input-flex-line > label {
+ text-align: left;
+ flex-grow: 1;
+ padding-right: 10px;
+}
+
+.sr-only {
+ position: absolute;
+ left: -99999999px;
+}
+
+.inline-banner-wrapper {
+ display: flex;
+}
+
+.inline-banner-wrapper > .inline-banner {
+ width: 100%;
+}
+
+.inline-banner {
+ margin: 10px;
+ border: 1px solid var(-border-color);
+ background-color: var(--banner-color);
+ color: var(--text-color);
+ padding: 10px;
+ word-break: break-word;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
\ No newline at end of file
diff --git a/src/assets/css/svgIconControl.css b/src/assets/css/svgIconControl.css
new file mode 100644
index 00000000..abc701c6
--- /dev/null
+++ b/src/assets/css/svgIconControl.css
@@ -0,0 +1,21 @@
+/**
+ * Author: Vera Konigin
+ * Site: https://groundedwren.neocities.org
+ * Contact: vera@groundedwren.com
+ *
+ * File Description: Styles for the SVG Icon web component
+ * CSS variables come from https://groundedwren.neocities.org/styles/gwBoilerPlatePersonalization.css
+ */
+
+ .icon {
+ fill: var(--icon-color);
+ width: 16px;
+ height: 16px;
+}
+
+gw-icon {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/src/assets/js/comments.js b/src/assets/js/comments.js
new file mode 100644
index 00000000..8bd07236
--- /dev/null
+++ b/src/assets/js/comments.js
@@ -0,0 +1,470 @@
+/**
+ * Author: Vera Konigin
+ * Site: https://groundedwren.neocities.org
+ * Contact: vera@groundedwren.com
+ *
+ * File Description: Comments Control
+ */
+
+registerNamespace("GW.Controls", function (ns)
+{
+ ns.CommentForm = class CommentForm extends HTMLElement
+ {
+ //#region staticProperties
+ static observedAttributes = [];
+ static instanceCount = 0;
+ static instanceMap = {};
+ //#endregion
+
+ //#region instance properties
+ instanceId;
+ isInitialized;
+ titleText;
+ discordURL;
+
+ //#region element properties
+ formEl;
+ titleEl;
+
+ dispNameInpt;
+ emailInpt;
+ websiteInpt;
+ respToInpt;
+ commentInpt;
+
+ resetBtn;
+ submitBtn;
+ //#endregion
+ //#endregion
+
+ constructor()
+ {
+ super();
+ this.instanceId = CommentForm.instanceCount++;
+ CommentForm.instanceMap[this.instanceId] = this;
+ }
+
+ get idKey()
+ {
+ return `gw-comment-form-${this.instanceId}`;
+ }
+
+ //#region HTMLElement implementation
+ connectedCallback()
+ {
+ if (this.isInitialized) { return; }
+
+ this.titleText = this.getAttribute("titleText") || "Add a Comment";
+ this.discordURL = this.getAttribute("discordURL")
+
+ this.renderContent();
+ this.registerHandlers();
+
+ this.isInitialized = true;
+ }
+ //#endregion
+
+ renderContent()
+ {
+ //Markup
+ this.innerHTML = `
+
+ `;
+
+ //element properties
+ this.formEl = document.getElementById(`${this.idKey}-form`);
+ this.titleEl = document.getElementById(`${this.idKey}-title`);
+
+ this.dispNameInpt = document.getElementById(`${this.idKey}-dispName`);
+ this.emailInpt = document.getElementById(`${this.idKey}-email`);
+ this.websiteInpt = document.getElementById(`${this.idKey}-website`);
+ this.respToInpt = document.getElementById(`${this.idKey}-respTo`);
+ this.commentInpt = document.getElementById(`${this.idKey}-comment`);
+
+ this.resetBtn = document.getElementById(`${this.idKey}-reset`);
+ this.submitBtn = document.getElementById(`${this.idKey}-submit`);
+
+ //default values
+ this.dispNameInpt.value = localStorage.getItem("comment-name") || "";
+ this.emailInpt.value = localStorage.getItem("comment-email") || "";
+ this.websiteInpt.value = localStorage.getItem("comment-website") || "";
+ }
+
+ //#region Handlers
+ registerHandlers()
+ {
+ this.formEl.onsubmit = this.onSubmit;
+ }
+
+ onSubmit = (event) =>
+ {
+ event.preventDefault();
+
+ const contentObj = {
+ name: this.dispNameInpt.value,
+ email: this.emailInpt.value,
+ website: this.websiteInpt.value,
+ responseTo: this.respToInpt.value,
+ comment: this.commentInpt.value,
+ timestamp: new Date().toUTCString(),
+ };
+ const contentAry = [];
+ for (let contentKey in contentObj)
+ {
+ contentAry.push(`${contentKey}=${contentObj[contentKey]}`);
+ }
+
+ const request = new XMLHttpRequest();
+ request.open(
+ "POST",
+ this.discordURL,
+ true
+ );
+ request.setRequestHeader("Content-Type", "application/json");
+
+ request.onreadystatechange = function ()
+ {
+ if (request.readyState == 4)
+ {
+ console.log(request.responseText);
+ }
+ };
+
+ request.send(JSON.stringify({ content: contentAry.join("; ") }));
+
+ localStorage.setItem("comment-name", contentObj.name);
+ localStorage.setItem("comment-email", contentObj.email);
+ localStorage.setItem("comment-website", contentObj.website);
+
+ this.formEl.reset();
+ this.dispNameInpt.value = contentObj.name;
+ this.emailInpt.value = contentObj.email;
+ this.websiteInpt.value = contentObj.website;
+
+ alert("Your message has been submitted!");
+ };
+ //#endregion
+ };
+ customElements.define("gw-comment-form", ns.CommentForm);
+
+ ns.CommentList = class CommentList extends HTMLElement
+ {
+ //#region staticProperties
+ static observedAttributes = [];
+ static instanceCount = 0;
+ static instanceMap = {};
+ //#endregion
+
+ //#region instance properties
+ instanceId;
+ isInitialized;
+ gSpreadsheetId;
+ gSheetId;
+ isNewestFirst;
+ gwCommentFormId;
+
+ //#region element properties
+ //#endregion
+ //#endregion
+
+ constructor()
+ {
+ super();
+ this.instanceId = CommentList.instanceCount++;
+ CommentList.instanceMap[this.instanceId] = this;
+ }
+
+ get idKey()
+ {
+ return `gw-comment-list-${this.instanceId}`;
+ }
+
+ //#region HTMLElement implementation
+ connectedCallback()
+ {
+ if (this.isInitialized) { return; }
+
+ this.gSpreadsheetId = this.getAttribute("gSpreadsheetId");
+ this.gSheetId = this.getAttribute("gSheetId");
+ this.isNewestFirst = this.getAttribute("isNewestFirst");
+ this.gwCommentFormId = this.getAttribute("gwCommentFormId");
+
+ this.loadAndRender();
+
+ this.isInitialized = true;
+ }
+ //#endregion
+
+ async loadAndRender()
+ {
+ this.innerHTML = `
+
+
+ Comments loading....
+
+ `
+
+ const sheetReader = new GW.Gizmos.GoogleSheetsReader(this.gSpreadsheetId, this.gSheetId);
+ const sheetData = await sheetReader.loadData();
+ this.innerHTML = "";
+
+ const allComments = sheetReader.rowData;
+ if (this.isNewestFirst)
+ {
+ allComments.reverse();
+ }
+
+ this.renderContent();
+ this.registerHandlers();
+
+ const allCommentsIndex = {};
+ const topLevelCommentIdxs = [];
+ const childCommentIdxs = [];
+ for (let i = 0; i < allComments.length; i++)
+ {
+ const comment = allComments[i];
+ allCommentsIndex[comment.ID] = i;
+ if (!comment.ResponseTo)
+ {
+ topLevelCommentIdxs.push(i);
+ }
+ else
+ {
+ childCommentIdxs.push(i);
+ }
+ }
+ childCommentIdxs.forEach(childIdx =>
+ {
+ const replyId = allComments[childIdx].ResponseTo;
+ const respondeeComment = allComments[allCommentsIndex[replyId]];
+
+ respondeeComment.childrenIdxs = respondeeComment.childrenIdxs || [];
+ respondeeComment.childrenIdxs.push(childIdx);
+ });
+
+ let commentsToBuild = [];
+ topLevelCommentIdxs.forEach(
+ topCommentIdx => commentsToBuild.push(
+ {
+ parent: this.containerEl,
+ parentId: null,
+ comment: allComments[topCommentIdx]
+ }
+ )
+ );
+
+ while (commentsToBuild.length > 0)
+ {
+ let { parent, parentId, comment } = commentsToBuild.shift();
+ if (!comment.Timestamp)
+ {
+ continue;
+ }
+ parent.insertAdjacentHTML("beforeend", `
+
+ `);
+
+ const commentEl = document.getElementById(`${this.idKey}-cmt-${comment.ID}`);
+ (comment.childrenIdxs || []).forEach(
+ childIdx => commentsToBuild.push({
+ parent: commentEl.articleEl,
+ parentId: comment.ID,
+ comment: allComments[childIdx]
+ })
+ );
+ }
+ }
+
+ renderContent()
+ {
+ //Markup
+ this.innerHTML = `
+
+ `;
+
+ //element properties
+ this.containerEl = document.getElementById(`${this.idKey}-container`);
+ }
+
+ //#region Handlers
+ registerHandlers()
+ {
+ }
+ //#endregion
+ };
+ customElements.define("gw-comment-list", ns.CommentList);
+
+ ns.CommentCard = class CommentCard extends HTMLElement
+ {
+ //#region staticProperties
+ static observedAttributes = [];
+ static instanceCount = 0;
+ static instanceMap = {};
+ //#endregion
+
+ //#region instance properties
+ instanceId;
+ isInitialized;
+ commentId;
+ replyToId;
+ commenterName;
+ isoTimestamp;
+ datetime;
+ websiteURL;
+ commentText;
+ gwCommentFormId;
+
+ //#region element properties
+ articleEl;
+ replyBtn;
+ //#endregion
+ //#endregion
+
+ constructor()
+ {
+ super();
+ this.instanceId = CommentCard.instanceCount++;
+ CommentCard.instanceMap[this.instanceId] = this;
+ }
+
+ get idKey()
+ {
+ return `gw-comment-card-${this.instanceId}`;
+ }
+
+ //#region HTMLElement implementation
+ connectedCallback()
+ {
+ if (this.isInitialized) { return; }
+
+ this.commentId = this.getAttribute("commentId");
+ this.replyToId = this.getAttribute("replyToId");
+ this.commenterName = this.getAttribute("commenterName");
+ this.isoTimestamp = this.getAttribute("isoTimestamp");
+ this.datetime = new Date(this.isoTimestamp);
+ this.websiteURL = this.getAttribute("websiteURL");
+ this.commentText = this.getAttribute("commentText");
+ this.gwCommentFormId = this.getAttribute("gwCommentFormId");
+
+ this.renderContent();
+ this.registerHandlers();
+
+ this.isInitialized = true;
+ }
+ //#endregion
+
+ renderContent()
+ {
+ const headerText = this.replyToId
+ ? `Comment #${this.commentId} replying to #${this.replyToId}`
+ : `Top level comment #${this.commentId}`;
+
+ const displayTimestamp = this.datetime.toLocaleString(
+ undefined,
+ { dateStyle: "short", timeStyle: "short" }
+ );
+
+ const commenterNameEl = this.websiteURL
+ ? ``
+ : ``;
+
+ //Markup
+ this.innerHTML = `
+
+
+ ${this.commentText}
+
+
+ `;
+
+ //element properties
+ this.articleEl = document.getElementById(`${this.idKey}-article`);
+ this.replyBtn = document.getElementById(`${this.idKey}-reply`);
+ }
+
+ //#region Handlers
+ registerHandlers()
+ {
+ this.replyBtn.onclick = this.onReply;
+ }
+
+ onReply = () =>
+ {
+ const gwCommentForm = document.getElementById(this.gwCommentFormId);
+ const respToInpt = gwCommentForm.respToInpt;
+ if (!respToInpt)
+ {
+ alert("Comment form not found");
+ return;
+ }
+
+ respToInpt.value = this.commentId;
+ respToInpt.focus();
+ };
+ //#endregion
+ };
+ customElements.define("gw-comment-card", ns.CommentCard);
+});
\ No newline at end of file
diff --git a/src/assets/js/googleSheetsReaderGizmo.js b/src/assets/js/googleSheetsReaderGizmo.js
new file mode 100644
index 00000000..318aac18
--- /dev/null
+++ b/src/assets/js/googleSheetsReaderGizmo.js
@@ -0,0 +1,179 @@
+/**
+ * Author: Vera Konigin
+ * Site: https://groundedwren.neocities.org
+ * Contact: vera@groundedwren.com
+ *
+ * File Description: Gizmo for reading from Google Sheets.
+ * Neocities editor users will see a lot of linter errors in this file, but none of them are real errors. The linter just doesn't understand some modern JS.
+ */
+
+ /**
+ * By default, any JavaScript code written is defined in the global namespace, which means it's accessible directly under the "window" element.
+ * If you have a lot of scripts, this can lead to clutter and naming collisions (what if two different scripts use a variable called "i"? They can inadvertently mess each other up).
+ * To get around this, we define the registerNamespace function in the global namespace, which just confines all the code in the function passed to it to a property under window.
+ * That property is represented as the "path" parameter. It is passed to the function for ease of access.
+ */
+function registerNamespace(path, nsFunc)
+{
+ var ancestors = path.split(".");
+
+ var ns = window;
+ for(var i = 0; i < ancestors.length; i++)
+ {
+ ns[ancestors[i]] = ns[ancestors[i]] || {};
+ ns = ns[ancestors[i]];
+ }
+ nsFunc(ns);
+}
+
+registerNamespace("GW.Gizmos", function(ns) {
+ /**
+ * A class to read from one page in a google sheet document
+ */
+ ns.GoogleSheetsReader = class GoogleSheetsReader
+ {
+ //setResponse is intended for React - it's how google responds to our GET. Fortunately valid JSON is inside this call, so we can just parse it out.
+ static #RESPONSE_PREFIX = "setResponse(";
+ static #RESPONSE_SUFFIX = ");";
+
+ //Here we can define any custom types based on column label. "Timestamp" is intended for ISO 8601 format date/time strings.
+ static #CUSTOM_LABEL_TYPES = {
+ "Timestamp": "timestamp"
+ };
+
+ spreadsheetId; //The ID of the spreadsheet. This is the part just after /d/ in the docs.google.com URL
+ sheetName; //The name of the particular sheet we're after
+
+ #sheetURL; //Composed request URL
+
+ loadPromise = null; //A promise created when loading begins and which resolves when data has finished loading.
+ tableJSON = null; //Raw JS Object version of the returned JSON.
+ rowData = null; //Parsed row data
+ colData = null; //A shortcut to the the column data in tableJSON
+ colIndex = null; //An index from column label to its metadata, plus array position
+
+ /**
+ * Constructs a GoogleSheetsReader object
+ * spreadsheetId is the part of the docs.google.com URL just after /d/
+ * sheetName is the name of the particular page
+ */
+ constructor(spreadsheetId, sheetName)
+ {
+ this.spreadsheetId = spreadsheetId;
+ this.sheetName = sheetName;
+ this.#sheetURL = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?sheet=${sheetName}`;
+ }
+
+ /**
+ * Loads and parses sheet data via HTTP GET
+ * Returns null on success, and an error string on failure.
+ */
+ async loadData()
+ {
+ this.loadPromise = this.#loadData();
+ return this.loadPromise;
+ }
+
+ async #loadData()
+ {
+ this.tableJSON = null;
+ this.rowData = null;
+ this.colData = null;
+ this.colIndex = null;
+
+ const response = await fetch(this.#sheetURL);
+ if (response.ok)
+ {
+ return response.text().then((unparsedData) =>
+ {
+ //This is parsing out the valid JSON from the React method they gave us
+ const targetData = unparsedData.split(
+ GoogleSheetsReader.#RESPONSE_PREFIX
+ )[1].split(
+ GoogleSheetsReader.#RESPONSE_SUFFIX
+ )[0];
+
+ this.tableJSON = GoogleSheetsReader.#applyCustomLabelTypes(JSON.parse(targetData).table);
+ this.rowData = GoogleSheetsReader.#parseAllRows(this.tableJSON);
+ this.colIndex = GoogleSheetsReader.#indexColumns(this.tableJSON);
+ this.colData = this.tableJSON.cols;
+
+ return null;
+ });
+ }
+ else
+ {
+ return response.statusText || response.status;
+ }
+ }
+
+ /**
+ * Overrides any google-returned column data types with custom ones based on label
+ */
+ static #applyCustomLabelTypes(tableJSON)
+ {
+ tableJSON.cols.forEach(col => {
+ if(this.#CUSTOM_LABEL_TYPES[col.label])
+ {
+ col.type = this.#CUSTOM_LABEL_TYPES[col.label]
+ };
+ });
+ return tableJSON;
+ }
+
+ static #parseAllRows(tableJSON)
+ {
+ const rowDataArray = [];
+ for(let i = 0; i < tableJSON.rows.length; i++)
+ {
+ rowDataArray.push(this.#parseRow(i, tableJSON));
+ }
+ return rowDataArray;
+ }
+
+ static #parseRow(rowIdx, tableJSON)
+ {
+ const rowData = {};
+ const cells = tableJSON.rows[rowIdx].c;
+
+ for(let i = 0; i < cells.length; i++)
+ {
+ rowData[tableJSON.cols[i].label] = this.#parseCellType(cells[i], tableJSON.cols[i].type);
+ }
+ return rowData;
+ }
+
+ /**
+ * Parses a cell based on its type. Further custom types will need their own parsing added here.
+ */
+ static #parseCellType(cellData, cellType)
+ {
+ switch (cellType)
+ {
+ case "string":
+ return cellData ? cellData.v : cellData;
+ case "number":
+ return cellData ? cellData.v : cellData;
+ case "datetime":
+ case "date":
+ return (cellData && cellData.v) ? eval("new " + cellData.v) : null;
+ case "timestamp":
+ const cellTimestamp = new Date(cellData ? cellData.v : "");
+ return isNaN(cellTimestamp) ? null : cellTimestamp;
+ default:
+ return cellData;
+ }
+ }
+
+ static #indexColumns(tableJSON)
+ {
+ const colIndex = {};
+ for(let i = 0; i < tableJSON.cols.length; i++)
+ {
+ const colData = tableJSON.cols[i];
+ colIndex[colData.label] = {...colData, index: i};
+ }
+ return colIndex;
+ }
+ };
+});
\ No newline at end of file
diff --git a/src/assets/js/svgIconControl.js b/src/assets/js/svgIconControl.js
new file mode 100644
index 00000000..546b3897
--- /dev/null
+++ b/src/assets/js/svgIconControl.js
@@ -0,0 +1,432 @@
+/**
+ * Author: Vera Konigin
+ * Site: https://groundedwren.neocities.org
+ * Contact: vera@groundedwren.com
+ *
+ * File Description: A web component to site SVG icons easily
+ */
+
+/**
+ * By default, any JavaScript code written is defined in the global namespace, which means it's accessible directly under the "window" element.
+ * If you have a lot of scripts, this can lead to clutter and naming collisions (what if two different scripts use a variable called "i"? They can inadvertently mess each other up).
+ * To get around this, we define the registerNamespace function in the global namespace, which just confines all the code in the function passed to it to a property under window.
+ * That property is represented as the "path" parameter. It is passed to the function for ease of access.
+ */
+function registerNamespace(path, nsFunc)
+{
+ var ancestors = path.split(".");
+
+ var ns = window;
+ for(var i = 0; i < ancestors.length; i++)
+ {
+ ns[ancestors[i]] = ns[ancestors[i]] || {};
+ ns = ns[ancestors[i]];
+ }
+ nsFunc(ns);
+}
+
+registerNamespace("GW.Controls.SVGLib", function(ns) {
+ const XML_NAMESPACE = "http://www.w3.org/2000/svg";
+
+ /**
+ * https://www.w3.org/TR/SVG/eltindex.html
+ */
+ ns.ElementTypes = {
+ svg: "svg", //viewBox, preserveAspectRatio, zoomAndPan, transform - https://www.w3.org/TR/SVG/struct.html#SVGElement
+ circle: "circle", //cx, cy, r - https://www.w3.org/TR/SVG/shapes.html#CircleElement
+ linearGradient: "linearGradient", //x1, y1, x2, y2, gradientUnits, gradientTransform, spreadMethod, href - https://www.w3.org/TR/SVG/pservers.html#LinearGradientElement
+ stop: "stop", //offset, stop-color https://www.w3.org/TR/SVG/pservers.html#StopElement
+ defs: "defs", //https://www.w3.org/TR/SVG/struct.html#DefsElement
+ rect: "rect", //x, y, width, height, rx, ry - https://www.w3.org/TR/SVG/shapes.html#RectElement
+ text: "text", //x, y, dominant-baseline, text-anchor, fill - https://www.w3.org/TR/SVG/text.html#TextElement
+ a: "a", //href, target - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/a
+ path: "path", //d, pathLength - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
+ title: "title", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
+ desc: "desc", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
+ foreignObject: "foreignObject", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
+ };
+
+ /**
+ * Shortcut to create an SVG element and append it to a parent
+ * @param parent the parent element
+ * ... see ns.createElement
+ */
+ function createChildElement(parent, elementType, attributes, innerHTML)
+ {
+ var childEl = createElement(elementType, attributes, innerHTML);
+ parent.appendChild(childEl);
+ return childEl;
+ }
+ ns.createChildElement = createChildElement;
+
+ /**
+ * Creates an SVG element to spec
+ * @param elementType type of SVG element (see ns.ElementTypes)
+ * @param attributes object of attributes and values
+ * @param innerHTML inner HTML
+ */
+ function createElement(elementType, attributes, innerHTML)
+ {
+ var el = document.createElementNS(XML_NAMESPACE, elementType);
+ Object.keys(attributes || {}).forEach((attr) => el.setAttributeNS(null, attr, attributes[attr]));
+ el.innerHTML = innerHTML || null;
+ return el;
+ }
+ ns.createElement = createElement;
+
+ //#region Icons
+ const ICON_CITATION = "";
+ const ICON_CLASS = "icon";
+
+ /**
+ * Icon SVG path definitions
+ */
+ ns.Icons = {
+ "circle-info": {
+ title: "info",
+ viewBox: "0 0 512 512",
+ d: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
+ },
+ "play": {
+ title: "play",
+ viewBox: "0 0 384 512",
+ d: "M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"
+ },
+ "backward-step": {
+ title: "backward step",
+ viewBox: "0 0 320 512",
+ d: "M267.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29V96c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160L64 241V96c0-17.7-14.3-32-32-32S0 78.3 0 96V416c0 17.7 14.3 32 32 32s32-14.3 32-32V271l11.5 9.6 192 160z"
+ },
+ "forward-step": {
+ title: "forward step",
+ viewBox: "0 0 320 512",
+ d: "M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z"
+ },
+ "pause": {
+ title: "pause",
+ viewBox: "0 0 320 512",
+ d: "M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"
+ },
+ "triangle-exclamation": {
+ title: "warning",
+ viewBox: "0 0 512 512",
+ d: "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
+ },
+ "laptop-code": {
+ title: "coding",
+ viewBox: "0 0 640 512",
+ d: "M64 96c0-35.3 28.7-64 64-64H512c35.3 0 64 28.7 64 64V352H512V96H128V352H64V96zM0 403.2C0 392.6 8.6 384 19.2 384H620.8c10.6 0 19.2 8.6 19.2 19.2c0 42.4-34.4 76.8-76.8 76.8H76.8C34.4 480 0 445.6 0 403.2zM281 209l-31 31 31 31c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-48-48c-9.4-9.4-9.4-24.6 0-33.9l48-48c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9zM393 175l48 48c9.4 9.4 9.4 24.6 0 33.9l-48 48c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l31-31-31-31c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0z"
+ },
+ "guitar": {
+ title: "guitar",
+ viewBox: "0 0 512 512",
+ d: "M465 7c-9.4-9.4-24.6-9.4-33.9 0L383 55c-2.4 2.4-4.3 5.3-5.5 8.5l-15.4 41-77.5 77.6c-45.1-29.4-99.3-30.2-131 1.6c-11 11-18 24.6-21.4 39.6c-3.7 16.6-19.1 30.7-36.1 31.6c-25.6 1.3-49.3 10.7-67.3 28.6C-16 328.4-7.6 409.4 47.5 464.5s136.1 63.5 180.9 18.7c17.9-17.9 27.4-41.7 28.6-67.3c.9-17 15-32.3 31.6-36.1c15-3.4 28.6-10.5 39.6-21.4c31.8-31.8 31-85.9 1.6-131l77.6-77.6 41-15.4c3.2-1.2 6.1-3.1 8.5-5.5l48-48c9.4-9.4 9.4-24.6 0-33.9L465 7zM208 256a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"
+ },
+ "pen-fancy": {
+ title: "pen",
+ viewBox: "0 0 512 512",
+ d: "M373.5 27.1C388.5 9.9 410.2 0 433 0c43.6 0 79 35.4 79 79c0 22.8-9.9 44.6-27.1 59.6L277.7 319l-10.3-10.3-64-64L193 234.3 373.5 27.1zM170.3 256.9l10.4 10.4 64 64 10.4 10.4-19.2 83.4c-3.9 17.1-16.9 30.7-33.8 35.4L24.4 510.3l95.4-95.4c2.6 .7 5.4 1.1 8.3 1.1c17.7 0 32-14.3 32-32s-14.3-32-32-32s-32 14.3-32 32c0 2.9 .4 5.6 1.1 8.3L1.7 487.6 51.5 310c4.7-16.9 18.3-29.9 35.4-33.8l83.4-19.2z"
+ },
+ "glasses": {
+ title: "glasses",
+ viewBox: "0 0 576 512",
+ d: "M118.6 80c-11.5 0-21.4 7.9-24 19.1L57 260.3c20.5-6.2 48.3-12.3 78.7-12.3c32.3 0 61.8 6.9 82.8 13.5c10.6 3.3 19.3 6.7 25.4 9.2c3.1 1.3 5.5 2.4 7.3 3.2c.9 .4 1.6 .7 2.1 1l.6 .3 .2 .1 .1 0 0 0 0 0s0 0-6.3 12.7h0l6.3-12.7c5.8 2.9 10.4 7.3 13.5 12.7h40.6c3.1-5.3 7.7-9.8 13.5-12.7l6.3 12.7h0c-6.3-12.7-6.3-12.7-6.3-12.7l0 0 0 0 .1 0 .2-.1 .6-.3c.5-.2 1.2-.6 2.1-1c1.8-.8 4.2-1.9 7.3-3.2c6.1-2.6 14.8-5.9 25.4-9.2c21-6.6 50.4-13.5 82.8-13.5c30.4 0 58.2 6.1 78.7 12.3L481.4 99.1c-2.6-11.2-12.6-19.1-24-19.1c-3.1 0-6.2 .6-9.2 1.8L416.9 94.3c-12.3 4.9-26.3-1.1-31.2-13.4s1.1-26.3 13.4-31.2l31.3-12.5c8.6-3.4 17.7-5.2 27-5.2c33.8 0 63.1 23.3 70.8 56.2l43.9 188c1.7 7.3 2.9 14.7 3.5 22.1c.3 1.9 .5 3.8 .5 5.7v6.7V352v16c0 61.9-50.1 112-112 112H419.7c-59.4 0-108.5-46.4-111.8-105.8L306.6 352H269.4l-1.2 22.2C264.9 433.6 215.8 480 156.3 480H112C50.1 480 0 429.9 0 368V352 310.7 304c0-1.9 .2-3.8 .5-5.7c.6-7.4 1.8-14.8 3.5-22.1l43.9-188C55.5 55.3 84.8 32 118.6 32c9.2 0 18.4 1.8 27 5.2l31.3 12.5c12.3 4.9 18.3 18.9 13.4 31.2s-18.9 18.3-31.2 13.4L127.8 81.8c-2.9-1.2-6-1.8-9.2-1.8zM64 325.4V368c0 26.5 21.5 48 48 48h44.3c25.5 0 46.5-19.9 47.9-45.3l2.5-45.6c-2.3-.8-4.9-1.7-7.5-2.5c-17.2-5.4-39.9-10.5-63.6-10.5c-23.7 0-46.2 5.1-63.2 10.5c-3.1 1-5.9 1.9-8.5 2.9zM512 368V325.4c-2.6-.9-5.5-1.9-8.5-2.9c-17-5.4-39.5-10.5-63.2-10.5c-23.7 0-46.4 5.1-63.6 10.5c-2.7 .8-5.2 1.7-7.5 2.5l2.5 45.6c1.4 25.4 22.5 45.3 47.9 45.3H464c26.5 0 48-21.5 48-48z"
+ },
+ "clock": {
+ title: "glasses",
+ viewBox: "0 0 512 512",
+ d: "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"
+ },
+ "envelope": {
+ title: "envelope",
+ viewBox: "0 0 512 512",
+ d: "M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"
+ },
+ "discord": {
+ title: "discord",
+ viewBox: "0 0 640 512",
+ d: "M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"
+ },
+ "plus": {
+ title: "plus",
+ viewBox: "0 0 448 512",
+ d: "M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"
+ },
+ "circle-check": {
+ title: "done",
+ viewBox: "0 0 512 512",
+ d: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
+ },
+ "delete-left": {
+ title: "delete",
+ viewBox: "0 0 576 512",
+ d: "M576 128c0-35.3-28.7-64-64-64H205.3c-17 0-33.3 6.7-45.3 18.7L9.4 233.4c-6 6-9.4 14.1-9.4 22.6s3.4 16.6 9.4 22.6L160 429.3c12 12 28.3 18.7 45.3 18.7H512c35.3 0 64-28.7 64-64V128zM271 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"
+ },
+ "link": {
+ title: "link",
+ viewBox: "0 0 640 512",
+ d: "M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"
+ },
+ "xmark": {
+ title: "close",
+ viewBox: "0 0 384 512",
+ d: "M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
+ },
+ "trash": {
+ title: "trash",
+ viewBox: "0 0 448 512",
+ d: "M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"
+ },
+ "thumbtack": {
+ title: "trash",
+ viewBox: "0 0 384 512",
+ d: "M32 32C32 14.3 46.3 0 64 0H320c17.7 0 32 14.3 32 32s-14.3 32-32 32H290.5l11.4 148.2c36.7 19.9 65.7 53.2 79.5 94.7l1 3c3.3 9.8 1.6 20.5-4.4 28.8s-15.7 13.3-26 13.3H32c-10.3 0-19.9-4.9-26-13.3s-7.7-19.1-4.4-28.8l1-3c13.8-41.5 42.8-74.8 79.5-94.7L93.5 64H64C46.3 64 32 49.7 32 32zM160 384h64v96c0 17.7-14.3 32-32 32s-32-14.3-32-32V384z"
+ },
+ "pen-to-square": {
+ title: "edit",
+ viewBox: "0 0 512 512",
+ d: "M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"
+ },
+ "book": {
+ title: "book",
+ viewBox: "0 0 448 512",
+ d: "M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z"
+ },
+ "gamepad": {
+ title: "game",
+ viewBox: "0 0 640 512",
+ d: "M192 64C86 64 0 150 0 256S86 448 192 448H448c106 0 192-86 192-192s-86-192-192-192H192zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24v32h32c13.3 0 24 10.7 24 24s-10.7 24-24 24H216v32c0 13.3-10.7 24-24 24s-24-10.7-24-24V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h32V200z"
+ },
+ "hammer": {
+ title: "game",
+ viewBox: "0 0 576 512",
+ d: "M413.5 237.5c-28.2 4.8-58.2-3.6-80-25.4l-38.1-38.1C280.4 159 272 138.8 272 117.6V105.5L192.3 62c-5.3-2.9-8.6-8.6-8.3-14.7s3.9-11.5 9.5-14l47.2-21C259.1 4.2 279 0 299.2 0h18.1c36.7 0 72 14 98.7 39.1l44.6 42c24.2 22.8 33.2 55.7 26.6 86L503 183l8-8c9.4-9.4 24.6-9.4 33.9 0l24 24c9.4 9.4 9.4 24.6 0 33.9l-88 88c-9.4 9.4-24.6 9.4-33.9 0l-24-24c-9.4-9.4-9.4-24.6 0-33.9l8-8-17.5-17.5zM27.4 377.1L260.9 182.6c3.5 4.9 7.5 9.6 11.8 14l38.1 38.1c6 6 12.4 11.2 19.2 15.7L134.9 484.6c-14.5 17.4-36 27.4-58.6 27.4C34.1 512 0 477.8 0 435.7c0-22.6 10.1-44.1 27.4-58.6z"
+ },
+ "location-dot": {
+ title: "location",
+ viewBox: "0 0 384 512",
+ d: "M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"
+ },
+ "box-open": {
+ title: "box",
+ viewBox: "0 0 640 512",
+ d: "M58.9 42.1c3-6.1 9.6-9.6 16.3-8.7L320 64 564.8 33.4c6.7-.8 13.3 2.7 16.3 8.7l41.7 83.4c9 17.9-.6 39.6-19.8 45.1L439.6 217.3c-13.9 4-28.8-1.9-36.2-14.3L320 64 236.6 203c-7.4 12.4-22.3 18.3-36.2 14.3L37.1 170.6c-19.3-5.5-28.8-27.2-19.8-45.1L58.9 42.1zM321.1 128l54.9 91.4c14.9 24.8 44.6 36.6 72.5 28.6L576 211.6v167c0 22-15 41.2-36.4 46.6l-204.1 51c-10.2 2.6-20.9 2.6-31 0l-204.1-51C79 419.7 64 400.5 64 378.5v-167L191.6 248c27.8 8 57.6-3.8 72.5-28.6L318.9 128h2.2z"
+ },
+ "calendar": {
+ title: "box",
+ viewBox: "0 0 448 512",
+ d: "M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z"
+ },
+ "people-group": {
+ title: "people",
+ viewBox: "0 0 640 512",
+ d: "M72 88a56 56 0 1 1 112 0A56 56 0 1 1 72 88zM64 245.7C54 256.9 48 271.8 48 288s6 31.1 16 42.3V245.7zm144.4-49.3C178.7 222.7 160 261.2 160 304c0 34.3 12 65.8 32 90.5V416c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V389.2C26.2 371.2 0 332.7 0 288c0-61.9 50.1-112 112-112h32c24 0 46.2 7.5 64.4 20.3zM448 416V394.5c20-24.7 32-56.2 32-90.5c0-42.8-18.7-81.3-48.4-107.7C449.8 183.5 472 176 496 176h32c61.9 0 112 50.1 112 112c0 44.7-26.2 83.2-64 101.2V416c0 17.7-14.3 32-32 32H480c-17.7 0-32-14.3-32-32zm8-328a56 56 0 1 1 112 0A56 56 0 1 1 456 88zM576 245.7v84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM320 32a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM240 304c0 16.2 6 31 16 42.3V261.7c-10 11.3-16 26.1-16 42.3zm144-42.3v84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM448 304c0 44.7-26.2 83.2-64 101.2V448c0 17.7-14.3 32-32 32H288c-17.7 0-32-14.3-32-32V405.2c-37.8-18-64-56.5-64-101.2c0-61.9 50.1-112 112-112h32c61.9 0 112 50.1 112 112z"
+ },
+ "comments": {
+ title: "comments",
+ viewBox: "0 0 640 512",
+ d: "M208 352c114.9 0 208-78.8 208-176S322.9 0 208 0S0 78.8 0 176c0 38.6 14.7 74.3 39.6 103.4c-3.5 9.4-8.7 17.7-14.2 24.7c-4.8 6.2-9.7 11-13.3 14.3c-1.8 1.6-3.3 2.9-4.3 3.7c-.5 .4-.9 .7-1.1 .8l-.2 .2 0 0 0 0C1 327.2-1.4 334.4 .8 340.9S9.1 352 16 352c21.8 0 43.8-5.6 62.1-12.5c9.2-3.5 17.8-7.4 25.3-11.4C134.1 343.3 169.8 352 208 352zM448 176c0 112.3-99.1 196.9-216.5 207C255.8 457.4 336.4 512 432 512c38.2 0 73.9-8.7 104.7-23.9c7.5 4 16 7.9 25.2 11.4c18.3 6.9 40.3 12.5 62.1 12.5c6.9 0 13.1-4.5 15.2-11.1c2.1-6.6-.2-13.8-5.8-17.9l0 0 0 0-.2-.2c-.2-.2-.6-.4-1.1-.8c-1-.8-2.5-2-4.3-3.7c-3.6-3.3-8.5-8.1-13.3-14.3c-5.5-7-10.7-15.4-14.2-24.7c24.9-29 39.6-64.7 39.6-103.4c0-92.8-84.9-168.9-192.6-175.5c.4 5.1 .6 10.3 .6 15.5z"
+ },
+ "clipboard-check": {
+ title: "clipboard",
+ viewBox: "0 0 384 512",
+ d: "M192 0c-41.8 0-77.4 26.7-90.5 64H64C28.7 64 0 92.7 0 128V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H282.5C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM305 273L177 401c-9.4 9.4-24.6 9.4-33.9 0L79 337c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L271 239c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
+ },
+ "user": {
+ title: "clipboard",
+ viewBox: "0 0 448 512",
+ d: "M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"
+ },
+ "door-open": {
+ title: "open door",
+ viewBox: "0 0 576 512",
+ d: "M320 32c0-9.9-4.5-19.2-12.3-25.2S289.8-1.4 280.2 1l-179.9 45C79 51.3 64 70.5 64 92.5V448H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H96 288h32V480 32zM256 256c0 17.7-10.7 32-24 32s-24-14.3-24-32s10.7-32 24-32s24 14.3 24 32zm96-128h96V480c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H512V128c0-35.3-28.7-64-64-64H352v64z"
+ },
+ "scroll": {
+ title: "scroll",
+ viewBox: "0 0 576 512",
+ d: "M0 80v48c0 17.7 14.3 32 32 32H48 96V80c0-26.5-21.5-48-48-48S0 53.5 0 80zM112 32c10 13.4 16 30 16 48V384c0 35.3 28.7 64 64 64s64-28.7 64-64v-5.3c0-32.4 26.3-58.7 58.7-58.7H480V128c0-53-43-96-96-96H112zM464 480c61.9 0 112-50.1 112-112c0-8.8-7.2-16-16-16H314.7c-14.7 0-26.7 11.9-26.7 26.7V384c0 53-43 96-96 96H368h96z"
+ },
+ "star": {
+ title: "star",
+ viewBox: "0 0 576 512",
+ d: "M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
+ },
+ "boxes-stacked": {
+ title: "stacked boxes",
+ viewBox: "0 0 576 512",
+ d: "M248 0H208c-26.5 0-48 21.5-48 48V160c0 35.3 28.7 64 64 64H352c35.3 0 64-28.7 64-64V48c0-26.5-21.5-48-48-48H328V80c0 8.8-7.2 16-16 16H264c-8.8 0-16-7.2-16-16V0zM64 256c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H224c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64H184v80c0 8.8-7.2 16-16 16H120c-8.8 0-16-7.2-16-16V256H64zM352 512H512c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64H472v80c0 8.8-7.2 16-16 16H408c-8.8 0-16-7.2-16-16V256H352c-15 0-28.8 5.1-39.7 13.8c4.9 10.4 7.7 22 7.7 34.2V464c0 12.2-2.8 23.8-7.7 34.2C323.2 506.9 337 512 352 512z"
+ },
+ "person-running": {
+ title: "person running",
+ viewBox: "0 0 448 512",
+ d: "M320 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8 .1 5.6 .3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4 5.1 12.3C289 264.7 323.9 288 362.7 288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H362.7c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15c-11.1-3.4-22.7-5.2-34.4-5.2c-31 0-60.8 12.3-82.7 34.3L57.4 153.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5L157 361.6l-9.5-6c-17.5-10.9-30.5-26.8-37.9-44.9L91.2 352z"
+ },
+ "handshake": {
+ title: "handshake",
+ viewBox: "0 0 640 512",
+ d: "M323.4 85.2l-96.8 78.4c-16.1 13-19.2 36.4-7 53.1c12.9 17.8 38 21.3 55.3 7.8l99.3-77.2c7-5.4 17-4.2 22.5 2.8s4.2 17-2.8 22.5l-20.9 16.2L550.2 352H592c26.5 0 48-21.5 48-48V176c0-26.5-21.5-48-48-48H516h-4-.7l-3.9-2.5L434.8 79c-15.3-9.8-33.2-15-51.4-15c-21.8 0-43 7.5-60 21.2zm22.8 124.4l-51.7 40.2C263 274.4 217.3 268 193.7 235.6c-22.2-30.5-16.6-73.1 12.7-96.8l83.2-67.3c-11.6-4.9-24.1-7.4-36.8-7.4C234 64 215.7 69.6 200 80l-72 48H48c-26.5 0-48 21.5-48 48V304c0 26.5 21.5 48 48 48H156.2l91.4 83.4c19.6 17.9 49.9 16.5 67.8-3.1c5.5-6.1 9.2-13.2 11.1-20.6l17 15.6c19.5 17.9 49.9 16.6 67.8-2.9c4.5-4.9 7.8-10.6 9.9-16.5c19.4 13 45.8 10.3 62.1-7.5c17.9-19.5 16.6-49.9-2.9-67.8l-134.2-123z"
+ },
+ "reply": {
+ title: "reply",
+ viewBox: "0 0 512 512",
+ d: "M205 34.8c11.5 5.1 19 16.6 19 29.2v64H336c97.2 0 176 78.8 176 176c0 113.3-81.5 163.9-100.2 174.1c-2.5 1.4-5.3 1.9-8.1 1.9c-10.9 0-19.7-8.9-19.7-19.7c0-7.5 4.3-14.4 9.8-19.5c9.4-8.8 22.2-26.4 22.2-56.7c0-53-43-96-96-96H224v64c0 12.6-7.4 24.1-19 29.2s-25 3-34.4-5.4l-160-144C3.9 225.7 0 217.1 0 208s3.9-17.7 10.6-23.8l160-144c9.4-8.5 22.9-10.6 34.4-5.4z"
+ },
+ "d20": {
+ title: "d20",
+ viewBox: "0 0 512 512",
+ d: "M48.7 125.8l53.2 31.9c7.8 4.7 17.8 2 22.2-5.9L201.6 12.1c3-5.4-.9-12.1-7.1-12.1c-1.6 0-3.2 .5-4.6 1.4L47.9 98.8c-9.6 6.6-9.2 20.9 .8 26.9zM16 171.7V295.3c0 8 10.4 11 14.7 4.4l60-92c5-7.6 2.6-17.8-5.2-22.5L40.2 158C29.6 151.6 16 159.3 16 171.7zM310.4 12.1l77.6 139.6c4.4 7.9 14.5 10.6 22.2 5.9l53.2-31.9c10-6 10.4-20.3 .8-26.9L322.1 1.4c-1.4-.9-3-1.4-4.6-1.4c-6.2 0-10.1 6.7-7.1 12.1zM496 171.7c0-12.4-13.6-20.1-24.2-13.7l-45.3 27.2c-7.8 4.7-10.1 14.9-5.2 22.5l60 92c4.3 6.7 14.7 3.6 14.7-4.4V171.7zm-49.3 246L286.1 436.6c-8.1 .9-14.1 7.8-14.1 15.9v52.8c0 3.7 3 6.8 6.8 6.8c.8 0 1.6-.1 2.4-.4l172.7-64c6.1-2.2 10.1-8 10.1-14.5c0-9.3-8.1-16.5-17.3-15.4zM233.2 512c3.7 0 6.8-3 6.8-6.8V452.6c0-8.1-6.1-14.9-14.1-15.9l-160.6-19c-9.2-1.1-17.3 6.1-17.3 15.4c0 6.5 4 12.3 10.1 14.5l172.7 64c.8 .3 1.6 .4 2.4 .4zM41.7 382.9l170.9 20.2c7.8 .9 13.4-7.5 9.5-14.3l-85.7-150c-5.9-10.4-20.7-10.8-27.3-.8L30.2 358.2c-6.5 9.9-.3 23.3 11.5 24.7zm439.6-24.8L402.9 238.1c-6.5-10-21.4-9.6-27.3 .8L290.2 388.5c-3.9 6.8 1.6 15.2 9.5 14.3l170.1-20c11.8-1.4 18-14.7 11.5-24.6zm-216.9 11l78.4-137.2c6.1-10.7-1.6-23.9-13.9-23.9H183.1c-12.3 0-20 13.3-13.9 23.9l78.4 137.2c3.7 6.4 13 6.4 16.7 0zM174.4 176H337.6c12.2 0 19.9-13.1 14-23.8l-80-144c-2.8-5.1-8.2-8.2-14-8.2h-3.2c-5.8 0-11.2 3.2-14 8.2l-80 144c-5.9 10.7 1.8 23.8 14 23.8z"
+ },
+ "screwdriver-wrench": {
+ title: "settings",
+ viewBox: "0 0 512 512",
+ d: "M78.6 5C69.1-2.4 55.6-1.5 47 7L7 47c-8.5 8.5-9.4 22-2.1 31.6l80 104c4.5 5.9 11.6 9.4 19 9.4h54.1l109 109c-14.7 29-10 65.4 14.3 89.6l112 112c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-112-112c-24.2-24.2-60.6-29-89.6-14.3l-109-109V104c0-7.5-3.5-14.5-9.4-19L78.6 5zM19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L233.7 374.3c-7.8-20.9-9-43.6-3.6-65.1l-61.7-61.7L19.9 396.1zM512 144c0-10.5-1.1-20.7-3.2-30.5c-2.4-11.2-16.1-14.1-24.2-6l-63.9 63.9c-3 3-7.1 4.7-11.3 4.7H352c-8.8 0-16-7.2-16-16V102.6c0-4.2 1.7-8.3 4.7-11.3l63.9-63.9c8.1-8.1 5.2-21.8-6-24.2C388.7 1.1 378.5 0 368 0C288.5 0 224 64.5 224 144l0 .8 85.3 85.3c36-9.1 75.8 .5 104 28.7L429 274.5c49-23 83-72.8 83-130.5zM56 432a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"
+ },
+ "dumbbell": {
+ title: "dumbbell",
+ viewBox: "0 0 640 512",
+ d: "M96 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32V224v64V448c0 17.7-14.3 32-32 32H128c-17.7 0-32-14.3-32-32V384H64c-17.7 0-32-14.3-32-32V288c-17.7 0-32-14.3-32-32s14.3-32 32-32V160c0-17.7 14.3-32 32-32H96V64zm448 0v64h32c17.7 0 32 14.3 32 32v64c17.7 0 32 14.3 32 32s-14.3 32-32 32v64c0 17.7-14.3 32-32 32H544v64c0 17.7-14.3 32-32 32H480c-17.7 0-32-14.3-32-32V288 224 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32zM416 224v64H224V224H416z"
+ },
+ "chevron-down": {
+ title: "chevron down",
+ viewBox: "0 0 512 512",
+ d: "M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
+ },
+ "chevron-up": {
+ title: "chevron up",
+ viewBox: "0 0 512 512",
+ d: "M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"
+ },
+ "chevron-left": {
+ title: "chevron left",
+ viewBox: "0 0 320 512",
+ d: "M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"
+ },
+ "chevron-right": {
+ title: "chevron right",
+ viewBox: "0 0 320 512",
+ d: "M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"
+ },
+ };
+
+ ns.createIcon = function createIcon(iconDef, title, desc, classes, style)
+ {
+ var iconEl = createElement(
+ ns.ElementTypes.svg,
+ {
+ "viewBox": iconDef.viewBox,
+ "class": `${ICON_CLASS} ${classes}`,
+ "role": "img",
+ "style": style || ""
+ },
+ ICON_CITATION
+ );
+
+ createChildElement(
+ iconEl,
+ ns.ElementTypes.title,
+ {},
+ title || iconDef.title
+ );
+ if (desc)
+ {
+ createChildElement(
+ iconEl,
+ ns.ElementTypes.desc,
+ {},
+ desc
+ );
+ }
+
+ createChildElement(
+ iconEl,
+ ns.ElementTypes.path,
+ {
+ "d": iconDef.d
+ }
+ );
+
+ return iconEl;
+ };
+ //#endregion
+});
+
+registerNamespace("GW.Controls", function(ns) {
+ ns.IconEl = class IconEl extends HTMLElement
+ {
+ //#region staticProperties
+ static observedAttributes = [];
+ static instanceCount = 0;
+ static instanceMap = {};
+ //#endregion
+
+ //#region instance properties
+ instanceId;
+ iconObj;
+ titleText;
+ isInitialized;
+
+ //#region element properties
+ //#endregion
+ //#endregion
+
+ constructor()
+ {
+ super();
+ this.instanceId = IconEl.instanceCount++;
+ IconEl.instanceMap[this.instanceId] = this;
+ }
+
+ get idKey()
+ {
+ return `gw-icon-${this.instanceId}`;
+ }
+
+ //#region HTMLElement implementation
+ connectedCallback()
+ {
+ if (this.isInitialized) { return; }
+
+ this.iconObj = ns.SVGLib.Icons[this.getAttribute("iconKey")];
+
+ this.titleText = this.getAttribute("title");
+ if (this.hasAttribute("titleId"))
+ {
+ const titleEl = document.getElementById(this.getAttribute("titleId"));
+ this.titleText = titleEl.innerText;
+ titleEl.remove();
+ }
+
+ this.iconDescription = this.getAttribute("description");
+ this.iconClasses = this.getAttribute("iconClasses");
+ this.iconStyle = this.getAttribute("iconStyle");
+
+ this.renderContent();
+ this.isInitialized = true;
+ }
+ //#endregion
+
+ renderContent()
+ {
+ if (!this.iconObj)
+ {
+ debugger;
+ return;
+ }
+
+ //Markup
+ this.appendChild(
+ ns.SVGLib.createIcon(
+ this.iconObj,
+ this.titleText,
+ this.iconDescription,
+ this.iconClasses,
+ this.iconStyle,
+ )
+ );
+
+ //element properties
+ }
+ };
+ customElements.define("gw-icon", ns.IconEl);
+});
\ No newline at end of file
diff --git a/src/pages/guestbook.njk b/src/pages/guestbook.njk
index 5825171d..d2faf1d6 100644
--- a/src/pages/guestbook.njk
+++ b/src/pages/guestbook.njk
@@ -16,93 +16,21 @@ eleventyComputed:
Archive of my previous guestbook hosted on 123Guestbook
-Leave A Message
+
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file