Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1619,3 +1619,92 @@ mark {
.theme-btn { width: 26px; height: 26px; }
.theme-btn svg { width: 14px; height: 14px; }
}


/* ==========================================================================
IMPACTMOJO BRAND UNIFICATION LAYER
Aligns Development Discourses with the shared library design system
(deep blue + teal). Loaded last so it wins. Light + dark defined
explicitly so the dark accent still overrides in dark mode.
========================================================================== */

:root {
--color-accent: #235789;
--color-accent-hover: #16416b;
--color-accent-light: #e9f1f9;
--color-accent-2: #2a9d8f;
--brand-grad: linear-gradient(135deg, #235789 0%, #2a7ab0 55%, #2a9d8f 130%);
--brand-ring: 0 0 0 3px rgba(35,87,137,0.18);
}
[data-theme="dark"] {
--color-accent: #6cb2ff;
--color-accent-hover: #93c6ff;
--color-accent-light: #16283c;
--color-accent-2: #4fd1c5;
--brand-grad: linear-gradient(135deg, #1b3b5c 0%, #23557f 55%, #1f5e57 130%);
--brand-ring: 0 0 0 3px rgba(108,178,255,0.22);
}

/* Brand mark → gradient chip (matches Dev Case Studies logo) */
.brand-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 26px; height: 26px; border-radius: 8px;
background: var(--brand-grad); color: #fff !important;
font-size: 1rem; box-shadow: var(--shadow-sm);
}

/* Ecosystem cross-links in the header nav */
.eco-nav { display: inline-flex; align-items: center; gap: 14px; }
.eco-link {
font-size: 0.85rem; font-weight: 600; color: var(--color-text-secondary);
border-bottom: 2px solid transparent; padding-bottom: 2px; white-space: nowrap;
}
.eco-link:hover { color: var(--color-accent); text-decoration: none; }
.eco-link.current { color: var(--color-accent); border-bottom-color: var(--color-accent); }

/* Stat numbers pick up the brand */
.stat-number { color: var(--color-accent); }

/* Cards: subtle lift + gradient accent strip on hover */
.resource-card { position: relative; transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; }
.resource-card::before {
content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
background: var(--brand-grad); opacity: 0;
border-radius: var(--radius-lg) var(--radius-lg) 0 0; transition: opacity 0.18s ease;
}
.resource-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.resource-card:hover::before { opacity: 1; }

/* Bookmark button on cards */
.rc-bookmark {
position: absolute; top: 10px; right: 10px; z-index: 2;
width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
background: color-mix(in srgb, var(--color-surface) 82%, transparent);
border: 1px solid var(--color-border-light); border-radius: 8px;
color: var(--color-text-muted); cursor: pointer;
opacity: 0; transform: translateY(-2px); transition: all 0.15s;
}
.resource-card:hover .rc-bookmark,
.rc-bookmark.saved,
.rc-bookmark:focus-visible { opacity: 1; transform: translateY(0); }
.rc-bookmark:hover { color: var(--color-accent); border-color: var(--color-accent); }
.rc-bookmark.saved { color: var(--color-accent); border-color: var(--color-accent); background: var(--color-accent-light); }
.rc-bookmark svg { width: 15px; height: 15px; }

/* Saved toggle in the results tools */
.tool-btn.active { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
.tool-btn .saved-count {
display: none; margin-left: 6px; font-size: 0.66rem; font-weight: 700; line-height: 15px;
min-width: 15px; text-align: center; padding: 0 4px; border-radius: 8px;
background: var(--color-accent); color: #fff;
}
.tool-btn.active .saved-count { background: rgba(255,255,255,0.25); }
.tool-btn .saved-count.visible { display: inline-block; }

/* Footer: brand gradient top edge (matches Dev Case Studies) */
.site-footer { border-top: 3px solid transparent; border-image: var(--brand-grad) 1; }

/* Focus rings on the brand */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 4px;
}
2 changes: 1 addition & 1 deletion icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<!-- PWA -->
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#2563eb">
<meta name="theme-color" content="#235789">
<link rel="icon" href="icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icon.svg">
<link rel="search" type="application/opensearchdescription+xml" title="Dev Discourses" href="opensearch.xml">
Expand Down Expand Up @@ -71,6 +71,11 @@ <h1 class="site-title">Development Discourses</h1>
<p class="site-tagline">A curated open-access library for development practitioners in South Asia</p>
</div>
<nav class="header-nav">
<span class="eco-nav" aria-label="ImpactMojo libraries">
<a href="https://varnasr.github.io/dev-case-studies/" class="eco-link">Case Studies</a>
<a href="https://varnasr.github.io/development-discourses/" class="eco-link current" aria-current="page">Discourses</a>
<a href="https://www.impactmojo.in/impactlex/" class="eco-link" target="_blank" rel="noopener">ImpactLex</a>
</span>
<a href="#about" class="nav-link">About</a>
<a href="https://www.impactmojo.in" class="nav-link" target="_blank" rel="noopener">ImpactMojo &nearr;</a>
<div class="theme-selector" role="group" aria-label="Theme selection">
Expand Down Expand Up @@ -195,6 +200,11 @@ <h1 class="site-title">Development Discourses</h1>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 3h5v5"></path><path d="M4 20 21 3"></path><path d="M21 16v5h-5"></path><path d="m15 15 6 6"></path><path d="M4 4l5 5"></path></svg>
<span>Surprise me</span>
</button>
<button class="tool-btn" id="savedBtn" aria-pressed="false" title="Show only saved resources">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
<span>Saved</span>
<span class="saved-count"></span>
</button>
<div class="export-menu">
<button class="tool-btn" id="exportBtn" title="Export the filtered list" aria-haspopup="true" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Expand Down
58 changes: 58 additions & 0 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,35 @@
let currentSearch = '';
let currentView = 'list';
let displayCount = 50;
let savedOnly = false;
const PAGE_SIZE = 50;
const RECENT_KEY = 'imx_recent';
const BOOKMARK_KEY = 'imx_bookmarks';

// ---- Bookmarks (saved reading list) ----
function resKey(r) { return String(r.id || r.url || r.title || ''); }
function getBookmarks() {
try { return JSON.parse(localStorage.getItem(BOOKMARK_KEY)) || []; } catch (e) { return []; }
}
function isBookmarked(key) { return getBookmarks().indexOf(key) !== -1; }
function toggleBookmark(key) {
const m = getBookmarks(); const i = m.indexOf(key);
if (i === -1) m.push(key); else m.splice(i, 1);
try { localStorage.setItem(BOOKMARK_KEY, JSON.stringify(m)); } catch (e) { /* ignore */ }
return i === -1;
}
function bookmarkSvg(filled) {
return '<svg viewBox="0 0 24 24" fill="' + (filled ? 'currentColor' : 'none') +
'" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>';
}
function updateSavedCount() {
const el = document.querySelector('#savedBtn .saved-count');
if (!el) return;
const n = getBookmarks().length;
el.textContent = n;
el.classList.toggle('visible', n > 0);
}

// DOM refs
const searchInput = document.getElementById('searchInput');
Expand Down Expand Up @@ -252,6 +279,33 @@
window.location.href = r.id ? 'resource.html?id=' + encodeURIComponent(r.id) : r.url;
});

