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
+
+
+
+
+
+
+
+
+
+
+
Fediverse Drama Museum
+
+
+
+
+
+
+
+
+
+
Carbon brainstorming
+
+
+
+
+
+
+
+
+
Monsters Inc Debate Hall
+
+
+
+
+
+
+
+
\ 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"