diff --git a/manifest-generator/.prettierrc.json b/manifest-generator/.prettierrc.json new file mode 100644 index 0000000..3626b62 --- /dev/null +++ b/manifest-generator/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "printWidth": 80, + "singleQuote": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "singleAttributePerLine": false +} diff --git a/manifest-generator/app.js b/manifest-generator/app.js new file mode 100644 index 0000000..0363299 --- /dev/null +++ b/manifest-generator/app.js @@ -0,0 +1,17 @@ +import { readManifestFromLocalStorage } from "./state.js"; + +const registerServiceWorker = async () => { + try { + await navigator.serviceWorker.register("sw.js"); + } catch (e) { + console.log(`Registration failed: ${e}`); + } +}; + +if (navigator.serviceWorker) { + registerServiceWorker(); +} + +// Grab previous state from Local Storage so that progress is not lost +// across sessions. +readManifestFromLocalStorage(); diff --git a/manifest-generator/components/app-view.js b/manifest-generator/components/app-view.js new file mode 100644 index 0000000..7401360 --- /dev/null +++ b/manifest-generator/components/app-view.js @@ -0,0 +1,87 @@ +// A split app-view to show the manifest viewer in the right pane and the editor in the left pane. + +import "./manifest-view/index.js"; +import "./navigation-view.js"; +import "./page-view.js"; +import "./styled-button.js"; + +const template = document.createElement("template"); +template.innerHTML = ` + + +
+ + +

Page 1

+
+ +

Page 2

