newsfeed
This commit is contained in:
parent
d64cc3fbf2
commit
a1464bb865
11
content/treehouse/new.tree
Normal file
11
content/treehouse/new.tree
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
%% template = "_new.hbs"
|
||||||
|
title = "a curated feed of updates to the house"
|
||||||
|
styles = ["new.css"]
|
||||||
|
feed = "news"
|
||||||
|
|
||||||
|
% id = "01HQ6G30PTVT5H0Z04VVRHEZQF"
|
||||||
|
- ever wondered how Terraria renders its worlds? or how editors like Tiled manage to make painting tiles so easy?
|
||||||
|
|
||||||
|
### tairu - an interactive exploration of 2D autotiling techniques
|
||||||
|
|
||||||
|
[read][page:programming/blog/tairu]
|
|
@ -43,6 +43,40 @@ struct ParsedTree {
|
||||||
target_path: PathBuf,
|
target_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Feed {
|
||||||
|
branches: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Page {
|
||||||
|
pub title: String,
|
||||||
|
pub thumbnail: Option<Thumbnail>,
|
||||||
|
pub scripts: Vec<String>,
|
||||||
|
pub styles: Vec<String>,
|
||||||
|
pub breadcrumbs: String,
|
||||||
|
pub tree_path: Option<String>,
|
||||||
|
pub tree: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Thumbnail {
|
||||||
|
pub url: String,
|
||||||
|
pub alt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct StaticTemplateData<'a> {
|
||||||
|
config: &'a Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PageTemplateData<'a> {
|
||||||
|
pub config: &'a Config,
|
||||||
|
pub page: Page,
|
||||||
|
pub feeds: &'a HashMap<String, Feed>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Generator {
|
impl Generator {
|
||||||
fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> {
|
fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> {
|
||||||
for entry in WalkDir::new(directory) {
|
for entry in WalkDir::new(directory) {
|
||||||
|
@ -172,14 +206,14 @@ impl Generator {
|
||||||
config: &Config,
|
config: &Config,
|
||||||
paths: &Paths<'_>,
|
paths: &Paths<'_>,
|
||||||
navigation_map: &NavigationMap,
|
navigation_map: &NavigationMap,
|
||||||
parsed_trees: impl IntoIterator<Item = ParsedTree>,
|
parsed_trees: Vec<ParsedTree>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
let mut config_derived_data = ConfigDerivedData::default();
|
let mut config_derived_data = ConfigDerivedData::default();
|
||||||
|
|
||||||
let mut template_file_ids = HashMap::new();
|
let mut template_file_ids = HashMap::new();
|
||||||
for entry in WalkDir::new(paths.template_dir) {
|
for entry in WalkDir::new(paths.template_dir) {
|
||||||
let entry = entry?;
|
let entry = entry.context("cannot read directory entry")?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !entry.file_type().is_dir() && path.extension() == Some(OsStr::new("hbs")) {
|
if !entry.file_type().is_dir() && path.extension() == Some(OsStr::new("hbs")) {
|
||||||
let relative_path = path
|
let relative_path = path
|
||||||
|
@ -194,12 +228,8 @@ impl Generator {
|
||||||
|
|
||||||
std::fs::create_dir_all(paths.template_target_dir)?;
|
std::fs::create_dir_all(paths.template_target_dir)?;
|
||||||
for (name, &file_id) in &template_file_ids {
|
for (name, &file_id) in &template_file_ids {
|
||||||
if !name.starts_with('_') {
|
let filename = name.rsplit_once('/').unwrap_or(("", name)).1;
|
||||||
#[derive(Serialize)]
|
if !filename.starts_with('_') {
|
||||||
struct StaticTemplateData<'a> {
|
|
||||||
config: &'a Config,
|
|
||||||
}
|
|
||||||
|
|
||||||
let templated_html = match handlebars.render(name, &StaticTemplateData { config }) {
|
let templated_html = match handlebars.render(name, &StaticTemplateData { config }) {
|
||||||
Ok(html) => html,
|
Ok(html) => html,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
@ -220,6 +250,24 @@ impl Generator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut feeds = HashMap::new();
|
||||||
|
|
||||||
|
for parsed_tree in &parsed_trees {
|
||||||
|
let roots = &treehouse.roots[&parsed_tree.tree_path];
|
||||||
|
|
||||||
|
if let Some(feed_name) = &roots.attributes.feed {
|
||||||
|
let mut feed = Feed {
|
||||||
|
branches: Vec::new(),
|
||||||
|
};
|
||||||
|
for &root in &roots.branches {
|
||||||
|
let branch = treehouse.tree.branch(root);
|
||||||
|
feed.branches.push(branch.attributes.id.clone());
|
||||||
|
}
|
||||||
|
dbg!(&feed.branches);
|
||||||
|
feeds.insert(feed_name.to_owned(), feed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for parsed_tree in parsed_trees {
|
for parsed_tree in parsed_trees {
|
||||||
let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.tree_path);
|
let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.tree_path);
|
||||||
|
|
||||||
|
@ -238,28 +286,6 @@ impl Generator {
|
||||||
&roots.branches,
|
&roots.branches,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Page {
|
|
||||||
pub title: String,
|
|
||||||
pub thumbnail: Option<Thumbnail>,
|
|
||||||
pub scripts: Vec<String>,
|
|
||||||
pub styles: Vec<String>,
|
|
||||||
pub breadcrumbs: String,
|
|
||||||
pub tree_path: Option<String>,
|
|
||||||
pub tree: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Thumbnail {
|
|
||||||
pub url: String,
|
|
||||||
pub alt: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct PageTemplateData<'a> {
|
|
||||||
pub config: &'a Config,
|
|
||||||
pub page: Page,
|
|
||||||
}
|
|
||||||
let template_data = PageTemplateData {
|
let template_data = PageTemplateData {
|
||||||
config,
|
config,
|
||||||
page: Page {
|
page: Page {
|
||||||
|
@ -280,16 +306,22 @@ impl Generator {
|
||||||
.map(|s| s.to_owned()),
|
.map(|s| s.to_owned()),
|
||||||
tree,
|
tree,
|
||||||
},
|
},
|
||||||
|
feeds: &feeds,
|
||||||
};
|
};
|
||||||
|
let template_name = roots
|
||||||
|
.attributes
|
||||||
|
.template
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "_tree.hbs".into());
|
||||||
|
|
||||||
treehouse.roots.insert(parsed_tree.tree_path, roots);
|
treehouse.roots.insert(parsed_tree.tree_path, roots);
|
||||||
|
|
||||||
let templated_html = match handlebars.render("_tree.hbs", &template_data) {
|
let templated_html = match handlebars.render(&template_name, &template_data) {
|
||||||
Ok(html) => html,
|
Ok(html) => html,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
Self::wrangle_handlebars_error_into_diagnostic(
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
treehouse,
|
treehouse,
|
||||||
template_file_ids["_tree.hbs"],
|
template_file_ids[&template_name],
|
||||||
error.line_no,
|
error.line_no,
|
||||||
error.column_no,
|
error.column_no,
|
||||||
error.desc,
|
error.desc,
|
||||||
|
|
|
@ -114,15 +114,22 @@ pub fn branch_to_html(
|
||||||
.map(|&branch_id| {
|
.map(|&branch_id| {
|
||||||
(
|
(
|
||||||
format!(
|
format!(
|
||||||
"/b?{}",
|
"{}/b?{}",
|
||||||
|
config.site,
|
||||||
treehouse.tree.branch(branch_id).attributes.id
|
treehouse.tree.branch(branch_id).attributes.id
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
"".into(),
|
"".into(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
"page" => {
|
||||||
|
Some((format!("{}/{}.html", config.site, linked).into(), "".into()))
|
||||||
|
}
|
||||||
"pic" => config.pics.get(linked).map(|filename| {
|
"pic" => config.pics.get(linked).map(|filename| {
|
||||||
(format!("/static/pic/{}", &filename).into(), "".into())
|
(
|
||||||
|
format!("{}/static/pic/{}", config.site, &filename).into(),
|
||||||
|
"".into(),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,11 @@ use serde::{Deserialize, Serialize};
|
||||||
/// Top-level `%%` root attributes.
|
/// Top-level `%%` root attributes.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct RootAttributes {
|
pub struct RootAttributes {
|
||||||
|
/// Template to use for generating the page.
|
||||||
|
/// Defaults to `_tree.hbs`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub template: Option<String>,
|
||||||
|
|
||||||
/// Title of the generated .html page.
|
/// Title of the generated .html page.
|
||||||
///
|
///
|
||||||
/// The page's tree path is used if empty.
|
/// The page's tree path is used if empty.
|
||||||
|
@ -26,6 +31,11 @@ pub struct RootAttributes {
|
||||||
/// These are relative to the /static/css directory.
|
/// These are relative to the /static/css directory.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub styles: Vec<String>,
|
pub styles: Vec<String>,
|
||||||
|
|
||||||
|
/// When specified, branches coming from this root will be added to a _feed_ with the given name.
|
||||||
|
/// Feeds can be read by Handlebars templates to generate content based on them.
|
||||||
|
#[serde(default)]
|
||||||
|
pub feed: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A picture reference.
|
/// A picture reference.
|
||||||
|
|
|
@ -369,6 +369,16 @@ th {
|
||||||
--recursive-casl: 0.5;
|
--recursive-casl: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Horizontal rules */
|
||||||
|
|
||||||
|
hr {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-1);
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
/* Style the noscript box a little more prettily. */
|
/* Style the noscript box a little more prettily. */
|
||||||
|
|
||||||
.noscript {
|
.noscript {
|
||||||
|
@ -413,6 +423,7 @@ nav {
|
||||||
|
|
||||||
nav .nav-page {
|
nav .nav-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,8 +446,49 @@ h1.page-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style the footer */
|
/* Style the `new` link on the homepage */
|
||||||
|
a[is="th-new"] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 50%;
|
||||||
|
|
||||||
|
&.has-news {
|
||||||
|
opacity: 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
& .new-text {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .badge {
|
||||||
|
margin-left: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style new badges */
|
||||||
|
span.badge {
|
||||||
|
--recursive-wght: 800;
|
||||||
|
--recursive-slnt: 0;
|
||||||
|
--recursive-mono: 1.0;
|
||||||
|
--recursive-casl: 0;
|
||||||
|
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: white;
|
||||||
|
background-color: #d01243;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.before-content {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the footer */
|
||||||
footer {
|
footer {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -561,6 +613,9 @@ th-literate-program[data-mode="output"] {
|
||||||
border-style: none;
|
border-style: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: block;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& img.placeholder.js {
|
||||||
transition: opacity var(--transition-duration);
|
transition: opacity var(--transition-duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -709,3 +764,11 @@ th-literate-program[data-mode="output"] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style settings sections */
|
||||||
|
|
||||||
|
section[is="th-settings"] {
|
||||||
|
/* Don't display settings when JavaScript is disabled.
|
||||||
|
JS overrides this value on the element itself. */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
103
static/css/new.css
Normal file
103
static/css/new.css
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
/* Give the intro and outro some breathing room. */
|
||||||
|
section {
|
||||||
|
padding: 1em 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style all links in the last paragraph as big buttons. */
|
||||||
|
.tree th-bc>p:last-child {
|
||||||
|
--transition-duration: 0.2s;
|
||||||
|
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
&>a {
|
||||||
|
padding: 0.5em 1.5em;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 2em;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
color var(--transition-duration),
|
||||||
|
background-color var(--transition-duration),
|
||||||
|
border-color var(--transition-duration);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: white;
|
||||||
|
background-color: #058ef0;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section[is="th-settings"] {
|
||||||
|
& h3 {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
& details>summary {
|
||||||
|
--recursive-wght: 700;
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
opacity: 50%;
|
||||||
|
transition: opacity var(--transition-duration);
|
||||||
|
|
||||||
|
&::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
--recursive-casl: 0.0;
|
||||||
|
--recursive-mono: 1.0;
|
||||||
|
--recursive-slnt: 0.0;
|
||||||
|
|
||||||
|
content: '+';
|
||||||
|
margin-right: 0.3em;
|
||||||
|
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& details[open]>summary {
|
||||||
|
opacity: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
color var(--transition-duration),
|
||||||
|
background-color var(--transition-duration),
|
||||||
|
border-color var(--transition-duration);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #058ef0;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -184,6 +184,7 @@ class OutputMode {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.frame.placeholderImage != null) {
|
if (this.frame.placeholderImage != null) {
|
||||||
|
this.frame.placeholderImage.classList.add("js");
|
||||||
this.frame.placeholderImage.classList.add("loading");
|
this.frame.placeholderImage.classList.add("loading");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
70
static/js/news.js
Normal file
70
static/js/news.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// news.js because new.js makes the TypeScript language server flip out.
|
||||||
|
// Likely because `new` is a keyword, but also, what the fuck.
|
||||||
|
|
||||||
|
import { getSettingValue } from "./settings.js";
|
||||||
|
import { Branch } from "./tree.js";
|
||||||
|
|
||||||
|
const seenStatesKey = "treehouse.news.seenBranches";
|
||||||
|
const seenStates = new Set(JSON.parse(localStorage.getItem(seenStatesKey)) || []);
|
||||||
|
|
||||||
|
let seenCount = seenStates.size;
|
||||||
|
let unseenCount = TREEHOUSE_NEWS_COUNT - seenCount;
|
||||||
|
|
||||||
|
function saveSeenStates() {
|
||||||
|
localStorage.setItem(seenStatesKey, JSON.stringify(Array.from(seenStates)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAsRead(branch) {
|
||||||
|
if (!seenStates.has(branch.namedID) && seenCount > 0) {
|
||||||
|
let badge = document.createElement("span");
|
||||||
|
badge.classList.add("badge", "red", "before-content");
|
||||||
|
badge.textContent = "new";
|
||||||
|
|
||||||
|
branch.branchContent.firstChild.insertBefore(badge, branch.branchContent.firstChild.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
seenStates.add(branch.namedID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initNewsPage() {
|
||||||
|
for (let [_, branch] of Branch.branchesByNamedID) {
|
||||||
|
markAsRead(branch);
|
||||||
|
}
|
||||||
|
saveSeenStates();
|
||||||
|
|
||||||
|
// If any branches are added past the initial load, add them to the seen set too.
|
||||||
|
Branch.onAdded.push(branch => {
|
||||||
|
markAsRead(branch);
|
||||||
|
saveSeenStates();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markAllAsUnread() {
|
||||||
|
localStorage.removeItem(seenStatesKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
class New extends HTMLAnchorElement {
|
||||||
|
connectedCallback() {
|
||||||
|
// Do not show the badge to people who have never seen any news.
|
||||||
|
// It's just annoying in that case.
|
||||||
|
// In case you do not wish to see the badge anymore, go to the news page and uncheck the
|
||||||
|
// checkbox at the bottom.
|
||||||
|
let userSawNews = seenCount > 0;
|
||||||
|
let userWantsToSeeNews = getSettingValue("showNewPostIndicator");
|
||||||
|
if (userSawNews && userWantsToSeeNews && unseenCount > 0) {
|
||||||
|
this.newText = document.createElement("span");
|
||||||
|
this.newText.classList.add("new-text");
|
||||||
|
this.newText.textContent = this.textContent;
|
||||||
|
this.textContent = "";
|
||||||
|
this.appendChild(this.newText);
|
||||||
|
|
||||||
|
this.badge = document.createElement("span");
|
||||||
|
this.badge.classList.add("badge", "red");
|
||||||
|
this.badge.textContent = unseenCount.toString();
|
||||||
|
this.appendChild(this.badge);
|
||||||
|
this.classList.add("has-news");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-new", New, { extends: "a" });
|
35
static/js/settings.js
Normal file
35
static/js/settings.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const settingsKey = "treehouse.settings";
|
||||||
|
const settings = JSON.parse(localStorage.getItem(settingsKey)) || {};
|
||||||
|
|
||||||
|
const defaultSettingValues = {
|
||||||
|
showNewPostIndicator: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
localStorage.setItem(settingsKey, JSON.stringify(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSettingValue(setting) {
|
||||||
|
return settings[setting] ?? defaultSettingValues[setting];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingCheckbox extends HTMLInputElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.checked = getSettingValue(this.id);
|
||||||
|
|
||||||
|
this.addEventListener("change", () => {
|
||||||
|
settings[this.id] = this.checked;
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-setting-checkbox", SettingCheckbox, { extends: "input" });
|
||||||
|
|
||||||
|
class Settings extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-settings", Settings, { extends: "section" });
|
|
@ -1,5 +1,5 @@
|
||||||
// Detect if we can have crucial functionality (ie. custom elements call constructors).
|
// Detect if we can have crucial functionality (ie. custom elements call constructors).
|
||||||
// This doesn't seem to happen in Epiphany, and possibly also other Webkit-based browsers.
|
// This doesn't seem to happen in Epiphany, and also other Webkit-based browsers.
|
||||||
let works = false;
|
let works = false;
|
||||||
class WebkitMoment extends HTMLLIElement {
|
class WebkitMoment extends HTMLLIElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -17,12 +17,11 @@ function branchIsOpen(branchID) {
|
||||||
return branchState[branchID];
|
return branchState[branchID];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Branch extends HTMLLIElement {
|
export class Branch extends HTMLLIElement {
|
||||||
static branchesByNamedID = new Map();
|
static branchesByNamedID = new Map();
|
||||||
|
static onAdded = [];
|
||||||
|
|
||||||
constructor() {
|
connectedCallback() {
|
||||||
super();
|
|
||||||
|
|
||||||
this.isLeaf = this.classList.contains("leaf");
|
this.isLeaf = this.classList.contains("leaf");
|
||||||
|
|
||||||
this.details = this.childNodes[0];
|
this.details = this.childNodes[0];
|
||||||
|
@ -48,16 +47,20 @@ class Branch extends HTMLLIElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let namedID = this.id.split(':')[1];
|
this.namedID = this.id.split(':')[1];
|
||||||
Branch.branchesByNamedID.set(namedID, this);
|
Branch.branchesByNamedID.set(this.namedID, this);
|
||||||
|
|
||||||
if (ulid.isCanonicalUlid(namedID)) {
|
if (ulid.isCanonicalUlid(this.namedID)) {
|
||||||
let timestamp = ulid.getTimestamp(namedID);
|
let timestamp = ulid.getTimestamp(this.namedID);
|
||||||
let date = document.createElement("span");
|
let date = document.createElement("span");
|
||||||
date.classList.add("branch-date");
|
date.classList.add("branch-date");
|
||||||
date.innerText = timestamp.toLocaleDateString();
|
date.innerText = timestamp.toLocaleDateString();
|
||||||
this.buttonBar.insertBefore(date, this.buttonBar.firstChild);
|
this.buttonBar.insertBefore(date, this.buttonBar.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let callback of Branch.onAdded) {
|
||||||
|
callback(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +71,8 @@ customElements.define("th-b", Branch, { extends: "li" });
|
||||||
class LinkedBranch extends Branch {
|
class LinkedBranch extends Branch {
|
||||||
static byLink = new Map();
|
static byLink = new Map();
|
||||||
|
|
||||||
constructor() {
|
connectedCallback() {
|
||||||
super();
|
super.connectedCallback();
|
||||||
|
|
||||||
this.linkedTree = this.getAttribute("data-th-link");
|
this.linkedTree = this.getAttribute("data-th-link");
|
||||||
LinkedBranch.byLink.set(this.linkedTree, this);
|
LinkedBranch.byLink.set(this.linkedTree, this);
|
||||||
|
|
8
template/README.md
Normal file
8
template/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Templates
|
||||||
|
|
||||||
|
This directory houses Handlebars templates, which are mostly used for reusable bits of the house.
|
||||||
|
|
||||||
|
Files that are not prefixed with a `_` are generated into their own `.html` files.
|
||||||
|
All other files are only loaded into Handlebars for use by other templates (or the generator itself.)
|
||||||
|
|
||||||
|
In particular, `_tree.hbs` is used as the default page template. This can be changed by including a `%% template = "_whatever.hbs"` at the top of your .tree file.
|
77
template/_new.hbs
Normal file
77
template/_new.hbs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en-US" prefix="og: https://ogp.me/ns#">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
{{> components/_head.hbs }}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{#> components/_nav.hbs }}
|
||||||
|
|
||||||
|
{{!-- For /index, include a "new" link that goes to the curated news feed page. --}}
|
||||||
|
{{#if (eq page.tree_path "index")}}
|
||||||
|
<a href="{{ config.site }}/treehouse/new.html" is="th-new">new</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{/ components/_nav.hbs }}
|
||||||
|
|
||||||
|
{{> components/_noscript.hbs }}
|
||||||
|
{{> components/_webkit.hbs }}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>welcome!</p>
|
||||||
|
<p>since you clicked here, you must be curious as to what's been going on since your last visit to the house. so
|
||||||
|
here's a recap just for you - enjoy!</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{{> components/_tree.hbs }}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>note that this page does not include any updates that were made to the website itself - for that, you can
|
||||||
|
visit <a href="{{ config.site }}/treehouse/changelog.html">the changelog</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section is="th-settings">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
settings
|
||||||
|
</summary>
|
||||||
|
<section>
|
||||||
|
<p>if you find the newsfeed annoying, you can customize some aspects of it.</p>
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" is="th-setting-checkbox" id="showNewPostIndicator">
|
||||||
|
<label for="showNewPostIndicator">show the <span class="badge red">1</span> badge on the homepage
|
||||||
|
for
|
||||||
|
new posts you haven't read yet</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button id="mark-all-as-unread"
|
||||||
|
title="Mostly useful for debugging purposes, but it's there if you really wanna do it.">
|
||||||
|
mark all as unread</button>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{!-- For all pages except the one linked from the footer, include the footer icon. --}}
|
||||||
|
{{#if (ne page.tree_path "treehouse")}}
|
||||||
|
{{> components/_footer.hbs }}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<script type="module" defer>
|
||||||
|
import { initNewsPage, markAllAsUnread } from "{{ config.site }}/static/js/news.js";
|
||||||
|
initNewsPage();
|
||||||
|
document.getElementById("mark-all-as-unread").addEventListener("click", () => {
|
||||||
|
markAllAsUnread();
|
||||||
|
alert("congration! you done it");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -3,161 +3,31 @@
|
||||||
<html lang="en-US" prefix="og: https://ogp.me/ns#">
|
<html lang="en-US" prefix="og: https://ogp.me/ns#">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
{{> components/_head.hbs }}
|
||||||
|
|
||||||
<title>{{#if (ne page.title config.user.title)}}{{ page.title }} · {{/if}}{{ config.user.title }}</title>
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
|
|
||||||
<link rel="preload" href="{{ config.site }}/static/font/Recursive_VF_1.085.woff2" as="font" type="font/woff2"
|
|
||||||
crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
|
|
||||||
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
|
|
||||||
|
|
||||||
<script>const TREEHOUSE_SITE = `{{ config.site }}`;</script>
|
|
||||||
<script type="module" src="{{ config.site }}/navmap.js"></script>
|
|
||||||
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
|
|
||||||
<script type="module" src="{{ config.site }}/static/js/usability.js"></script>
|
|
||||||
<script type="module" src="{{ config.site }}/static/js/tree.js"></script>
|
|
||||||
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
|
|
||||||
<script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script>
|
|
||||||
|
|
||||||
<meta property="og:site_name" content="{{ config.user.title }}">
|
|
||||||
<meta property="og:title" content="{{ page.title }}">
|
|
||||||
{{!--
|
|
||||||
This is a bit of a hack to quickly insert metadata into generated pages without going through Handlebars, which
|
|
||||||
would involve registering, parsing, and generating a page from a template.
|
|
||||||
Yes it would be more flexible that way, but it doesn't need to be.
|
|
||||||
It just needs to be a string replacement.
|
|
||||||
--}}
|
|
||||||
<!-- treehouse-ca37057a-cff5-45b3-8415-3b02dbf6c799-per-branch-metadata -->
|
|
||||||
{{#if page.thumbnail}}
|
|
||||||
<meta property="og:image" content="{{ page.thumbnail.url }}">
|
|
||||||
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
|
|
||||||
{{/if}}
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
{{#> components/_nav.hbs }}
|
||||||
<a href="{{ config.site }}/" title="Back to homepage">
|
|
||||||
<svg class="logo" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
||||||
d="M8 3H7H6V4V5H4V6H6V9V10H7H10V12H11V10H12H13V9V8V7H12H11H10V8V9H7V6H8H9V5V4V3H8ZM12 9H11V8H12V9ZM7 5V4H8V5H7ZM3 5H2V6H3V5ZM10 13H11V14H10V13Z"
|
|
||||||
fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="nav-page">
|
{{!-- For /index, include a "new" link that goes to the curated news feed page. --}}
|
||||||
{{#if page.breadcrumbs}}
|
{{#if (eq page.tree_path "index")}}
|
||||||
<ol class="breadcrumbs">
|
<a href="{{ config.site }}/treehouse/new.html" is="th-new">new</a>
|
||||||
{{{ page.breadcrumbs }}}
|
{{/if}}
|
||||||
</ol>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if (and (ne page.title config.user.title) (ne page.title page.tree_path))}}
|
{{/ components/_nav.hbs }}
|
||||||
<h1 class="page-title">{{ page.title }}</h1>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<noscript>
|
{{> components/_noscript.hbs }}
|
||||||
<div class="noscript" role="note">
|
{{> components/_webkit.hbs }}
|
||||||
<p>hey! looks like you have <strong>JavaScript disabled.</strong><br>
|
|
||||||
I respect that decision, but you may find the experience of browsing the treehouse… not great.<br>
|
|
||||||
for example, links to branches may not work properly. I cannot do anything about this; it's due to how
|
|
||||||
the <code><details></code> element works.<br>
|
|
||||||
(a <code><details></code> will not expand itself automatically to reveal the linked element to
|
|
||||||
you.)<br>
|
|
||||||
I did my best to at least keep the site readable in this state, but you can only do so much with plain
|
|
||||||
HTML and CSS.</p>
|
|
||||||
|
|
||||||
<p><strong>Pinky promise this website does not contain any malicious code such as trackers or cryptocurrency
|
{{!--
|
||||||
miners.</strong><br>
|
NOTE: ~ because components/_tree.hbss must not include any extra indentation, because it may
|
||||||
if you don't believe me, you're free to inspect the source yourself! all the scripts are written
|
contain pre elements which shouldn't be indented.
|
||||||
lovingly in vanilla JS (not minified!) by yours truly ❤️</p>
|
--}}
|
||||||
<small>and if this box is annoying, feel free to block it with uBlock Origin or something. I have no
|
{{~> components/_tree.hbs }}
|
||||||
way of remembering you closed it, and don't wanna add a database to this website. simplicity
|
|
||||||
rules!</small>
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="webkit-makes-me-go-insane" class="noscript" role="note">
|
{{!-- For all pages except the one linked from the footer, include the footer icon. --}}
|
||||||
<p>hey! looks like you're using a weird or otherwise quirky web browser. this basically means, the website will
|
{{#if (ne page.tree_path "treehouse")}}
|
||||||
not work for you correctly. I might fix it in the future but I have very limited time to work on this
|
{{> components/_footer.hbs }}
|
||||||
website and so don't have an estimate on when that might happen.</p>
|
|
||||||
<p>in the meantime I suggest switching to <a href="https://firefox.com">something more modern.</a></p>
|
|
||||||
<p>sorry for the inconvenience!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="tree">
|
|
||||||
{{!-- Append page styles and scripts into the main content, such that they can be inlined
|
|
||||||
into linked branches when those are loaded in. Putting them in the page's head would make
|
|
||||||
extracting them way more painful than it needs to be. --}}
|
|
||||||
|
|
||||||
{{#each page.styles}}
|
|
||||||
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
{{#each page.scripts}}
|
|
||||||
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
{{{ page.tree }}}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<th-emoji-tooltips></th-emoji-tooltips>
|
|
||||||
|
|
||||||
{{#if (ne page.tree_path 'treehouse')}}
|
|
||||||
<footer>
|
|
||||||
<a href="{{ config.site }}/treehouse.html">
|
|
||||||
<svg id="footer-icon" width="32" height="32" viewBox="0 0 32 32" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="all">
|
|
||||||
<mask id="mask">
|
|
||||||
<rect width="32" height="32" fill="black" />
|
|
||||||
|
|
||||||
<clipPath id="treehouse">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="white" transform="translate(0 12)"
|
|
||||||
d="M2.95266 3.95816C2.74074 1.83892 4.40494 0 6.53475 0C8.68036 0 10.3496 1.86501 10.1127 3.9975L10.0568 4.5L10.352 4.37352C11.7717 3.76506 13.316 4.92718 13.1244 6.45988L13.0568 7C14.1537 6.56127 15.3084 7.4907 15.1142 8.65595L15.0449 9.07153C14.7633 10.7614 13.3012 12 11.588 12H4.05892C2.0541 12 0.358966 10.5159 0.0940032 8.52866L0.0241185 8.00452C-0.210422 6.24546 1.30006 4.74903 3.05685 5L2.95266 3.95816ZM4.55685 7H2.55685V8H4.55685V7ZM4.55685 9H2.55685V10H4.55685V9ZM5.55685 7H7.55685V8H5.55685V7ZM7.55685 9H5.55685V10H7.55685V9ZM5.55685 13H7.55685L8.05685 16L9.55685 13H10.5569L9.49201 16.5495C9.21835 17.4617 9.39407 18.4496 9.96549 19.2115L10.5569 20H7.55685V18H6.55685V20H4.55685L5.35542 18.9352C5.80652 18.3338 6.01534 17.5848 5.94053 16.8367L5.55685 13Z" />
|
|
||||||
</clipPath>
|
|
||||||
|
|
||||||
<clipPath id="rectangleClip">
|
|
||||||
<rect id="rectangle1" width="16" height="16" />
|
|
||||||
</clipPath>
|
|
||||||
|
|
||||||
<clipPath id="rectangleTreehouseClip" clip-path="url(#treehouse)">
|
|
||||||
<rect id="rectangle2" width="16" height="16" />
|
|
||||||
</clipPath>
|
|
||||||
|
|
||||||
<g transform="translate(3 0)">
|
|
||||||
<rect width="32" height="32" fill="white" clip-path="url(#treehouse)" />
|
|
||||||
<rect width="32" height="32" fill="white" clip-path="url(#rectangleClip)" />
|
|
||||||
<rect width="32" height="32" fill="black" clip-path="url(#rectangleTreehouseClip)" />
|
|
||||||
</g>
|
|
||||||
</mask>
|
|
||||||
|
|
||||||
<rect width="32" height="32" fill="currentColor" mask="url(#mask)" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#rectangle1,
|
|
||||||
#rectangle2 {
|
|
||||||
transform: translate(16px, 12px) rotate(15deg) translate(-8px, -8px);
|
|
||||||
rx: 0px;
|
|
||||||
transition: all 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#all:hover #rectangle1,
|
|
||||||
#all:hover #rectangle2 {
|
|
||||||
transform: translate(22px, 24px) rotate(360deg) translate(-2px, -2px);
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
rx: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
49
template/components/_footer.hbs
Normal file
49
template/components/_footer.hbs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<footer>
|
||||||
|
<a href="{{ config.site }}/treehouse.html">
|
||||||
|
<svg id="footer-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="all">
|
||||||
|
<mask id="mask">
|
||||||
|
<rect width="32" height="32" fill="black" />
|
||||||
|
|
||||||
|
<clipPath id="treehouse">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" fill="white" transform="translate(0 12)"
|
||||||
|
d="M2.95266 3.95816C2.74074 1.83892 4.40494 0 6.53475 0C8.68036 0 10.3496 1.86501 10.1127 3.9975L10.0568 4.5L10.352 4.37352C11.7717 3.76506 13.316 4.92718 13.1244 6.45988L13.0568 7C14.1537 6.56127 15.3084 7.4907 15.1142 8.65595L15.0449 9.07153C14.7633 10.7614 13.3012 12 11.588 12H4.05892C2.0541 12 0.358966 10.5159 0.0940032 8.52866L0.0241185 8.00452C-0.210422 6.24546 1.30006 4.74903 3.05685 5L2.95266 3.95816ZM4.55685 7H2.55685V8H4.55685V7ZM4.55685 9H2.55685V10H4.55685V9ZM5.55685 7H7.55685V8H5.55685V7ZM7.55685 9H5.55685V10H7.55685V9ZM5.55685 13H7.55685L8.05685 16L9.55685 13H10.5569L9.49201 16.5495C9.21835 17.4617 9.39407 18.4496 9.96549 19.2115L10.5569 20H7.55685V18H6.55685V20H4.55685L5.35542 18.9352C5.80652 18.3338 6.01534 17.5848 5.94053 16.8367L5.55685 13Z" />
|
||||||
|
</clipPath>
|
||||||
|
|
||||||
|
<clipPath id="rectangleClip">
|
||||||
|
<rect id="rectangle1" width="16" height="16" />
|
||||||
|
</clipPath>
|
||||||
|
|
||||||
|
<clipPath id="rectangleTreehouseClip" clip-path="url(#treehouse)">
|
||||||
|
<rect id="rectangle2" width="16" height="16" />
|
||||||
|
</clipPath>
|
||||||
|
|
||||||
|
<g transform="translate(3 0)">
|
||||||
|
<rect width="32" height="32" fill="white" clip-path="url(#treehouse)" />
|
||||||
|
<rect width="32" height="32" fill="white" clip-path="url(#rectangleClip)" />
|
||||||
|
<rect width="32" height="32" fill="black" clip-path="url(#rectangleTreehouseClip)" />
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
|
||||||
|
<rect width="32" height="32" fill="currentColor" mask="url(#mask)" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#rectangle1,
|
||||||
|
#rectangle2 {
|
||||||
|
transform: translate(16px, 12px) rotate(15deg) translate(-8px, -8px);
|
||||||
|
rx: 0px;
|
||||||
|
transition: all 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#all:hover #rectangle1,
|
||||||
|
#all:hover #rectangle2 {
|
||||||
|
transform: translate(22px, 24px) rotate(360deg) translate(-2px, -2px);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
rx: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</footer>
|
37
template/components/_head.hbs
Normal file
37
template/components/_head.hbs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<title>{{#if (ne page.title config.user.title)}}{{ page.title }} · {{/if}}{{ config.user.title }}</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="preload" href="{{ config.site }}/static/font/Recursive_VF_1.085.woff2" as="font" type="font/woff2"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
|
||||||
|
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const TREEHOUSE_SITE = `{{ config.site }}`;
|
||||||
|
const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }};
|
||||||
|
</script>
|
||||||
|
<script type="module" src="{{ config.site }}/navmap.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/usability.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/settings.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/tree.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script>
|
||||||
|
<script type="module" src="{{ config.site }}/static/js/news.js"></script>
|
||||||
|
|
||||||
|
<meta property="og:site_name" content="{{ config.user.title }}">
|
||||||
|
<meta property="og:title" content="{{ page.title }}">
|
||||||
|
{{!--
|
||||||
|
This is a bit of a hack to quickly insert metadata into generated pages without going through Handlebars, which
|
||||||
|
would involve registering, parsing, and generating a page from a template.
|
||||||
|
Yes it would be more flexible that way, but it doesn't need to be.
|
||||||
|
It just needs to be a string replacement.
|
||||||
|
--}}
|
||||||
|
<!-- treehouse-ca37057a-cff5-45b3-8415-3b02dbf6c799-per-branch-metadata -->
|
||||||
|
{{#if page.thumbnail}}
|
||||||
|
<meta property="og:image" content="{{ page.thumbnail.url }}">
|
||||||
|
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
|
||||||
|
{{/if}}
|
23
template/components/_nav.hbs
Normal file
23
template/components/_nav.hbs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<nav>
|
||||||
|
<a href="{{ config.site }}/" title="Back to homepage">
|
||||||
|
<svg class="logo" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M8 3H7H6V4V5H4V6H6V9V10H7H10V12H11V10H12H13V9V8V7H12H11H10V8V9H7V6H8H9V5V4V3H8ZM12 9H11V8H12V9ZM7 5V4H8V5H7ZM3 5H2V6H3V5ZM10 13H11V14H10V13Z"
|
||||||
|
fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="nav-page">
|
||||||
|
{{#if page.breadcrumbs}}
|
||||||
|
<ol class="breadcrumbs">
|
||||||
|
{{{ page.breadcrumbs }}}
|
||||||
|
</ol>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (and (ne page.title config.user.title) (ne page.title page.tree_path))}}
|
||||||
|
<h1 class="page-title">{{ page.title }}</h1>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{> @partial-block }}
|
||||||
|
</nav>
|
20
template/components/_noscript.hbs
Normal file
20
template/components/_noscript.hbs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<noscript>
|
||||||
|
<div class="noscript" role="note">
|
||||||
|
<p>hey! looks like you have <strong>JavaScript disabled.</strong><br>
|
||||||
|
I respect that decision, but you may find the experience of browsing the treehouse… not great.<br>
|
||||||
|
for example, links to branches may not work properly. I cannot do anything about this; it's due to how
|
||||||
|
the <code><details></code> element works.<br>
|
||||||
|
(a <code><details></code> will not expand itself automatically to reveal the linked element to
|
||||||
|
you.)<br>
|
||||||
|
I did my best to at least keep the site readable in this state, but you can only do so much with plain
|
||||||
|
HTML and CSS.</p>
|
||||||
|
|
||||||
|
<p><strong>Pinky promise this website does not contain any malicious code such as trackers or cryptocurrency
|
||||||
|
miners.</strong><br>
|
||||||
|
if you don't believe me, you're free to inspect the source yourself! all the scripts are written
|
||||||
|
lovingly in vanilla JS (not minified!) by yours truly ❤️</p>
|
||||||
|
<small>and if this box is annoying, feel free to block it with uBlock Origin or something. I have no
|
||||||
|
way of remembering you closed it, and don't wanna add a database to this website. simplicity
|
||||||
|
rules!</small>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
17
template/components/_tree.hbs
Normal file
17
template/components/_tree.hbs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<main class="tree">
|
||||||
|
{{!-- Append page styles and scripts into the main content, such that they can be inlined
|
||||||
|
into linked branches when those are loaded in. Putting them in the page's head would make
|
||||||
|
extracting them way more painful than it needs to be. --}}
|
||||||
|
|
||||||
|
{{#each page.styles}}
|
||||||
|
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#each page.scripts}}
|
||||||
|
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{{ page.tree }}}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<th-emoji-tooltips></th-emoji-tooltips>
|
7
template/components/_webkit.hbs
Normal file
7
template/components/_webkit.hbs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<div id="webkit-makes-me-go-insane" class="noscript" role="note">
|
||||||
|
<p>hey! looks like you're using a weird or otherwise quirky web browser. this basically means, the website will
|
||||||
|
not work for you correctly. I might fix it in the future but I have very limited time to work on this
|
||||||
|
website and so don't have an estimate on when that might happen.</p>
|
||||||
|
<p>in the meantime I suggest switching to <a href="https://firefox.com">something more modern.</a></p>
|
||||||
|
<p>sorry for the inconvenience!</p>
|
||||||
|
</div>
|
Loading…
Reference in a new issue