diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..51d1042 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "eslint.experimental.useFlatConfig": true, + "eslint.options": { "overrideConfigFile": "./config/eslint.config.mjs" }, +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/config/eslint.config.mjs b/config/eslint.config.mjs index 612f561..c521439 100644 --- a/config/eslint.config.mjs +++ b/config/eslint.config.mjs @@ -1,14 +1,14 @@ import { FlatCompat } from '@eslint/eslintrc'; import globals from 'globals'; -import path from 'path'; +import {dirname } from 'path'; import { fileURLToPath } from 'url'; import js from '@eslint/js'; // mimic CommonJS variables -- not needed if using CommonJS -const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); const compat = new FlatCompat({ - baseDirectory: dirname, + baseDirectory: _dirname, }); export default [ @@ -16,7 +16,14 @@ export default [ { languageOptions: { globals: globals.browser, + ecmaVersion: 'latest', + sourceType: 'module', }, }, ...compat.extends('airbnb-base'), -]; \ No newline at end of file + { + rules: { + 'import/prefer-default-export': 'off', + }, + } +]; diff --git a/package-lock.json b/package-lock.json index 4b67ef7..4e87c6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache", "dependencies": { "bootstrap-icons": "^1.11.3", + "highlight.js": "^11.9.0", "lit": "^3.1.2" }, "devDependencies": { @@ -1930,6 +1931,14 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", diff --git a/package.json b/package.json index 6b21af2..2d8e510 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "bootstrap-icons": "^1.11.3", + "highlight.js": "^11.9.0", "lit": "^3.1.2" }, "files": [ diff --git a/src/app/CanvasElement.js b/src/app/CanvasElement.js new file mode 100644 index 0000000..dd0fae7 --- /dev/null +++ b/src/app/CanvasElement.js @@ -0,0 +1,138 @@ +import { LitElement, html, css, nothing } from 'lit'; + +/** + * @class CanvasElement + * + * This class is used to contain content boxes which are stacked + * vertically or horizontally within the canvas. + * + * @property {Boolean} vertical - Fill the canvas vertically rather than horizontally, default false + * @property {String} backgroundColor - Background color of the canvas, light or dark, default light + * + * @example + * + * .... + * .... + * .... + * + */ +export class CanvasElement extends LitElement { + static get localName() { + return 'wc-canvas'; + } + + constructor() { + super(); + this.backgroundColor = 'light'; + this.vertical = false; + } + + static get properties() { + return { + vertical: { type: Boolean }, + backgroundColor: { type: String }, + }; + } + + static get styles() { + return css` + div { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + } + + /* Any canvas section is in flexbox mode with a fixed size */ + ::slotted(wc-canvas-section),::slotted(wc-canvas-navbar) { + display: flex; + justify-content: start; + border-style: none; + } + + div.vertical { + flex-direction: column; + + /* The first section in vertical mode gets a border below */ + > ::slotted(wc-canvas-navbar:first-child),::slotted(wc-canvas-section:first-child) { + min-height: 40px; + border-bottom-style: solid; + } + + /* The last section in vertical mode gets a border above */ + > ::slotted(wc-canvas-navbar:last-child),::slotted(wc-canvas-section:last-child) { + min-height: 30px; + border-top-style: solid; + } + } + + div:not(.vertical) { + flex-direction: row; + + /* The first section in horizontal mode gets a border right */ + > ::slotted(wc-canvas-navbar:first-child),::slotted(wc-canvas-section:first-child) { + min-width: 60px; + border-right-style: solid; + } + + /* The last section in horizonal mode gets a border left */ + > ::slotted(wc-canvas-navbar:last-child),::slotted(wc-canvas-section:last-child) { + min-width: 60px; + border-left-style: solid; + } + } + + /* Flex containers stretch */ + ::slotted(wc-canvas-section[flex]) { + flex: 999 0; + overflow: auto; + } + + /* Hidden navbars are hidden */ + ::slotted(wc-canvas-navbar[hidden]) { + display: none; + } + + /* Light theme setting colours and border widths */ + div.bg-color-light { + & ::slotted(wc-canvas-navbar), ::slotted(wc-canvas-section) { + background-color: var(--light-color); + color: var(--dark-color); + border-color: var(--grey-20-color); + border-width: 1px; + } + } + + /* Dark theme setting colours and border widths */ + div.bg-color-dark { + & ::slotted(wc-canvas-navbar), ::slotted(wc-canvas-section) { + background-color: var(--dark-color); + color: var(--light-color); + border-color: var(--grey-40-color); + border-width: 1px; + } + } + `; + } + + render() { + return html` +
+ +
+ `; + } + + get className() { + const classes = []; + if (this.backgroundColor) { + classes.push(`bg-color-${this.backgroundColor}`); + } + if (this.vertical) { + classes.push('vertical'); + } + return classes.join(' '); + } +} diff --git a/src/app/CanvasNavbarElement.js b/src/app/CanvasNavbarElement.js new file mode 100644 index 0000000..5d1cfdb --- /dev/null +++ b/src/app/CanvasNavbarElement.js @@ -0,0 +1,61 @@ +import { html, css, nothing } from 'lit'; +import { CanvasSectionElement } from './CanvasSectionElement'; + +/** + * @class CanvasNavbarElement + * + * This class is a navigaton used to contain content boxes which are stacked + * vertically or horizontally within the canvas. + * + * @property {Boolean} hidden - Whether the navigation is hidden, default false + * + * @example + * + * .... + * .... + * .... + * + */ +export class CanvasNavbarElement extends CanvasSectionElement { + static get localName() { + return 'wc-canvas-navbar'; + } + + constructor() { + super(); + this.hidden = false; + } + + static get properties() { + return { + hidden: { type: Boolean }, + }; + } + + static get styles() { + return css` + /* Navbars are flexed to fill the available space */ + div { + display: flex; + align-items: start; + flex: 1; + } + `; + } + + render() { + return html` +
+ +
+ `; + } + + get classes() { + const classes = []; + if (this.hidden) { + classes.push('hidden'); + } + return classes; + } +} diff --git a/src/app/CanvasSectionElement.js b/src/app/CanvasSectionElement.js new file mode 100644 index 0000000..8404632 --- /dev/null +++ b/src/app/CanvasSectionElement.js @@ -0,0 +1,53 @@ +import { LitElement, html, css, nothing } from 'lit'; + +/** + * @class CanvasSectionElement + * + * This class is used for content boxes which are stacked + * vertically or horizontally within a canvas. The content + * boxes can be flexed to fill the available space. + * + * @property {Boolean} flex - Flex the element to fill the available space, default false + * + * @example + * + * .... + * .... + * .... + * + */ +export class CanvasSectionElement extends LitElement { + static get localName() { + return 'wc-canvas-section'; + } + + static get properties() { + return { + flex: { type: Boolean }, + }; + } + + static get styles() { + return css` + .flex { + flex: 999 0; + } + `; + } + + render() { + return html` +
+ +
+ `; + } + + get className() { + const classes = []; + if (this.flex) { + classes.push('flex'); + } + return classes.join(' '); + } +} diff --git a/src/app/CardElement.js b/src/app/CardElement.js new file mode 100644 index 0000000..5a87d30 --- /dev/null +++ b/src/app/CardElement.js @@ -0,0 +1,90 @@ +import { LitElement, html, css, nothing } from 'lit'; + +/** + * @class CardElement + * + * This class is used as a information card to display content + * + * @property {String} backgroundColor - The color theme of the card, default light + * @property {Number} width - The width of the card in 1/12th of the available space + * @property {Boolean} hidden - Whether the card is hidden, default false + * + * @example + * + * + *

Card Title

+ *
+ *
+ */ +export class CardElement extends LitElement { + static get localName() { + return 'wc-card'; + } + + constructor() { + super(); + + // Default properties + this.backgroundColor = 'light'; + this.width = 0; + this.hidden = false; + } + + static get properties() { + return { + backgroundColor: { type: String, attribute: true }, + width: { type: Number, attribute: true }, + hidden: { type: Boolean, attribute: true }, + }; + } + + render() { + this.style.maxWidth = `${this.flexBasis}`; + this.style.flexBasis = `${this.flexBasis}`; + return html` +
+ +
+ `; + } + + get classes() { + const classes = []; + classes.push('card'); + if (this.width) { + classes.push(`width-${this.width}`); + } + if (this.backgroundColor) { + classes.push(`bg-${this.backgroundColor}`); + } + return classes; + } + + get flexBasis() { + if (this.width > 0 && this.width <= 12) { + return `${this.width * (100 / 12)}%`; + } + return 'none'; + } +} + +/* + --primary-color: #16f; + --secondary-color: #aaa; + --success-color: #285; + --warning-color: #f72; + --error-color: #f55; + --light-color: #eee; + --white-color: #fff; + --dark-color: #333; + --black-color: #000; + --grey-10-color: #EAEAEA; + --grey-20-color: #D0D0D0; + --grey-30-color: #B6B6B6; + --grey-40-color: #9C9C9C; + --grey-50-color: #828282; + --grey-60-color: #6A6A6A; + --grey-70-color: #4E4E4E; + --grey-80-color: #343434; + --grey-90-color: #1A1A1A; + */ \ No newline at end of file diff --git a/src/app/CardGroupElement.js b/src/app/CardGroupElement.js new file mode 100644 index 0000000..830d33a --- /dev/null +++ b/src/app/CardGroupElement.js @@ -0,0 +1,100 @@ +import { LitElement, html, css, nothing } from 'lit'; + +/** + * @class CardGroupElement + * + * This class is used as a group of information cards for layout purposes + * + * @example + * + * + *

Card Title

+ *
+ *
+ */ +export class CardGroupElement extends LitElement { + static get localName() { + return 'wc-card-group'; + } + + static get styles() { + return css` + :host { + flex: 0 1 auto; + } + div { + display: flex; + flex-wrap: wrap; + } + + /* Hidden Cards */ + ::slotted(wc-card[hidden]) { + display: none; + } + + /* Colours */ + ::slotted(wc-card) { + position: relative; + border: 1px solid var(--grey-20-color); + border-radius: var(--card-border-radius); + margin: var(--card-margin-y) var(--card-margin-x); + padding: var(--card-padding-y) var(--card-padding-x); + } + ::slotted(wc-card) { /* TODO: Default should vary based on theme */ + background-color: var(--light-color); + color: var(--dark-color); + } + ::slotted(wc-card[backgroundColor="primary"]) { + background-color: var(--primary-color); + color: var(--light-color); + } + ::slotted(wc-card[backgroundColor="secondary"]) { + background-color: var(--secondary-color); + color: var(--dark-color); + } + ::slotted(wc-card[backgroundColor="success"]) { + background-color: var(--success-color); + color: var(--light-color); + } + ::slotted(wc-card[backgroundColor="warning"]) { + background-color: var(--warning-color); + color: var(--light-color); + } + ::slotted(wc-card[backgroundColor="error"]) { + background-color: var(--error-color); + color: var(--light-color); + } + ::slotted(wc-card[backgroundColor="light"]) { + background-color: var(--light-color); + color: var(--dark-color); + } + ::slotted(wc-card[backgroundColor="dark"]) { + background-color: var(--dark-color); + color: var(--light-color); + } + ::slotted(wc-card[backgroundColor="black"]) { + background-color: var(--black-color); + color: var(--white-color); + } + ::slotted(wc-card[backgroundColor="white"]) { + background-color: var(--white-color); + color: var(--black-color); + } + `; + } + + render() { + return html` +
+ +
+ `; + } + + // eslint-disable-next-line class-methods-use-this + get classes() { + const classes = []; + classes.push('card-group'); + return classes; + } +} diff --git a/src/app/IconElement.js b/src/app/IconElement.js new file mode 100644 index 0000000..68108d6 --- /dev/null +++ b/src/app/IconElement.js @@ -0,0 +1,81 @@ +import { LitElement, svg, css, nothing } from 'lit'; +import icons from 'bootstrap-icons/bootstrap-icons.svg'; + +/** + * @class IconElement + * + * This class is used to display a bootstrap icon + * + * @property {Boolean} name - The name of the icon + * @property {Boolean} size - The size of the icon + * + * @example + * + */ +export class IconElement extends LitElement { + static get localName() { + return 'wc-icon'; + } + + constructor() { + super(); + this.name = 'bootstrap-reboot'; + this.size = 'default'; + } + + static get properties() { + return { + name: { type: String }, + size: { type: String }, + }; + } + + static get styles() { + return css` + :host { + display: inline-block; + vertical-align: middle; + } + .size-default { + position: relative; + width: var(--icon-size-default); + height: var(--icon-size-default); + } + .size-small { + position: relative; + width: var(--icon-size-small); + height: var(--icon-size-small); + } + .size-medium { + position: relative; + width: var(--icon-size-medium); + height: var(--icon-size-medium); + } + .size-large { + position: relative; + width: var(--icon-size-large); + height: var(--icon-size-large); + } + .size-xlarge { + position: relative; + width: var(--icon-size-xlarge); + height: var(--icon-size-xlarge); + } + svg { + width: 100%; + height: 100%; + fill: currentColor; + } + `; + } + + get classes() { + const classes = []; + classes.push(`size-${this.size}`); + return classes; + } + + render() { + return svg`
`; + } +} diff --git a/src/app/NavDividerElement.js b/src/app/NavDividerElement.js new file mode 100644 index 0000000..0b1d4fe --- /dev/null +++ b/src/app/NavDividerElement.js @@ -0,0 +1,32 @@ +import { LitElement, html, nothing } from 'lit'; + +/** + * @class NavDividerElement + * + * This class is used as a visible divider between navigation items + * + * @example + * + * .... + * + * .... + * + */ +export class NavDividerElement extends LitElement { + static get localName() { + return 'wc-nav-divider'; + } + + render() { + return html` +
  • + `; + } + + // eslint-disable-next-line class-methods-use-this + get classes() { + const classes = []; + classes.push('divider'); + return classes; + } +} diff --git a/src/app/NavGroupElement.js b/src/app/NavGroupElement.js new file mode 100644 index 0000000..5dcf3cb --- /dev/null +++ b/src/app/NavGroupElement.js @@ -0,0 +1,129 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { EventType } from '../core/Event'; + +/** + * @class NavGroupElement + * + * This class is used to contain a list of navigation items, either horizontally or vertically. + * + * @property {Boolean} vertical - Fill the canvas vertically rather than horizontally, default false + * @property {Boolean} flex - Take up all available space, default false + * @event CLICK - Dispatched when a navigation item is clicked, which is not disabled + * + * @example + * + * .... + * + * .... + * + */ +export class NavGroupElement extends LitElement { + static get localName() { + return 'wc-nav-group'; + } + + constructor() { + super(); + this.vertical = false; + } + + static get properties() { + return { + vertical: { type: Boolean }, + flex: { type: Boolean }, + }; + } + + static get styles() { + return css` + :host { + flex: 1 0; + height: 100%; + } + ul { + display: flex; + flex-direction: row; + list-style-type: none; + padding: 0; + margin: 0; + } + ul.vertical { + flex-direction: column; + height: 100%; + } + ::slotted(*) { + cursor: pointer; + user-select: none; + } + ::slotted(wc-nav-item) { + flex: 0; + margin: var(--nav-item-margin-y) var(--nav-item-margin-x); + padding: var(--nav-item-padding-y) var(--nav-item-padding-x); + border-radius: var(--nav-item-border-radius); + } + ::slotted(wc-nav-spacer) { + flex: 1; + } + ul:not([vertical]) ::slotted(wc-nav-divider) { + border-left: 1px solid var(--nav-item-divider-color); + } + ul.vertical ::slotted(wc-nav-divider) { + border-top: 1px solid var(--nav-item-divider-color); + } + ::slotted(wc-nav-item[selected]) { + color: var(--nav-item-selected-color); + background-color: var(--nav-item-selected-background-color,red); + } + ::slotted(wc-nav-item[disabled]) { + color: var(--nav-item-disabled-color); + cursor: default; + } + `; + } + + render() { + return html` + + `; + } + + get classes() { + const classes = []; + if (this.vertical) { + classes.push('vertical'); + } + if (this.flex) { + classes.push('flex'); + } + return classes; + } + + /** + * Select the named item from the list of wv-nav-item elements + * + * @param {String} name - Name of the item to select + */ + select(name) { + this.querySelectorAll('wc-nav-item').forEach((item) => { + if (item.name === name) { + item.setAttribute('selected', true); + } else { + item.removeAttribute('selected'); + } + }); + } + + // eslint-disable-next-line class-methods-use-this + onClick(event) { + const target = event.target.closest('wc-nav-item'); + if (target && !target.disabled) { + this.dispatchEvent(new CustomEvent(EventType.CLICK, { + bubbles: true, + composed: true, + detail: target.name || target.textContent.trim(), + })); + } + } +} diff --git a/src/app/NavItemElement.js b/src/app/NavItemElement.js new file mode 100644 index 0000000..ed9fa2f --- /dev/null +++ b/src/app/NavItemElement.js @@ -0,0 +1,59 @@ +import { LitElement, html, nothing } from 'lit'; + +/** + * @class NavItemElement + * + * This class is used as a navigation item within a group of navigational items + * + * @property {String} name - The name of the navigational item + * @property {Boolean} selected - Whether the item is selected + * @property {Boolean} disabled - Whether the item is disabled + * + * @example + * + * .... + * + * .... + * + */ +export class NavItemElement extends LitElement { + static get localName() { + return 'wc-nav-item'; + } + + constructor() { + super(); + + // Default properties + this.name = ''; + this.selected = false; + this.disabled = false; + } + + static get properties() { + return { + name: { type: String }, + selected: { type: Boolean }, + disabled: { type: Boolean }, + }; + } + + render() { + return html` +
  • + +
  • + `; + } + + get classes() { + const classes = []; + if (this.selected) { + classes.push('selected'); + } + if (this.disabled) { + classes.push('disabled'); + } + return classes; + } +} diff --git a/src/app/NavSpacerElement.js b/src/app/NavSpacerElement.js new file mode 100644 index 0000000..93ede72 --- /dev/null +++ b/src/app/NavSpacerElement.js @@ -0,0 +1,40 @@ +import { LitElement, html, css, nothing } from 'lit'; + +/** + * @class NavSpacerElement + * + * This class is used as a spacer to separate navigation items to push siblings apart + * + * @example + * + * .... + * + * .... + * + */ +export class NavSpacerElement extends LitElement { + static get localName() { + return 'wc-nav-spacer'; + } + + static get styles() { + return css` + :host { + flex: 1 0; + } + `; + } + + render() { + return html` +
  • + `; + } + + // eslint-disable-next-line class-methods-use-this + get classes() { + const classes = []; + classes.push('spacer'); + return classes; + } +} diff --git a/src/app/app.css b/src/app/app.css new file mode 100644 index 0000000..9aa2529 --- /dev/null +++ b/src/app/app.css @@ -0,0 +1,28 @@ +:root { + /* Navigational items */ + --nav-item-padding-x: 0.5em; + --nav-item-padding-y: 0.5em; + --nav-item-margin-x: 0.5em; + --nav-item-margin-y: 0; + --nav-item-border-radius: var(--border-radius); + --nav-item-hover-font-weight: var(--font-weight-semibold); + --nav-item-selected-color: var(--light-color); + --nav-item-disabled-color: var(--grey-40-color); + --nav-item-divider-color: var(--grey-20-color); + --nav-item-selected-background-color: var(--primary-color); + + /* icon */ + --icon-size-default: 1.5em; + --icon-size-small: 1em; + --icon-size-medium: 1.5em; + --icon-size-large: 2em; + --icon-size-xlarge: 3em; + + /* card */ + --card-margin-x: 0.5em; + --card-margin-y: 0.5em; + --card-padding-x: 0.4em; + --card-padding-y: 0.4em; + --card-border-radius: var(--border-radius); + --card-transition-delay: 300ms; +} diff --git a/src/app/app.js b/src/app/app.js new file mode 100644 index 0000000..03ded30 --- /dev/null +++ b/src/app/app.js @@ -0,0 +1,27 @@ +// CSS +import './app.css'; + +// Classes +import { CanvasElement } from './CanvasElement'; +import { CanvasSectionElement } from './CanvasSectionElement'; +import { CanvasNavbarElement } from './CanvasNavbarElement'; +import { NavGroupElement } from './NavGroupElement'; +import { NavItemElement } from './NavItemElement'; +import { NavSpacerElement } from './NavSpacerElement'; +import { NavDividerElement } from './NavDividerElement'; +import { IconElement } from './IconElement'; +import { CardGroupElement } from './CardGroupElement'; +import { CardElement } from './CardElement'; + +// Web Components +customElements.define(CanvasElement.localName, CanvasElement); // wc-canvas +customElements.define(CanvasSectionElement.localName, CanvasSectionElement); // wc-canvas-section +customElements.define(CanvasNavbarElement.localName, CanvasNavbarElement); // wc-canvas-navbar +customElements.define(NavGroupElement.localName, NavGroupElement); // wc-nav-group +customElements.define(NavItemElement.localName, NavItemElement); // wc-nav-item +customElements.define(NavSpacerElement.localName, NavSpacerElement); // wc-nav-spacer +customElements.define(NavDividerElement.localName, NavDividerElement); // wc-nav-divider +customElements.define(IconElement.localName, IconElement); // wc-icon +customElements.define(CardGroupElement.localName, CardGroupElement); // wc-card-group +customElements.define(CardElement.localName, CardElement); // wc-card + diff --git a/src/button/ButtonElement.js b/src/button/ButtonElement.js new file mode 100644 index 0000000..fb9ccb3 --- /dev/null +++ b/src/button/ButtonElement.js @@ -0,0 +1,179 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { EventType } from '../core/Event'; + +/** + * @class ButtonElement + * + * This class is used as a button, either a control (close, help, etc), a form submission or + * a normal button. + * + * @property {String} name - The name of the button + * @property {String} type - The type of the button if not standard + * (control, submit) + * @property {String} textTransform - If the button text should be transformed + * (uppercase, lowercase, capitalize) + * @property {Boolean} disabled - Whether the button is disabled + * @property {String} popup - The associated popup to show when the button is clicked + * + * @example + * Close + */ +export class ButtonElement extends LitElement { + static get localName() { + return 'wc-button'; + } + + constructor() { + super(); + + // Default properties + this.name = ''; + this.type = ''; + this.textTransform = ''; + this.disabled = false; + this.popup = ''; + } + + static get properties() { + return { + name: { type: String }, + type: { type: String }, + textTransform: { type: String }, + disabled: { type: Boolean }, + popup: { type: String }, + }; + } + + static get styles() { + return css` + button { + display: inline-block; + position: relative; + margin: none; + padding: var(--button-padding-y) var(--button-padding-x); + color: var(--button-color); + background-color: var(--button-background-color); + cursor: pointer; + user-select: none; + } + button.control { + color: var(--button-control-color); + background: transparent; + border: none; + padding: var(--button-control-padding-y) var(--button-control-padding-x); + + &:hover { + color: var(--button-control-color-hover); + } + &:active { + color: var(--button-control-color-active); + } + &:disabled { + color: var(--button-control-color-disabled); + cursor: default; + } + } + `; + } + + get classes() { + const classes = []; + if (this.type) { + classes.push(this.type.toLowerCase()); + } + if (this.textTransform) { + classes.push(`text-transform-${this.textTransform.toLowerCase()}`); + } + return classes; + } + + get buttonTitle() { + return this.textContent.trim(); + } + + get controlButtonIcon() { + switch (this.buttonTitle.toLowerCase()) { + case 'close': + return 'x-square'; + case 'help': + return 'question-square'; + case 'maximise': + return 'fullscreen'; + case 'minimise': + return 'fullscreen-exit'; + default: + return ''; + } + } + + firstUpdated() { + if (this.popup) { + // Attach the popover target element + const buttonNode = this.shadowRoot.querySelector('button'); + const targetNode = document.getElementById(this.popup); + if (buttonNode && targetNode) { + buttonNode.popoverTargetElement = targetNode; + } + } + } + + renderButton() { + return html` + + `; + } + + renderSubmit() { + return html` + + `; + } + + renderControl() { + const icon = this.controlButtonIcon; + if (icon) { + return html` + + `; + } + return this.renderButton(); + } + + render() { + switch (this.type.toLowerCase()) { + case 'control': + return this.renderControl(); + case 'submit': + return this.renderSubmit(); + default: + return this.renderButton(); + } + } + + // Change the selected state when the input is changed + onClick() { + if (!this.disabled) { + this.dispatchEvent(new CustomEvent(EventType.CLICK, { + bubbles: true, + composed: true, + detail: this.name || this.buttonTitle, + })); + } + } +} diff --git a/src/button/ControlButtonGroupElement.js b/src/button/ControlButtonGroupElement.js new file mode 100644 index 0000000..3839508 --- /dev/null +++ b/src/button/ControlButtonGroupElement.js @@ -0,0 +1,62 @@ +import { LitElement, html, css, nothing } from 'lit'; + +/** + * @class ControlButtonGroupElement + * + * This class is used as a group of control buttons, as decoration of a card, located on the + * top right corner of the card. + * + * @property {String} name - The name of the control group + * + * @example + * + * Close + * + */ +export class ControlButtonGroupElement extends LitElement { + static get localName() { + return 'wc-control-button-group'; + } + + constructor() { + super(); + + // Default properties + this.name = ''; + } + + static get properties() { + return { + name: { type: String }, + }; + } + + static get styles() { + return css` + div { + position: absolute; + top: 0; + right: 0; + padding: 0; + border: 0; + background: transparent; + margin: var(--control-button-group-margin-y) var(--control-button-group-margin-x) ; + } + `; + } + + // eslint-disable-next-line class-methods-use-this + get classes() { + const classes = []; + classes.push('control-button-group'); + return classes; + } + + render() { + return html` +
    + +
    + `; + } +} diff --git a/src/button/button.css b/src/button/button.css new file mode 100644 index 0000000..56686f3 --- /dev/null +++ b/src/button/button.css @@ -0,0 +1,39 @@ + +:root { + /* button */ + --button-background-color: var(--control-color); + --button-background-color-hover: var(--control-color-hover); + --button-background-color-active: var(--control-color-active); + --button-background-color-disabled: var(--control-color-disabled); + --button-color: var(--dark-color); + --button-color-hover: var(--light-color); + --button-color-active: var(--light-color); + --button-color-disabled: var(--light-color); + --button-font-weight: var(--font-weight-normal); + --button-font-weight-hover: var(--font-weight-bold); + --button-font-weight-active: var(--font-weight-bold); + --button-font-weight-disabled: var(--font-weight-normal); + --button-font-weight-default: var(--font-weight-semibold); + --button-border: none; + --button-border-color: var(--black-color); + --button-border-radius: var(--border-radius); + --button-border-radius-left: var(--button-border-radius); + --button-border-radius-right: var(--button-border-radius); + --button-font-size: var(--font-size-normal); + --button-padding-x: 0.45em; + --button-padding-y: 0.45em; + --button-offset-active: 0.1em; + --button-group-divider-color: var(--light-color); + + /* close */ + --button-control-padding-x: 0; + --button-control-padding-y: 0; + --button-control-color: var(--control-color); + --button-control-color-hover: var(--control-color-hover); + --button-control-color-active: var(--control-color-active); + --button-control-color-disabled: var(--control-color-disabled); + + /* control button group */ + --control-button-group-margin-y: 0.45em; + --control-button-group-margin-x: 0.3em; +} diff --git a/src/button/button.js b/src/button/button.js new file mode 100644 index 0000000..edd3992 --- /dev/null +++ b/src/button/button.js @@ -0,0 +1,11 @@ +// CSS +import './button.css'; + +// Classes +import { ControlButtonGroupElement } from './ControlButtonGroupElement'; +import { ButtonElement } from './ButtonElement'; + +// wc-control-button-group +customElements.define(ControlButtonGroupElement.localName, ControlButtonGroupElement); +// wc-button +customElements.define(ButtonElement.localName, ButtonElement); diff --git a/src/component/badge/BadgeElement.js b/src/component/badge/BadgeElement.js deleted file mode 100644 index a334297..0000000 --- a/src/component/badge/BadgeElement.js +++ /dev/null @@ -1,109 +0,0 @@ - -import { LitElement, html, css, customElement } from 'lit'; - -/** - * A badge element class that can be used to display a badge with text or icon. - * For example: - * - * ```html - * New - * - * ``` - */ -export class BadgeElement extends LitElement { - static localName = 'wc-badge'; - - constructor() { - super(); - // Default properties - this.transform = 'none'; - this.backgroundColor = 'primary'; - } - - static get properties() { - return { - /** - * The text transform, none, uppercase, lowercase,capitalize - * @type {String} - * @memberof BadgeElement - */ - transform: { type: String }, - - /** - * The badge background color. One of the following: primary, secondary, success, warning, danger, light, dark - * @type {String} - * @memberof BadgeElement - */ - backgroundColor: { type: String }, - }; - } - - static get styles() { - return css` - span { - display: inline-block; - background-color: var(--badge-background-color); - color: var(--badge-color); - padding: var(--badge-padding-y) var(--badge-padding-x); - font-size: var(--badge-font-size); - font-weight: var(--badge-font-weight); - border-top-left-radius: var(--badge-border-radius-left); - border-bottom-left-radius: var(--badge-border-radius-left); - border-top-right-radius: var(--badge-border-radius-right); - border-bottom-right-radius: var(--badge-border-radius-right); - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - cursor: default; - } - .text-transform-capitalize { - text-transform: capitalize; - } - .text-transform-uppercase { - text-transform: uppercase; - } - .text-transform-lowercase { - text-transform: lowercase; - } - .text-transform-none { - text-transform: none; - } - .bg-color-primary { - background-color: var(--primary-color); - color: var(--light-color); - } - .bg-color-secondary { - background-color: var(--secondary-color); - color: var(--dark-color); - } - .bg-color-success { - background-color: var(--success-color); - color: var(--light-color); - } - .bg-color-warning { - background-color: var(--warning-color); - color: var(--light-color); - } - .bg-color-error { - background-color: var(--error-color); - color: var(--light-color); - } - .bg-color-light { - background-color: var(--light-color); - color: var(--dark-color); - } - .bg-color-dark { - background-color: var(--dark-color); - color: var(--light-color); - } - `; - } - - render() { - return html` - - `; - } -} - diff --git a/src/component/badge/BadgeGroupElement.js b/src/component/badge/BadgeGroupElement.js deleted file mode 100644 index 25756a3..0000000 --- a/src/component/badge/BadgeGroupElement.js +++ /dev/null @@ -1,40 +0,0 @@ - -import { LitElement, html, css } from 'lit'; - -/** - * wc-badge-group is a group of badges - * - * @slot - This element has a slot to include wc-badge elements - */ -export class BadgeGroupElement extends LitElement { - constructor() { - super(); - } - - static get styles() { - return css` - span { - display: flex; - --badge-border-radius-left: 0; - --badge-border-radius-right: 0; - } - ::slotted(:not(:last-child)) { - border-right: 1px solid var(--badge-group-divider-color); - } - ::slotted(*:first-child) { - --badge-border-radius-left: var(--badge-border-radius); - } - ::slotted(*:last-child) { - --badge-border-radius-right: var(--badge-border-radius); - } - `; - } - - render() { - return html``; - } -} - -customElements.define('wc-badge-group', BadgeGroupElement); - - diff --git a/src/component/nav/NavBarElement.js b/src/component/nav/NavBarElement.js deleted file mode 100644 index 9813c1c..0000000 --- a/src/component/nav/NavBarElement.js +++ /dev/null @@ -1,56 +0,0 @@ - -import { LitElement, html, css } from 'lit'; - -/** - * NavBarElement - */ -export class NavBarElement extends LitElement { - static get properties() { - return { - /** - * Name of the button to use when firing the EVENT_CLICK event - * @type {String} - */ - name: { type: String }, - - /** - * Whether the button is disabled - * @type {Boolean} - */ - disabled: { type: Boolean }, - }; - } - static get styles() { - return css` - nav { - position: relative; - padding: var(--navbar-padding-y) var(--navbar-padding-x) var(--navbar-padding-y) var(--navbar-padding-x); - background-color: var(--navbar-background-color); - color: var(--navbar-color); - border-bottom: 1px solid var(--navbar-border-edge-color); - } - ul { - list-style: none; - display: flex; - flex-flow: row wrap; - justify-content: start; - padding-left: 0; - } - `; - } - - render() { - return html` - - `; - } - - constructor() { - super(); - // Default properties - this.name = ''; - this.disabled = false; - } -} - -customElements.define('wc-navbar', NavBarElement); \ No newline at end of file diff --git a/src/component/nav/NavItemElement.js b/src/component/nav/NavItemElement.js deleted file mode 100644 index 9886f40..0000000 --- a/src/component/nav/NavItemElement.js +++ /dev/null @@ -1,64 +0,0 @@ - -import { LitElement, html, css } from 'lit'; -import Event from '../../core/Event'; - -/** - * NavItemElement - */ -export class NavItemElement extends LitElement { - constructor() { - super(); - // Default properties - this.name = ''; - this.disabled = false; - } - static get properties() { - return { - /** - * Name of the item to use when firing the EVENT_CLICK event - * @type {String} - */ - name: { type: String }, - - /** - * Whether the item is disabled - * @type {Boolean} - */ - disabled: { type: Boolean }, - }; - } - static get styles() { - return css` - li { - color: var(--nav-item-color); - padding: var(--nav-item-padding-y) var(--nav-item-padding-x) var(--nav-item-padding-y) var(--nav-item-padding-x); - cursor: pointer; - } - li:hover { - color: var(--nav-item-color-hover); - } - li.disabled,li.disabled:hover { - color: var(--nav-item-color-disabled); - cursor: default; - } - `; - } - render() { - return html` -
  • - `; - } - onClick() { - if (!this.disabled) { - this.dispatchEvent(new CustomEvent( - Event.EVENT_CLICK, { - bubbles: true, - composed: true, - detail: this.name || this.textContent - }, - )); - } - } -} - -customElements.define('wc-nav-item', NavItemElement); \ No newline at end of file diff --git a/src/css/core.css b/src/core.css similarity index 59% rename from src/css/core.css rename to src/core.css index a533da1..0652e5b 100644 --- a/src/css/core.css +++ b/src/core.css @@ -21,7 +21,15 @@ --white-color: #fff; --dark-color: #333; --black-color: #000; - --grey-900-color: #222; + --grey-10-color: #EAEAEA; + --grey-20-color: #D0D0D0; + --grey-30-color: #B6B6B6; + --grey-40-color: #9C9C9C; + --grey-50-color: #828282; + --grey-60-color: #6A6A6A; + --grey-70-color: #4E4E4E; + --grey-80-color: #343434; + --grey-90-color: #1A1A1A; /* controls */ --control-color: #bbb; @@ -64,39 +72,6 @@ /* borders */ --border-radius: 0.20em; - /* button */ - --button-background-color: var(--control-color); - --button-background-color-hover: var(--control-color-hover); - --button-background-color-active: var(--control-color-active); - --button-background-color-disabled: var(--control-color-disabled); - --button-color: var(--dark-color); - --button-color-hover: var(--light-color); - --button-color-active: var(--light-color); - --button-color-disabled: var(--light-color); - --button-font-weight: var(--font-weight-normal); - --button-font-weight-hover: var(--font-weight-bold); - --button-font-weight-active: var(--font-weight-bold); - --button-font-weight-disabled: var(--font-weight-normal); - --button-font-weight-default: var(--font-weight-semibold); - --button-border: none; - --button-border-color: var(--black-color); - --button-border-radius: var(--border-radius); - --button-border-radius-left: var(--button-border-radius); - --button-border-radius-right: var(--button-border-radius); - --button-font-size: var(--font-size-normal); - --button-padding-x: 0.45em; - --button-padding-y: 0.45em; - --button-offset-active: 0.1em; - --button-group-divider-color: var(--light-color); - - /* close */ - --button-close-size: 16px; - --button-close-padding: 0; - --button-close-color: var(--control-color); - --button-close-color-hover: var(--control-color-hover); - --button-close-color-active: var(--control-color-active); - --button-close-color-disabled: var(--control-color-disabled); - /* badge */ --badge-padding-x: 0.35em; --badge-padding-y: 0.35em; @@ -107,13 +82,6 @@ --badge-border-radius-right: var(--badge-border-radius); --badge-group-divider-color: var(--light-color); - /* icon */ - --icon-size-default: 1.5em; - --icon-size-small: 1em; - --icon-size-medium: 1.5em; - --icon-size-large: 2em; - --icon-size-xlarge: 3em; - /* modal */ --modal-margin-x: 20%; --modal-padding: calc(var(--spacer) * 1.5); @@ -127,34 +95,6 @@ --modal-canvas-color: var(--black-color); --modal-canvas-opacity: 0.7; - /* navbar */ - --navbar-color: var(--light-color); - --navbar-background-color: var(--primary-color); - --navbar-border-edge-color: var(--secondary-color); - --navbar-padding-x: calc(var(--spacer) * 0.1); - --navbar-padding-y: calc(var(--spacer) * 0.2); - - /* nav-item */ - --nav-item-padding-x: var(--spacer-1); - --nav-item-padding-y: calc(var(--spacer) * 0.2); - --nav-item-color: var(--light-color); - --nav-item-color-hover: var(--white-color); - --nav-item-color-disabled: var(--secondary-color); - - --navitem-color: var(--dark-color); - --navitem-color-hover: var(--dark-color); - --navitem-color-active: var(--dark-color); - --navitem-color-disabled: var(--light-color); - --navitem-background-color: inherit; - --navitem-background-color-hover: inherit; - --navitem-background-color-active: inherit; - --navitem-background-color-disabled: inherit; - --navitem-font-weight: var(--font-weight-normal); - --navitem-font-weight-active: var(--font-weight-bold); - --navitem-font-weight-hover: var(--font-weight-bold); - --navitem-font-weight-disabled: var(--font-weight-light); - --navitem-padding: calc(var(--spacer) * 0.5) var(--spacer); - /* forms */ --form-input-background-color: none; --form-input-color: var(--dark-color); diff --git a/src/core/Event.js b/src/core/Event.js index 2871ddd..34451b5 100644 --- a/src/core/Event.js +++ b/src/core/Event.js @@ -1,10 +1,14 @@ -// Define events used by web components -const EVENT_CLICK = 'js-click'; -const EVENT_HOVER = 'js-hover'; -const EVENT_START = 'js-start'; -const EVENT_DONE = 'js-done'; -const EVENT_ERROR = 'js-error'; - -export default { - EVENT_CLICK, EVENT_HOVER, EVENT_START, EVENT_DONE, EVENT_ERROR, +/** + * Define events used by web components + * @readonly + * @enum {String} + */ +export const EventType = { + CHANGE: 'wc-change', + CLICK: 'wc-click', + EVENT_CLICK: 'ws-click', + EVENT_HOVER: 'ws-hover', + EVENT_START: 'ws-start', + EVENT_DONE: 'ws-done', + EVENT_ERROR: 'ws-error', }; diff --git a/src/core/Model.js b/src/core/Model.js index 5e50254..88b3595 100644 --- a/src/core/Model.js +++ b/src/core/Model.js @@ -3,11 +3,8 @@ * class for all data models. * @class */ +// eslint-disable-next-line import/prefer-default-export export class Model { - constructor() { - // Initialize properties - } - static get properties() { return {}; } diff --git a/src/core/Provider.js b/src/core/Provider.js index 5307607..57308a9 100644 --- a/src/core/Provider.js +++ b/src/core/Provider.js @@ -1,4 +1,4 @@ -import Event from './Event'; +import { Event } from './Event'; /** * Provider of data. In general, add provider to the controller using diff --git a/src/css/document.css b/src/document.css similarity index 83% rename from src/css/document.css rename to src/document.css index 63466e6..dc134e5 100644 --- a/src/css/document.css +++ b/src/document.css @@ -1,5 +1,5 @@ -@import url(../assets/font/opensans.css); +@import url(./assets/font/opensans.css); body { margin: 0; @@ -46,7 +46,7 @@ h6 { font-size: var(--h6-font-size); } -/* HR */ +/* Horizontal Ruler */ hr { margin: var(--spacer-1) 0; height: 1px; @@ -83,3 +83,15 @@ div.container { margin: var(--spacer-6) !important; } +/* text transformation */ +.text-transform-uppercase { + text-transform: uppercase; +} + +.text-transform-lowercase { + text-transform: lowercase; +} + +.text-transform-capitalize { + text-transform: capitalize; +} diff --git a/src/esbuild.js b/src/esbuild.js index 445eac8..9fd9f25 100644 --- a/src/esbuild.js +++ b/src/esbuild.js @@ -1,5 +1,5 @@ - +/* Code to reload in the esbuild serve development environment */ window.addEventListener('load', () => { - console.log('esbuild'); - new EventSource('/esbuild').addEventListener('change', () => location.reload()); + // eslint-disable-next-line no-restricted-globals + new EventSource('/esbuild').addEventListener('change', () => location.reload()); }); diff --git a/src/form/FormControlElement.js b/src/form/FormControlElement.js new file mode 100644 index 0000000..d622c11 --- /dev/null +++ b/src/form/FormControlElement.js @@ -0,0 +1,156 @@ +import { LitElement, html, nothing, css } from 'lit'; + +/** + * @class FormControlElement + * + * This class is used as a base class for all form elements + * + * @property {String} name - The name of the switch + * @property {String} value - The value of the control + * @property {Boolean} disabled - Whether the form control is disabled + * @property {Boolean} required - Whether the form control is required before submitting + * @property {Boolean} autocomplete - Whether the form control allows autocomplete + * + * @example + * Power + */ +export class FormControlElement extends LitElement { + static get localName() { + return 'wc-form-control'; + } + + constructor() { + super(); + + // Attach with the form + this.internals = this.attachInternals(); + + // Default properties + this.name = ''; + this.value = ''; + this.disabled = false; + this.required = false; + this.autocomplete = false; + } + + static get formAssociated() { + return true; + } + + static get properties() { + return { + name: { type: String }, + value: { type: String }, + disabled: { type: Boolean }, + required: { type: Boolean }, + autocomplete: { type: Boolean }, + }; + } + + static get styles() { + return css` + :host { + display: inline-block; + flex-shrink: 0; + padding: var(--form-control-padding-y) var(--form-control-padding-x); + } + label { + cursor: pointer; + user-select: none; + } + label.switch { + & input { + width: var(--form-switch-width); + height: var(--form-switch-height); + border-radius: var(--form-switch-border-radius); + vertical-align: middle; + appearance: none; + transition: background-position var(--form-switch-transition) ease-in-out; + background-image: var(--form-switch-background-image); + background-color: var(--form-switch-background-color); + background-repeat: no-repeat; + background-position: left center; + background-size: contain; + } + & input:checked { + background-position: right center; + } + } + + label.select { + cursor: inherit; + + & select { + width: 100%; + cursor: pointer; + font-family: inherit; + appearance: none; + padding: var(--form-select-padding-y) var(--form-select-padding-x); + background-image: var(--form-select-background-image); + background-color: var(--form-select-background-color); + background-repeat: no-repeat; + background-position: right center; + background-size: contain; + border: 1px solid var(--form-select-border-color); + border-radius: var(--form-select-border-radius); + + &:focus { + outline: 0; + } + } + } + `; + } + + render() { + return html` + + `; + } + + // Return classes for the form control + get classes() { + const classes = []; + if (this.disabled) { + classes.push('disabled'); + } + if (this.required) { + classes.push('required'); + } + return classes; + } + + // Form control properties + get form() { return this.internals ? this.internals.form : null; } + + // Form control properties + get type() { return this.localName; } + + // Form control properties + get validity() { return this.internals ? this.internals.validity : null; } + + // Form control properties + get validationMessage() { return this.internals ? this.internals.validationMessage : null; } + + // Form control properties + get willValidate() { return this.internals ? this.internals.willValidate : null; } + + // Event hander for input event to update the value + onInput(event) { + if (!this.disabled) { + this.value = event.target.value; + this.internals.setFormValue(this.value); + return true; + } + return false; + } +} diff --git a/src/form/FormSelectElement.js b/src/form/FormSelectElement.js new file mode 100644 index 0000000..337db3b --- /dev/null +++ b/src/form/FormSelectElement.js @@ -0,0 +1,70 @@ +import { html, css, nothing } from 'lit'; +import { FormControlElement } from './FormControlElement'; +import { EventType } from '../core/Event'; +/** + * @class FormSelectElement + * + * This class is used to create a selection from many options + * + * @property {Array} options - The list of options to select from + * @property {Boolean} multiple - Whether multiple options can be selected + * + * @example + * Colour + */ +export class FormSelectElement extends FormControlElement { + static get localName() { + return 'wc-form-select'; + } + + constructor() { + super(); + + // Default properties + this.options = []; + this.multiple = false; + } + + static get properties() { + return { + options: { type: Array }, + multiple: { type: Boolean }, + }; + } + + render() { + return html` + + `; + } + + // Return classes for the switch control + get classes() { + const classes = super.classes; + classes.push('select'); + return classes; + } + + // Change the selected state when the input is changed + onInput(event) { + if (super.onInput(event)) { + this.dispatchEvent(new CustomEvent(EventType.CHANGE, { + bubbles: true, + composed: true, + detail: this.value, + })); + } + } +} diff --git a/src/form/FormSwitchElement.js b/src/form/FormSwitchElement.js new file mode 100644 index 0000000..7966bd4 --- /dev/null +++ b/src/form/FormSwitchElement.js @@ -0,0 +1,66 @@ +import { html, nothing } from 'lit'; +import { FormControlElement } from './FormControlElement'; +import { EventType } from '../core/Event'; + +/** + * @class FormSwitchElement + * + * This class is used to create a binary switch + * + * @property {Boolean} selected - Whether the switch is checked + * + * @example + * Power + */ +export class FormSwitchElement extends FormControlElement { + static get localName() { + return 'wc-form-switch'; + } + + constructor() { + super(); + + // Default properties + this.selected = false; + } + + static get properties() { + return { + selected: { type: Boolean }, + }; + } + + render() { + return html` + + `; + } + + // Return classes for the switch control + get classes() { + const classes = super.classes; + classes.push('switch'); + return classes; + } + + // Change the selected state when the input is changed + onInput(event) { + if (super.onInput(event)) { + this.selected = event.target.checked; + this.dispatchEvent(new CustomEvent(EventType.CHANGE, { + bubbles: true, + composed: true, + detail: this.name || this.textContent.trim(), + })); + } + } +} diff --git a/src/form/form.css b/src/form/form.css new file mode 100644 index 0000000..0218456 --- /dev/null +++ b/src/form/form.css @@ -0,0 +1,21 @@ +:root { + /* General controls */ + --form-control-padding-x: 0.5em; + --form-control-padding-y: 0.1em; + + /* Switches */ + --form-switch-width: 2.5em; + --form-switch-height: 1.5em; + --form-switch-color: var(--primary-color); /* TODO: Does not work with the SVG yet */ + --form-switch-background-color: var(--control-color); + --form-switch-border-radius: var(--form-switch-width); + --form-switch-background-image: url("data:image/svg+xml,"); + --form-switch-transition: 300ms; + + /* Selects */ + --form-select-padding-x: 0.5em; + --form-select-padding-y: 0.1em; + --form-select-border-color: var(--control-color); + --form-select-background-color: var(--control-color); + --form-select-background-image: url("data:image/svg+xml,"); +} diff --git a/src/form/form.js b/src/form/form.js new file mode 100644 index 0000000..9144133 --- /dev/null +++ b/src/form/form.js @@ -0,0 +1,10 @@ +// CSS +import './form.css'; + +// Classes +import { FormSwitchElement } from './FormSwitchElement'; +import { FormSelectElement } from './FormSelectElement'; + +// Web Components +customElements.define(FormSwitchElement.localName, FormSwitchElement); // wc-form-switch +customElements.define(FormSelectElement.localName, FormSelectElement); // wc-form-select diff --git a/src/index.html b/src/index.html index a161b66..176bfda 100644 --- a/src/index.html +++ b/src/index.html @@ -10,196 +10,153 @@ - - menu - File - Edit - Selection - View - Disabled - help - - -
    -

    Buttons

    - -

    Individual

    - Save - Retry - -

    Group

    - - Save - Retry - Cancel - -
    -
    - -
    -

    Badge

    - -

    Individual

    - default - primary - secondary - success - warning - error - light - dark -
    - -

    Group

    - - default - primary - secondary - success - warning - error - light - dark - - -
    -
    - -
    -

    Close Button

    -
    - -

    Here is some content within a container

    -
    -
    -
    - -
    -

    Icon

    - DEFAULT - SMALL - MEDIUM - LARGE - XLARGE -
    - -
    - -
    -

    Modal

    - Pop-up - - -

    Here is some content within a modal

    -
    - - -
    -

    Here is some content within a sidemodal

    -
    -
    -
    -
    - -
    -
    -

    Form

    - - - Name - - Email Address - - Date of Birth - - - Address - - - - - - - Save - Cancel - - - - -
    -
    -
    - -
    -

    Row Layout

    - -

    Two Columns

    - - - Left Hand Side - - - Right Hand Side - - -
    - -

    Three Columns

    - - - Left - - - Middle - - - Right - - -
    - -

    Four Columns

    - - - Col 1 - - - Col 2 - - - Col 3 - - - Col 4 - - -
    - -

    Six Columns

    - - - Col 1 - - - Col 2 - - - Col 3 - - - Col 4 - - - Col 5 - - - Col 6 - - -
    - -
    + + + + + Home + + + Dashboard + + + Orders + + + Products + + + Customers + + + Help + + + + + Dark Theme + + + Vertical + + + EndNav + + + + + + + +

    Card

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in + voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    + + Toggle + +

    Card

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in + voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    + Popover content +
    +
    + +

    Card

    + Background + Colour + + Width + +
    + + + Close + Help + +

    Header

    +

    Subtitle

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in + voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    + +
    + + + Close + + The operation was successful + + + + Close + + Warning: Something happened + + + + Close + + Error: An internal error occurred + +
    +
    + END +
    - + \ No newline at end of file diff --git a/src/index.html-old b/src/index.html-old new file mode 100644 index 0000000..e64e025 --- /dev/null +++ b/src/index.html-old @@ -0,0 +1,335 @@ + + + + + + + Storybook + + + + + + + + + menu + File + Edit + Selection + View + Disabled + + help + + + + + + Home + + + Dashboard + + + Orders + + + Products + + + Customers + + + + Help + + + + +
    + + Tables + Buttons + Badges + Icons + Code + + Disabled + + + +
    +

    Tables

    + Table of Fruit + +
    +
    + +
    +

    Buttons

    + +

    Individual

    + Save + Retry + +

    Group

    + + Save + Retry + Cancel + +
    +
    + +
    +

    Badge

    + +

    Individual

    + default + primary + secondary + success + warning + error + light + dark +
    + +

    Group

    + + default + primary + secondary + success + warning + error + light + dark + +
    +
    + +
    +

    Icon

    + DEFAULT + SMALL + MEDIUM + LARGE + XLARGE +
    +
    + +
    +

    Code

    +
    + + console.log('hello'); + console.log('hello'); + console.log('hello'); + + /* this is some more javascript */ + console.log('hello'); + +
    +
    +
    +
    +
    + + +
    +

    Close Button

    +
    + +

    Here is some content within a container

    +
    +
    +
    + + +
    + +
    +

    Modal

    + Pop-up + + +

    Here is some content within a modal

    +
    + + + + + Home + + + Dashboard + + + Orders + + + Products + + + Customers + + + + Help + + + +
    +
    + +
    +
    +

    Form

    + + + Name + + Email + Address + + Date of + Birth + + + Address + + + + + + + Save + Cancel + + + + +
    +
    +
    + +
    +

    Row Layout

    + +

    Two Columns

    + + + Left Hand Side + + + Right Hand Side + + +
    + +

    Three Columns

    + + + Left + + + Middle + + + Right + + +
    + +

    Four Columns

    + + + Col 1 + + + Col 2 + + + Col 3 + + + Col 4 + + +
    + +

    Six Columns

    + + + Col 1 + + + Col 2 + + + Col 3 + + + Col 4 + + + Col 5 + + + Col 6 + + +
    + +
    +
    + + + + Home + + + Dashboard + + + Orders + + + Products + + + Customers + + + + Help + + + + + + + menu + File + Edit + Selection + View + Disabled + help + + + +
    + + + \ No newline at end of file diff --git a/src/index.js b/src/index.js index 1e59dca..fb2ccf6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,61 +1,16 @@ - // This file defines all the styles and elements used for the web components -// Core -import './core/Model'; -import './core/Provider'; - -// Buttons -import './component/button/ButtonElement'; -import './component/button/ButtonGroupElement'; -import './component/button/CloseButtonElement'; - -// Badges -import { BadgeElement } from './component/badge/BadgeElement'; -import './component/badge/BadgeGroupElement'; - -customElements.define(BadgeElement.localName, BadgeElement); // wc-badge - -// Icons -import './component/icon/IconElement'; - -// Navigation -import './component/nav/NavBarElement'; -import './component/nav/NavItemElement'; +// Global styles +import './core.css'; +import './document.css'; -// Modal Elements -import './component/modal/ModalElement'; -import './component/modal/SideModalElement'; - -// Form Elements -import './component/form/FormElement'; -import './component/form/FormTextElement'; -import './component/form/FormDateElement'; - -// Layout Elements -import { SpacerElement } from './component/layout/SpacerElement'; -import { RowElement, RowCell1Element, RowCell2Element,RowCell3Element,RowCell4Element,RowCell5Element,RowCell6Element,RowCell7Element,RowCell8Element,RowCell9Element,RowCell10Element,RowCell11Element,RowCell12Element } from './component/layout/RowElement'; - -customElements.define(SpacerElement.localName, SpacerElement); // wc-spacer -customElements.define(RowElement.localName, RowElement); // wc-row -customElements.define(RowCell1Element.localName, RowCell1Element); // wc-row-1 -customElements.define(RowCell2Element.localName, RowCell2Element); // wc-row-2 -customElements.define(RowCell3Element.localName, RowCell3Element); // wc-row-3 -customElements.define(RowCell4Element.localName, RowCell4Element); // wc-row-4 -customElements.define(RowCell5Element.localName, RowCell5Element); // wc-row-5 -customElements.define(RowCell6Element.localName, RowCell6Element); // wc-row-6 -customElements.define(RowCell7Element.localName, RowCell7Element); // wc-row-7 -customElements.define(RowCell8Element.localName, RowCell8Element); // wc-row-8 -customElements.define(RowCell9Element.localName, RowCell9Element); // wc-row-9 -customElements.define(RowCell10Element.localName, RowCell10Element); // wc-row-10 -customElements.define(RowCell11Element.localName, RowCell11Element); // wc-row-11 -customElements.define(RowCell12Element.localName, RowCell12Element); // wc-row-12 - -// CSS -import './css/core.css'; -import './css/document.css'; +// Components +import './app/app'; +import './layout/layout'; +import './form/form'; +import './button/button'; +import './popup/popup'; // Other -import './esbuild.js'; -import './test.js'; - +import './esbuild'; +import './load'; diff --git a/src/layout/CodeElement.css.js b/src/layout/CodeElement.css.js new file mode 100644 index 0000000..9a73d13 --- /dev/null +++ b/src/layout/CodeElement.css.js @@ -0,0 +1,118 @@ +import { css } from 'lit'; + +export const style = css` +.hljs { + color: #24292e; + background: #ffffff; + } + + .hljs-doctag, + .hljs-keyword, + .hljs-meta .hljs-keyword, + .hljs-template-tag, + .hljs-template-variable, + .hljs-type, + .hljs-variable.language_ { + /* prettylights-syntax-keyword */ + color: #d73a49; + } + + .hljs-title, + .hljs-title.class_, + .hljs-title.class_.inherited__, + .hljs-title.function_ { + /* prettylights-syntax-entity */ + color: #6f42c1; + } + + .hljs-attr, + .hljs-attribute, + .hljs-literal, + .hljs-meta, + .hljs-number, + .hljs-operator, + .hljs-variable, + .hljs-selector-attr, + .hljs-selector-class, + .hljs-selector-id { + /* prettylights-syntax-constant */ + color: #005cc5; + } + + .hljs-regexp, + .hljs-string, + .hljs-meta .hljs-string { + /* prettylights-syntax-string */ + color: #032f62; + } + + .hljs-built_in, + .hljs-symbol { + /* prettylights-syntax-variable */ + color: #e36209; + } + + .hljs-comment, + .hljs-code, + .hljs-formula { + /* prettylights-syntax-comment */ + color: #6a737d; + } + + .hljs-name, + .hljs-quote, + .hljs-selector-tag, + .hljs-selector-pseudo { + /* prettylights-syntax-entity-tag */ + color: #22863a; + } + + .hljs-subst { + /* prettylights-syntax-storage-modifier-import */ + color: #24292e; + } + + .hljs-section { + /* prettylights-syntax-markup-heading */ + color: #005cc5; + font-weight: bold; + } + + .hljs-bullet { + /* prettylights-syntax-markup-list */ + color: #735c0f; + } + + .hljs-emphasis { + /* prettylights-syntax-markup-italic */ + color: #24292e; + font-style: italic; + } + + .hljs-strong { + /* prettylights-syntax-markup-bold */ + color: #24292e; + font-weight: bold; + } + + .hljs-addition { + /* prettylights-syntax-markup-inserted */ + color: #22863a; + background-color: #f0fff4; + } + + .hljs-deletion { + /* prettylights-syntax-markup-deleted */ + color: #b31d28; + background-color: #ffeef0; + } + + .hljs-char.escape_, + .hljs-link, + .hljs-params, + .hljs-property, + .hljs-punctuation, + .hljs-tag { + /* purposely ignored */ + } + `; diff --git a/src/layout/CodeElement.js b/src/layout/CodeElement.js new file mode 100644 index 0000000..ffbd342 --- /dev/null +++ b/src/layout/CodeElement.js @@ -0,0 +1,68 @@ +import { + LitElement, html, css, nothing, +} from 'lit'; +import { style } from './CodeElement.css.js'; +import hljs from 'highlight.js/lib/core'; +import javascript from 'highlight.js/lib/languages/javascript'; + +/* register only certain languages */ +hljs.registerLanguage('javascript', javascript); + +/** + * CodeElement + * This class is used to create a code block element, which + * displays code according to the language specified. + * + * @example + * + * console.log('Hello, World!'); + * + */ +export class CodeElement extends LitElement { + static get localName() { + return 'wc-code'; + } + + constructor() { + super(); + this.code = 'javascript'; + } + + static get properties() { + return { + /** + * The code language, for syntax highlighting. + * @type {String} + * @default javascript + * @memberof CodeElement + */ + code: { type: String }, + }; + } + + static get styles() { + return [css` + pre { + white-space: pre-line; + } + `, style]; + } + + get className() { + const classes = []; + if (this.code) { + classes.push(this.code); + } + return classes.join(' '); + } + + render() { + return html` +
    
    +    `;
    +  }
    +
    +  firstUpdated() {
    +    this.shadowRoot.querySelector('pre').innerHTML = hljs.highlight(this.innerHTML, { language: this.code }).value;
    +  }
    +}
    diff --git a/src/layout/layout.js b/src/layout/layout.js
    new file mode 100644
    index 0000000..6b459e5
    --- /dev/null
    +++ b/src/layout/layout.js
    @@ -0,0 +1,6 @@
    +
    +// Web Components
    +import { CodeElement } from './CodeElement';
    +
    +// Web Components
    +customElements.define(CodeElement.localName, CodeElement); // wc-code
    diff --git a/src/load.js b/src/load.js
    new file mode 100644
    index 0000000..9638536
    --- /dev/null
    +++ b/src/load.js
    @@ -0,0 +1,4 @@
    +window.addEventListener('load', () => {
    +  // Initialize the application here
    +  console.log('App loaded');
    +});
    diff --git a/src/old/component/badge/BadgeElement.js b/src/old/component/badge/BadgeElement.js
    new file mode 100644
    index 0000000..5d80e34
    --- /dev/null
    +++ b/src/old/component/badge/BadgeElement.js
    @@ -0,0 +1,129 @@
    +import {
    +  LitElement, html, css, nothing,
    +} from 'lit';
    +
    +/**
    +* BadgeElement - A badge element class that can be used to display a badge with text or icon.
    +*
    +* @example
    +* New
    +* 
    +*/
    +export class BadgeElement extends LitElement {
    +  static get localName() {
    +    return 'wc-badge';
    +  }
    +
    +  /**
    +    * @constructor
    +    * Badge element constructor. Sets the default properties, textTransform and backgroundColor
    +    * to none and primary respectively.
    +    */
    +  constructor() {
    +    super();
    +
    +    // Default properties
    +    this.textTransform = 'none';
    +    this.backgroundColor = 'primary';
    +  }
    +
    +  static get properties() {
    +    return {
    +      /**
    +       * @property {String} textTransform
    +       * @memberof BadgeElement
    +       *
    +       * The text transform, one of: none, uppercase, lowercase, capitalize
    +       */
    +      textTransform: { type: String },
    +
    +      /**
    +       * @property {String} backgroundColor
    +       * @memberof BadgeElement
    +       *
    +       * The badge background color. One of the following: primary, secondary, success,
    +       * warning, danger, light, dark
    +       */
    +      backgroundColor: { type: String },
    +    };
    +  }
    +
    +  static get styles() {
    +    return css`
    +    span {
    +      display: inline-block;
    +      background-color: var(--badge-background-color); 
    +      color: var(--badge-color);
    +      padding: var(--badge-padding-y) var(--badge-padding-x);
    +      font-size: var(--badge-font-size);
    +      font-weight:  var(--badge-font-weight);
    +      border-top-left-radius: var(--badge-border-radius-left);
    +      border-bottom-left-radius: var(--badge-border-radius-left);
    +      border-top-right-radius: var(--badge-border-radius-right);
    +      border-bottom-right-radius: var(--badge-border-radius-right);      
    +      line-height: 1;
    +      text-align: center;
    +      white-space: nowrap;
    +      vertical-align: baseline;
    +      cursor: default;
    +    }
    +    .text-transform-capitalize {
    +      text-transform: capitalize;
    +    }
    +    .text-transform-uppercase {
    +      text-transform: uppercase;
    +    }
    +    .text-transform-lowercase {
    +      text-transform: lowercase;
    +    }
    +    .text-transform-none {
    +      text-transform: none;
    +    }
    +    .bg-color-primary {
    +      background-color: var(--primary-color);
    +      color: var(--light-color);
    +    }
    +    .bg-color-secondary {
    +      background-color: var(--secondary-color);
    +      color: var(--dark-color);
    +    }
    +    .bg-color-success {
    +      background-color: var(--success-color);
    +      color: var(--light-color);
    +    }
    +    .bg-color-warning {
    +      background-color: var(--warning-color);
    +      color: var(--light-color);
    +    }
    +    .bg-color-error {
    +      background-color: var(--error-color);
    +      color: var(--light-color);
    +    }
    +    .bg-color-light {
    +      background-color: var(--light-color);
    +      color: var(--dark-color);
    +    }
    +    .bg-color-dark {
    +      background-color: var(--dark-color);
    +      color: var(--light-color);
    +    }  
    +    `;
    +  }
    +
    +  get className() {
    +    let className = '';
    +    if (this.textTransform) {
    +      className += `text-transform-${this.textTransform}`;
    +    }
    +    if (this.backgroundColor) {
    +      className += ` bg-color-${this.backgroundColor}`;
    +    }
    +    return className;
    +  }
    +
    +  render() {
    +    return html`
    +      
    +    `;
    +  }
    +}
    diff --git a/src/old/component/badge/BadgeGroupElement.js b/src/old/component/badge/BadgeGroupElement.js
    new file mode 100644
    index 0000000..b28e847
    --- /dev/null
    +++ b/src/old/component/badge/BadgeGroupElement.js
    @@ -0,0 +1,41 @@
    +import { LitElement, html, css } from 'lit';
    +
    +/**
    +  *  wc-badge-group is a group of badges
    +  *
    +  * @example
    +  * 
    +  *   New
    +  *   
    +  * 
    +  */
    +// eslint-disable-next-line import/prefer-default-export
    +export class BadgeGroupElement extends LitElement {
    +  static get localName() {
    +    return 'wc-badge-group';
    +  }
    +
    +  static get styles() {
    +    return css`
    +    span {
    +      display: flex;
    +      --badge-border-radius-left: 0;
    +      --badge-border-radius-right: 0;
    +    }
    +    ::slotted(:not(:last-child)) {
    +      border-right: 1px solid var(--badge-group-divider-color);
    +    }
    +    ::slotted(*:first-child) {
    +      --badge-border-radius-left: var(--badge-border-radius);
    +    }
    +    ::slotted(*:last-child) {
    +      --badge-border-radius-right: var(--badge-border-radius);
    +    }
    +    `;
    +  }
    +
    +  // eslint-disable-next-line class-methods-use-this
    +  render() {
    +    return html``;
    +  }
    +}
    diff --git a/src/old/component/badge/badge.css b/src/old/component/badge/badge.css
    new file mode 100644
    index 0000000..38a360e
    --- /dev/null
    +++ b/src/old/component/badge/badge.css
    @@ -0,0 +1,12 @@
    +
    +/* badge */
    +:root {
    +    --badge-padding-x: 0.35em;
    +    --badge-padding-y: 0.35em;
    +    --badge-font-size: var(--font-size-small);
    +    --badge-font-weight: var(--font-weight-normal);
    +    --badge-border-radius: var(--border-radius);
    +    --badge-border-radius-left: var(--badge-border-radius);
    +    --badge-border-radius-right: var(--badge-border-radius);
    +    --badge-group-divider-color: var(--light-color);
    +}
    diff --git a/src/old/component/badge/badge.js b/src/old/component/badge/badge.js
    new file mode 100644
    index 0000000..93a0b0e
    --- /dev/null
    +++ b/src/old/component/badge/badge.js
    @@ -0,0 +1,8 @@
    +// Import assets
    +import './badge.css';
    +import { BadgeElement } from './BadgeElement';
    +import { BadgeGroupElement } from './BadgeGroupElement';
    +
    +// Define the custom elements
    +customElements.define(BadgeElement.localName, BadgeElement); // wc-badge
    +customElements.define(BadgeGroupElement.localName, BadgeGroupElement); // wc-badge-group
    diff --git a/src/component/button/ButtonElement.js b/src/old/component/button/ButtonElement.js
    similarity index 99%
    rename from src/component/button/ButtonElement.js
    rename to src/old/component/button/ButtonElement.js
    index cedcea0..efe2946 100644
    --- a/src/component/button/ButtonElement.js
    +++ b/src/old/component/button/ButtonElement.js
    @@ -1,6 +1,6 @@
     
     import { LitElement, html, css } from 'lit';
    -import Event from '../../core/Event';
    +import { Event } from '../../core/Event';
     
     /**
      * A button element
    diff --git a/src/component/button/ButtonGroupElement.js b/src/old/component/button/ButtonGroupElement.js
    similarity index 100%
    rename from src/component/button/ButtonGroupElement.js
    rename to src/old/component/button/ButtonGroupElement.js
    diff --git a/src/component/button/CloseButtonElement.js b/src/old/component/button/CloseButtonElement.js
    similarity index 58%
    rename from src/component/button/CloseButtonElement.js
    rename to src/old/component/button/CloseButtonElement.js
    index 36a85bc..a17d8b9 100644
    --- a/src/component/button/CloseButtonElement.js
    +++ b/src/old/component/button/CloseButtonElement.js
    @@ -1,34 +1,35 @@
    -
     import { LitElement, html, css } from 'lit';
    -import Event from '../../core/Event';
    +import { Event } from '../../core/Event';
     
     /**
      * CloseButtonElement
      */
     export class CloseButtonElement extends LitElement {
    -    constructor() {
    -        super();
    -        // Default properties
    -        this.name = 'wc-close';
    -        this.disabled = false;
    -    }
    -    static get properties() {
    -        return {
    -            /**
    -             * Name of the button to use when firing the EVENT_CLICK event
    -             * @type {String}
    -             */
    -            name: { type: String },
    +  constructor() {
    +    super();
    +    // Default properties
    +    this.name = 'wc-close';
    +    this.disabled = false;
    +  }
    +
    +  static get properties() {
    +    return {
    +      /**
    +       * Name of the button to use when firing the EVENT_CLICK event
    +       * @type {String}
    +       */
    +      name: { type: String },
    +
    +      /**
    +       * Whether the button is disabled
    +       * @type {Boolean}
    +       */
    +      disabled: { type: Boolean },
    +    };
    +  }
     
    -            /**
    -             * Whether the button is disabled
    -             * @type {Boolean}
    -             */
    -            disabled: { type: Boolean },
    -        };
    -    }
    -    static get styles() {
    -        return css`
    +  static get styles() {
    +    return css`
                 button {
                     position: absolute;
                     top: 0;
    @@ -56,23 +57,23 @@ export class CloseButtonElement extends LitElement {
                     color: var(--button-close-color-disabled); 
                 }
             `;
    -    }
    -    render() {
    -        return html`
    +  }
    +
    +  render() {
    +    return html`
                 
             `;
    -    }
    -    onClick() {
    -        this.dispatchEvent(new CustomEvent(
    -            Event.EVENT_CLICK, { 
    -                bubbles: true,
    -                composed: true,
    -                detail: this.name 
    -            },
    -        ));
    -    }
    +  }
    +
    +  onClick() {
    +    this.dispatchEvent(new CustomEvent(Event.EVENT_CLICK, {
    +      bubbles: true,
    +      composed: true,
    +      detail: this.name,
    +    }));
    +  }
     }
     
     customElements.define('wc-close', CloseButtonElement);
    diff --git a/src/component/form/FormDateElement.js b/src/old/component/form/FormDateElement.js
    similarity index 100%
    rename from src/component/form/FormDateElement.js
    rename to src/old/component/form/FormDateElement.js
    diff --git a/src/component/form/FormElement.js b/src/old/component/form/FormElement.js
    similarity index 100%
    rename from src/component/form/FormElement.js
    rename to src/old/component/form/FormElement.js
    diff --git a/src/component/form/FormElement.js-OLD b/src/old/component/form/FormElement.js-OLD
    similarity index 100%
    rename from src/component/form/FormElement.js-OLD
    rename to src/old/component/form/FormElement.js-OLD
    diff --git a/src/component/form/FormPasswordElement.js-OLD b/src/old/component/form/FormPasswordElement.js-OLD
    similarity index 100%
    rename from src/component/form/FormPasswordElement.js-OLD
    rename to src/old/component/form/FormPasswordElement.js-OLD
    diff --git a/src/component/form/FormTextElement.js b/src/old/component/form/FormTextElement.js
    similarity index 100%
    rename from src/component/form/FormTextElement.js
    rename to src/old/component/form/FormTextElement.js
    diff --git a/src/component/icon/IconElement.js b/src/old/component/icon/IconElement.js
    similarity index 94%
    rename from src/component/icon/IconElement.js
    rename to src/old/component/icon/IconElement.js
    index 605ea63..205515d 100644
    --- a/src/component/icon/IconElement.js
    +++ b/src/old/component/icon/IconElement.js
    @@ -23,6 +23,10 @@ export class IconElement extends LitElement {
     
         static get styles() {
             return css`
    +        :host {
    +            display: inline-block;
    +            vertical-align: middle;
    +        }
             .size-default {
                 position: relative;
                 width: var(--icon-size-default);
    diff --git a/src/component/layout/RowElement.js b/src/old/component/layout/RowElement.js
    similarity index 100%
    rename from src/component/layout/RowElement.js
    rename to src/old/component/layout/RowElement.js
    diff --git a/src/component/layout/SpacerElement.js b/src/old/component/layout/SpacerElement.js
    similarity index 100%
    rename from src/component/layout/SpacerElement.js
    rename to src/old/component/layout/SpacerElement.js
    diff --git a/src/component/modal/ModalElement.js b/src/old/component/modal/ModalElement.js
    similarity index 100%
    rename from src/component/modal/ModalElement.js
    rename to src/old/component/modal/ModalElement.js
    diff --git a/src/component/modal/SideModalElement.js b/src/old/component/modal/SideModalElement.js
    similarity index 100%
    rename from src/component/modal/SideModalElement.js
    rename to src/old/component/modal/SideModalElement.js
    diff --git a/src/old/component/nav/CanvasElement.js b/src/old/component/nav/CanvasElement.js
    new file mode 100644
    index 0000000..2a12ae7
    --- /dev/null
    +++ b/src/old/component/nav/CanvasElement.js
    @@ -0,0 +1,46 @@
    +import { LitElement, html, css } from 'lit';
    +
    +/**
    +* CanvasElement
    +* This class is used to create a sidebar left/right element that can be used to 
    +* display a sidebar either on the left or right side of the screen, with content
    +* in the middle. The sidebars can be used to display navigation links, icons, 
    +* or any other content, and can be customized to collapse or expand with animations.
    +*
    +* @example
    +* 
    +*   Left sidebar
    +*   This is the content
    +*   Right sidebar
    +* 
    +*/
    +export class CanvasElement extends LitElement {
    +  static get localName() {
    +    return 'wc-canvas';
    +  }
    +
    +  static get properties() {
    +    return {};
    +  }
    +
    +  static get styles() {
    +    return css`
    +      div.canvas {
    +        width: 100vw;
    +        height: 100vh;
    +        display: flex;
    +        flex-direction: column;
    +        overflow: hidden;
    +      }
    +    `;
    +  }
    +
    +  // eslint-disable-next-line class-methods-use-this
    +  render() {
    +    return html`
    +      
    + +
    + `; + } +} diff --git a/src/old/component/nav/ContentElement.js b/src/old/component/nav/ContentElement.js new file mode 100644 index 0000000..00615c3 --- /dev/null +++ b/src/old/component/nav/ContentElement.js @@ -0,0 +1,42 @@ +import { LitElement, html, css } from 'lit'; + +/** +* ContentElement +* This class is used as a content container within a canvas element +*/ +// eslint-disable-next-line import/prefer-default-export +export class ContentElement extends LitElement { + static get localName() { + return 'wc-content'; + } + + static get properties() { + return { + /** + * Width of the content element. If empty, the content will take up the remaining space + * and if to zero, no content will be displayed + * + * @type {String} + * @default '' + * @memberof ContentElement + */ + width: { type: String }, + }; + } + + static get styles() { + return css` + div { + overflow: scroll; + align-self: stretch; + } + `; + } + + // eslint-disable-next-line class-methods-use-this + render() { + return html` +
    + `; + } +} diff --git a/src/old/component/nav/NavBarElement.js b/src/old/component/nav/NavBarElement.js new file mode 100644 index 0000000..c7054e2 --- /dev/null +++ b/src/old/component/nav/NavBarElement.js @@ -0,0 +1,122 @@ +import { + LitElement, html, css, nothing, +} from 'lit'; + +/** + * NavBarElement + * This class is used to create a navigational bar at the top or bottom of the window. + * + * @example + * + *

    Stick Top

    + * +*/ +export class NavBarElement extends LitElement { + static get localName() { + return 'wc-navbar'; + } + + constructor() { + super(); + // Default properties + this.position = 'top'; + this.backgroundColor = 'primary'; + this.hidden = false; + } + + static get properties() { + return { + /** + * Position of the navbar, one of 'top', 'bottom' + * + * @type {String} + * @default top + * @memberof NavBarElement + */ + position: { type: String }, + + /** + * Background color of the navbar, one of primary, secondary, light, white, dark, black + * + * @type {String} + * @default primary + * @memberof NavBarElement + */ + backgroundColor: { type: String }, + + /** + * If true, bar is hidden and takes up no space + * + * @type {Boolean} + * @default false + * @memberof NavBarElement + */ + hidden: { type: Boolean }, + }; + } + + static get styles() { + return css` + div { + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + div ::slotted(*) { + flex: 1; + } + .hidden { + display: none; + } + .bg-color-primary { + background-color: var(--primary-color); + color: var(--white-color); + border-color: var(--grey-80-color); + } + .bg-color-secondary { + background-color: var(--secondary-color); + color: var(--white-color); + border-color: var(--grey-20-color); + } + .bg-color-light { + background-color: var(--light-color); + color: var(--dark-color); + border-color: var(--grey-20-color); + } + .bg-color-white { + background-color: var(--white-color); + color: var(--black-color); + border-color: var(--grey-20-color); + } + .bg-color-dark { + background-color: var(--dark-color); + color: var(--light-color); + border-color: var(--grey-80-color); + } + .bg-color-black { + background-color: var(--black-color); + color: var(--white-color); + border-color: var(--grey-80-color); + } + `; + } + + get className() { + let className = this.position === 'bottom' ? 'bottom ' : 'top '; + if (this.backgroundColor) { + className += `bg-color-${this.backgroundColor}`; + } + if (this.hidden) { + className += ' hidden'; + } + return className; + } + + // eslint-disable-next-line class-methods-use-this + render() { + return html` +
    + `; + } +} diff --git a/src/old/component/nav/NavElement.js b/src/old/component/nav/NavElement.js new file mode 100644 index 0000000..4c3b7fc --- /dev/null +++ b/src/old/component/nav/NavElement.js @@ -0,0 +1,71 @@ +import { + LitElement, html, css, nothing, +} from 'lit'; + +/** + * NavElement + * This class is used to create a navigational list, either horizontal or vertical. + * + * @example +* +* File +* +*/ +export class NavElement extends LitElement { + static get localName() { + return 'wc-nav'; + } + + static get properties() { + return { + /** + * Whether the navigation is vertical, rather than horizontal + * @type {Boolean} + * @default false + * @memberof NavElement + */ + vertical: { type: Boolean }, + }; + } + + static get styles() { + return css` + ul { + display: flex; + margin: 0; + border: 0; + padding: 0; + list-style: none; + font-size: var(--nav-item-font-size); + font-weight: var(--nav-item-font-weight); + } + ul.horizontal { + flex-direction: row; + } + ul.vertical { + flex-direction: column; + } + ::slotted(wc-nav-item:hover) { + color: var(--light-color); + background-color: var(--primary-color); + } + `; + } + + get className() { + let className = ''; + if (this.vertical) { + className += 'vertical '; + } else { + className += 'horizontal '; + } + return className; + } + + // eslint-disable-next-line class-methods-use-this + render() { + return html` +
    + `; + } +} diff --git a/src/old/component/nav/NavItemElement.js b/src/old/component/nav/NavItemElement.js new file mode 100644 index 0000000..3cf65b0 --- /dev/null +++ b/src/old/component/nav/NavItemElement.js @@ -0,0 +1,68 @@ +import { LitElement, html, css } from 'lit'; +import { Event } from '../core/Event'; + +/** + * NavItemElement + * This class is used to create a navigational item, which responds to hover and click events + * + * @example + * + * File + * + */ +export class NavItemElement extends LitElement { + static get localName() { + return 'wc-nav-item'; + } + + constructor() { + super(); + // Default properties + this.name = ''; + this.disabled = false; + } + + static get properties() { + return { + /** + * Name of the item to use when firing the EVENT_CLICK event + * @type {String} + */ + name: { type: String }, + + /** + * Whether the item is disabled + * @type {Boolean} + */ + disabled: { type: Boolean }, + }; + } + + static get styles() { + return css` + li { + padding: var(--nav-item-padding-y) var(--nav-item-padding-x) var(--nav-item-padding-y) var(--nav-item-padding-x); + cursor: pointer; + } + li.disabled,li.disabled:hover { + cursor: default; + } + `; + } + + render() { + return html` +
  • + `; + } + + onClick() { + if (!this.disabled) { + this.dispatchEvent(new CustomEvent(Event.EVENT_CLICK, { + bubbles: true, + composed: true, + detail: this.name || this.textContent, + })); + } + } +} diff --git a/src/old/component/nav/NavSpacerElement.js b/src/old/component/nav/NavSpacerElement.js new file mode 100644 index 0000000..af533c9 --- /dev/null +++ b/src/old/component/nav/NavSpacerElement.js @@ -0,0 +1,37 @@ +import { LitElement, html, css } from 'lit'; + +/** + * NavSpacerElement + * This class is used to create a spacer element between items above and below (or left and right) + * + * @example + * + * Top + * + * Bottom + * + */ +export class NavSpacerElement extends LitElement { + static get localName() { + return 'wc-nav-spacer'; + } + + static get properties() { + return {}; + } + + static get styles() { + return css` + :host { + flex: 1; + } + `; + } + + // eslint-disable-next-line class-methods-use-this + render() { + return html` +
  • + `; + } +} diff --git a/src/old/component/nav/SideBarElement.js b/src/old/component/nav/SideBarElement.js new file mode 100644 index 0000000..8e2f8ee --- /dev/null +++ b/src/old/component/nav/SideBarElement.js @@ -0,0 +1,82 @@ +import { LitElement, html, css } from 'lit'; + +/** + * SideBarElement + * This class is used to create a left or right hand sidebar within + * a canvas element + */ +export class SideBarElement extends LitElement { + static get localName() { + return 'wc-sidebar'; + } + + constructor() { + super(); + // Default properties + this.backgroundColor = 'primary'; + } + + static get properties() { + return { + /** + * Background color of the sidebar, one of primary, secondary, light, white, dark, black + * + * @type {String} + * @default primary + * @memberof SideBarElement + */ + backgroundColor: { type: String }, + }; + } + + static get styles() { + return css` + .bg-color-primary { + background-color: var(--primary-color); + color: var(--white-color); + border-color: var(--white-color); + } + .bg-color-secondary { + background-color: var(--secondary-color); + color: var(--white-color); + border-color: var(--white-color); + } + .bg-color-light { + background-color: var(--light-color); + color: var(--dark-color); + border-color: var(--dark-color); + } + .bg-color-white { + background-color: var(--white-color); + color: var(--black-color); + border-color: var(--black-color); + } + .bg-color-dark { + background-color: var(--dark-color); + color: var(--light-color); + border-color: var(--light-color); + } + .bg-color-black { + background-color: var(--black-color); + color: var(--white-color); + border-color: var(--black-color); + } + `; + } + + get className() { + let className = ''; + if (this.backgroundColor) { + className += `bg-color-${this.backgroundColor}`; + } + return className; + } + + render() { + return html` +
    + +
    + `; + } +} diff --git a/src/old/component/nav/TabElement.js b/src/old/component/nav/TabElement.js new file mode 100644 index 0000000..09bfd96 --- /dev/null +++ b/src/old/component/nav/TabElement.js @@ -0,0 +1,86 @@ +import { + LitElement, html, css, nothing, +} from 'lit'; + +/** + * TabElement + * This class is used to create a tab, within a tab group, which + * displays a tabbed navigation horizontally. + * + * @example + * + * Tab1 + * Tab2 + * Tab3 + * Tab4 + * + */ +export class TabElement extends LitElement { + static get localName() { + return 'wc-tab'; + } + + static get properties() { + return { + /** + * Whether the tab is selected. Only one tab within a group should + * be selected at a time. + * @type {Boolean} + * @default false + * @memberof TabElement + */ + selected: { type: Boolean }, + + /** + * Whether the tab can be selected. + * @type {Boolean} + * @default false + * @memberof TabElement + */ + disabled: { type: Boolean }, + + /** + * The name of the tab, for click events. + * @type {String} + * @default '' + * @memberof TabElement + */ + name: { type: String }, + }; + } + + static get styles() { + return css` + li { + vertical-align: middle; + } + `; + } + + get className() { + const classes = []; + if (this.selected) { + classes.push('selected'); + } + if (this.disabled) { + classes.push('disabled'); + } + return classes.join(' '); + } + + render() { + return html` +
  • + `; + } + + onClick() { + if (!this.disabled) { + this.dispatchEvent(new CustomEvent(Event.EVENT_CLICK, { + bubbles: true, + composed: true, + detail: this.name || this.textContent, + })); + } + } +} diff --git a/src/old/component/nav/TabGroupElement.js b/src/old/component/nav/TabGroupElement.js new file mode 100644 index 0000000..ee9988e --- /dev/null +++ b/src/old/component/nav/TabGroupElement.js @@ -0,0 +1,168 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { Event } from '../core/Event'; + +/** + * TabGroupElement + * This class is used to create a tab group, which + * displays a tabbed navigation horizontally and allows + * selection of one tab within the group. + * + * @example + * + * Tab1 + * Tab2 + * Tab3 + * Tab4 + * + */ +export class TabGroupElement extends LitElement { + static get localName() { + return 'wc-tab-group'; + } + + constructor() { + super(); + this.backgroundColor = 'light'; + this.hidden = false; + } + + static get properties() { + return { + /** + * Background color of the tab group, one of primary, secondary, light, white, dark, black + * + * @type {String} + * @default light + * @memberof TabGroupElement + */ + backgroundColor: { type: String }, + + /** + * If true, tab group is hidden and takes up no space + * + * @type {Boolean} + * @default false + * @memberof TabGroupElement + */ + hidden: { type: Boolean }, + }; + } + + static get styles() { + return css` + ul { + display: flex; + margin: 0; + border: 0; + padding: 0; + padding-left: 4px; + list-style: none; + } + ::slotted(wc-tab) { + border-bottom: none; + padding: 10px; + margin-right: 4px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + cursor: pointer; + user-select: none; + } + + /* dark theme */ + ul.bg-color-dark { + border-bottom: 1px solid var(--grey-80-color); + } + .bg-color-dark ::slotted(wc-tab) { + background-color: var(--dark-color); + color: var(--light-color); + border-width: 1px; + border-style: solid; + border-bottom: none; + border-color: var(--grey-80-color); + } + .bg-color-dark ::slotted(wc-tab:hover) { + background-color: var(--primary-color); + color: var(--white-color); + } + .bg-color-dark ::slotted(wc-tab:active) { + background-color: var(--primary-color); + color: var(--white-color); + font-weight: var(--font-weight-bold); + } + + /* light theme */ + ul.bg-color-light { + border-bottom: 1px solid var(--grey-40-color); + } + .bg-color-light ::slotted(wc-tab) { + background-color: var(--light-color); + color: var(--dark-color); + border-width: 1px; + border-style: solid; + border-bottom: none; + border-color: var(--grey-40-color); + } + .bg-color-light ::slotted(wc-tab:hover),::slotted(wc-tab:active),::slotted(wc-tab[selected]) { + background-color: var(--primary-color); + color: var(--white-color); + } + .bg-color-light ::slotted(wc-tab:active) { + font-weight: var(--font-weight-bold); + } + .bg-color-light ::slotted(wc-tab[disabled]) { + background-color: inherit; + font-weight: inherit; + color: var(--grey-40-color); + cursor: inherit; + } + `; + } + + get className() { + const classes = []; + if (this.backgroundColor) { + classes.push(`bg-color-${this.backgroundColor}`); + } + if (this.hidden) { + classes.push('hidden'); + } + return classes.join(' '); + } + + render() { + return html` +
    + `; + } + + /** + * Select a tab by name, and deselect all other tabs + * @param {String} name: The name of the tab to select + * @returns The node that was selected, or null + */ + select(target) { + const tabs = this.querySelectorAll('wc-tab'); + const name = target.name || target.textContent; + let selectedNode = null; + tabs.forEach((tab) => { + if (tab.name === name && !tab.selected) { + tab.setAttribute('selected', 'selected'); + selectedNode = tab; + } else if (tab.name !== name && tab.selected) { + tab.removeAttribute('selected'); + } + }); + return selectedNode; + } + + onClick(event) { + const selected = this.select(event.target); + if (selected) { + this.dispatchEvent(new CustomEvent(Event.EVENT_CLICK, { + bubbles: true, + composed: true, + detail: selected.name || selected.textContent, + })); + } + } +} diff --git a/src/old/component/nav/ViewController.js b/src/old/component/nav/ViewController.js new file mode 100644 index 0000000..4d2b799 --- /dev/null +++ b/src/old/component/nav/ViewController.js @@ -0,0 +1,35 @@ +import { LitElement } from 'lit'; +import { Event } from '../core/Event'; + +/** + * ViewController + * This class is used to reflect changes in hosts to keep them + * in sync with each other. + */ +export class ViewController { + #hosts = []; + + constructor(...hosts) { + // Add the hosts + hosts.forEach((host) => { + if (host instanceof LitElement) { + console.log('ViewController', host); + this.#hosts.push(host); + host.addController(this); + + // Listen for events + host.addEventListener(Event.EVENT_CLICK, (event) => { + this.select(event.target, event.detail); + }); + } + }); + } + + select(sender, name) { + this.#hosts.forEach((host) => { + if (host !== sender && host.select) { + host.select(name); + } + }); + } +} diff --git a/src/old/component/nav/ViewElement.js b/src/old/component/nav/ViewElement.js new file mode 100644 index 0000000..1278c5a --- /dev/null +++ b/src/old/component/nav/ViewElement.js @@ -0,0 +1,63 @@ +import { + LitElement, html, css, nothing, +} from 'lit'; + +/** + * ViewElement + * This class is used to create a view, within a view group, which + * displays one view at a time. In general you should use a + * TabGroupElement as a controller for this view group to switch + * between visible views. + * + * @example + * + * View1 + * View2 + * View3 + * View4 + * + */ +export class ViewElement extends LitElement { + static get localName() { + return 'wc-view'; + } + + static get properties() { + return { + /** + * Whether the tab is selected. Only one tab within a group should + * be selected at a time. + * @type {Boolean} + * @default false + * @memberof TabElement + */ + selected: { type: Boolean }, + + /** + * The name of the tab, for click events. + * @type {String} + * @default '' + * @memberof TabElement + */ + name: { type: String }, + }; + } + + static get styles() { + return css``; + } + + get className() { + const classes = []; + if (this.selected) { + classes.push('selected'); + } + return classes.join(' '); + } + + render() { + return html` +
    + `; + } +} diff --git a/src/old/component/nav/ViewGroupElement.js b/src/old/component/nav/ViewGroupElement.js new file mode 100644 index 0000000..2b158ce --- /dev/null +++ b/src/old/component/nav/ViewGroupElement.js @@ -0,0 +1,108 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { Event } from '../core/Event'; + +/** + * ViewGroupElement + * This class is used to create a view, within a view group, which + * displays one view at a time. In general you should use a + * TabGroupElement as a controller for this view group to switch + * between visible views. + * + * @example + * + * View1 + * View2 + * View3 + * View4 + * + */ +export class ViewGroupElement extends LitElement { + static get localName() { + return 'wc-view-group'; + } + + constructor() { + super(); + this.backgroundColor = 'light'; + } + + static get properties() { + return { + /** + * Background color of the view group, one of light or dark + * + * @type {String} + * @default light + * @memberof TabGroupElement + */ + backgroundColor: { type: String }, + }; + } + + static get styles() { + return css` + div { + position: relative; + height: 100vh; + } + ::slotted(wc-view) { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + transition: visibility 0.2s, opacity 0.2s; + } + ::slotted(wc-view[selected]) { + opacity: 1; + flex: 1 0; + } + ::slotted(wc-view:not([selected])) { + opacity: 0; + flex: 0 0; + } + + /* light theme */ + div.bg-color-light { + border-bottom: 1px solid var(--grey-40-color); + border-left: 1px solid var(--grey-40-color); + border-right: 1px solid var(--grey-40-color); + } + `; + } + + // eslint-disable-next-line class-methods-use-this + get className() { + const classes = []; + if (this.backgroundColor) { + classes.push(`bg-color-${this.backgroundColor}`); + } + return classes.join(' '); + } + + render() { + return html` +
    + `; + } + + /** + * Select a view by name, and deselect all other view + * + * @param {String} name: The name of the tab to select + * @returns The node that was selected, or null + */ + select(name) { + const views = this.querySelectorAll('wc-view'); + let selectedNode = null; + views.forEach((view) => { + if (view.name === name && !view.selected) { + view.setAttribute('selected', 'selected'); + selectedNode = view; + } else if (view.name !== name && view.selected) { + view.removeAttribute('selected'); + } + }); + return selectedNode; + } +} diff --git a/src/old/component/nav/nav.css b/src/old/component/nav/nav.css new file mode 100644 index 0000000..11f1ab1 --- /dev/null +++ b/src/old/component/nav/nav.css @@ -0,0 +1,44 @@ + +:root { + /* navbar */ + --navbar-color: var(--light-color); + --navbar-background-color: var(--primary-color); + --navbar-border-edge-color: var(--secondary-color); + --navbar-padding-x: calc(var(--spacer) * 0.1); + --navbar-padding-y: calc(var(--spacer) * 0.2); + + /* nav-item */ + --nav-item-font-size: var(--font-size-normal); + --nav-item-font-weight: var(--font-weight-normal); + --nav-item-padding-x: var(--spacer-1); + --nav-item-padding-y: calc(var(--spacer) * 0.6); + --nav-item-color: var(--white-color); + --nav-item-color-hover: var(--white-color); + --nav-item-color-disabled: var(--secondary-color); + --nav-item-divider-color: none; + + /* sidebar */ + --sidebar-color: var(--light-color); + --sidebar-background-color: var(--dark-color); + --sidebar-border-edge-color: var(--light-color); + --sidebar-padding-x: calc(var(--spacer) * 0); + --sidebar-padding-y: calc(var(--spacer) * 1); +} + +/* + + --navitem-color: var(--dark-color); + --navitem-color-hover: var(--dark-color); + --navitem-color-active: var(--dark-color); + --navitem-color-disabled: var(--light-color); + --navitem-background-color: inherit; + --navitem-background-color-hover: inherit; + --navitem-background-color-active: inherit; + --navitem-background-color-disabled: inherit; + --navitem-font-weight: var(--font-weight-normal); + --navitem-font-weight-active: var(--font-weight-bold); + --navitem-font-weight-hover: var(--font-weight-bold); + --navitem-font-weight-disabled: var(--font-weight-light); + --navitem-padding: calc(var(--spacer) * 0.5) var(--spacer); + +*/ \ No newline at end of file diff --git a/src/old/component/nav/nav.js b/src/old/component/nav/nav.js new file mode 100644 index 0000000..80d3fa3 --- /dev/null +++ b/src/old/component/nav/nav.js @@ -0,0 +1,29 @@ +// CSS +import './nav.css'; + +// Classes +import { NavBarElement } from './NavBarElement'; +import { NavElement } from './NavElement'; +import { NavItemElement } from './NavItemElement'; +import { NavSpacerElement } from './NavSpacerElement'; +import { CanvasElement } from './CanvasElement'; +import { SideBarElement } from './SideBarElement'; +import { ContentElement } from './ContentElement'; +import { TabElement } from './TabElement'; +import { TabGroupElement } from './TabGroupElement'; +import { ViewElement } from './ViewElement'; +import { ViewGroupElement } from './ViewGroupElement'; +import { ViewController } from './ViewController'; + +// Web Components +customElements.define(NavBarElement.localName, NavBarElement); // wc-navbar +customElements.define(NavElement.localName, NavElement); // wc-nav +customElements.define(NavItemElement.localName, NavItemElement); // wc-nav-item +customElements.define(NavSpacerElement.localName, NavSpacerElement); // wc-nav-spacer +customElements.define(CanvasElement.localName, CanvasElement); // wc-canvas +customElements.define(SideBarElement.localName, SideBarElement); // wc-sidebar +customElements.define(ContentElement.localName, ContentElement); // wc-content +customElements.define(TabElement.localName, TabElement); // wc-tab +customElements.define(TabGroupElement.localName, TabGroupElement); // wc-tab-group +customElements.define(ViewElement.localName, ViewElement); // wc-view +customElements.define(ViewGroupElement.localName, ViewGroupElement); // wc-view-group diff --git a/src/old/component/table/TableColumn.js b/src/old/component/table/TableColumn.js new file mode 100644 index 0000000..87b0737 --- /dev/null +++ b/src/old/component/table/TableColumn.js @@ -0,0 +1,5 @@ +export class TableColumn { + constructor(name) { + this.name = name; + } +} diff --git a/src/old/component/table/TableElement.js b/src/old/component/table/TableElement.js new file mode 100644 index 0000000..79cc6c1 --- /dev/null +++ b/src/old/component/table/TableElement.js @@ -0,0 +1,163 @@ +import { + LitElement, html, css, nothing, +} from 'lit'; + +import { TableColumn } from './TableColumn'; + +/** +* TableElement - A class that can be used to display a table +* +* @example +* Table Caption +*/ +export class TableElement extends LitElement { + static get localName() { + return 'wc-table'; + } + + constructor() { + super(); + + // Set default values for properties + this.data = []; + this.backgroundColor = ''; + } + + static get properties() { + return { + /** + * @property {Array} data + * @memberof TableElement + * + * The array of data loaded into the table. Each element may be an array or object. + */ + data: { type: Array }, + + /** + * @property {Boolean} stripedRows + * @memberof TableElement + * + * Whether to offset the colour of the rows in the table + */ + stripedRows: { type: Boolean }, + + /** + * @property {String} backgroundColor + * @memberof TableElement + * @default '' + * + * The colour of the background of the table. Either primary, secondary, light, dark, black or white. + */ + backgroundColor: { type: String }, + }; + } + + static get styles() { + return css` + table { + width: 100%; + border-collapse: collapse; + border: 1px solid gray; + } + table.bg-color-primary { + background-color: var(--primary-color); + color: var(--light-color); + } + table.bg-color-secondary { + background-color: var(--secondary-color); + color: var(--light-color); + } + table.bg-color-light { + background-color: var(--light-color); + color: var(--dark-color); + } + table.bg-color-dark { + background-color: var(--dark-color); + color: var(--light-color); + } + table.bg-color-white { + background-color: var(--white-color); + color: var(--black-color); + } + table.bg-color-black { + background-color: var(--black-color); + color: var(--white-color); + } + table th { + text-align: left; + backdrop-filter: var(--table-row-filter-header) + } + table td,th { + padding: var(--table-cell-padding-y) var(--table-cell-padding-x); + } + table.striped-rows tbody tr:nth-child(odd) { + backdrop-filter: var(--table-row-filter-odd); + } + `; + } + + // eslint-disable-next-line class-methods-use-this + get tableColumns() { + return [ + new TableColumn('fruit'), + new TableColumn('color'), + new TableColumn('weight'), + ]; + } + + #renderTableHeader() { + return html` + + ${this.tableColumns.map((column) => html`${column.name}`)} + + `; + } + + #renderTableCell(column, i, j) { + const row = this.data[i]; + if (Array.isArray(row) && row.length <= j) { + return row[j]; + } + if (typeof row === 'object' && column.name in row) { + return row[column.name]; + } + return nothing; + } + + #renderTableRow(i) { + return this.tableColumns.map((column, j) => html`${this.#renderTableCell(column, i, j)}`); + } + + #renderTableRows() { + return html` + ${this.data.map((_, i) => html`${this.#renderTableRow(i)}`)} + `; + } + + get className() { + const classes = ['table']; + if (this.backgroundColor) { + classes.push(`bg-color-${this.backgroundColor}`); + } + if (this.stripedRows) { + classes.push('striped-rows'); + } + return classes.join(' '); + } + + render() { + const tableHeader = this.#renderTableHeader(); + const tableRows = this.#renderTableRows(); + return html` + + + ${tableHeader} + + + ${tableRows} + +
    + + `; + } +} diff --git a/src/old/component/table/table.css b/src/old/component/table/table.css new file mode 100644 index 0000000..babb260 --- /dev/null +++ b/src/old/component/table/table.css @@ -0,0 +1,7 @@ +:root { + --table-cell-padding-x: calc(var(--spacer) * 0.2); + --table-cell-padding-y: calc(var(--spacer) * 0.2); + + --table-row-filter-header: brightness(50%); + --table-row-filter-odd: brightness(90%); +} diff --git a/src/old/component/table/table.js b/src/old/component/table/table.js new file mode 100644 index 0000000..c786172 --- /dev/null +++ b/src/old/component/table/table.js @@ -0,0 +1,8 @@ +// CSS +import './table.css'; + +// Web Components +import { TableElement } from './TableElement'; + +// Web Components +customElements.define(TableElement.localName, TableElement); // wc-table diff --git a/src/popup/PopupElement.js b/src/popup/PopupElement.js new file mode 100644 index 0000000..c728530 --- /dev/null +++ b/src/popup/PopupElement.js @@ -0,0 +1,28 @@ +import { LitElement, html, nothing } from 'lit'; + +/** + * @class PopupElement + * + * This class is used as a popup, which is initially hidden but can be shown indefinitely + * or for a specific duration. + * + * @example + * Suprise! + */ +export class PopupElement extends LitElement { + static get localName() { + return 'wc-popup'; + } + + // eslint-disable-next-line class-methods-use-this + get classes() { + const classes = []; + return classes; + } + + render() { + return html` +
    + `; + } +} diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..e69de29 diff --git a/src/popup/popup.js b/src/popup/popup.js new file mode 100644 index 0000000..355e735 --- /dev/null +++ b/src/popup/popup.js @@ -0,0 +1,8 @@ +// CSS +import './popup.css'; + +// Classes +import { PopupElement } from './PopupElement'; + +// Web Components +customElements.define(PopupElement.localName, PopupElement); // wc-popup diff --git a/src/test.js b/src/test.js deleted file mode 100644 index e5e132d..0000000 --- a/src/test.js +++ /dev/null @@ -1,72 +0,0 @@ - - -// Core -import Provider from './core/Provider'; -import Event from './core/Event'; - -// Test -window.addEventListener('load', () => { - // Button Group - document.querySelector('wc-button-group').addEventListener(Event.EVENT_CLICK, (evt) => { - console.log("Button Group Click", evt.detail); - }); - - // Card - document.querySelector('.wc-card').addEventListener(Event.EVENT_CLICK, (evt) => { - console.log("Card Click", evt.detail); - }); - - // Provider - var p = new Provider("http://localhost:8000/"); - p.addEventListener(Event.EVENT_ERROR, (evt) => { - console.log("Got error:", evt); - }); - p.addEventListener(Event.EVENT_START, (evt) => { - console.log("START", evt.detail); - }); - p.addEventListener(Event.EVENT_DONE, (evt) => { - console.log("DONE", evt.detail); - }); - p.fetch("/", {}); - - // Buttons - document.querySelectorAll('wc-button').forEach((button) => { - button.addEventListener(Event.EVENT_CLICK, (evt) => { - switch (evt.detail) { - case "modal": - document.querySelector('wc-modal').toggle(); - break; - default: - console.log("Button Click", evt.detail); - } - }); - }); - - // Close Modal - document.querySelector('wc-modal').addEventListener(Event.EVENT_CLICK, (evt) => { - if (evt.detail == "modal-close") { - document.querySelector('wc-modal').hide(); - } - }); - document.querySelector('wc-sidemodal').addEventListener(Event.EVENT_CLICK, (evt) => { - if (evt.detail == "modal-close") { - document.querySelector('wc-sidemodal').hide(); - } - }); - - // NavItem - document.querySelectorAll('wc-nav-item').forEach((navitem) => { - navitem.addEventListener(Event.EVENT_CLICK, (evt) => { - switch (evt.detail) { - case "menu": - document.querySelector('wc-sidemodal').toggle(); - break; - case 'help': - document.querySelector('wc-sidemodal').toggle(); - break; - default: - console.log("NavItem Click", evt.detail); - } - }); - }); -}); diff --git a/src/test.js-old b/src/test.js-old new file mode 100644 index 0000000..b6eaacd --- /dev/null +++ b/src/test.js-old @@ -0,0 +1,78 @@ +// Core +import Provider from './core/Provider.js'; +import { Event } from './core/Event.js'; +import { ViewController } from './nav/ViewController.js'; + +// Test +window.addEventListener('load', () => { + // Tab View Controller + // eslint-disable-next-line no-new + new ViewController( + document.querySelector('wc-tab-group#tab-group'), + document.querySelector('wc-view-group#view-group'), + ); + + // Button Group + document.querySelector('wc-button-group').addEventListener(Event.EVENT_CLICK, (evt) => { + console.log("Button Group Click", evt.detail); + }); + + // Card + document.querySelector('.wc-card').addEventListener(Event.EVENT_CLICK, (evt) => { + console.log("Card Click", evt.detail); + }); + + // Provider + var p = new Provider("http://localhost:8000/"); + p.addEventListener(Event.EVENT_ERROR, (evt) => { + console.log("Got error:", evt); + }); + p.addEventListener(Event.EVENT_START, (evt) => { + console.log("START", evt.detail); + }); + p.addEventListener(Event.EVENT_DONE, (evt) => { + console.log("DONE", evt.detail); + }); + p.fetch("/", {}); + + // Buttons + document.querySelectorAll('wc-button').forEach((button) => { + button.addEventListener(Event.EVENT_CLICK, (evt) => { + switch (evt.detail) { + case "modal": + document.querySelector('wc-modal').toggle(); + break; + default: + console.log("Button Click", evt.detail); + } + }); + }); + + // Close Modal + document.querySelector('wc-modal').addEventListener(Event.EVENT_CLICK, (evt) => { + if (evt.detail == "modal-close") { + document.querySelector('wc-modal').hide(); + } + }); + document.querySelector('wc-sidemodal').addEventListener(Event.EVENT_CLICK, (evt) => { + if (evt.detail == "modal-close") { + document.querySelector('wc-sidemodal').hide(); + } + }); + + // NavItem + document.querySelectorAll('wc-nav-item').forEach((navitem) => { + navitem.addEventListener(Event.EVENT_CLICK, (evt) => { + switch (evt.detail) { + case "menu": + document.querySelector('wc-sidemodal').toggle(); + break; + case 'help': + document.querySelector('wc-sidemodal').toggle(); + break; + default: + console.log("NavItem Click", evt.detail); + } + }); + }); +});