// Bookmark toggle on cards (card is a link, so stop navigation)
resourcesList.addEventListener('click', function (e) {
const btn = e.target.closest('.rc-bookmark');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const key = decodeURIComponent(btn.dataset.key || '');
const nowSaved = toggleBookmark(key);
btn.classList.toggle('saved', nowSaved);
btn.setAttribute('aria-pressed', String(nowSaved));
btn.setAttribute('title', nowSaved ? 'Saved — click to remove' : 'Save for later');
btn.innerHTML = bookmarkSvg(nowSaved);
updateSavedCount();
if (savedOnly) { displayCount = PAGE_SIZE; applyFilters(); }
});

// Saved-only toggle
const savedBtn = document.getElementById('savedBtn');
if (savedBtn) savedBtn.addEventListener('click', function () {
savedOnly = !savedOnly;
savedBtn.classList.toggle('active', savedOnly);
savedBtn.setAttribute('aria-pressed', String(savedOnly));
displayCount = PAGE_SIZE;
applyFilters();
});
updateSavedCount();

// Export
if (exportBtn) {
exportBtn.addEventListener('click', function (e) {
Expand Down Expand Up @@ -327,6 +381,7 @@
const terms = currentSearch ? currentSearch.split(/\s+/).filter(Boolean) : [];

filteredResources = allResources.filter(r => {
if (savedOnly && !isBookmarked(resKey(r))) return false;
if (currentTopic !== 'all' && r.topic !== currentTopic) return false;
if (currentType !== 'all' && r.type !== currentType) return false;
if (currentAccess !== 'all' && (r.access_type || 'check_access') !== currentAccess) return false;
Expand Down Expand Up @@ -448,8 +503,11 @@
const typeLabel = r.type === 'grey_literature' ? 'Grey Lit' : capitalize(r.type);
const accessClass = r.access_type || 'check_access';
const accessLabel = accessLabelOf(accessClass);
const bkey = resKey(r);
const bsaved = isBookmarked(bkey);

card.innerHTML = `
<button class="rc-bookmark${bsaved ? ' saved' : ''}" data-key="${encodeURIComponent(bkey)}" aria-pressed="${bsaved}" title="${bsaved ? 'Saved — click to remove' : 'Save for later'}" aria-label="Save resource">${bookmarkSvg(bsaved)}</button>
<div class="resource-card-header">
<span class="resource-type-badge ${typeClass}">${typeLabel}</span>
<span class="resource-title">${highlight(r.title, terms)}</span>
Expand Down
2 changes: 1 addition & 1 deletion manifest.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scope": "./",
"display": "standalone",
"background_color": "#faf9f7",
"theme_color": "#2563eb",
"theme_color": "#235789",
"lang": "en",
"categories": ["education", "books", "reference"],
"icons": [
Expand Down
Loading