Merge pull request #7 from vector-im/bwindels/theming

Initial theming support
This commit is contained in:
Bruno Windels 2020-08-12 14:44:26 +00:00 committed by GitHub
commit 5183615994
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 778 additions and 245 deletions

View file

@ -9,38 +9,17 @@
<meta name="apple-mobile-web-app-title" content="Hydrogen Chat"> <meta name="apple-mobile-web-app-title" content="Hydrogen Chat">
<meta name="description" content="A matrix chat application"> <meta name="description" content="A matrix chat application">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css"> <link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/themes/element/theme.css" title="Element Theme">
<link rel="alternate stylesheet" type="text/css" href="src/ui/web/css/themes/bubbles/theme.css" title="Bubbles Theme">
</head> </head>
<body class="hydrogen"> <body class="hydrogen">
<script id="version" type="disabled"> <script id="version" type="disabled">
</script>
<script id="phone-debug-pre" type="disabled">
window.DEBUG = true;
window.debugConsoleBuffer = "";
console.error = (...params) => {
const lastLines = "...\n" + window.debugConsoleBuffer.split("\n").slice(-10).join("\n");
// window.debugConsoleBuffer = window.debugConsoleBuffer + "ERR " + params.join(" ") + "\n";
// const location = new Error().stack.split("\n")[2];
alert(params.join(" ") +"\n...\n" + lastLines);
};
console.log = console.info = console.warn = (...params) => {
window.debugConsoleBuffer = window.debugConsoleBuffer + params.join(" ") + "\n";
};
window.HYDROGEN_VERSION = "%%VERSION%%"; window.HYDROGEN_VERSION = "%%VERSION%%";
</script> </script>
<script id="main" type="module"> <script id="main" type="module">
import main from "./src/main.js"; import {main} from "./src/main.js";
main(document.body); main(document.body);
</script> </script>
<script id="phone-debug-post" type="disabled">
setTimeout(() => {
const showlogs = document.getElementById("showlogs");
showlogs.addEventListener("click", () => {
const lastLines = "...\n" + window.debugConsoleBuffer.split("\n").slice(-20).join("\n");
alert(lastLines);
}, true);
showlogs.innerText = "Show last 20 log lines";
}, 5000);
</script>
<script id="service-worker" type="disabled"> <script id="service-worker" type="disabled">
if('serviceWorker' in navigator) { if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js') navigator.serviceWorker.register('sw.js')

View file

@ -43,6 +43,7 @@ const PROJECT_NAME = "Hydrogen Chat";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../"); const projectDir = path.join(__dirname, "../");
const cssDir = path.join(projectDir, "src/ui/web/css/");
const targetDir = path.join(projectDir, "target"); const targetDir = path.join(projectDir, "target");
const {debug, noOffline, legacy} = process.argv.reduce((params, param) => { const {debug, noOffline, legacy} = process.argv.reduce((params, param) => {
@ -67,31 +68,80 @@ async function build() {
if (legacy) { if (legacy) {
bundleName = `${PROJECT_ID}-legacy.js`; bundleName = `${PROJECT_ID}-legacy.js`;
} }
await buildHtml(version, bundleName);
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8");
const doc = cheerio.load(devHtml);
const themes = [];
findThemes(doc, themeName => {
themes.push(themeName);
});
// also creates the directories where the theme css bundles are placed in,
// so do it first
const themeAssets = await copyThemeAssets(themes, legacy);
await buildHtml(doc, version, bundleName);
if (legacy) { if (legacy) {
await buildJsLegacy(bundleName); await buildJsLegacy(bundleName);
await buildCssLegacy();
} else { } else {
await buildJs(bundleName); await buildJs(bundleName);
await buildCss();
} }
await buildCssBundles(legacy ? buildCssLegacy : buildCss, themes);
if (offline) { if (offline) {
await buildOffline(version, bundleName); await buildOffline(version, bundleName, themeAssets);
} }
console.log(`built ${PROJECT_ID}${legacy ? " legacy" : ""} ${version} successfully`); console.log(`built ${PROJECT_ID}${legacy ? " legacy" : ""} ${version} successfully`);
} }
async function buildHtml(version, bundleName) { async function findThemes(doc, callback) {
doc("link[rel~=stylesheet][title]").each((i, el) => {
const theme = doc(el);
const href = theme.attr("href");
const themesPrefix = "/themes/";
const prefixIdx = href.indexOf(themesPrefix);
if (prefixIdx !== -1) {
const themeNameStart = prefixIdx + themesPrefix.length;
const themeNameEnd = href.indexOf("/", themeNameStart);
const themeName = href.substr(themeNameStart, themeNameEnd - themeNameStart);
callback(themeName, theme);
}
});
}
async function copyThemeAssets(themes, legacy) {
const assets = [];
// create theme directories and copy assets
await fs.mkdir(path.join(targetDir, "themes"));
for (const theme of themes) {
assets.push(`themes/${theme}/bundle.css`);
const themeDstFolder = path.join(targetDir, `themes/${theme}`);
await fs.mkdir(themeDstFolder);
const themeSrcFolder = path.join(cssDir, `themes/${theme}`);
await copyFolder(themeSrcFolder, themeDstFolder, file => {
const isUnneededFont = legacy ? file.endsWith(".woff2") : file.endsWith(".woff");
if (!file.endsWith(".css") && !isUnneededFont) {
assets.push(file.substr(cssDir.length));
return true;
}
return false;
});
}
return assets;
}
async function buildHtml(doc, version, bundleName) {
// transform html file // transform html file
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8"); // change path to main.css to css bundle
const doc = cheerio.load(devHtml); doc("link[rel=stylesheet]:not([title])").attr("href", `${PROJECT_ID}.css`);
doc("link[rel=stylesheet]").attr("href", `${PROJECT_ID}.css`); // change paths to all theme stylesheets
findThemes(doc, (themeName, theme) => {
theme.attr("href", `themes/${themeName}/bundle.css`);
});
doc("script#main").replaceWith( doc("script#main").replaceWith(
`<script type="text/javascript" src="${bundleName}"></script>` + `<script type="text/javascript" src="${bundleName}"></script>` +
`<script type="text/javascript">${PROJECT_ID}Bundle.main(document.body);</script>`); `<script type="text/javascript">${PROJECT_ID}Bundle.main(document.body);</script>`);
removeOrEnableScript(doc("script#phone-debug-pre"), debug);
removeOrEnableScript(doc("script#phone-debug-post"), debug);
removeOrEnableScript(doc("script#service-worker"), offline); removeOrEnableScript(doc("script#service-worker"), offline);
const versionScript = doc("script#version"); const versionScript = doc("script#version");
@ -146,9 +196,17 @@ async function buildJsLegacy(bundleName) {
}); });
} }
async function buildOffline(version, bundleName) { async function buildOffline(version, bundleName, themeAssets) {
const {offlineAssets, cacheAssets} = themeAssets.reduce((result, asset) => {
if (asset.endsWith(".css")) {
result.offlineAssets.push(asset);
} else {
result.cacheAssets.push(asset);
}
return result;
}, {offlineAssets: [], cacheAssets: []});
// write offline availability // write offline availability
const offlineFiles = [bundleName, `${PROJECT_ID}.css`, "index.html", "icon-192.png"]; const offlineFiles = [bundleName, `${PROJECT_ID}.css`, "index.html", "icon-192.png"].concat(offlineAssets);
// write appcache manifest // write appcache manifest
const manifestLines = [ const manifestLines = [
@ -164,7 +222,8 @@ async function buildOffline(version, bundleName) {
// write service worker // write service worker
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8"); let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`); swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
swSource = swSource.replace(`"%%FILES%%"`, JSON.stringify(offlineFiles)); swSource = swSource.replace(`"%%OFFLINE_FILES%%"`, JSON.stringify(offlineFiles));
swSource = swSource.replace(`"%%CACHE_FILES%%"`, JSON.stringify(cacheAssets));
await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8"); await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8");
// write web manifest // write web manifest
const webManifest = { const webManifest = {
@ -180,25 +239,31 @@ async function buildOffline(version, bundleName) {
await fs.writeFile(path.join(targetDir, "icon-192.png"), icon); await fs.writeFile(path.join(targetDir, "icon-192.png"), icon);
} }
async function buildCss() { async function buildCssBundles(buildFn, themes) {
// create css bundle const cssMainFile = path.join(cssDir, "main.css");
const cssMainFile = path.join(projectDir, "src/ui/web/css/main.css"); await buildFn(cssMainFile, path.join(targetDir, `${PROJECT_ID}.css`));
const preCss = await fs.readFile(cssMainFile, "utf8"); for (const theme of themes) {
await buildFn(
path.join(cssDir, `themes/${theme}/theme.css`),
path.join(targetDir, `themes/${theme}/bundle.css`)
);
}
}
async function buildCss(entryPath, bundlePath) {
const preCss = await fs.readFile(entryPath, "utf8");
const cssBundler = postcss([postcssImport]); const cssBundler = postcss([postcssImport]);
const result = await cssBundler.process(preCss, {from: cssMainFile}); const result = await cssBundler.process(preCss, {from: entryPath});
await fs.writeFile(path.join(targetDir, `${PROJECT_ID}.css`), result.css, "utf8"); await fs.writeFile(bundlePath, result.css, "utf8");
} }
async function buildCssLegacy() { async function buildCssLegacy(entryPath, bundlePath) {
// create css bundle const preCss = await fs.readFile(entryPath, "utf8");
const cssMainFile = path.join(projectDir, "src/ui/web/css/main.css");
const preCss = await fs.readFile(cssMainFile, "utf8");
const cssBundler = postcss([postcssImport, cssvariables(), flexbugsFixes()]); const cssBundler = postcss([postcssImport, cssvariables(), flexbugsFixes()]);
const result = await cssBundler.process(preCss, {from: cssMainFile}); const result = await cssBundler.process(preCss, {from: entryPath});
await fs.writeFile(path.join(targetDir, `${PROJECT_ID}.css`), result.css, "utf8"); await fs.writeFile(bundlePath, result.css, "utf8");
} }
function removeOrEnableScript(scriptNode, enable) { function removeOrEnableScript(scriptNode, enable) {
if (enable) { if (enable) {
scriptNode.attr("type", "text/javascript"); scriptNode.attr("type", "text/javascript");
@ -209,9 +274,7 @@ function removeOrEnableScript(scriptNode, enable) {
async function removeDirIfExists(targetDir) { async function removeDirIfExists(targetDir) {
try { try {
const files = await fs.readdir(targetDir); await fs.rmdir(targetDir, {recursive: true});
await Promise.all(files.map(filename => fs.unlink(path.join(targetDir, filename))));
await fs.rmdir(targetDir);
} catch (err) { } catch (err) {
if (err.code !== "ENOENT") { if (err.code !== "ENOENT") {
throw err; throw err;
@ -219,4 +282,18 @@ async function removeDirIfExists(targetDir) {
} }
} }
async function copyFolder(srcRoot, dstRoot, filter) {
const dirEnts = await fs.readdir(srcRoot, {withFileTypes: true});
for (const dirEnt of dirEnts) {
const dstPath = path.join(dstRoot, dirEnt.name);
const srcPath = path.join(srcRoot, dirEnt.name);
if (dirEnt.isDirectory()) {
await fs.mkdir(dstPath);
await copyFolder(srcPath, dstPath, filter);
} else if (dirEnt.isFile() && filter(srcPath)) {
await fs.copyFile(srcPath, dstPath);
}
}
}
build().catch(err => console.error(err)); build().catch(err => console.error(err));

View file

@ -15,13 +15,17 @@ limitations under the License.
*/ */
const VERSION = "%%VERSION%%"; const VERSION = "%%VERSION%%";
const FILES = "%%FILES%%"; const OFFLINE_FILES = "%%OFFLINE_FILES%%";
const cacheName = `brawl-${VERSION}`; // TODO: cache these files when requested
// The difficulty is that these are relative filenames, and we don't have access to document.baseURI
// Clients.match({type: "window"}).url and assume they are all the same? they really should be ... safari doesn't support this though
const CACHE_FILES = "%%CACHE_FILES%%";
const cacheName = `hydrogen-${VERSION}`;
self.addEventListener('install', function(e) { self.addEventListener('install', function(e) {
e.waitUntil( e.waitUntil(
caches.open(cacheName).then(function(cache) { caches.open(cacheName).then(function(cache) {
return cache.addAll(FILES); return cache.addAll(OFFLINE_FILES);
}) })
); );
}); });

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,7 +19,6 @@ limitations under the License.
--avatar-size: 32px; --avatar-size: 32px;
width: var(--avatar-size); width: var(--avatar-size);
height: var(--avatar-size); height: var(--avatar-size);
border-radius: 100px;
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
-moz-user-select: none; -moz-user-select: none;
@ -29,8 +29,6 @@ limitations under the License.
font-size: calc(var(--avatar-size) * 0.6); font-size: calc(var(--avatar-size) * 0.6);
text-align: center; text-align: center;
letter-spacing: calc(var(--avatar-size) * -0.05); letter-spacing: calc(var(--avatar-size) * -0.05);
background: white;
color: black;
speak: none; speak: none;
} }

15
src/ui/web/css/font.css Normal file
View file

@ -0,0 +1,15 @@
/** from https://gist.github.com/mfornos/9991865 */
@font-face {
font-family: 'emoji';
src: local('Apple Color Emoji'),
local('Segoe UI Emoji'),
local('Segoe UI Symbol'),
local('Noto Color Emoji'),
local('Android Emoji'),
local('EmojiSymbols'),
local('Symbola');
/* Emoji unicode blocks */
unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF;
}

26
src/ui/web/css/form.css Normal file
View file

@ -0,0 +1,26 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.form > div {
margin: 0.4em 0;
}
.form input {
display: block;
width: 100%;
box-sizing: border-box;
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,9 +18,6 @@ limitations under the License.
html { html {
height: 100%; height: 100%;
} }
body {
margin: 0;
}
.SessionView { .SessionView {
display: flex; display: flex;

View file

@ -16,8 +16,6 @@ limitations under the License.
.LeftPanel { .LeftPanel {
background: #333;
color: white;
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; overscroll-behavior: contain;
} }
@ -29,24 +27,10 @@ limitations under the License.
} }
.LeftPanel li { .LeftPanel li {
margin: 5px;
padding: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.LeftPanel li {
border-bottom: 1px #555 solid;
}
.LeftPanel li:last-child {
border-bottom: none;
}
.LeftPanel li > * {
margin-right: 10px;
}
.LeftPanel div.description { .LeftPanel div.description {
margin: 0; margin: 0;
flex: 1 1 0; flex: 1 1 0;
@ -58,7 +42,3 @@ limitations under the License.
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.LeftPanel .description .last-message {
font-size: 0.8em;
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,6 +15,39 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/** contains styles for everything before the session view, like the session picker, login, load view, ... */
.SessionPickerView {
padding: 0.4em;
}
.SessionPickerView ul {
list-style: none;
padding: 0;
}
.SessionPickerView li {
margin: 0.4em 0;
padding: 0.5em;
}
.SessionPickerView .sessionInfo {
cursor: pointer;
display: flex;
}
.SessionPickerView li span.userId {
flex: 1;
}
.SessionPickerView li span.error {
margin: 0 20px;
}
.LoginView {
padding: 0.4em;
}
.SessionLoadView { .SessionLoadView {
display: flex; display: flex;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -13,7 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import url('font.css');
@import url('layout.css'); @import url('layout.css');
@import url('login.css'); @import url('login.css');
@import url('left-panel.css'); @import url('left-panel.css');
@ -21,6 +22,8 @@ limitations under the License.
@import url('timeline.css'); @import url('timeline.css');
@import url('avatar.css'); @import url('avatar.css');
@import url('spinner.css'); @import url('spinner.css');
@import url('form.css');
@import url('status.css');
/* only if the body contains the whole app (e.g. we're not embedded in a page), make some changes */ /* only if the body contains the whole app (e.g. we're not embedded in a page), make some changes */
body.hydrogen { body.hydrogen {
@ -32,10 +35,6 @@ body.hydrogen {
.hydrogen { .hydrogen {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
background-color: black;
color: white;
} }
.hiddenWithLayout { .hiddenWithLayout {
@ -45,78 +44,3 @@ body.hydrogen {
.hidden { .hidden {
display: none !important; display: none !important;
} }
.SessionStatusView {
display: flex;
padding: 5px;
background-color: #555;
}
.SessionStatusView p {
margin: 0 10px;
word-break: break-all;
word-break: break-word;
}
.SessionStatusView button {
border: none;
background: none;
color: currentcolor;
text-decoration: underline;
}
.RoomPlaceholderView {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
}
.SessionPickerView {
padding: 0.4em;
}
.SessionPickerView ul {
list-style: none;
padding: 0;
}
.SessionPickerView li {
margin: 0.4em 0;
font-size: 1.2em;
background-color: grey;
padding: 0.5em;
}
.SessionPickerView .sessionInfo {
cursor: pointer;
display: flex;
}
.SessionPickerView li span.userId {
flex: 1;
}
.SessionPickerView li span.error {
margin: 0 20px;
}
.LoginView {
padding: 0.4em;
}
a {
color: white;
}
.form > div {
margin: 0.4em 0;
}
.form input {
display: block;
width: 100%;
box-sizing: border-box;
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,10 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.RoomPlaceholderView {
display: flex;
flex-direction: row;
}
.RoomHeader { .RoomHeader {
padding: 10px; align-items: center;
background-color: #333;
} }
.RoomHeader > *:last-child { .RoomHeader > *:last-child {
@ -30,16 +34,7 @@ limitations under the License.
} }
.RoomHeader button { .RoomHeader button {
width: 40px;
height: 40px;
display: none;
font-size: 1.5em;
padding: 0;
display: block; display: block;
background: white;
border: none;
font-weight: bolder;
line-height: 40px;
} }
.RoomHeader .back { .RoomHeader .back {
@ -52,24 +47,11 @@ limitations under the License.
} }
.RoomHeader .topic { .RoomHeader .topic {
font-size: 0.8em;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.back::before {
content: "☰";
}
.more::before {
content: "⋮";
}
.RoomHeader {
align-items: center;
}
.RoomHeader .description { .RoomHeader .description {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
@ -82,14 +64,8 @@ limitations under the License.
margin: 0; margin: 0;
} }
.RoomView_error {
color: red;
}
.MessageComposer > input { .MessageComposer > input {
display: block; display: block;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 0.8em;
border: none;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -37,6 +38,12 @@ limitations under the License.
animation-duration: 2s; animation-duration: 2s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;
/**
* TODO
* see if with IE11 we can just set a static stroke state and make it rotate?
*/
stroke-dasharray: 0 0 10 90;
fill: none; fill: none;
stroke: currentcolor; stroke: currentcolor;
stroke-width: 12; stroke-width: 12;

33
src/ui/web/css/status.css Normal file
View file

@ -0,0 +1,33 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.SessionStatusView {
display: flex;
}
.SessionStatusView p {
margin: 0 10px;
word-break: break-all;
word-break: break-word;
}
.SessionStatusView button {
border: none;
background: none;
color: currentcolor;
text-decoration: underline;
}

View file

@ -0,0 +1,7 @@
things that go in the theme:
- margin specialization
- padding
- colors (foreground, background, border, ...)
- border-radius
- font faces, weights and sizes
- alignment

View file

@ -0,0 +1,186 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.hydrogen {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, sans-serif, 'emoji';
background-color: black;
color: white;
}
.avatar {
border-radius: 100%;
background: white;
color: black;
}
.LeftPanel {
background: #333;
color: white;
}
.LeftPanel ul {
padding: 0;
margin: 0;
}
.LeftPanel li {
margin: 5px;
padding: 10px;
/* vertical align */
align-items: center;
}
.LeftPanel li {
border-bottom: 1px #555 solid;
}
.LeftPanel li:last-child {
border-bottom: none;
}
.LeftPanel li > * {
margin-right: 10px;
}
.LeftPanel .description .last-message {
font-size: 0.8em;
}
a {
color: white;
}
.SessionStatusView {
padding: 5px;
background-color: #555;
}
.RoomPlaceholderView {
align-items: center;
justify-content: center;
}
.SessionPickerView li {
font-size: 1.2em;
background-color: grey;
}
.RoomHeader {
padding: 10px;
background-color: #333;
}
.RoomHeader button {
width: 40px;
height: 40px;
font-size: 1.5em;
padding: 0;
background: white;
border: none;
font-weight: bolder;
line-height: 40px;
}
.back::before {
content: "☰";
}
.more::before {
content: "⋮";
}
.RoomHeader .topic {
font-size: 0.8em;
}
.RoomHeader {
padding: 10px;
background-color: #333;
}
.RoomView_error {
color: red;
}
.MessageComposer > input {
padding: 0.8em;
border: none;
}
.message-container {
max-width: 80%;
padding: 5px 10px;
margin: 5px 10px;
background: blue;
}
.message-container .sender {
margin: 5px 0;
font-size: 0.9em;
font-weight: bold;
}
.TextMessageView .message-container time {
padding: 2px 0 0px 20px;
font-size: 0.9em;
color: lightblue;
}
.message-container time {
font-size: 0.9em;
color: lightblue;
}
.own time {
color: lightgreen;
}
.own .message-container {
background-color: darkgreen;
}
.TextMessageView.own .message-container {
margin-left: auto;
}
.TextMessageView.pending .message-container {
background-color: #333;
}
.TextMessageView .message-container time {
float: right;
}
.message-container p {
margin: 5px 0;
}
.AnnouncementView {
margin: 5px 0;
padding: 5px 10%;
}
.AnnouncementView > div {
margin: 0 auto;
padding: 10px 20px;
background-color: #333;
font-size: 0.9em;
color: #CCC;
text-align: center;
}

View file

@ -0,0 +1,152 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("inter/Inter-Thin.woff2?v=3.13") format("woff2"),
url("inter/Inter-Thin.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("inter/Inter-ThinItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-ThinItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("inter/Inter-ExtraLight.woff2?v=3.13") format("woff2"),
url("inter/Inter-ExtraLight.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("inter/Inter-ExtraLightItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-ExtraLightItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("inter/Inter-Light.woff2?v=3.13") format("woff2"),
url("inter/Inter-Light.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("inter/Inter-LightItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-LightItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("inter/Inter-Regular.woff2?v=3.13") format("woff2"),
url("inter/Inter-Regular.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("inter/Inter-Italic.woff2?v=3.13") format("woff2"),
url("inter/Inter-Italic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("inter/Inter-Medium.woff2?v=3.13") format("woff2"),
url("inter/Inter-Medium.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-MediumItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("inter/Inter-SemiBold.woff2?v=3.13") format("woff2"),
url("inter/Inter-SemiBold.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("inter/Inter-Bold.woff2?v=3.13") format("woff2"),
url("inter/Inter-Bold.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-BoldItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("inter/Inter-ExtraBold.woff2?v=3.13") format("woff2"),
url("inter/Inter-ExtraBold.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("inter/Inter-ExtraBoldItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-ExtraBoldItalic.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("inter/Inter-Black.woff2?v=3.13") format("woff2"),
url("inter/Inter-Black.woff?v=3.13") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("inter/Inter-BlackItalic.woff2?v=3.13") format("woff2"),
url("inter/Inter-BlackItalic.woff?v=3.13") format("woff");
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,188 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
@import url('inter.css');
.hydrogen {
font-family: 'Inter', sans-serif, 'emoji';
background-color: white;
color: black;
}
.avatar {
border-radius: 100%;
background: black;
color: white;
}
.LeftPanel {
background: #333;
color: white;
}
.LeftPanel ul {
padding: 0;
margin: 0;
}
.LeftPanel li {
margin: 5px;
padding: 10px;
/* vertical align */
align-items: center;
}
.LeftPanel li {
border-bottom: 1px #555 solid;
}
.LeftPanel li:last-child {
border-bottom: none;
}
.LeftPanel li > * {
margin-right: 10px;
}
.LeftPanel .description .last-message {
font-size: 0.8em;
}
a {
color: white;
}
.SessionStatusView {
padding: 5px;
background-color: #555;
}
.RoomPlaceholderView {
align-items: center;
justify-content: center;
}
.SessionPickerView li {
font-size: 1.2em;
background-color: grey;
}
.RoomHeader {
padding: 10px;
background-color: #333;
}
.RoomHeader button {
width: 40px;
height: 40px;
font-size: 1.5em;
padding: 0;
background: white;
border: none;
font-weight: bolder;
line-height: 40px;
}
.back::before {
content: "☰";
}
.more::before {
content: "⋮";
}
.RoomHeader .topic {
font-size: 0.8em;
}
.RoomHeader {
padding: 10px;
background-color: #333;
}
.RoomView_error {
color: red;
}
.MessageComposer > input {
padding: 0.8em;
border: none;
}
.message-container {
max-width: 80%;
padding: 5px 10px;
margin: 5px 10px;
background: blue;
}
.message-container .sender {
margin: 5px 0;
font-size: 0.9em;
font-weight: bold;
}
.TextMessageView .message-container time {
padding: 2px 0 0px 20px;
font-size: 0.9em;
color: lightblue;
}
.message-container time {
font-size: 0.9em;
color: lightblue;
}
.own time {
color: lightgreen;
}
.own .message-container {
background-color: darkgreen;
}
.TextMessageView.own .message-container {
margin-left: auto;
}
.TextMessageView.pending .message-container {
background-color: #333;
}
.TextMessageView .message-container time {
float: right;
}
.message-container p {
margin: 5px 0;
}
.AnnouncementView {
margin: 5px 0;
padding: 5px 10%;
}
.AnnouncementView > div {
margin: 0 auto;
padding: 10px 20px;
background-color: #333;
font-size: 0.9em;
color: #CCC;
text-align: center;
}

View file

@ -28,21 +28,11 @@ limitations under the License.
.message-container { .message-container {
flex: 0 1 auto; flex: 0 1 auto;
max-width: 80%;
padding: 5px 10px;
margin: 5px 10px;
background: blue;
/* first try break-all, then break-word, which isn't supported everywhere */ /* first try break-all, then break-word, which isn't supported everywhere */
word-break: break-all; word-break: break-all;
word-break: break-word; word-break: break-word;
} }
.message-container .sender {
margin: 5px 0;
font-size: 0.9em;
font-weight: bold;
}
.message-container img { .message-container img {
display: block; display: block;
width: 100%; width: 100%;
@ -54,50 +44,7 @@ limitations under the License.
min-width: 0; min-width: 0;
} }
.TextMessageView.own .message-container {
margin-left: auto;
}
.TextMessageView .message-container time {
float: right;
padding: 2px 0 0px 20px;
font-size: 0.9em;
color: lightblue;
}
.message-container time {
font-size: 0.9em;
color: lightblue;
}
.own time {
color: lightgreen;
}
.own .message-container {
background-color: darkgreen;
}
.TextMessageView.pending .message-container {
background-color: #333;
}
.message-container p {
margin: 5px 0;
}
.AnnouncementView { .AnnouncementView {
margin: 5px 0;
padding: 5px 10%;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.AnnouncementView > div {
margin: 0 auto;
padding: 10px 20px;
background-color: #333;
font-size: 0.9em;
color: #CCC;
text-align: center;
}

View file

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="css/main.css"> <link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="css/themes/bubbles/theme.css">
</head> </head>
<body> <body>
<script type="text/javascript"> <script type="text/javascript">
@ -53,6 +54,7 @@
<script id="main" type="module"> <script id="main" type="module">
import {LoginView} from "./login/LoginView.js"; import {LoginView} from "./login/LoginView.js";
const view = new LoginView(vm({ const view = new LoginView(vm({
isBusy: true,
loadViewModel: vm({ loadViewModel: vm({
loadLabel: "Doing something important...", loadLabel: "Doing something important...",
loading: true, loading: true,