+
+
+ +
+`; + +class AppView extends HTMLElement { + constructor() { + super(); + + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + this.navigationView = this.shadowRoot.querySelector("navigation-view"); + + this.pageIds = ["page-1", "page-2"]; + this.currentPageIdIndex = 0; + + this.navigationView.addEventListener("next", () => this.nextPage()); + this.navigationView.addEventListener("prev", () => this.prevPage()); + this.navigationView.addEventListener("skip", () => this.skipPage()); + } + + nextPage() { + this.jumpToPage( + Math.min(this.currentPageIdIndex + 1, this.pageIds.length - 1) + ); + } + + prevPage() { + this.jumpToPage(Math.max(this.currentPageIdIndex - 1, 0)); + } + + skipPage() { + this.jumpToPage( + Math.min(this.currentPageIdIndex + 1, this.pageIds.length - 1) + ); + } + + jumpToPage(pageIndex) { + this.currentPageIdIndex = pageIndex; + this.navigationView.setAttribute( + "current-id", + this.pageIds[this.currentPageIdIndex] + ); + } +} + +customElements.define("app-view", AppView); diff --git a/manifest-generator/components/color-picker.js b/manifest-generator/components/color-picker.js new file mode 100644 index 0000000..0e5bc0c --- /dev/null +++ b/manifest-generator/components/color-picker.js @@ -0,0 +1,58 @@ +// Component for a color picker -- a text label and a color input box. +/* + Usage: + +*/ +class ColorPicker extends HTMLElement { + #inputElement; + constructor() { + super(); + this.#inputElement = document.createElement("input"); + + // Create a shadow root + const shadow = this.attachShadow({ mode: "open" }); + + const tableWrapper = document.createElement("div"); + tableWrapper.setAttribute("class", "table"); + + // Create the page label + const inputLabel = document.createElement("p"); + inputLabel.setAttribute("class", "tableitem"); + inputLabel.textContent = `${this.getAttribute("label")}`; + + // Create the input element + this.#inputElement.setAttribute("type", "color"); + this.#inputElement.setAttribute("class", "tableitem"); + + // Style the elements + const style = document.createElement("style"); + style.textContent = `.tableitem { + align-self: center; + } + + .table { + display: flex; + flex-direction: column; + }`; + + const stylesheet = document.createElement("link"); + stylesheet.setAttribute("rel", "stylesheet"); + stylesheet.setAttribute("href", "styles/defaults.css"); + + // Append the text and input elements to the table + tableWrapper.append(inputLabel); + tableWrapper.append(this.#inputElement); + + // Append the table and style to the shadow DOM + shadow.append(tableWrapper); + shadow.append(style); + shadow.append(stylesheet); + } + + getUserInput() { + return this.#inputElement.value; + } +} + +// Define the new element +customElements.define("color-picker", ColorPicker); diff --git a/manifest-generator/components/display-mode.js b/manifest-generator/components/display-mode.js new file mode 100644 index 0000000..e4c505e --- /dev/null +++ b/manifest-generator/components/display-mode.js @@ -0,0 +1,111 @@ +// Component for a images as radio buttons. +/* + Usage: + + +*/ + +const template = document.createElement("template"); +template.innerHTML = ` + + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+`; + +class DisplayMode extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + getUserInput() { + const inputElement = this.shadowRoot.querySelector( + "input[type='radio']:checked" + ); + if (inputElement) { + return inputElement.value; + } + return; + } +} + +// Define the new element +customElements.define("display-mode", DisplayMode); diff --git a/manifest-generator/components/long-text-input-example.html b/manifest-generator/components/long-text-input-example.html new file mode 100644 index 0000000..7a97d12 --- /dev/null +++ b/manifest-generator/components/long-text-input-example.html @@ -0,0 +1,18 @@ + + + + + Simple word count web component + + + + + + + + + + diff --git a/manifest-generator/components/long-text-input.js b/manifest-generator/components/long-text-input.js new file mode 100644 index 0000000..21c7748 --- /dev/null +++ b/manifest-generator/components/long-text-input.js @@ -0,0 +1,70 @@ +// Component for a long text input box. Optional attributes for a label and placeholder text. +// See long-text-input-example.html for usage examples. + +class LongTextInput extends HTMLElement { + #inputElement; + constructor() { + super(); + + // Create a shadow root + const shadow = this.attachShadow({ mode: "open" }); + + const tableWrapper = document.createElement("div"); + tableWrapper.setAttribute("class", "table"); + + // Create the page label + const inputLabel = document.createElement("p"); + inputLabel.setAttribute("class", "table-item"); + if (this.getAttribute("label")) { + inputLabel.textContent = `${this.getAttribute("label")}`; + } else { + inputLabel.setAttribute("hidden", true); + } + + // Create the input element + this.#inputElement = document.createElement("textarea"); + this.#inputElement.setAttribute("class", "table-item"); + if (this.getAttribute("placeholder-text")) { + this.#inputElement.setAttribute( + "placeholder", + `${this.getAttribute("placeholder-text")}` + ); + } + + // Style the elements + const style = document.createElement("style"); + style.textContent = `.table-item { + align-self: center; + } + + .table { + display: flex; + flex-direction: column; + }`; + + const stylesheetDefault = document.createElement("link"); + stylesheetDefault.setAttribute("rel", "stylesheet"); + stylesheetDefault.setAttribute("href", "../styles/defaults.css"); + + const stylesheetInput = document.createElement("link"); + stylesheetInput.setAttribute("rel", "stylesheet"); + stylesheetInput.setAttribute("href", "../styles/input.css"); + + // Append the text and input elements to the table + tableWrapper.append(inputLabel); + tableWrapper.append(this.#inputElement); + + // Append the table and style to the shadow DOM + shadow.append(tableWrapper); + shadow.append(style); + shadow.append(stylesheetDefault); + shadow.append(stylesheetInput); + } + + getUserInput() { + return this.#inputElement.value; + } +} + +// Define the new element +customElements.define("long-text-input", LongTextInput); diff --git a/manifest-generator/components/manifest-view/index.js b/manifest-generator/components/manifest-view/index.js new file mode 100644 index 0000000..a05af49 --- /dev/null +++ b/manifest-generator/components/manifest-view/index.js @@ -0,0 +1,54 @@ +import JSONView from "./json.js"; + +const json = { + name: "manifest-generator", + short_name: "manifest-generator", + start_url: "/", + display: "standalone", + background_color: "#fff", + theme_color: "#fff", + description: "A simple tool to generate a web app manifest", + icons: [ + { + src: "https://manifest-gen.now.sh/static/icon-192x192.png", + sizes: "192x192", + type: "image/png", + }, + { + src: "https://manifest-gen.now.sh/static/icon-512x512.png", + sizes: "512x512", + type: "image/png", + }, + ], +}; + +// create a web component +class ManifestView extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = ` + + +
+

