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
5 changes: 3 additions & 2 deletions website/src/components/CodeBlock.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import { EN, type Locale, t } from '../i18n';
import { highlight } from '../lib/highlight';
import ControlIcon from './ControlIcon';

interface Props {
code: string;
Expand All @@ -25,7 +26,7 @@ const html = await highlight(code, lang);
data-copy-from="[data-code]"
data-track="copy"
{...trackData}
class="absolute top-2 right-2 rounded-lg border border-line bg-surface px-2 py-1 text-xs text-muted opacity-0 transition group-hover/code:opacity-100 focus:opacity-100 data-[copied]:text-accent"
><span data-copy-label>{t(locale).common.copy}</span></button
class="absolute top-2 right-2 inline-flex items-center gap-1 rounded-lg border border-line bg-surface px-2 py-1 text-xs text-muted opacity-0 transition group-hover/code:opacity-100 focus:opacity-100 data-[copied]:text-accent"
><ControlIcon client:idle icon="copy" size={14} /><span data-copy-label>{t(locale).common.copy}</span></button
>
</div>
88 changes: 88 additions & 0 deletions website/src/components/ControlIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useEffect, useState } from 'react';
import UseAnimations from 'react-useanimations';
import arrowRight from '../../../icons/arrowRight.json';
import chevronDown from '../../../icons/chevronDown.json';
import copy from '../../../icons/copy.json';
import cpu from '../../../icons/cpu.json';
import droplet from '../../../icons/droplet.json';
import packageIcon from '../../../icons/package.json';
import plusToX from '../../../icons/plusToX.json';
import server from '../../../icons/server.json';
import users from '../../../icons/users.json';

const ICONS = {
arrowRight,
chevronDown,
copy,
cpu,
droplet,
package: packageIcon,
plusToX,
server,
users,
};

/**
* An icon inside Astro markup, played by the element around it, which isn't React's. So the icon
* is a toggle driven from that element's events:
* - `hover`: forward while its closest link, button, label or `[data-icon-trigger]` is hovered or
* keyboard-focused, back on leave;
* - `open`: at its end state while its closest `<details>` is open (e.g. plus to X).
*/
export default function ControlIcon({
icon,
on = 'hover',
size = 16,
className,
}: {
icon: keyof typeof ICONS;
on?: 'hover' | 'open';
size?: number;
className?: string;
}) {
const [span, setSpan] = useState<HTMLElement | null>(null);
const [active, setActive] = useState(false);

useEffect(() => {
if (on === 'open') {
const details = span?.closest('details');
if (!details) return undefined;
const sync = () => setActive(details.open);
sync();
details.addEventListener('toggle', sync);
return () => details.removeEventListener('toggle', sync);
}
const target = span?.closest('a, button, label, [data-icon-trigger]');
if (!target) return undefined;
// Touch devices fire pointer events on tap; only a mouse hovers.
const enter = (event: PointerEvent) => event.pointerType === 'mouse' && setActive(true);
const leave = (event: PointerEvent) => event.pointerType === 'mouse' && setActive(false);
const focus = (event: FocusEvent) =>
(event.target as Element).matches(':focus-visible') && setActive(true);
const blur = () => setActive(false);
const events = [
['pointerenter', enter],
['pointerleave', leave],
['focusin', focus],
['focusout', blur],
] as [string, EventListener][];
for (const [type, listener] of events) target.addEventListener(type, listener);
return () => {
for (const [type, listener] of events) target.removeEventListener(type, listener);
};
}, [span, on]);

return (
<UseAnimations
animation={{ animationData: ICONS[icon], interaction: 'click-toggle' }}
active={active}
size={size}
className={className}
render={(_eventProps, animationProps) => (
<span ref={setSpan} className="contents">
<span {...animationProps} />
</span>
)}
/>
);
}
10 changes: 5 additions & 5 deletions website/src/components/InstallCommand.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { EN, type Locale, t } from '../i18n';
import ControlIcon from './ControlIcon';

interface Props {
command?: string;
Expand All @@ -23,9 +24,8 @@ const { command = 'npm i react-useanimations', class: className, locale = EN } =
<!-- No aria-label: the name is the visible text ('npm i react-useanimations Copy'). -->
<span class="text-subtle select-none" aria-hidden="true">$</span>
<span>{command}</span>
<span
data-copy-label
class="rounded-full bg-surface-2 px-2.5 py-1 font-sans text-xs text-muted transition group-hover:text-fg"
>{t(locale).common.copy}</span
>
<span class="inline-flex items-center gap-1.5 rounded-full bg-surface-2 px-2.5 py-1 font-sans text-xs text-muted transition group-hover:text-fg">
<ControlIcon client:idle icon="copy" size={14} />
<span data-copy-label>{t(locale).common.copy}</span>
</span>
</button>
63 changes: 63 additions & 0 deletions website/src/components/SearchIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useState } from 'react';
import UseAnimations from 'react-useanimations';
import searchToX from '../../../icons/searchToX.json';
import { islands } from '../i18n/messages/en/islands';

