Skip to content

Commit 9cfa927

Browse files
committed
fix: enhance TypeScript API search functionality and styling
1 parent 40551ce commit 9cfa927

File tree

9 files changed

+376
-97
lines changed

9 files changed

+376
-97
lines changed

src/frontend/src/components/api-reference/ApiSearchBar.astro

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
284284
.api-search-bar-unified .api-filter-chip.active[data-kind='record'] { color: var(--api-kind-record); }
285285
.api-search-bar-unified .api-filter-chip.active[data-kind='delegate'] { color: var(--api-kind-delegate); }
286286
.api-search-bar-unified .api-filter-chip.active[data-kind='record struct'] { color: var(--api-kind-record-struct); }
287+
.api-search-bar-unified .api-filter-chip.active[data-kind='function'] { color: #3b82f6; }
287288
.api-search-bar-unified .api-filter-chip.active[data-kind='method'] { color: #3b82f6; }
288289
.api-search-bar-unified .api-filter-chip.active[data-kind='property'] { color: #10b981; }
289290
.api-search-bar-unified .api-filter-chip.active[data-kind='constructor'] { color: #8b5cf6; }
@@ -308,6 +309,7 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
308309
.api-search-bar-unified .api-filter-dot.kind-record { background: var(--api-kind-record); }
309310
.api-search-bar-unified .api-filter-dot.kind-delegate { background: var(--api-kind-delegate); }
310311
.api-search-bar-unified .api-filter-dot.kind-record-struct { background: var(--api-kind-record-struct); }
312+
.api-search-bar-unified .api-filter-dot.kind-function { background: #2563a8; }
311313
.api-search-bar-unified .api-filter-dot.kind-method { background: #2563a8; }
312314
.api-search-bar-unified .api-filter-dot.kind-property { background: #0a7d56; }
313315
.api-search-bar-unified .api-filter-dot.kind-constructor { background: #6d3dc8; }
@@ -396,6 +398,7 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
396398
.api-search-bar-unified .api-kind-micro.kind-record { background: var(--api-kind-record); }
397399
.api-search-bar-unified .api-kind-micro.kind-delegate { background: var(--api-kind-delegate); }
398400
.api-search-bar-unified .api-kind-micro.kind-record-struct { background: var(--api-kind-record-struct); }
401+
.api-search-bar-unified .api-kind-micro.kind-function { background: #2563a8; }
399402
.api-search-bar-unified .api-kind-micro.kind-method { background: #2563a8; }
400403
.api-search-bar-unified .api-kind-micro.kind-property { background: #0a7d56; }
401404
.api-search-bar-unified .api-kind-micro.kind-constructor { background: #6d3dc8; }
@@ -424,6 +427,11 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
424427
background: color-mix(in srgb, var(--api-kind-struct) 10%, #f5f5f5);
425428
border-color: color-mix(in srgb, var(--api-kind-struct) 25%, transparent);
426429
}
430+
[data-theme='light'] .api-search-bar-unified .api-kind-micro.kind-function {
431+
color: #2563a8;
432+
background: color-mix(in srgb, #2563a8 10%, #f5f5f5);
433+
border: 1px solid color-mix(in srgb, #2563a8 25%, transparent);
434+
}
427435
[data-theme='light'] .api-search-bar-unified .api-kind-micro.kind-method {
428436
color: #2563a8;
429437
background: color-mix(in srgb, #2563a8 10%, #f5f5f5);

src/frontend/src/pages/reference/api/typescript/[module]/[item]/index.astro

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
99
import { Code } from '@astrojs/starlight/components';
1010
import { tsModuleSlug, getTsModules, simplifyType, formatTsSignature } from '@utils/ts-modules';
1111
import { getTsApiReferenceSidebar } from '@utils/ts-api-sidebar';
12-
import { getTsItemSlug, getTsMethodSlug, getTsStandaloneFunctions, getTsTopLevelRouteItems } from '@utils/ts-api-routes';
12+
import { getTsFunctionDisplayLabel, isTsExtensionStyleFunction } from '@utils/ts-api-function-kind';
13+
import { getTsItemSlug, getTsMemberAnchor, getTsMethodSlug, getTsStandaloneFunctions, getTsTopLevelRouteItems } from '@utils/ts-api-routes';
1314
import Breadcrumb from '@components/Breadcrumb.astro';
1415
import InpageSearch from '@components/api-reference/InpageSearch.astro';
1516
import TsMemberCard from '@components/api-reference/TsMemberCard.astro';
@@ -93,9 +94,11 @@ function typeColorIndex(s: string): number {
9394
return Math.abs(h) % 4;
9495
}
9596
97+
const isExtensionStyleFunction = itemKind === 'function' && isTsExtensionStyleFunction(item);
98+
9699
const kindLabel = itemKind === 'handle' ? (item.isInterface ? 'Interface' : 'Handle')
97100
: itemKind === 'dto' ? 'Type'
98-
: itemKind === 'function' ? 'Function'
101+
: itemKind === 'function' ? getTsFunctionDisplayLabel(item)
99102
: 'Enum';
100103
101104
const kindPillClass = itemKind === 'handle'
@@ -181,19 +184,15 @@ const memberCount = getters.length + setters.length + methods.length +
181184
(itemKind === 'enum' ? (item.members ?? []).length : 0);
182185
const hasSearchableMembers = memberCount >= 5;
183186
184-
function memberAnchor(name: string): string {
185-
return name.toLowerCase().replace(/[^a-z0-9]/g, '-');
186-
}
187-
188187
interface TypeIndexEntry { n: string; k: string; s: string; r?: string; a: string; }
189188
const typeIndex: TypeIndexEntry[] = [];
190189
if (itemKind === 'handle') {
191-
for (const g of getters) typeIndex.push({ n: g.name, k: 'property', s: g.description ?? '', r: g.returnType, a: memberAnchor(g.name) });
190+
for (const g of getters) typeIndex.push({ n: g.name, k: 'property', s: g.description ?? '', r: g.returnType, a: getTsMemberAnchor(g.name) });
192191
for (const m of methods) typeIndex.push({ n: m.name, k: 'method', s: m.description ?? '', r: m.returnType, a: getTsMethodSlug(m, methods, item.name) });
193192
} else if (itemKind === 'dto') {
194-
for (const f of (item.fields ?? [])) typeIndex.push({ n: f.name, k: 'field', s: '', r: f.type, a: memberAnchor(f.name) });
193+
for (const f of (item.fields ?? [])) typeIndex.push({ n: f.name, k: 'field', s: '', r: f.type, a: getTsMemberAnchor(f.name) });
195194
} else if (itemKind === 'enum') {
196-
for (const m of (item.members ?? [])) typeIndex.push({ n: m, k: 'enum member', s: '', a: memberAnchor(m) });
195+
for (const m of (item.members ?? [])) typeIndex.push({ n: m, k: 'enum member', s: '', a: getTsMemberAnchor(m) });
197196
}
198197
const typeSearchKinds = [...new Set(typeIndex.map((e: TypeIndexEntry) => e.k))].sort();
199198
const typeIndexJson = JSON.stringify(typeIndex);
@@ -298,7 +297,7 @@ if (itemKind === 'handle') {
298297
{getters.map((g: any) => {
299298
const hasSetter = setters.some((s: any) => s.name.replace(/^set/, '').toLowerCase() === g.name.toLowerCase());
300299
return (
301-
<div id={memberAnchor(g.name)}>
300+
<div id={getTsMemberAnchor(g.name)}>
302301
<TsMemberCard
303302
member={{ ...g, accessorLabel: hasSetter ? 'get · set' : 'get' }}
304303
variant="property"
@@ -309,7 +308,7 @@ if (itemKind === 'handle') {
309308
{setters.filter((s: any) =>
310309
!getters.some((g: any) => g.name.toLowerCase() === s.name.replace(/^set/, '').toLowerCase())
311310
).map((s: any) => (
312-
<div id={memberAnchor(s.name)}>
311+
<div id={getTsMemberAnchor(s.name)}>
313312
<TsMemberCard
314313
member={{ ...s, accessorLabel: 'set' }}
315314
variant="property"
@@ -347,7 +346,7 @@ if (itemKind === 'handle') {
347346
<h2 id="fields">Fields</h2>
348347
<div class="api-list ts-member-stack">
349348
{item.fields.map((f: any) => (
350-
<div id={memberAnchor(f.name)}>
349+
<div id={getTsMemberAnchor(f.name)}>
351350
<TsMemberCard
352351
member={{ name: f.name, returnType: f.type, isOptional: f.isOptional, description: null }}
353352
variant="field"
@@ -364,7 +363,7 @@ if (itemKind === 'handle') {
364363
<h2 id="values">Values</h2>
365364
<div class="api-list ts-member-stack">
366365
{item.members.map((m: string, i: number) => (
367-
<div id={memberAnchor(m)}>
366+
<div id={getTsMemberAnchor(m)}>
368367
<div class="api-list-item mc">
369368
<div class="api-list-header">
370369
<code class="mc-param-name">{m}</code>
@@ -401,7 +400,7 @@ if (itemKind === 'handle') {
401400
{(item.expandedTargetTypes ?? []).length > 0 && (
402401
<section>
403402
<h2 id="applies-to">Applies to</h2>
404-
<p class="fn-applies-desc">This function is an extension method on the following types:</p>
403+
<p class="fn-applies-desc">This {isExtensionStyleFunction ? 'method' : 'function'} applies to the following types:</p>
405404
<div class="api-list not-content">
406405
{item.expandedTargetTypes.map((t: string) => {
407406
const simpleName = simplifyType(t);

src/frontend/src/pages/reference/api/typescript/[module]/index.astro

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
1010
import { tsModuleSlug, getTsModules, simplifyType } from '@utils/ts-modules';
1111
import { getTsApiReferenceSidebar } from '@utils/ts-api-sidebar';
12+
import {
13+
getTsCallableIdentityKey,
14+
getTsFunctionDisplayKind,
15+
getTsFunctionDisplayLabel,
16+
} from '@utils/ts-api-function-kind';
1217
import { getTsItemSlug, getTsMethodSlug, getTsStandaloneFunctions, getTsTopLevelRouteItems } from '@utils/ts-api-routes';
1318
import InpageSearch from '@components/api-reference/InpageSearch.astro';
1419
import Breadcrumb from '@components/Breadcrumb.astro';
@@ -83,6 +88,7 @@ interface PkgIndexEntry {
8388
8489
const pkgIndex: PkgIndexEntry[] = [];
8590
let memberCount = 0;
91+
const standaloneFunctionKeys = new Set(standaloneFunctions.map((fn: any) => getTsCallableIdentityKey(fn)));
8692
8793
function capabilityKindToSearchKind(capKind: string): string {
8894
switch (capKind) {
@@ -115,6 +121,10 @@ for (const h of ((pkg.handleTypes ?? []) as any[])) {
115121
});
116122
// Index members from capabilities
117123
for (const cap of (h.capabilities ?? [])) {
124+
if ((cap.kind === 'Method' || cap.kind === 'InstanceMethod') && standaloneFunctionKeys.has(getTsCallableIdentityKey(cap))) {
125+
continue;
126+
}
127+
118128
memberCount++;
119129
const memberName = cap.name ?? cap.capabilityId?.split('/').pop() ?? '';
120130
const memberHref = cap.kind === 'Method' || cap.kind === 'InstanceMethod'
@@ -163,7 +173,7 @@ for (const f of (getTsStandaloneFunctions(pkg) as any[])) {
163173
n: f.name,
164174
f: f.qualifiedName ?? f.name,
165175
ns: modOf(f.capabilityId ?? f.name),
166-
k: 'function',
176+
k: getTsFunctionDisplayKind(f),
167177
s: (f.description ?? '').slice(0, 160),
168178
h: `${base}/reference/api/typescript/${pkgSlug}/${getTsItemSlug(f, topLevelItems)}/`,
169179
});
@@ -268,7 +278,7 @@ const pkgName = pkg.package.name;
268278
{standaloneFunctions.map((f: any) => (
269279
<a href={`${base}/reference/api/typescript/${pkgSlug}/${getTsItemSlug(f, topLevelItems)}/`} class="api-list-item">
270280
<div class="api-list-header">
271-
<span class="api-kind-pill kind-method">function</span>
281+
<span class="api-kind-pill kind-method">{getTsFunctionDisplayLabel(f).toLowerCase()}</span>
272282
<code class="api-type-link">{f.name}</code>
273283
{f.returnsBuilder && <span class="api-mini-badge">builder</span>}
274284
</div>

src/frontend/src/pages/reference/api/typescript/index.astro

Lines changed: 21 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* /reference/api/typescript/ — TypeScript API Reference landing page with search.
44
*
55
* Lists all TypeScript API modules with a client-side search bar that lets
6-
* users filter across functions, handle types, types, and enums.
6+
* users filter across functions, handle members, handle types, types, and enums.
77
*/
88
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
99
import { tsModuleSlug, getTsModules, simplifyType } from '@utils/ts-modules';
1010
import { getTsApiReferenceSidebar } from '@utils/ts-api-sidebar';
11+
import { buildTsApiSearchIndex } from '@utils/ts-api-search';
1112
import ApiSearchBar from '@components/api-reference/ApiSearchBar.astro';
1213
import Breadcrumb from '@components/Breadcrumb.astro';
1314
@@ -23,82 +24,25 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
2324
const allTsVersions = [...new Set(sorted.map(p => p.package.version).filter(Boolean))] as string[];
2425
2526
/* ── Build the search index ─────────────────────────────────────── */
26-
27-
interface IndexEntry {
28-
/** Short name */
29-
n: string;
30-
/** Fully-qualified name or signature */
31-
f: string;
32-
/** Kind (method, handle, dto, enum, property, etc.) */
33-
k: string;
34-
/** Package name */
35-
p: string;
36-
/** Description text */
37-
s: string;
38-
/** Parent type name (for capabilities on handle types) */
39-
t?: string;
40-
}
41-
42-
const index: IndexEntry[] = [];
27+
const index = buildTsApiSearchIndex(sorted, base);
4328
let functionCount = 0;
4429
let typeCount = 0;
4530
4631
for (const pkg of sorted) {
47-
const pkgName = pkg.package.name;
48-
const pkgVersion = pkg.package.version;
49-
50-
// Index functions
5132
for (const func of pkg.functions ?? []) {
5233
functionCount++;
53-
index.push({
54-
n: func.name,
55-
f: func.signature ?? func.qualifiedName ?? func.name,
56-
k: func.kind === 'PropertyGetter' ? 'property' :
57-
func.kind === 'PropertySetter' ? 'property' :
58-
func.kind === 'InstanceMethod' ? 'method' : 'method',
59-
p: pkgName,
60-
s: func.description ?? '',
61-
...(pkgVersion ? { v: pkgVersion } : {}),
62-
});
6334
}
6435
65-
// Index handle types
6636
for (const handle of pkg.handleTypes ?? []) {
6737
typeCount++;
68-
index.push({
69-
n: handle.name,
70-
f: handle.fullName ?? handle.name,
71-
k: handle.isInterface ? 'interface' : 'handle',
72-
p: pkgName,
73-
s: `Handle type${handle.isInterface ? ' (interface)' : ''}`,
74-
...(pkgVersion ? { v: pkgVersion } : {}),
75-
});
7638
}
7739
78-
// Index DTO types
7940
for (const dto of pkg.dtoTypes ?? []) {
8041
typeCount++;
81-
index.push({
82-
n: dto.name,
83-
f: dto.fullName ?? dto.name,
84-
k: 'type',
85-
p: pkgName,
86-
s: `Type with ${dto.fields?.length ?? 0} fields`,
87-
...(pkgVersion ? { v: pkgVersion } : {}),
88-
});
8942
}
9043
91-
// Index enum types
9244
for (const enumType of pkg.enumTypes ?? []) {
9345
typeCount++;
94-
index.push({
95-
n: enumType.name,
96-
f: enumType.fullName ?? enumType.name,
97-
k: 'enum',
98-
p: pkgName,
99-
s: `Enum: ${(enumType.members ?? []).join(', ')}`,
100-
...(pkgVersion ? { v: pkgVersion } : {}),
101-
});
10246
}
10347
}
10448
@@ -134,7 +78,7 @@ const indexJson = JSON.stringify(index);
13478

13579
<ApiSearchBar
13680
id="ts-api"
137-
placeholder="Search functions, types, enums"
81+
placeholder="Search functions, methods, properties, types"
13882
kinds={allKinds}
13983
versions={allTsVersions}
14084
defaultStatsText={`${functionCount.toLocaleString()} functions and ${typeCount.toLocaleString()} types across ${sorted.length.toLocaleString()} modules`}
@@ -185,15 +129,18 @@ const indexJson = JSON.stringify(index);
185129
k: string;
186130
p: string;
187131
s: string;
132+
h: string;
188133
t?: string;
189134
v?: string;
135+
m?: boolean;
190136
}
191137

192138
const PAGE_SIZE = 10;
193139
const DEBOUNCE_MS = 250;
194140
function fmtNum(n: number): string { return n.toLocaleString(); }
195141

196142
const KIND_COLORS: Record<string, string> = {
143+
function: '#3b82f6',
197144
method: '#3b82f6',
198145
property: '#10b981',
199146
handle: '#8b5cf6',
@@ -293,12 +240,12 @@ const indexJson = JSON.stringify(index);
293240
const visibleEntries = this.activeVersions === null
294241
? this.index
295242
: this.index.filter(entry => entry.v && this.activeVersions!.has(entry.v));
296-
const functionKinds = new Set(['method', 'property']);
243+
const functionKinds = new Set(['function', 'method']);
297244

298245
return {
299-
packageCount: new Set(visibleEntries.map(entry => entry.p)).size,
300-
functionCount: visibleEntries.filter(entry => functionKinds.has(entry.k)).length,
301-
typeCount: visibleEntries.filter(entry => !functionKinds.has(entry.k)).length,
246+
packageCount: new Set(visibleEntries.filter(entry => !entry.m).map(entry => entry.p)).size,
247+
functionCount: visibleEntries.filter(entry => !entry.m && functionKinds.has(entry.k)).length,
248+
typeCount: visibleEntries.filter(entry => !entry.m && !functionKinds.has(entry.k)).length,
302249
};
303250
}
304251

@@ -461,6 +408,7 @@ const indexJson = JSON.stringify(index);
461408
const fullLower = entry.f.toLowerCase();
462409
const descLower = entry.s.toLowerCase();
463410
const pkgLower = entry.p.toLowerCase();
411+
const parentLower = entry.t?.toLowerCase() ?? '';
464412

465413
let score = 0;
466414
for (const token of tokens) {
@@ -469,6 +417,7 @@ const indexJson = JSON.stringify(index);
469417
else if (nameLower.includes(token)) score += 40;
470418
else if (this.camelMatch(entry.n, token)) score += 55;
471419
else if (fullLower.includes(token)) score += 30;
420+
else if (parentLower.includes(token)) score += 25;
472421
else if (pkgLower.includes(token)) score += 15;
473422
else if (descLower.includes(token)) score += 10;
474423
else { score = 0; break; }
@@ -546,26 +495,21 @@ const indexJson = JSON.stringify(index);
546495

547496
private renderResult(entry: IndexEntry, tokens: string[]): string {
548497
const kindColor = KIND_COLORS[entry.k] ?? KIND_COLORS['method'];
549-
const pkgSlug = entry.p.toLowerCase();
550-
const itemSlug = entry.n.toLowerCase().replace(/[^a-z0-9]+/g, '-');
551-
552-
// Only types (handle, interface, type, enum) have detail pages
553-
const TYPE_KINDS = new Set(['handle', 'interface', 'type', 'enum']);
554-
const isType = TYPE_KINDS.has(entry.k);
555-
const href = isType ? `${this.base}/reference/api/typescript/${pkgSlug}/${itemSlug}/` : null;
556-
const tag = href ? 'a' : 'div';
557-
const hrefAttr = href ? ` href="${this.esc(href)}"` : '';
498+
const kindClass = entry.k.replace(/\s+/g, '-');
558499

559-
return `<${tag}${hrefAttr} class="api-list-item api-search-result" title="${this.esc(entry.f)} — ${this.esc(entry.p)}">
500+
return `<a href="${this.esc(entry.h)}" class="api-list-item api-search-result" title="${this.esc(entry.f)} — ${this.esc(entry.p)}${entry.t ? ` (${this.esc(entry.t)})` : ''}">
560501
<div class="api-list-header">
561-
<span class="api-search-result-name" style="color: ${kindColor}">${this.highlight(entry.n, tokens)}</span>
562-
<span class="api-kind-micro kind-${entry.k}">${this.esc(entry.k)}</span>
502+
<span class="api-result-name-wrap">
503+
<span class="api-search-result-name" style="color: ${kindColor}">${this.highlight(entry.n, tokens)}</span>
504+
${entry.t ? `<span class="api-result-dot">&nbsp;·&nbsp;</span><span class="api-result-parent">${this.esc(entry.t)}</span>` : ''}
505+
</span>
506+
<span class="api-kind-micro kind-${kindClass}">${this.esc(entry.k)}</span>
563507
<div class="trailing">
564508
<span class="api-list-meta" title="${this.esc(entry.p)}">${this.esc(entry.p)}</span>
565509
</div>
566510
</div>
567511
${entry.s ? `<div class="api-list-desc">${this.esc(entry.s)}</div>` : ''}
568-
</${tag}>`;
512+
</a>`;
569513
}
570514

571515
private highlight(text: string, tokens: string[]): string {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { TsFunction } from './ts-modules';
2+
3+
type TsFunctionLike = Pick<TsFunction, 'capabilityId' | 'expandedTargetTypes' | 'name' | 'qualifiedName' | 'signature'>;
4+
5+
export function isTsExtensionStyleFunction(fn: Pick<TsFunctionLike, 'expandedTargetTypes'>): boolean {
6+
return (fn.expandedTargetTypes?.length ?? 0) > 0;
7+
}
8+
9+
export function getTsFunctionDisplayKind(fn: Pick<TsFunctionLike, 'expandedTargetTypes'>): 'function' | 'method' {
10+
return isTsExtensionStyleFunction(fn) ? 'method' : 'function';
11+
}
12+
13+
export function getTsFunctionDisplayLabel(fn: Pick<TsFunctionLike, 'expandedTargetTypes'>): 'Function' | 'Method' {
14+
return isTsExtensionStyleFunction(fn) ? 'Method' : 'Function';
15+
}
16+
17+
export function getTsCallableIdentityKey(callable: TsFunctionLike): string {
18+
return callable.capabilityId ?? `${callable.qualifiedName ?? callable.name}::${callable.signature ?? ''}`;
19+
}

0 commit comments

Comments
 (0)