diff --git a/build.js b/build.js new file mode 100644 index 0000000..63baa01 --- /dev/null +++ b/build.js @@ -0,0 +1,231 @@ +const pug = require("pug") +const sass = require("sass") +const fs = require("fs").promises +const os = require("os") +const crypto = require("crypto") +const path = require("path") +const pj = path.join +const babel = require("@babel/core") +const fetch = require("node-fetch") +const chalk = require("chalk") +const hint = require("jshint").JSHINT + +process.chdir(pj(__dirname, "src")) + +const buildDir = "../build" + +const validationQueue = [] +const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/" +const static = new Map() +const links = new Map() +const pugLocals = {static, links} + +const spec = require("./spec.js") + +function hash(buffer) { + return crypto.createHash("sha256").update(buffer).digest("hex").slice(0, 10) +} + +function validate(filename, body, type) { + const promise = fetch(validationHost+"?out=json", { + method: "POST", + body, + headers: { + "content-type": `text/${type}; charset=UTF-8` + } + }).then(res => res.json()).then(root => { + return function cont() { + let concerningMessages = 0 + for (const message of root.messages) { + if (message.hiliteStart) { + let type = message.type + if (message.type === "error") { + type = chalk.red("error") + } else if (message.type === "warning") { + type = chalk.yellow("warning") + } else { + continue // don't care about info + } + concerningMessages++ + console.log(`validation: ${type} in ${filename}`) + console.log(` ${message.message}`) + const text = message.extract.replace(/\n/g, "⏎").replace(/\t/g, " ") + console.log(chalk.grey( + " " + + text.slice(0, message.hiliteStart) + + chalk.inverse(text.substr(message.hiliteStart, message.hiliteLength)) + + text.slice(message.hiliteStart+message.hiliteLength) + )) + } else { + console.log(message) + } + } + if (!concerningMessages) { + console.log(`validation: ${chalk.green("ok")} for ${filename}`) + } + } + }) + validationQueue.push(promise) + return promise +} + +function runHint(filename, source) { + hint(source, { + esversion: 9, + undef: true, + unused: true, + loopfunc: true, + globals: ["require", "console", "URLSearchParams", "L"], + strict: "global", + browser: true + }) + const result = hint.data() + if (result.errors && result.errors.length) { + for (const error of result.errors) { + if (error.evidence) { + const text = error.evidence.replace(/\t/g, " ") + let type = error.code.startsWith("W") ? chalk.yellow("warning") : chalk.red("error") + console.log(`hint: ${type} in ${filename}`) + console.log(` ${error.line}:${error.character}: ${error.reason} (${error.code})`) + console.log(chalk.gray( + " " + + text.slice(0, error.character) + + chalk.inverse(text.substr(error.character, 1)) + + text.slice(error.character+1) + )) + } + } + console.log(`hint: ${chalk.cyan(result.errors.length+" problems")} in ${filename}`) + } else { + console.log(`hint: ${chalk.green("ok")} for ${filename}`) + } +} + +async function addFile(sourcePath, targetPath) { + const contents = await fs.readFile(pj(".", sourcePath), {encoding: null}) + static.set(sourcePath, `${targetPath}?static=${hash(contents)}`) + fs.writeFile(pj(buildDir, targetPath), contents) +} + +async function addJS(sourcePath, targetPath) { + const source = await fs.readFile(pj(".", sourcePath), {encoding: "utf8"}) + static.set(sourcePath, `${targetPath}?static=${hash(source)}`) + runHint(sourcePath, source); + fs.writeFile(pj(buildDir, targetPath), source) +} + +async function addSass(sourcePath, targetPath) { + const renderedCSS = sass.renderSync({ + file: pj(".", sourcePath), + outputStyle: "expanded", + indentType: "tab", + indentWidth: 1, + functions: { + "static($name)": function(name) { + if (!(name instanceof sass.types.String)) { + throw "$name: expected a string" + } + const result = static.get(name.getValue()) + if (typeof result === "string") { + return new sass.types.String(result) + } else { + throw new Error("static file '"+name.getValue()+"' does not exist") + } + } + } + }).css + static.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`) + validate(sourcePath, renderedCSS, "css") + await fs.writeFile(pj(buildDir, targetPath), renderedCSS) +} + +async function addPug(sourcePath, targetPath) { + function getRelative(staticTarget) { + const pathLayer = (path.dirname(targetPath).replace(/\/$/, "").match(/\//g) || []).length + const prefix = Array(pathLayer).fill("../").join("") + const result = prefix + staticTarget.replace(/^\//, "") + if (result) return result + else return "./" + } + function getStatic(target) { + return getRelative(static.get(target)) + } + function getStaticName(target) { + return getRelative(static.get(target)).replace(/\?.*$/, "") + } + function getLink(target) { + return getRelative(links.get(target)) + } + const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals}) + let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "") + validate(sourcePath, renderedWithoutPHP, "html") + await fs.writeFile(pj(buildDir, targetPath), renderedHTML) +} + +async function addBabel(sourcePath, targetPath) { + const originalCode = await fs.readFile(pj(".", sourcePath), "utf8") + + const compiled = babel.transformSync(originalCode, { + sourceMaps: false, + sourceType: "script", + presets: [ + [ + "@babel/env", { + targets: { + "ie": 11 + } + } + ] + ], + generatorOpts: { + comments: false, + minified: false, + sourceMaps: false, + } + }) + + const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}` + + static.set(sourcePath, filenameWithQuery) + + await Promise.all([ + fs.writeFile(pj(buildDir, targetPath), originalCode), + fs.writeFile(pj(buildDir, minFilename), compiled.code), + fs.writeFile(pj(buildDir, mapFilename), JSON.stringify(compiled.map)) + ]) +} + +;(async () => { + // Stage 1: Register + for (const item of spec) { + if (item.type === "pug") { + links.set(item.source, item.target.replace(/index.html$/, "")) + } + } + + // Stage 2: Build + for (const item of spec) { + if (item.type === "file") { + await addFile(item.source, item.target) + } else if (item.type === "js") { + await addJS(item.source, item.target) + } else if (item.type === "sass") { + await addSass(item.source, item.target) + } else if (item.type === "babel") { + await addBabel(item.source, item.target) + } else if (item.type === "pug") { + await addPug(item.source, item.target) + } else { + throw new Error("Unknown item type: "+item.type) + } + } + + console.log(chalk.green("All files emitted.")) + + await Promise.all(validationQueue).then(v => { + console.log(`validation: using host ${chalk.cyan(validationHost)}`) + v.forEach(cont => cont()) + }) + + console.log(chalk.green("Build complete.") + "\n\n------------\n") +})() diff --git a/build/index.html b/build/index.html new file mode 100644 index 0000000..5af69a2 --- /dev/null +++ b/build/index.html @@ -0,0 +1,78 @@ + + + + + + + Carbon + + +
+
+
+
+
+
+
Directs
+
+
+
+
Channels
+
+
+
+
Fediverse Drama Museum
+
+
+
+
Epicord
+
+
+
+
Invidious
+
+
+
+
+
+
+
+
Carbon brainstorming
+
+
+
+
riley
+
+
+
+
BadAtNames
+
+
+
+
lepton
+
+
+
+
cockandball
+
+
+
+
Bibliogram
+
+
+
+
Monsters Inc Debate Hall
+
+
+
+
DRB clan
+
+
+
+
mettaton simp zone
+
+
+
+
+ + \ No newline at end of file diff --git a/build/static/basic.js b/build/static/basic.js new file mode 100644 index 0000000..c525e80 --- /dev/null +++ b/build/static/basic.js @@ -0,0 +1,148 @@ +/** + * Shortcut for querySelector. + * @template {HTMLElement} T + * @returns {T} + */ +const q = s => document.querySelector(s); +/** + * Shortcut for querySelectorAll. + * @template {HTMLElement} T + * @returns {T[]} + */ +const qa = s => document.querySelectorAll(s); + +/** + * An easier, chainable, object-oriented way to create and update elements + * and children according to related data. Subclass ElemJS to create useful, + * advanced data managers, or just use it inline to quickly make a custom element. + * Created by Cadence Ember in 2018. + */ +class ElemJS { + constructor(type) { + if (type instanceof HTMLElement) { + // If passed an existing element, bind to it + this.bind(type); + } else { + // Otherwise, create a new detached element to bind to + this.bind(document.createElement(type)); + } + this.children = []; + } + + /** Bind this construct to an existing element on the page. */ + bind(element) { + this.element = element; + this.element.js = this; + return this; + } + + /** Add a class. */ + class() { + for (let name of arguments) if (name) this.element.classList.add(name); + return this; + } + + /** Remove a class. */ + removeClass() { + for (let name of arguments) if (name) this.element.classList.remove(name); + return this; + } + + /** Set a JS property on the element. */ + direct(name, value) { + if (name) this.element[name] = value; + return this; + } + + /** Set an attribute on the element. */ + attribute(name, value) { + if (name) this.element.setAttribute(name, value != undefined ? value : ""); + return this; + } + + /** Set a style on the element. */ + style(name, value) { + if (name) this.element.style[name] = value; + return this; + } + + /** Set the element's ID. */ + id(name) { + if (name) this.element.id = name; + return this; + } + + /** Attach a callback function to an event on the element. */ + on(name, callback) { + this.element.addEventListener(name, callback); + return this; + } + + /** Set the element's text. */ + text(name) { + this.element.innerText = name; + return this; + } + + /** Create a text node and add it to the element. */ + addText(name) { + const node = document.createTextNode(name); + this.element.appendChild(node); + return this; + } + + /** Set the element's HTML content. */ + html(name) { + this.element.innerHTML = name; + return this; + } + + /** + * Add children to the element. + * Children can either be an instance of ElemJS, in + * which case the element will be appended as a child, + * or a string, in which case the string will be added as a text node. + * Each child should be a parameter to this method. + */ + child(...children) { + for (const toAdd of children) { + if (typeof toAdd === "object" && toAdd !== null) { + // Should be an instance of ElemJS, so append as child + toAdd.parent = this; + this.element.appendChild(toAdd.element); + this.children.push(toAdd); + } else if (typeof toAdd === "string") { + // Is a string, so add as text node + this.addText(toAdd); + } + } + return this; + } + + /** + * Remove all children from the element. + */ + clearChildren() { + this.children.length = 0; + while (this.element.lastChild) this.element.removeChild(this.element.lastChild); + } + + /** + * Remove this element. + */ + remove() { + let index; + if (this.parent && (index = this.parent.children.indexOf(this)) !== -1) { + this.parent.children.splice(index, 1); + } + this.parent = null; + this.element.remove(); + } +} + +/** Shortcut for `new ElemJS`. */ +function ejs(tag) { + return new ElemJS(tag); +} + +export {q, qa, ElemJS, ejs} diff --git a/build/static/groups.js b/build/static/groups.js new file mode 100644 index 0000000..7de9c02 --- /dev/null +++ b/build/static/groups.js @@ -0,0 +1,15 @@ +import {q} from "./basic.js" + +let state = "CLOSED" + +const groups = q("#c-groups") +const rooms = q("#c-rooms") + +groups.addEventListener("click", () => { + console.log("hello", groups) + groups.classList.add("c-groups__display--closed") +}) + +rooms.addEventListener("mouseout", () => { + groups.classList.remove("c-groups__display--closed") +}) diff --git a/build/static/main.css b/build/static/main.css new file mode 100644 index 0000000..514569f --- /dev/null +++ b/build/static/main.css @@ -0,0 +1,99 @@ +@font-face { + font-family: Whitney; + font-weight: 500; + src: url(/static/whitney-500.woff?static=ba33ed18fe) format("woff2"); +} +body { + font-family: sans-serif; + background-color: #36393e; + color: #ddd; + font-size: 24px; + font-family: Whitney; + margin: 0; + height: 100vh; +} + +.main { + height: 100vh; + display: flex; +} + +.c-rooms { + background-color: #2f3135; + padding: 8px; + width: 240px; + font-size: 20px; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: #42454a #2f3135; +} + +.c-room { + display: flex; + align-items: center; + padding: 8px; + cursor: pointer; +} +.c-room:hover { + background-color: #393c42; + border-radius: 8px; +} +.c-room__icon { + width: 36px; + height: 36px; + background-color: #bbb; + margin-right: 8px; + border-radius: 50%; + flex-shrink: 0; +} +.c-room__name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c-groups { + position: relative; + width: 80px; +} +.c-groups__display { + background-color: #202224; + overflow: hidden; + width: 80px; + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; +} +.c-groups__display:not(.c-groups__display--closed):hover { + width: 300px; +} +.c-groups__container { + width: 300px; + padding: 8px; +} + +.c-group { + display: flex; + align-items: center; + padding: 4px 8px; + cursor: pointer; + border-radius: 8px; +} +.c-group:hover { + background-color: #2f3135; +} +.c-group__icon { + width: 48px; + height: 48px; + background-color: #999; + border-radius: 50%; + margin-right: 16px; + flex-shrink: 0; +} +.c-group__name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/build/static/whitney-500.woff b/build/static/whitney-500.woff new file mode 100644 index 0000000..fc82138 Binary files /dev/null and b/build/static/whitney-500.woff differ diff --git a/spec.js b/spec.js new file mode 100644 index 0000000..ef562ca --- /dev/null +++ b/spec.js @@ -0,0 +1,32 @@ +module.exports = [ + { + type: "file", + source: "/assets/fonts/whitney-500.woff", + target: "/static/whitney-500.woff" + }, + { + type: "file", + source: "/js/basic.js", + target: "/static/basic.js" + }, + { + type: "file", + source: "/js/groups.js", + target: "/static/groups.js" + }, + { + type: "file", + source: "/assets/fonts/whitney-500.woff", + target: "/static/whitney-500.woff" + }, + { + type: "sass", + source: "/sass/main.sass", + target: "/static/main.css" + }, + { + type: "pug", + source: "/home.pug", + target: "/index.html" + } +] diff --git a/src/assets/fonts/whitney-500.woff b/src/assets/fonts/whitney-500.woff new file mode 100644 index 0000000..fc82138 Binary files /dev/null and b/src/assets/fonts/whitney-500.woff differ diff --git a/src/home.pug b/src/home.pug new file mode 100644 index 0000000..420382a --- /dev/null +++ b/src/home.pug @@ -0,0 +1,38 @@ +mixin group(name) + .c-group + .c-group__icon + .c-group__name= name + +mixin room(name) + .c-room + .c-room__icon + .c-room__name= name + +doctype html +html + head + meta(charset="utf-8") + link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass")) + script(type="module" src=getStatic("/js/groups.js")) + title Carbon + body + main.main + .c-groups + .c-groups__display#c-groups + .c-groups__container + +group("Directs") + +group("Channels") + +group("Fediverse Drama Museum") + +group("Epicord") + +group("Invidious") + .c-rooms#c-rooms + +room("Carbon brainstorming") + +room("riley") + +room("BadAtNames") + +room("lepton") + +room("cockandball") + +room("Bibliogram") + +room("Monsters Inc Debate Hall") + +room("DRB clan") + +room("mettaton simp zone") + .c-chat diff --git a/src/js/basic.js b/src/js/basic.js new file mode 100644 index 0000000..c525e80 --- /dev/null +++ b/src/js/basic.js @@ -0,0 +1,148 @@ +/** + * Shortcut for querySelector. + * @template {HTMLElement} T + * @returns {T} + */ +const q = s => document.querySelector(s); +/** + * Shortcut for querySelectorAll. + * @template {HTMLElement} T + * @returns {T[]} + */ +const qa = s => document.querySelectorAll(s); + +/** + * An easier, chainable, object-oriented way to create and update elements + * and children according to related data. Subclass ElemJS to create useful, + * advanced data managers, or just use it inline to quickly make a custom element. + * Created by Cadence Ember in 2018. + */ +class ElemJS { + constructor(type) { + if (type instanceof HTMLElement) { + // If passed an existing element, bind to it + this.bind(type); + } else { + // Otherwise, create a new detached element to bind to + this.bind(document.createElement(type)); + } + this.children = []; + } + + /** Bind this construct to an existing element on the page. */ + bind(element) { + this.element = element; + this.element.js = this; + return this; + } + + /** Add a class. */ + class() { + for (let name of arguments) if (name) this.element.classList.add(name); + return this; + } + + /** Remove a class. */ + removeClass() { + for (let name of arguments) if (name) this.element.classList.remove(name); + return this; + } + + /** Set a JS property on the element. */ + direct(name, value) { + if (name) this.element[name] = value; + return this; + } + + /** Set an attribute on the element. */ + attribute(name, value) { + if (name) this.element.setAttribute(name, value != undefined ? value : ""); + return this; + } + + /** Set a style on the element. */ + style(name, value) { + if (name) this.element.style[name] = value; + return this; + } + + /** Set the element's ID. */ + id(name) { + if (name) this.element.id = name; + return this; + } + + /** Attach a callback function to an event on the element. */ + on(name, callback) { + this.element.addEventListener(name, callback); + return this; + } + + /** Set the element's text. */ + text(name) { + this.element.innerText = name; + return this; + } + + /** Create a text node and add it to the element. */ + addText(name) { + const node = document.createTextNode(name); + this.element.appendChild(node); + return this; + } + + /** Set the element's HTML content. */ + html(name) { + this.element.innerHTML = name; + return this; + } + + /** + * Add children to the element. + * Children can either be an instance of ElemJS, in + * which case the element will be appended as a child, + * or a string, in which case the string will be added as a text node. + * Each child should be a parameter to this method. + */ + child(...children) { + for (const toAdd of children) { + if (typeof toAdd === "object" && toAdd !== null) { + // Should be an instance of ElemJS, so append as child + toAdd.parent = this; + this.element.appendChild(toAdd.element); + this.children.push(toAdd); + } else if (typeof toAdd === "string") { + // Is a string, so add as text node + this.addText(toAdd); + } + } + return this; + } + + /** + * Remove all children from the element. + */ + clearChildren() { + this.children.length = 0; + while (this.element.lastChild) this.element.removeChild(this.element.lastChild); + } + + /** + * Remove this element. + */ + remove() { + let index; + if (this.parent && (index = this.parent.children.indexOf(this)) !== -1) { + this.parent.children.splice(index, 1); + } + this.parent = null; + this.element.remove(); + } +} + +/** Shortcut for `new ElemJS`. */ +function ejs(tag) { + return new ElemJS(tag); +} + +export {q, qa, ElemJS, ejs} diff --git a/src/js/groups.js b/src/js/groups.js new file mode 100644 index 0000000..7de9c02 --- /dev/null +++ b/src/js/groups.js @@ -0,0 +1,15 @@ +import {q} from "./basic.js" + +let state = "CLOSED" + +const groups = q("#c-groups") +const rooms = q("#c-rooms") + +groups.addEventListener("click", () => { + console.log("hello", groups) + groups.classList.add("c-groups__display--closed") +}) + +rooms.addEventListener("mouseout", () => { + groups.classList.remove("c-groups__display--closed") +}) diff --git a/src/sass/base.sass b/src/sass/base.sass new file mode 100644 index 0000000..4321ab5 --- /dev/null +++ b/src/sass/base.sass @@ -0,0 +1,17 @@ +@font-face + font-family: Whitney + font-weight: 500 + src: url(static("/assets/fonts/whitney-500.woff")) format("woff2") + +body + font-family: sans-serif + background-color: #36393e + color: #ddd + font-size: 24px + font-family: Whitney + margin: 0 + height: 100vh + +.main + height: 100vh + display: flex diff --git a/src/sass/colors.sass b/src/sass/colors.sass new file mode 100644 index 0000000..f69d71b --- /dev/null +++ b/src/sass/colors.sass @@ -0,0 +1,5 @@ +$dark: #36393e +$darker: #2f3135 +$darkest: #202224 +$mild: #393c42 +$milder: #42454a diff --git a/src/sass/components/groups.sass b/src/sass/components/groups.sass new file mode 100644 index 0000000..376c22a --- /dev/null +++ b/src/sass/components/groups.sass @@ -0,0 +1,51 @@ +@use "../colors" as c +@use "./rooms" as rooms + +$icon-size: 48px +$icon-padding: 8px +$base-width: $icon-size + $icon_padding * 4 +$out-width: $base-width + rooms.$list-width - 20px + +.c-groups + position: relative + width: $base-width + + &__display + background-color: c.$darkest + overflow: hidden + width: $base-width + position: absolute + left: 0 + top: 0 + bottom: 0 + right: 0 + + &:not(&--closed):hover + width: $out-width + + &__container + width: $out-width + padding: $icon-padding + +.c-group + display: flex + align-items: center + padding: $icon-padding / 2 $icon-padding + cursor: pointer + border-radius: 8px + + &:hover + background-color: c.$darker + + &__icon + width: $icon-size + height: $icon-size + background-color: #999 + border-radius: 50% + margin-right: $icon-padding * 2 + flex-shrink: 0 + + &__name + white-space: nowrap + overflow: hidden + text-overflow: ellipsis diff --git a/src/sass/components/rooms.sass b/src/sass/components/rooms.sass new file mode 100644 index 0000000..1fd18e2 --- /dev/null +++ b/src/sass/components/rooms.sass @@ -0,0 +1,37 @@ +@use "../colors" as c + +$list-width: 240px +$icon-size: 36px +$icon-padding: 8px + +.c-rooms + background-color: c.$darker + padding: $icon-padding + width: $list-width + font-size: 20px + overflow-y: scroll + scrollbar-width: thin + scrollbar-color: c.$milder c.$darker + +.c-room + display: flex + align-items: center + padding: $icon-padding + cursor: pointer + + &:hover + background-color: c.$mild + border-radius: 8px + + &__icon + width: $icon-size + height: $icon-size + background-color: #bbb + margin-right: $icon-padding + border-radius: 50% + flex-shrink: 0 + + &__name + white-space: nowrap + overflow: hidden + text-overflow: ellipsis diff --git a/src/sass/main.sass b/src/sass/main.sass new file mode 100644 index 0000000..40317cf --- /dev/null +++ b/src/sass/main.sass @@ -0,0 +1,3 @@ +@use "./base" +@use "./components/groups" +@use "./components/rooms"