const fieldOf = (button: HTMLButtonElement | null) =>
button?.closest('label')?.querySelector<HTMLInputElement>('[data-search]');

/**
* The icon of the browser's search field (the input with `data-search` in the same label): a
* magnifier that turns into a clear button while the field has text.
*/
export default function SearchIcon({
strings = islands.search,
}: {
/** The button's text in the page's language: t(locale).islands.search. */
strings?: typeof islands.search;
}) {
const [button, setButton] = useState<HTMLButtonElement | null>(null);
const [filled, setFilled] = useState(false);

useEffect(() => {
const input = fieldOf(button);
if (!input) return undefined;
// scripts/browser.ts may have filled the field from ?q= before this island hydrated.
const sync = () => setFilled(input.value !== '');
sync();
input.addEventListener('input', sync);
return () => input.removeEventListener('input', sync);
}, [button]);

// The browser script filters on the field's input event, so clearing goes through it too.
const clear = () => {
const input = fieldOf(button);
if (!input) return;
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.focus();
};

return (
<UseAnimations
animation={{ animationData: searchToX, interaction: 'click-toggle' }}
active={filled}
size={18}
onClick={clear}
render={({ onClick }, animationProps) => (
<button
type="button"
ref={setButton}
onClick={onClick}
aria-label={strings.clear}
// Only the X is a button; the magnifier is a picture.
aria-hidden={!filled}
tabIndex={filled ? 0 : -1}
className={`absolute top-1/2 left-2.5 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-full text-subtle transition-colors hover:text-fg ${filled ? '' : 'pointer-events-none'}`}
>
<span {...animationProps} />
</button>
)}
/>
);
}
2 changes: 1 addition & 1 deletion website/src/i18n/messages/cs/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const home: Messages['home'] = {
title: 'Nainstalovat, importovat, hotovo.',
intro:
'React komponenta bez závislostí, registr shadcn, soubory ke stažení se stálými URL a katalog, který přečtou i AI agenti.',
docs: 'Přečíst dokumentaci →',
docs: 'Přečíst dokumentaci',
tablist: 'Platforma',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Soubory', agents: 'Agenti' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/cs/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'Menu',
nav: 'Hlavní navigace',
},
search: {
clear: 'Vymazat hledání',
},
themeToggle: {
toLight: 'Přepnout na světlý motiv',
toDark: 'Přepnout na tmavý motiv',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/de/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const home: Messages['home'] = {
title: 'Installieren, importieren, fertig.',
intro:
'Eine React-Komponente ohne Abhängigkeiten, eine shadcn-Registry, Dateien zum Herunterladen unter stabilen URLs und ein Katalog, den KI-Agenten lesen können.',
docs: 'Zur Dokumentation →',
docs: 'Zur Dokumentation',
tablist: 'Plattform',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Dateien', agents: 'Agenten' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/de/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'Menü',
nav: 'Hauptnavigation',
},
search: {
clear: 'Suche löschen',
},
themeToggle: {
toLight: 'Zum hellen Design wechseln',
toDark: 'Zum dunklen Design wechseln',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/en/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const home = {
title: 'Install, import, done.',
intro:
'A React component with no dependencies, a shadcn registry, downloadable files with stable URLs, and a catalog that AI agents can read.',
docs: 'Read the documentation →',
docs: 'Read the documentation',
/** Accessible name of the tab list (screen readers only). */
tablist: 'Platform',
/** Tab names; React and shadcn are names and stay as they are. */
Expand Down
4 changes: 4 additions & 0 deletions website/src/i18n/messages/en/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export const islands = {
menu: 'Menu',
nav: 'Main',
},
/** The search field of the icon browser: its X clears the search. */
search: {
clear: 'Clear search',
},
themeToggle: {
toLight: 'Switch to light theme',
toDark: 'Switch to dark theme',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/es/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const home: Messages['home'] = {
title: 'Instala, importa y listo.',
intro:
'Un componente de React sin dependencias, un registro de shadcn, archivos descargables con URL estables y un catálogo que pueden leer los agentes de IA.',
docs: 'Leer la documentación →',
docs: 'Leer la documentación',
tablist: 'Plataforma',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Archivos', agents: 'Agentes' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/es/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const islands: Messages['islands'] = {
menu: 'Menú',
nav: 'Principal',
},
search: {
clear: 'Borrar la búsqueda',
},
themeToggle: {
toLight: 'Cambiar al tema claro',
toDark: 'Cambiar al tema oscuro',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/fr/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const home: Messages['home'] = {
title: 'Installer, importer, c’est tout.',
intro:
'Un composant React sans dépendance, un registre shadcn, des fichiers téléchargeables aux URL stables et un catalogue lisible par les agents IA.',
docs: 'Lire la documentation →',
docs: 'Lire la documentation',
tablist: 'Plateforme',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Fichiers', agents: 'Agents' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/fr/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'Menu',
nav: 'Navigation principale',
},
search: {
clear: 'Effacer la recherche',
},
themeToggle: {
toLight: 'Passer au thème clair',
toDark: 'Passer au thème sombre',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/id/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const home: Messages['home'] = {
title: 'Instal, import, selesai.',
intro:
'Komponen React tanpa dependensi, registry shadcn, file yang bisa diunduh dengan URL tetap, dan katalog yang bisa dibaca agen AI.',
docs: 'Baca dokumentasi →',
docs: 'Baca dokumentasi',
tablist: 'Platform',
tabs: { react: 'React', shadcn: 'shadcn', files: 'File', agents: 'Agen AI' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/id/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'Menu',
nav: 'Utama',
},
search: {
clear: 'Hapus pencarian',
},
themeToggle: {
toLight: 'Beralih ke tema terang',
toDark: 'Beralih ke tema gelap',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/ja/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const home: Messages['home'] = {
title: 'インストールして、インポートするだけ。',
intro:
'依存関係のない React コンポーネント、shadcn レジストリ、URL が固定されたダウンロード用ファイル、そして AI エージェントが読めるカタログを用意しています。',
docs: 'ドキュメントを読む →',
docs: 'ドキュメントを読む',
tablist: 'プラットフォーム',
tabs: { react: 'React', shadcn: 'shadcn', files: 'ファイル', agents: 'エージェント' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/ja/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'メニュー',
nav: 'メイン',
},
search: {
clear: '検索をクリア',
},
themeToggle: {
toLight: 'ライトテーマに切り替える',
toDark: 'ダークテーマに切り替える',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/ko/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const home: Messages['home'] = {
title: '설치하고, 가져오면 끝.',
intro:
'의존성 없는 React 컴포넌트, shadcn 레지스트리, 고정 URL로 내려받는 파일, 그리고 AI 에이전트가 읽을 수 있는 카탈로그를 제공합니다.',
docs: '문서 읽기 →',
docs: '문서 읽기',
tablist: '플랫폼',
tabs: { react: 'React', shadcn: 'shadcn', files: '파일', agents: '에이전트' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/ko/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const islands: Messages['islands'] = {
menu: '메뉴',
nav: '주 메뉴',
},
search: {
clear: '검색어 지우기',
},
themeToggle: {
toLight: '라이트 테마로 전환',
toDark: '다크 테마로 전환',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/pt-br/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const home: Messages['home'] = {
title: 'Instale, importe, pronto.',
intro:
'Um componente React sem dependências, um registry do shadcn, arquivos para download com URLs estáveis e um catálogo que agentes de IA conseguem ler.',
docs: 'Leia a documentação →',
docs: 'Leia a documentação',
tablist: 'Plataforma',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Arquivos', agents: 'Agentes' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/pt-br/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'Menu',
nav: 'Principal',
},
search: {
clear: 'Limpar a pesquisa',
},
themeToggle: {
toLight: 'Mudar para o tema claro',
toDark: 'Mudar para o tema escuro',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/ru/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const home: Messages['home'] = {
title: 'Установили, импортировали, готово.',
intro:
'React-компонент без зависимостей, shadcn-реестр, файлы для скачивания по постоянным URL и каталог, который умеют читать ИИ-агенты.',
docs: 'Читать документацию →',
docs: 'Читать документацию',
tablist: 'Платформа',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Файлы', agents: 'Агенты' },
codeText: {
Expand Down
3 changes: 3 additions & 0 deletions website/src/i18n/messages/ru/islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = {
menu: 'Меню',
nav: 'Основная навигация',
},
search: {
clear: 'Очистить поиск',
},
themeToggle: {
toLight: 'Включить светлую тему',
toDark: 'Включить тёмную тему',
Expand Down
2 changes: 1 addition & 1 deletion website/src/i18n/messages/tr/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const home: Messages['home'] = {
title: 'Kur, import et, bitti.',
intro:
'Bağımlılığı olmayan bir React bileşeni, bir shadcn registry, kalıcı URL’lerden indirilebilen dosyalar ve yapay zekâ ajanlarının okuyabildiği bir katalog.',
docs: 'Dokümantasyonu okuyun →',
docs: 'Dokümantasyonu okuyun',
tablist: 'Platform',
tabs: { react: 'React', shadcn: 'shadcn', files: 'Dosyalar', agents: 'Ajanlar' },
codeText: {
Expand Down
Loading
Loading