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
12 changes: 12 additions & 0 deletions scripts/verify-static-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,18 @@ for (const [route, html] of documents) {
}
}

const lucideRoot = documents.get('/docs/reference/api/lucide/root') ?? '';
assert(
/id="fullscreen-icon"[\s\S]*?<svg[^>]*data-icon="FullscreenIcon"/.test(
lucideRoot
),
'/docs/reference/api/lucide/root must render the FullscreenIcon tile'
);
assert(
lucideRoot.includes('href="https://lucide.dev/"'),
'/docs/reference/api/lucide/root must attribute the upstream Lucide project'
);

for (const asset of [
'assets/askr-logo-64.avif',
'assets/askr-logo-64.png',
Expand Down
6 changes: 5 additions & 1 deletion src/pages/docs/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function packageReference(

const pageLoader = () => import('./page');
const apiLoader = () => import('./api-page');
const lucideApiLoader = () => import('./lucide-api-page');

type PageInput = {
title: string;
Expand Down Expand Up @@ -1560,7 +1561,10 @@ const apiPages: DocsPageDefinition[] = apiManifest.map((entrypoint) => ({
},
],
keywords: [entrypoint.importName, entrypoint.packageName],
loader: apiLoader,
loader:
entrypoint.packageName === '@askrjs/lucide' && entrypoint.subpath === '.'
? lucideApiLoader
: apiLoader,
}));

const orderedPages = [...authoredPages, ...apiPages];
Expand Down
145 changes: 145 additions & 0 deletions src/pages/docs/lucide-api-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { For, state } from '@askrjs/askr';
import { Link, currentRoute } from '@askrjs/askr/router';
import { ArrowLeftIcon, SearchIcon } from '@askrjs/lucide';
import { apiManifest } from './api-manifest';
import { apiSymbolSets } from './api-snapshot';
import { resolveDocsRoute } from './catalog';
import { lucideIconsByName } from './lucide-icons';

const entrypoint = apiManifest.find(
(candidate) =>
candidate.packageName === '@askrjs/lucide' && candidate.subpath === '.'
)!;
const symbols = apiSymbolSets[entrypoint.symbolSet];
Comment on lines +9 to +13
const iconSymbols = symbols.filter((symbol) =>
lucideIconsByName.has(symbol.name)
);
const typeSymbols = symbols.filter(
(symbol) => !lucideIconsByName.has(symbol.name)
);

function revealRequestedIcon(element: HTMLElement, anchor: string) {
if (typeof window !== 'undefined' && window.location.hash === `#${anchor}`)
window.requestAnimationFrame(() => element.scrollIntoView());
}

export default function LucideApiPage() {
const route = resolveDocsRoute(currentRoute());
const [query, setQuery] = state('');
const visibleIcons = () => {
const normalized = query().trim().toLowerCase();
return normalized
? iconSymbols.filter((symbol) =>
symbol.name.toLowerCase().includes(normalized)
)
: iconSymbols;
};

return (
<article class="docs-article docs-api-page" data-docs-route={route}>
<nav class="docs-breadcrumbs" aria-label="Breadcrumb">
<Link href="/docs">Docs</Link>
<span>/</span>
<Link href="/docs/reference">Reference</Link>
<span>/</span>
<Link href="/docs/reference/api">API</Link>
<span>/</span>
<span aria-current="page">{entrypoint.importName}</span>
</nav>
<header class="docs-article__header">
<div class="docs-eyebrow">
<span>Generated API snapshot</span>
</div>
<h1>{entrypoint.importName}</h1>
<p>
Browse the icon components published in {entrypoint.packageName}{' '}
{entrypoint.version}. The icon artwork comes from the{' '}
<a href="https://lucide.dev/" rel="external">
Lucide icon project
</a>
.
</p>
<ul class="package-badges">
<li>
<code>{entrypoint.importName}</code>
<span>{entrypoint.version}</span>
</li>
</ul>
</header>
<section aria-labelledby="exports">
<h2 id="exports" class="anchored-heading">
<a href="#exports">Exports</a>
</h2>
<p>
Search {iconSymbols.length} icon components. Each tile has a stable
anchor for direct links and shows the component name to import.
</p>
<label class="gallery-search">
<SearchIcon size={18} aria-hidden="true" />
<span class="sr-only">Filter Lucide icons</span>
<input
type="search"
value={query()}
placeholder={`Filter ${iconSymbols.length} icons`}
onInput={(event: Event) =>
setQuery((event.target as HTMLInputElement).value)
}
/>
</label>
<p class="gallery-count">{visibleIcons().length} icons</p>
<div class="icon-gallery icon-gallery--api">
<For each={visibleIcons} by={(symbol) => symbol.name}>
{(symbol) => {
const Icon = lucideIconsByName.get(symbol.name)!;
return (
<article
id={symbol.anchor}
class="icon-gallery__item"
key={symbol.name}
title={symbol.name}
ref={(element: HTMLElement) =>
revealRequestedIcon(element, symbol.anchor)
}
>
<a
class="icon-gallery__anchor"
href={`#${symbol.anchor}`}
aria-label={`Link to ${symbol.name}`}
>
<Icon size={26} aria-hidden="true" />
<code>{symbol.name}</code>
</a>
</article>
);
}}
</For>
</div>
<div class="api-symbols api-symbols--types">
<For each={typeSymbols} by={(symbol) => symbol.name}>
{(symbol) => (
<article id={symbol.anchor} class="api-symbol" key={symbol.name}>
<h3>
<a href={`#${symbol.anchor}`}>
<code>{symbol.name}</code>
</a>
<span>type</span>
</h3>
<pre>
<code>{symbol.signature}</code>
</pre>
</article>
)}
</For>
</div>
</section>
<nav class="docs-pagination" aria-label="Documentation pagination">
<Link href="/docs/reference/api">
<ArrowLeftIcon size={16} aria-hidden="true" />
<span>
<small>Back to</small>API Index
</span>
</Link>
</nav>
</article>
);
}
36 changes: 20 additions & 16 deletions src/pages/docs/lucide-gallery.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { For, state } from '@askrjs/askr';
import { Link } from '@askrjs/askr/router';
import * as iconExports from '@askrjs/lucide';
import { ArrowLeftIcon, CircleIcon, SearchIcon } from '@askrjs/lucide';
import { ArrowLeftIcon, SearchIcon } from '@askrjs/lucide';
import { apiManifest } from './api-manifest';
import { lucideIcons } from './lucide-icons';

type IconComponent = typeof CircleIcon;
const icons = Object.entries(iconExports)
.filter(
([name, value]) => name.endsWith('Icon') && typeof value === 'function'
)
.map(([name, value]) => ({ name, Icon: value as IconComponent }))
.sort((left, right) => left.name.localeCompare(right.name));
const lucideVersion = apiManifest.find(
(entrypoint) =>
entrypoint.packageName === '@askrjs/lucide' && entrypoint.subpath === '.'
)!.version;
Comment on lines +7 to +10

export default function LucideGalleryPage() {
const [query, setQuery] = state('');
const visible = () => {
const normalized = query().trim().toLowerCase();
return normalized
? icons.filter((icon) => icon.name.toLowerCase().includes(normalized))
: icons;
? lucideIcons.filter((icon) =>
icon.name.toLowerCase().includes(normalized)
)
: lucideIcons;
};
return (
<article
Expand All @@ -37,14 +37,18 @@ export default function LucideGalleryPage() {
</div>
<h1>Lucide Gallery</h1>
<p>
Search every icon exported by @askrjs/lucide 0.0.4. Import the
selected component by name and give icon-only controls an accessible
label.
Search every icon exported by @askrjs/lucide {lucideVersion}. The icon
artwork comes from the{' '}
<a href="https://lucide.dev/" rel="external">
Lucide icon project
</a>
. Import the selected component by name and give icon-only controls an
accessible label.
</p>
<ul class="package-badges">
<li>
<code>@askrjs/lucide</code>
<span>0.0.4</span>
<span>{lucideVersion}</span>
</li>
</ul>
</header>
Expand All @@ -58,7 +62,7 @@ export default function LucideGalleryPage() {
<input
type="search"
value={query()}
placeholder={`Filter ${icons.length} icons`}
placeholder={`Filter ${lucideIcons.length} icons`}
onInput={(event: Event) =>
setQuery((event.currentTarget as HTMLInputElement).value)
}
Expand Down
18 changes: 18 additions & 0 deletions src/pages/docs/lucide-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as iconExports from '@askrjs/lucide';
import { CircleIcon } from '@askrjs/lucide';

export type LucideIconComponent = typeof CircleIcon;

export const lucideIcons = Object.entries(iconExports)
.filter(
([name, value]) => name.endsWith('Icon') && typeof value === 'function'
)
.map(([name, value]) => ({
name,
Icon: value as LucideIconComponent,
}))
.sort((left, right) => left.name.localeCompare(right.name));

export const lucideIconsByName = new Map(
lucideIcons.map(({ name, Icon }) => [name, Icon])
);
Loading