Files
unique/src/components/FindCard.astro
T
rzen 7e1b200bea 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
2026-05-04 18:57:04 -04:00

293 lines
7.7 KiB
Plaintext

---
import { Image } from 'astro:assets';
import type { FindItem } from '../lib/find-items';
import { findHero } from '../lib/find-hero';
import SourceBadge from './SourceBadge.astro';
interface Props {
item: FindItem;
inList?: boolean;
capturedAt?: Date;
availableTags?: string[];
}
const { item, inList, capturedAt, availableTags = [] } = Astro.props;
const hero = findHero(item.id);
const showInListAttr = import.meta.env.DEV && inList !== undefined;
const selectedTags: string[] = item.data.editorialTags ?? [];
const selectedSet = new Set(selectedTags);
const fmtCaptured = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
function host(url: string) {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch {
return url;
}
}
---
<article
class:list={['find-card', { 'has-hero': !!hero }]}
data-in-list={showInListAttr ? String(inList) : undefined}
>
{import.meta.env.DEV && (
<label class="edit-checkbox" aria-label={`Select ${item.data.name}`}>
<input type="checkbox" data-find-checkbox data-slug={item.id} />
</label>
)}
{hero && (
<Image
class="media"
src={hero}
widths={[320, 480, 640]}
sizes="(max-width: 520px) 92vw, (max-width: 880px) 45vw, 240px"
loading="lazy"
decoding="async"
alt={item.data.name}
/>
)}
<div class="content">
<div class="head">
<a href={`/find/${item.id}/`} class="name">{item.data.name}</a>
<SourceBadge source={item.data.source} />
</div>
{item.data.subtitle && <p class="subtitle">{item.data.subtitle}</p>}
<p class="external">
<span class="ext-label">→</span>
<span class="host">{host(item.data.link)}</span>
</p>
{capturedAt && capturedAt.valueOf() > 0 && (
<p class="captured-at-row">
<time datetime={capturedAt.toISOString()} class="captured-at">
{fmtCaptured.format(capturedAt)}
</time>
</p>
)}
{import.meta.env.DEV && (
<div class="edit-tags">
{selectedTags.length > 0 && (
<div class="edit-tags-selected" aria-hidden="true">
{selectedTags.map((tag) => (
<span class={`edit-tag is-selected ${tag === 'delete' ? 'is-delete' : ''}`}>{tag}</span>
))}
</div>
)}
<div class="edit-tags-picker" role="group" aria-label="Editorial tags">
{availableTags.map((tag) => (
<button
type="button"
class:list={[
'edit-tag',
{ 'is-selected': selectedSet.has(tag), 'is-delete': tag === 'delete' },
]}
data-edit-action="copy"
data-command={`/edit-find-tag ${item.id} ${tag}`}
data-confirm={selectedSet.has(tag)
? `Remove "${tag}" from "${item.data.name}".`
: `Tag "${item.data.name}" with "${tag}".`}
>
{tag}
</button>
))}
<button
type="button"
class="edit-tag edit-tag-prompt"
data-edit-action="tag-prompt"
data-slug={item.id}
data-name={item.data.name}
aria-label="Add custom tag"
title="Add custom tag"
>
</button>
</div>
</div>
)}
</div>
</article>
<style>
.find-card {
position: relative;
border-radius: 8px;
border: 1px solid var(--rule);
background: var(--bg);
overflow: hidden;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.find-card:hover {
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);
}
.find-card:hover .name { color: var(--accent); }
.media {
display: block;
width: 100%;
height: auto;
background: var(--card-fill);
border-bottom: 1px solid var(--rule);
}
.content {
padding: 0.95rem 1.05rem 1rem;
}
.head {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.name {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1.05rem;
color: var(--fg);
letter-spacing: -0.01em;
text-decoration: none;
transition: color 0.18s ease;
}
.name:hover { text-decoration: none; }
/* Stretched click target — covers the whole card. */
.name::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
}
/* Anchors inside the card (e.g. the source badge) sit above the stretched target. */
.find-card :global(a:not(.name)) {
position: relative;
z-index: 1;
}
.subtitle {
margin: 0 0 0.6rem;
color: var(--muted);
font-size: 0.92rem;
line-height: 1.4;
font-style: italic;
}
.external {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
color: var(--muted);
display: flex;
gap: 0.35rem;
align-items: baseline;
}
.ext-label { color: var(--accent); font-family: ui-sans-serif, system-ui, sans-serif; }
.captured-at-row {
margin: 0.25rem 0 0;
line-height: 1;
}
.captured-at {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.65rem;
color: var(--muted);
white-space: nowrap;
}
/* Editorial tags (dev-only) */
.edit-tags {
margin-top: 0.55rem;
position: relative;
z-index: 2;
}
.edit-tags-selected,
.edit-tags-picker {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.edit-tags-picker { display: none; }
.find-card:hover .edit-tags-selected,
.find-card:focus-within .edit-tags-selected { display: none; }
.find-card:hover .edit-tags-picker,
.find-card:focus-within .edit-tags-picker { display: flex; }
.edit-tag {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
line-height: 1;
padding: 0.28rem 0.55rem;
margin: 0;
border-radius: 999px;
border: 1px solid var(--rule);
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
button.edit-tag { font: inherit; font-size: 0.68rem; font-weight: 600; }
.edit-tag:hover {
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
}
.edit-tag.is-selected {
color: var(--bg);
background: var(--fg);
border-color: var(--fg);
}
.edit-tag.is-selected:hover {
background: color-mix(in srgb, var(--fg) 85%, var(--accent));
color: var(--bg);
border-color: color-mix(in srgb, var(--fg) 85%, var(--accent));
}
.edit-tag.is-delete {
color: #c0392b;
border-color: color-mix(in srgb, #c0392b 35%, var(--rule));
}
.edit-tag.is-delete.is-selected {
background: #c0392b;
color: #fff;
border-color: #c0392b;
}
.edit-tag-prompt {
font-size: 0.85rem;
padding: 0.1rem 0.6rem 0.18rem;
color: var(--muted);
}
.edit-checkbox {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 3;
width: 1.6rem;
height: 1.6rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
border: 1px solid var(--rule);
border-radius: 4px;
cursor: pointer;
}
.edit-checkbox input {
width: 1rem;
height: 1rem;
margin: 0;
cursor: pointer;
accent-color: var(--accent);
}
.find-card:has(.edit-checkbox input:checked) {
border-color: var(--accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 28%, transparent);
}
</style>