open-design/apps/landing-page/app/pages/skills/[slug]/index.astro
Jane 40ae0836dd
feat(landing-page): rebuild plugins library to mirror in-app taxonomy (#2926)
* feat(landing-page): synthesize fallback preview cards for instruction skills

The skill catalog renders a diagonal-stripe placeholder for any skill
without a runnable example.html, which leaves ~70% of /skills/ as a
field of bare grey thumbs (instruction skills like copywriting,
creative-director, color-expert, brainstorming have no static demo
because their output depends on the agent's input).

Synthesize a typographic editorial card from each SKILL.md frontmatter
and screenshot it through the same Playwright pipeline that handles
real demos, so every catalog row carries a thumbnail. Cards include:

  - OPEN DESIGN · SKILL top label + Nº NNN index (1..96 over the
    instruction subset, sorted by od.featured then alphabetical)
  - Big Playfair Display slug with a coral dot accent
  - Italic serif description clamped to 3 lines
  - mode/category chips + "Curated from <author>" attribution
  - Warm-paper background with a subtle 135° stripe to thread the
    landing's existing visual language

Bundle a few related improvements caught while building this:

  - SkillRecord gains a `kind: 'instruction' | 'template'` field so
    the detail page can render differently per kind (instruction
    skills now render the SKILL.md body inline as "About this skill",
    template skills keep the click-to-expand iframe demo).
  - Catalog row thumbnails switch from the bespoke IntersectionObserver
    pipeline to native `loading="lazy"` (with eager + fetchpriority=high
    on the first 3). The observer's swap latency stranded mid-list
    rows on the SVG placeholder during fast scrolls; native lazy uses
    the browser's 1250-3000px lookahead so the placeholder flash is
    gone.
  - precise-lazyload rootMargin bumped to 1500px for any remaining
    data-precise-src callers.
  - CI cache key for generated previews now folds in
    fallback-preview-card.ts so a template tweak invalidates the cache.

* feat(landing-page): rebuild plugins library to mirror in-app taxonomy

The marketing site's `/skills/`, `/templates/`, `/systems/`, `/craft/`
top-level entries were organized around author-supplied `od.mode` /
`od.scenario` taxonomies that visitors never see inside Open Design
itself. The in-app Plugins home (`apps/web/src/components/plugins-home/`)
groups every bundled plugin by the artifact it produces — Prototype,
Live Artifact, Slides, Image, Video, HyperFrames, Audio — and that's
the language users encounter the moment they open the product.

This PR rebuilds the public library around the same taxonomy and the
same data source so a visitor reading "Templates · 231" on the
marketing site sees the same 231 inside the app.

## What changes

- New top-level `/plugins/` hub: four tiles (Templates, Skills,
  Systems, Craft) with live counts pulled straight from
  `plugins/_official/<bucket>/<slug>/open-design.json` — the daemon's
  bundled-plugin registry.
- `/plugins/templates/` lists every bundled plugin that lands in one
  of the seven artifact kinds. Seven sub-routes
  (`/plugins/templates/prototype/`, `/deck/`, `/image/`, `/video/`,
  `/hyperframes/`, `/audio/`, `/live-artifact/`) carry the same chip
  rail with an active state, so visitors can switch artifact kinds
  with one click without losing the rail.
- Each artifact-kind sub-route shows a Scene chip rail when the kind
  has scene buckets (Prototype / Slides / Image / Video each get
  five-six). The Scene filter runs client-side via inline `style.display`
  toggles; URLs stay one-per-kind so we don't multiply 25 × 18 locales
  worth of static pages just for filter combinations.
- `/plugins/skills/` collects the instruction-only entries (mode
  doesn't fit any of the seven kinds) — copywriting, color theory,
  creative direction, brainstorming, etc.
- `/plugins/systems/` lists the 150 bundled design systems via the
  legacy SystemCard renderer (palette swatches, tagline) so the
  visual treatment matches the in-product library.
- `/plugins/craft/` keeps the existing craft principles list.
- `/plugins/<manifest-id>/` detail pages built from manifest metadata:
  hero (poster image or playable Cloudflare Stream MP4 for video
  templates), author / mode / scenario / tags, GitHub source link.
  Author URLs pointing at the `nexu-io` org redirect to the
  `nexu-io/open-design` repo so the attribution is actionable.
- Header dropdown labelled "Plugins" with the four sub-routes; footer
  Library column updated to match.
- Old marketplace registry pages under `/plugins/` and
  `/[locale]/plugins/` removed (they were a dormant placeholder UI;
  the actual manifests it tried to load lived nowhere). The rest of
  the legacy plugin-registry loader stays intact for any other
  consumer.

## Preview generation

Bundled plugins ship `od.preview.poster` URLs on R2 for image and
video templates; those are used directly. The other 293 entries
(html-mode examples, design-systems, scenarios) had no poster, so
`generate-previews.ts` was extended to:

1. Screenshot a local `example.html` referenced by `od.preview.entry`
   when present (134 examples).
2. Synthesize the same typographic editorial card the SKILL.md
   fallback uses, sourced from manifest title / description / mode /
   author (159 systems / scenarios / misc).

Output lands at `public/previews/plugins/<manifest-id>.png`. The
catalog loader checks for the local file when the manifest carries no
poster URL, so the row's `<img src>` always has something to point at.

Result: every catalog row and every detail page has a thumbnail;
visiting `/plugins/templates/video/` shows the same 48 entries the
in-app Plugins home shows, hyperframes the same 13, etc.

## Counts

- Templates: 231 (Prototype 59 + Slides 59 + Image 46 + Video 48 +
  HyperFrames 13 + Audio 1 + Live Artifact 5)
- Skills: 15
- Systems: 150
- Craft: 11

Atoms (13 infrastructure plugins, `od.kind === 'atom'`) are filtered
to mirror the in-app behaviour.

* fix(landing-page): use Astro 6 render() helper for SKILL.md body

Astro 6 dropped `entry.render()` in favour of a top-level `render(entry)`
helper imported from `astro:content`. The instruction-kind skill detail
page was still using the legacy method, which compiled locally on Astro
6 only because tsx ignored the missing prototype method, but `astro
check` (run in CI) flagged it as ts(2551) and broke the workflow.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-26 02:49:58 +00:00

472 lines
25 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
/*
* /skills/<slug>/ — a detail page per skill.
*
* Two flavours render slightly differently:
* - `template` skills get a click-to-expand iframe of their
* `example.html` demo and stay deliberately brief — the demo is the
* content, the README is one click away on GitHub.
* - `instruction` skills (no runnable demo) instead render the full
* SKILL.md body inline, so the page reads like a brief: what the
* skill does, when it triggers, how to use it. Otherwise the page
* would be a one-line description and a row of CTAs.
*/
import { getEntry, render } from 'astro:content';
import Layout from '../../../_components/sub-page-layout.astro';
import LazyImg from '../../../_components/lazy-img.astro';
import { getSkillRecords, type SkillRecord } from '../../../_lib/catalog';
import {
getLandingUiCopy,
localeFromPath,
localizedHref,
type LandingLocaleCode,
} from '../../../i18n';
/*
* Localized share-copy template, keyed by landing locale. The brand
* keyword "open-source Claude Design alternative" stays in English
* because that's the canonical search query Google associates with
* the domain — translating it would split the entity claim. The
* surrounding sentence ("I'm using X from @opendesignai") translates
* per locale so the message reads as one coherent voice instead of
* mixing two scripts in a single share post.
*
* `{name}` and `{description}` are interpolated at render time.
* `{url}` is replaced with the canonical detail-page URL.
*/
type ShareTemplate = (vars: { name: string; description: string; url: string }) => string;
const SHARE_COPY: Record<LandingLocaleCode, ShareTemplate> = {
en: ({ name, description, url }) => `🎨 Just discovered ${name} on @opendesignai — the open-source Claude Design alternative.
✨ Local-first · BYOK · your agent does the design.
→ ${url}`,
zh: ({ name, description, url }) => `🎨 安利一个:@opendesignai 上的 ${name} —— Claude Design 的开源替代品。
✨ 本地优先 · 自带模型 · 让你自己的 agent 做设计。
→ ${url}`,
'zh-tw': ({ name, description, url }) => `🎨 推薦一個:@opendesignai 上的 ${name} —— Claude Design 的開源替代品。
✨ 本地優先 · 自帶模型 · 讓你自己的 agent 做設計。
→ ${url}`,
ja: ({ name, description, url }) => `🎨 @opendesignai で ${name} を発見 —— オープンソースの Claude Design 代替。
✨ ローカル優先 · BYOK · あなたのエージェントが設計する。
→ ${url}`,
ko: ({ name, description, url }) => `🎨 @opendesignai에서 ${name} 발견 —— 오픈 소스 Claude Design 대안.
✨ 로컬 우선 · BYOK · 에이전트가 디자인합니다.
→ ${url}`,
de: ({ name, description, url }) => `🎨 Gerade entdeckt: ${name} auf @opendesignai — die Open-Source-Alternative zu Claude Design.
✨ Local-first · BYOK · dein Agent designt.
→ ${url}`,
fr: ({ name, description, url }) => `🎨 Découvert : ${name} sur @opendesignai — l'alternative open-source à Claude Design.
✨ Local-first · BYOK · votre agent fait le design.
→ ${url}`,
ru: ({ name, description, url }) => `🎨 Нашёл ${name} на @opendesignai — open-source альтернативу Claude Design.
✨ Локально · BYOK · агент сам делает дизайн.
→ ${url}`,
es: ({ name, description, url }) => `🎨 Acabo de descubrir ${name} en @opendesignai — la alternativa open-source a Claude Design.
✨ Local-first · BYOK · tu agente diseña.
→ ${url}`,
'pt-br': ({ name, description, url }) => `🎨 Acabei de descobrir ${name} no @opendesignai — a alternativa open-source ao Claude Design.
✨ Local-first · BYOK · seu agente faz o design.
→ ${url}`,
it: ({ name, description, url }) => `🎨 Ho appena scoperto ${name} su @opendesignai — l'alternativa open-source a Claude Design.
✨ Local-first · BYOK · il tuo agente progetta.
→ ${url}`,
vi: ({ name, description, url }) => `🎨 Vừa khám phá ${name} trên @opendesignai — giải pháp mã nguồn mở thay thế Claude Design.
✨ Ưu tiên local · BYOK · agent của bạn thiết kế.
→ ${url}`,
pl: ({ name, description, url }) => `🎨 Właśnie odkryłem ${name} na @opendesignai — open-source'ową alternatywę dla Claude Design.
✨ Local-first · BYOK · twój agent projektuje.
→ ${url}`,
id: ({ name, description, url }) => `🎨 Baru nemu ${name} di @opendesignai — alternatif open-source untuk Claude Design.
✨ Local-first · BYOK · agent kamu yang nge-desain.
→ ${url}`,
nl: ({ name, description, url }) => `🎨 Net ontdekt: ${name} op @opendesignai — het open-source alternatief voor Claude Design.
✨ Local-first · BYOK · jouw agent ontwerpt.
→ ${url}`,
ar: ({ name, description, url }) => `🎨 اكتشفت للتو ${name} على @opendesignai — البديل مفتوح المصدر لـ Claude Design.
✨ محلي أولًا · BYOK · وكيلك يصمّم.
→ ${url}`,
tr: ({ name, description, url }) => `🎨 Yeni keşfettim: ${name} (@opendesignai) — Claude Design'a açık kaynaklı alternatif.
✨ Local-first · BYOK · ajanın tasarlıyor.
→ ${url}`,
uk: ({ name, description, url }) => `🎨 Щойно знайшов ${name} на @opendesignai — open-source альтернативу Claude Design.
✨ Local-first · BYOK · ваш агент робить дизайн.
→ ${url}`,
};
export async function getStaticPaths() {
const skills = await getSkillRecords();
return skills.map((skill) => ({
params: { slug: skill.slug },
props: { skill, all: skills },
}));
}
interface Props {
skill: SkillRecord;
all: ReadonlyArray<SkillRecord>;
}
const { skill: routeSkill, all: routeAll } = Astro.props as Props;
const locale = localeFromPath(Astro.url.pathname);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const all = locale === 'en' ? routeAll : await getSkillRecords(locale);
const skill = all.find((item) => item.slug === routeSkill.slug) ?? routeSkill;
const title = ui.catalog.skills.detailTitle(skill.name);
const description = skill.description.length > 0
? skill.description
: ui.catalog.skills.detailFallbackDescription(skill.name);
const skillUrl = `https://open-design.ai/skills/${skill.slug}/`;
const shareCopy = (SHARE_COPY[locale] ?? SHARE_COPY.en)({
name: skill.name,
description,
url: skillUrl,
});
// Share-dialog UI strings localized inline. Keeping them next to the
// page that uses them avoids growing the global UI bundle for what's
// effectively four short labels per locale.
const SHARE_UI: Record<LandingLocaleCode, { title: string; lead: string; copyText: string; copyLink: string; jumpTo: string; openLabel: string }> = {
en: { title: 'Share this skill', lead: 'Copy the message below, then jump to the platform you want to share on and paste.', copyText: 'Copy text', copyLink: 'Copy link only', jumpTo: 'Then jump to:', openLabel: 'Share ↗' },
zh: { title: '分享这个 skill', lead: '复制下面的文案,然后跳到你想分享的平台粘贴即可。', copyText: '复制文案', copyLink: '只复制链接', jumpTo: '跳转到:', openLabel: '分享 ↗' },
'zh-tw': { title: '分享這個 skill', lead: '複製下面的文案,然後跳到你想分享的平台貼上即可。', copyText: '複製文案', copyLink: '只複製連結', jumpTo: '跳轉到:', openLabel: '分享 ↗' },
ja: { title: 'この skill を共有', lead: '下のメッセージをコピーしてから、共有したいプラットフォームに移動して貼り付けてください。', copyText: 'テキストをコピー', copyLink: 'リンクのみコピー', jumpTo: 'プラットフォームへ:', openLabel: '共有 ↗' },
ko: { title: '이 skill 공유', lead: '아래 메시지를 복사한 다음 공유할 플랫폼으로 이동해 붙여넣으세요.', copyText: '텍스트 복사', copyLink: '링크만 복사', jumpTo: '플랫폼으로:', openLabel: '공유 ↗' },
de: { title: 'Diesen Skill teilen', lead: 'Kopiere die Nachricht unten und füge sie auf der gewünschten Plattform ein.', copyText: 'Text kopieren', copyLink: 'Nur Link kopieren', jumpTo: 'Zur Plattform:', openLabel: 'Teilen ↗' },
fr: { title: 'Partager ce skill', lead: 'Copiez le message ci-dessous, puis ouvrez la plateforme de votre choix et collez.', copyText: 'Copier le texte', copyLink: 'Copier le lien', jumpTo: 'Aller sur :', openLabel: 'Partager ↗' },
ru: { title: 'Поделиться скиллом', lead: 'Скопируйте сообщение ниже, затем перейдите на нужную платформу и вставьте.', copyText: 'Скопировать текст', copyLink: 'Только ссылка', jumpTo: 'Перейти:', openLabel: 'Поделиться ↗' },
es: { title: 'Compartir este skill', lead: 'Copia el mensaje y abre la plataforma donde quieras compartirlo.', copyText: 'Copiar texto', copyLink: 'Solo el enlace', jumpTo: 'Ir a:', openLabel: 'Compartir ↗' },
'pt-br': { title: 'Compartilhar skill', lead: 'Copie a mensagem e abra a plataforma onde quer compartilhar.', copyText: 'Copiar texto', copyLink: 'Só o link', jumpTo: 'Ir para:', openLabel: 'Compartilhar ↗' },
it: { title: 'Condividi lo skill', lead: 'Copia il messaggio e apri la piattaforma su cui vuoi condividere.', copyText: 'Copia testo', copyLink: 'Solo il link', jumpTo: 'Vai a:', openLabel: 'Condividi ↗' },
vi: { title: 'Chia sẻ skill', lead: 'Sao chép nội dung dưới đây, rồi mở nền tảng bạn muốn chia sẻ và dán vào.', copyText: 'Sao chép', copyLink: 'Chỉ sao chép link', jumpTo: 'Mở:', openLabel: 'Chia sẻ ↗' },
pl: { title: 'Udostępnij ten skill', lead: 'Skopiuj wiadomość poniżej, otwórz wybraną platformę i wklej.', copyText: 'Kopiuj tekst', copyLink: 'Skopiuj link', jumpTo: 'Przejdź do:', openLabel: 'Udostępnij ↗' },
id: { title: 'Bagikan skill ini', lead: 'Salin pesan di bawah, lalu buka platform yang ingin Anda gunakan dan tempel.', copyText: 'Salin teks', copyLink: 'Salin tautan', jumpTo: 'Buka:', openLabel: 'Bagikan ↗' },
nl: { title: 'Deel deze skill', lead: 'Kopieer het bericht hieronder en plak het op het platform van jouw keuze.', copyText: 'Tekst kopiëren', copyLink: 'Alleen de link', jumpTo: 'Ga naar:', openLabel: 'Delen ↗' },
ar: { title: 'شارك هذه المهارة', lead: 'انسخ الرسالة أدناه، ثم انتقل إلى المنصة التي تريد المشاركة عليها والصقها.', copyText: 'انسخ النص', copyLink: 'انسخ الرابط فقط', jumpTo: 'انتقل إلى:', openLabel: 'مشاركة ↗' },
tr: { title: 'Bu skilli paylaş', lead: 'Aşağıdaki mesajı kopyala, dilediğin platformu açıp yapıştır.', copyText: 'Metni kopyala', copyLink: 'Sadece linki kopyala', jumpTo: 'Şuraya git:', openLabel: 'Paylaş ↗' },
uk: { title: 'Поділитись скілом', lead: 'Скопіюйте повідомлення нижче, потім перейдіть на платформу й вставте.', copyText: 'Копіювати текст', copyLink: 'Тільки посилання', jumpTo: 'Перейти:', openLabel: 'Поділитись ↗' },
};
const shareUi = SHARE_UI[locale] ?? SHARE_UI.en;
const related = all
.filter((s) => s.slug !== skill.slug)
.filter((s) => s.mode === skill.mode || s.scenario === skill.scenario)
.slice(0, 4);
/*
* Instruction skills don't have a runnable demo to iframe — to avoid
* a near-empty detail page, render the SKILL.md prose inline so the
* page reads like a brief. Template skills keep the page deliberately
* brief because their demo is the content; their full SKILL.md is one
* "Find on GitHub" click away.
*
* Astro 6 exposes the markdown pipeline through a top-level
* `render(entry)` helper rather than the legacy `entry.render()`
* method. The output (heading anchors, smart-typography, GFM
* tables) styles cleanly with the existing `.detail-md` rules.
*/
const skillEntry =
skill.kind === 'instruction' ? await getEntry('skills', `${skill.slug}/SKILL`) : null;
const SkillBody = skillEntry ? (await render(skillEntry)).Content : null;
const jsonLd = [
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: Astro.site?.toString() },
{ '@type': 'ListItem', position: 2, name: ui.catalog.skills.detailLabel, item: new URL('/skills/', Astro.site).toString() },
{ '@type': 'ListItem', position: 3, name: skill.name, item: new URL(`/skills/${skill.slug}/`, Astro.site).toString() },
],
},
{
'@context': 'https://schema.org',
'@type': 'SoftwareSourceCode',
name: skill.name,
description,
codeRepository: skill.source,
programmingLanguage: 'Markdown',
keywords: skill.triggers.join(', '),
license: 'https://www.apache.org/licenses/LICENSE-2.0',
},
];
---
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
<span>/</span>
<span aria-current="page">{skill.name}</span>
</nav>
<article class="detail">
<header class="detail-head">
<span class="label">
{ui.catalog.skills.detailLabel}
{typeof skill.featured === 'number' && (
<span class="ix">{ui.catalog.skills.featuredNumber(String(skill.featured).padStart(2, '0'))}</span>
)}
</span>
<h1 class="display">{skill.name}<span class="dot">.</span></h1>
<p class="lead">{description}</p>
<div class="detail-actions">
{/*
Two primary CTAs. "Use this skill" v1 sends users to the OD
desktop release page — install the app first, then run the
skill. Routing here rather than to /quickstart/ keeps the
flow concrete (download a binary now) instead of asking
users to read an install doc. Once the desktop client
exposes a registered URL scheme, this anchor flips to a
JS-driven `od://skill/<slug>` try + fallback without
changing the page surface.
*/}
<a
class="btn btn-primary"
href="https://github.com/nexu-io/open-design/releases"
target="_blank"
rel="noopener"
>
Use this skill →
</a>
<a
class="btn btn-ghost"
href={skill.source}
target="_blank"
rel="noopener"
>
Find on GitHub →
</a>
{skill.upstream && (
<a class="btn btn-ghost" href={skill.upstream} target="_blank" rel="noopener">
{ui.catalog.skills.upstream}
</a>
)}
<button
type="button"
class="btn btn-ghost detail-share-trigger"
data-share-open={`skill:${skill.slug}`}
>
{shareUi.openLabel}
</button>
</div>
</header>
{skill.kind === 'template' && skill.previewUrl && (
<figure class="detail-preview">
{/*
Click-to-expand interactive preview. Only template-kind skills
ship a runnable example.html, so this block is gated on kind
rather than just `previewUrl` — instruction skills now have a
synthesized cover thumbnail too, but no iframe target. The
thumb is the summary of a `<details>` element: clicking opens
the live iframe, replacing the thumb with the canonical
`<slug>/example.html` rendered inside a sandboxed frame.
*/}
<details class="detail-preview-live">
<summary class="detail-preview-thumb-trigger" aria-label={`Open interactive preview for ${skill.name}`}>
<LazyImg
src={skill.previewUrl}
alt={`${skill.name} example output`}
loading="priority"
/>
<span class="detail-preview-thumb-overlay" aria-hidden="true">
<span class="detail-preview-thumb-cta">Click for live preview ↗</span>
</span>
</summary>
<div class="detail-preview-frame-wrap">
<iframe
src={`/skills/${skill.slug}/example.html`}
title={`${skill.name} interactive preview`}
loading="lazy"
sandbox="allow-scripts allow-same-origin"
class="detail-preview-frame"
/>
<a
class="detail-preview-popout"
href={`/skills/${skill.slug}/example.html`}
target="_blank"
rel="noopener"
aria-label="Open preview in new tab"
>
Open in new tab ↗
</a>
</div>
</details>
<figcaption>
{ui.catalog.skills.previewCaption(skill.slug)}
</figcaption>
</figure>
)}
{/*
Share modal — opens a `<dialog>` containing the canonical share
copy (with the brand keyword "open-source Claude Design
alternative" baked in), a one-click "Copy" button, and a row of
platform jump buttons. Each platform button just opens the
vendor's compose URL — the user pastes the already-copied text.
This works around a real cross-platform pain point: LinkedIn /
Facebook ignore pre-fill `text` params, X has length limits that
truncate Chinese content unpredictably, and Reddit's title param
survives but title-only is a weak signal. Copy-then-paste is
uniformly reliable.
The trigger sits inside `.detail-actions` instead of as a
separate row below `.detail-meta` so it has visual weight equal
to the primary CTAs. Joey called this out specifically.
*/}
<dialog
class="detail-share-dialog"
data-share-dialog={`skill:${skill.slug}`}
>
<form method="dialog" class="detail-share-dialog-form">
<header class="detail-share-dialog-head">
<h2>{shareUi.title}</h2>
<button type="submit" class="detail-share-dialog-close" aria-label="Close" value="cancel">×</button>
</header>
<p class="detail-share-dialog-lead">{shareUi.lead}</p>
<textarea
class="detail-share-dialog-text"
readonly
rows="6"
data-share-text
>{shareCopy}</textarea>
<div class="detail-share-dialog-actions">
<button
type="button"
class="btn btn-primary detail-share-dialog-copy"
data-share-copy
>
{shareUi.copyText}
</button>
<button
type="button"
class="btn btn-ghost detail-share-dialog-copy-link"
data-copy-link={skillUrl}
>
{shareUi.copyLink}
</button>
</div>
{/*
Platform jump buttons — official brand logos rendered as
inline SVG (no third-party icon font, no client JS). Each
opens the vendor's compose surface in a new tab; the user
pastes the already-copied text. Email channel was dropped
per Joey's revision; the four channels here cover the
highest-value SEO + virality surfaces.
*/}
<div class="detail-share-dialog-platforms">
<span class="detail-share-dialog-platforms-label">{shareUi.jumpTo}</span>
<a class="detail-share-platform-btn" href="https://x.com/compose/post" target="_blank" rel="noopener" aria-label="X">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.65l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25h6.815l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"/></svg>
<span class="sr-only">X</span>
</a>
<a class="detail-share-platform-btn" href="https://www.linkedin.com/feed/?shareActive=true" target="_blank" rel="noopener" aria-label="LinkedIn">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
<span class="sr-only">LinkedIn</span>
</a>
<a class="detail-share-platform-btn" href="https://www.reddit.com/submit" target="_blank" rel="noopener" aria-label="Reddit">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 01-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 01.042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 014.028 12.3c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 01.14-.197.35.35 0 01.238-.042l2.906.617a1.214 1.214 0 011.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 00-.231.094.33.33 0 000 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 00.029-.463.33.33 0 00-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 00-.232-.095z"/></svg>
<span class="sr-only">Reddit</span>
</a>
<a class="detail-share-platform-btn" href="https://www.facebook.com/" target="_blank" rel="noopener" aria-label="Facebook">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
<span class="sr-only">Facebook</span>
</a>
</div>
</form>
</dialog>
<dl class="detail-meta">
{skill.mode && (
<Fragment>
<dt>{ui.catalog.skills.mode}</dt>
<dd>{skill.modeLabel ?? skill.mode}</dd>
</Fragment>
)}
{skill.scenario && (
<Fragment>
<dt>{ui.catalog.skills.scenario}</dt>
<dd>{skill.scenarioLabel ?? skill.scenario}</dd>
</Fragment>
)}
{skill.platform && (
<Fragment>
<dt>{ui.catalog.skills.platform}</dt>
<dd>{skill.platformLabel ?? skill.platform}</dd>
</Fragment>
)}
{skill.category && (
<Fragment>
<dt>{ui.catalog.systems.category}</dt>
<dd>{skill.categoryLabel ?? skill.category}</dd>
</Fragment>
)}
</dl>
{skill.triggers.length > 0 && (
<section class="detail-block">
<h2>{ui.catalog.skills.triggers}</h2>
<p class="block-lead">
{ui.catalog.skills.triggersLead}
</p>
<ul class="trigger-list">
{skill.triggers.map((t) => <li><code>{t}</code></li>)}
</ul>
</section>
)}
{skill.examplePrompt && (
<section class="detail-block">
<h2>{ui.catalog.skills.examplePrompt}</h2>
<pre class="example-prompt">{skill.examplePrompt}</pre>
</section>
)}
{SkillBody && (
<section class="detail-block detail-md">
<h2>About this skill</h2>
<SkillBody />
</section>
)}
{related.length > 0 && (
<section class="detail-block">
<h2>{ui.catalog.skills.related}</h2>
<ul class="related-grid">
{related.map((r) => (
<li>
<a href={href(`/skills/${r.slug}/`)}>
<span class="related-name">{r.name}</span>
<span class="related-desc">{r.description}</span>
<span class="related-meta">
{r.modeLabel && <span class="meta-tag">{r.modeLabel}</span>}
{r.scenarioLabel && <span class="meta-tag muted">{r.scenarioLabel}</span>}
</span>
</a>
</li>
))}
</ul>
</section>
)}
</article>
</Layout>