usability enhancements

This commit is contained in:
2026-02-13 03:12:42 -05:00
parent bf4a40f533
commit 0e21e035f5
28 changed files with 904 additions and 15 deletions

View File

@@ -15,6 +15,7 @@
<meta name="twitter:description" content="Understand content attribution and ownership boundaries for ClawFort AI-generated summaries.">
<meta name="twitter:image" content="/static/images/placeholder.png">
<link rel="canonical" href="/attribution">
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
<title>Attribution and Ownership Disclaimer - ClawFort</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@@ -20,10 +20,11 @@
<link rel="canonical" href="/" id="canonical-link">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<title>ClawFort AI News</title>
<title>ClawFort AI News</title>
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Noto+Sans+Tamil:wght@400;500;600;700&family=Noto+Sans+Malayalam:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
@@ -36,7 +37,9 @@
700: '#4263eb', 900: '#1e2a5e', 950: '#0f172a'
}
},
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
fontFamily: {
sans: ['Inter', 'Noto Sans Tamil', 'Noto Sans Malayalam', 'system-ui', 'sans-serif']
}
}
}
}
@@ -208,6 +211,45 @@
border-color: #5c7cfa;
box-shadow: 0 0 0 1px #5c7cfa inset;
}
html[data-lang='ta'] .news-card-summary,
html[data-lang='ml'] .news-card-summary,
html[data-lang='ta'] .hero-summary,
html[data-lang='ml'] .hero-summary,
html[data-lang='ta'] .modal-body-text,
html[data-lang='ml'] .modal-body-text {
font-size: 1.02rem;
line-height: 1.78;
letter-spacing: 0.01em;
}
.share-icon-btn {
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(92, 124, 250, 0.14);
color: #dbeafe;
transition: background 180ms ease;
}
.share-icon-btn:hover { background: rgba(92, 124, 250, 0.25); }
.footer-link { text-decoration: underline; text-underline-offset: 2px; }
.footer-link:hover { color: #dbeafe; }
.contact-hint {
position: fixed;
z-index: 60;
max-width: 280px;
background: rgba(15, 23, 42, 0.96);
color: #e2e8f0;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 10px;
padding: 8px 10px;
font-size: 12px;
line-height: 1.5;
pointer-events: none;
box-shadow: 0 8px 22px rgba(2, 6, 23, 0.45);
}
*:focus-visible {
outline: 2px solid #5c7cfa;
outline-offset: 2px;
@@ -291,7 +333,7 @@
<p class="text-base sm:text-lg max-w-3xl line-clamp-3 mb-4 hero-summary" x-text="item.summary"></p>
<div class="flex flex-wrap items-center gap-4 text-sm hero-meta">
<button class="px-3 py-1.5 rounded-md bg-cf-500/20 text-cf-300 hover:bg-cf-500/30 transition-colors"
@click="trackEvent('hero-cta-click', { article_id: item.id }); window.dispatchEvent(new CustomEvent('open-summary', { detail: item }))">
@click="trackEvent('hero-cta-click', { article_id: item.id, article_title: item.headline }); window.dispatchEvent(new CustomEvent('open-summary', { detail: item }))">
Read TL;DR
</button>
<a :href="item.source_url" target="_blank" rel="noopener"
@@ -300,6 +342,9 @@
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>
@@ -364,9 +409,12 @@
@click.stop="trackEvent('source-link-click')"
x-show="item.source_url"
x-text="extractDomain(item.source_url)"></a>
<span x-text="timeAgo(item.published_at)"></span>
<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>
</div>
<button @click="openSummary(item)"
<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">
Read TL;DR
</button>
@@ -423,6 +471,21 @@
</a>
</div>
<div>
<h3 class="text-sm uppercase tracking-wide font-semibold mb-2 modal-section-title">Share</h3>
<div class="flex items-center gap-2">
<a :href="shareLink('x', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on X" title="Share on X">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M18.9 2H22l-6.8 7.8L23 22h-6.2l-4.9-6.4L6.2 22H3l7.3-8.4L1 2h6.3l4.4 5.8L18.9 2z"/></svg>
</a>
<a :href="shareLink('whatsapp', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on WhatsApp" title="Share on WhatsApp">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M20.5 3.5A11.8 11.8 0 0 0 12.1 0C5.5 0 .1 5.3.1 11.9c0 2.1.5 4.1 1.6 5.9L0 24l6.4-1.7a12 12 0 0 0 5.7 1.4h.1c6.6 0 11.9-5.3 11.9-11.9 0-3.2-1.3-6.2-3.6-8.3zM12.2 21.7h-.1a9.9 9.9 0 0 1-5-1.4l-.4-.2-3.8 1 1-3.7-.2-.4a9.8 9.8 0 0 1-1.5-5.2c0-5.4 4.4-9.8 9.9-9.8 2.6 0 5.1 1 6.9 2.9a9.7 9.7 0 0 1 2.8 6.9c0 5.4-4.4 9.9-9.8 9.9zm5.4-7.3c-.3-.1-1.8-.9-2.1-1-.3-.1-.5-.1-.7.2-.2.3-.8 1-1 1.2-.2.2-.3.2-.6.1-1.6-.8-2.6-1.5-3.7-3.3-.3-.5.3-.5 1-1.8.1-.2.1-.4 0-.6l-1-2.4c-.2-.6-.5-.5-.7-.5h-.6c-.2 0-.6.1-.9.4-.3.3-1.2 1.1-1.2 2.8 0 1.6 1.2 3.2 1.4 3.4.2.2 2.3 3.6 5.6 5 .8.3 1.4.5 1.9.7.8.3 1.5.2 2 .1.6-.1 1.8-.8 2-1.5.2-.7.2-1.4.1-1.5-.1-.1-.3-.2-.6-.3z"/></svg>
</a>
<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>
</div>
</div>
<p class="text-xs modal-powered">Powered by Perplexity</p>
</div>
</div>
@@ -445,15 +508,20 @@
<div class="fixed left-0 right-0 pointer-events-none" style="top:75%" x-data x-intersect:enter="trackDepth(75)"></div>
</main>
<footer class="border-t border-white/5 py-8 text-center text-sm text-gray-500">
<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">
<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="underline hover:text-gray-300">Terms of Use</a>
<a href="/attribution" class="underline hover:text-gray-300">Attribution</a>
<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>
<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>
</p>
<p>&copy; <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p>
</div>
<div x-show="hintVisible" x-cloak class="contact-hint" :style="`left:${hintX}px; top:${hintY}px`" x-text="hintText"></div>
</footer>
<div id="cookie-consent-banner" class="hidden fixed bottom-4 left-1/2 -translate-x-1/2 w-[95%] max-w-3xl z-50 rounded-lg border border-white/15 bg-slate-900/95 backdrop-blur p-4 transition-opacity duration-700 opacity-100">
@@ -506,7 +574,47 @@ function toAbsoluteUrl(url) {
function articlePermalink(item) {
if (!item?.id) return toAbsoluteUrl('/');
return toAbsoluteUrl(`/#news-${item.id}`);
return toAbsoluteUrl(`/?article=${item.id}`);
}
function getPermalinkArticleId() {
const params = new URLSearchParams(window.location.search);
const article = params.get('article');
if (article && /^\d+$/.test(article)) return Number(article);
const match = (window.location.hash || '').match(/^#news-(\d+)$/);
if (match) return Number(match[1]);
return null;
}
function setPermalinkArticleId(articleId) {
if (!articleId) return;
const url = new URL(window.location.href);
url.searchParams.set('article', String(articleId));
url.hash = '';
window.history.replaceState({}, '', url.toString());
}
function clearPermalinkArticleId() {
const url = new URL(window.location.href);
url.searchParams.delete('article');
if ((url.hash || '').startsWith('#news-')) url.hash = '';
window.history.replaceState({}, '', url.toString());
}
function shareLink(provider, item) {
const permalink = articlePermalink(item);
const encodedUrl = encodeURIComponent(permalink);
const encodedTitle = encodeURIComponent(item?.headline || 'ClawFort AI News');
if (provider === 'x') {
return `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
}
if (provider === 'whatsapp') {
return `https://wa.me/?text=${encodedTitle}%20${encodedUrl}`;
}
if (provider === 'linkedin') {
return `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
}
return permalink;
}
function setMetaContent(selector, content, attribute = 'content') {
@@ -519,13 +627,13 @@ function syncSeoMeta(item) {
const canonical = toAbsoluteUrl(window.location.pathname || '/');
setMetaContent('#canonical-link', canonical, 'href');
setMetaContent('#meta-og-url', canonical);
document.title = 'ClawFort AI News';
if (!item) return;
const title = `${item.headline} | ClawFort`;
const title = 'ClawFort AI News';
const description = item.summary || 'Latest AI news updates from ClawFort.';
const image = toAbsoluteUrl(preferredImage(item));
document.title = title;
setMetaContent('meta[name="description"]', description);
setMetaContent('meta[property="og:title"]', title);
setMetaContent('meta[property="og:description"]', description);
@@ -741,6 +849,7 @@ function getPreferredLanguage() {
function setPreferredLanguage(language) {
const normalized = normalizeLanguage(language);
window._selectedLanguage = normalized;
document.documentElement.setAttribute('data-lang', normalized);
safeSetStorage('clawfort_language', normalized);
setCookie('clawfort_language', normalized);
const select = document.getElementById('language-select');
@@ -756,10 +865,103 @@ window._selectedLanguage = getPreferredLanguage();
window._themeChoice = getPreferredTheme();
window.__heroNewsItem = null;
window.__feedNewsItems = [];
document.documentElement.setAttribute('data-lang', window._selectedLanguage);
applyTheme(window._themeChoice);
syncSeoMeta(null);
syncStructuredData();
const CONTACT_HINTS = [
'If you have feedback, I would love to hear it.',
'Found a bug? Send a note and I will take a look.',
'Have an idea for ClawFort? Reach out anytime.',
'Suggestions are always welcome here.',
'Want a feature? Let me know by email.',
'Your feedback helps improve this project.',
'Questions, suggestions, or praise - all welcome.',
'Noticed something odd? Please report it.',
'Tell me what would make this site better.',
'Happy to hear your thoughts on UX and content.',
'If something feels off, I am listening.',
'Feature requests are open - send yours.',
'If you care about this project, drop a line.',
'Spotted a typo? I appreciate quick heads-up notes.',
'Your perspective can shape the next update.',
'If this helped you, tell me what to improve next.',
'Ideas are fuel. Share one when you can.',
'Small feedback can create big improvements.',
'If a page confused you, let me know why.',
'Product feedback is always in scope.',
'Have performance concerns? I want to hear them.',
'Your suggestions are part of the roadmap.',
'See something broken? I can fix it faster with details.',
'If a feature is missing, say the word.',
'Tell me how ClawFort could serve you better.',
'Think the UI can be clearer? Share your take.',
'Want better readability options? Send feedback.',
'If translation quality feels off, please report it.',
'Any accessibility concern is worth sharing.',
'If a link fails, I would appreciate the report.',
'Help make this news feed sharper with feedback.',
'Your comments help prioritize updates.',
'Share your favorite improvement idea.',
'If you have a workflow pain point, mention it.',
'Even one sentence of feedback helps.',
'Tell me which feature you use most.',
'If sharing feels clunky, let me know.',
'Want custom views? I am open to suggestions.',
'If something is hard to discover, please say so.',
'I appreciate concrete suggestions and examples.',
'If the mobile layout needs work, send details.',
'A quick email can spark the next fix.',
'Thanks for helping improve this project.',
'If this tool saves you time, tell me what else you need.',
'I welcome constructive feedback.',
'Have a better idea? I am curious.',
'If you found a regression, please report it.',
'Your use case matters - share it.',
'If something should be simpler, I want to know.',
'Feedback is always appreciated. Thank you.',
];
function footerEnhancements() {
return {
githubUrl: '',
contactEmail: '',
hintVisible: false,
hintText: '',
hintX: 16,
hintY: 16,
async init() {
try {
const resp = await fetch('/config');
if (!resp.ok) return;
const cfg = await resp.json();
this.githubUrl = cfg.github_repo_url || '';
this.contactEmail = cfg.contact_email || '';
} catch {}
},
scrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
},
randomHint() {
return CONTACT_HINTS[Math.floor(Math.random() * CONTACT_HINTS.length)];
},
showHint(event) {
this.hintText = this.randomHint();
this.hintVisible = true;
this.moveHint(event);
},
moveHint(event) {
if (!event) return;
this.hintX = Math.min(event.clientX + 16, window.innerWidth - 290);
this.hintY = Math.min(event.clientY + 18, window.innerHeight - 80);
},
hideHint() {
this.hintVisible = false;
},
};
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const choice = getPreferredTheme();
if (choice === 'system') {
@@ -834,6 +1036,7 @@ function newsFeed() {
async init() {
await this.waitForHero();
await this.loadMore();
await this.openFromPermalink();
this.initialLoading = false;
this.setupObserver();
window.addEventListener('language-changed', async () => {
@@ -845,6 +1048,7 @@ function newsFeed() {
window.__feedNewsItems = [];
await this.waitForHero();
await this.loadMore();
await this.openFromPermalink();
this.initialLoading = false;
});
@@ -919,23 +1123,52 @@ function newsFeed() {
this.observer.observe(this.$refs.sentinel);
},
async openFromPermalink() {
const articleId = getPermalinkArticleId();
if (!articleId) return;
if (window.__heroNewsItem?.id === articleId) {
this.openSummary(window.__heroNewsItem);
return;
}
let item = this.items.find(i => i.id === articleId);
let attempts = 0;
while (!item && this.hasMore && attempts < 5) {
await this.loadMore();
item = this.items.find(i => i.id === articleId);
attempts += 1;
}
if (item) this.openSummary(item);
},
openSummary(item) {
this.modalItem = item;
this.modalOpen = true;
this.modalImageLoading = true;
this.modalTldrLoading = true;
setPermalinkArticleId(item?.id);
setTimeout(() => {
if (this.modalOpen) this.modalTldrLoading = false;
}, 250);
trackEvent('summary-modal-open', { article_id: item.id });
trackEvent('summary-modal-open', {
article_id: item.id,
article_title: item.headline || null,
});
},
closeSummary() {
const id = this.modalItem ? this.modalItem.id : null;
const title = this.modalItem ? this.modalItem.headline : null;
this.modalOpen = false;
this.modalItem = null;
this.modalTldrLoading = true;
trackEvent('summary-modal-close', { article_id: id });
clearPermalinkArticleId();
trackEvent('summary-modal-close', {
article_id: id,
article_title: title,
});
},
trackSummarySource(item) {

View File

@@ -15,6 +15,7 @@
<meta name="twitter:description" content="Read ClawFort terms governing informational use of AI-generated and aggregated content.">
<meta name="twitter:image" content="/static/images/placeholder.png">
<link rel="canonical" href="/terms">
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
<title>Terms of Use - ClawFort</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>