mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(landing-page): YouMind-style grid + share popover for /plugins/templates/ The list-style catalog rows that landed in PR #3010 read as a long table of items rather than a discoverable grid. Product feedback (after benchmarking against youmind.com/zh-CN/seedance-2-0-prompts) wanted: - A YouMind-shape card with a top accent band, video / poster preview area, author + attribution row, an excerpt frame, and a primary CTA paired with a share button. - Hover-autoplay on the 46 video templates whose manifest carries a Cloudflare Stream MP4. The data was already there since PR #3010; the catalog row just rendered the poster as a static `<img>`. - A counter chip on the right of the hero that surfaces the live total (`Total · 231`) instead of baking the number into the H1 ("231 runnable templates."). The hero now reads as `OPEN SOURCE CLAUDE DESIGN` eyebrow + `Templates.` static H1, which also threads the brand keyword into the page's SEO surface. - A six-question FAQ block below the grid covering license, BYOK keys, contribution, and the "open source Claude Design alternative" positioning explicitly. Implementation: - `_components/template-card.astro` — new card component. Accent band hue is derived from `od.mode` so artifacts of the same kind get a consistent color (video green, prototype blue, deck mustard, image wisteria, hyperframes coral, audio amber, live-artifact teal), falling back to a stable per-index hue for unrecognized modes. Featured tag (yellow, on-brand) is visible when the manifest tag list contains `featured`; the rest of the card is locale-resolved via the same `resolveBundledTitle` / `resolveBundledDescription` helpers PR #3010 added. - `pages/plugins/templates/index.astro` + `[kind]/index.astro` — grid layout (`.tpl-grid`, `repeat(auto-fill, minmax(340px, 1fr))`), hero with counter chip, FAQ section on the parent only. Adjacent filter strips share a single divider rather than drawing one each, so the kind + scene chip block reads as one filter unit instead of three stacked horizontal cuts. - Hover-autoplay observer + share button click handler bundled into one `<script>` per page so they share the same boot lifecycle. The earlier split version dispatched `astro:page-load` from the autoplay block before the share block's listener attached, which dropped the share click on the floor; the merged init() runs eagerly when DOM is ready, re-runs idempotently on `astro:page-load` (Astro view transition), and uses `data-tpl-init` / `data-tpl-share-bound` markers to prevent double-binding. - Card share is a popover, not a system share sheet. The detail page's `<dialog class="detail-share-dialog">` UI is reused (single instance per page populated per click), but `<dialog>.show()` runs in non-modal mode and JS positions it via `getBoundingClientRect()` to unfold above-right of the trigger button. Outside-click and Escape close the popover; the existing `data-share-copy` / `data-copy-link` handlers in `header-enhancer.astro` wire Copy text + Copy link automatically. Width tuned to 420px so it fits next to a 340px-wide card without spilling onto the next column. - `_redirects` already covers retired Skills + Craft routes (PR #3010) so this grid pivot doesn't need new redirects. Out of scope for this PR (kept lean): - Multi-locale hero + FAQ copy. Hero / FAQ render in English on every locale right now; the `pcopy.tileTemplates` chip rail and per-card title/description still localize per PR #3010. Locale rollout for the hero + FAQ is a follow-up. - Sort + filter buttons in the YouMind reference top-right (we still show artifact-kind chips only). Sort by featured weight is the most likely next step. - `od.featured` weight as a featured proxy. We currently key off `tags?.includes('featured')` which is 0-match across the catalog today; promoting the numeric weight into `BundledPluginRecord` is a separate small commit. `pnpm --filter @open-design/landing-page typecheck` clean (0 errors). * feat(landing-page): localize templates chrome + FAQPage JSON-LD + hover-only autoplay Three follow-ups Looper flagged on the YouMind-style grid (PR #3185): - **Localizable hero / FAQ / card chrome.** PR #3185 wired the grid through `pcopy` for record titles + descriptions but hard-coded the surrounding chrome — hero eyebrow / lead / counter label, FAQ head, Featured tag, "Read full prompt", "Use this template", and the share-button `aria-label` — to English. `/ja/plugins/templates/`, `/zh-CN/plugins/templates/video/`, etc. now ship those strings via `pcopy.*` keys (`templatesHeroEyebrow`, `templatesHeroLead`, `templatesCounterLabel`, `cardFeaturedTag`, `cardReadFullPrompt`, `cardUseTemplate`, `cardShareAria`, `faqHead`, `faqItems`). English is the base; per-locale overrides for hero copy + 6 FAQ Q&A pairs remain a follow-up (the PR-#3185 "Out of scope" item), so the 17 non-English locales fall back to English chrome instead of showing undefined values. - **`FAQPage` JSON-LD entity.** The visible accordion was a SEO surface but `jsonLd` was still a single `CollectionPage`. Switched it to an array and appended a `FAQPage` whose `mainEntity` is each question + answer from `pcopy.faqItems`, so the structured-data payload search engines see and the visible <details> share one source of truth — drift between them is now mechanical, not editorial. - **Hover-only autoplay (not viewport autoplay).** The previous observer played every video the moment its card scrolled into the viewport, which contradicted the PR's stated hover-autoplay contract and spawned N simultaneous decoders on a casual scroll. The IntersectionObserver now hydrates `data-src` -> `src` lazily (one-shot, then unobserve) at a 300px rootMargin; `play()` and `pause()` are gated to `pointerenter` / `pointerleave` (plus `focusin` / `focusout` for keyboard users) on the parent `.tpl-media` host so hovering anywhere on the preview frame triggers playback. Same change applied to the `[kind]` route so faceted pages behave identically. Validation: pnpm --filter @open-design/landing-page typecheck -> 0 errors / 0 warnings; local dev (port 3061) renders 231 cards / 46 data-tpl-autoplay markers / FAQPage entity present in jsonLd / 6 FAQ summaries; zh-CN locale falls back to English chrome (expected, the locale routes themselves remain out of scope per PR #3185). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Joey-nexu <joeylee12629@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
7.5 KiB
Text
182 lines
7.5 KiB
Text
---
|
|
/*
|
|
* Card-style row used by `/plugins/templates/` (and its kind-scoped
|
|
* facets) — the YouMind-inspired layout product called for after PR
|
|
* #3010 shipped:
|
|
*
|
|
* ┌──────────────────────────┐
|
|
* │ ▆ accent band │
|
|
* │ [Featured] │ ← optional, top-anchored
|
|
* │ ┌──────────────────────┐ │
|
|
* │ │ 16:9 video / poster │ │ ← hover-autoplay when video
|
|
* │ └──────────────────────┘ │
|
|
* │ @author DATE │
|
|
* │ ┌──────────────────────┐ │
|
|
* │ │ Read full prompt → │ │
|
|
* │ │ <description clamp> │ │
|
|
* │ └──────────────────────┘ │
|
|
* │ [ Use this template ] [⤴] │
|
|
* └──────────────────────────┘
|
|
*
|
|
* The legacy `plugin-row.astro` row component still backs
|
|
* `/plugins/skills/` and other list-style routes; this file is
|
|
* intentionally separate so a future refactor can split the data
|
|
* shapes cleanly. For now it accepts the same `BundledPluginRecord`
|
|
* shape that the templates page already iterates over.
|
|
*
|
|
* Hover-autoplay is wired here via a small inline script per page —
|
|
* one IntersectionObserver across all `<video data-tpl-autoplay>`
|
|
* elements pauses anything outside the viewport so we don't spawn
|
|
* 200 simultaneous decoders on a long grid.
|
|
*/
|
|
import {
|
|
resolveBundledDescription,
|
|
resolveBundledTitle,
|
|
type BundledPluginRecord,
|
|
} from '../_lib/bundled-plugins';
|
|
import { localeFromPath, localizedHref } from '../i18n';
|
|
import { localizeTaxonomyValue } from '../content-i18n';
|
|
import { getPluginsCopy } from '../_lib/plugins-i18n';
|
|
|
|
export interface Props {
|
|
record: BundledPluginRecord;
|
|
/** Index used to derive a stable accent-band hue. */
|
|
index: number;
|
|
/** Force-mark the card as featured (e.g. top-N from `od.featured`). */
|
|
featured?: boolean;
|
|
/**
|
|
* Subcategory slug used by the per-kind page's client-side scene
|
|
* filter to toggle visibility — same contract `plugin-row.astro`
|
|
* exposes so the existing filter script keeps working when the row
|
|
* is rendered as a card.
|
|
*/
|
|
dataScene?: string;
|
|
}
|
|
|
|
const { record, index, featured = false, dataScene } = Astro.props;
|
|
const locale = localeFromPath(Astro.url.pathname);
|
|
const href = (path: string) => localizedHref(path, locale);
|
|
|
|
const name = resolveBundledTitle(record, locale);
|
|
const description = resolveBundledDescription(record, locale);
|
|
const detailHref = href(record.detailHref);
|
|
|
|
/*
|
|
* Share text — bake the localized share template into a `data-share-text`
|
|
* attribute on the share button so the page-level click handler can
|
|
* surface it via the Web Share API on mobile or the clipboard on desktop
|
|
* without each card needing its own dialog. The detail page still ships
|
|
* a full `<dialog>` for the platform jump-to grid; cards on the catalog
|
|
* surface get the lighter affordance.
|
|
*/
|
|
const pcopy = getPluginsCopy(locale);
|
|
const shareUrl = new URL(detailHref, 'https://open-design.ai').toString();
|
|
const shareText = pcopy.shareTemplate({ title: name, url: shareUrl });
|
|
|
|
/*
|
|
* Accent band — derived from the plugin's `mode` so the same kind of
|
|
* artifact gets the same hue across the grid (videos green, prototypes
|
|
* blue, etc.). Falls back to a stable per-index hue rotation so cards
|
|
* without a recognized mode still get a band rather than disappearing
|
|
* visually. The seven-slot palette mirrors YouMind's color-coded
|
|
* top-band pattern but uses our paper-tone-friendly hues.
|
|
*/
|
|
const ACCENT_BY_MODE: Record<string, string> = {
|
|
video: '#7fb46a', // mossy green
|
|
prototype: '#5b8db8', // dusty blue
|
|
deck: '#e9b94a', // mustard (matches token)
|
|
image: '#a285c2', // wisteria
|
|
hyperframes: '#d97373', // coral-rose
|
|
audio: '#d28d3f', // amber
|
|
'live-artifact': '#5fb0a8', // teal
|
|
'design-system': '#7a8857', // olive (matches token's --olive ish)
|
|
};
|
|
const PALETTE = ['#7fb46a', '#5b8db8', '#e9b94a', '#a285c2', '#d97373', '#d28d3f', '#5fb0a8', '#7a8857'];
|
|
const accent = (record.mode && ACCENT_BY_MODE[record.mode]) ?? PALETTE[index % PALETTE.length];
|
|
|
|
/*
|
|
* Author + date strip. Author surfaces the manifest's `author.name`
|
|
* with a leading `@` per YouMind convention; first-party manifests
|
|
* read as `@open-design`. Date is intentionally absent — bundled
|
|
* manifests don't carry a `published_at` and inferring it from git
|
|
* mtime adds build-time complexity for a soft signal. We render
|
|
* a paper-toned `Open Design` attribution badge in the date slot
|
|
* instead so the row still balances visually.
|
|
*/
|
|
const authorRaw = record.authorName ?? 'Open Design';
|
|
const authorHandle = `@${authorRaw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
|
|
|
|
/*
|
|
* Localized chip labels. The catalog row's prior chip rail leaked
|
|
* raw English mode/scenario slugs on un-localized rows; the same
|
|
* fallback rule applies here. When `localizeTaxonomyValue` returns
|
|
* undefined the chip drops rather than showing a kebab slug.
|
|
*/
|
|
const modeLabel = localizeTaxonomyValue(record.mode, locale);
|
|
const scenarioLabel = localizeTaxonomyValue(record.scenario, locale);
|
|
|
|
const isVideoPreview = record.previewType === 'video' && record.previewVideo;
|
|
---
|
|
|
|
<article class={`tpl-card${featured ? ' tpl-card-featured' : ''}`} data-mode={record.mode ?? 'misc'} data-scene={dataScene}>
|
|
<span class="tpl-band" aria-hidden="true" style={`background:${accent}`}></span>
|
|
{featured && <span class="tpl-featured-tag">{pcopy.cardFeaturedTag}</span>}
|
|
|
|
<a class="tpl-media" href={detailHref} aria-label={name}>
|
|
{isVideoPreview ? (
|
|
<video
|
|
class="tpl-media-video"
|
|
muted
|
|
loop
|
|
playsinline
|
|
preload="none"
|
|
poster={record.previewPoster}
|
|
data-tpl-autoplay
|
|
data-src={record.previewVideo}
|
|
/>
|
|
) : record.previewPoster ? (
|
|
<img
|
|
class="tpl-media-poster"
|
|
src={record.previewPoster}
|
|
alt={pcopy.previewImageAlt(name)}
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
) : (
|
|
<span class="tpl-media-empty" aria-hidden="true" />
|
|
)}
|
|
{modeLabel && <span class="tpl-media-kind">{modeLabel}</span>}
|
|
</a>
|
|
|
|
<div class="tpl-meta">
|
|
<span class="tpl-author">{authorHandle}</span>
|
|
<span class="tpl-meta-date">Open Design</span>
|
|
</div>
|
|
|
|
<a class="tpl-excerpt" href={detailHref}>
|
|
<span class="tpl-excerpt-head">{pcopy.cardReadFullPrompt}</span>
|
|
<h3 class="tpl-excerpt-title">{name}</h3>
|
|
<p class="tpl-excerpt-body">{description}</p>
|
|
</a>
|
|
|
|
<div class="tpl-actions">
|
|
<a class="tpl-cta" href={detailHref}>{pcopy.cardUseTemplate}</a>
|
|
<button
|
|
type="button"
|
|
class="tpl-share"
|
|
data-tpl-card-share
|
|
data-share-text={shareText}
|
|
data-share-url={shareUrl}
|
|
data-share-title={name}
|
|
aria-label={pcopy.cardShareAria(name)}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<circle cx="18" cy="5" r="3" />
|
|
<circle cx="6" cy="12" r="3" />
|
|
<circle cx="18" cy="19" r="3" />
|
|
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
|
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</article>
|