Skip to content

Commit d6d7341

Browse files
author
Mark Riechers
authored
Merge pull request #13 from MarkOnFire/claude/style-suggested-tools-X90ey
Redesign suggested tools with card-based layout
2 parents df79390 + d4cf009 commit d6d7341

3 files changed

Lines changed: 283 additions & 26 deletions

File tree

index.html

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,95 @@
235235
from { opacity: 0; transform: translateY(8px); }
236236
to { opacity: 1; transform: translateY(0); }
237237
}
238+
239+
/* -------------------------------------------------------
240+
* Suggested (external) tool cards
241+
* ------------------------------------------------------- */
242+
243+
.suggested-tools {
244+
margin-top: var(--space-2xl);
245+
}
246+
247+
.section-heading {
248+
font-size: var(--text-xl);
249+
font-weight: 600;
250+
color: var(--color-text-heading);
251+
margin-bottom: var(--space-sm);
252+
letter-spacing: -0.01em;
253+
}
254+
255+
.suggested-grid {
256+
margin-top: var(--space-lg);
257+
}
258+
259+
/* External card modifier — dashed border + relative for badge */
260+
.tool-card--external {
261+
position: relative;
262+
border-style: dashed;
263+
cursor: default;
264+
}
265+
266+
.tool-card--external:hover {
267+
border-color: var(--color-border-hover);
268+
transform: translateY(-2px);
269+
}
270+
271+
/* "External ↗" badge */
272+
.external-badge {
273+
position: absolute;
274+
top: var(--space-md);
275+
right: var(--space-md);
276+
font-size: 0.6875rem;
277+
font-weight: 500;
278+
line-height: 1;
279+
padding: 3px 8px;
280+
border-radius: 999px;
281+
color: var(--color-primary);
282+
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface-raised));
283+
border: 1px solid color-mix(in srgb, var(--color-primary) 25%, var(--color-border));
284+
letter-spacing: 0.02em;
285+
white-space: nowrap;
286+
pointer-events: none;
287+
}
288+
289+
/* Links list inside an external card */
290+
.tool-card-links {
291+
display: flex;
292+
flex-direction: column;
293+
gap: 6px;
294+
margin-top: var(--space-md);
295+
}
296+
297+
.tool-card-links a {
298+
font-size: var(--text-sm);
299+
color: var(--color-primary);
300+
text-decoration: none;
301+
display: inline-flex;
302+
align-items: center;
303+
gap: 3px;
304+
width: fit-content;
305+
}
306+
307+
.tool-card-links a:hover {
308+
color: var(--color-primary-hover);
309+
text-decoration: underline;
310+
}
311+
312+
.external-arrow {
313+
font-size: 0.75em;
314+
opacity: 0.7;
315+
}
316+
317+
/* Staggered entrance for suggested cards */
318+
@media (prefers-reduced-motion: no-preference) {
319+
.suggested-grid .tool-card {
320+
animation: fadeIn 0.3s ease both;
321+
}
322+
.suggested-grid .tool-card:nth-child(1) { animation-delay: 100ms; }
323+
.suggested-grid .tool-card:nth-child(2) { animation-delay: 200ms; }
324+
.suggested-grid .tool-card:nth-child(3) { animation-delay: 300ms; }
325+
.suggested-grid .tool-card:nth-child(4) { animation-delay: 400ms; }
326+
}
238327
</style>
239328
</head>
240329
<body>
@@ -284,12 +373,53 @@ <h2 class="tool-name">HTML Formatter &amp; Tidy</h2>
284373
<!-- SUGGESTED_TOOLS_START -->
285374
<section class="suggested-tools">
286375
<h2 class="section-heading">Suggested Tools</h2>
287-
<ul class="suggested-list">
288-
<li><a href="https://www.castfeedvalidator.com/" target="_blank" rel="noopener">Cast Feed Validator <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
289-
<li><a href="https://validator.w3.org/feed/" target="_blank" rel="noopener">W3C Feed Validator <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
290-
<li><a href="https://embedresponsively.com/" target="_blank" rel="noopener">Embed Responsively <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
291-
<li><a href="https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2" target="_blank" rel="noopener">Image Alt Writer 2 <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
292-
</ul>
376+
<div class="tools-grid suggested-grid">
377+
<div class="tool-card tool-card--external">
378+
<span class="external-badge" aria-label="External tool">External &#8599;</span>
379+
<span class="tool-icon" aria-hidden="true">📡</span>
380+
<h3 class="tool-name">Feed Validators</h3>
381+
<p class="tool-description">Validate podcast and web feeds for errors and compatibility before publishing.</p>
382+
<div class="tool-card-links">
383+
<a href="https://www.castfeedvalidator.com/" target="_blank" rel="noopener">Cast Feed Validator <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
384+
<a href="https://validator.w3.org/feed/" target="_blank" rel="noopener">W3C Feed Validator <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
385+
</div>
386+
<div class="tool-tags">
387+
<span class="tool-tag">RSS</span>
388+
<span class="tool-tag">Podcasts</span>
389+
<span class="tool-tag">Validation</span>
390+
</div>
391+
</div>
392+
393+
<div class="tool-card tool-card--external">
394+
<span class="external-badge" aria-label="External tool">External &#8599;</span>
395+
<span class="tool-icon" aria-hidden="true">📐</span>
396+
<h3 class="tool-name">Responsive Embeds</h3>
397+
<p class="tool-description">Generate responsive embed codes for videos, maps, and other media.</p>
398+
<div class="tool-card-links">
399+
<a href="https://embedresponsively.com/" target="_blank" rel="noopener">Embed Responsively <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
400+
</div>
401+
<div class="tool-tags">
402+
<span class="tool-tag">Responsive</span>
403+
<span class="tool-tag">Embeds</span>
404+
<span class="tool-tag">Video</span>
405+
</div>
406+
</div>
407+
408+
<div class="tool-card tool-card--external">
409+
<span class="external-badge" aria-label="External tool">External &#8599;</span>
410+
<span class="tool-icon" aria-hidden="true">🖼️</span>
411+
<h3 class="tool-name">Image Alt Text</h3>
412+
<p class="tool-description">AI-powered tool to generate descriptive, accessible alt text for images.</p>
413+
<div class="tool-card-links">
414+
<a href="https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2" target="_blank" rel="noopener">Image Alt Writer 2 <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
415+
</div>
416+
<div class="tool-tags">
417+
<span class="tool-tag">Accessibility</span>
418+
<span class="tool-tag">AI</span>
419+
<span class="tool-tag">Images</span>
420+
</div>
421+
</div>
422+
</div>
293423
</section>
294424
<!-- SUGGESTED_TOOLS_END -->
295425
</main>

