This commit is contained in:
@@ -63,6 +63,10 @@ making changes; it is the authoritative operations and deployment runbook.
|
|||||||
mustard labels and are excluded from discovery.
|
mustard labels and are excluded from discovery.
|
||||||
- Keep project devlog entries in the homepage Recent Activity feed alongside
|
- Keep project devlog entries in the homepage Recent Activity feed alongside
|
||||||
projects, blog entries, and articles, ordered by publication date.
|
projects, blog entries, and articles, ordered by publication date.
|
||||||
|
- Keep Recent Activity batching as a progressive enhancement: render the full
|
||||||
|
semantic feed, show six items initially with JavaScript, reveal six more per
|
||||||
|
activation, announce the visible count, and leave all items available when
|
||||||
|
JavaScript is disabled.
|
||||||
- When `README.md` changes, keep the README-derived example blog entry aligned
|
- When `README.md` changes, keep the README-derived example blog entry aligned
|
||||||
with the runbook content it demonstrates.
|
with the runbook content it demonstrates.
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ The public routes are `/projects/thinkloom/`,
|
|||||||
native Lektor records and approved images. This site owns their models,
|
native Lektor records and approved images. This site owns their models,
|
||||||
templates, layout, repository-information card, and import policy. Imported
|
templates, layout, repository-information card, and import policy. Imported
|
||||||
devlog entries also join the homepage Recent Activity feed, ordered with
|
devlog entries also join the homepage Recent Activity feed, ordered with
|
||||||
projects, articles, and blog entries by publication date.
|
projects, articles, and blog entries by publication date. The feed initially
|
||||||
|
shows six items and reveals up to six more per button activation. This is a
|
||||||
|
progressive enhancement implemented by `assets/static/activity.js`; without
|
||||||
|
JavaScript, the complete semantic feed remains visible.
|
||||||
|
|
||||||
`configs/project-sources.ini` is the allowlist. Each source declares a stable
|
`configs/project-sources.ini` is the allowlist. Each source declares a stable
|
||||||
project ID, credential-free HTTPS Git URL, public web/API URL, and branch. Add
|
project ID, credential-free HTTPS Git URL, public web/API URL, and branch. Add
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
(() => {
|
||||||
|
for (const feed of document.querySelectorAll("[data-activity-feed]")) {
|
||||||
|
const items = [...feed.querySelectorAll("[data-activity-item]")];
|
||||||
|
const batchSize = Number.parseInt(feed.dataset.batchSize || "6", 10);
|
||||||
|
const controls = feed.parentElement.querySelector("[data-activity-controls]");
|
||||||
|
const status = controls?.querySelector("[data-activity-status]");
|
||||||
|
const button = controls?.querySelector("[data-activity-more]");
|
||||||
|
|
||||||
|
if (!controls || !status || !button || items.length <= batchSize) continue;
|
||||||
|
|
||||||
|
let visibleCount = Math.min(batchSize, items.length);
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
for (const [index, item] of items.entries()) {
|
||||||
|
item.hidden = index >= visibleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = items.length - visibleCount;
|
||||||
|
status.textContent = `Showing ${visibleCount} of ${items.length} recent activity items.`;
|
||||||
|
button.disabled = remaining === 0;
|
||||||
|
button.textContent = remaining
|
||||||
|
? `Show ${Math.min(batchSize, remaining)} more`
|
||||||
|
: "All activity shown";
|
||||||
|
controls.hidden = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
visibleCount = Math.min(visibleCount + batchSize, items.length);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -57,6 +57,23 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
|||||||
.timeline-entry h2, .entry-card h2 { margin: 10px 0 5px; font-size: 21px; line-height: 1.3; }
|
.timeline-entry h2, .entry-card h2 { margin: 10px 0 5px; font-size: 21px; line-height: 1.3; }
|
||||||
.timeline-entry p, .entry-card p { max-width: 680px; margin: 0 0 10px; color: var(--muted); }
|
.timeline-entry p, .entry-card p { max-width: 680px; margin: 0 0 10px; color: var(--muted); }
|
||||||
.read-more { color: var(--magenta); font-size: 10px; }
|
.read-more { color: var(--magenta); font-size: 10px; }
|
||||||
|
.activity-controls { display: flex; justify-content: space-between; align-items: center; gap: 18px; padding: 24px 0 0 37px; }
|
||||||
|
.activity-controls[hidden] { display: none; }
|
||||||
|
.activity-status { margin: 0; color: var(--muted); font: 500 9px var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
.activity-more-button {
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
background: transparent;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font: 500 9px var(--mono);
|
||||||
|
letter-spacing: .08em;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.activity-more-button:hover,
|
||||||
|
.activity-more-button:focus-visible { border-color: var(--teal); color: var(--teal); }
|
||||||
|
.activity-more-button:disabled { cursor: default; opacity: .55; }
|
||||||
|
|
||||||
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
|
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
|
||||||
.entry-card { min-height: 245px; padding: 28px; background: var(--bg); }
|
.entry-card { min-height: 245px; padding: 28px; background: var(--bg); }
|
||||||
@@ -286,6 +303,7 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
|||||||
.card-grid { grid-template-columns: 1fr; }
|
.card-grid { grid-template-columns: 1fr; }
|
||||||
.tag-directory { grid-template-columns: 1fr; }
|
.tag-directory { grid-template-columns: 1fr; }
|
||||||
.tag-filter { padding-block: 18px; }
|
.tag-filter { padding-block: 18px; }
|
||||||
|
.activity-controls { align-items: flex-start; padding-left: 30px; }
|
||||||
.tag-directory-card { min-height: 0; }
|
.tag-directory-card { min-height: 0; }
|
||||||
.project-hero { padding-block: 44px; }
|
.project-hero { padding-block: 44px; }
|
||||||
.project-hero-inner,
|
.project-hero-inner,
|
||||||
|
|||||||
+2
-1
@@ -9,8 +9,9 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600&display=swap" rel="stylesheet">
|
||||||
<link rel="icon" href="{{ '/static/favicon.svg'|url }}" type="image/svg+xml">
|
<link rel="icon" href="{{ '/static/favicon.svg'|url }}" type="image/svg+xml">
|
||||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=6">
|
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=7">
|
||||||
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
|
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
|
||||||
|
<script src="{{ '/static/activity.js'|url }}?v=1" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
|
|||||||
+6
-2
@@ -12,7 +12,7 @@
|
|||||||
<span>Recent Activity</span>
|
<span>Recent Activity</span>
|
||||||
<span>Sorted by date</span>
|
<span>Sorted by date</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline">
|
<div class="timeline" id="recent-activity-list" data-activity-feed data-batch-size="6">
|
||||||
{% set entries = [] %}
|
{% set entries = [] %}
|
||||||
{% for section_id in ['projects', 'articles', 'blog'] %}
|
{% for section_id in ['projects', 'articles', 'blog'] %}
|
||||||
{% for item in site.get('/' ~ section_id).children %}
|
{% for item in site.get('/' ~ section_id).children %}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for item in entries|sort(attribute='date', reverse=true) %}
|
{% for item in entries|sort(attribute='date', reverse=true) %}
|
||||||
<article class="timeline-entry accent-{{ item.kicker|lower }}">
|
<article class="timeline-entry accent-{{ item.kicker|lower }}" data-activity-item>
|
||||||
<div class="entry-meta">
|
<div class="entry-meta">
|
||||||
<span class="label">{{ item.kicker }}</span>
|
<span class="label">{{ item.kicker }}</span>
|
||||||
<time datetime="{{ item.date }}">{{ item.date|dateformat('YYYY.MM.dd') }}</time>
|
<time datetime="{{ item.date }}">{{ item.date|dateformat('YYYY.MM.dd') }}</time>
|
||||||
@@ -41,5 +41,9 @@
|
|||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="activity-controls" data-activity-controls hidden>
|
||||||
|
<p class="activity-status" data-activity-status aria-live="polite"></p>
|
||||||
|
<button class="activity-more-button" type="button" data-activity-more aria-controls="recent-activity-list">Show 6 more</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user