From d26b319fd6f98517cc3421f10bf18698b953e4d2 Mon Sep 17 00:00:00 2001 From: Ruben Rodriguez Date: Thu, 13 Sep 2018 20:39:48 -0400 Subject: Updated extensions list for v60 --- .../jid1-KtlZuoiikVfFew@jetpack/html/README | 21 ++ .../html/background-panel.png | Bin 0 -> 14814 bytes .../jid1-KtlZuoiikVfFew@jetpack/html/common.css | 29 ++ .../html/display_panel/content/display-panel.html | 104 +++++++ .../html/display_panel/content/main_panel.js | 177 ++++++++++++ .../html/display_panel/content/panel-styles.css | 157 +++++++++++ .../html/librejs-title.png | Bin 0 -> 14123 bytes .../html/preferences_panel/pref.js | 307 +++++++++++++++++++++ .../html/preferences_panel/preferences_panel.html | 86 ++++++ .../html/preferences_panel/prefs.css | 91 ++++++ 10 files changed, 972 insertions(+) create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/README create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/background-panel.png create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/common.css create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/display-panel.html create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/main_panel.js create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/panel-styles.css create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/librejs-title.png create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/pref.js create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/preferences_panel.html create mode 100644 data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/prefs.css (limited to 'data/extensions/jid1-KtlZuoiikVfFew@jetpack/html') diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/README b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/README new file mode 100644 index 0000000..a56ea46 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/README @@ -0,0 +1,21 @@ +/** + * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. + * * + * Copyright (C) 2011, 2012, 2014 Loic J. Duros + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +All images in this directory are free, released under the GPLv3 or later. \ No newline at end of file diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/background-panel.png b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/background-panel.png new file mode 100644 index 0000000..022ffb3 Binary files /dev/null and b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/background-panel.png differ diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/common.css b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/common.css new file mode 100644 index 0000000..cf2c5d1 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/common.css @@ -0,0 +1,29 @@ +html { + padding:0px; + margin:0px; + color:#000 !important; + background:url('background-panel.png') !important; +} +body { + padding:0; + margin:10px 30px 10px 20px; + color:#000; +} + +div.libre { + position: relative; +} + +.libre { + width:230px; + height:104px; + display:block; +} +h1.libre { + font-size:1.5em; + font-weight:normal; + padding:0; + font-weight:bold; + background:url('librejs-title.png') no-repeat top left; + text-indent:-1000px; +} diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/display-panel.html b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/display-panel.html new file mode 100644 index 0000000..df153b3 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/display-panel.html @@ -0,0 +1,104 @@ + + + + +Display JS Monitoring Panel + + + + + + + +
+
+

This whole site

+
+ + + +
+
+
+

+

+ LibreJS will decide whether blocking these scripts next time this page is loaded. +

+
    +
  • + : +

    +
    + + + + +
    +
  • +
+
+
+

+
    +
    +
    +

    +
      +
      +
      +

      +
        +
        +
        +

        +
          +
          +
          + + + diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/main_panel.js b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/main_panel.js new file mode 100644 index 0000000..c55b167 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/main_panel.js @@ -0,0 +1,177 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* * +* Copyright (C) 2017, 2018 NateN1222 +* Copyright (C) 2018 Ruben Rodriguez +* Copyright (C) 2018 Giorgio Maone +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +*/ + +var fromTab = window.location.hash.match(/^#fromTab=(\d+)/) && RegExp.$1; +if (fromTab) { + let browserStyle = document.createElement("link"); + browserStyle.rel = "stylesheet"; + browserStyle.href = "chrome://browser/content/extension.css"; + document.head.appendChild(browserStyle); + document.documentElement.classList.add("tab"); +} + +var myPort = browser.runtime.connect({name: "port-from-cs"}); +var currentReport; + +// Sends a message that tells the background script the window is open +myPort.postMessage({"update": true, tabId: parseInt(currentReport && currentReport.tabId || fromTab) || ""}); + +// Display the actual extension version Number +document.querySelector("#version").textContent = browser.runtime.getManifest().version; + +var liTemplate = document.querySelector("#li-template"); +liTemplate.remove(); + +document.querySelector("#info").addEventListener("click", e => { + let button = e.target; + if (!button.matches(".buttons > button")) return; + let li = button.closest("li"); + let entry = li && li._scriptEntry || [currentReport.url, "Page's site"]; + let action = button.className; + let site = button.name === "*"; + if (site) { + ([action] = action.split("-")); + } + myPort.postMessage({[action]: entry, site, tabId: currentReport.tabId}); +}); + +document.querySelector("#report-tab").onclick = e => { + myPort.postMessage({report_tab: currentReport}); + close(); +} + +document.querySelector("#complain").onclick = e => { + myPort.postMessage({invoke_contact_finder: currentReport}); + close(); +} + +document.querySelector("#open-options").onclick = e => { + browser.runtime.openOptionsPage(); + close(); +} + +document.querySelector("#reload").onclick = async e => { + let {tabId} = currentReport; + if (tabId) { + await browser.tabs.reload(tabId); + myPort.postMessage({"update": true, tabId}); + } +}; + +/* +* Takes in the [[file_id, reason],...] array and the group name for one group +* of scripts found in this tab, rendering it as a list with management buttons. +* Groups are "unknown", "blacklisted", "whitelisted", "accepted", and "blocked". +*/ +function createList(data, group){ + var {url} = data; + let entries = data[group]; + let container = document.getElementById(group); + let heading = container.querySelector("h2"); + var list = container.querySelector("ul"); + list.classList.toggle(group, true); + if (Array.isArray(entries) && entries.length) { + heading.innerHTML = `${group} scripts in ${url}:`; + container.classList.remove("empty"); + } else { + // default message + list.innerHTML = `
        • No ${group} scripts on this page.
        • ` + entries = data[group] = []; + container.classList.add("empty"); + } + // generate list + for (let entry of entries) { + let [scriptId, reason] = entry; + let li = liTemplate.cloneNode(true); + let a = li.querySelector("a"); + a.href = scriptId.split("(")[0]; + a.textContent = scriptId; + li.querySelector(".reason").textContent = reason; + let bySite = !!reason.match(/https?:\/\/[^/]+\/\*/); + li.classList.toggle("by-site", bySite); + if (bySite) { + let domain = li.querySelector(".forget .domain"); + if (domain) domain.textContent = RegExp.lastMatch; + } + li._scriptEntry = entry; + list.appendChild(li); + } + +} + +/** +* Updates scripts lists and buttons to act on them. +* If return_HTML is true, it returns the HTML of the popup window without updating it. +* example report argument: +* { +* "accepted": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], +* "blocked": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], +* "whitelisted": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], +* "blacklisted": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], +* "unknown": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], +* "url":"example.com" +* }; +* +*/ +function refreshUI(report) { + currentReport = report; + + document.querySelector("#site").className = report.siteStatus || ""; + document.querySelector("#site h2").textContent = + `This site ${report.site}`; + + for (let toBeErased of document.querySelectorAll("#info h2:not(.site) > *, #info ul > *")) { + toBeErased.remove(); + } + + let scriptsCount = 0; + for (let group of ["unknown", "accepted", "whitelisted", "blocked", "blacklisted"]) { + if (group in report) createList(report, group); + scriptsCount += report[group].length; + } + + for (let b of document.querySelectorAll(`.forget, .whitelist, .blacklist`)) { + b.disabled = false; + } + for (let b of document.querySelectorAll( + `.unknown .forget, .accepted .forget, .blocked .forget, + .whitelisted .whitelist, .blacklisted .blacklist` + )) { + b.disabled = true; + } + + let noscript = scriptsCount === 0; + document.body.classList.toggle("empty", noscript); +} + +myPort.onMessage.addListener(m => { + if (m.show_info) { + refreshUI(m.show_info); + } +}); + +function print_local_storage(){ + myPort.postMessage({"printlocalstorage": true}); +} +function delete_local_storage(){ + myPort.postMessage({"deletelocalstorage":true}); +} diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/panel-styles.css b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/panel-styles.css new file mode 100644 index 0000000..cbf5cf5 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/display_panel/content/panel-styles.css @@ -0,0 +1,157 @@ +/** + * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. + * * + * Copyright (C) 2011, 2012, 2014 Loic J. Duros + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +@import url("/html/common.css"); + +body { + width:500px; +} +#header{ +display:block; +width:500px; +} + +h2 { + font-size:1.1em; + font-weight:bold; + font-family:arial; + border-bottom:4px solid #444; + padding-bottom:0; + margin:10px 0 0 0; + line-height:140%; +} +code { + font-size:1.2em; + margin:0; + padding:0; +} +ul { + margin:0; + padding:0; + list-style:none; +} +#info li { + padding:5px; + border-bottom:2px solid #CCC; + margin:0; + overflow: hidden; +} + +#info ul ul { + margin:10px; + list-style:disc; +} +#info ul ul li { + padding:5px; + border-bottom:0; +} +#info { + clear:both; +} + +#info .type-name { + text-transform: uppercase; + font-weight: bold; +} + +#info .accepted-js .type-name { + color: #080; +} + +#info .blocked-js .type-name { + color: #800; +} + +#info .unknown-js .type-name { + color: #008; +} + +#info .unknown-js .reason { + display: none; +} + +.by-site button.forget, button.forget[name="*"] { + display: none; +} + +.by-site button.forget[name="*"], #site .forget[name="*"] { + display: initial; +} + + +button.whitelist { + color: #080; +} +button.blacklist { + color: #800; +} +button.forget { + color: #008; +} + +button:disabled { + color: #888 !important; +} + +span.accepted, span.blocked { + color:#008e00; + font-size:145%; + font-variant:small-caps; + font-weight:bold; +} + +span.blocked { + color:#8e0000; +} + +.title-area { + width: 250px; + float:left !important; + text-align: center !important; +} + +.title-area #librejs-web-link { + font-size: 18px; +} + +#librejs-web-labels-pages>ul { + margin-top: 8px; + font-size: 14px; + list-style-type: disc; +} + + +.empty #site, .unknown-js.empty { + display: none; +} + +.tab #must-reload, .tab #buttons, .empty #buttons { + display: none; +} + +#buttons button { + width: 100%; + text-align: center; +} + + + +#complain { + display: none; /* TODO: Complaint to owner UI */ +} diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/librejs-title.png b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/librejs-title.png new file mode 100644 index 0000000..c1a911c Binary files /dev/null and b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/librejs-title.png differ diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/pref.js b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/pref.js new file mode 100644 index 0000000..9cecbb6 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/pref.js @@ -0,0 +1,307 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2017 Nathan Nichols +* Copyright (C) 2018 Giorgio maone +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see . +*/ + +(() => { + "use strict"; + + const LIST_NAMES = ["white", "black"]; + + var Model = { + lists: {}, + prefs: null, + + malformedUrl(url) { + let error = null; + try { + let objUrl = new URL(url); + url = objUrl.href; + if (!objUrl.protocol.startsWith("http")) { + error = "Please enter http:// or https:// URLs only"; + } else if (!/^[^*]+\*?$/.test(url)) { + error = "Only one single trailing path wildcard (/*) allowed"; + } + } catch (e) { + error = "Invalid URL"; + if (url && !url.includes("://")) error += ": missing protocol, either http:// or https://"; + else if (url.endsWith("://")) error += ": missing domain name"; + } + return error; + }, + + async save(prefs = this.prefs) { + if (prefs !== this.prefs) { + this.prefs = Object.assign(this.prefs, prefs); + } + this.saving = true; + try { + return await browser.storage.local.set(prefs); + } finally { + this.saving = false; + } + }, + + async addToList(list, ...items) { + let other = list === Model.lists.black ? Model.lists.white : Model.lists.black; + this.saving = true; + try { + await Promise.all([ + other.remove(...items), + list.store(...items) + ]); + } finally { + this.saving = false; + } + } + }; + Model.loading = (async () => { + let prefsNames = [ + "whitelist", + "blacklist", + "subject", + "body" + ]; + Model.prefs = await browser.storage.local.get(prefsNames.map(name => `pref_${name}`)); + + for (let listName of LIST_NAMES) { + let prefName = `pref_${listName}list`; + await (Model.lists[listName] = new ListStore(prefName, Storage.CSV)) + .load(Model.prefs[prefName]); + } + })(); + + var Controller = { + init() { + let widgetsRoot = this.root = document.getElementById("widgets"); + for (let widget of widgetsRoot.querySelectorAll('[id^="pref_"]')) { + if (widget.id in Model.lists) { + populateListUI(widget); + } else if (widget.id in Model.prefs) { + widget.value = Model.prefs[widget.id]; + } + } + + this.populateListUI(); + this.syncAll(); + + for (let ev in Listeners) { + widgetsRoot.addEventListener(ev, Listeners[ev]); + } + document.getElementById("site").onfocus = e => { + if (!e.target.value.trim()) { + e.target.value = "https://"; + } + }; + + browser.storage.onChanged.addListener(changes => { + if (!Model.saving && + ("pref_whitelist" in changes || "pref_blacklist" in changes)) { + setTimeout(() => { + this.populateListUI(); + this.syncAll(); + }, 10); + } + }); + }, + + async addSite(list) { + let url = document.getElementById("site").value.trim(); + + if (url && !Model.malformedUrl(url)) { + await this.addToList(list, url); + } + }, + async addToList(list, ...items) { + await Model.addToList(list, ...items); + this.populateListUI(); + this.syncAll(); + }, + async swapSelection(list) { + let origin = list === Model.lists.black ? "white" : "black"; + await this.addToList(list, ...Array.map( + document.querySelectorAll(`select#${origin} option:checked`), + option => option.value) + ); + }, + + syncAll() { + this.syncListsUI(); + this.syncSiteUI(); + }, + + syncSiteUI() { + let widget = document.getElementById("site"); + let list2button = listName => document.getElementById(`cmd-${listName}list-site`); + + for (let bi of LIST_NAMES.map(list2button)) { + bi.disabled = true; + } + + let url = widget.value.trim(); + let malformedUrl = url && Model.malformedUrl(url); + widget.classList.toggle("error", !!malformedUrl); + document.getElementById("site-error").textContent = malformedUrl || ""; + if (!url) return; + if (url !== widget.value) { + widget.value = url; + } + + for (let listName of LIST_NAMES) { + let list = Model.lists[listName]; + if (!list.contains(url)) { + list2button(listName).disabled = false; + } + } + }, + + syncListsUI() { + let total = 0; + for (let id of ["black", "white"]) { + let selected = document.querySelectorAll(`select#${id} option:checked`).length; + let other = id === "black" ? "white" : "black"; + document.getElementById(`cmd-${other}list`).disabled = selected === 0; + total += selected; + } + document.getElementById("cmd-delete").disabled = total === 0; + }, + + async deleteSelection() { + for (let id of ["black", "white"]) { + let selection = document.querySelectorAll(`select#${id} option:checked`); + await Model.lists[id].remove(...Array.map(selection, option => option.value)); + } + this.populateListUI(); + this.syncAll(); + }, + + populateListUI(widget) { + if (!widget) { + for(let id of ["white", "black"]) { + this.populateListUI(document.getElementById(id)); + } + return; + } + widget.innerHTML = ""; + let items = [...Model.lists[widget.id].items].sort(); + let options = new DocumentFragment(); + for (let item of items) { + let option = document.createElement("option"); + option.value = option.textContent = option.title = item; + options.appendChild(option); + } + widget.appendChild(options); + } + }; + + var KeyEvents = { + Delete(e) { + if (e.target.matches("#lists select")) { + Controller.deleteSelection(); + } + }, + Enter(e) { + if (e.target.id === "site") { + e.target.parentElement.querySelector("button[default]").click(); + } + }, + KeyA(e) { + if (e.target.matches("select") && e.ctrlKey) { + for (let o of e.target.options) { + o.selected = true; + } + Controller.syncListsUI(); + } + } + } + + var Listeners = { + async change(e) { + let {target} = e; + let {id} = target; + + if (id in Model.lists) { + Controller.syncListsUI(); + let selection = target.querySelectorAll("option:checked"); + if (selection.length === 1) { + document.getElementById("site").value = selection[0].value; + } + return; + } + }, + + click(e) { + let {target} = e; + + if (!/^cmd-(white|black|delete)(list-site)?/.test(target.id)) return; + e.preventDefault(); + let cmd = RegExp.$1; + if (cmd === "delete") { + Controller.deleteSelection(); + return; + } + let list = Model.lists[cmd]; + if (list) { + Controller[RegExp.$2 ? "addSite" : "swapSelection"](list); + return; + } + }, + + keypress(e) { + let {code} = e; + if (code && typeof KeyEvents[code] === "function") { + if (KeyEvents[code](e) === false) { + e.preventDefault(); + } + return; + } + }, + + async input(e) { + let {target} = e; + let {id} = target; + if (!id) return; + + if (id === "site") { + Controller.syncSiteUI(); + let url = target.value; + if (url) { + let o = document.querySelector(`#lists select option[value="${url}"]`); + if (o) { + o.scrollIntoView(); + o.selected = true; + } + } + return; + } + + if (id.startsWith("pref_")) { + await Model.save({[id]: target.value}); + return; + } + } + }; + + window.addEventListener("DOMContentLoaded", async e => { + await Model.loading; + Controller.init(); + }); + +})(); diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/preferences_panel.html b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/preferences_panel.html new file mode 100644 index 0000000..effb724 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/preferences_panel.html @@ -0,0 +1,86 @@ + + + + + + + LibreJS preferences + + + + + + + +
          + +

          LibreJS

          +
          +

          Settings

          +
          +
          +
          Allow or block scripts matching the following URLs ("*" matches any path) + +
          + + + +
          +
          +
          +
          + + +
          +
          + + + +
          +
          + + +
          +
          +
          + +
          Complaint email defaults + + + + +
          +
          + + diff --git a/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/prefs.css b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/prefs.css new file mode 100644 index 0000000..b52d6c5 --- /dev/null +++ b/data/extensions/jid1-KtlZuoiikVfFew@jetpack/html/preferences_panel/prefs.css @@ -0,0 +1,91 @@ +@import url("chrome://browser/content/extension.css"); +@import url("/html/common.css"); +h3 { + position: absolute; + bottom: 0px; + left: 240px; + font-size: 18px; +} +textarea { + width: 100%; +} +fieldset { + border: none; + padding: 0; + margin-top: 1em; + border-top: 1px solid #ccc; +} +legend { + font-weight: bold; + margin: 0; + padding: 0; +} +label, legend { + display: block; + font-size: 1.2em; +} + +#lists { + display: flex; + flex-direction: row; +} +.list-container { + flex: 3; + flex-direction: row; +} +.list-container select { +width: 100% +} + +.black { + color: #600; +} +.white { + color: #060; +} + +#commands { + display: flex; + justify-content: center; + flex: none; + flex-flow: column nowrap; +} + +#commands button { + font-weight: bold; +} +input[type="text"] { + width: 100%; +} + +#lists label { + font-weight: bold; +} +#lists select { + color: black; +} +#black { + background-color: #fcc; +} +#white { + background-color: #cfc; +} + +#new-site { + display: flex; + flex 2; +} +.error-msg { + color: red; +} +.error-msg::after { + content: "\00A0"; +} +.error { + background: #ffe; + color: #800; +} + +#section-complaint { + display: none; /* TODO: Complaint to owner UI */ +} -- cgit v1.2.3