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
28 changes: 25 additions & 3 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ These exact strings. Do not paraphrase them per page.
| Demo groups | Films and launch videos · Landing pages · UI components · Diagrams and explainers | guidance only: `DEMO_GROUPS` in `shared/demo-groups.ts`: the `/skills/demos` group headings and their lines |
| Writing demo | Writing · Articles, READMEs, and PR descriptions. · Files · Updated · Diff · Markdown · No newline at end of file | guidance only: writing support on the existing demos board. Show file buttons with Markdown icons and one Diff toggle with an icon. Show the updated document by default. Diff compares the original seed and updated Markdown with line numbers and syntax highlighting. Fit short documents to their content and cap long documents. Keep the no-Skill baseline in recording data for later evaluations. No decorative text. |
| Writing demo example | Example data · Example data. No Skills were run. | guidance only: each known writing Skill uses `/skills/demos/<owner>/<repo>/<name>`, with `/{name}` as its heading. All candidate rows share handwritten UI fixtures. Keep the disclosure visible. Show no recording metadata. Reuse the demo footer with Skill links, Run it yourself, and Copy demo link. The run prompt supplies the original fixture documents. README and PR documents use GitHub-style Markdown with the shared Skill renderer and rangi highlighter. Diff toggles the Markdown comparison on and off. |
| Demo actions | Run it yourself · Agent prompt · Terminal · Copy Agent prompt · Copy command · Copied · Copy demo link · Copied demo link | guidance only: demo stages. The Agent prompt includes the recorded task. The terminal option loads the Skill. Run is hidden on phones. |
| Demo actions | Run it yourself · Agent prompt · Terminal · Copy Agent prompt · Copy command · Copied · Copy demo link · Copied demo link | guidance only: demo stages. The Agent prompt includes the recorded task. The terminal option loads the Skill. Run is available on phones. |
| Demo stage | Open the demo | guidance only: the demo stage link. The hero shows the output, author row, and this link only. On the homepage it opens the demo page; on a demo page it opens the Skill page Demo panel |
| Home demo carousel | Previous demo · Next demo · {n} of {total} | guidance only: the manual homepage demo controls. The position is live text for screen readers. |
| Demo page prompt | Prompt | guidance only: the selected demo page keeps its recorded task in an expandable disclosure beside the model and effort, above the output frame. |
| Demo panel | Demo · Prompt · Open live demo · Open in a new tab · Run it yourself · All demos | guidance only: the Skill page Demo panel. "Show screenshots" replaces "Open live demo" while the live output shows. "All demos" opens the demo page. It hides for a Skill page only demo, and while the Skill holds a run check flag |
| Demo page prompt | Prompt | guidance only: full and compact demo stages keep the recorded task in a closed disclosure above the output frame. |
| Demo panel | Demo · Prompt · Open live demo · Open in a new tab · Run it yourself · All demos | guidance only: the Skill page Demo panel. Prompt and setup start in a closed disclosure. "Show screenshots" replaces "Open live demo" while the live output shows. "All demos" opens the demo page. It hides for a Skill page only demo, and while the Skill holds a run check flag |
| Demo token usage | {n} tokens · Input: {input}; cached input: {cached}; output: {output} | guidance only: recording metadata. Counts come from recorded usage. Cached input is included in input. Omit unavailable counts. |
| Demo provenance | {Agent logo} {model}, {effort} effort | guidance only: the recording line on the Skill page panel and the homepage stage. Screen readers and the title get "Recorded with {agent}, {model}, {effort} effort". Omit effort when the recording has no evidence for it. No date and no GitHub link: the Skill page is where visitors inspect a Skill |
| Demo outdated | Recorded on an older version of this Skill. | guidance only: under the Demo panel when the Skill moved past the recorded commit |
Expand Down Expand Up @@ -180,6 +180,28 @@ The weekly card uses `Trending skills this week` as its title and links to the t
Each row links the Skill, the dev count, and `Source`. Keep the run command beside the source link.
Use `7-day social mentions · {date}` as the footer. Format the UTC date as `2 Oct 2026`.

