First deployment
This commit is contained in:
@@ -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>© <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 = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user