First deployment
Some checks failed
quality-gates / lint-and-test (push) Has been cancelled
quality-gates / security-scan (push) Has been cancelled

This commit is contained in:
2026-02-13 09:14:04 -05:00
parent 0e21e035f5
commit 679561bcdb
128 changed files with 3479 additions and 120 deletions

View File

@@ -103,8 +103,13 @@
--cf-select-text: #ffffff;
--cf-select-border: rgba(255, 255, 255, 0.55);
}
.cf-body { background: var(--cf-bg); color: var(--cf-text); }
.cf-body { background: var(--cf-bg); color: var(--cf-text); padding-bottom: 78px; }
.cf-header { background: var(--cf-header-bg); }
.cf-header-top { box-shadow: none; border-color: rgba(148, 163, 184, 0.12); }
.cf-header-scrolled {
box-shadow: 0 10px 28px rgba(2, 6, 23, 0.24);
border-color: rgba(148, 163, 184, 0.28);
}
.cf-card { background: var(--cf-card-bg) !important; }
.cf-modal { background: var(--cf-modal-bg); }
.cf-select {
@@ -129,7 +134,7 @@
}
.theme-menu-item:hover { background: rgba(92, 124, 250, 0.15); }
.hero-overlay {
background: linear-gradient(to top, rgba(2, 6, 23, 0.94), rgba(15, 23, 42, 0.62), rgba(15, 23, 42, 0.22), transparent);
background: linear-gradient(to top, rgba(2, 6, 23, 0.97), rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.35), transparent);
}
.hero-title { color: #e2e8f0; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); }
.hero-summary { color: #cbd5e1; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55); }
@@ -221,6 +226,14 @@
line-height: 1.78;
letter-spacing: 0.01em;
}
html[data-lang='ta'] .hero-title,
html[data-lang='ml'] .hero-title {
text-shadow: 0 3px 12px rgba(0, 0, 0, 0.75);
}
html[data-lang='ta'] .hero-summary,
html[data-lang='ml'] .hero-summary {
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
}
.share-icon-btn {
width: 34px;
height: 34px;
@@ -234,8 +247,42 @@
transition: background 180ms ease;
}
.share-icon-btn:hover { background: rgba(92, 124, 250, 0.25); }
html[data-theme='light'] .share-icon-btn {
color: #1d4ed8;
border-color: rgba(37, 99, 235, 0.45);
background: rgba(59, 130, 246, 0.12);
}
html[data-theme='light'] .share-icon-btn:hover {
background: rgba(59, 130, 246, 0.2);
}
.footer-link { text-decoration: underline; text-underline-offset: 2px; }
.footer-link:hover { color: #dbeafe; }
.site-footer {
background: color-mix(in srgb, var(--cf-bg) 92%, transparent);
backdrop-filter: blur(10px);
}
.back-to-top-island {
position: fixed;
right: 14px;
bottom: 88px;
z-index: 55;
border-radius: 9999px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(15, 23, 42, 0.88);
color: #dbeafe;
min-width: 44px;
min-height: 44px;
padding: 0 13px;
font-size: 12px;
font-weight: 700;
line-height: 1;
box-shadow: 0 12px 24px rgba(2, 6, 23, 0.35);
}
html[data-theme='light'] .back-to-top-island {
background: rgba(248, 250, 252, 0.96);
color: #1e3a8a;
border-color: rgba(37, 99, 235, 0.35);
}
.contact-hint {
position: fixed;
z-index: 60;
@@ -256,14 +303,31 @@
}
@media (max-width: 640px) {
.theme-btn { width: 26px; height: 26px; }
.cf-body { padding-bottom: 84px; }
html[data-lang='ta'] .hero-title,
html[data-lang='ml'] .hero-title {
font-size: 1.9rem;
line-height: 1.26;
letter-spacing: 0.01em;
}
html[data-lang='ta'] .hero-summary,
html[data-lang='ml'] .hero-summary {
font-size: 1.08rem;
line-height: 1.86;
letter-spacing: 0.015em;
}
.back-to-top-island {
right: 10px;
bottom: 82px;
}
}
</style>
</head>
<body class="font-sans min-h-screen overflow-x-hidden cf-body">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-cf-500 text-white px-3 py-2 rounded">Skip to content</a>
<header class="sticky top-0 z-50 backdrop-blur-lg border-b border-white/5 cf-header">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<header x-data="headerFx()" x-init="init()" :class="scrolled ? 'cf-header-scrolled' : 'cf-header-top'" class="sticky top-0 z-50 backdrop-blur-lg border-b border-white/5 cf-header transition-all duration-300">
<div :class="scrolled ? 'h-14' : 'h-16'" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between transition-all duration-300">
<a href="/" class="flex items-center gap-2.5 group">
<svg class="w-8 h-8 text-cf-500 group-hover:text-cf-400 transition-colors" viewBox="0 0 32 32" fill="none">
<path d="M16 2L4 8v8c0 7.7 5.1 14.9 12 16 6.9-1.1 12-8.3 12-16V8L16 2z" fill="currentColor" fill-opacity="0.15" stroke="currentColor" stroke-width="1.5"/>
@@ -342,9 +406,6 @@
x-show="item.source_url">
Via: <span x-text="extractDomain(item.source_url)" class="underline underline-offset-2"></span>
</a>
<a :href="articlePermalink(item)" class="hover:text-cf-300 transition-colors underline underline-offset-2">
Permalink
</a>
<span x-show="item.image_credit" x-text="'Image: ' + item.image_credit"></span>
</div>
</div>
@@ -409,10 +470,7 @@
@click.stop="trackEvent('source-link-click')"
x-show="item.source_url"
x-text="extractDomain(item.source_url)"></a>
<div class="flex items-center gap-2">
<a :href="articlePermalink(item)" class="hover:text-cf-300 underline underline-offset-2">Link</a>
<span x-text="timeAgo(item.published_at)"></span>
</div>
<span x-text="timeAgo(item.published_at)"></span>
</div>
<button @click="trackEvent('feed-cta-click', { article_id: item.id, article_title: item.headline }); openSummary(item)"
class="w-full text-center text-xs font-semibold rounded-md px-3 py-2 bg-cf-500/15 hover:bg-cf-500/25 transition-colors news-card-btn">
@@ -483,7 +541,13 @@
<a :href="shareLink('linkedin', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on LinkedIn" title="Share on LinkedIn">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M4.98 3.5a2.5 2.5 0 1 1 0 5.001A2.5 2.5 0 0 1 4.98 3.5zM3 9h4v12H3zM10 9h3.8v1.7h.1c.5-1 1.8-2.1 3.7-2.1 4 0 4.7 2.6 4.7 6V21h-4v-5.4c0-1.3 0-2.9-1.8-2.9s-2.1 1.4-2.1 2.8V21h-4V9z"/></svg>
</a>
<button type="button" class="share-icon-btn" aria-label="Copy article link" title="Copy article link"
@click="copyShareLink(modalItem)">
<svg x-show="copyStatus !== 'copied'" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M16 1H6a2 2 0 0 0-2 2v12h2V3h10V1zm3 4H10a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H10V7h9v14z"/></svg>
<svg x-show="copyStatus === 'copied'" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"/></svg>
</button>
</div>
<p class="text-xs text-gray-400 mt-2" x-show="copyStatus === 'copied'">Permalink copied.</p>
</div>
<p class="text-xs modal-powered">Powered by Perplexity</p>
@@ -508,16 +572,54 @@
<div class="fixed left-0 right-0 pointer-events-none" style="top:75%" x-data x-intersect:enter="trackDepth(75)"></div>
</main>
<footer x-data="footerEnhancements()" x-init="init()" class="border-t border-white/5 py-8 text-center text-sm text-gray-500">
<div class="max-w-7xl mx-auto px-4 space-y-2">
<section x-data="policyDisclosures()" x-init="init()">
<div x-show="openType" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center" @keydown.window="onKeydown($event)">
<div class="absolute inset-0 bg-black/70" @click="close()"></div>
<div role="dialog" aria-modal="true" :aria-label="openType === 'terms' ? 'Terms of Use' : 'Attribution'"
class="relative w-full sm:w-[92vw] lg:w-[70vw] xl:w-[60vw] 2xl:w-[50vw] max-w-[1200px] mx-4 max-h-[96vh] overflow-auto rounded-xl border border-white/10 bg-[#0f172a]">
<div class="p-6 space-y-5 cf-modal">
<div class="flex justify-end">
<button type="button" @click="close()" aria-label="Close policy modal" class="transition-colors modal-close-btn">Close</button>
</div>
<template x-if="openType === 'terms'">
<div>
<h2 class="text-xl sm:text-2xl font-bold leading-tight modal-article-title">Terms of Use</h2>
<div class="space-y-3 text-sm leading-relaxed modal-body-text mt-3">
<p>The information shown on this site is provided for general informational use only. It is not independently verified by the site owner.</p>
<p>Any use of this content is entirely at your own risk. You are responsible for validating facts, citations, and suitability before relying on or redistributing the information.</p>
<p>Content may originate from external and AI-generated sources and may include errors, omissions, or outdated material.</p>
</div>
</div>
</template>
<template x-if="openType === 'attribution'">
<div>
<h2 class="text-xl sm:text-2xl font-bold leading-tight modal-article-title">Attribution and Ownership Disclaimer</h2>
<div class="space-y-3 text-sm leading-relaxed modal-body-text mt-3">
<p>None of the content presented on this site is created by the owner as an individual person.</p>
<p>The content is AI-generated and automatically assembled from external sources and generated summaries.</p>
<p>The owner does not claim personal authorship, editorial ownership, or direct responsibility for generated statements beyond operating the site interface.</p>
</div>
</div>
</template>
</div>
</div>
</div>
</section>
<button x-data="backToTopIsland()" x-init="init()" x-show="visible" x-cloak @click="scrollTop()"
class="back-to-top-island" aria-label="Back to top" title="Back to top">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M12 5 5 12h4v7h6v-7h4z"/></svg>
</button>
<footer x-data="footerEnhancements()" x-init="init()" class="site-footer sticky bottom-0 z-40 border-t border-white/10 py-3 text-center text-xs text-gray-500">
<div class="max-w-7xl mx-auto px-4 space-y-1.5">
<p>Powered by <a href="https://www.perplexity.ai" target="_blank" rel="noopener" class="text-cf-400 hover:text-cf-300 transition-colors">Perplexity</a></p>
<p class="space-x-3">
<a href="/terms" class="footer-link">Terms of Use</a>
<a href="/attribution" class="footer-link">Attribution</a>
<button type="button" class="footer-link" @click="scrollTop()">Back to Top</button>
<button type="button" class="footer-link" @click="window.dispatchEvent(new CustomEvent('open-policy-modal', { detail: { type: 'terms' } }))">Terms of Use</button>
<button type="button" class="footer-link" @click="window.dispatchEvent(new CustomEvent('open-policy-modal', { detail: { type: 'attribution' } }))">Attribution</button>
<a x-show="githubUrl" :href="githubUrl" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<a x-show="contactEmail" :href="'mailto:' + contactEmail" class="footer-link"
@mouseenter="showHint($event)" @mousemove="moveHint($event)" @mouseleave="hideHint()">Email me</a>
@mouseenter="showHint($event)" @mousemove="moveHint($event)" @mouseleave="hideHint()" x-text="contactEmail"></a>
</p>
<p>&copy; <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p>
</div>
@@ -586,6 +688,11 @@ function getPermalinkArticleId() {
return null;
}
function hasPermalinkArticleParam() {
const params = new URLSearchParams(window.location.search);
return params.has('article');
}
function setPermalinkArticleId(articleId) {
if (!articleId) return;
const url = new URL(window.location.href);
@@ -601,6 +708,28 @@ function clearPermalinkArticleId() {
window.history.replaceState({}, '', url.toString());
}
function getPolicyModalType() {
const params = new URLSearchParams(window.location.search);
const modal = (params.get('policy') || '').toLowerCase();
if (modal === 'terms' || modal === 'attribution') return modal;
return '';
}
function setPolicyModalType(type) {
if (!type) return;
const url = new URL(window.location.href);
url.searchParams.set('policy', type);
window.history.replaceState({}, '', url.toString());
}
function clearPolicyModalType() {
const url = new URL(window.location.href);
if (url.searchParams.has('policy')) {
url.searchParams.delete('policy');
window.history.replaceState({}, '', url.toString());
}
}
function shareLink(provider, item) {
const permalink = articlePermalink(item);
const encodedUrl = encodeURIComponent(permalink);
@@ -937,7 +1066,7 @@ function footerEnhancements() {
if (!resp.ok) return;
const cfg = await resp.json();
this.githubUrl = cfg.github_repo_url || '';
this.contactEmail = cfg.contact_email || '';
this.contactEmail = (cfg.contact_email || '').trim();
} catch {}
},
scrollTop() {
@@ -962,6 +1091,89 @@ function footerEnhancements() {
};
}
function headerFx() {
return {
scrolled: false,
init() {
const onScroll = () => {
this.scrolled = window.scrollY > 18;
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
},
};
}
function backToTopIsland() {
return {
visible: false,
init() {
const onScroll = () => {
this.visible = window.scrollY > Math.max(380, window.innerHeight * 0.6);
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
},
scrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
},
};
}
function policyDisclosures() {
return {
openType: '',
lastActiveElement: null,
init() {
const fromUrl = getPolicyModalType();
if (fromUrl) this.open(fromUrl);
window.addEventListener('open-policy-modal', (event) => {
const modalType = event?.detail?.type;
if (modalType === 'terms' || modalType === 'attribution') {
this.open(modalType);
}
});
},
open(type) {
this.lastActiveElement = document.activeElement;
this.openType = type;
setPolicyModalType(type);
setTimeout(() => {
const closeButton = document.querySelector('[aria-label="Close policy modal"]');
if (closeButton instanceof HTMLElement) closeButton.focus();
}, 0);
},
close() {
this.openType = '';
clearPolicyModalType();
if (this.lastActiveElement instanceof HTMLElement) {
this.lastActiveElement.focus();
}
},
onKeydown(event) {
if (!this.openType) return;
if (event.key === 'Escape') {
this.close();
return;
}
if (event.key !== 'Tab') return;
const focusable = Array.from(document.querySelectorAll('button, a, [tabindex]:not([tabindex="-1"])')).filter(
(el) => el instanceof HTMLElement && !el.hasAttribute('disabled') && el.offsetParent !== null,
);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
},
};
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const choice = getPreferredTheme();
if (choice === 'system') {
@@ -1031,6 +1243,7 @@ function newsFeed() {
modalItem: null,
modalImageLoading: true,
modalTldrLoading: true,
copyStatus: '',
imageLoaded: {},
async init() {
@@ -1056,6 +1269,11 @@ function newsFeed() {
if (!event?.detail) return;
this.openSummary(event.detail);
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && this.modalOpen) {
this.closeSummary();
}
});
},
waitForHero() {
@@ -1125,7 +1343,10 @@ function newsFeed() {
async openFromPermalink() {
const articleId = getPermalinkArticleId();
if (!articleId) return;
if (!articleId) {
if (hasPermalinkArticleParam()) clearPermalinkArticleId();
return;
}
if (window.__heroNewsItem?.id === articleId) {
this.openSummary(window.__heroNewsItem);
@@ -1140,14 +1361,26 @@ function newsFeed() {
attempts += 1;
}
if (item) this.openSummary(item);
if (item) {
this.openSummary(item);
return;
}
clearPermalinkArticleId();
},
openSummary(item) {
this.modalItem = item;
const normalized = {
...(item || {}),
summary_image_url: item?.summary_image_url || item?.image_url || '/static/images/placeholder.png',
tldr_points: Array.isArray(item?.tldr_points)
? item.tldr_points
: toBulletPoints(item?.summary_body || item?.summary || ''),
};
this.modalItem = normalized;
this.modalOpen = true;
this.modalImageLoading = true;
this.modalTldrLoading = true;
this.copyStatus = '';
setPermalinkArticleId(item?.id);
setTimeout(() => {
if (this.modalOpen) this.modalTldrLoading = false;
@@ -1164,6 +1397,7 @@ function newsFeed() {
this.modalOpen = false;
this.modalItem = null;
this.modalTldrLoading = true;
this.copyStatus = '';
clearPermalinkArticleId();
trackEvent('summary-modal-close', {
article_id: id,
@@ -1177,6 +1411,36 @@ function newsFeed() {
article_id: item.id,
source_url: item.source_url,
});
},
async copyShareLink(item) {
if (!item?.id) return;
const permalink = articlePermalink(item);
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(permalink);
} else {
const temp = document.createElement('textarea');
temp.value = permalink;
temp.setAttribute('readonly', '');
temp.style.position = 'absolute';
temp.style.left = '-9999px';
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
document.body.removeChild(temp);
}
this.copyStatus = 'copied';
trackEvent('summary-modal-copy-link', {
article_id: item.id,
article_title: item.headline || null,
});
setTimeout(() => {
if (this.copyStatus === 'copied') this.copyStatus = '';
}, 1400);
} catch {
this.copyStatus = '';
}
}
};
}