## Account entry and setup

| Surface | Canonical strings | Use |
| --- | --- | --- |
| Login | Sign In with GitHub · Sign Up with GitHub · Continue with GitHub | Sign-in and sign-up share GitHub authentication. Preserve the requested action and return page when switching. |
| Login switch | New to skilld? · Sign up · Already have an account? · Sign in | Link between the sign-in and sign-up views. Sign-up shows the account benefits. |
| Login description | Keep your liked Skills and watched Repositories together. | Explain account value below either authentication heading. |
| Login benefits | Your skills · Like Skills to find them again in your account. · Watch for changes · Watch the Repositories you use and see what changed. · Email updates · Choose the weekly, monthly watched changes, or neither. | Three account benefits. Running Skills remains anonymous. |
| Login permissions | We ask for your public profile and email address. | Keep permission scope separate from account benefits. |
| Login benefits heading | Keep updated. | Sign-up benefits panel. |
| Login intent | Sign in to like this Skill. · Sign in to watch this Repository. · Sign in to watch this collection. | Explain the action carried into authentication. |
| Login meta | Sign in with GitHub to keep liked Skills, watch for changes, and choose your emails. | Login page description. |
| Like needs a Skill | Open a Skill before liking it. | A Repository with several Skills cannot imply which Skill to like. |
| Star import | We check up to 1,000 starred GitHub Repositories against the registry. Choose which to watch. Email updates are optional. | Match registry coverage, regardless of Repository name. |
| Star import empty | Find Skills from your GitHub stars · Import stars · None of the checked Repositories have Skills in the registry yet. · Browse curated Skills, then like one to keep it in your account. · Import your GitHub stars, or browse curated Skills. | Offer discovery when imported stars have no matching Skills. |
| Star import progress | Page {page} · {total} Repositories checked · {matched} match the registry | Report checked Repositories and registry matches separately. |
| Star import error | Could not import GitHub stars. Try again. | Use when an import fails without a specific error. |
| Star import restart | Another star import started. Import your stars again. · Star import pages must run in order. Restart from page one. | Browser and CLI import recovery. |
| Star import date | Last complete GitHub import: {date} | A bounded partial import has no completion date. |
| Star import partial | Checked the first 1,000 GitHub stars. Some starred Repositories were not checked. | The page limit leaves coverage partial. |
| Account without likes, with watches | Your watched Repositories are ready · {n} watched {Repository/Repositories}. Like a Skill to keep it here. · Browse skills · View watched Repositories | Show existing account value when the liked Skills view is empty. |

## Register by context

