Make builds of the editor and the website serve their own local fonts (#2186)

* WIP

* Done?

* Install fonts in CI

* Use absolute path so minified inlined CSS works

* Fix Bezier-rs demo fonts?

* Use opsz

* Revert removal of text balancer

* Pull in the text balancer from our static host
This commit is contained in:
Keavon Chambers 2025-05-19 02:38:29 -07:00 committed by GitHub
parent ea59f10b50
commit e57637aab1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1305 additions and 755 deletions

View file

@ -13,7 +13,7 @@ on:
- website/**
env:
CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.rs" data-api="/visit/event" src="/visit/script.js"></script>
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.rs" data-api="/visit/event" src="/visit/script.hash.js"></script>
jobs:
build:
@ -30,7 +30,7 @@ jobs:
- name: 🕸 Install Zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.19.1
tool: zola@0.20.0
- name: ✂ Replace template in <head> of index.html
run: |
@ -42,7 +42,8 @@ jobs:
MODE: prod
run: |
cd website
zola --config config_prod.toml build
npm run install-fonts
zola --config config.toml build --minify
- name: 🔍 Check if `website/other` directory changed
uses: dorny/paths-filter@v3

6
Cargo.lock generated
View file

@ -3362,7 +3362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@ -6635,9 +6635,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.0"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",

View file

@ -734,7 +734,7 @@ impl OverlayContext {
);
}
self.render_context.set_font("12px Source Sans Pro, Arial, sans-serif");
self.render_context.set_font(r#"12px "Source Sans Pro", Arial, sans-serif"#);
self.render_context.set_fill_style_str(font_color);
self.render_context.fill_text(text, 0., 0.).expect("Failed to draw the text at the calculated position");
self.render_context.reset_transform().expect("Failed to reset the render context transform");

View file

@ -22,7 +22,7 @@ Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for
## ESLint configurations: `.eslintrc.js`
[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support, [Airbnb](https://github.com/airbnb/javascript)'s popular catalog of sane defaults, and [Prettier](https://prettier.io/)'s role as a code formatter.
[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter.
## npm ecosystem packages: `package.json`

View file

@ -1,79 +1,54 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Graphite</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?display=block&family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400;1,700&family=Inconsolata:wght@400;700">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#473a3a">
<meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5">
<meta name="apple-mobile-web-app-title" content="Graphite">
<meta name="application-name" content="Graphite">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<meta name="color-scheme" content="dark only">
<meta name="darkreader-lock">
<!-- INDEX_HTML_HEAD_REPLACEMENT -->
</head>
<body>
<noscript>JavaScript is required</noscript>
<style>
body {
background: #222;
height: 100%;
margin: 0;
}
body::after {
content: "";
display: block;
position: absolute;
left: 50%;
top: 50%;
width: 60px;
height: 60px;
border-radius: 50%;
border: 4px solid #eee;
border-color: #eee transparent #eee transparent;
animation: spinning-loading-indicator 1s linear infinite;
}
@keyframes spinning-loading-indicator {
0% {
transform: translate(-30px, -30px) rotate(0deg);
<head>
<meta charset="utf-8" />
<title>Graphite</title>
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
<link rel="manifest" href="site.webmanifest" />
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#473a3a" />
<meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5" />
<meta name="apple-mobile-web-app-title" content="Graphite" />
<meta name="application-name" content="Graphite" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="theme-color" content="#ffffff" />
<meta name="color-scheme" content="dark only" />
<meta name="darkreader-lock" />
<!-- INDEX_HTML_HEAD_REPLACEMENT -->
</head>
<body>
<noscript>JavaScript is required</noscript>
<style>
body {
background: #222;
height: 100%;
margin: 0;
}
100% {
transform: translate(-30px, -30px) rotate(360deg);
body::after {
content: "";
display: block;
position: absolute;
left: 50%;
top: 50%;
width: 60px;
height: 60px;
border-radius: 50%;
border: 4px solid #eee;
border-color: #eee transparent #eee transparent;
animation: spinning-loading-indicator 1s linear infinite;
}
}
</style>
<script>
// Confirm the browser is compatible before initializing the application
// Display an error if the browser is incompatible with a required API
// This is run outside the JS code bundle in case unsupported features cause a syntax error when parsing the bundle, preventing the any bundle code from running
let incompatibility;
if (!("BigUint64Array" in window)) {
incompatibility = `
<style>
body::after { content: none; }
h2, p, a { text-align: center; color: #eee; font-family: "Source Sans Pro", Arial, sans-serif; }
</style>
<h2>This browser is too old to run Graphite</h2>
<p>Please upgrade to a modern web browser such as the latest Firefox, Chrome, Edge, or Safari version 15 or newer.</p>
<p>(The <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility" target="_blank"><code>BigInt64Array</code></a>
JavaScript API must be supported by the browser for Graphite to function.)</p>
`.trim();
}
// Load the editor application or display the incompatibility message
if (incompatibility) {
document.body.innerHTML += incompatibility;
}
</script>
<script type="module" src="src/main.ts"></script>
</body>
@keyframes spinning-loading-indicator {
0% {
transform: translate(-30px, -30px) rotate(0deg);
}
100% {
transform: translate(-30px, -30px) rotate(360deg);
}
}
</style>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View file

@ -7,7 +7,8 @@
"name": "graphite-web-frontend",
"license": "Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.2.0",
"@fontsource/inconsolata": "^5.2.5",
"@fontsource/source-sans-pro": "^5.2.5",
"class-transformer": "^0.5.1",
"idb-keyval": "^6.2.1",
"reflect-metadata": "^0.2.2"
@ -555,6 +556,21 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fontsource/inconsolata": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.5.tgz",
"integrity": "sha512-OvzkZY5qYghv/jEV6cfGZzFhdFTvSnU+ExPC7WcZ7w8PdRhtiu/SpcBWOBt+3LXgS0n9qyepgq4zZmxlDTlGGQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/source-sans-pro": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.2.5.tgz",
"integrity": "sha512-ypendqc4pYUc+EgF7qqPY9iVYEz1t/Qr03VojKxG/2g3dnpHa1B6DOlDxWQjQXDj5QrG6inEqGT0g+edjALZyg==",
"license": "OFL-1.1"
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -1050,16 +1066,6 @@
"vite": "^5.0.0"
}
},
"node_modules/@tauri-apps/api": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.2.0.tgz",
"integrity": "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",

View file

@ -29,6 +29,8 @@
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\""
},
"dependencies": {
"@fontsource/inconsolata": "^5.2.5",
"@fontsource/source-sans-pro": "^5.2.5",
"class-transformer": "^0.5.1",
"idb-keyval": "^6.2.1",
"reflect-metadata": "^0.2.2"

View file

@ -1,6 +1,13 @@
// This file is the browser's entry point for the JS bundle
// reflect-metadata allows for runtime reflection of types in JavaScript.
// Fonts
import "@fontsource/inconsolata";
import "@fontsource/source-sans-pro/400-italic.css";
import "@fontsource/source-sans-pro/400.css";
import "@fontsource/source-sans-pro/700-italic.css";
import "@fontsource/source-sans-pro/700.css";
// `reflect-metadata` allows for runtime reflection of types in JavaScript.
// It is needed for class-transformer to work and is imported as a side effect.
// The library replaces the Reflect API on the window to support more features.
import "reflect-metadata";

View file

@ -9,11 +9,13 @@ module.exports = {
ecmaVersion: 2020,
},
extends: [
// JS defaults
"airbnb-base",
// General Prettier defaults
"eslint:recommended",
"plugin:import/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"prettier",
],
plugins: ["import", "@typescript-eslint", "prettier"],
settings: {
// https://github.com/import-js/eslint-plugin-import#resolvers
"import/resolver": {
@ -30,7 +32,6 @@ module.exports = {
"!.*.js",
"!.*.ts",
],
plugins: ["prettier"],
rules: {
// Standard ESLint config
indent: "off",

2
website/.gitignore vendored
View file

@ -1,3 +1,5 @@
node_modules/
public/
static/fonts/
static/syntax-highlighting.css
static/text-balancer.js

View file

@ -1,17 +0,0 @@
title = "Graphite"
description = "2D raster & vector editor that melds traditional layers & tools with a modern node-based procedural workflow."
base_url = "https://graphite.rs"
feed_filenames = ["rss.xml"]
compile_sass = true
minify_html = true
[markdown]
highlight_code = true
highlight_theme = "css"
highlight_themes_css = [
{ theme = "kronuz", filename = "syntax-highlighting.css" },
]
[extra]
# Put all your custom variables here

View file

@ -3,12 +3,14 @@ title = "Free online vector editor & procedural design tool"
template = "section.html"
[extra]
css = ["/page/index.css", "/component/carousel.css", "/component/feature-icons.css", "/component/feature-box.css", "/component/youtube-embed.css", "/layout/balance-text.css"]
css = ["/page/index.css", "/component/carousel.css", "/component/feature-icons.css", "/component/feature-box.css", "/component/youtube-embed.css"]
js = ["/js/carousel.js", "/js/youtube-embed.js", "/js/video-autoplay.js"]
linked_js = ["https://static.graphite.rs/text-balancer/text-balancer.js"]
linked_js = []
meta_description = "Open source free software. A vector graphics creativity suite with a clean, intuitive interface. Opens instantly (no signup) and runs locally in a browser. Exports SVG, PNG, JPG."
+++
<!-- replacements::text_balancer() -->
<!-- ▛ LOGO ▜ -->
<section id="logo">
<div class="block">

View file

@ -3,7 +3,6 @@ title = "About Graphite"
[extra]
css = ["/page/about.css", "/component/feature-box.css"]
linked_css = ["https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap"]
+++
<section>
@ -117,7 +116,7 @@ It's easy to learn and teach, yet Graphite's accessible design does not sacrific
<img src="https://static.graphite.rs/content/about/core-team-photo-keavon-chambers.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Keavon Chambers" />
## Keavon Chambers <span class="handle">(@Keavon)</span> <span class="flag" title="American">🇺🇸</span>
## Keavon Chambers <span class="handle">(@Keavon)</span> <img src="https://static.graphite.rs/icons/flags/us.png" class="flag" title="American" />
***Founder, UI & product design, frontend, editor systems***
@ -128,7 +127,7 @@ Keavon is a creative generalist with a love for the fusion of arts and technolog
<img src="https://static.graphite.rs/content/about/core-team-photo-dennis-kobert.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Dennis Kobert" />
## Dennis Kobert <span class="handle">(@TrueDoctor)</span> <span class="flag" title="German">🇩🇪</span>
## Dennis Kobert <span class="handle">(@TrueDoctor)</span> <img src="https://static.graphite.rs/icons/flags/de.png" class="flag" title="German" />
***Graphene node engine, research, architecture***
@ -143,7 +142,7 @@ Dennis is a mix between a mathematician and a mad scientist. While still enjoyin
<img src="https://static.graphite.rs/content/about/core-team-photo-hypercube__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Hypercube" />
## "Hypercube" <span class="handle">(@0Hypercube)</span> <span class="flag" title="British">🇬🇧</span>
## "Hypercube" <span class="handle">(@0Hypercube)</span> <img src="https://static.graphite.rs/icons/flags/gb.png" class="flag" title="British" />
***Editor systems, nodes, tools, architecture***
@ -155,7 +154,7 @@ Dennis is a mix between a mathematician and a mad scientist. While still enjoyin
<img src="https://static.graphite.rs/content/about/core-team-photo-adam-gerhant.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Adam Gerhant" />
## Adam Gerhant <span class="handle">(@pendapia)</span> <span class="flag" title="American">🇺🇸</span>
## Adam Gerhant <span class="handle">(@pendapia)</span> <img src="https://static.graphite.rs/icons/flags/us.png" class="flag" title="American" />
***Editor graph tooling, node data formats***

View file

@ -145,17 +145,21 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Desktop app (Windows, Mac, Linux)</span>
</div>
<div class="feature-icon ongoing">
<div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 40" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Simplified main properties panel</span>
</div>
<div class="feature-icon">
<div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>GPU-accelerated raster rendering</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Infinitely zoomable/panable content</span>
<span>Infinitely zoomable/panable raster</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Select mode (marquee masking)</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
@ -171,7 +175,7 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 57" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Signed distance fields</span>
<span>Signed distance field rendering</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 56" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
@ -181,10 +185,6 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Command palette and context menus</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Select mode (marquee masking)</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Local fonts access</span>

194
website/install-fonts.js Normal file
View file

@ -0,0 +1,194 @@
const fs = require("fs");
const https = require("https");
const path = require("path");
// Define basePath
const basePath = path.resolve(__dirname);
// Define files to copy as [source, destination] pairs
// Files with the same destination will be concatenated
const FILES_TO_COPY = [
["node_modules/@fontsource-variable/inter/opsz.css", "static/fonts/common.css"],
["node_modules/@fontsource-variable/inter/opsz-italic.css", "static/fonts/common.css"],
["node_modules/@fontsource/bona-nova/700.css", "static/fonts/common.css"],
];
// Define directories to copy recursively as [source, destination] pairs
const DIRECTORIES_TO_COPY = [
["node_modules/@fontsource-variable/inter/files", "static/fonts/files"],
["node_modules/@fontsource/bona-nova/files", "static/fonts/files"],
];
// Track processed destination files and CSS content
const processedDestinations = new Set();
const cssDestinations = new Set();
const allCopiedFiles = new Set();
// Process each file
FILES_TO_COPY.forEach(([source, dest]) => {
// Convert relative paths to absolute paths
const sourcePath = path.join(basePath, source);
const destPath = path.join(basePath, dest);
// Track CSS destinations for later analysis
if (dest.endsWith(".css")) {
cssDestinations.add(destPath);
}
// Ensure destination directory exists
const destDir = path.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
console.log(`Created directory: ${destDir}`);
}
try {
// Read source file content
const content = fs.readFileSync(sourcePath, "utf8");
// Check if destination has been processed before
if (processedDestinations.has(destPath)) {
// Append to existing file
fs.appendFileSync(destPath, "\n\n" + content);
console.log(`Appended: ${sourcePath}${destPath}`);
} else {
// First time writing to this destination - copy the file
fs.writeFileSync(destPath, content);
processedDestinations.add(destPath);
console.log(`Copied: ${sourcePath}${destPath}`);
}
// Replace all occurrences of "./files" with "/fonts" in the destination file
let destFileContent = fs.readFileSync(destPath, "utf8");
destFileContent = destFileContent.replaceAll("./files/", "/fonts/files/");
fs.writeFileSync(destPath, destFileContent);
} catch (error) {
console.error(`Error processing ${sourcePath} to ${destPath}:`, error);
process.exit(1);
}
});
// Function to recursively copy a directory
function copyDirectoryRecursive(source, destination) {
// Ensure destination directory exists
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination, { recursive: true });
console.log(`Created directory: ${destination}`);
}
// Get all items in the source directory
const items = fs.readdirSync(source);
// Process each item
items.forEach((item) => {
const sourcePath = path.join(source, item);
const destPath = path.join(destination, item);
// Check if item is a directory or file
const stats = fs.statSync(sourcePath);
if (stats.isDirectory()) {
// Recursively copy subdirectory
copyDirectoryRecursive(sourcePath, destPath);
} else {
// Copy file and track it
fs.copyFileSync(sourcePath, destPath);
allCopiedFiles.add(destPath);
console.log(`Copied: ${sourcePath}${destPath}`);
}
});
}
// Process each directory
DIRECTORIES_TO_COPY.forEach(([source, dest]) => {
// Convert relative paths to absolute paths
const sourcePath = path.join(basePath, source);
const destPath = path.join(basePath, dest);
try {
copyDirectoryRecursive(sourcePath, destPath);
console.log(`Copied directory: ${sourcePath}${destPath}`);
} catch (error) {
console.error(`Error copying directory ${sourcePath} to ${destPath}:`, error);
process.exit(1);
}
});
console.log("All files and directories copied successfully!");
// Now check which of the copied files are actually referenced in CSS
console.log("\nChecking for unused font files...");
// Read all CSS content and join it
let allCssContent = "";
cssDestinations.forEach((cssPath) => {
try {
const content = fs.readFileSync(cssPath, "utf8");
allCssContent += content;
} catch (error) {
console.error(`Error reading CSS file ${cssPath}:`, error);
}
});
// Filter files that aren't referenced in CSS
const unusedFiles = [];
allCopiedFiles.forEach((filePath) => {
const fileName = path.basename(filePath);
// Check if the file name is mentioned in any CSS
if (!allCssContent.includes(fileName)) {
unusedFiles.push(filePath);
}
});
// Delete unused files
if (unusedFiles.length > 0) {
console.log(`Found ${unusedFiles.length} unused font files to delete:`);
unusedFiles.forEach((filePath) => {
try {
fs.unlinkSync(filePath);
console.log(`Deleted unused file: ${filePath}`);
} catch (error) {
console.error(`Error deleting file ${filePath}:`, error);
}
});
} else {
console.log("No unused font files found.");
}
console.log("\nFont installation complete!");
// Fetch and save text-balancer.js, which we don't commit to the repo so we're not version controlling dependency code
const textBalancerUrl = "https://static.graphite.rs/text-balancer/text-balancer.js";
const textBalancerDest = path.join(basePath, "static", "text-balancer.js");
console.log("\nDownloading text-balancer.js...");
https
.get(textBalancerUrl, (res) => {
if (res.statusCode !== 200) {
console.error(`Failed to download text-balancer.js. Status code: ${res.statusCode}`);
res.resume();
return;
}
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
// Ensure destination directory exists
const destDir = path.dirname(textBalancerDest);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
console.log(`Created directory: ${destDir}`);
}
fs.writeFileSync(textBalancerDest, data, "utf8");
console.log(`Downloaded and saved: ${textBalancerDest}`);
} catch (error) {
console.error(`Error saving text-balancer.js:`, error);
}
});
})
.on("error", (err) => {
console.error(`Error downloading text-balancer.js:`, err);
});

View file

@ -4,9 +4,7 @@
<meta charset="utf-8" />
<title>Bezier-rs Interactive Documentation</title>
<link rel="stylesheet" href="./style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bona+Nova:wght@700&family=Inter:wght@500&display=swap" rel="stylesheet" />
<link href="fonts.css" rel="stylesheet" />
</head>
<body>
<noscript>JavaScript is required</noscript>

View file

@ -8,7 +8,7 @@
html,
body {
font-family: "Inter", sans-serif;
font-family: "Inter Variable", sans-serif;
text-align: center;
background-color: white;
}

View file

@ -29,5 +29,6 @@ mkdir dist/libraries/bezier-rs
cd bezier-rs-demos
npm ci
NODE_ENV=production npm run build
cp ../../static/fonts/common.css dist/fonts.css
mv dist/* ../dist/libraries/bezier-rs
cd ..

1507
website/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@
"name": "graphite-website",
"description": "Graphite's website. This npm package is for dev tooling only, such as eslint.",
"private": true,
"scripts": {},
"repository": {
"type": "git",
"url": "git+https://github.com/GraphiteEditor/Graphite.git"
@ -10,15 +9,22 @@
"author": "Graphite Authors <contact@graphite.rs>",
"license": "Apache-2.0",
"homepage": "https://graphite.rs",
"scripts": {
"install-fonts": "npm ci && node install-fonts.js"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"eslint": "^9.11.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
"sass": "^1.78.0"
"sass": "1.78.0"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.5",
"@fontsource/bona-nova": "^5.2.5"
}
}

View file

@ -39,7 +39,7 @@ html,
body {
color: var(--color-navy);
background: white;
font-family: "Inter", sans-serif;
font-family: "Inter Variable", sans-serif;
line-height: 1.5;
font-weight: 500;
font-size: 18px;
@ -157,12 +157,12 @@ body > .page {
background-image: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,15C5.12471,9.753694 0.5,8.795225 0.5,4.736524 C0.5,-0.507473 7.468734,0 8,4.967381 C8.531266,0 15.5,-0.507473 15.5,4.736524 C15.5,8.795225 10.87529,9.753694 8,15z" fill="%23cc304f" /></svg>\
');
display: inline-block;
width: 0.75em;
height: 0.75em;
margin-left: 0.25em;
margin-bottom: -0.1em;
vertical-align: baseline;
display: inline-block;
width: 0.75em;
height: 0.75em;
margin-left: 0.25em;
margin-bottom: -0.1em;
vertical-align: baseline;
}
}
@ -323,7 +323,6 @@ body > .page {
margin-top: 20px;
}
}
}
details[open] summary::before {
@ -436,7 +435,7 @@ h3,
h4,
h5,
h6 {
font-family: "Inter", sans-serif;
font-family: "Inter Variable", sans-serif;
line-height: 1.5;
font-weight: 800;
display: inline-block;
@ -663,7 +662,7 @@ hr,
.arrow::after {
content: " »";
font-family: "Inter", sans-serif;
font-family: "Inter Variable", sans-serif;
}
.video-background {

View file

@ -38,7 +38,7 @@ pre {
content: attr(data-lang);
color: rgba(var(--color-seaside-rgb), 0.5);
text-transform: lowercase;
font-family: "Inter", sans-serif;
font-family: "Inter Variable", sans-serif;
font-size: 0.75em;
font-weight: 700;
font-style: italic;

View file

@ -46,7 +46,7 @@
h1.feature-box-header.feature-box-header {
&,
& a {
font-family: "Inter", sans-serif;
font-family: "Inter Variable", sans-serif;
line-height: 1.5;
font-weight: 800;
text-transform: uppercase;

View file

@ -1,22 +0,0 @@
// CSS component of the JS text balancer script.
// This must be loaded as an inline stylesheet in the head of the
// document to avoid the possibility of a visible layout shift.
.balance-text {
visibility: hidden;
}
@media (scripting: none) {
.balance-text {
visibility: visible !important;
}
}
@supports (text-wrap: balance) {
.balance-text,
.balanced-text {
text-align: left;
text-wrap: balance;
visibility: visible;
}
}

View file

@ -5,8 +5,12 @@
animation: fadeInAfterWait 2s ease-in 2s forwards;
@keyframes fadeInAfterWait {
0% { opacity: 0; }
100% { opacity: 1; }
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
@ -34,11 +38,9 @@
}
.flag {
font-family: "Noto Color Emoji", sans-serif;
font-style: normal;
width: 1em;
height: 1em;
vertical-align: middle;
font-size: 0.8em;
cursor: default;
}
}
}

View file

@ -43,7 +43,7 @@
</div>
</section>
{%- if not page.summary -%}
{{ throw(message = "ARTICLE HAS NO SUMMARY! After the first paragraph (or two short ones), a `<!-- more -->` comment must be inserted in the markdown. Otherwise the blog page would be missing its preview text." | safe) }}
{{ throw(message = "------------------------------------------------------------> ARTICLE HAS NO SUMMARY! After the first paragraph (or two short ones), a `<!-- more -->` comment must be inserted in the markdown. Otherwise the blog page would be missing its preview text." | safe) }}
{%- endif -%}
{%- endblock content -%}

View file

@ -27,15 +27,19 @@
{% block rss -%}
<link rel="alternate" type="application/rss+xml" title="RSS" href="{{ get_url(path = 'blog/rss.xml', trailing_slash = false) | safe }}" />
{%- endblock %}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
{#- ======================================================================== -#}
{#- ON EVERY PAGE OF THE SITE: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#}
{#- ======================================================================== -#}
{%- set global_linked_css = ["https://fonts.googleapis.com/css2?family=Bona+Nova:wght@700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"] -%}
{%- set global_linked_js = [] -%}
{%- set global_css = ["/base.css"] -%}
{%- set global_linked_css = [] -%}
{%- set global_js = ["/js/text-justification.js", "/js/navbar.js"] -%}
{%- set global_css = ["/base.css", "/fonts/common.css"] -%}
{%- set fonts_loaded = load_data(path="static/fonts/common.css", format="plain", required=false) -%}
{%- if not fonts_loaded -%}
{{ throw(message="------------------------------------------------------------> FONTS ARE NOT INSTALLED! Before running Zola, execute `npm run install-fonts` from the `/website` directory.") }}
{%- endif -%}
{#- RETRIEVE FROM TEMPLATES AND PAGES: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#}
{#- ================================================================================ -#}
@ -89,6 +93,7 @@
{{ load_data(path = path) | safe }}
{{ "</" ~ "script>" | safe }}
{%- endfor %}
{{- get_env(name = "INDEX_HTML_HEAD_INCLUSION", default = "") | safe }}
</head>
<body>
@ -119,8 +124,10 @@
</header>
<main>
{%- filter replace(from = "<!-- replacements::blog_posts(count = 2) -->", to = replacements::blog_posts(count = 2)) -%}
{%- filter replace(from = "<!-- replacements::text_balancer() -->", to = replacements::text_balancer()) -%}
{%- block content -%}{%- endblock -%}
{%- endfilter -%}
{%- endfilter -%}
</main>
<footer>
<hr />

View file

@ -12,3 +12,29 @@
</div>
{% endfor %}
{% endmacro blog_posts %}
{% macro text_balancer() %}
<style>
.balance-text {
visibility: hidden;
}
@media (scripting: none) {
.balance-text {
visibility: visible !important;
}
}
@supports (text-wrap: balance) {
.balance-text,
.balanced-text {
text-align: left;
text-wrap: balance;
visibility: visible;
}
}
</style>
<script>
{{ load_data(path="static/text-balancer.js", format="plain") | safe }}
</script>
{% endmacro text_balancer %}