Manifest

+ +
+ `; + } +} + +// create a web component +customElements.define("manifest-view", ManifestView); + +// export to use in other files +export default ManifestView; diff --git a/manifest-generator/components/manifest-view/json-array.js b/manifest-generator/components/manifest-view/json-array.js new file mode 100644 index 0000000..921fe88 --- /dev/null +++ b/manifest-generator/components/manifest-view/json-array.js @@ -0,0 +1,53 @@ +const template = document.createElement("template"); +template.innerHTML = ` + +
+`; + +class JSONArray extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + connectedCallback() { + const jsonValue = this.getAttribute("json"); + this.json = JSON.parse(decodeURIComponent(jsonValue)); + this.render(); + } + + disconnectedCallback() {} + + render() { + const jsonValue = this.json; + const arrayNode = this.shadowRoot.querySelector(".node"); + for (let json of jsonValue) { + const node = document.createElement("json-view"); + node.setAttribute("json", encodeURIComponent(JSON.stringify(json))); + arrayNode.appendChild(node); + } + } + + observedAttributes() { + return ["json"]; + } +} + +customElements.define("json-array", JSONArray); + +export default JSONArray; diff --git a/manifest-generator/components/manifest-view/json.js b/manifest-generator/components/manifest-view/json.js new file mode 100644 index 0000000..e2e7fda --- /dev/null +++ b/manifest-generator/components/manifest-view/json.js @@ -0,0 +1,83 @@ +import "./node.js"; + +// Define a custom element for representing a JSON document +const template = document.createElement("template"); +template.innerHTML = ` + + +
+`; + +class JSONView extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + connectedCallback() { + const jsonValue = this.getAttribute("json"); + this.json = JSON.parse(decodeURIComponent(jsonValue)); + + this.render(); + } + + render() { + const jsonView = this.shadowRoot.querySelector(".json"); + jsonView.addEventListener("click", (e) => { + const isCollapsed = jsonView.getAttribute("collapsed") !== null; + jsonView.toggleAttribute("collapsed"); + if (isCollapsed) { + jsonView.innerHTML = ""; + this.renderNodes(jsonView, this.json); + } else { + jsonView.innerHTML = "..."; + } + e.stopPropagation(); + }); + + this.renderNodes(jsonView, this.json); + } + + renderNodes(jsonView, json) { + Object.keys(json).forEach((key) => { + const node = document.createElement("json-node"); + var nodeType = typeof json[key]; + // find if object is an array + if (Array.isArray(json[key])) { + nodeType = "array"; + } + node.setAttribute("type", nodeType); + // if the value is an object, add values recursively + if (nodeType === "object" || nodeType === "array") { + node.setAttribute("key", key); + node.setAttribute( + "value", + encodeURIComponent(JSON.stringify(json[key])) + ); + jsonView.appendChild(node); + return; + } + node.setAttribute("key", key); + node.setAttribute("value", json[key]); + jsonView.appendChild(node); + }); + } +} + +customElements.define("json-view", JSONView); + +export default JSONView; diff --git a/manifest-generator/components/manifest-view/node.js b/manifest-generator/components/manifest-view/node.js new file mode 100644 index 0000000..ae4345b --- /dev/null +++ b/manifest-generator/components/manifest-view/node.js @@ -0,0 +1,84 @@ +import "./json-array.js"; + +// Define a custom element for representing a JSON node +const template = document.createElement("template"); +template.innerHTML = ` + + +
+ + +
+`; +class Node extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + connectedCallback() { + this.key = this.getAttribute("key"); + this.value = this.getAttribute("value"); + this.render(); + } + + observedAttributes() { + return ["key", "value"]; + } + + render() { + const node = this.shadowRoot.querySelector(".node"); + + const type = this.getAttribute("type"); + const key = this.shadowRoot.querySelector(".key"); + const value = this.shadowRoot.querySelector(".value"); + key.textContent = `"${this.key}" : `; + if (type === "object" || type === "array") { + node.addEventListener("click", (e) => { + const isCollapsed = node.getAttribute("collapsed") !== null; + node.toggleAttribute("collapsed"); + if (!isCollapsed) { + value.innerHTML = "..."; + } else { + this.renderValue(value, type, this.value); + } + e.stopPropagation(); + }); + } + this.renderValue(value, type, this.value); + } + + renderValue(element, type, value) { + if (type === "array") { + element.innerHTML = ``; + return; + } + if (type === "object") { + element.innerHTML = ``; + return; + } + element.textContent = value; + } +} + +customElements.define("json-node", Node); + +export default Node; diff --git a/manifest-generator/components/navigation-view.js b/manifest-generator/components/navigation-view.js new file mode 100644 index 0000000..0d08ab8 --- /dev/null +++ b/manifest-generator/components/navigation-view.js @@ -0,0 +1,129 @@ +import "./styled-button.js"; + +const template = document.createElement("template"); +template.innerHTML = ` + + +
+
+ +
+ +
+`; + +const attributes = { + currentId: { + name: "current-id", + required: true, + }, + pageSelector: { + name: "page-selector", + required: true, + }, +}; + +class NavigationView extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: "open" }); + shadow.append(template.content.cloneNode(true)); + this.prevButton = this.shadowRoot.querySelector("#prev"); + this.nextButton = this.shadowRoot.querySelector("#next"); + this.skipButton = this.shadowRoot.querySelector("#skip"); + + this.prevButton.addEventListener("click", () => { + this.shadowRoot.dispatchEvent( + new CustomEvent("prev", { + composed: true, + bubbles: true, + }) + ); + }); + + this.nextButton.addEventListener("click", () => { + this.shadowRoot.dispatchEvent( + new CustomEvent("next", { + composed: true, + bubbles: true, + }) + ); + }); + + this.skipButton.addEventListener("click", () => { + this.shadowRoot.dispatchEvent( + new CustomEvent("skip", { + composed: true, + bubbles: true, + }) + ); + }); + + this.shadowRoot.querySelector("slot").addEventListener("slotchange", () => { + this.togglePage(this.currentId); + }); + } + + static get observedAttributes() { + return Object.values(attributes).map((opt) => opt.name); + } + + // This method doesn't validate any form inputs, just html attributes. + validateAttributes(changedValue) { + Object.entries(attributes).forEach(([field, opts]) => { + if (changedValue !== undefined && opts.name !== changedValue) return; + const attribute = this.getAttribute(opts.name); + if (opts.required && !attribute) + throw new Error( + `Attribute ${opts.name} should be set in component ${this.tagName}` + ); + this[field] = attribute; + }); + } + + togglePage(id) { + const pages = this.querySelectorAll(this.pageSelector); + pages.forEach((page) => { + page.toggleAttribute("hidden", page.pageId !== id); + }); + } + + connectedCallback() { + this.validateAttributes(); + } + + attributeChangedCallback(attr, oldVal, newVal) { + this.validateAttributes(); + if (oldVal !== null && attr == attributes.currentId.name) { + this.togglePage(newVal); + } + } +} + +customElements.define("navigation-view", NavigationView); diff --git a/manifest-generator/components/page-view-example.html b/manifest-generator/components/page-view-example.html new file mode 100644 index 0000000..3430fb3 --- /dev/null +++ b/manifest-generator/components/page-view-example.html @@ -0,0 +1,28 @@ + + + + + PageView example page + + + + + + + + + + + + + + diff --git a/manifest-generator/components/page-view.js b/manifest-generator/components/page-view.js new file mode 100644 index 0000000..8f92fbf --- /dev/null +++ b/manifest-generator/components/page-view.js @@ -0,0 +1,37 @@ +// Component for a page wrapper -- includes an h1 for page title and a slot for each custom component. +// See page-view-example.html for usage eaxmple. +// Has a public API `getId()` that returns this page's unique ID, set via page-id attribute. + +class PageView extends HTMLElement { + #id; + + constructor() { + super(); + + const pageViewTemplate = document.createElement("template"); + pageViewTemplate.innerHTML = ` + + + +