| Context | Register | Example |
Expand Down
19 changes: 14 additions & 5 deletions app/components/DemoStage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import DemoWriting from './home-demos/_DemoWriting.vue'
/**
* One demo on show: how it was recorded, the output in a window one screen
* tall (a page that scrolls inside it, or a film that plays while on screen),
* and the Skill's row with its run command. `showPrompt` adds the prompt in
* full, for a list that names the demos without their prompts. `live` puts a
* and the Skill's row with its run command. `showPrompt` adds a disclosure
* for the recorded task. `live` puts a
* page demo's real output in the window, in a frame with no same-origin
* access; the route serves it under a CSP sandbox too. `opens` picks where
* "Open the demo" goes: the demo's own page, or, on that page, the Skill
Expand Down Expand Up @@ -56,9 +56,14 @@ const liveUrl = computed(() => live && !demo.video && !demo.writing ? demo.liveU
<UIcon name="i-lucide-arrow-right" class="size-3.5 shrink-0" aria-hidden="true" />
</NuxtLink>
</div>
<p v-if="showPrompt && presentation === 'full'" class="demo-stage__prompt">
<span class="data-label mr-2">Prompt</span>{{ demo.prompt }}
</p>
<details v-if="showPrompt && presentation === 'full'" class="demo-stage__task demo-stage__task--full">
<summary class="data-label">
Prompt
</summary>
<p class="demo-stage__prompt">
{{ demo.prompt }}
</p>
</details>
<div v-if="demo.writing" ref="stage">
<DemoWriting :writing="demo.writing" />
</div>
Expand Down Expand Up @@ -225,6 +230,10 @@ const liveUrl = computed(() => live && !demo.video && !demo.writing ? demo.liveU
outline-offset: 2px;
}

.demo-stage__task--full summary {
padding-inline-end: 0;
}

/* The live page scrolls inside its own frame. */
.demo-stage__window[data-live] {
overflow: hidden;
Expand Down
1 change: 0 additions & 1 deletion app/components/_DemoActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,4 @@ async function share() {
<style scoped>
.demo-actions__format[aria-pressed='true'] { background: var(--ui-bg-accented); color: var(--ui-text-highlighted); }
.demo-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
@media (max-width: 39.99rem) { .demo-actions__run { display: none; } }
</style>
1 change: 1 addition & 0 deletions app/components/_DemoFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const emit = defineEmits<{ action: [action: { event: 'share' } | { event: 'copy'
}
.demo-footer__open:hover { color: var(--ui-text-highlighted); }
@media (max-width: 39.99rem) {
.demo-footer { grid-template-columns: minmax(0, 1fr); }
.demo-footer :deep(.skill-card--row) { padding-block: 0.5rem; }
.demo-footer :deep(.skill-card__row-end) { display: none; }
}
Expand Down
49 changes: 49 additions & 0 deletions app/plugins/signup-clicks.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEventListener } from '@vueuse/core'
import { signupClickEvent } from '#shared/signup-analytics'

export default defineNuxtPlugin((nuxtApp) => {
const recordSignup = useSignupEvents()
const route = useRoute()
const proof = useState<string | null>('signup-click-proof', () => null)
let preparedPage = ''
let expiresAt = 0
async function prepareProof() {
const page = route.fullPath
if (preparedPage === page && expiresAt > Date.now())
return
if (![...document.querySelectorAll<HTMLAnchorElement>('a[href]')].some(link => signupClickEvent(link.href, window.location.origin)))
return
preparedPage = page
expiresAt = Date.now() + 240_000
proof.value = null
const result = await $fetch<{ proof: string | null }>('/api/events/signup-proof', { retry: false }).catch(() => {
console.warn('[signup-analytics] Could not prepare click proof')
return null
})
if (route.fullPath === page)
proof.value = result?.proof ?? null
if (!result?.proof)
expiresAt = 0
}
nuxtApp.hook('app:mounted', prepareProof)
nuxtApp.hook('page:finish', prepareProof)
watch(() => route.fullPath, () => {
proof.value = null
})
useEventListener(document, ['pointerover', 'focusin'], (event) => {
const link = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[href]') : null
if (link && signupClickEvent(link.href, window.location.origin))
void prepareProof()
})
useEventListener(document, 'click', (event) => {
// Page loads, prefetches, and programmatic DOM clicks are not click evidence.
if (!event.isTrusted || !(event.target instanceof Element))
return
const link = event.target.closest<HTMLAnchorElement>('a[href]')
if (!link)
return
const point = signupClickEvent(link.href, window.location.origin)
if (point)
recordSignup(point)
})
})
11 changes: 9 additions & 2 deletions layers/identity/app/components/LikeButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { owner, repo, name, count = 0, variant = 'detail' } = defineProps<{
}>()

const route = useRoute()
const { state, loginUrl } = useAuth()
const { state } = useAuth()
const { isLiked, isPending, likeCount, observeLikeCount, ensureLoaded, toggle } = useLikes()

const skillRef = computed(() => ({ owner, repo, name }))
Expand All @@ -29,7 +29,14 @@ const displayCount = computed(() => likeCount(skillRef.value) ?? baseline.value)

// Anonymous clicks bounce through OAuth carrying the intent, and the callback
// replays it via watch-actions.ts, so the user lands back on a liked skill.
const anonHref = computed(() => loginUrl({ returnTo: route.fullPath, action: 'like-skill' }))
const anonHref = computed(() => {
const skillPath = `/gh/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(name)}`
const repoPath = `/gh/${owner}/${repo}`
const returnTo = variant === 'detail' && (route.path === repoPath || route.path === skillPath)
? route.fullPath
: skillPath
return `/login?${new URLSearchParams({ return_to: returnTo, action: 'like-skill' })}`
})

const NUDGE_KEY = 'skilld:like-digest-nudge-dismissed'
const nudgeDismissed = ref(true)
Expand Down
8 changes: 4 additions & 4 deletions layers/identity/app/components/WatchCollectionButton.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
const props = defineProps<{ login: string, slug: string }>()
const { state, loginUrl } = useAuth()
const { state } = useAuth()
const submitting = ref(false)
const watched = ref(false)

const anonHref = computed(() => loginUrl({
returnTo: `/@${props.login}/${props.slug}`,
const anonHref = computed(() => `/login?${new URLSearchParams({
return_to: `/@${props.login}/${props.slug}`,
action: 'watch-collection',
}))
})}`)

async function watchNow() {
submitting.value = true
Expand Down
10 changes: 8 additions & 2 deletions layers/identity/app/composables/useSignupEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import type { SignupBrowserEvent } from '#shared/signup-analytics'

export function useSignupEvents() {
const { loggedIn } = useUserSession()
const proof = useState<string | null>('signup-click-proof', () => null)
return (body: SignupBrowserEvent): void => {
if (!loggedIn.value)
if (!loggedIn.value && body.stage !== 'oauth')
return
if (body.stage === 'oauth' && !proof.value)
return
const headers = body.stage === 'oauth' ? { 'x-signup-proof': proof.value! } : undefined
if (body.stage === 'oauth')
proof.value = null
// Keep the action independent of analytics and survive same-tab redirects.
void $fetch('/api/events/signup', { method: 'POST', body, retry: false, keepalive: true }).catch(() => {
void $fetch('/api/events/signup', { method: 'POST', body, headers, retry: false, keepalive: true }).catch(() => {
console.warn('[signup-analytics] Could not record signup event')
})
}
Expand Down
13 changes: 9 additions & 4 deletions layers/identity/app/layouts/auth.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script setup lang="ts">
const route = useRoute()
</script>

<template>
<div class="flex min-h-dvh flex-col bg-muted/40">
<header class="flex items-center justify-between gap-4 px-4 py-3 sm:px-6">
Expand All @@ -7,11 +11,12 @@
</NuxtLink>
<UColorModeButton color="neutral" variant="ghost" class="min-h-11 min-w-11" />
</header>
<main id="main-content" tabindex="-1" class="flex flex-1 flex-col items-center justify-center px-4 pt-6 pb-16 sm:px-6">
<NuxtLink to="/" aria-label="skilld, home" class="mb-8 flex min-h-11 items-center rounded-lg">
<AppLogo />
<main id="main-content" tabindex="-1" class="editorial-band flex flex-1 flex-col items-center justify-center px-4 pt-6 pb-16 sm:px-6">
<div v-if="route.meta.authWide" class="editorial-atmosphere" data-palette="rose" data-geometry="bloom" data-intensity="ambient" aria-hidden="true" />
<NuxtLink to="/" aria-label="skilld, home" class="editorial-band__content mb-10 flex min-h-11 items-center rounded-lg">
<AppLogo :class="route.meta.authWide ? 'h-auto w-36' : undefined" />
</NuxtLink>
<div class="w-full max-w-2xl">
<div class="editorial-band__content w-full" :class="route.meta.authWide ? 'max-w-[51rem]' : 'max-w-2xl'">
<slot />
</div>
</main>
Expand Down
Loading
Loading