mirror of
https://github.com/godotengine/godot-interactive-changelog.git
synced 2025-12-31 01:49:28 +03:00
Initial commit
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Project folders.
|
||||
node_modules/
|
||||
out/
|
||||
logs/
|
||||
|
||||
# Development environments.
|
||||
.idea/
|
||||
.vscode/
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright © 2023-present Godot Engine contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Godot Interactive Changelog
|
||||
|
||||
This project is provided for Godot Engine users and contributors to document
|
||||
in a complete and comprehensive manner changes that go into each release and
|
||||
developer preview of the engine. This project aims to largely remove the manual
|
||||
labor from creating and maintaining a project changelog, and provides tools
|
||||
to generate static information, such as CHANGELOG.md and release notes for the blog.
|
||||
|
||||
The benefit of this automated system is that it allows to track PRs as well as
|
||||
individual commits, preserving important context for the work that goes into
|
||||
each release. For example, it can also detect commits that were cherry-picked,
|
||||
and identify their original PRs.
|
||||
|
||||
Live website: https://godotengine.github.io/godot-interactive-changelog/
|
||||
|
||||
## Contributing
|
||||
|
||||
This project is written in JavaScript and is built using Node.JS. HTML and CSS are
|
||||
used for the presentation. The end result of the build process is completely static
|
||||
and can be server from any web server, no Node.JS required.
|
||||
|
||||
Front-end is designed in a reactive manner using industry standard Web Components
|
||||
(powered by `lit-element`). This provides native browser support, and results in a
|
||||
small overhead from the build process.
|
||||
|
||||
To build the project locally you need to have Node.JS installed (12.x and newer
|
||||
should work just fine).
|
||||
|
||||
This project uses GitHub's GraphQL API. To fetch live data you need to generate
|
||||
a [personal OAuth token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
|
||||
You can supply your token to the scripts using the `GRAPHQL_TOKEN` environment
|
||||
variable. Note, that if you don't have member access to the organization, you
|
||||
may not be able to access all the information used when generating the database.
|
||||
|
||||
1. Clone or download the project.
|
||||
2. From the project root run `npm install` or `yarn` to install dependencies.
|
||||
3. Run `npm run build` or `yarn run build` to build the pages.
|
||||
4. Run `npm run compose-db` or `yarn run compose-db` to fetch the data from GitHub.
|
||||
5. Serve the `out/` folder with your method of choice (e.g. using Python 3:
|
||||
`python -m http.server 8080 -d ./out`).
|
||||
|
||||
`rollup` is used for browser packing of scripts and copying of static assets. The
|
||||
data fetching script is plain JavaScript with `node-fetch` used to polyfill
|
||||
`fetch()`-like API.
|
||||
|
||||
## License
|
||||
|
||||
This project is provided under the [MIT License](LICENSE.md).
|
||||
41
build/posthtml-include.js
Normal file
41
build/posthtml-include.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import parser from "posthtml-parser";
|
||||
|
||||
export default function(options) {
|
||||
options = options || {};
|
||||
options.root = options.root || './';
|
||||
options.encoding = options.encoding || 'utf-8';
|
||||
|
||||
return function posthtmlInclude(tree) {
|
||||
tree.match({ tag: 'include' }, function(node) {
|
||||
if (!node.attrs.src) {
|
||||
return {
|
||||
tag: false,
|
||||
content: null
|
||||
};
|
||||
}
|
||||
|
||||
const src = path.resolve(options.root, node.attrs.src);
|
||||
const source = fs.readFileSync(src, options.encoding);
|
||||
const subtree = parser(source);
|
||||
subtree.match = tree.match;
|
||||
const content = source.indexOf('include') !== -1? posthtmlInclude(subtree): subtree;
|
||||
|
||||
if (tree.messages) {
|
||||
tree.messages.push({
|
||||
type: "dependency",
|
||||
file: src
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tag: false,
|
||||
content: content
|
||||
};
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
};
|
||||
9
build/res/empty_index.html
Normal file
9
build/res/empty_index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nothing to see here</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
31
build/rollup-posthtml-template.js
Normal file
31
build/rollup-posthtml-template.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import posthtml from 'posthtml';
|
||||
import include from './posthtml-include';
|
||||
import { green } from 'colorette';
|
||||
|
||||
export default function(options = {}) {
|
||||
return {
|
||||
name: 'posthtml',
|
||||
buildEnd: async () => {
|
||||
if (!options.src || !options.dest) {
|
||||
return;
|
||||
}
|
||||
const html = await fs.readFile(options.src, { encoding: 'utf-8' });
|
||||
|
||||
const plugins = [
|
||||
include({
|
||||
root: './src'
|
||||
})
|
||||
];
|
||||
const result = await posthtml(plugins).process(html);
|
||||
|
||||
try {
|
||||
await fs.unlink(options.dest);
|
||||
} catch (exc) { }
|
||||
|
||||
await fs.writeFile(options.dest, result.html, { encoding: 'utf-8' });
|
||||
console.log(green(`written html template ${options.dest}`))
|
||||
}
|
||||
};
|
||||
}
|
||||
357
compose-db.js
Normal file
357
compose-db.js
Normal file
@@ -0,0 +1,357 @@
|
||||
const fs = require('fs').promises;
|
||||
const fsConstants = require('fs').constants;
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const ExitCodes = {
|
||||
"RequestFailure": 1,
|
||||
"ParseFailure": 2,
|
||||
};
|
||||
|
||||
const PULLS_PER_PAGE = 100;
|
||||
const API_RATE_LIMIT = `
|
||||
rateLimit {
|
||||
limit
|
||||
cost
|
||||
remaining
|
||||
resetAt
|
||||
}
|
||||
`;
|
||||
|
||||
class DataFetcher {
|
||||
constructor(data_owner, data_repo) {
|
||||
this.repo_ssh_path = `git@github.com:${data_owner}/${data_repo}.git`;
|
||||
this.api_rest_path = `https://api.github.com/repos/${data_owner}/${data_repo}`;
|
||||
this.api_repository_id = `owner:"${data_owner}" name:"${data_repo}"`;
|
||||
|
||||
this.page_count = 1;
|
||||
this.last_cursor = "";
|
||||
}
|
||||
|
||||
async _logResponse(data, name) {
|
||||
try {
|
||||
try {
|
||||
await fs.access("logs", fsConstants.R_OK | fsConstants.W_OK);
|
||||
} catch (err) {
|
||||
await fs.mkdir("logs");
|
||||
}
|
||||
|
||||
await fs.writeFile(`logs/${name}.json`, JSON.stringify(data, null, 4), {encoding: "utf-8"});
|
||||
} catch (err) {
|
||||
console.error("Error saving log file: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
_handleResponseErrors(queryID, res) {
|
||||
console.warn(` Failed to get data from '${queryID}'; server responded with ${res.status} ${res.statusText}`);
|
||||
const retry_header = res.headers.get("Retry-After");
|
||||
if (retry_header) {
|
||||
console.log(` Retry after: ${retry_header}`);
|
||||
}
|
||||
}
|
||||
|
||||
_handleDataErrors(data) {
|
||||
if (typeof data["errors"] === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(` Server handled the request, but there were errors:`);
|
||||
data.errors.forEach((item) => {
|
||||
console.log(` [${item.type}] ${item.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async fetchGithub(query) {
|
||||
const init = {};
|
||||
init.method = "POST";
|
||||
init.headers = {};
|
||||
init.headers["Content-Type"] = "application/json";
|
||||
if (process.env.GRAPHQL_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GRAPHQL_TOKEN}`;
|
||||
} else if (process.env.GITHUB_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
init.body = JSON.stringify({
|
||||
query,
|
||||
});
|
||||
|
||||
return await fetch("https://api.github.com/graphql", init);
|
||||
}
|
||||
|
||||
async fetchGithubRest(query) {
|
||||
const init = {};
|
||||
init.method = "GET";
|
||||
init.headers = {};
|
||||
init.headers["Content-Type"] = "application/json";
|
||||
if (process.env.GRAPHQL_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GRAPHQL_TOKEN}`;
|
||||
} else if (process.env.GITHUB_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
return await fetch(`${this.api_rest_path}${query}`, init);
|
||||
}
|
||||
|
||||
async checkRates() {
|
||||
try {
|
||||
const query = `
|
||||
query {
|
||||
${API_RATE_LIMIT}
|
||||
}
|
||||
`;
|
||||
|
||||
const res = await this.fetchGithub(query);
|
||||
if (res.status !== 200) {
|
||||
this._handleResponseErrors(this.api_repository_id, res);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
await this._logResponse(data, "_rate_limit");
|
||||
this._handleDataErrors(data);
|
||||
|
||||
const rate_limit = data.data["rateLimit"];
|
||||
console.log(` [$${rate_limit.cost}] Available API calls: ${rate_limit.remaining}/${rate_limit.limit}; resets at ${rate_limit.resetAt}`);
|
||||
} catch (err) {
|
||||
console.error(" Error checking the API rate limits: " + err);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPulls(page) {
|
||||
try {
|
||||
let after_cursor = "";
|
||||
let after_text = "initial";
|
||||
if (this.last_cursor !== "") {
|
||||
after_cursor = `after: "${this.last_cursor}"`;
|
||||
after_text = after_cursor;
|
||||
}
|
||||
|
||||
const query = `
|
||||
query {
|
||||
${API_RATE_LIMIT}
|
||||
repository(${this.api_repository_id}) {
|
||||
pullRequests(first:${PULLS_PER_PAGE} ${after_cursor} states: MERGED) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
number
|
||||
url
|
||||
title
|
||||
state
|
||||
isDraft
|
||||
|
||||
createdAt
|
||||
updatedAt
|
||||
|
||||
baseRef {
|
||||
name
|
||||
}
|
||||
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
|
||||
... on User {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
milestone {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let page_text = page;
|
||||
if (this.page_count > 1) {
|
||||
page_text = `${page}/${this.page_count}`;
|
||||
}
|
||||
console.log(` Requesting page ${page_text} of pull request data (${after_text}).`);
|
||||
|
||||
const res = await this.fetchGithub(query);
|
||||
if (res.status !== 200) {
|
||||
this._handleResponseErrors(this.api_repository_id, res);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
await this._logResponse(data, `data_page_${page}`);
|
||||
this._handleDataErrors(data);
|
||||
|
||||
const rate_limit = data.data["rateLimit"];
|
||||
const repository = data.data["repository"];
|
||||
const pulls_data = mapNodes(repository.pullRequests);
|
||||
|
||||
console.log(` [$${rate_limit.cost}] Retrieved ${pulls_data.length} pull requests; processing...`);
|
||||
|
||||
this.last_cursor = repository.pullRequests.pageInfo.endCursor;
|
||||
this.page_count = Math.ceil(repository.pullRequests.totalCount / PULLS_PER_PAGE);
|
||||
|
||||
return pulls_data;
|
||||
} catch (err) {
|
||||
console.error(" Error fetching pull request data: " + err);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataProcessor {
|
||||
constructor() {
|
||||
this.authors = {};
|
||||
this.pulls = [];
|
||||
}
|
||||
|
||||
processPulls(pullsRaw) {
|
||||
try {
|
||||
pullsRaw.forEach((item) => {
|
||||
// Compile basic information about a PR.
|
||||
let pr = {
|
||||
"id": item.id,
|
||||
"public_id": item.number,
|
||||
"url": item.url,
|
||||
"diff_url": `${item.url}.diff`,
|
||||
"patch_url": `${item.url}.patch`,
|
||||
|
||||
"title": item.title,
|
||||
"state": item.state,
|
||||
"is_draft": item.isDraft,
|
||||
"authored_by": null,
|
||||
"created_at": item.createdAt,
|
||||
"updated_at": item.updatedAt,
|
||||
|
||||
"target_branch": item.baseRef.name,
|
||||
"milestone": null,
|
||||
};
|
||||
|
||||
// Store the target branch if it hasn't been stored.
|
||||
if (!this.branches.includes(pr.target_branch)) {
|
||||
this.branches.push(pr.target_branch);
|
||||
}
|
||||
|
||||
// Compose and link author information.
|
||||
const author = {
|
||||
"id": "",
|
||||
"user": "ghost",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/10137?v=4",
|
||||
"url": "https://github.com/ghost",
|
||||
"pull_count": 0,
|
||||
};
|
||||
if (item.author != null) {
|
||||
author["id"] = item.author.id;
|
||||
author["user"] = item.author.login;
|
||||
author["avatar"] = item.author.avatarUrl;
|
||||
author["url"] = item.author.url;
|
||||
}
|
||||
pr.authored_by = author.id;
|
||||
|
||||
// Store the author if they haven't been stored.
|
||||
if (typeof this.authors[author.id] === "undefined") {
|
||||
this.authors[author.id] = author;
|
||||
}
|
||||
this.authors[author.id].pull_count++;
|
||||
|
||||
// Add the milestone, if available.
|
||||
if (item.milestone) {
|
||||
pr.milestone = {
|
||||
"id": item.milestone.id,
|
||||
"title": item.milestone.title,
|
||||
"url": item.milestone.url,
|
||||
};
|
||||
}
|
||||
|
||||
this.pulls.push(pr);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(" Error parsing pull request data: " + err);
|
||||
process.exitCode = ExitCodes.ParseFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapNodes(object) {
|
||||
return object.edges.map((item) => item["node"])
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Internal utility methods.
|
||||
const checkForExit = () => {
|
||||
if (process.exitCode > 0) {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
const delay = async (msec) => {
|
||||
return new Promise(resolve => setTimeout(resolve, msec));
|
||||
}
|
||||
|
||||
console.log("[*] Building local pull request database.");
|
||||
|
||||
let data_owner = "godotengine";
|
||||
let data_repo = "godot";
|
||||
process.argv.forEach((arg) => {
|
||||
if (arg.indexOf("owner:") === 0) {
|
||||
data_owner = arg.substring(6);
|
||||
}
|
||||
if (arg.indexOf("repo:") === 0) {
|
||||
data_repo = arg.substring(5);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[*] Configured for the "${data_owner}/${data_repo}" repository.`);
|
||||
const dataFetcher = new DataFetcher(data_owner, data_repo);
|
||||
const dataProcessor = new DataProcessor();
|
||||
|
||||
console.log("[*] Checking the rate limits before.");
|
||||
await dataFetcher.checkRates();
|
||||
checkForExit();
|
||||
|
||||
console.log("[*] Fetching pull request data from GitHub.");
|
||||
// Pages are starting with 1 for better presentation.
|
||||
let page = 1;
|
||||
while (page <= dataFetcher.page_count) {
|
||||
const pullsRaw = await dataFetcher.fetchPulls(page);
|
||||
dataProcessor.processPulls(pullsRaw);
|
||||
checkForExit();
|
||||
page++;
|
||||
|
||||
// Wait for a bit before proceeding to avoid hitting the secondary rate limit in GitHub API.
|
||||
// See https://docs.github.com/en/rest/guides/best-practices-for-integrators#dealing-with-secondary-rate-limits.
|
||||
await delay(1500);
|
||||
}
|
||||
|
||||
console.log("[*] Checking the rate limits after.")
|
||||
await dataFetcher.checkRates();
|
||||
checkForExit();
|
||||
|
||||
console.log("[*] Finalizing database.")
|
||||
const output = {
|
||||
"generated_at": Date.now(),
|
||||
"authors": dataProcessor.authors,
|
||||
"pulls": dataProcessor.pulls,
|
||||
};
|
||||
try {
|
||||
console.log("[*] Storing database to file.");
|
||||
await fs.writeFile(`out/${data_owner}.${data_repo}.data.json`, JSON.stringify(output), {encoding: "utf-8"});
|
||||
console.log("[*] Database built.");
|
||||
} catch (err) {
|
||||
console.error("Error saving database file: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
1161
package-lock.json
generated
Normal file
1161
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "godot-interactive-changelog",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"compose-db": "node ./compose-db.js"
|
||||
},
|
||||
"author": "Yuri Sizov <yuris@humnom.net>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.6.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"lit-element": "^2.2.1",
|
||||
"marked": "^0.7.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"posthtml": "^0.12.0",
|
||||
"rollup": "^1.24.0",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-includepaths": "^0.2.3",
|
||||
"rollup-plugin-node-resolve": "^5.2.0"
|
||||
}
|
||||
}
|
||||
116
rollup.config.js
Normal file
116
rollup.config.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||
import includePaths from 'rollup-plugin-includepaths'
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import posthtmlTemplate from './build/rollup-posthtml-template';
|
||||
import babel from 'rollup-plugin-babel';
|
||||
|
||||
const INPUT_ROOT = 'src';
|
||||
const INPUT_PATHS_ROOT = path.join(INPUT_ROOT, 'paths');
|
||||
const INPUT_SHARED_ROOT = path.join(INPUT_ROOT, 'shared');
|
||||
const INPUT_STATIC_ROOT = path.join(INPUT_ROOT, 'static');
|
||||
|
||||
const ENTRY_FILE_NAME = 'entry.js';
|
||||
const TEMPLATE_FILE_NAME = 'template.html';
|
||||
const GLOBAL_FILE_NAME = 'global.js';
|
||||
|
||||
const OUTPUT_ROOT = 'out';
|
||||
const OUTPUT_STYLES = path.join(OUTPUT_ROOT, 'styles');
|
||||
const OUTPUT_STYLES_SHARED = path.join(OUTPUT_STYLES, 'shared');
|
||||
const OUTPUT_SCRIPTS = path.join(OUTPUT_ROOT, 'scripts');
|
||||
const OUTPUT_SCRIPTS_SHARED = path.join(OUTPUT_SCRIPTS, 'shared');
|
||||
|
||||
const generateConfig = async () => {
|
||||
let configs = [];
|
||||
|
||||
getGlobalConfig(configs);
|
||||
await getPathsConfigs(configs);
|
||||
|
||||
return configs;
|
||||
};
|
||||
const getGlobalConfig = (configs) => {
|
||||
const globalScriptPath = path.join(INPUT_SHARED_ROOT, 'scripts', GLOBAL_FILE_NAME);
|
||||
const outputPath = path.join(OUTPUT_SCRIPTS_SHARED, GLOBAL_FILE_NAME);
|
||||
|
||||
const sharedStylesGlob = path.join(INPUT_SHARED_ROOT, 'styles/**/*.css').replace(/\\/g, '/'); // Windows path not supported by copy plugin
|
||||
const staticGlob = path.join(INPUT_STATIC_ROOT, '/**/*.*').replace(/\\/g, '/'); // Windows path not supported by copy plugin
|
||||
|
||||
configs.push({
|
||||
input: globalScriptPath,
|
||||
output: {
|
||||
name: 'global',
|
||||
file: outputPath,
|
||||
format: 'iife'
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: sharedStylesGlob, dest: OUTPUT_STYLES_SHARED },
|
||||
{ src: staticGlob, dest: OUTPUT_ROOT },
|
||||
],
|
||||
verbose: true
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
};
|
||||
const getPathsConfigs = async (configs) => {
|
||||
try {
|
||||
// Collect paths to process
|
||||
const paths = await fs.readdir(INPUT_PATHS_ROOT);
|
||||
|
||||
for (const itemPath of paths) {
|
||||
const itemRoot = path.join(INPUT_PATHS_ROOT, itemPath);
|
||||
const itemFiles = await fs.readdir(itemRoot);
|
||||
|
||||
if (itemFiles.indexOf(ENTRY_FILE_NAME) < 0) {
|
||||
throw Error(`Missing entry script for "${itemPath}" path`);
|
||||
}
|
||||
if (itemFiles.indexOf(TEMPLATE_FILE_NAME) < 0) {
|
||||
throw Error(`Missing HTML template for "${itemPath}" path`);
|
||||
}
|
||||
|
||||
const entryPath = path.join(itemRoot, ENTRY_FILE_NAME);
|
||||
const templatePath = path.join(itemRoot, TEMPLATE_FILE_NAME).replace(/\\/g, '/'); // Windows path not supported by copy plugin
|
||||
const bundlePath = path.join(OUTPUT_ROOT, 'scripts', `${itemPath}.js`);
|
||||
const htmlPath = path.join(OUTPUT_ROOT, `${itemPath}.html`);
|
||||
|
||||
configs.push({
|
||||
input: entryPath,
|
||||
output: {
|
||||
name: itemPath,
|
||||
file: bundlePath,
|
||||
format: 'iife'
|
||||
},
|
||||
plugins: [
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
plugins: [
|
||||
[ '@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true } ],
|
||||
'@babel/plugin-proposal-class-properties'
|
||||
]
|
||||
}),
|
||||
includePaths({
|
||||
paths: [ './' ]
|
||||
}),
|
||||
nodeResolve(),
|
||||
commonjs({
|
||||
sourceMap: false
|
||||
}),
|
||||
posthtmlTemplate({
|
||||
src: templatePath,
|
||||
dest: htmlPath
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (exc) {
|
||||
console.error(exc);
|
||||
}
|
||||
};
|
||||
|
||||
export default generateConfig();
|
||||
83
src/paths/index/components/IndexDescription.js
Normal file
83
src/paths/index/components/IndexDescription.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
@customElement('gr-index-description')
|
||||
export default class IndexDescription extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
:host .header-description {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
color: var(--dimmed-font-color);
|
||||
}
|
||||
|
||||
:host .header-description-column {
|
||||
flex: 2;
|
||||
}
|
||||
:host .header-description-column.header-extra-links {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host .header-description a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host .header-description a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
:host hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--g-background-extra-color);
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .header-description {
|
||||
padding: 0 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host .header-description-column {
|
||||
width: 100%;
|
||||
}
|
||||
:host .header-description-column.header-extra-links {
|
||||
text-align: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: Date }) generated_at = null;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="header-description">
|
||||
<div class="header-description-column">
|
||||
This page lists comprehensive changelogs for each release of Godot,
|
||||
including developer previews (alpha, beta, RC).
|
||||
<br>
|
||||
The purpose of this page is to remove a lot of manual labor from
|
||||
creating and maintaining a changelog for every release, and to provide
|
||||
more powerful tools for users browsing the set of changes.
|
||||
</div>
|
||||
<div class="header-description-column header-extra-links">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
116
src/paths/index/components/IndexHeader.js
Normal file
116
src/paths/index/components/IndexHeader.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
@customElement('gr-index-entry')
|
||||
export default class IndexHeader extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
--header-meta-color: #98a5b8;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--header-meta-color: #515c6c;
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
}
|
||||
|
||||
:host .header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host .header-metadata {
|
||||
color: var(--header-meta-color);
|
||||
text-align: right;
|
||||
}
|
||||
:host .header-metadata a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host .header-metadata a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .header {
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
}
|
||||
:host .header-title,
|
||||
:host .header-metadata {
|
||||
width: 100%;
|
||||
}
|
||||
:host .header-metadata {
|
||||
padding-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: Date }) generated_at = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Auto-refresh about once a minute so that the relative time of generation is always actual.
|
||||
this._refreshTimeout = setTimeout(this._refresh.bind(this), 60 * 1000);
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.requestUpdate();
|
||||
|
||||
// Continue updating.
|
||||
this._refreshTimeout = setTimeout(this._refresh.bind(this), 60 * 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
let generatedAt = "";
|
||||
let generatedRel = "";
|
||||
|
||||
if (this.generated_at) {
|
||||
generatedAt = greports.format.formatTimestamp(this.generated_at);
|
||||
|
||||
let timeValue = (Date.now() - this.generated_at) / (1000 * 60);
|
||||
let timeUnit = "minute";
|
||||
|
||||
if (timeValue < 1) {
|
||||
generatedRel = "just now";
|
||||
} else {
|
||||
if (timeValue > 60) {
|
||||
timeValue = timeValue / 60;
|
||||
timeUnit = "hour";
|
||||
}
|
||||
|
||||
generatedRel = greports.format.formatTimespan(-Math.round(timeValue), timeUnit);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<h1 class="header-title">
|
||||
Godot Interactive Changelog
|
||||
</h1>
|
||||
<div class="header-metadata">
|
||||
${(this.generated_at ? html`
|
||||
<span title="${generatedAt}">
|
||||
data generated ${generatedRel}
|
||||
</span>
|
||||
` : '')}
|
||||
<br/>
|
||||
<a
|
||||
href="https://github.com/godotengine/godot-interactive-changelog"
|
||||
target="_blank"
|
||||
>
|
||||
contribute on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
107
src/paths/index/entry.js
Normal file
107
src/paths/index/entry.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
import PageContent from 'src/shared/components/PageContent';
|
||||
import SharedNavigation from 'src/shared/components/SharedNavigation';
|
||||
import IndexHeader from "./components/IndexHeader";
|
||||
import IndexDescription from "./components/IndexDescription";
|
||||
|
||||
@customElement('entry-component')
|
||||
export default class EntryComponent extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
}
|
||||
|
||||
:host .files {
|
||||
display: flex;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .files {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._entryRequested = false;
|
||||
this._isLoading = true;
|
||||
this._generatedAt = null;
|
||||
|
||||
this._selectedRepository = "godotengine/godot";
|
||||
|
||||
this._restoreUserPreferences();
|
||||
this._requestData();
|
||||
}
|
||||
|
||||
performUpdate() {
|
||||
this._requestData();
|
||||
super.performUpdate();
|
||||
}
|
||||
|
||||
_restoreUserPreferences() {
|
||||
const userPreferences = greports.util.getLocalPreferences();
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
_saveUserPreferences() {
|
||||
const currentPreferences = {
|
||||
// ...
|
||||
};
|
||||
|
||||
greports.util.setLocalPreferences(currentPreferences);
|
||||
}
|
||||
|
||||
async _requestData() {
|
||||
if (this._entryRequested) {
|
||||
return;
|
||||
}
|
||||
this._entryRequested = true;
|
||||
this._isLoading = true;
|
||||
|
||||
const data = await greports.api.getData(this._selectedRepository);
|
||||
|
||||
if (data) {
|
||||
this._generatedAt = data.generated_at;
|
||||
|
||||
// ...
|
||||
} else {
|
||||
this._generatedAt = null;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
this._isLoading = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render(){
|
||||
return html`
|
||||
<page-content>
|
||||
<shared-nav></shared-nav>
|
||||
<gr-index-entry .generated_at="${this._generatedAt}"></gr-index-entry>
|
||||
<gr-index-description></gr-index-description>
|
||||
|
||||
${(this._isLoading ? html`
|
||||
<h3>Loading...</h3>
|
||||
` : html`
|
||||
<div>...</div>
|
||||
`)}
|
||||
</page-content>
|
||||
`;
|
||||
}
|
||||
}
|
||||
16
src/paths/index/template.html
Normal file
16
src/paths/index/template.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<include src="shared/partials/head_content.html"></include>
|
||||
|
||||
<title>Godot PRs by File</title>
|
||||
<meta name="description" content="Godot Engine PRs grouped by individual source files they affect">
|
||||
<meta name="keywords" content="godot, godot engine, gamedev, project management">
|
||||
|
||||
<script src="scripts/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<entry-component></entry-component>
|
||||
<include src="shared/partials/body_content.html"></include>
|
||||
</body>
|
||||
</html>
|
||||
28
src/shared/components/PageContent.js
Normal file
28
src/shared/components/PageContent.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LitElement, html, css, customElement } from 'lit-element';
|
||||
|
||||
@customElement('page-content')
|
||||
export default class PageContent extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Component styling **/
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 0 12px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
render(){
|
||||
return html`
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
119
src/shared/components/SharedNavigation.js
Normal file
119
src/shared/components/SharedNavigation.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { LitElement, html, css, customElement } from 'lit-element';
|
||||
|
||||
@customElement('shared-nav')
|
||||
export default class SharedNavigation extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
}
|
||||
|
||||
:host .nav-container a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host .nav-container a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
:host .nav-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
background: var(--g-background-color);
|
||||
}
|
||||
|
||||
:host .nav-item {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
:host .nav-item:hover {
|
||||
background-color: var(--g-background-extra2-color);
|
||||
}
|
||||
|
||||
:host .nav-toggler {
|
||||
display: none;
|
||||
background-image: url('hamburger.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
:host .nav-toggler:hover {
|
||||
background-color: var(--g-background-extra2-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
:host .nav-container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
:host .nav-container.nav-active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host .nav-toggler {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._mobileActive = false;
|
||||
}
|
||||
|
||||
_onMobileToggled() {
|
||||
this._mobileActive = !this._mobileActive;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render(){
|
||||
const containerClassList = [ "nav-container" ];
|
||||
if (this._mobileActive) {
|
||||
containerClassList.push("nav-active");
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="${containerClassList.join(" ")}">
|
||||
<a href="https://godotengine.github.io/doc-status/" target="_blank" class="nav-item">
|
||||
ClassRef Status
|
||||
</a>
|
||||
<a href="https://godot-proposals-viewer.github.io/" target="_blank" class="nav-item">
|
||||
Proposal Viewer
|
||||
</a>
|
||||
<a href="https://godotengine.github.io/godot-team-reports/" target="_blank" class="nav-item">
|
||||
Team Reports
|
||||
</a>
|
||||
<a href="https://godotengine.github.io/godot-prs-by-file/" target="_blank" class="nav-item">
|
||||
PRs by File
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav-toggler"
|
||||
@click="${this._onMobileToggled}"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
0
src/shared/partials/body_content.html
Normal file
0
src/shared/partials/body_content.html
Normal file
9
src/shared/partials/head_content.html
Normal file
9
src/shared/partials/head_content.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500i,700,900&display=swap&subset=latin-ext" rel="stylesheet">
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
<link href="styles/shared/normalize.css" rel="stylesheet">
|
||||
<link href="styles/shared/global.css" rel="stylesheet">
|
||||
|
||||
<script src="scripts/shared/global.js"></script>
|
||||
131
src/shared/scripts/global.js
Normal file
131
src/shared/scripts/global.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const LOCAL_PREFERENCE_PREFIX = "_godot_icl"
|
||||
const LOCAL_PREFERENCE_DEFAULTS = {
|
||||
|
||||
};
|
||||
|
||||
// API Interaction
|
||||
const ReportsAPI = {
|
||||
async get(path = '/') {
|
||||
const res = await fetch(`${path}`);
|
||||
if (res.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
async getData(repositoryId) {
|
||||
const idBits = repositoryId.split("/");
|
||||
|
||||
return await this.get(`${idBits[0]}.${idBits[1]}.data.json`);
|
||||
},
|
||||
};
|
||||
|
||||
// Content helpers
|
||||
const ReportsFormatter = {
|
||||
formatDate(dateString) {
|
||||
const options = {
|
||||
year: 'numeric', month: 'long', day: 'numeric',
|
||||
};
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', options);
|
||||
|
||||
const date = new Date(dateString);
|
||||
return dateFormatter.format(date);
|
||||
},
|
||||
|
||||
formatTimestamp(timeString) {
|
||||
const options = {
|
||||
year: 'numeric', month: 'long', day: 'numeric',
|
||||
hour: 'numeric', hour12: false, minute: 'numeric',
|
||||
timeZone: 'UTC', timeZoneName: 'short',
|
||||
};
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', options);
|
||||
|
||||
const date = new Date(timeString);
|
||||
return dateFormatter.format(date);
|
||||
},
|
||||
|
||||
formatTimespan(timeValue, timeUnit) {
|
||||
const options = {
|
||||
style: 'long',
|
||||
};
|
||||
const timeFormatter = new Intl.RelativeTimeFormat('en-US', options);
|
||||
|
||||
return timeFormatter.format(timeValue, timeUnit);
|
||||
},
|
||||
|
||||
getDaysSince(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const msBetween = (new Date()) - date;
|
||||
const days = Math.floor(msBetween / (1000 * 60 * 60 * 24));
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
formatDays(days) {
|
||||
return days + " " + (days !== 1 ? "days" : "day");
|
||||
},
|
||||
};
|
||||
|
||||
const ReportsUtils = {
|
||||
createEvent(name, detail = {}) {
|
||||
return new CustomEvent(name, {
|
||||
detail: detail
|
||||
});
|
||||
},
|
||||
|
||||
getHistoryHash() {
|
||||
let rawHash = window.location.hash;
|
||||
if (rawHash !== "") {
|
||||
return rawHash.substr(1);
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
|
||||
setHistoryHash(hash) {
|
||||
const url = new URL(window.location);
|
||||
url.hash = hash;
|
||||
window.history.pushState({}, "", url);
|
||||
},
|
||||
|
||||
navigateHistoryHash(hash) {
|
||||
this.setHistoryHash(hash);
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
getLocalPreferences() {
|
||||
// Always fallback on defaults.
|
||||
const localPreferences = { ...LOCAL_PREFERENCE_DEFAULTS };
|
||||
|
||||
for (let key in localPreferences) {
|
||||
const storedValue = localStorage.getItem(`${LOCAL_PREFERENCE_PREFIX}_${key}`);
|
||||
if (storedValue != null) {
|
||||
localPreferences[key] = JSON.parse(storedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return localPreferences;
|
||||
},
|
||||
|
||||
setLocalPreferences(currentPreferences) {
|
||||
for (let key in currentPreferences) {
|
||||
// Only store known properties.
|
||||
if (key in LOCAL_PREFERENCE_DEFAULTS) {
|
||||
localStorage.setItem(`${LOCAL_PREFERENCE_PREFIX}_${key}`, JSON.stringify(currentPreferences[key]));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
resetLocalPreferences() {
|
||||
this.setLocalPreferences(LOCAL_PREFERENCE_DEFAULTS);
|
||||
},
|
||||
};
|
||||
|
||||
const ReportsSingleton = {
|
||||
api: ReportsAPI,
|
||||
format: ReportsFormatter,
|
||||
util: ReportsUtils,
|
||||
};
|
||||
|
||||
window.greports = ReportsSingleton;
|
||||
55
src/shared/styles/global.css
Normal file
55
src/shared/styles/global.css
Normal file
@@ -0,0 +1,55 @@
|
||||
/** Colors and variables **/
|
||||
:root {
|
||||
--g-background-color: #fcfcfa;
|
||||
--g-background-extra-color: #98a5b8;
|
||||
--g-background-extra2-color: #cad3e1;
|
||||
--g-font-color: #121314;
|
||||
--g-font-size: 15px;
|
||||
--g-font-weight: 400;
|
||||
--g-line-height: 20px;
|
||||
|
||||
--link-font-color: #1d6dff;
|
||||
--link-font-color-hover: #1051c9;
|
||||
--link-font-color-inactive: #35496f;
|
||||
|
||||
--dimmed-font-color: #535c5f;
|
||||
--light-font-color: #6b7893;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--g-background-color: #0d1117;
|
||||
--g-background-extra-color: #515c6c;
|
||||
--g-background-extra2-color: #22252b;
|
||||
--g-font-color: rgba(228, 228, 232, 0.9);
|
||||
|
||||
--link-font-color: #367df7;
|
||||
--link-font-color-hover: #6391ec;
|
||||
--link-font-color-inactive: #abbdcc;
|
||||
|
||||
--dimmed-font-color: #929da0;
|
||||
--light-font-color: #8491ab;
|
||||
}
|
||||
}
|
||||
|
||||
/** General styling **/
|
||||
html {}
|
||||
|
||||
body {
|
||||
background: var(--g-background-color);
|
||||
color: var(--g-font-color);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: var(--g-font-size);
|
||||
font-weight: var(--g-font-weight);
|
||||
line-height: var(--g-line-height);
|
||||
min-width: 380px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-font-color);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
349
src/shared/styles/normalize.css
vendored
Normal file
349
src/shared/styles/normalize.css
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
BIN
src/static/favicon.png
Normal file
BIN
src/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
1
src/static/icons/hamburger.svg
Normal file
1
src/static/icons/hamburger.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m4 3c-1.108 0-2 .89201-2 2s.89201 2 2 2h16c1.108 0 2-.89201 2-2s-.89202-2-2-2zm0 6.7773c-1.108 0-2 .94178-2 2.1113s.89201 2.1113 2 2.1113h16c1.108 0 2-.94178 2-2.1113s-.89201-2.1113-2-2.1113zm0 7c-1.108 0-2 .94178-2 2.1113s.89203 2.1113 2 2.1113h16c1.108 0 2-.94178 2-2.1113s-.89203-2.1113-2-2.1113z" fill="#e0e0e0" /></svg>
|
||||
|
After Width: | Height: | Size: 417 B |
Reference in New Issue
Block a user