refactor(templates): decouple css and javascript from html #45

Merged
eleith merged 2 commits from gemini/refactor/decouple-assets into main 2025-08-19 04:42:21 +00:00
5 changed files with 463 additions and 428 deletions

No files matched your search

+2 -427
View File
@@ -6,262 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Category.Title}}</title>
<style>
:root {
--background-color: #f3f4f6;
--text-color: #374151;
--container-background: #ffffff;
--header-title-color: #1f2937;
--header-date-color: #6b7280;
--group-title-color: #6b7280;
--entry-background: #f9fafb;
--entry-border-color: #e5e7eb;
--entry-focus-shadow: rgba(59, 130, 246, 0.5);
--entry-focus-border: #3b82f6;
--entry-summary-color: #1d4ed8;
--entry-open-background: #e0f2fe;
--entry-open-border: #bfdbfe;
--entry-active-shadow: rgba(234, 88, 12, 0.7);
--entry-active-border: #ea580c;
--entry-active-background: #fff7ed;
--entry-meta-text-color: #6b7280;
--entry-meta-border-color: #e5e7eb;
--entry-content-text-color: #4b5563;
--entry-link-color: #3b82f6;
--entry-link-focus-background: rgba(59, 130, 246, 0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1f2937;
--text-color: #d1d5db;
--container-background: #111827;
--header-title-color: #f9fafb;
--header-date-color: #9ca3af;
--group-title-color: #9ca3af;
--entry-background: #374151;
--entry-border-color: #4b5563;
--entry-focus-shadow: rgba(96, 165, 250, 0.5);
--entry-focus-border: #60a5fa;
--entry-summary-color: #93c5fd;
--entry-open-background: #374151;
--entry-open-border: #4b5563;
--entry-active-shadow: rgba(251, 146, 60, 0.7);
--entry-active-border: #fb923c;
--entry-active-background: #4a2c13;
--entry-meta-text-color: #9ca3af;
--entry-meta-border-color: #4b5563;
--entry-content-text-color: #d1d5db;
--entry-link-color: #00ff00;
--entry-visited-link-color: #00cc00;
--entry-link-focus-background: rgba(96, 165, 250, 0.1);
}
}
a {
color: var(--entry-link-color);
}
a:visited {
color: var(--entry-visited-link-color);
}
body {
font-family: sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 1.5rem;
}
section.header {
margin-bottom: 1.5rem;
text-align: center;
}
section.header h1.title {
font-size: 1.875rem;
font-weight: 700;
color: var(--header-title-color);
margin-bottom: 0.25rem;
}
section.header .date {
font-size: 0.875rem;
color: var(--header-date-color);
}
section.summary p {
margin: 1rem auto;
padding: 0.5rem 1rem;
background-color: var(--container-background);
border: 1px solid var(--entry-border-color);
border-radius: 0.5rem;
}
h2.group-entries-title {
font-size: 1.25rem;
font-weight: 500;
color: var(--group-title-color);
margin-top: 0.5rem;
margin-bottom: 0.5rem;
padding-left: 0.5rem;
}
section.entry {
background-color: var(--entry-background);
border-radius: 0.5rem;
padding: 0;
margin-bottom: 1rem;
border: 1px solid var(--entry-border-color);
}
section.entry:focus-within {
box-shadow: 0 0 0 3px var(--entry-focus-shadow);
border-color: var(--entry-focus-border);
}
summary.entry {
font-size: 1.25rem;
font-weight: 450;
padding: 0.5rem;
cursor: pointer;
color: var(--entry-summary-color);
text-decoration: none;
list-style: none;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
details[open] summary.entry {
white-space: normal;
overflow: visible;
text-overflow: initial;
}
summary.entry:focus {
outline: none;
}
section.entry:has(details[open]) {
background-color: var(--entry-open-background);
border-color: var(--entry-open-border);
}
section.entry.active {
box-shadow: 0 0 0 3px var(--entry-active-shadow);
border-color: var(--entry-active-border);
background-color: var(--entry-active-background);
}
section.entry.viewed:not(.active) {
filter: grayscale(100%);
opacity: 0.75;
}
.entry-meta {
filter: grayscale(100%);
font-size: 0.875rem;
color: var(--entry-meta-text-color);
display: flex;
flex-direction: row;
gap: 0.25rem;
align-items: center;
justify-content: start;
border-top: 1px solid var(--entry-meta-border-color);
}
section.entry.active > .entry-meta {
filter: none;
}
.entry-meta-tab {
padding: 0.25rem 0.5rem;
border-right: 1px solid var(--entry-meta-border-color);
display: flex;
}
.entry-meta-tab-feed {
display: flex;
align-items: center;
gap: 0.25rem;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
min-width: 0;
}
.entry-meta-tab:last-child {
border-right: none;
}
.entry-meta-tab-feed-title {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.entry-content {
font-size: 1rem;
color: var(--entry-content-text-color);
padding: 1rem;
overflow-wrap: anywhere;
}
.entry-content img {
max-width: 100%;
height: auto;
border-radius: 0.25rem;
margin-top: 0.5rem;
}
.no-entries {
text-align: center;
font-style: italic;
color: var(--text-color);
padding: 2rem;
}
a.entry-link {
color: var(--entry-link-color);
text-decoration: none;
}
a.entry-link:hover {
text-decoration: underline;
}
a.entry-link:focus {
outline: 2px solid var(--entry-link-color);
outline-offset: 2px;
border-radius: 0.25rem;
background-color: var(--entry-link-focus-background);
}
details.entry {}
details.entry h1 {
font-size: 1.5rem;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
details summary {
cursor: pointer;
}
details summary>* {
display: inline;
}
{{include "static/style.css"}}
{{range .FeedIcons}} div.feed-icon-{{.FeedID}} {
background-image: url("data:{{.Data}}");
@@ -269,7 +14,6 @@
width: 16px;
height: 16px;
}
{{end}}
</style>
</head>
@@ -334,176 +78,7 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const entries = Array.from(document.querySelectorAll('section.entry'));
let activeEntry = null;
let markAsReadTimer = null;
let focusTimer = null;
let lastScrollY = window.scrollY;
function getVisibleEntries() {
return entries.filter(entry => !entry.classList.contains('entry-hidden'));
}
function markAsRead(entry) {
const visibleEntries = getVisibleEntries();
const entryIndex = visibleEntries.indexOf(entry);
for (let i = 0; i < entryIndex; i++) {
visibleEntries[i].classList.add('viewed');
}
}
function setActiveEntry(entry, preventScroll = false) {
clearTimeout(markAsReadTimer);
if (activeEntry) {
activeEntry.classList.remove('active');
}
activeEntry = entry;
if (activeEntry) {
activeEntry.classList.add('viewed');
activeEntry.classList.add('active');
if (!preventScroll) {
activeEntry.querySelector('summary')?.focus();
}
markAsReadTimer = setTimeout(() => {
markAsRead(activeEntry);
}, 1000);
}
}
function focusEntry(startIndex, direction) {
let nextIndex = startIndex;
const visibleEntries = getVisibleEntries();
if (visibleEntries.length === 0) {
setActiveEntry(null);
return;
}
if (direction === 'next') {
nextIndex = Math.min(startIndex + 1, visibleEntries.length - 1);
} else if (direction === 'prev') {
nextIndex = Math.max(startIndex - 1, 0);
}
const newEntry = visibleEntries[nextIndex];
if (newEntry) {
setActiveEntry(newEntry);
}
}
function navigate(direction) {
const visibleEntries = getVisibleEntries();
if (visibleEntries.length === 0) return;
const currentIndex = activeEntry ? visibleEntries.indexOf(activeEntry) : -1;
switch (direction) {
case 'next':
focusEntry(currentIndex, 'next');
break;
case 'prev':
focusEntry(currentIndex, 'prev');
break;
case 'first':
setActiveEntry(visibleEntries[0]);
break;
case 'last':
setActiveEntry(visibleEntries[visibleEntries.length - 1]);
break;
}
}
const observer = new IntersectionObserver((intersectionEntries) => {
const scrollDirection = window.scrollY > lastScrollY ? 'down' : 'up';
lastScrollY = window.scrollY;
const activeEntryIsVisible = entries.some(entry => {
if (entry === activeEntry) {
const rect = entry.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight;
}
return false;
});
if (activeEntryIsVisible) {
return;
}
intersectionEntries.forEach(intersectionEntry => {
const {
target,
isIntersecting,
boundingClientRect
} = intersectionEntry;
if (scrollDirection === 'down' && !isIntersecting && boundingClientRect.top < 0) {
clearTimeout(focusTimer);
focusTimer = setTimeout(() => {
const visibleEntries = getVisibleEntries();
const nextEntry = visibleEntries[visibleEntries.indexOf(target) + 1];
if (nextEntry) {
setActiveEntry(nextEntry, true);
}
}, 450);
}
});
}, {
threshold: 0.5
});
entries.forEach(entry => observer.observe(entry));
if (entries.length) {
setActiveEntry(getVisibleEntries()[0]);
}
document.addEventListener('click', (event) => {
const target = event.target;
const entry = target.closest('section.entry');
if (entry) {
setActiveEntry(entry);
}
});
document.addEventListener('keydown', (event) => {
if (['j', 'k', 'v', 'D', 'U', 'V'].includes(event.key)) {
event.preventDefault();
switch (event.key) {
case 'j':
navigate('next');
break;
case 'k':
navigate('prev');
break;
case 'U':
navigate('last');
break;
case 'D':
navigate('first');
break;
case 'V':
if (activeEntry) {
const link = activeEntry.querySelector('a[data-link-internal]');
if (link && link.href) window.open(link.href, '_blank');
}
break;
case 'v':
if (activeEntry) {
const link = activeEntry.querySelector('a[data-link-external]');
if (link && link.href) window.open(link.href, '_blank');
}
break;
}
}
});
});
{{include "static/script.js"}}
</script>
</body>
+34
View File
@@ -0,0 +1,34 @@
package templates
import (
"embed"
"html/template"
"io/fs"
"path/filepath"
"strings"
)
//go:embed static
var staticFS embed.FS
func FuncMap() template.FuncMap {
return template.FuncMap{
"include": include,
}
}
func include(filename string) (any, error) {
b, err := fs.ReadFile(staticFS, filename)
if err != nil {
return "", err
}
switch strings.ToLower(filepath.Ext(filename)) {
case ".css":
return template.CSS(b), nil
case ".js":
return template.JS(b), nil
default:
return template.HTML(b), nil
}
}
+170
View File
@@ -0,0 +1,170 @@
document.addEventListener('DOMContentLoaded', () => {
const entries = Array.from(document.querySelectorAll('section.entry'));
let activeEntry = null;
let markAsReadTimer = null;
let focusTimer = null;
let lastScrollY = window.scrollY;
function getVisibleEntries() {
return entries.filter(entry => !entry.classList.contains('entry-hidden'));
}
function markAsRead(entry) {
const visibleEntries = getVisibleEntries();
const entryIndex = visibleEntries.indexOf(entry);
for (let i = 0; i < entryIndex; i++) {
visibleEntries[i].classList.add('viewed');
}
}
function setActiveEntry(entry, preventScroll = false) {
clearTimeout(markAsReadTimer);
if (activeEntry) {
activeEntry.classList.remove('active');
}
activeEntry = entry;
if (activeEntry) {
activeEntry.classList.add('viewed');
activeEntry.classList.add('active');
if (!preventScroll) {
activeEntry.querySelector('summary')?.focus();
}
markAsReadTimer = setTimeout(() => {
markAsRead(activeEntry);
}, 1000);
}
}
function focusEntry(startIndex, direction) {
let nextIndex = startIndex;
const visibleEntries = getVisibleEntries();
if (visibleEntries.length === 0) {
setActiveEntry(null);
return;
}
if (direction === 'next') {
nextIndex = Math.min(startIndex + 1, visibleEntries.length - 1);
} else if (direction === 'prev') {
nextIndex = Math.max(startIndex - 1, 0);
}
const newEntry = visibleEntries[nextIndex];
if (newEntry) {
setActiveEntry(newEntry);
}
}
function navigate(direction) {
const visibleEntries = getVisibleEntries();
if (visibleEntries.length === 0) return;
const currentIndex = activeEntry ? visibleEntries.indexOf(activeEntry) : -1;
switch (direction) {
case 'next':
focusEntry(currentIndex, 'next');
break;
case 'prev':
focusEntry(currentIndex, 'prev');
break;
case 'first':
setActiveEntry(visibleEntries[0]);
break;
case 'last':
setActiveEntry(visibleEntries[visibleEntries.length - 1]);
break;
}
}
const observer = new IntersectionObserver((intersectionEntries) => {
const scrollDirection = window.scrollY > lastScrollY ? 'down' : 'up';
lastScrollY = window.scrollY;
const activeEntryIsVisible = entries.some(entry => {
if (entry === activeEntry) {
const rect = entry.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight;
}
return false;
});
if (activeEntryIsVisible) {
return;
}
intersectionEntries.forEach(intersectionEntry => {
const {
target,
isIntersecting,
boundingClientRect
} = intersectionEntry;
if (scrollDirection === 'down' && !isIntersecting && boundingClientRect.top < 0) {
clearTimeout(focusTimer);
focusTimer = setTimeout(() => {
const visibleEntries = getVisibleEntries();
const nextEntry = visibleEntries[visibleEntries.indexOf(target) + 1];
if (nextEntry) {
setActiveEntry(nextEntry, true);
}
}, 450);
}
});
}, {
threshold: 0.5
});
entries.forEach(entry => observer.observe(entry));
if (entries.length) {
setActiveEntry(getVisibleEntries()[0]);
}
document.addEventListener('click', (event) => {
const target = event.target;
const entry = target.closest('section.entry');
if (entry) {
setActiveEntry(entry);
}
});
document.addEventListener('keydown', (event) => {
if (['j', 'k', 'v', 'D', 'U', 'V'].includes(event.key)) {
event.preventDefault();
switch (event.key) {
case 'j':
navigate('next');
break;
case 'k':
navigate('prev');
break;
case 'U':
navigate('last');
break;
case 'D':
navigate('first');
break;
case 'V':
if (activeEntry) {
const link = activeEntry.querySelector('a[data-link-internal]');
if (link && link.href) window.open(link.href, '_blank');
}
break;
case 'v':
if (activeEntry) {
const link = activeEntry.querySelector('a[data-link-external]');
if (link && link.href) window.open(link.href, '_blank');
}
break;
}
}
});
});
+256
View File
@@ -0,0 +1,256 @@
:root {
--background-color: #f3f4f6;
--text-color: #374151;
--container-background: #ffffff;
--header-title-color: #1f2937;
--header-date-color: #6b7280;
--group-title-color: #6b7280;
--entry-background: #f9fafb;
--entry-border-color: #e5e7eb;
--entry-focus-shadow: rgba(59, 130, 246, 0.5);
--entry-focus-border: #3b82f6;
--entry-summary-color: #1d4ed8;
--entry-open-background: #e0f2fe;
--entry-open-border: #bfdbfe;
--entry-active-shadow: rgba(234, 88, 12, 0.7);
--entry-active-border: #ea580c;
--entry-active-background: #fff7ed;
--entry-meta-text-color: #6b7280;
--entry-meta-border-color: #e5e7eb;
--entry-content-text-color: #4b5563;
--entry-link-color: #3b82f6;
--entry-link-focus-background: rgba(59, 130, 246, 0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1f2937;
--text-color: #d1d5db;
--container-background: #111827;
--header-title-color: #f9fafb;
--header-date-color: #9ca3af;
--group-title-color: #9ca3af;
--entry-background: #374151;
--entry-border-color: #4b5563;
--entry-focus-shadow: rgba(96, 165, 250, 0.5);
--entry-focus-border: #60a5fa;
--entry-summary-color: #93c5fd;
--entry-open-background: #374151;
--entry-open-border: #4b5563;
--entry-active-shadow: rgba(251, 146, 60, 0.7);
--entry-active-border: #fb923c;
--entry-active-background: #4a2c13;
--entry-meta-text-color: #9ca3af;
--entry-meta-border-color: #4b5563;
--entry-content-text-color: #d1d5db;
--entry-link-color: #00ff00;
--entry-visited-link-color: #00cc00;
--entry-link-focus-background: rgba(96, 165, 250, 0.1);
}
}
a {
color: var(--entry-link-color);
}
a:visited {
color: var(--entry-visited-link-color);
}
body {
font-family: sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 1.5rem;
}
section.header {
margin-bottom: 1.5rem;
text-align: center;
}
section.header h1.title {
font-size: 1.875rem;
font-weight: 700;
color: var(--header-title-color);
margin-bottom: 0.25rem;
}
section.header .date {
font-size: 0.875rem;
color: var(--header-date-color);
}
section.summary p {
margin: 1rem auto;
padding: 0.5rem 1rem;
background-color: var(--container-background);
border: 1px solid var(--entry-border-color);
border-radius: 0.5rem;
}
h2.group-entries-title {
font-size: 1.25rem;
font-weight: 500;
color: var(--group-title-color);
margin-top: 0.5rem;
margin-bottom: 0.5rem;
padding-left: 0.5rem;
}
section.entry {
background-color: var(--entry-background);
border-radius: 0.5rem;
padding: 0;
margin-bottom: 1rem;
border: 1px solid var(--entry-border-color);
}
section.entry:focus-within {
box-shadow: 0 0 0 3px var(--entry-focus-shadow);
border-color: var(--entry-focus-border);
}
summary.entry {
font-size: 1.25rem;
font-weight: 450;
padding: 0.5rem;
cursor: pointer;
color: var(--entry-summary-color);
text-decoration: none;
list-style: none;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
details[open] summary.entry {
white-space: normal;
overflow: visible;
text-overflow: initial;
}
summary.entry:focus {
outline: none;
}
section.entry:has(details[open]) {
background-color: var(--entry-open-background);
border-color: var(--entry-open-border);
}
section.entry.active {
box-shadow: 0 0 0 3px var(--entry-active-shadow);
border-color: var(--entry-active-border);
background-color: var(--entry-active-background);
}
section.entry.viewed:not(.active) {
filter: grayscale(100%);
opacity: 0.75;
}
.entry-meta {
filter: grayscale(100%);
font-size: 0.875rem;
color: var(--entry-meta-text-color);
display: flex;
flex-direction: row;
gap: 0.25rem;
align-items: center;
justify-content: start;
border-top: 1px solid var(--entry-meta-border-color);
}
section.entry.active > .entry-meta {
filter: none;
}
.entry-meta-tab {
padding: 0.25rem 0.5rem;
border-right: 1px solid var(--entry-meta-border-color);
display: flex;
}
.entry-meta-tab-feed {
display: flex;
align-items: center;
gap: 0.25rem;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
min-width: 0;
}
.entry-meta-tab:last-child {
border-right: none;
}
.entry-meta-tab-feed-title {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.entry-content {
font-size: 1rem;
color: var(--entry-content-text-color);
padding: 1rem;
overflow-wrap: anywhere;
}
.entry-content img {
max-width: 100%;
height: auto;
border-radius: 0.25rem;
margin-top: 0.5rem;
}
.no-entries {
text-align: center;
font-style: italic;
color: var(--text-color);
padding: 2rem;
}
a.entry-link {
color: var(--entry-link-color);
text-decoration: none;
}
a.entry-link:hover {
text-decoration: underline;
}
a.entry-link:focus {
outline: 2px solid var(--entry-link-color);
outline-offset: 2px;
border-radius: 0.25rem;
background-color: var(--entry-link-focus-background);
}
details.entry {}
details.entry h1 {
font-size: 1.5rem;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
details summary {
cursor: pointer;
}
details summary > * {
display: inline;
}
+1 -1
View File
@@ -27,7 +27,7 @@ func init() {
archiveTemplateName := "entries.gohtml"
emailTemplateName := "email.gotxt"
ArchiveTemplate, err = htmlTemplate.New(archiveTemplateName).Funcs(htmlTemplate.FuncMap{
ArchiveTemplate, err = htmlTemplate.New(archiveTemplateName).Funcs(FuncMap()).Funcs(htmlTemplate.FuncMap{
"htmlEscape": func(s string) htmlTemplate.HTML {
return htmlTemplate.HTML(s)
},