diff --git a/app/composables/useSkillSearchBox.ts b/app/composables/useSkillSearchBox.ts index 46b71483..7b71e690 100644 --- a/app/composables/useSkillSearchBox.ts +++ b/app/composables/useSkillSearchBox.ts @@ -1,4 +1,5 @@ import type { SearchRow } from './useSkillSearch' +import { classifySearchQuery } from '#shared/skill-search-query' import { searchCellId, searchResultsRoute } from './useSkillSearch' /** The id the panel's grid takes, so every input points at the same element. */ @@ -35,6 +36,20 @@ export function useSkillSearchBox(options: { inputs: () => (HTMLInputElement | n } = search const keyboardSelection = ref(false) + const pendingRepositoryQuery = ref(null) + + watch([trimmedQuery, open], ([term, visible]) => { + if (!visible || term !== pendingRepositoryQuery.value) + pendingRepositoryQuery.value = null + }, { flush: 'sync' }) + + watch(state, (current) => { + if (pendingRepositoryQuery.value === null || current._tag === 'loading') + return + pendingRepositoryQuery.value = null + if (current._tag !== 'error') + submitQuery() + }) function clearSelection(): void { keyboardSelection.value = false @@ -91,6 +106,12 @@ export function useSkillSearchBox(options: { inputs: () => (HTMLInputElement | n function submitQuery(): void { const current = state.value + // Enter can beat the debounced lookup. Keep repository intent until it answers. + if (current._tag === 'loading' && classifySearchQuery(trimmedQuery.value)._tag === 'repository') { + openPanel() + pendingRepositoryQuery.value = trimmedQuery.value + return + } if (current._tag === 'repository') { if (current.status._tag === 'idle') void submitRepository(current.repository) diff --git a/layers/registry/server/data/skill-demos.json b/layers/registry/server/data/skill-demos.json index f7cd5b14..70577bd6 100644 --- a/layers/registry/server/data/skill-demos.json +++ b/layers/registry/server/data/skill-demos.json @@ -1389,6 +1389,46 @@ } ] }, + { + "owner": "vojtaholik", + "repo": "good-css", + "name": "good-css", + "makes": "ui-component", + "authorName": null, + "sourceUrl": "https://github.com/vojtaholik/good-css/blob/main/skills/good-css/SKILL.md", + "prompt": "Build a polished responsive reading-list component for a fictional independent design journal called Margin. Show six article cards with different title lengths, categories, reading times, and CSS-drawn editorial cover art. Include working category filters, a light/dark theme switch, and bookmark toggles with an in-page saved count. Use the modern CSS techniques taught by this Skill to make the same component work in a narrow sidebar and a wide main column. Add a simple width control so visitors can see it adapt. Keep the reading list the main visual focus. Use warm paper, strong editorial typography, and restrained vermilion accents. No invented testimonials or statistics. Save one self-contained index.html with inline CSS and runtime JavaScript. No external assets or dependencies. Use TypeScript for any tooling. The demo runs in an opaque iframe with sandbox allow-scripts only: no storage, form submissions, popups, or downloads. Every control must work by keyboard. Support reduced motion and widths from 390px to 1440px. Do not ask questions.", + "agent": "Codex", + "agentVersion": "0.161.0", + "model": "gpt-6.1-sol", + "effort": "medium", + "skillCommit": "e777d97100d184cc78ffea6c5603f409610d5ea4", + "recordedAt": "2026-10-08", + "outputFile": "index.html", + "shots": [ + { + "file": "desktop-90180cd258.jpg", + "width": 1440, + "height": 1672, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Build a polished responsive reading-list component for a fictional independent design journal called Margin. Show six ar", + "poster": { + "file": "desktop-poster-4f0dd2b501.jpg", + "height": 900 + } + }, + { + "file": "mobile-e7b77d8d88.jpg", + "width": 390, + "height": 1839, + "viewport": "mobile", + "alt": "Phone screenshot of the page the Agent built for: Build a polished responsive reading-list component for a fictional independent design journal called Margin. Show six ar", + "poster": { + "file": "mobile-poster-ae31c943c8.jpg", + "height": 844 + } + } + ] + }, { "owner": "yfe404", "repo": "tech-visualizer", diff --git a/layers/registry/server/demos/vojtaholik/good-css/good-css/index.html b/layers/registry/server/demos/vojtaholik/good-css/good-css/index.html new file mode 100644 index 00000000..824af994 --- /dev/null +++ b/layers/registry/server/demos/vojtaholik/good-css/good-css/index.html @@ -0,0 +1,315 @@ + + + + + + + +Margin · The reading list + + + +
+
+
Margin↗
+

An independent journal
of design & culture

+ +
+
+

For the curious, by the curious

Ideas worth
making room for.

+

On the things we make, the spaces we inhabit, and the ideas that stay with us.

Read slowly. Think widely.

+
+ +
+

The reading list

+

06 articles

+
+
+ +

Design

The quiet power of doing less

What happens when we stop adding, and start paying attention?

+

6 min read Essay

+
+
+ +

Typography

A typeface is a voice before a word is read

The subtle ways letterforms shape what we hear on the page.

+

8 min read Perspective

+
+
+ +

Culture

In praise of the ordinary

Finding something remarkable in the things we walk past every day.

+

4 min read Observation

+
+
+ +

Spaces

Rooms for thinking

A window, a chair, a little daylight. The architecture of a good idea.

+

5 min read Field notes

+
+
+ +

Design

The things we make, and the things that make us

Tracing the invisible connections between our lives and our work.

+

11 min read Long read

+
+
+ +

Culture

Stay curious.

An open mind is a practice. Here is a place to begin.

+

3 min read Short thought

+
+
+ +
A good place to pause
+
+

Margin   An independent point of view.

A fictional journal. A real moment to slow down.

+
+

+ + + diff --git a/test/unit/home-search.nuxt.test.ts b/test/unit/home-search.nuxt.test.ts index c3e61d19..411eb1f4 100644 --- a/test/unit/home-search.nuxt.test.ts +++ b/test/unit/home-search.nuxt.test.ts @@ -92,6 +92,54 @@ afterEach(() => { }) describe('homepage search interactions', () => { + it.each(['vojtaholik/good-css', 'https://github.com/vojtaholik/good-css'])('waits for repository lookup before submitting %s', async (term) => { + const repository = { _tag: 'repository' as const, owner: 'vojtaholik', repo: 'good-css', url: 'https://github.com/vojtaholik/good-css' } + query.value = term + rows.value = [] + stateOverride.value = { _tag: 'loading', rows: [] } + const wrapper = await mountSearch() + await press(wrapper, 'Enter', 'Enter') + expect(navigate).not.toHaveBeenCalled() + expect(submitRepository).not.toHaveBeenCalled() + + stateOverride.value = { _tag: 'repository', repository, status: { _tag: 'idle' }, rows: [{ _tag: 'index', repository }] } + await flushPromises() + expect(submitRepository).toHaveBeenCalledExactlyOnceWith(repository) + expect(navigate).not.toHaveBeenCalled() + }) + + it('opens an indexed repository after a pending submission resolves', async () => { + query.value = 'vercel-labs/agent-skills' + stateOverride.value = { _tag: 'loading', rows: [] } + const wrapper = await mountSearch() + await press(wrapper, 'Enter') + expect(navigate).not.toHaveBeenCalled() + stateOverride.value = { _tag: 'ready', rows: [repositoryRow], total: 7, repository: repositoryRow.repository } + await flushPromises() + expect(navigate).toHaveBeenCalledExactlyOnceWith('/gh/vercel-labs/agent-skills') + }) + + it.each(['edit', 'Escape', 'error'])('cancels pending repository submission on %s', async (action) => { + query.value = 'vercel-labs/agent-skills' + stateOverride.value = { _tag: 'loading', rows: [] } + const wrapper = await mountSearch() + await press(wrapper, 'Enter') + if (action === 'edit') { + await wrapper.get('input').setValue('vue') + } + else if (action === 'Escape') { + await press(wrapper, 'Escape') + } + else { + stateOverride.value = { _tag: 'error', error: new Error('offline') } + await flushPromises() + } + stateOverride.value = { _tag: 'ready', rows: [repositoryRow], total: 7, repository: repositoryRow.repository } + await flushPromises() + expect(navigate).not.toHaveBeenCalled() + expect(submitRepository).not.toHaveBeenCalled() + }) + it('submits a name to the results page on Enter when nothing was selected', async () => { const wrapper = await mountSearch() await press(wrapper, 'Enter') diff --git a/test/unit/skill-search-state.test.ts b/test/unit/skill-search-state.test.ts index e64d4a89..9138e2dd 100644 --- a/test/unit/skill-search-state.test.ts +++ b/test/unit/skill-search-state.test.ts @@ -125,7 +125,7 @@ describe('search results belong to the current query', () => { expect(search.rows.value).toMatchObject([{ _tag: 'skill', skill: { name: 'shipreel' } }]) }) - it('offers the index action for a Repository the registry does not hold', async () => { + it.each(['someone/new-skills', 'https://github.com/someone/new-skills'])('offers the index action for an unlisted repository entered as %s', async (query) => { fetchMock.mockResolvedValue({ kind: 'repository', repository: { _tag: 'not-indexed', owner: 'someone', repo: 'new-skills', url: 'https://github.com/someone/new-skills' }, @@ -135,7 +135,7 @@ describe('search results belong to the current query', () => { total: 0, mode: null, }) - await type('https://github.com/someone/new-skills') + await type(query) await debounce() expect(search.state.value).toMatchObject({ _tag: 'repository', status: { _tag: 'idle' } }) expect(search.rows.value).toEqual([{ _tag: 'index', repository: { _tag: 'repository', owner: 'someone', repo: 'new-skills', url: 'https://github.com/someone/new-skills' } }])