Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ These exact strings. Do not paraphrase them per page.
| Asset | String | Where it goes |
| --- | --- | --- |
| Name | `skilld` lowercase by default, `Skilld` only at a sentence start, `skilld.dev` for the domain. Never SKILLD, Skill'd, Skill-d, SkillD. | guidance only (applies everywhere, not one placement) |
| Tagline | Curated agent skills by humans. | index OG image alt |
| Tagline | Curated agent skills by humans. | guidance only: the five-word product summary. The homepage and its OG card use the Home H1 instead |
| Site description | Curated agent skills by humans, written by real maintainers in their own GitHub repos | `nuxt.config.ts` site description (feeds every page's meta description) |
| Elevator pitch | Skilld is a curated registry of agent skills written by real people in the GitHub repos you already depend on. One install command, every agent; watch your stack and get a digest when it changes. | guidance only: no page ships it yet (use it where two sentences fit) |
| Home H1 | Agent skills for you and your agent | guidance only: the homepage H1. The page draws the rose brand dot as its full stop, so never type a period |
| Home H1 | Agent skills for you and your agent | guidance only: the homepage H1, and the title of the homepage OG card. The page draws the rose brand dot as its full stop, so never type a period. The card types no full stop either, because its lockup dot is its one rose element. It breaks the line after "for you", as the page does. Its alt text is "skilld, agent skills for you and your agent" |
| Home verb line | search Β· run Β· install Β· keep current | guidance only: the homepage line under the H1. Each verb links to its `/cli` section: `#search`, `#run`, `#install`, `#update` |
| Home CLI link | The skilld CLI β†’ | guidance only: the homepage link to `/cli` at the end of the verb line. The arrow is an icon |
| Home caption | Try any skill before you install it. Your agent can search for its own. | guidance only: the homepage caption under the verb line. It assumes the reader knows what a Skill is, so it names only what sets skilld apart. "Search for its own" is the skilld Skill behind the promo label |
| Home caption | Try any skill before you install it. Your agent can search for its own. | guidance only: the homepage caption under the verb line. It assumes the reader knows what a Skill is, so it names only what sets skilld apart. "Search for its own" is the skilld Skill behind the promo label. The homepage OG card uses it as its description |
| Home claims line | Open-source CLI, no telemetry Β· Analytics without cookies or IPs Β· A skills.sh alternative | guidance only: the homepage claims line under the search. The third item links to `/vs/skills-sh`, and the promo label closes the line |
| Promo label | Teach your agent skilld | guidance only: the last item of the homepage claims line. It opens a popover with `npx skilld install skilld --global` |
| Step 01: find | Find Β· Stay hyped. A curated registry, plus what devs talk about on X and Bluesky. | guidance only: the first step of the homepage lifecycle band. The step name comes first, then the pitch line, then the description. Links `/skills/trending`. The braille spark motif pairs with the pitch line |
Expand Down
15 changes: 8 additions & 7 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ Three textures replace the noise field. Each has one job, and none appears outsi
| --- | --- | --- |
| **Braille names** | Hero, OG cards, CLI | Rows of real Skill names in braille. Far from the rose dot they thin to sparse dots. Near it they resolve into letters. Each hop of the pick sends three rings outward, half a second apart. Every cell is a real character, so the same field prints in a terminal |
| **Converge** | Dividers and loading | Scattered dots fall into one line and end on a rose dot. Many Skills in, one out. It reads as progress |
| **File minimap** | Skill pages and per-Skill OG cards | Each Skill as its own `SKILL.md`: frontmatter, headings, lists and code drawn as rows of dots. The rose dot is the cursor on the last changed line |
| **File minimap** | Skill pages | Each Skill as its own `SKILL.md`: frontmatter, headings, lists and code drawn as rows of dots. The rose dot is the cursor on the last changed line |

Rules for every texture:

Expand All @@ -332,14 +332,14 @@ Rules for every texture:
5. **One per region.** No other motion runs in the same viewport region as a moving texture.
6. **Converge is the loader.** At small sizes the file minimap looks like a skeleton loader. Do not use it as one.

**On OG cards.** An OG card is one still frame, drawn server side as SVG dots. The OG renderer has only Plus Jakarta Sans and IBM Plex Mono, and neither has braille glyphs, so a card never relies on a glyph or a canvas. The lockup's dot is the card's one rose element. Textures on a card draw stone only, or use the lockup's dot as their focal point. `app/utils/og-texture.ts` draws them.
**On OG cards.** An OG card is the homepage hero as one still frame, drawn server side. The OG renderer has only Plus Jakarta Sans and IBM Plex Mono, and neither has braille glyphs, so the texture is SVG dots, never a glyph or a canvas. `OgLayout` draws the frame for every card: the lockup centred at the top, the content centred under it, and Braille names in the margins. The field resolves toward the lockup's dot, which is the card's one rose element. The title takes the hero H1's face, Plus Jakarta Sans 600 at βˆ’0.045em. A Skill name has no space to wrap at, so it shrinks to fit one line. Text that needs two lines breaks into two of even length, since the renderer's `text-wrap: balance` pushes the lines off centre. `app/utils/og-texture.ts` draws the dots, `app/utils/og-style.ts` holds the type, and `OgLines` draws the even lines.

| Card | Texture |
| Card | Content |
| --- | --- |
| Splash (`/`) | Braille names. The focal point is the lockup's dot, so the noise resolves into the wordmark |
| Skill | File minimap down the right edge, faded in from the left. Each Skill seeds its own files from its owner, repository and name |
| Make a skill | One `SKILL.md` drawn large from its frontmatter down |
| Page, Collection, Curator | None |
| Page | Title and description. `/` uses it with the hero H1 and caption |
| Skill | Name, then owner, then the curator note or the curator count |
| Collection | Name, description, curator, then the curator note or the first Skills |
| Curator | Avatar, name, handle with counts, description, then the first Skills |

### Motifs

Expand Down Expand Up @@ -455,6 +455,7 @@ These are editorial sets, not necessarily database collections yet. If a set has
- **One geometry feeds every placement**: `shared/brand-mark.ts` holds the coordinates and the outlined wordmark. `AppLogo`, the OG lockup and every file in `public/` come from it, and `pnpm brand:assets` writes the files. The old chevron lived in hand-written copies, and their rose shades and wordmark weights had drifted apart. Confirmed 2026-10-06.
- **Three textures replace the noise field**: Braille names for the hero, OG cards and the CLI; Converge for dividers and loading; File minimap for Skill pages and per-Skill OG cards. One job each, so no texture becomes wallpaper. Confirmed 2026-10-06.
- **The lockup dot is an OG card's one rose element**: the 4px rose top bar, the rose quote rails and the rose avatar ring went stone. On the Splash card, the braille field radiates from the lockup's dot, so the texture spends no second rose. Confirmed 2026-10-06.
- **OG cards repeat the homepage hero**: every card centres the lockup, a Plus Jakarta Sans title and its detail, with Braille names in the margins. Harlan chose this over a left-aligned editorial card and a card that leads with the run command. It supersedes the per-card textures above: the Skill card's file minimap, the Make a skill card's large `SKILL.md`, and the separate Splash and Make a skill templates are gone. Confirmed 2026-10-06.
- **The braille spark replaces the trending flame**: seven braille bars for seven days of mentions, today in rose. The emoji exception for Trending is retired. The spark keeps the sourced mention count beside it, which the flame never carried. Confirmed 2026-10-06.
- **Run and install read as different materials**: run is a dashed border with a hollow dot, because nothing stays; install is a solid border with a filled dot, because files land. Run is preselected. Confirmed 2026-10-06.
- **Trending shares the /skills shell**: `/skills/trending` uses the full-width layout from `/skills`. Its heading replaces `CompactPageHeader` and lines up with the results column. A sticky 14rem sidebar holds the range links, then the weekly CTA and the skilld install. Nothing sits right of the board, so the board takes every column the sidebar leaves. Rows switch on their own width, and the post carousel shows two cards where it has room. On narrow screens the ranges are a compact row above the board, and the CTA stays after the fifth row. Harlan asked for this on 2026-10-06. It supersedes the 2026-10-01 call that dropped the 9/3 sidebar for taking a quarter of the board's width: that sidebar sat right of a 64rem board, and this one sits left of a board that runs the full page width.
Expand Down
111 changes: 36 additions & 75 deletions app/components/OgImage/Collection.takumi.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ogCount, ogInitials, ogText, ogTextList } from '../../utils/og-props'
import { OG_DIMMED, OG_MUTED, OG_RULE, OG_SURFACE, ogLineStyle, ogTitleSize } from '../../utils/og-style'

