diff --git a/public/styles/components/resolver.css b/public/styles/components/resolver.css index 198076a..15e2c04 100644 --- a/public/styles/components/resolver.css +++ b/public/styles/components/resolver.css @@ -1,6 +1,8 @@ .resolver-form { display: grid; gap: var(--space-2); + position: relative; + z-index: 2; } .resolver-form label { @@ -19,6 +21,7 @@ display: grid; gap: var(--space-2); position: relative; + z-index: 1; } .resolver-form input { @@ -52,77 +55,9 @@ } .htmx-indicator { - visibility: hidden; - opacity: 0; -} - -.htmx-request .htmx-indicator, -.htmx-request.htmx-indicator { - visibility: visible; - opacity: 1; -} - -.resolver-form__suggestions:empty { display: none; } -.resolver-form__suggestions { - position: absolute; - z-index: 10; - inset-block-start: calc(100% + var(--space-2)); - inset-inline: 0; -} - -.nsid-suggestions { - display: grid; - max-block-size: 14rem; - overflow: auto; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - box-shadow: var(--shadow-panel); -} - -.nsid-suggestions__item { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - inline-size: 100%; - padding: 0.7rem 0.85rem; - color: var(--text); - background: transparent; - border: 0; - border-radius: 0; - text-align: start; - cursor: pointer; -} - -.nsid-suggestions__item + .nsid-suggestions__item { - border-block-start: 1px solid var(--border); -} - -.nsid-suggestions__item:hover, -.nsid-suggestions__item:focus-visible { - background: var(--accent-bg); - filter: none; -} - -.nsid-suggestions__item span { - min-inline-size: 0; - overflow-wrap: anywhere; - font-family: ui-monospace, 'SFMono-Regular', 'SF Mono', Consolas, 'Liberation Mono', monospace; - font-size: var(--size-sm); -} - -.nsid-suggestions__item small { - flex: 0 0 auto; - color: var(--muted); - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; -} - .resolver-result:empty { display: none; } @@ -142,6 +77,15 @@ font-weight: 600; } +.resolver-state.htmx-indicator { + display: none; +} + +.htmx-request .resolver-state.htmx-indicator, +.htmx-request.resolver-state.htmx-indicator { + display: flex; +} + .resolver-state--loading { color: var(--muted); background: var(--surface-subtle); diff --git a/src/lexicon.ts b/src/lexicon.ts index d090318..b4a1460 100644 --- a/src/lexicon.ts +++ b/src/lexicon.ts @@ -4,7 +4,6 @@ import type { ResolvedLexicon, LexiconDocument, ParsedNsid, - NsidSuggestion, DidDocument, RepoGetRecordResponse } from './types'; @@ -14,7 +13,6 @@ const NSID_PATTERN = const LEXICON_SCHEMA_COLLECTION = 'com.atproto.lexicon.schema'; const LEXICON_CACHE_PREFIX = 'lexicon:'; -const MAX_SUGGESTIONS = 10; export async function sha256Json(value: unknown): Promise { const bytes = new TextEncoder().encode(JSON.stringify(value)); @@ -69,42 +67,6 @@ export async function resolveLexicon(nsid: string, env: RuntimeEnv): Promise { - const normalized = normalizeSuggestQuery(query); - if (normalized === '') { - return []; - } - - const boundedLimit = Math.max(1, Math.min(Math.floor(limit), MAX_SUGGESTIONS)); - const results = await env.LEXICONS.list({ prefix: `${LEXICON_CACHE_PREFIX}${normalized}`, limit: boundedLimit * 3 }); - const candidates = await Promise.all( - results.keys.map(async (key) => { - const nsid = key.name.slice(LEXICON_CACHE_PREFIX.length); - const cached = await env.LEXICONS.get(key.name, 'json'); - - return { nsid, cached }; - }) - ); - - return candidates - .filter( - ({ nsid, cached }) => nsid.length > 0 && isValidNsid(nsid) && cached !== null && !isLegacyGeneratedCache(cached) - ) - .map(({ nsid }) => nsid) - .sort((left, right) => left.localeCompare(right)) - .slice(0, boundedLimit) - .map((nsid) => ({ nsid, source: 'cache' })); -} - -function normalizeSuggestQuery(query: string): string { - const trimmed = query.trim(); - if (trimmed.length > 317 || !/^[A-Za-z0-9.-]+$/.test(trimmed)) { - return ''; - } - - return trimmed; -} - function normalizeCachedLexicon(value: ResolvedLexicon, parsed: ParsedNsid): ResolvedLexicon { return { ...value, source: { name: value.source.name ?? parsed.dnsName, url: value.source.url } }; } diff --git a/src/main.ts b/src/main.ts index 6d2d857..84aa585 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ import { Hono } from 'hono'; import { apiError, jsonResponse } from './http'; -import { mapResolveError, resolveLexicon, resolvePathNsid, suggestNsids } from './lexicon'; +import { mapResolveError, resolveLexicon, resolvePathNsid } from './lexicon'; import { Index } from './view'; -import { JSONView, Suggestions } from './view/partials'; +import { JSONView } from './view/partials'; const app = new Hono<{ Bindings: Env }>(); @@ -28,12 +28,6 @@ app.get('/partials/resolve', async (c) => { } }); -app.get('/partials/suggest', async (c) => { - const query = c.req.query('nsid') ?? ''; - const suggestions = await suggestNsids(query, c.env); - return c.html(Suggestions(suggestions)); -}); - app.options('/api/resolve/*', () => { return new Response(null, { status: 204, headers: CORS_HEADERS }); }); diff --git a/src/types.ts b/src/types.ts index f31b9fb..59a309a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,8 +23,6 @@ export type ResolvedLexicon = { cache: 'hit' | 'miss'; }; -export type NsidSuggestion = { nsid: string; source: 'cache' }; - export type DidDocument = { service?: Array<{ id?: string; type?: string; serviceEndpoint?: string }> }; export type RepoGetRecordResponse = { value?: unknown }; diff --git a/src/view/partials.tsx b/src/view/partials.tsx index b70e01e..f842409 100644 --- a/src/view/partials.tsx +++ b/src/view/partials.tsx @@ -1,17 +1,5 @@ -import type { NsidSuggestion } from '../types'; - const copyButtonAttributes = { 'x-on:click': 'copy()', 'x-text': "copied ? 'Copied' : 'Copy'" }; -const suggestionButtonAttributes = { - 'x-on:click': `$refs.nsid.value = $el.dataset.nsid; - -$refs.suggestions.innerHTML = ''; - -autocompleteOpen = false; $refs.nsid.focus(); - -$refs.form.requestSubmit()` -}; - export function JSONView(value: unknown, nsid: string) { const json = JSON.stringify(value, null, 2); const failed = isErrorPayload(value); @@ -46,24 +34,3 @@ function isErrorPayload(value: unknown): boolean { const error = (value as Record).error; return typeof error === 'object' && error !== null; } - -export function Suggestions(suggestions: NsidSuggestion[]) { - if (suggestions.length === 0) { - return <>; - } - - return ( -
- {suggestions.map((suggestion) => ( - - ))} -
- ); -} diff --git a/src/view/partials/form.tsx b/src/view/partials/form.tsx index 3ee4ee6..a123c14 100644 --- a/src/view/partials/form.tsx +++ b/src/view/partials/form.tsx @@ -6,12 +6,8 @@ const FORM_EVENTS = { 'x-on:htmx:timeout': 'if ($event.target === $refs.form) { resolveLoading = false; resolveFailed = true }' }; -const FIELD_EVENTS = { 'x-on:click.outside': 'autocompleteOpen = false' }; - const INPUT_EVENTS = { - 'x-bind:aria-busy': 'resolveLoading', - 'x-on:focus': 'autocompleteOpen = true', - 'x-on:input': 'autocompleteOpen = true' + 'x-bind:aria-busy': 'resolveLoading' }; export function Resolver({ baseUrl }: { baseUrl: string }) { @@ -19,7 +15,7 @@ export function Resolver({ baseUrl }: { baseUrl: string }) {
+ x-data="{ resolveLoading: false, resolveFailed: false }">

Resolve a schema

Enter an NSID and the resolver will look for its published DNS TXT record.

@@ -34,7 +30,7 @@ export function Resolver({ baseUrl }: { baseUrl: string }) { hx-indicator="#resolve-loading" {...FORM_EVENTS}> -
+
-
diff --git a/test/lexicon.test.ts b/test/lexicon.test.ts index de242d6..3b0cab5 100644 --- a/test/lexicon.test.ts +++ b/test/lexicon.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { isLexiconDocument, resolveLexicon, suggestNsids } from '../src/lexicon'; +import { isLexiconDocument, resolveLexicon } from '../src/lexicon'; import type { RuntimeEnv, ResolvedLexicon } from '../src/types'; const originalFetch = globalThis.fetch; @@ -145,34 +145,4 @@ describe('Lexicon resolver', () => { expect(fetchMock).toHaveBeenCalledOnce(); }); - it('suggests NSIDs from cached lexicon keys', async () => { - const env = testEnv( - new Map([ - ['lexicon:app.bsky.feed.post', JSON.stringify(resolvedLexicon('app.bsky.feed.post'))], - ['lexicon:app.bsky.graph.follow', JSON.stringify(resolvedLexicon('app.bsky.graph.follow'))], - ['other:app.bsky.feed.post', '{}'] - ]) - ); - - await expect(suggestNsids('app.bsky.f', env)).resolves.toEqual([{ nsid: 'app.bsky.feed.post', source: 'cache' }]); - }); - - it('does not suggest for empty or unsafe queries', async () => { - const env = testEnv( - new Map([['lexicon:app.bsky.feed.post', JSON.stringify(resolvedLexicon('app.bsky.feed.post'))]]) - ); - - await expect(suggestNsids('', env)).resolves.toEqual([]); - await expect(suggestNsids('../app', env)).resolves.toEqual([]); - }); - - it('does not suggest legacy generated cache entries', async () => { - const legacyCached = { - ...resolvedLexicon('app.bsky.feed.post'), - source: { name: 'atcute', url: 'https://example.com/generated/app.bsky.feed.post.json', type: 'atcute-generated' } - }; - const env = testEnv(new Map([['lexicon:app.bsky.feed.post', JSON.stringify(legacyCached)]])); - - await expect(suggestNsids('app.bsky.f', env)).resolves.toEqual([]); - }); }); diff --git a/test/view.test.ts b/test/view.test.ts index 93fdfb6..f0b8448 100644 --- a/test/view.test.ts +++ b/test/view.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { renderToString } from 'hono/jsx/dom/server'; -import { JSONView, Suggestions } from '../src/view/partials'; +import { JSONView } from '../src/view/partials'; describe('View helpers', () => { it('escapes JSON before rendering the Prism partial', () => { @@ -28,18 +28,4 @@ describe('View helpers', () => { expect(html).toContain('Error: com.example'); }); - it('renders cached NSID suggestions', () => { - const html = renderToString(Suggestions([{ nsid: 'app.bsky.feed.post', source: 'cache' }])); - - expect(html).toContain('class="nsid-suggestions"'); - expect(html).toContain('data-nsid="app.bsky.feed.post"'); - expect(html).toContain('$refs.form.requestSubmit()'); - }); - - it('escapes suggestion values', () => { - const html = renderToString(Suggestions([{ nsid: '