diff --git a/website/src/components/CodeBlock.astro b/website/src/components/CodeBlock.astro index e6c1f7c..304d9c6 100644 --- a/website/src/components/CodeBlock.astro +++ b/website/src/components/CodeBlock.astro @@ -1,6 +1,7 @@ --- import { EN, type Locale, t } from '../i18n'; import { highlight } from '../lib/highlight'; +import ControlIcon from './ControlIcon'; interface Props { code: string; @@ -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" - >{t(locale).common.copy}{t(locale).common.copy} diff --git a/website/src/components/ControlIcon.tsx b/website/src/components/ControlIcon.tsx new file mode 100644 index 0000000..6c2f526 --- /dev/null +++ b/website/src/components/ControlIcon.tsx @@ -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 `
` 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(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 ( + ( + + + + )} + /> + ); +} diff --git a/website/src/components/InstallCommand.astro b/website/src/components/InstallCommand.astro index 6fc36fb..dbb9cbe 100644 --- a/website/src/components/InstallCommand.astro +++ b/website/src/components/InstallCommand.astro @@ -1,5 +1,6 @@ --- import { EN, type Locale, t } from '../i18n'; +import ControlIcon from './ControlIcon'; interface Props { command?: string; @@ -23,9 +24,8 @@ const { command = 'npm i react-useanimations', class: className, locale = EN } = {command} - {t(locale).common.copy} + + + {t(locale).common.copy} + diff --git a/website/src/components/SearchIcon.tsx b/website/src/components/SearchIcon.tsx new file mode 100644 index 0000000..7823fe3 --- /dev/null +++ b/website/src/components/SearchIcon.tsx @@ -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('[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(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 ( + ( + + )} + /> + ); +} diff --git a/website/src/i18n/messages/cs/home.ts b/website/src/i18n/messages/cs/home.ts index 9447a11..35842b3 100644 --- a/website/src/i18n/messages/cs/home.ts +++ b/website/src/i18n/messages/cs/home.ts @@ -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: { diff --git a/website/src/i18n/messages/cs/islands.ts b/website/src/i18n/messages/cs/islands.ts index c152301..5ffd907 100644 --- a/website/src/i18n/messages/cs/islands.ts +++ b/website/src/i18n/messages/cs/islands.ts @@ -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', diff --git a/website/src/i18n/messages/de/home.ts b/website/src/i18n/messages/de/home.ts index 4040078..d914239 100644 --- a/website/src/i18n/messages/de/home.ts +++ b/website/src/i18n/messages/de/home.ts @@ -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: { diff --git a/website/src/i18n/messages/de/islands.ts b/website/src/i18n/messages/de/islands.ts index da6f564..d8361f3 100644 --- a/website/src/i18n/messages/de/islands.ts +++ b/website/src/i18n/messages/de/islands.ts @@ -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', diff --git a/website/src/i18n/messages/en/home.ts b/website/src/i18n/messages/en/home.ts index f38c867..42b394e 100644 --- a/website/src/i18n/messages/en/home.ts +++ b/website/src/i18n/messages/en/home.ts @@ -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. */ diff --git a/website/src/i18n/messages/en/islands.ts b/website/src/i18n/messages/en/islands.ts index 405a2c4..2421e02 100644 --- a/website/src/i18n/messages/en/islands.ts +++ b/website/src/i18n/messages/en/islands.ts @@ -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', diff --git a/website/src/i18n/messages/es/home.ts b/website/src/i18n/messages/es/home.ts index 8dfbada..4204f74 100644 --- a/website/src/i18n/messages/es/home.ts +++ b/website/src/i18n/messages/es/home.ts @@ -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: { diff --git a/website/src/i18n/messages/es/islands.ts b/website/src/i18n/messages/es/islands.ts index 8e5f08c..855963c 100644 --- a/website/src/i18n/messages/es/islands.ts +++ b/website/src/i18n/messages/es/islands.ts @@ -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', diff --git a/website/src/i18n/messages/fr/home.ts b/website/src/i18n/messages/fr/home.ts index df72cf1..2bb25ca 100644 --- a/website/src/i18n/messages/fr/home.ts +++ b/website/src/i18n/messages/fr/home.ts @@ -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: { diff --git a/website/src/i18n/messages/fr/islands.ts b/website/src/i18n/messages/fr/islands.ts index 13aeee9..06b9f53 100644 --- a/website/src/i18n/messages/fr/islands.ts +++ b/website/src/i18n/messages/fr/islands.ts @@ -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', diff --git a/website/src/i18n/messages/id/home.ts b/website/src/i18n/messages/id/home.ts index 019a3b7..49d63c6 100644 --- a/website/src/i18n/messages/id/home.ts +++ b/website/src/i18n/messages/id/home.ts @@ -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: { diff --git a/website/src/i18n/messages/id/islands.ts b/website/src/i18n/messages/id/islands.ts index b31a45a..984aea5 100644 --- a/website/src/i18n/messages/id/islands.ts +++ b/website/src/i18n/messages/id/islands.ts @@ -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', diff --git a/website/src/i18n/messages/ja/home.ts b/website/src/i18n/messages/ja/home.ts index f51f093..e79b3ee 100644 --- a/website/src/i18n/messages/ja/home.ts +++ b/website/src/i18n/messages/ja/home.ts @@ -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: { diff --git a/website/src/i18n/messages/ja/islands.ts b/website/src/i18n/messages/ja/islands.ts index 542bab2..1a4a414 100644 --- a/website/src/i18n/messages/ja/islands.ts +++ b/website/src/i18n/messages/ja/islands.ts @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = { menu: 'メニュー', nav: 'メイン', }, + search: { + clear: '検索をクリア', + }, themeToggle: { toLight: 'ライトテーマに切り替える', toDark: 'ダークテーマに切り替える', diff --git a/website/src/i18n/messages/ko/home.ts b/website/src/i18n/messages/ko/home.ts index 6617a79..1d70cce 100644 --- a/website/src/i18n/messages/ko/home.ts +++ b/website/src/i18n/messages/ko/home.ts @@ -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: { diff --git a/website/src/i18n/messages/ko/islands.ts b/website/src/i18n/messages/ko/islands.ts index 39f1627..98c7e46 100644 --- a/website/src/i18n/messages/ko/islands.ts +++ b/website/src/i18n/messages/ko/islands.ts @@ -45,6 +45,9 @@ export const islands: Messages['islands'] = { menu: '메뉴', nav: '주 메뉴', }, + search: { + clear: '검색어 지우기', + }, themeToggle: { toLight: '라이트 테마로 전환', toDark: '다크 테마로 전환', diff --git a/website/src/i18n/messages/pt-br/home.ts b/website/src/i18n/messages/pt-br/home.ts index 0c5dced..7e12052 100644 --- a/website/src/i18n/messages/pt-br/home.ts +++ b/website/src/i18n/messages/pt-br/home.ts @@ -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: { diff --git a/website/src/i18n/messages/pt-br/islands.ts b/website/src/i18n/messages/pt-br/islands.ts index bcd4dcd..54f64a6 100644 --- a/website/src/i18n/messages/pt-br/islands.ts +++ b/website/src/i18n/messages/pt-br/islands.ts @@ -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', diff --git a/website/src/i18n/messages/ru/home.ts b/website/src/i18n/messages/ru/home.ts index 02f10d0..adbc6e8 100644 --- a/website/src/i18n/messages/ru/home.ts +++ b/website/src/i18n/messages/ru/home.ts @@ -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: { diff --git a/website/src/i18n/messages/ru/islands.ts b/website/src/i18n/messages/ru/islands.ts index 3d007a5..0faa44d 100644 --- a/website/src/i18n/messages/ru/islands.ts +++ b/website/src/i18n/messages/ru/islands.ts @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = { menu: 'Меню', nav: 'Основная навигация', }, + search: { + clear: 'Очистить поиск', + }, themeToggle: { toLight: 'Включить светлую тему', toDark: 'Включить тёмную тему', diff --git a/website/src/i18n/messages/tr/home.ts b/website/src/i18n/messages/tr/home.ts index 2408201..f74bb8e 100644 --- a/website/src/i18n/messages/tr/home.ts +++ b/website/src/i18n/messages/tr/home.ts @@ -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: { diff --git a/website/src/i18n/messages/tr/islands.ts b/website/src/i18n/messages/tr/islands.ts index ce12a97..b30e005 100644 --- a/website/src/i18n/messages/tr/islands.ts +++ b/website/src/i18n/messages/tr/islands.ts @@ -44,6 +44,9 @@ export const islands: Messages['islands'] = { menu: 'Menü', nav: 'Ana menü', }, + search: { + clear: 'Aramayı temizle', + }, themeToggle: { toLight: 'Açık temaya geç', toDark: 'Koyu temaya geç', diff --git a/website/src/i18n/messages/vi/home.ts b/website/src/i18n/messages/vi/home.ts index 4fffcb7..98a66c4 100644 --- a/website/src/i18n/messages/vi/home.ts +++ b/website/src/i18n/messages/vi/home.ts @@ -77,7 +77,7 @@ export const home: Messages['home'] = { title: 'Cài, import, xong.', intro: 'Một React component không có dependency, một shadcn registry, các tệp tải về với URL cố định, và một catalog mà AI agent đọc được.', - docs: 'Đọc tài liệu →', + docs: 'Đọc tài liệu', tablist: 'Nền tảng', tabs: { react: 'React', shadcn: 'shadcn', files: 'Tệp', agents: 'Agent' }, codeText: { diff --git a/website/src/i18n/messages/vi/islands.ts b/website/src/i18n/messages/vi/islands.ts index 2a3e170..563bfe8 100644 --- a/website/src/i18n/messages/vi/islands.ts +++ b/website/src/i18n/messages/vi/islands.ts @@ -45,6 +45,9 @@ export const islands: Messages['islands'] = { menu: 'Menu', nav: 'Điều hướng chính', }, + search: { + clear: 'Xóa tìm kiếm', + }, themeToggle: { toLight: 'Chuyển sang giao diện sáng', toDark: 'Chuyển sang giao diện tối', diff --git a/website/src/i18n/messages/zh/home.ts b/website/src/i18n/messages/zh/home.ts index 242dfa8..30b037a 100644 --- a/website/src/i18n/messages/zh/home.ts +++ b/website/src/i18n/messages/zh/home.ts @@ -75,7 +75,7 @@ export const home: Messages['home'] = { title: '安装、导入,就这么简单。', intro: '零依赖的 React 组件、shadcn 注册表、带固定网址的可下载文件,以及 AI 智能体可以读取的图标目录。', - docs: '阅读文档 →', + docs: '阅读文档', tablist: '平台', tabs: { react: 'React', shadcn: 'shadcn', files: '文件', agents: '智能体' }, codeText: { diff --git a/website/src/i18n/messages/zh/islands.ts b/website/src/i18n/messages/zh/islands.ts index ac3ea97..79751cd 100644 --- a/website/src/i18n/messages/zh/islands.ts +++ b/website/src/i18n/messages/zh/islands.ts @@ -45,6 +45,9 @@ export const islands: Messages['islands'] = { menu: '菜单', nav: '主导航', }, + search: { + clear: '清除搜索', + }, themeToggle: { toLight: '切换到浅色主题', toDark: '切换到深色主题', diff --git a/website/src/styles/global.css b/website/src/styles/global.css index 485cd22..73fa9dc 100644 --- a/website/src/styles/global.css +++ b/website/src/styles/global.css @@ -351,6 +351,11 @@ } } +/* The search field clears with its own animated X (components/SearchIcon.tsx), not the browser's. */ +[data-search]::-webkit-search-cancel-button { + appearance: none; +} + /* ICON BROWSER: one card per icon (components/IconCard.tsx). Plain classes instead of utility lists, which repeated 300 times on the home page. */ .icon-card { diff --git a/website/src/views/HomePage.astro b/website/src/views/HomePage.astro index 2eecaf6..ab775d4 100644 --- a/website/src/views/HomePage.astro +++ b/website/src/views/HomePage.astro @@ -3,11 +3,13 @@ import type { Interaction } from 'react-useanimations'; import CodeBlock from '../components/CodeBlock.astro'; import ColorRow from '../components/ColorRow'; +import ControlIcon from '../components/ControlIcon'; import IconCard from '../components/IconCard'; import IconDialog, { type DialogIcon, type Templates } from '../components/IconDialog'; import IconField from '../components/IconField.astro'; import InstallCommand from '../components/InstallCommand.astro'; import InteractionDemo from '../components/InteractionDemo'; +import SearchIcon from '../components/SearchIcon'; import { alternatesOf, EN, type Locale, localize, t } from '../i18n'; import Layout from '../layouts/Layout.astro'; import { highlight } from '../lib/highlight'; @@ -244,11 +246,11 @@ const jsonLd = {
v3 {h.hero.badge(icons.length)} - → +

{h.hero.headline.start}{h.hero.headline.accent}{h.hero.headline.end} @@ -340,15 +342,7 @@ const jsonLd = {

{ @@ -490,7 +477,7 @@ const jsonLd = { {e.docs}{e.docs}
@@ -535,8 +522,10 @@ const jsonLd = { {f.title}
-
-

{f.colors}

+
+

+ {f.colors} +

-
-

{f.tiny}

+
+

+ {f.tiny} +

8 kB

{f.tinyText}

-
-

{f.accessible}

+
+

+ {f.accessible} +

{f.accessibleText}

-
-

{f.ssr}

+
+

+ {f.ssr} +

{f.ssrText}

-
-

{f.agents}

+
+

+ {f.agents} +

@@ -634,7 +631,7 @@ const jsonLd = {

{item.question}

- +

{item.answer} @@ -663,8 +660,10 @@ const jsonLd = {

{h.cta.text}

- {h.cta.start}{h.cta.start}