${this.getAttribute("title")}

+ My Default Text`; + + // Create a shadow root + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(pageViewTemplate.content.cloneNode(true)); + + // Set the id field based on the id attribute + this.#id = this.getAttribute("page-id"); + } + + get pageId() { + return this.#id; + } +} + +// Define the new element +customElements.define("page-view", PageView); diff --git a/manifest-generator/components/placeholder-component.js b/manifest-generator/components/placeholder-component.js new file mode 100644 index 0000000..40bd22c --- /dev/null +++ b/manifest-generator/components/placeholder-component.js @@ -0,0 +1 @@ +// This is just to show folder structure and will be deleted later. diff --git a/manifest-generator/components/simple-text-input-example.html b/manifest-generator/components/simple-text-input-example.html new file mode 100644 index 0000000..d578adc --- /dev/null +++ b/manifest-generator/components/simple-text-input-example.html @@ -0,0 +1,18 @@ + + + + + Simple word count web component + + + + + + + + + + diff --git a/manifest-generator/components/simple-text-input.js b/manifest-generator/components/simple-text-input.js new file mode 100644 index 0000000..69c9f91 --- /dev/null +++ b/manifest-generator/components/simple-text-input.js @@ -0,0 +1,71 @@ +// Component for a simple text input field. Optional attributes for a label and placeholder text. +// See simple-text-input-example.html for usage examples. + +class SimpleTextInput extends HTMLElement { + #inputElement; + constructor() { + super(); + + // Create a shadow root + const shadow = this.attachShadow({ mode: "open" }); + + const tableWrapper = document.createElement("div"); + tableWrapper.setAttribute("class", "table"); + + // Create the page label + const inputLabel = document.createElement("p"); + inputLabel.setAttribute("class", "table-item"); + if (this.getAttribute("label")) { + inputLabel.textContent = `${this.getAttribute("label")}`; + } else { + inputLabel.setAttribute("hidden", true); + } + + // Create the input element + this.#inputElement = document.createElement("input"); + this.#inputElement.setAttribute("class", "table-item"); + if (this.getAttribute("placeholder-text")) { + this.#inputElement.setAttribute( + "placeholder", + `${this.getAttribute("placeholder-text")}` + ); + } + + // Style the elements + const style = document.createElement("style"); + style.textContent = `.table-item { + align-self: center; + } + + .table { + display: flex; + flex-direction: column; + }`; + + // Import stylesheets + const stylesheetDefault = document.createElement("link"); + stylesheetDefault.setAttribute("rel", "stylesheet"); + stylesheetDefault.setAttribute("href", "../styles/defaults.css"); + + const stylesheetInput = document.createElement("link"); + stylesheetInput.setAttribute("rel", "stylesheet"); + stylesheetInput.setAttribute("href", "../styles/input.css"); + + // Append the text and input elements to the table + tableWrapper.append(inputLabel); + tableWrapper.append(this.#inputElement); + + // Append the table and style to the shadow DOM + shadow.append(tableWrapper); + shadow.append(style); + shadow.append(stylesheetDefault); + shadow.append(stylesheetInput); + } + + getUserInput() { + return this.#inputElement.value; + } +} + +// Define the new element +customElements.define("simple-text-input", SimpleTextInput); diff --git a/manifest-generator/components/styled-button.js b/manifest-generator/components/styled-button.js new file mode 100644 index 0000000..244a963 --- /dev/null +++ b/manifest-generator/components/styled-button.js @@ -0,0 +1,63 @@ +const template = document.createElement("template"); +template.innerHTML = ` + + + +`; + +const attributes = { + // type: "primary" | "secondary" + type: { + name: "type", + required: false, + allowedValues: ["primary", "secondary"], + default: "primary", + }, +}; + +class StyledButton extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: "open" }); + shadow.append(template.content.cloneNode(true)); + this.button = this.shadowRoot.querySelector("button"); + } + + static get observedAttributes() { + return Object.values(attributes).map((opt) => opt.name); + } + + // This method doesn't validate any form inputs, just html attributes. + validateAttributes(changedValue) { + Object.entries(attributes).forEach(([field, opts]) => { + if (changedValue !== undefined && opts.name !== changedValue) return; + let attribute = this.getAttribute(opts.name); + if ((attribute === undefined || attribute === null) && opts.default) + attribute = opts.default; + if (opts.required && (attribute === undefined || attributes === null)) + throw new Error( + `Attribute ${opts.name} should be set in component ${this.tagName}` + ); + if (!opts.allowedValues.find((values) => values === attribute)) + throw new Error( + `Attribute ${opts.name} can only be values: [${opts.allowedValues}]` + ); + this[field] = attribute; + }); + } + + connectedCallback() { + this.validateAttributes(); + } + + attributeChangedCallback(attr) { + this.validateAttributes(); + + if (attr === attributes.type.name) { + this.button.classList.toggle("btn-primary", this.type === "primary"); + this.button.classList.toggle("btn-secondary", this.type === "secondary"); + } + } +} + +customElements.define("styled-button", StyledButton); diff --git a/manifest-generator/design-reference.html b/manifest-generator/design-reference.html new file mode 100644 index 0000000..7834a6a --- /dev/null +++ b/manifest-generator/design-reference.html @@ -0,0 +1,70 @@ + + + + + Manifest Generator + + + + + + + + + +
+ Back home +
+
+

Title Lorem ipsum

+

H1 Lorem ipsum

+

H2 Lorem ipsum

+

H3 Lorem ipsum

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis + molestie, dictum est a, mattis tellus. Sed dignissim, metus nec + fringilla accumsan, risus sem sollicitudin lacus, ut interdum tellus + elit sed risus. Maecenas eget condimentum velit, sit amet feugiat + lectus. Class aptent taciti sociosqu ad litora torquent per conubia + nostra, per inceptos himenaeos. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis + molestie, dictum est a, mattis tellus. +

+
+ Link +
+

Error: Lorem ipsum dolor sit

+
+ +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+ + diff --git a/manifest-generator/icons/48x48.png b/manifest-generator/icons/48x48.png new file mode 100644 index 0000000..b549bc4 Binary files /dev/null and b/manifest-generator/icons/48x48.png differ diff --git a/manifest-generator/icons/512x512.png b/manifest-generator/icons/512x512.png new file mode 100644 index 0000000..cb88de1 Binary files /dev/null and b/manifest-generator/icons/512x512.png differ diff --git a/manifest-generator/index.html b/manifest-generator/index.html new file mode 100644 index 0000000..762d118 --- /dev/null +++ b/manifest-generator/index.html @@ -0,0 +1,23 @@ + + + + + Manifest Generator + + + + + + + + + + + + + + + diff --git a/manifest-generator/manifest.json b/manifest-generator/manifest.json new file mode 100644 index 0000000..393628d --- /dev/null +++ b/manifest-generator/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Manifest Generator", + "start_url": "./index.html", + "display": "standalone", + "icons": [ + { + "src": "icons/48x48.png", + "type": "image/png", + "sizes": "48x48" + }, + { + "src": "icons/512x512.png", + "type": "image/png", + "sizes": "512x512" + } + ] +} diff --git a/manifest-generator/state.js b/manifest-generator/state.js new file mode 100644 index 0000000..5fa4f84 --- /dev/null +++ b/manifest-generator/state.js @@ -0,0 +1,28 @@ +let manifestState = {}; + +// Read (Get) entire object from LocalStorage. +// Other places can use this as: +// const state = getManifest(); +// state.name ... +export const getManifest = () => { + return structuredClone(manifestState); +}; + +// Write (Set) entire object LocalStorage. +// Other places can use this as: +// let state = getManifest(); +// state.name = "new value"; +// setManifest(state); +export const setManifest = (newState) => { + if (newState == manifestState) { + return; + } + + manifestState = newState; + localStorage.setItem("manifest", JSON.stringify(newState)); +}; + +export const readManifestFromLocalStorage = () => { + const manifestString = localStorage.getItem("manifest"); + manifestState = JSON.parse(manifestString); +}; diff --git a/manifest-generator/style.css b/manifest-generator/style.css new file mode 100644 index 0000000..9ae8be9 --- /dev/null +++ b/manifest-generator/style.css @@ -0,0 +1,7 @@ +@import url("./styles/defaults.css"); +@import url("./styles/button.css"); +@import url("./styles/input.css"); + +body { + background: var(--c-bkg); +} diff --git a/manifest-generator/styles/button.css b/manifest-generator/styles/button.css new file mode 100644 index 0000000..48bfa31 --- /dev/null +++ b/manifest-generator/styles/button.css @@ -0,0 +1,77 @@ +button { + appearance: button; + -webkit-appearance: button; + + background-color: initial; + background-image: none; + + font-family: inherit; + font-size: 16px; + + text-transform: uppercase; + + color: var(--c-text); + + cursor: pointer; + margin: 0 0.2em; +} + +a.btn-primary, +a.btn-secondary, +a.btn-primary:focus-visible, +a.btn-secondary:focus-visible { + border: none; + color: var(--c-white); + text-transform: uppercase; + font-size: 16px; + line-height: 3em; + margin: 0 0.2em; +} + +button.btn-primary, +a.btn-primary { + background-color: var(--c-blue); + font-weight: 500; + padding: 0.8em 1.5em; + border-radius: var(--border-radius); + + transition: var(--transition-color); +} + +button.btn-primary:hover, +button.btn-primary:focus, +a.btn-primary:hover, +a.btn-primary:focus { + background-color: var(--c-blue-dark); +} + +button.btn-primary:focus-visible, +a.btn-primary:focus-visible { + outline: 2px solid var(--c-blue-dark); + outline-offset: 2px; + background-color: var(--c-blue-dark); +} + +button.btn-secondary, +a.btn-secondary { + background-color: var(--c-gray); + font-weight: 500; + padding: 0.8em 1.5em; + border-radius: var(--border-radius); + + transition: var(--transition-color); +} + +button.btn-secondary:hover, +button.btn-secondary:focus, +a.btn-secondary:hover, +a.btn-secondary:focus { + background-color: var(--c-gray-light); +} + +button.btn-secondary:focus-visible, +a.btn-secondary:focus-visible { + outline: 2px solid var(--c-gray-light); + outline-offset: 2px; + background-color: var(--c-gray-light); +} diff --git a/manifest-generator/styles/defaults.css b/manifest-generator/styles/defaults.css new file mode 100644 index 0000000..65b5517 --- /dev/null +++ b/manifest-generator/styles/defaults.css @@ -0,0 +1,140 @@ +:root { + --c-bkg: #252931; + --c-bkg-secondary: #353942; + --c-white: #ffffff; + --c-blue: #528bff; + --c-gray: #555b68; + --c-gray-light: #898b90; + --c-blue-gray: #898b90; + --c-blue-dark: #2870ff; + --c-red: #cb463a; + --c-bkg-card: var(--c-bkg-secondary); + --c-text: var(--c-white); + --c-error: var(--c-red); + + --border-radius: 10px; + + --transition-duration: 150ms; + --transition-color: color var(--transition-duration) ease-in-out, + background-color var(--transition-duration) ease-in-out; +} + +*, +:after, +:before { + box-sizing: border-box; + border: 0 solid #e5e7eb; +} + +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: "Montserrat", ui-sans-serif, system-ui, -apple-system, + BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, + sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, + Noto Color Emoji; +} + +body { + margin: 0; + line-height: inherit; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} +a { + color: inherit; + text-decoration: inherit; +} + +h1, +h2, +h3, +p, +span, +a { + color: var(--c-text); +} + +h1.text-title { + font-size: 72px; + line-height: 110%; + font-weight: 900; +} + +h1 { + font-size: 48px; + line-height: 110%; + letter-spacing: -1%; + font-weight: 700; +} + +h2 { + font-size: 32px; + line-height: 110%; + letter-spacing: 0%; + font-weight: 700; +} + +h3 { + font-size: 24px; + line-height: 150%; + letter-spacing: 0%; + font-weight: 500; +} + +p, +span, +a { + font-size: 16px; + line-height: 150%; + letter-spacing: 2%; + font-weight: 400; +} + +.text-sub { + font-size: 14px; + line-height: 150%; + letter-spacing: 2%; + font-weight: 400; +} + +.text-error { + color: var(--c-error); +} + +a { + font-size: 16px; + line-height: 150%; + font-weight: 500; + border-bottom: 2px solid currentColor; +} + +a:hover, +a:focus { + color: var(--c-blue); + transition: var(--transition-color); +} + +a:focus-within { + outline: none; +} + +p a { + font-size: inherit; + font-weight: inherit; + color: var(--c-blue); +} + +p a:hover, +p a:focus { + color: var(--c-blue-dark); +} diff --git a/manifest-generator/styles/input.css b/manifest-generator/styles/input.css new file mode 100644 index 0000000..43d1d65 --- /dev/null +++ b/manifest-generator/styles/input.css @@ -0,0 +1,45 @@ +input[type="text"] { + background-color: var(--c-gray); + padding: 0.8em 1em; + border-radius: var(--border-radius); + color: var(--c-text); + font-size: 16px; + font-weight: 400; + font-family: inherit; + margin: 0 0.2em; +} + +::placeholder { + color: var(--c-blue-gray); + opacity: 1; /* Firefox */ +} +:-ms-input-placeholder { + /* Internet Explorer 10-11 */ + color: var(--c-blue-gray); +} +::-ms-input-placeholder { + /* Microsoft Edge */ + color: var(--c-blue-gray); +} + +textarea { + background-color: var(--c-gray); + padding: 0.8em 1em; + border-radius: var(--border-radius); + color: var(--c-text); + font-size: 16px; + font-weight: 400; + font-family: inherit; + margin: 0 0.2em; + height: 10em; + min-width: 400px; + resize: vertical; +} + +input[error], +textarea[error] { + outline-style: solid; + outline-color: var(--c-red); + outline-width: 2px; + outline-offset: 2px; +} diff --git a/manifest-generator/sw.js b/manifest-generator/sw.js new file mode 100644 index 0000000..0da6ac1 --- /dev/null +++ b/manifest-generator/sw.js @@ -0,0 +1,8 @@ +self.addEventListener("fetch", (e) => { + e.respondWith( + fetch(e.request).catch(() => { + // To-do: useful offline experience. + return new Response("Hello offline page"); + }) + ); +});