Convert styling to Tailwind (#15)

* Initialize tailwind

* Convert styling to Tailwind

* Add console output for CSS build

* Allow reading the feeds from an environment variable

* Change link styling and no-script time format

* Fix ellipses
This commit is contained in:
Carter McBride 2024-07-17 12:01:36 -06:00 committed by GitHub
parent 58d8f68825
commit 2932a6276e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 345 additions and 216 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
node_modules/*
public/index.html
public/styles.css
dist/*
.DS_Store
configs/feeds.json

BIN
bun.lockb

Binary file not shown.

View file

@ -1,100 +0,0 @@
{
"Comics": [
"http://www.catanacomics.com/rss",
"http://feeds.feedburner.com/InvisibleBread",
"https://hejibits.com/rss#_=_",
"http://www.hrwiki.org/wiki/Special:Updates",
"http://rockpapercynic.tumblr.com/rss"
],
"Podcasts & Videos": [
"https://lilyandsam.show/feed",
"https://www.youtube.com/feeds/videos.xml?channel_id=UC3g-w83Cb5pEAu5UmRrge-A",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCakAg8hC_RFJm4RI3DlD7SA",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCHZOwvEh9FAG95RO3PWhe5g",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCvVsD2hFZRgKNH7x5Q1wwug",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCDq5v10l4wkV5-ZBIJJFbzQ",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCMkbjxvwur30YrFWw8kpSaw",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCSuT9FSddzI6W5Bij9XwtmA",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCqqJQ_cXSat0KIAVfIfKkVA",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCuvSqzfO_LV_QzHdmEj84SQ",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCSdma21fnJzgmPodhC9SJ3g",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCj1VqrHhDte54oLgPG4xpuQ",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCy0tKL1T7wFoYcxCe0xjN6Q",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCsvn_Po0SmunchJYOWpOxMg"
],
"Friends and Family": [
"https://www.goodreads.com/review/list_rss/132710826?shelf=read",
"https://joekhoury.blog/feed/",
"https://mastodon.social/users/samwarnick.rss"
],
"Games": [
"https://www.nomanssky.com/news/feed/",
"https://www.lexaloffle.com/bbs/feed.php?uid=1",
"https://www.factorio.com/blog/rss",
"https://kill-the-newsletter.com/feeds/pghhn8aaf264tukg.xml",
"http://www.suppermariobroth.com/rss",
"http://unknownworlds.com/subnautica/feed/",
"https://store.steampowered.com/feeds/news/app/1675200/?cc=US&l=english"
],
"Finance": [
"http://feeds.feedburner.com/MrMoneyMustache",
"http://feeds.feedburner.com/DoctorOfCredit",
"https://kill-the-newsletter.com/feeds/tgor1vrwcf3f0uyo.xml",
"https://kill-the-newsletter.com/feeds/5q4gs79dfh32yr3h.xml"
],
"News": [
"http://www.economist.com/rss/the_world_this_week_rss.xml",
"https://newsroom.churchofjesuschrist.org/rss",
"http://feeds.feedburner.com/LdsChurchGrowth",
"https://www.sltrib.com/arc/outboundfeeds/news/?outputType=xml",
"https://www.ksl.com/rss/news/news_utah",
"https://www.readtangle.com/posts/rss/"
],
"Parenting": [
"https://technosapiens.substack.com/feed",
"https://parentdata.org/feed/"
],
"Fashion": [
"http://www.heddels.com/feed/",
"http://putthison.com/rss",
"https://articlesofinterest.substack.com/feed",
"https://dappered.com/feed/",
"https://dieworkwear.com/feed/",
"http://fromsqualortoballer.com/rss",
"https://fabricateurialist.substack.com/feed",
"https://www.menswearmusings.com/feed/",
"https://www.permanentstyle.com/feed"
],
"Products": [
"https://jellyfin.org/index.xml",
"https://gitlab.com/CalcProgrammer1/OpenRGB/-/tags?format=atom",
"http://feeds.feedburner.com/psblog",
"https://matrix.org/blog/feed/",
"https://brave.com/feed/",
"https://bitwarden.com/blog/feed.xml"
],
"Humor": [
"https://kill-the-newsletter.com/feeds/d90ng280lhh3p2nq.xml",
"https://aiweirdness.com/rss",
"http://mcmansionhell.tumblr.com/rss"
],
"Other": [
"https://kill-the-newsletter.com/feeds/400aiwxy5ox6ao0d.xml",
"http://brandonsanderson.com/feed/",
"https://kill-the-newsletter.com/feeds/j3c4wngg2hpjmdt8.xml",
"https://astralcodexten.substack.com/feed/"
],
"Tech News": [
"https://samwarnick.com/feed.rss",
"http://feeds.feedburner.com/CssTricks",
"http://chriscoyier.net/feed/",
"https://jenniferdaniel.substack.com/feed/",
"http://www.gamingonlinux.com/article_rss.php",
"http://www.theverge.com/rss/full.xml",
"https://hnrss.org/frontpage?points=70"
],
"Emulation": [
"http://melonds.kuribo64.net/rss.php",
"https://dolphin-emu.org/blog/feeds/"
]
}

View file

@ -7,7 +7,7 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Carter's RSS Feeds</title>
<link rel="icon" href="news-emoji.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="styles.css" />
<link rel="manifest" href="manifest.json" />
<script type="module" defer>
/**
@ -98,35 +98,45 @@
</script>
</head>
<body>
<header>
<body class="font-system bg-arc-background text-base m-4">
<header class="text-2xl text-arc-title">
<h1>📰 Carter's RSS Feeds</h1>
</header>
<main>
<main class="grid gap-4 grid-cols-[repeat(auto-fill,_minmax(300px,_1fr))] mt-2">
{% for group, feeds in data %}
<section>
<h2>{{ group }}</h2>
<section class="">
<h2 class="text-xl">{{ group }}</h2>
{% for feed in feeds %}
<details>
<summary>
<span class="feed-title">{{ feed.title }}</span>
<span class="feed-url" title="{{feed.feed}}">({{ feed.feed }})</span>
<details class="group">
<summary class="cursor-pointer p-2 rounded-xl mt-1 transition-colors group-has-[.has-recent]:border-2 group-has-[.has-recent]:border-arc-title hover:bg-arc-hover flex flex-row items-center">
{% if feed.hasRecent %}
<div class="text-xl mr-2" aria-label="Feed has recent items">✳︎</div>
{% endif %}
<div class="w-full">
<div>{{ feed.title }}</div>
<div class="text-sm whitespace-nowrap text-ellipsis w-11/12 overflow-hidden text-arc-subtitle" title="{{feed.feed}}">({{ feed.feed }})</div>
</div>
</summary>
<ul>
<ul class="max-h-64 overflow-y-auto list-none m-0">
{% for item in feed.items %}
<li class="{% if item.isRecent %}has-recent{% endif %}">
<div>
<a class="article-title" href="{{ item.link }}" target="_blank" rel="noopener norefferer nofollow">{{
item.title | safe}}</a>
</div>
<div class="article-timestamp">
<relative-time data-time="{{item.timestamp}}">{{ item.timestamp | relative}}</relative-time>
</div>
{% if yazzyUrl %}
<div class="article-links">
<a href="{{ yazzyUrl }}/{{ item.link }}" target="_blank" rel="noreferrer noopener">yazzy</a>
</div>
<li class="{% if item.isRecent %}has-recent {% endif %} flex flex-row items-center p-2 rounded-xl mt-1 transition-colors hover:bg-arc-hover">
{% if item.isRecent %}
<div class="text-xl mr-2" aria-label="Item is recent">✳︎</div>
{% endif %}
<div>
<div>
<a class="no-underline hover:text-inherit" href="{{ item.link }}" target="_blank" rel="noopener norefferer nofollow">{{
item.title | safe}}</a>
</div>
<div class="text-sm text-arc-subtitle">
<relative-time data-time="{{item.timestamp}}">{{ item.timestamp | formatDateTime}}</relative-time>
</div>
{% if yazzyUrl %}
<div class="text-sm text-arc-subtitle">
<a href="{{ yazzyUrl }}/{{ item.link }}" target="_blank" rel="noreferrer noopener">yazzy</a>
</div>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
@ -136,17 +146,17 @@
{% endfor %}
</main>
{% if errors | length > 0 %}
<h2>Errors</h2>
<h2 class="text-xl">Errors</h2>
<p>There were errors trying to parse these feeds:</p>
<ul class="errors">
{% for error in errors %}
<li>{{ error }}</li>
<li class="break-all">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<footer>
<hr>
<p>Last updated <relative-time data-time="{{ now }}">{{ now | relative}}</relative-time>.</p>
<p>Last updated <relative-time data-time="{{ now }}">{{ now | formatDateTime}}</relative-time>.</p>
<p>
<a href="https://github.com/carterworks/rss-reader">View on GitHub</a>
</p>

View file

@ -7,8 +7,9 @@
"type": "module",
"scripts": {
"clean": "rm -rf dist",
"build": "bun src/index.ts",
"check": "biome check --write ./{src,config,public} ./*.json --no-errors-on-unmatched"
"build": "NODE_ENV=production bun src/index.ts",
"check": "biome check --write ./{src,config,public} ./*.{json,js} --no-errors-on-unmatched",
"dev": "bun src/index.ts"
},
"author": {
"name": "George Mandis",
@ -36,6 +37,7 @@
"@types/bun": "latest",
"@types/nunjucks": "^3.2.2",
"@types/xml2js": "^0.4.11",
"tailwindcss": "^3.4.6",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"typescript-eslint": "^7.13.1"

View file

@ -1,80 +0,0 @@
:root {
--color-bg: oklch(97.1% 0.024 88.23);
--color-hover: oklch(94.48% 0.024 88.23);
--color-text: oklch(24.74% 0.024 88.23);
--color-new: oklch(94.48% 0.024 38.23);
}
body {
font-family: system-ui;
font-size: 18px;
background: var(--color-bg);
color: var(--color-text);
}
main {
display: grid;
gap: 1em;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
section ul {
max-height: 250px;
overflow-y: auto;
}
.inline-icon {
height: 1em;
vertical-align: text-bottom;
}
summary {
cursor: pointer;
text-overflow: ellipsis;
}
summary,
details li {
padding: 0.5em;
border-radius: 12px;
margin-block-start: 4px;
transition: background cubic-bezier(0.39, 0.575, 0.565, 1) 0.2s;
}
summary:hover,
details li:hover {
background: var(--color-hover)
}
details:has(li.has-recent) {
summary,
li.has-recent {
background: var(--color-new);
}
}
.errors li {
word-break: break-all;
}
.feed-url {
color: #aaa;
white-space: nowrap;
display: inline-block;
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
}
.article-timestamp,
.feed-url,
.article-links {
font-size: 0.75em;
}
details ul {
list-style-type: none;
margin: 0;
}

3
public/styles.input.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -18,6 +18,7 @@ import Parser from "rss-parser";
import type { FeedItem, Feeds } from "./@types/bubo";
import { render } from "./renderer.js";
import {
buildCSS,
getBuboInfo,
getFeedList,
getLink,
@ -26,9 +27,21 @@ import {
parseFeed,
} from "./utilities.js";
const cssInput = "./public/styles.input.css";
const cssOutput = "./public/styles.css";
const minifyCss = process.env.NODE_ENV === "production";
await buildCSS(minifyCss, cssInput, cssOutput);
const buboInfo = await getBuboInfo();
const parser = new Parser();
const feedList = await getFeedList();
const feedOptions: Parameters<typeof getFeedList>[0] = {
feeds: process.env.FEEDS,
feedFilePath: process.env.FEEDS
? ""
: process.env.FEED_FILE ?? "../config/feeds.json",
};
console.log("feedOptions", JSON.stringify(feedOptions, null, 2));
const feedList = await getFeedList(feedOptions);
const feedListLength =
Object.entries(feedList).flat(2).length - Object.keys(feedList).length;
@ -90,7 +103,7 @@ const finishBuild: () => void = async () => {
data: sortedFeeds,
errors: errors,
info: buboInfo,
yazzyUrl
yazzyUrl,
});
// write the output to public/index.html
@ -140,7 +153,7 @@ const processFeed =
item.link = getLink(item);
const timestamp = new Date(Number.parseInt(item.timestamp));
const eightHoursAgo = new Date();
eightHoursAgo.setHours(eightHoursAgo.getHours() - 8);
eightHoursAgo.setHours(eightHoursAgo.getHours() - 24);
item.isRecent = timestamp > eightHoursAgo;
}

View file

@ -23,6 +23,11 @@ env.addFilter("formatTime", (dateString): string => {
return !Number.isNaN(date.getTime()) ? date.toLocaleTimeString() : dateString;
});
env.addFilter("formatDateTime", (dateString): string => {
const date: Date = new Date(Number.parseInt(dateString));
return !Number.isNaN(date.getTime()) ? date.toLocaleString() : dateString;
});
env.addGlobal("now", new Date().getTime());
// load the template

View file

@ -1,3 +1,4 @@
import { $ } from "bun";
/*
There's a little inconsistency with how feeds report certain things like
title, links and timestamps. These helpers try to normalize that bit and
@ -75,11 +76,18 @@ export async function parseFeed(response: Response): Promise<JSONValue> {
return (rssFeed && rssFeed) || (jsonFeed && jsonFeed) || {};
}
export const getFeedList = async (): Promise<JSONValue> => {
export const getFeedList = async ({
feedFilePath,
feeds,
}: { feedFilePath?: string; feeds?: string }): Promise<JSONValue> => {
if (feeds) {
return JSON.parse(feeds);
}
if (!feedFilePath) {
throw new Error("No feed list provided");
}
return JSON.parse(
(
await readFile(new URL("../config/feeds.json", import.meta.url))
).toString(),
(await readFile(new URL(feedFilePath, import.meta.url))).toString(),
);
};
@ -88,3 +96,18 @@ export const getBuboInfo = async (): Promise<JSONValue> => {
(await readFile(new URL("../package.json", import.meta.url))).toString(),
);
};
export const buildCSS = async (
minify: boolean,
input: string,
destination: string,
): Promise<void> => {
const output =
await $`bun x tailwindcss -i ${input} ${minify ? "--minify" : ""} -o ${destination}`;
if (output.exitCode !== 0) {
const err = new TextDecoder().decode(output.stderr);
throw new Error(`Building tailwind failed: ${err}`);
}
console.log(`Successfully built CSS to ${destination}`);
return;
};

251
tailwind.config.js Normal file
View file

@ -0,0 +1,251 @@
const plugin = require("tailwindcss/plugin");
/** @type {import('tailwindcss').Config} */
export default {
content: ["./config/*.html"],
theme: {
extend: {
fontFamily: {
// from https://github.com/system-fonts/modern-font-stacks
/* System UI fonts are those native to the operating system interface.
* They are highly legible and easy to read at small sizes, contains
* many font weights, and is ideal for UI elements.
*/
system: ["system-ui", "sans-serif"],
/* Transitional typefaces are a mix between Old Style and Modern
* typefaces that was developed during The Enlightenment. One of the
* most famous examples of a Transitional typeface is Times New Roman,
* which was developed for the Times of London newspaper.
*/
transitional: [
"Charter",
"'Bitstream Charter'",
"'Sitka Text'",
"Cambria",
"serif",
],
/* Old Style typefaces are characterized by diagonal stress, low
* contrast between thick and thin strokes, and rounded serifs, and
* were developed in the Renaissance period. One of the most famous
* examples of an Old Style typeface is Garamond.
*/
"old-style": [
"'Iowan Old Style'",
"'Palatino Linotype'",
"'URW Palladio L'",
"P052",
"serif",
],
/* Humanist typefaces are characterized by their organic, calligraphic
* forms and low contrast between thick and thin strokes. These
* typefaces are inspired by the handwriting of the Renaissance period
* and are often considered to be more legible and easier to read than
* other sans-serif typefaces.
*/
humanist: [
"Seravek",
"'Gill Sans Nova'",
"Ubuntu",
"Calibri",
"'DejaVu Sans'",
"source-sans-pro",
"sans-serif",
],
/* Geometric Humanist typefaces are characterized by their clean,
* geometric forms and uniform stroke widths. These typefaces are often
* considered to be modern and sleek in appearance, and are often used
* for headlines and other display purposes. Futura is a famous example
* of this classification.
*/
"gemoetric-humanist": [
"Avenir",
"Montserrat",
"Corbel",
"'URW Gothic'",
"source-sans-pro",
"sans-serif",
],
/* Classical Humanist typefaces are characterized by how the strokes
* subtly widen as they reach the stroke terminals without ending in a
* serif. These typefaces are inspired by classical Roman capitals and
* the stone-carving on Renaissance-period tombstones.
*/
"classical-humanist": [
"Optima",
"Candara",
"'Noto Sans'",
"source-sans-pro",
"sans-serif",
],
/* Neo-Grotesque typefaces are a style of sans-serif that was developed
* in the late 19th and early 20th centuries and is characterized by its
* clean, geometric forms and uniform stroke widths. One of the most
* famous examples of a Neo-Grotesque typeface is Helvetica.
*/
"neo-grotesque": [
"Inter",
"Roboto",
"'Helvetica Neue'",
"'Arial Nova'",
"'Nimbus Sans'",
"Arial",
"sans-serif",
],
/* Monospace Slab Serif typefaces are characterized by their fixed-width
* letters, which have the same width regardless of their shape, and its
* simple, geometric forms. Used to emulate typewriter output for
* reports, tabular work and technical documentation.
*/
"monospace-slab-serif": [
"'Nimbus Mono PS'",
"'Courier New'",
"monospace",
],
/* Monospace Code typefaces are specifically designed for use in
* programming and other technical applications. These typefaces are
* characterized by their monospaced design, which means that all
* letters and characters have the same width, and their clear, legible
* forms.
*/
"monospace-code": [
"ui-monospace",
"'Cascadia Code'",
"'Source Code Pro'",
"Menlo",
"Consolas",
"'DejaVu Sans Mono'",
"monospace",
],
/* Industrial typefaces originated in the late 19th century and was
* heavily influenced by the advancements in technology and industry
* during that time. Industrial typefaces are characterized by their
* bold, sans-serif letterforms, simple and straightforward appearance,
* and the use of straight lines and geometric shapes.
*/
industrial: [
"Bahnschrift",
"'DIN Alternate'",
"'Franklin Gothic Medium'",
"'Nimbus Sans Narrow'",
"sans-serif-condensed",
"sans-serif",
],
/* Rounded typefaces are characterized by the rounded curved letterforms
* and give a softer, friendlier appearance. The rounded edges give the
* typeface a more organic and
* playful feel, making it suitable for use in informal or child-friendly
* designs. The rounded sans-serif style has been popular since the 1950s,
* and it continues to be widely used in advertising, branding, and
* other forms of graphic design.
*/
"rounded-sans": [
"ui-rounded",
"'Hiragino Maru Gothic ProN'",
"Quicksand",
"Comfortaa",
"Manjari",
"'Arial Rounded MT'",
"'Arial Rounded MT Bold'",
"Calibri",
"source-sans-pro",
"sans-serif",
],
/* Slab Serif typefaces are characterized by the presence of thick,
* block-like serifs on the ends of each letterform. These serifs are
* usually unbracketed, meaning they do not have any curved or tapered
* transitions to the main stroke of the letter.
*/
"slab-serif": [
"Rockwell",
"'Rockwell Nova'",
"'Roboto Slab'",
"'DejaVu Serif'",
"'Sitka Small'",
"serif",
],
/* Antique typefaces, also known as Egyptians, are a subset of serif
* typefaces that were popular in the 19th century. They are
* characterized by their block-like serifs and thick uniform stroke
* weight.
*/
antique: [
"Superclarendon",
"'Bookman Old Style'",
"'URW Bookman'",
"'URW Bookman L'",
"'Georgia Pro'",
"Georgia",
"serif",
],
/* Didone typefaces, also known as Modern typefaces, are characterized
* by the high contrast between thick and thin strokes, vertical stress,
* and hairline serifs with no bracketing. The Didone style emerged in
* the late 18th century and gained popularity during the 19th century.
*/
didone: [
"Didot",
"'Bodoni MT'",
"'Noto Serif Display'",
"'URW Palladio L'",
"P052",
"Sylfaen",
"serif",
],
/* Handwritten typefaces are designed to mimic the look and feel of
* handwriting. Despite the vast array of handwriting styles, this font
* stack tend to adopt a more informal and everyday style of handwriting.
*/
handwritten: [
"'Segoe Print'",
"'Bradley Hand'",
"Chilanka",
"TSCu_Comic",
"casual",
"cursive",
],
emoji: [
"'Apple Color Emoji'",
"'Segoe UI Emoji'",
"'Segoe UI Symbol'",
"'Noto Color Emoji'",
],
},
colors: {
/*
* The Arc browser (can) expose its theme color to the pages beneath as
* CSS custom properties.
*/
arc: {
"background-simple": "var(--arc-background-simple-color, #FDF8EBFF)",
maxContrast: "var(--arc-palette-maxContrastColor, #6F540AFF)",
minContrast: "var(--arc-palette-minContrastColor, #FDF8EBFF)",
hover: "var(--arc-palette-hover, #E2DDCAFF)",
foregroundPrimary: "var(--arc-palette-foregroundPrimary, #EBB218FF)",
foregroundSecondary:
"var(--arc-palette-foregroundSecondary, #EBB218FF)",
foregroundTertiary:
"var(--arc-palette-foregroundTertiary, #FDF8EBFF)",
title: "var(--arc-palette-title, #211900FF)",
subtitle: "var(--arc-palette-subtitle, #C5BB96FF)",
background: "var(--arc-palette-background, #F1EEE5FF)",
backgroundExtra: "var(--arc-palette-backgroundExtra, #FEFDFCFF)",
cutout: "var(--arc-palette-cutoutColor, #FDF8EBFF)",
focus: "var(--arc-palette-focus, #B7A97CFF)",
},
},
},
},
plugins: [
plugin(({ addBase, theme }) => {
addBase({
a: {
textDecoration: "underline",
transition: "color 0.2s",
cursor: "pointer",
"&:hover": {
color: theme("colors.arc.subtitle"),
},
},
});
}),
],
};