scripts/build-suggested-tools.js

Lines changed: 129 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* Each page must contain marker comments:
66
* <!-- SUGGESTED_TOOLS_START --> ... <!-- SUGGESTED_TOOLS_END -->
77
*
8-
* The home page gets a standalone <section>, while tool sub-pages get
9-
* inline links appended to their existing "Related tools:" footer line.
8+
* The home page gets card-based layout (using ### groups with metadata).
9+
* Tool sub-pages get inline links appended to their existing footer line.
1010
*/
1111

1212
const fs = require('fs');
@@ -27,24 +27,64 @@ const PAGE_MAP = {
2727
// ---------------------------------------------------------------------------
2828
function parseMarkdown(src) {
2929
const sections = {};
30-
let current = null;
30+
let currentSection = null;
31+
let currentGroup = null;
3132

3233
for (const raw of src.split('\n')) {
3334
const line = raw.trim();
3435

3536
// H2 heading = page key
36-
const h2 = line.match(/^##\s+(.+)$/);
37+
const h2 = line.match(/^##\s+([^#].*)$/);
3738
if (h2) {
38-
current = h2[1].trim().toLowerCase();
39-
sections[current] = [];
39+
currentSection = h2[1].trim().toLowerCase();
40+
sections[currentSection] = { groups: [], links: [] };
41+
currentGroup = null;
4042
continue;
4143
}
4244

45+
if (!currentSection) continue;
46+
47+
// H3 heading = card group (e.g., "### 📡 Feed Validators")
48+
const h3 = line.match(/^###\s+(.+)$/);
49+
if (h3) {
50+
const titleText = h3[1].trim();
51+
// Extract leading emoji icon if present
52+
const iconMatch = titleText.match(/^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s*/u);
53+
const icon = iconMatch ? iconMatch[1] : null;
54+
const name = iconMatch ? titleText.slice(iconMatch[0].length).trim() : titleText;
55+
56+
currentGroup = { icon, name, description: '', tags: [], links: [] };
57+
sections[currentSection].groups.push(currentGroup);
58+
continue;
59+
}
60+
61+
// Tags line (e.g., "Tags: RSS, Podcasts, Validation")
62+
if (currentGroup) {
63+
const tagsMatch = line.match(/^Tags:\s*(.+)$/i);
64+
if (tagsMatch) {
65+
currentGroup.tags = tagsMatch[1].split(',').map(t => t.trim()).filter(Boolean);
66+
continue;
67+
}
68+
}
69+
4370
// Markdown link inside a list item
44-
if (current && /^-\s+\[/.test(line)) {
71+
if (/^-\s+\[/.test(line)) {
4572
const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
4673
if (match) {
47-
sections[current].push({ name: match[1], url: match[2] });
74+
const link = { name: match[1], url: match[2] };
75+
if (currentGroup) {
76+
currentGroup.links.push(link);
77+
} else {
78+
sections[currentSection].links.push(link);
79+
}
80+
}
81+
continue;
82+
}
83+
84+
// Plain text line = description for current group
85+
if (currentGroup && line && !line.startsWith('#') && !line.startsWith('-')) {
86+
if (!currentGroup.description) {
87+
currentGroup.description = line;
4888
}
4989
}
5090
}
@@ -53,10 +93,58 @@ function parseMarkdown(src) {
5393
}
5494

5595
// ---------------------------------------------------------------------------
56-
// Render HTML for each mode
96+
// Render HTML — card layout for home page
97+
// ---------------------------------------------------------------------------
98+
function renderCardSection(groups) {
99+
const cards = groups.map(group => {
100+
const parts = [];
101+
parts.push(' <div class="tool-card tool-card--external">');
102+
parts.push(' <span class="external-badge" aria-label="External tool">External &#8599;</span>');
103+
104+
if (group.icon) {
105+
parts.push(` <span class="tool-icon" aria-hidden="true">${group.icon}</span>`);
106+
}
107+
108+
parts.push(` <h3 class="tool-name">${group.name}</h3>`);
109+
110+
if (group.description) {
111+
parts.push(` <p class="tool-description">${group.description}</p>`);
112+
}
113+
114+
if (group.links.length > 0) {
115+
parts.push(' <div class="tool-card-links">');
116+
for (const l of group.links) {
117+
parts.push(` <a href="${l.url}" target="_blank" rel="noopener">${l.name} <span class="external-arrow" aria-hidden="true">&#8599;</span></a>`);
118+
}
119+
parts.push(' </div>');
120+
}
121+
122+
if (group.tags.length > 0) {
123+
parts.push(' <div class="tool-tags">');
124+
for (const t of group.tags) {
125+
parts.push(` <span class="tool-tag">${t}</span>`);
126+
}
127+
parts.push(' </div>');
128+
}
129+
130+
parts.push(' </div>');
131+
return parts.join('\n');
132+
}).join('\n\n');
133+
134+
return [
135+
' <section class="suggested-tools">',
136+
' <h2 class="section-heading">Suggested Tools</h2>',
137+
' <div class="tools-grid suggested-grid">',
138+
cards,
139+
' </div>',
140+
' </section>',
141+
].join('\n');
142+
}
143+
144+
// ---------------------------------------------------------------------------
145+
// Render HTML — simple list fallback (home page without groups)
57146
// ---------------------------------------------------------------------------
58-
function renderSection(tools) {
59-
if (tools.length === 0) return '';
147+
function renderListSection(tools) {
60148
const items = tools
61149
.map(t =>
62150
` <li><a href="${t.url}" target="_blank" rel="noopener">${t.name} <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>`
@@ -72,9 +160,30 @@ function renderSection(tools) {
72160
].join('\n');
73161
}
74162

75-
function renderInline(tools) {
76-
if (tools.length === 0) return '';
77-
return tools
163+
// ---------------------------------------------------------------------------
164+
// Render HTML — home page section (cards or list)
165+
// ---------------------------------------------------------------------------
166+
function renderSection(section) {
167+
if (section.groups && section.groups.length > 0) {
168+
return renderCardSection(section.groups);
169+
}
170+
if (section.links && section.links.length > 0) {
171+
return renderListSection(section.links);
172+
}
173+
return '';
174+
}
175+
176+
// ---------------------------------------------------------------------------
177+
// Render HTML — inline links for tool sub-pages
178+
// ---------------------------------------------------------------------------
179+
function renderInline(section) {
180+
// Collect all links from both groups and flat links
181+
const allLinks = [...section.links];
182+
for (const group of section.groups) {
183+
allLinks.push(...group.links);
184+
}
185+
if (allLinks.length === 0) return '';
186+
return allLinks
78187
.map(t =>
79188
` <a href="${t.url}" target="_blank" rel="noopener">${t.name}</a>`
80189
)
@@ -109,8 +218,8 @@ const sections = parseMarkdown(md);
109218
let changed = 0;
110219

111220
for (const [key, config] of Object.entries(PAGE_MAP)) {
112-
const tools = sections[key];
113-
if (!tools || tools.length === 0) {
221+
const section = sections[key];
222+
if (!section || (section.groups.length === 0 && section.links.length === 0)) {
114223
console.log(` skip ${config.file} (no tools listed under "${key}")`);
115224
continue;
116225
}
@@ -119,17 +228,18 @@ for (const [key, config] of Object.entries(PAGE_MAP)) {
119228
const html = fs.readFileSync(filePath, 'utf-8');
120229

121230
const rendered = config.mode === 'section'
122-
? renderSection(tools)
123-
: renderInline(tools);
231+
? renderSection(section)
232+
: renderInline(section);
124233

125234
const result = inject(html, rendered);
126235
if (result === null) {
127236
console.error(` ERROR ${config.file}: missing ${START} / ${END} markers`);
128237
process.exit(1);
129238
}
130239

240+
const toolCount = section.groups.length + section.links.length;
131241
fs.writeFileSync(filePath, result, 'utf-8');
132-
console.log(` wrote ${config.file} (${tools.length} tools)`);
242+
console.log(` wrote ${config.file} (${toolCount} entries)`);
133243
changed++;
134244
}
135245

suggested-tools.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,30 @@ sections across the site. Run `node scripts/build-suggested-tools.js` or
55
push to trigger the GitHub Action to rebuild.
66

77
Each `##` section maps to a page (`home`, `formatter`, `og-image`).
8-
Add a standard markdown link to include a tool on that page.
8+
9+
For the **home** page, use `###` sub-headings to define card groups.
10+
Each group gets an icon (emoji in the heading), a plain-text description
11+
line, a `Tags:` line, and one or more markdown links. Similar tools
12+
share a single card.
13+
14+
Sub-pages (`formatter`, `og-image`) keep the flat link list format.
915

1016
## home
1117

18+
### 📡 Feed Validators
19+
Validate podcast and web feeds for errors and compatibility before publishing.
20+
Tags: RSS, Podcasts, Validation
1221
- [Cast Feed Validator](https://www.castfeedvalidator.com/)
1322
- [W3C Feed Validator](https://validator.w3.org/feed/)
23+
24+
### 📐 Responsive Embeds
25+
Generate responsive embed codes for videos, maps, and other media.
26+
Tags: Responsive, Embeds, Video
1427
- [Embed Responsively](https://embedresponsively.com/)
28+
29+
### 🖼️ Image Alt Text
30+
AI-powered tool to generate descriptive, accessible alt text for images.
31+
Tags: Accessibility, AI, Images
1532
- [Image Alt Writer 2](https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2)
1633

1734
## formatter

0 commit comments

Comments
 (0)