site: editorial tooling, grid masonry fix, new reviews + finds
UI / dev tooling - /find/ index: sort by file birthtime desc (sub-day chronology even when many finds share the same frontmatter date); render the captured-at on its own line under the host (~0.65rem mono muted) - New dev-only filter at the top of /find/: All / Not in lists, persisted in localStorage; "not in lists" hides finds referenced by any superpost - New editorial-tags chip strip on each find card (dev-only): selected tags visible by default, full picker on hover/focus-within with a "…" button for arbitrary tag entry; clicks copy /edit-find-tag <slug> <tag> to the clipboard via the existing EditConfirm singleton - New optional editorialTags: string[] field on the find collection - New src/data/editorial-tags.json — master list of available tags (initial set: ["delete"]); display order follows file order /grid/ now uses the JS Masonry component instead of CSS column-count, so the middle column no longer "dips"; tile hover updated to match the home page's accent-glow / no-translate style Content - New review: apex-markdown-processor (graduated from find of same slug; hero reused from the find folder) - New review: sindre-sorhus-older-mac-apps (graduated; intentionally hero-less per the source page's text-only design) - New review: superkey (added by user; hero wired in src/lib/hero.ts) - 24 new finds captured by /daily-finds across travel, jokes, quotes, product picks, and indie tools - 2 new finds superposts: 2026-05-04-a-small-atlas, 2026-05-04-off-until-needed Misc - daily-finds.log.md and candidate-sources.md updated with the day's runs - build-finds skill prompt iterated by user
This commit is contained in:
+117
-4
@@ -1,15 +1,45 @@
|
||||
---
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import Masonry from '../../components/Masonry.astro';
|
||||
import FindCard from '../../components/FindCard.astro';
|
||||
import EditAction from '../../components/EditAction.astro';
|
||||
import { getAllFinds } from '../../lib/find-items';
|
||||
import editorialTags from '../../data/editorial-tags.json';
|
||||
|
||||
const INITIAL_GRID = 18;
|
||||
const BATCH_SIZE = 18;
|
||||
|
||||
const finds = await getAllFinds();
|
||||
const findRoot = path.join(process.cwd(), 'src/content/find');
|
||||
|
||||
function capturedAtFor(slug: string): Date {
|
||||
try {
|
||||
const stat = fs.statSync(path.join(findRoot, slug, 'index.mdx'));
|
||||
// birthtime is creation time (true on macOS/Windows). Falls back to mtime
|
||||
// on filesystems without birthtime support.
|
||||
return new Date(stat.birthtimeMs || stat.mtimeMs);
|
||||
} catch {
|
||||
return new Date(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Decorate each find with its file-creation timestamp, then sort by that
|
||||
// (newest first) — gives true chronological order even when many finds
|
||||
// share the same day in their frontmatter `date`.
|
||||
const decorated = (await getAllFinds())
|
||||
.map((item) => ({ item, capturedAt: capturedAtFor(item.id) }))
|
||||
.sort((a, b) => b.capturedAt.valueOf() - a.capturedAt.valueOf());
|
||||
|
||||
const finds = decorated.map((d) => d.item);
|
||||
const lastUpdated = finds[0]?.data.date;
|
||||
|
||||
// Slugs referenced by any superpost in the `finds/` collection — dev-only filter.
|
||||
const inListSlugs = new Set(
|
||||
(await getCollection('finds')).flatMap((l) => l.data.items)
|
||||
);
|
||||
const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -19,9 +49,29 @@ const lastUpdated = finds[0]?.data.date;
|
||||
lastUpdated={lastUpdated}
|
||||
noindex
|
||||
>
|
||||
<Masonry initial={INITIAL_GRID} batch={BATCH_SIZE}>
|
||||
{finds.map((f) => <FindCard item={f} />)}
|
||||
</Masonry>
|
||||
{import.meta.env.DEV && (
|
||||
<div class="find-filter" role="group" aria-label="Filter">
|
||||
<button type="button" data-find-filter-set="all" aria-current="true">
|
||||
All ({finds.length})
|
||||
</button>
|
||||
<button type="button" data-find-filter-set="not-in-lists">
|
||||
Not in lists ({notInListCount})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div data-find-filter="all">
|
||||
<Masonry initial={INITIAL_GRID} batch={BATCH_SIZE}>
|
||||
{decorated.map(({ item, capturedAt }) => (
|
||||
<FindCard
|
||||
item={item}
|
||||
inList={inListSlugs.has(item.id)}
|
||||
capturedAt={capturedAt}
|
||||
availableTags={editorialTags}
|
||||
/>
|
||||
))}
|
||||
</Masonry>
|
||||
</div>
|
||||
|
||||
{import.meta.env.DEV && (
|
||||
<div class="edit-floating-toolbar" data-find-toolbar>
|
||||
@@ -50,6 +100,34 @@ const lastUpdated = finds[0]?.data.date;
|
||||
});
|
||||
refresh();
|
||||
}
|
||||
|
||||
// Dev-only filter: toggle "all" vs "not-in-lists" on the masonry wrapper.
|
||||
const wrapper = document.querySelector<HTMLDivElement>('[data-find-filter]');
|
||||
const filterButtons = document.querySelectorAll<HTMLButtonElement>('[data-find-filter-set]');
|
||||
const FILTER_KEY = 'find-filter';
|
||||
|
||||
if (wrapper && filterButtons.length) {
|
||||
type FilterValue = 'all' | 'not-in-lists';
|
||||
const apply = (value: FilterValue) => {
|
||||
wrapper.setAttribute('data-find-filter', value);
|
||||
for (const b of filterButtons) {
|
||||
if (b.dataset.findFilterSet === value) b.setAttribute('aria-current', 'true');
|
||||
else b.removeAttribute('aria-current');
|
||||
}
|
||||
try { localStorage.setItem(FILTER_KEY, value); } catch (e) {}
|
||||
};
|
||||
|
||||
let saved: string | null = null;
|
||||
try { saved = localStorage.getItem(FILTER_KEY); } catch (e) {}
|
||||
if (saved === 'all' || saved === 'not-in-lists') apply(saved);
|
||||
|
||||
for (const b of filterButtons) {
|
||||
b.addEventListener('click', () => {
|
||||
const v = b.dataset.findFilterSet;
|
||||
if (v === 'all' || v === 'not-in-lists') apply(v);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -78,5 +156,40 @@ const lastUpdated = finds[0]?.data.date;
|
||||
color: var(--fg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.find-filter {
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
margin-bottom: 1.25rem;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.find-filter button {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0.4rem 0.95rem;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.find-filter button + button {
|
||||
border-left: 1px solid var(--rule);
|
||||
}
|
||||
.find-filter button:hover { color: var(--accent); }
|
||||
.find-filter button[aria-current="true"] {
|
||||
background: var(--fg);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
/* Hide in-list cards when the filter is set to not-in-lists. */
|
||||
[data-find-filter="not-in-lists"] [data-in-list="true"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</BaseLayout>
|
||||
|
||||
+41
-62
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Masonry from '../components/Masonry.astro';
|
||||
import { heroes } from '../lib/hero';
|
||||
|
||||
const reviews = (await getCollection('reviews')).sort(
|
||||
@@ -24,79 +25,56 @@ const lastUpdated = reviews[0]?.data.date;
|
||||
lastUpdated={lastUpdated}
|
||||
noindex
|
||||
>
|
||||
<ul class="grid">
|
||||
{
|
||||
tiles.map((tile) => (
|
||||
<li>
|
||||
<a href={tile.href} class:list={['tile', { 'text-only': !tile.hero }]}>
|
||||
{tile.hero?.kind === 'image' && (
|
||||
<img
|
||||
class="media"
|
||||
src={tile.hero.src.src}
|
||||
width={tile.hero.src.width}
|
||||
height={tile.hero.src.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
alt={tile.name}
|
||||
/>
|
||||
)}
|
||||
{tile.hero?.kind === 'video' && (
|
||||
<video
|
||||
class="media"
|
||||
src={tile.hero.src}
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
preload="none"
|
||||
aria-label={tile.name}
|
||||
/>
|
||||
)}
|
||||
<div class="text">
|
||||
<span class="name">{tile.name}</span>
|
||||
<span class="subtitle">{tile.subtitle}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<Masonry>
|
||||
{tiles.map((tile) => (
|
||||
<article>
|
||||
<a href={tile.href} class:list={['tile', { 'text-only': !tile.hero }]}>
|
||||
{tile.hero?.kind === 'image' && (
|
||||
<img
|
||||
class="media"
|
||||
src={tile.hero.src.src}
|
||||
width={tile.hero.src.width}
|
||||
height={tile.hero.src.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
alt={tile.name}
|
||||
/>
|
||||
)}
|
||||
{tile.hero?.kind === 'video' && (
|
||||
<video
|
||||
class="media"
|
||||
src={tile.hero.src}
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
preload="none"
|
||||
aria-label={tile.name}
|
||||
/>
|
||||
)}
|
||||
<div class="text">
|
||||
<span class="name">{tile.name}</span>
|
||||
<span class="subtitle">{tile.subtitle}</span>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</Masonry>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
column-count: 3;
|
||||
column-gap: 1rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
.grid { column-count: 2; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.grid { column-count: 1; }
|
||||
}
|
||||
|
||||
.grid > li {
|
||||
break-inside: avoid;
|
||||
display: block;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--rule);
|
||||
border: 2px solid var(--rule);
|
||||
background: var(--bg);
|
||||
color: inherit;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
transition: box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.tile:hover {
|
||||
text-decoration: none;
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
|
||||
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.18);
|
||||
border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
|
||||
box-shadow: 0 0 20px -4px color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
}
|
||||
.tile:hover .name { color: var(--accent); }
|
||||
|
||||
@@ -112,6 +90,7 @@ const lastUpdated = reviews[0]?.data.date;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
background: var(--card-fill);
|
||||
}
|
||||
.name {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
||||
|
||||
@@ -5,8 +5,8 @@ import Masonry from '../components/Masonry.astro';
|
||||
import { heroes } from '../lib/hero';
|
||||
import { buildOgImage, buildItemList, WEBSITE, ORGANIZATION } from '../lib/seo';
|
||||
|
||||
const HERO_SLUG = 'alt-tab';
|
||||
const INITIAL_GRID = 7;
|
||||
const HERO_SLUG = 'superkey';
|
||||
const INITIAL_GRID = 12;
|
||||
const BATCH_SIZE = 7;
|
||||
|
||||
const reviews = (await getCollection('reviews')).sort(
|
||||
|
||||
Reference in New Issue
Block a user