// Declared as the wire types the OG image URL can deliver, not as the types the
// template wants. A login such as `24601` arrives as a number.
Expand All @@ -23,6 +24,7 @@ const safeReasonSkill = computed(() => ogText(props.reasonSkill))
const safeSkillCount = computed(() => ogCount(props.skillCount))
const safeSkills = computed(() => ogTextList(props.skills))
const initials = computed(() => ogInitials(ogText(props.curatorName) || safeCuratorHandle.value))
const titleSize = computed(() => ogTitleSize(safeName.value, { max: 80, min: 56, lines: 2 }))

const truncatedReason = computed(() => {
const reason = ogText(props.reason)
Expand All @@ -35,87 +37,46 @@ const truncatedReason = computed(() => {

<template>
<OgLayout>
<div class="px-15 py-14 flex flex-col justify-center gap-8 h-full">
<OgBrand :size="36" />
<OgLines :text="safeName" :size="titleSize" face="title" />

<div class="flex flex-col gap-3">
<div
class="text-6xl tracking-tighter font-mono leading-none"
:style="{ lineClamp: 1, textOverflow: 'ellipsis' }"
>
{{ safeName }}
</div>

<div
v-if="safeDescription"
class="text-3xl"
:style="{ color: 'oklch(0.62 0.01 60)', lineClamp: 2, textOverflow: 'ellipsis' }"
>
{{ safeDescription }}
</div>
</div>
<OgLines v-if="safeDescription" class="mt-5" :text="safeDescription" :size="28" />

<!-- Curator -->
<div class="flex items-center gap-3">
<span
class="flex items-center justify-center rounded-full overflow-hidden shrink-0"
:style="{ width: '40px', height: '40px', background: 'oklch(0.22 0.012 60)' }"
>
<img
v-if="safeCuratorAvatar"
:src="safeCuratorAvatar"
:alt="safeCuratorHandle"
width="40"
height="40"
class="w-full h-full object-cover"
>
<span
v-else
class="font-medium"
:style="{ fontSize: '14px', color: 'oklch(0.62 0.01 60)' }"
>
{{ initials }}
</span>
</span>
<span class="text-3xl font-mono" :style="{ color: 'oklch(0.62 0.01 60)' }">
@{{ safeCuratorHandle }}
</span>
<span class="text-3xl" :style="{ color: 'oklch(0.62 0.01 60)' }">
Β· {{ safeSkillCount }} skill{{ safeSkillCount !== 1 ? 's' : '' }}
</span>
</div>

<!-- Curator note -->
<div
v-if="truncatedReason"
class="flex flex-col gap-2"
:style="{
borderLeft: '3px solid oklch(0.36 0.012 60)',
paddingLeft: '16px',
}"
<!-- Curator -->
<div class="flex items-center justify-center mt-7" :style="{ gap: '12px' }">
<span
class="flex items-center justify-center rounded-full overflow-hidden shrink-0"
:style="{ width: '40px', height: '40px', background: OG_SURFACE }"
>
<span
class="text-2xl leading-snug"
:style="{ color: 'oklch(0.93 0.005 60)', lineClamp: 3, textOverflow: 'ellipsis' }"
>
&ldquo;{{ truncatedReason }}&rdquo;
</span>
<span
v-if="safeReasonSkill"
class="font-mono text-xl"
:style="{ color: 'oklch(0.62 0.01 60)' }"
<img
v-if="safeCuratorAvatar"
:src="safeCuratorAvatar"
:alt="safeCuratorHandle"
width="40"
height="40"
class="w-full h-full object-cover"
>
on {{ safeReasonSkill }}
<span v-else class="font-medium" :style="{ fontSize: '14px', color: OG_MUTED }">
{{ initials }}
</span>
</div>
</span>
<span class="text-3xl" :style="{ color: OG_MUTED }">
@{{ safeCuratorHandle }} Β· {{ safeSkillCount }} skill{{ safeSkillCount !== 1 ? 's' : '' }}
</span>
</div>

<div
v-else-if="safeSkills.length"
class="font-mono text-2xl"
:style="{ color: 'oklch(0.62 0.01 60)', lineClamp: 1, textOverflow: 'ellipsis' }"
>
{{ safeSkills.slice(0, 4).join(' Β· ') }}
</div>
<!-- Curator note -->
<div
v-if="truncatedReason"
class="flex flex-col items-center mt-7"
:style="{ width: '100%', borderTop: `2px solid ${OG_RULE}`, paddingTop: '20px', gap: '6px' }"
>
<OgLines :text="`β€œ${truncatedReason}”`" :size="24" color="oklch(0.9 0.006 60)" />
<span v-if="safeReasonSkill" class="font-mono text-xl" :style="{ color: OG_MUTED }">
on {{ safeReasonSkill }}
</span>
</div>
<div v-else-if="safeSkills.length" class="font-mono mt-6" :style="ogLineStyle(22, 1, OG_DIMMED)">
{{ safeSkills.slice(0, 4).join(' Β· ') }}
</div>
</OgLayout>
</template>
101 changes: 33 additions & 68 deletions app/components/OgImage/Curator.takumi.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ogCount, ogInitials, ogText, ogTextList } from '../../utils/og-props'
import { OG_DIMMED, OG_MUTED, OG_RULE, OG_SURFACE, ogLineStyle, ogTitleSize, ogTitleStyle } from '../../utils/og-style'

// Declared as the wire types the OG image URL can deliver, not as the types the
// template wants. A login such as `24601` arrives as a number.
Expand All @@ -20,10 +21,14 @@ const safeAvatar = computed(() => ogText(props.avatar))
const initials = computed(() => ogInitials(safeDisplayName.value || safeHandle.value))
const safeSkills = computed(() => ogTextList(props.skills))

const stats = computed(() => {
// Without a display name, the handle is the title.
const heading = computed(() => safeDisplayName.value || `@${safeHandle.value}`)
const titleSize = computed(() => ogTitleSize(heading.value, { max: 72, min: 48 }))

const meta = computed(() => {
const collectionCount = ogCount(props.collectionCount)
const skillCount = ogCount(props.skillCount)
const items: string[] = []
const items: string[] = safeDisplayName.value ? [`@${safeHandle.value}`] : []
if (collectionCount > 0)
items.push(`${collectionCount} collection${collectionCount !== 1 ? 's' : ''}`)
if (skillCount > 0)
Expand All @@ -34,76 +39,36 @@ const stats = computed(() => {

<template>
<OgLayout>
<div class="px-15 py-14 flex flex-col justify-center gap-8 h-full">
<OgBrand :size="36" />

<div class="flex items-center gap-6">
<!-- Avatar with a stone ring: the lockup dot is the card's one rose element -->
<span
class="flex items-center justify-center rounded-full overflow-hidden shrink-0"
:style="{
width: '100px',
height: '100px',
border: '3px solid oklch(0.36 0.012 60)',
background: 'oklch(0.22 0.012 60)',
}"
>
<img
v-if="safeAvatar"
:src="safeAvatar"
:alt="safeHandle"
width="96"
height="96"
class="w-full h-full object-cover"
>
<span
v-else
class="font-medium"
:style="{ fontSize: '32px', color: 'oklch(0.62 0.01 60)' }"
>
{{ initials }}
</span>
</span>
<!-- A stone ring: the lockup dot is the card's one rose element. -->
<span
class="flex items-center justify-center rounded-full overflow-hidden shrink-0 mb-6"
:style="{ width: '96px', height: '96px', border: `3px solid ${OG_RULE}`, background: OG_SURFACE }"
>
<img
v-if="safeAvatar"
:src="safeAvatar"
:alt="safeHandle"
width="90"
height="90"
class="w-full h-full object-cover"
>
<span v-else class="font-medium" :style="{ fontSize: '32px', color: OG_MUTED }">
{{ initials }}
</span>
</span>

<div class="flex flex-col gap-1">
<div
v-if="safeDisplayName"
class="text-5xl font-mono tracking-tight leading-none"
>
{{ safeDisplayName }}
</div>
<div
class="text-4xl font-mono tracking-tight leading-none"
:style="{ color: 'oklch(0.62 0.01 60)' }"
>
@{{ safeHandle }}
</div>
</div>
</div>
<div :style="{ ...ogTitleStyle(titleSize, 1), wordBreak: 'break-all' }">
{{ heading }}
</div>

<div
v-if="safeDescription"
class="text-3xl"
:style="{ color: 'oklch(0.62 0.01 60)', opacity: 0.7, lineClamp: 2, textOverflow: 'ellipsis' }"
>
{{ safeDescription }}
</div>
<div v-if="meta" class="mt-4" :style="ogLineStyle(28, 1)">
{{ meta }}
</div>

<div
v-if="stats"
class="text-3xl"
:style="{ color: 'oklch(0.62 0.01 60)' }"
>
{{ stats }}
</div>
<OgLines v-if="safeDescription" class="mt-5" :text="safeDescription" :size="26" :color="OG_DIMMED" />

<div
v-if="safeSkills.length"
class="font-mono text-2xl"
:style="{ color: 'oklch(0.62 0.01 60)', lineClamp: 1, textOverflow: 'ellipsis' }"
>
{{ safeSkills.slice(0, 3).join(' Β· ') }}
</div>
<div v-if="safeSkills.length" class="font-mono mt-5" :style="ogLineStyle(22, 1, OG_DIMMED)">
{{ safeSkills.slice(0, 3).join(' Β· ') }}
</div>
</OgLayout>
</template>
Loading