From e789bef26c651bff26dfb33b5b8884b99fbf634d Mon Sep 17 00:00:00 2001 From: Tuan Phung <16448321+tuanphungcz@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:41:06 +0200 Subject: [PATCH] Website: animated icons across the whole site, not only the home page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Icon stills (category, interaction, icon index, Lottie and shadcn grids, related icons, other versions, category and interaction cards) play on hover or keyboard focus. Each icon's Lottie JSON loads on the first hover (IconStill.astro, scripts/hover-icons.ts, HoverIcon.tsx); icons whose first frame is empty keep their still. - Copy buttons: the copy icon turns into a checkmark after a successful copy (install command, code blocks, icon dialog, documentation). copy.ts sets data-copied only on success. - Icon dialog: the close button is a plus that turns into an X on every open; download links get a download icon, "Icon page" an arrow. - Icon pages: breadcrumb chevrons, download icons, plus/X FAQ, arrows on "All … icons" and previous/next. - Arrows on the icon index category links, the Lottie and shadcn "All icons" links and the home FAQ links; the text arrows are gone from those labels in all 13 languages. - Language switcher and the documentation's "On this page" get a chevron, the language banner's close a plus/X, footer links that leave the site an external-link icon, the 404 page an alert that plays in and home/grid icons. - ControlIcon: new modes `screen` (plays in once on screen) and `copied`. --- website/src/components/CodeBlock.astro | 2 +- website/src/components/ControlIcon.tsx | 155 ++++++++++++++---- website/src/components/Footer.astro | 4 +- website/src/components/HoverIcon.tsx | 69 ++++++++ website/src/components/IconDialog.tsx | 28 ++-- website/src/components/IconGrid.astro | 3 +- website/src/components/IconStill.astro | 25 +++ website/src/components/InstallCommand.astro | 2 +- website/src/components/LanguageBanner.astro | 6 +- website/src/components/LanguageSwitcher.astro | 2 + website/src/i18n/messages/cs/islands.ts | 2 +- website/src/i18n/messages/cs/pages.ts | 6 +- website/src/i18n/messages/de/islands.ts | 2 +- website/src/i18n/messages/de/pages.ts | 6 +- website/src/i18n/messages/en/islands.ts | 2 +- website/src/i18n/messages/en/pages.ts | 6 +- website/src/i18n/messages/es/islands.ts | 2 +- website/src/i18n/messages/es/pages.ts | 6 +- website/src/i18n/messages/fr/islands.ts | 2 +- website/src/i18n/messages/fr/pages.ts | 6 +- website/src/i18n/messages/id/islands.ts | 2 +- website/src/i18n/messages/id/pages.ts | 6 +- website/src/i18n/messages/ja/islands.ts | 2 +- website/src/i18n/messages/ja/pages.ts | 6 +- website/src/i18n/messages/ko/islands.ts | 2 +- website/src/i18n/messages/ko/pages.ts | 6 +- website/src/i18n/messages/pt-br/islands.ts | 2 +- website/src/i18n/messages/pt-br/pages.ts | 6 +- website/src/i18n/messages/ru/islands.ts | 2 +- website/src/i18n/messages/ru/pages.ts | 6 +- website/src/i18n/messages/tr/islands.ts | 2 +- website/src/i18n/messages/tr/pages.ts | 6 +- website/src/i18n/messages/vi/islands.ts | 2 +- website/src/i18n/messages/vi/pages.ts | 6 +- website/src/i18n/messages/zh/islands.ts | 2 +- website/src/i18n/messages/zh/pages.ts | 6 +- website/src/pages/404.astro | 14 +- website/src/pages/icons/[id].astro | 50 ++++-- website/src/scripts/copy.ts | 9 +- website/src/scripts/hover-icons.ts | 34 ++++ website/src/views/CategoryIndexPage.astro | 3 +- website/src/views/DocumentationPage.astro | 18 +- website/src/views/HomePage.astro | 3 +- website/src/views/IconIndexPage.astro | 4 +- website/src/views/InteractionIndexPage.astro | 3 +- website/src/views/LottieIconsPage.astro | 3 +- website/src/views/ShadcnIconsPage.astro | 3 +- 47 files changed, 405 insertions(+), 139 deletions(-) create mode 100644 website/src/components/HoverIcon.tsx create mode 100644 website/src/components/IconStill.astro create mode 100644 website/src/scripts/hover-icons.ts diff --git a/website/src/components/CodeBlock.astro b/website/src/components/CodeBlock.astro index 304d9c6..b3a6e32 100644 --- a/website/src/components/CodeBlock.astro +++ b/website/src/components/CodeBlock.astro @@ -27,6 +27,6 @@ const html = await highlight(code, lang); data-track="copy" {...trackData} 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" - >{t(locale).common.copy}{t(locale).common.copy} diff --git a/website/src/components/ControlIcon.tsx b/website/src/components/ControlIcon.tsx index 6c2f526..72faead 100644 --- a/website/src/components/ControlIcon.tsx +++ b/website/src/components/ControlIcon.tsx @@ -1,58 +1,58 @@ import { useEffect, useState } from 'react'; import UseAnimations from 'react-useanimations'; +import alertTriangle from '../../../icons/alertTriangle.json'; +import arrowLeft from '../../../icons/arrowLeft.json'; import arrowRight from '../../../icons/arrowRight.json'; +import checkmark from '../../../icons/checkmark.json'; import chevronDown from '../../../icons/chevronDown.json'; +import chevronRight from '../../../icons/chevronRight.json'; import copy from '../../../icons/copy.json'; import cpu from '../../../icons/cpu.json'; +import download from '../../../icons/download.json'; import droplet from '../../../icons/droplet.json'; +import externalLink from '../../../icons/externalLink.json'; +import grid from '../../../icons/grid.json'; +import home from '../../../icons/home.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 = { + alertTriangle, + arrowLeft, arrowRight, + checkmark, chevronDown, + chevronRight, copy, cpu, + download, droplet, + externalLink, + grid, + home, 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); +type IconName = keyof typeof ICONS; +/** Whether the element around `wrapper` hovers, has keyboard focus, or (`open`) is an open ``. */ +function useTrigger(wrapper: HTMLElement | null, on: 'hover' | 'open') { + const [active, setActive] = useState(false); useEffect(() => { if (on === 'open') { - const details = span?.closest('details'); + const details = wrapper?.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]'); + const target = wrapper?.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); @@ -70,19 +70,108 @@ export default function ControlIcon({ return () => { for (const [type, listener] of events) target.removeEventListener(type, listener); }; - }, [span, on]); + }, [wrapper, on]); + return active; +} + +/** Whether the icon in `wrapper` is on screen; not while a closed dialog or a hidden banner holds it. */ +function useOnScreen(wrapper: HTMLElement | null) { + const [visible, setVisible] = useState(false); + useEffect(() => { + const icon = wrapper?.firstElementChild; + if (!icon) return undefined; + const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting)); + observer.observe(icon); + return () => observer.disconnect(); + }, [wrapper]); + return visible; +} + +/** Whether the copy button around `wrapper` reports a copy (scripts/copy.ts sets `data-copied`). */ +function useCopied(wrapper: HTMLElement | null, enabled: boolean) { + const [copied, setCopied] = useState(false); + useEffect(() => { + const button = enabled && wrapper?.closest('[data-copy], [data-copy-from]'); + if (!button) return undefined; + const observer = new MutationObserver(() => setCopied(button.hasAttribute('data-copied'))); + observer.observe(button, { attributeFilter: ['data-copied'] }); + return () => observer.disconnect(); + }, [wrapper, enabled]); + return copied; +} + +type Look = { size: number; className?: string }; +/** The icon as a toggle: forward to its end state while `active`, back when not. */ +const Toggle = ({ icon, active, size, className }: Look & { icon: IconName; active: boolean }) => ( + } + /> +); + +/** Plays in as soon as it's on screen. */ +function Arriving({ icon, ...look }: Look & { icon: IconName }) { + const [wrapper, setWrapper] = useState(null); + const visible = useOnScreen(wrapper); + return ( + + + + ); +} + +/** + * 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); + * - `screen`: at its end state while it's on screen, so it plays in on load or when the dialog or + * banner around it opens. + * + * With `copied`, a copy button around it shows that icon, played in, while it reports a copy. + */ +export default function ControlIcon({ + icon, + on = 'hover', + copied, + size = 16, + className, +}: { + icon: IconName; + on?: 'hover' | 'open' | 'screen'; + copied?: IconName; + size?: number; + className?: string; +}) { + const look = { size, className }; + return on === 'screen' ? ( + + ) : ( + + ); +} + +function Triggered({ + icon, + on, + copied, + ...look +}: Look & { icon: IconName; on: 'hover' | 'open'; copied?: IconName }) { + const [wrapper, setWrapper] = useState(null); + const active = useTrigger(wrapper, on); + const showCopied = useCopied(wrapper, copied !== undefined); return ( - ( - - - + + {copied && showCopied ? ( + + ) : ( + )} - /> + ); } diff --git a/website/src/components/Footer.astro b/website/src/components/Footer.astro index bf36c5e..ec62f18 100644 --- a/website/src/components/Footer.astro +++ b/website/src/components/Footer.astro @@ -1,4 +1,5 @@ --- +import ControlIcon from './ControlIcon'; import Logo from './Logo.astro'; import { BUILT, EN, type Locale, localize, t } from '../i18n'; import { footer } from '../lib/site'; @@ -28,8 +29,9 @@ const languages = BUILT.map((l) => ({ locale: l, href: alternates?.[l.hreflang] {column.links.map((link) => ( - + {link.label} + {link.href.startsWith('https:') && } ))} diff --git a/website/src/components/HoverIcon.tsx b/website/src/components/HoverIcon.tsx new file mode 100644 index 0000000..22ff140 --- /dev/null +++ b/website/src/components/HoverIcon.tsx @@ -0,0 +1,69 @@ +import { useEffect, useRef, useState } from 'react'; +import UseAnimations, { type EventProps, type Interaction } from 'react-useanimations'; + +// Inside a link a click navigates, so every icon plays on hover, in its closest hover form. +const ON_HOVER: Record = { + hover: 'hover', + 'hover-loop': 'hover-loop', + 'hover-replay': 'hover-replay', + loop: 'hover-loop', + 'click-replay': 'hover-replay', + 'click-toggle': 'hover', +}; + +/** + * An icon that plays while `trigger`, a link in Astro markup (not React's), is hovered or has + * keyboard focus. It mounts on the first hover (scripts/hover-icons.ts), so it starts right away + * when that hover is still going on. + */ +export default function HoverIcon({ + animationData, + interaction, + trigger, +}: { + animationData: object; + interaction: Interaction; + trigger: HTMLElement; +}) { + const handlers = useRef(null); + const [wrapper, setWrapper] = useState(null); + + useEffect(() => { + if (!wrapper) return undefined; + // The component's own hover and focus handlers, fed from the trigger's native events. + const call = (name: 'onMouseEnter' | 'onMouseLeave' | 'onFocus' | 'onBlur') => + handlers.current?.[name]({ defaultPrevented: false, currentTarget: trigger } as never); + // Touch devices fire pointer events on tap; only a mouse hovers. + const enter = (event: PointerEvent) => event.pointerType === 'mouse' && call('onMouseEnter'); + const leave = (event: PointerEvent) => event.pointerType === 'mouse' && call('onMouseLeave'); + const focus = () => call('onFocus'); + const blur = () => call('onBlur'); + const events = [ + ['pointerenter', enter], + ['pointerleave', leave], + ['focusin', focus], + ['focusout', blur], + ] as [string, EventListener][]; + for (const [type, listener] of events) trigger.addEventListener(type, listener); + if (trigger.matches(':hover')) call('onMouseEnter'); + else if (trigger.matches(':focus-visible')) call('onFocus'); + return () => { + for (const [type, listener] of events) trigger.removeEventListener(type, listener); + }; + }, [wrapper, trigger]); + + return ( + { + handlers.current = eventProps; + return ( + + + + ); + }} + /> + ); +} diff --git a/website/src/components/IconDialog.tsx b/website/src/components/IconDialog.tsx index 0d04f38..778dd1a 100644 --- a/website/src/components/IconDialog.tsx +++ b/website/src/components/IconDialog.tsx @@ -3,6 +3,7 @@ import UseAnimations, { type Interaction } from 'react-useanimations'; import { islands } from '../i18n/messages/en/islands'; import { loadIconData } from '../lib/icon-data'; import { tabFromKey } from '../lib/tabs'; +import ControlIcon from './ControlIcon'; import { openIcon } from './IconCard'; export type DialogIcon = { @@ -59,8 +60,9 @@ function Code({ data-track="copy" data-track-icon={icon} data-track-code={code} - className="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" + className="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" > + {label} @@ -200,16 +202,8 @@ export default function IconDialog({ aria-label={s.close} className="rounded-full p-2 text-muted hover:bg-surface-2 hover:text-fg" > - - - + {/* A plus that turns into an X each time the dialog opens. */} + + ZIP + Lottie JSON {icon.svg.map((state) => ( @@ -312,8 +308,9 @@ export default function IconDialog({ data-track-icon={icon.id} data-track-format="svg" data-track-file={`${state}.svg`} - className="rounded-full border border-line-strong px-4 py-2 text-sm hover:bg-surface-2" + className="inline-flex items-center gap-1.5 rounded-full border border-line-strong px-4 py-2 text-sm hover:bg-surface-2" > + {icon.svg.length > 1 ? `${state}.svg` : 'SVG'} ))} @@ -321,9 +318,10 @@ export default function IconDialog({ href={`/icons/${icon.id}`} data-track="icon-page" data-track-icon={icon.id} - className="ml-auto self-center text-sm text-muted underline-offset-4 hover:text-fg hover:underline" + className="ml-auto inline-flex items-center gap-1 self-center text-sm text-muted underline-offset-4 hover:text-fg hover:underline" > {s.iconPage} + diff --git a/website/src/components/IconGrid.astro b/website/src/components/IconGrid.astro index 024c9f0..968f865 100644 --- a/website/src/components/IconGrid.astro +++ b/website/src/components/IconGrid.astro @@ -1,5 +1,6 @@ --- import { EN, type Locale } from '../i18n'; +import IconStill from './IconStill.astro'; import type { Icon } from '../lib/icons'; interface Props { @@ -24,7 +25,7 @@ const lang = locale === EN ? undefined : 'en'; hreflang={lang} class="flex aspect-[1/1.08] flex-col items-center justify-center gap-3 rounded-2xl border border-line bg-surface px-2 text-center text-[13px] text-muted transition hover:border-line-strong hover:text-fg" > - + {lang ? {icon.title} : icon.title} diff --git a/website/src/components/IconStill.astro b/website/src/components/IconStill.astro new file mode 100644 index 0000000..49be5af --- /dev/null +++ b/website/src/components/IconStill.astro @@ -0,0 +1,25 @@ +--- +import type { Icon } from '../lib/icons'; + +interface Props { + icon: Icon; + class?: string; +} + +// An icon's still (lib/icons.ts) inside a link; it plays once the link is hovered or focused +// (scripts/hover-icons.ts). +const { icon, class: className } = Astro.props; +--- + + + + diff --git a/website/src/components/InstallCommand.astro b/website/src/components/InstallCommand.astro index dbb9cbe..dce1bce 100644 --- a/website/src/components/InstallCommand.astro +++ b/website/src/components/InstallCommand.astro @@ -25,7 +25,7 @@ const { command = 'npm i react-useanimations', class: className, locale = EN } = $ {command} - + {t(locale).common.copy} diff --git a/website/src/components/LanguageBanner.astro b/website/src/components/LanguageBanner.astro index 4aaa990..0bb29d0 100644 --- a/website/src/components/LanguageBanner.astro +++ b/website/src/components/LanguageBanner.astro @@ -3,6 +3,7 @@ // redirects (Google asks sites not to, and it would hide the other versions from crawlers and // people); it only suggests, in that language, and remembers a dismissal. import { BUILT, type Locale, t } from '../i18n'; +import ControlIcon from './ControlIcon'; interface Props { locale: Locale; @@ -25,8 +26,9 @@ const offers = BUILT.filter((l) => l !== locale && alternates[l.hreflang]).map(( > - - × + + + diff --git a/website/src/components/LanguageSwitcher.astro b/website/src/components/LanguageSwitcher.astro index 6d6ece9..cb9fddd 100644 --- a/website/src/components/LanguageSwitcher.astro +++ b/website/src/components/LanguageSwitcher.astro @@ -3,6 +3,7 @@ // page where the page exists only in English (icon pages). Plain links in a , so it works // without JavaScript and crawlers see every language version. import { BUILT, type Locale, localize, t } from '../i18n'; +import ControlIcon from './ControlIcon'; interface Props { locale: Locale; @@ -28,6 +29,7 @@ const flagClass = 'h-[15px] w-5 shrink-0 rounded-[3px] object-cover ring-1 ring- > {short(locale)} + {languages.map(({ locale: l, href }) => ( diff --git a/website/src/i18n/messages/cs/islands.ts b/website/src/i18n/messages/cs/islands.ts index 5ffd907..10089a3 100644 --- a/website/src/i18n/messages/cs/islands.ts +++ b/website/src/i18n/messages/cs/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Soubory' }, copy: 'Kopírovat', download: 'Stáhnout', - iconPage: 'Stránka ikony (anglicky) →', + iconPage: 'Stránka ikony (anglicky)', }, interactionDemo: { click: 'Klikněte na kartu', diff --git a/website/src/i18n/messages/cs/pages.ts b/website/src/i18n/messages/cs/pages.ts index 2d7133d..12dc360 100644 --- a/website/src/i18n/messages/cs/pages.ts +++ b/website/src/i18n/messages/cs/pages.ts @@ -36,7 +36,7 @@ export const pages: Messages['pages'] = { `Všechny ikony sady rozdělené podle kategorií. Po otevření ikony uvidíte živý náhled, kód pro React a shadcn/ui a soubory Lottie JSON, SVG a ZIP ke stažení (stránky ikon jsou v angličtině). Pokud chcete hledat podle klíčových slov a vyzkoušet animace přímo na místě, použijte ${link(browser, 'prohlížeč ikon')}.`, categoriesNav: 'Kategorie', byInteraction: 'Podle interakce:', - categoryLink: (count: number, name: string) => `${icons(count)} v kategorii ${name} →`, + categoryLink: (count: number, name: string) => `${icons(count)} v kategorii ${name}`, }, categories: { @@ -113,7 +113,7 @@ export const pages: Messages['pages'] = { 'Každá ikona useAnimations je standardní Lottie JSON animace, takže ji přehraje jakýkoli Lottie přehrávač: lottie-web na webu, lottie-ios, lottie-android, Flutter i React Native. Soubory jsou zdarma pod licencí MIT, bez registrace, a jsou dostupné na stálých URL s povoleným CORS, takže je můžete načítat přímo nebo si je stáhnout do projektu.', popular: 'Oblíbené Lottie ikony', allIcons: (count: number) => - `${form(count, { one: 'Jediná', few: 'Všechny', other: 'Všech' })} ${icons(count)} →`, + `${form(count, { one: 'Jediná', few: 'Všechny', other: 'Všech' })} ${icons(count)}`, filesTitle: 'Soubory a URL', filesIntroHtml: `Každá ikona má na adrese ${code('/icons/<id>')} stránku s náhledem a soubory ke stažení (v angličtině). Samotné soubory:`, files: { @@ -225,7 +225,7 @@ export const pages: Messages['pages'] = { codeText: { closeMenu: 'Zavřít menu', openMenu: 'Otevřít menu' }, popular: 'Oblíbené ikony pro shadcn/ui', allIcons: (count: number) => - `${form(count, { one: 'Jediná', few: 'Všechny', other: 'Všech' })} ${icons(count)} →`, + `${form(count, { one: 'Jediná', few: 'Všechny', other: 'Všech' })} ${icons(count)}`, popularNote: 'Na stránce každé ikony najdete její příkaz pro shadcn (stránky ikon jsou v angličtině).', faq: (f: { count: number; version: string }): Faq => [ diff --git a/website/src/i18n/messages/de/islands.ts b/website/src/i18n/messages/de/islands.ts index d8361f3..7806835 100644 --- a/website/src/i18n/messages/de/islands.ts +++ b/website/src/i18n/messages/de/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Dateien' }, copy: 'Kopieren', download: 'Herunterladen', - iconPage: 'Zur Icon-Seite →', + iconPage: 'Zur Icon-Seite', }, interactionDemo: { click: 'Auf die Karte klicken', diff --git a/website/src/i18n/messages/de/pages.ts b/website/src/i18n/messages/de/pages.ts index b0c72a9..b5f0bd7 100644 --- a/website/src/i18n/messages/de/pages.ts +++ b/website/src/i18n/messages/de/pages.ts @@ -24,7 +24,7 @@ export const pages: Messages['pages'] = { `Alle Icons der Sammlung, nach Kategorie gruppiert. Jedes Icon führt zu einer Seite mit Live-Vorschau, dem Code für React und shadcn/ui sowie den Downloads als Lottie JSON, SVG und ZIP (die Icon-Seiten sind nur auf Englisch verfügbar). Um nach Stichworten zu suchen und die Animationen direkt auszuprobieren, nutzen Sie den ${link(browser, 'Icon-Browser')}.`, categoriesNav: 'Kategorien', byInteraction: 'Nach Interaktion:', - categoryLink: (count: number, name: string) => `${name}: ${iconCount(count)} →`, + categoryLink: (count: number, name: string) => `${name}: ${iconCount(count)}`, }, categories: { @@ -103,7 +103,7 @@ export const pages: Messages['pages'] = { intro: 'Jedes useAnimations-Icon ist eine Standard-Lottie-JSON-Animation und läuft damit in jedem Lottie-Player: lottie-web im Web, lottie-ios, lottie-android, Flutter und React Native. Die Dateien sind unter der MIT-Lizenz kostenlos, ohne Registrierung, und liegen unter stabilen URLs mit aktiviertem CORS. Sie können sie also direkt laden oder in Ihr Projekt herunterladen.', popular: 'Beliebte Lottie-Icons', - allIcons: (count: number) => `Alle ${count} Icons →`, + allIcons: (count: number) => `Alle ${count} Icons`, filesTitle: 'Dateien und URLs', filesIntroHtml: `Jedes Icon hat eine Seite unter ${code('/icons/<id>')} mit Vorschau und Downloads (nur auf Englisch). Die Dateien selbst:`, files: { @@ -214,7 +214,7 @@ export const pages: Messages['pages'] = { `Icons übernehmen die Textfarbe: ${code('text-primary')} oder ${code('text-muted-foreground')} färben sie um, und Dark Mode braucht nichts Zusätzliches. Weitere Props und Muster finden Sie in der ${link(docs, 'Dokumentation')}.`, codeText: { closeMenu: 'Menü schließen', openMenu: 'Menü öffnen' }, popular: 'Beliebte Icons für shadcn/ui', - allIcons: (count: number) => `Alle ${count} Icons →`, + allIcons: (count: number) => `Alle ${count} Icons`, popularNote: 'Jede Icon-Seite zeigt ihren shadcn-Befehl (die Icon-Seiten sind nur auf Englisch verfügbar).', faq: (f: { count: number; version: string }): Faq => [ diff --git a/website/src/i18n/messages/en/islands.ts b/website/src/i18n/messages/en/islands.ts index 2421e02..893906f 100644 --- a/website/src/i18n/messages/en/islands.ts +++ b/website/src/i18n/messages/en/islands.ts @@ -42,7 +42,7 @@ export const islands = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Files' }, copy: 'Copy', download: 'Download', - iconPage: 'Icon page →', + iconPage: 'Icon page', }, /** The interaction demo cards (home page and interaction pages): what to do with the card. */ interactionDemo: { diff --git a/website/src/i18n/messages/en/pages.ts b/website/src/i18n/messages/en/pages.ts index d070a25..02cf401 100644 --- a/website/src/i18n/messages/en/pages.ts +++ b/website/src/i18n/messages/en/pages.ts @@ -32,7 +32,7 @@ export const pages = { /** Before the list of interaction links. */ byInteraction: 'By interaction:', /** Link from a category section to its page; `name` is the category name, e.g. 'Navigation'. */ - categoryLink: (count: number, name: string) => `${count} ${name.toLowerCase()} icons →`, + categoryLink: (count: number, name: string) => `${count} ${name.toLowerCase()} icons`, }, /** /categories: the list of categories. */ @@ -121,7 +121,7 @@ export const pages = { intro: 'Every useAnimations icon is a standard Lottie JSON animation, so any Lottie player can play it: lottie-web on the web, lottie-ios, lottie-android, Flutter and React Native. The files are free under the MIT license, with no sign-up, and served from stable URLs with CORS enabled, so you can load them directly or download them into your project.', popular: 'Popular Lottie icons', - allIcons: (count: number) => `All ${count} icons →`, + allIcons: (count: number) => `All ${count} icons`, filesTitle: 'Files and URLs', filesIntroHtml: `Each icon has a page at ${code('/icons/<id>')} with a preview and downloads. The files themselves:`, /** Comments after each URL in the list of files. */ @@ -246,7 +246,7 @@ export const pages = { /** aria-label values inside the Button example code. */ codeText: { closeMenu: 'Close menu', openMenu: 'Open menu' }, popular: 'Popular icons for shadcn/ui', - allIcons: (count: number) => `All ${count} icons →`, + allIcons: (count: number) => `All ${count} icons`, popularNote: 'Every icon page shows its shadcn command.', /** `version` is the react-useanimations version range, e.g. 3.1.0. */ faq: (f: { count: number; version: string }): Faq => [ diff --git a/website/src/i18n/messages/es/islands.ts b/website/src/i18n/messages/es/islands.ts index 855963c..1bdede1 100644 --- a/website/src/i18n/messages/es/islands.ts +++ b/website/src/i18n/messages/es/islands.ts @@ -34,7 +34,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Archivos' }, copy: 'Copiar', download: 'Descargar', - iconPage: 'Página del icono →', + iconPage: 'Página del icono', }, interactionDemo: { click: 'Haz clic en la tarjeta', diff --git a/website/src/i18n/messages/es/pages.ts b/website/src/i18n/messages/es/pages.ts index 759d3bd..e122c46 100644 --- a/website/src/i18n/messages/es/pages.ts +++ b/website/src/i18n/messages/es/pages.ts @@ -29,7 +29,7 @@ export const pages: Messages['pages'] = { categoriesNav: 'Categorías', byInteraction: 'Por interacción:', categoryLink: (count: number, name: string) => - name === 'Otros' ? `${icons(count)} más →` : `${icons(count)} de ${name.toLowerCase()} →`, + name === 'Otros' ? `${icons(count)} más` : `${icons(count)} de ${name.toLowerCase()}`, }, categories: { @@ -106,7 +106,7 @@ export const pages: Messages['pages'] = { intro: 'Cada icono de useAnimations es una animación Lottie JSON estándar, así que cualquier reproductor de Lottie puede reproducirla: lottie-web en la web, lottie-ios, lottie-android, Flutter y React Native. Los archivos son gratis con la licencia MIT, sin registro, y se sirven desde URL estables con CORS habilitado, así que puedes cargarlos directamente o descargarlos en tu proyecto.', popular: 'Iconos Lottie populares', - allIcons: (count: number) => `Los ${count} iconos →`, + allIcons: (count: number) => `Los ${count} iconos`, filesTitle: 'Archivos y URL', filesIntroHtml: `Cada icono tiene una página en ${code('/icons/<id>')} (en inglés) con vista previa y descargas. Los archivos en sí:`, files: { @@ -217,7 +217,7 @@ export const pages: Messages['pages'] = { `Los iconos siguen el color del texto, así que ${code('text-primary')} o ${code('text-muted-foreground')} les cambian el color y el modo oscuro no necesita nada más. Encontrarás más props y patrones en la ${link(docs, 'documentación')}.`, codeText: { closeMenu: 'Cerrar menú', openMenu: 'Abrir menú' }, popular: 'Iconos populares para shadcn/ui', - allIcons: (count: number) => `Los ${count} iconos →`, + allIcons: (count: number) => `Los ${count} iconos`, popularNote: 'La página de cada icono (en inglés) muestra su comando de shadcn.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/fr/islands.ts b/website/src/i18n/messages/fr/islands.ts index 06b9f53..b048c0c 100644 --- a/website/src/i18n/messages/fr/islands.ts +++ b/website/src/i18n/messages/fr/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Fichiers' }, copy: 'Copier', download: 'Télécharger', - iconPage: 'Page de l’icône →', + iconPage: 'Page de l’icône', }, interactionDemo: { click: 'Cliquez sur la carte', diff --git a/website/src/i18n/messages/fr/pages.ts b/website/src/i18n/messages/fr/pages.ts index 666e8c4..0b4ee08 100644 --- a/website/src/i18n/messages/fr/pages.ts +++ b/website/src/i18n/messages/fr/pages.ts @@ -28,7 +28,7 @@ export const pages: Messages['pages'] = { categoriesNav: 'Catégories', byInteraction: 'Par interaction :', categoryLink: (count: number, name: string) => - `${iconCount(count)} ${CATEGORY_OF[name] ?? `« ${name} »`} →`, + `${iconCount(count)} ${CATEGORY_OF[name] ?? `« ${name} »`}`, }, categories: { @@ -107,7 +107,7 @@ export const pages: Messages['pages'] = { intro: 'Chaque icône useAnimations est une animation Lottie JSON standard : n’importe quel lecteur Lottie peut la jouer, que ce soit lottie-web sur le web, lottie-ios, lottie-android, Flutter ou React Native. Les fichiers sont gratuits sous licence MIT, sans inscription, et servis depuis des URL stables avec CORS activé : vous pouvez les charger directement ou les télécharger dans votre projet.', popular: 'Icônes Lottie populaires', - allIcons: (count: number) => `Les ${count} icônes →`, + allIcons: (count: number) => `Les ${count} icônes`, filesTitle: 'Fichiers et URL', filesIntroHtml: `Chaque icône a sa page (en anglais) à l’adresse ${code('/icons/<id>')}, avec un aperçu et les téléchargements. Les fichiers eux-mêmes :`, files: { @@ -218,7 +218,7 @@ export const pages: Messages['pages'] = { `Les icônes suivent la couleur du texte : ${code('text-primary')} ou ${code('text-muted-foreground')} suffisent à les recolorer, et le mode sombre ne demande rien de plus. D’autres props et exemples sont dans la ${link(docs, 'documentation')}.`, codeText: { closeMenu: 'Fermer le menu', openMenu: 'Ouvrir le menu' }, popular: 'Icônes populaires pour shadcn/ui', - allIcons: (count: number) => `Les ${count} icônes →`, + allIcons: (count: number) => `Les ${count} icônes`, popularNote: 'La page de chaque icône indique sa commande shadcn.', faq: (f: { count: number; version: string }) => [ { diff --git a/website/src/i18n/messages/id/islands.ts b/website/src/i18n/messages/id/islands.ts index 984aea5..a3b4a0d 100644 --- a/website/src/i18n/messages/id/islands.ts +++ b/website/src/i18n/messages/id/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'File' }, copy: 'Salin', download: 'Unduh', - iconPage: 'Halaman ikon →', + iconPage: 'Halaman ikon', }, interactionDemo: { click: 'Klik kartunya', diff --git a/website/src/i18n/messages/id/pages.ts b/website/src/i18n/messages/id/pages.ts index 9d25240..b3da6a2 100644 --- a/website/src/i18n/messages/id/pages.ts +++ b/website/src/i18n/messages/id/pages.ts @@ -23,7 +23,7 @@ export const pages: Messages['pages'] = { `Semua ikon dalam koleksi, dikelompokkan per kategori. Buka salah satunya untuk melihat pratinjau langsung, kode React dan shadcn/ui, serta unduhan Lottie JSON, SVG, dan ZIP (halaman ikon tersedia dalam bahasa Inggris). Untuk mencari berdasarkan kata kunci dan mencoba animasinya langsung, pakai ${link(browser, 'penjelajah ikon')}.`, categoriesNav: 'Kategori', byInteraction: 'Berdasarkan interaksi:', - categoryLink: (count: number, name: string) => `${count} ikon ${name.toLowerCase()} →`, + categoryLink: (count: number, name: string) => `${count} ikon ${name.toLowerCase()}`, }, categories: { @@ -96,7 +96,7 @@ export const pages: Messages['pages'] = { intro: 'Setiap ikon useAnimations adalah animasi Lottie JSON standar, jadi bisa diputar oleh Lottie player mana pun: lottie-web di web, lottie-ios, lottie-android, Flutter, dan React Native. File-nya gratis di bawah lisensi MIT, tanpa perlu mendaftar, dan disajikan dari URL tetap dengan CORS aktif, sehingga bisa Anda muat langsung atau unduh ke dalam proyek.', popular: 'Ikon Lottie populer', - allIcons: (count: number) => `Semua ${count} ikon →`, + allIcons: (count: number) => `Semua ${count} ikon`, filesTitle: 'File dan URL', filesIntroHtml: `Setiap ikon punya halaman di ${code('/icons/<id>')} (dalam bahasa Inggris) dengan pratinjau dan unduhan. File-nya sendiri:`, files: { @@ -207,7 +207,7 @@ export const pages: Messages['pages'] = { `Ikon mengikuti warna teks, jadi ${code('text-primary')} atau ${code('text-muted-foreground')} langsung mengubah warnanya, dan dark mode tidak butuh pengaturan tambahan. Props dan pola lainnya ada di ${link(docs, 'dokumentasi')}.`, codeText: { closeMenu: 'Tutup menu', openMenu: 'Buka menu' }, popular: 'Ikon populer untuk shadcn/ui', - allIcons: (count: number) => `Semua ${count} ikon →`, + allIcons: (count: number) => `Semua ${count} ikon`, popularNote: 'Setiap halaman ikon (dalam bahasa Inggris) menampilkan perintah shadcn-nya.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/ja/islands.ts b/website/src/i18n/messages/ja/islands.ts index 1a4a414..2cc0d2c 100644 --- a/website/src/i18n/messages/ja/islands.ts +++ b/website/src/i18n/messages/ja/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'ファイル' }, copy: 'コピー', download: 'ダウンロード', - iconPage: 'アイコンのページ →', + iconPage: 'アイコンのページ', }, interactionDemo: { click: 'カードをクリック', diff --git a/website/src/i18n/messages/ja/pages.ts b/website/src/i18n/messages/ja/pages.ts index c959094..2f01a82 100644 --- a/website/src/i18n/messages/ja/pages.ts +++ b/website/src/i18n/messages/ja/pages.ts @@ -25,7 +25,7 @@ export const pages: Messages['pages'] = { categoriesNav: 'カテゴリ', byInteraction: 'インタラクション別:', categoryLink: (count: number, name: string) => - `${/[A-Za-z0-9]$/.test(name) ? `${name} ` : name}のアイコン ${count} 個 →`, + `${/[A-Za-z0-9]$/.test(name) ? `${name} ` : name}のアイコン ${count} 個`, }, categories: { @@ -104,7 +104,7 @@ export const pages: Messages['pages'] = { intro: 'useAnimations のアイコンはすべて標準的な Lottie JSON アニメーションなので、Web の lottie-web をはじめ、lottie-ios、lottie-android、Flutter、React Native など、どの Lottie プレーヤーでも再生できます。ファイルは MIT ライセンスで無料、登録も不要です。CORS を有効にした固定の URL から配信しているので、直接読み込むことも、プロジェクトにダウンロードして使うこともできます。', popular: '人気の Lottie アイコン', - allIcons: (count: number) => `全 ${count} 個のアイコン →`, + allIcons: (count: number) => `全 ${count} 個のアイコン`, filesTitle: 'ファイルと URL', filesIntroHtml: `各アイコンには、プレビューとダウンロードを載せた ${code('/icons/<id>')} のページがあります。ファイル本体は次のとおりです:`, files: { @@ -215,7 +215,7 @@ export const pages: Messages['pages'] = { `アイコンはテキストの色に追従するので、${code('text-primary')} や ${code('text-muted-foreground')} で色が変わり、ダークモードにも追加の対応は要りません。そのほかの props や使い方は${link(docs, 'ドキュメント')}を参照してください。`, codeText: { closeMenu: 'メニューを閉じる', openMenu: 'メニューを開く' }, popular: '人気の shadcn/ui 用アイコン', - allIcons: (count: number) => `全 ${count} 個のアイコン →`, + allIcons: (count: number) => `全 ${count} 個のアイコン`, popularNote: 'shadcn のコマンドは各アイコンのページに載っています。', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/ko/islands.ts b/website/src/i18n/messages/ko/islands.ts index 98c7e46..f97a658 100644 --- a/website/src/i18n/messages/ko/islands.ts +++ b/website/src/i18n/messages/ko/islands.ts @@ -34,7 +34,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: '파일' }, copy: '복사', download: '다운로드', - iconPage: '아이콘 페이지 →', + iconPage: '아이콘 페이지', }, interactionDemo: { click: '카드를 클릭해 보세요', diff --git a/website/src/i18n/messages/ko/pages.ts b/website/src/i18n/messages/ko/pages.ts index 66be137..7fbcfcb 100644 --- a/website/src/i18n/messages/ko/pages.ts +++ b/website/src/i18n/messages/ko/pages.ts @@ -24,7 +24,7 @@ export const pages: Messages['pages'] = { `세트의 모든 아이콘을 카테고리별로 모았습니다. 아이콘을 열면 실시간 미리보기, React·shadcn/ui 코드, Lottie JSON·SVG·ZIP 다운로드가 있습니다(아이콘 페이지는 영어로 제공됩니다). 키워드로 검색하고 애니메이션을 바로 재생해 보려면 ${link(browser, '아이콘 브라우저')}를 이용하세요.`, categoriesNav: '카테고리', byInteraction: '인터랙션별:', - categoryLink: (count: number, name: string) => `${name} 아이콘 ${count}개 →`, + categoryLink: (count: number, name: string) => `${name} 아이콘 ${count}개`, }, categories: { @@ -100,7 +100,7 @@ export const pages: Messages['pages'] = { intro: 'useAnimations의 모든 아이콘은 표준 Lottie JSON 애니메이션이라서 웹의 lottie-web, lottie-ios, lottie-android, Flutter, React Native 등 어떤 Lottie 플레이어로도 재생할 수 있습니다. 파일은 MIT 라이선스로 무료이고 가입도 필요 없습니다. CORS가 허용된 고정 URL로 제공되므로 직접 불러오거나 프로젝트에 내려받아 쓰면 됩니다.', popular: '인기 Lottie 아이콘', - allIcons: (count: number) => `아이콘 ${count}개 전체 →`, + allIcons: (count: number) => `아이콘 ${count}개 전체`, filesTitle: '파일과 URL', filesIntroHtml: `각 아이콘에는 미리보기와 다운로드가 있는 ${code('/icons/<id>')} 페이지가 있습니다. 파일은 다음과 같습니다.`, files: { @@ -212,7 +212,7 @@ export const pages: Messages['pages'] = { `아이콘은 텍스트 색상을 따라가므로 ${code('text-primary')}나 ${code('text-muted-foreground')}로 색을 바꿀 수 있고, 다크 모드에도 따로 할 일이 없습니다. 다른 props와 패턴은 ${link(docs, '문서')}에 있습니다.`, codeText: { closeMenu: '메뉴 닫기', openMenu: '메뉴 열기' }, popular: 'shadcn/ui 인기 아이콘', - allIcons: (count: number) => `아이콘 ${count}개 전체 →`, + allIcons: (count: number) => `아이콘 ${count}개 전체`, popularNote: '각 아이콘 페이지에 shadcn 명령이 나와 있습니다.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/pt-br/islands.ts b/website/src/i18n/messages/pt-br/islands.ts index 54f64a6..9957b9b 100644 --- a/website/src/i18n/messages/pt-br/islands.ts +++ b/website/src/i18n/messages/pt-br/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Arquivos' }, copy: 'Copiar', download: 'Baixar', - iconPage: 'Página do ícone →', + iconPage: 'Página do ícone', }, interactionDemo: { click: 'Clique no card', diff --git a/website/src/i18n/messages/pt-br/pages.ts b/website/src/i18n/messages/pt-br/pages.ts index a6fa980..10efc15 100644 --- a/website/src/i18n/messages/pt-br/pages.ts +++ b/website/src/i18n/messages/pt-br/pages.ts @@ -29,7 +29,7 @@ export const pages: Messages['pages'] = { categoriesNav: 'Categorias', byInteraction: 'Por interação:', categoryLink: (count: number, name: string) => - name === 'Outros' ? `${count} outros ícones →` : `${icons(count)} de ${lower(name)} →`, + name === 'Outros' ? `${count} outros ícones` : `${icons(count)} de ${lower(name)}`, }, categories: { @@ -104,7 +104,7 @@ export const pages: Messages['pages'] = { intro: 'Cada ícone do useAnimations é uma animação Lottie JSON padrão, então qualquer player Lottie consegue tocá-la: lottie-web na web, lottie-ios, lottie-android, Flutter e React Native. Os arquivos são gratuitos sob a licença MIT, sem cadastro, e servidos em URLs estáveis com CORS habilitado, então você pode carregá-los direto ou baixá-los para o seu projeto.', popular: 'Ícones Lottie populares', - allIcons: (count: number) => `Todos os ${count} ícones →`, + allIcons: (count: number) => `Todos os ${count} ícones`, filesTitle: 'Arquivos e URLs', filesIntroHtml: `Cada ícone tem uma página em ${code('/icons/<id>')} (em inglês) com prévia e downloads. Os arquivos em si:`, files: { @@ -216,7 +216,7 @@ export const pages: Messages['pages'] = { `Os ícones seguem a cor do texto, então ${code('text-primary')} ou ${code('text-muted-foreground')} mudam a cor deles, e o modo escuro não precisa de nada extra. Mais props e padrões estão na ${link(docs, 'documentação')}.`, codeText: { closeMenu: 'Fechar menu', openMenu: 'Abrir menu' }, popular: 'Ícones populares para shadcn/ui', - allIcons: (count: number) => `Todos os ${count} ícones →`, + allIcons: (count: number) => `Todos os ${count} ícones`, popularNote: 'A página de cada ícone (em inglês) mostra o comando do shadcn.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/ru/islands.ts b/website/src/i18n/messages/ru/islands.ts index 0faa44d..6f4345f 100644 --- a/website/src/i18n/messages/ru/islands.ts +++ b/website/src/i18n/messages/ru/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Файлы' }, copy: 'Копировать', download: 'Скачать', - iconPage: 'Страница иконки →', + iconPage: 'Страница иконки', }, interactionDemo: { click: 'Кликните по карточке', diff --git a/website/src/i18n/messages/ru/pages.ts b/website/src/i18n/messages/ru/pages.ts index cbe31c5..a589826 100644 --- a/website/src/i18n/messages/ru/pages.ts +++ b/website/src/i18n/messages/ru/pages.ts @@ -58,7 +58,7 @@ export const pages: Messages['pages'] = { `Все иконки набора, сгруппированные по категориям. Откройте любую, чтобы увидеть живое превью, код для React и shadcn/ui и скачать Lottie JSON, SVG или ZIP. Чтобы искать по ключевым словам и пробовать анимации прямо на месте, используйте ${link(browser, 'поиск по иконкам')}.`, categoriesNav: 'Категории', byInteraction: 'По взаимодействию:', - categoryLink: (count: number, name: string) => `${icons(count)} в категории «${name}» →`, + categoryLink: (count: number, name: string) => `${icons(count)} в категории «${name}»`, }, categories: { @@ -140,7 +140,7 @@ export const pages: Messages['pages'] = { intro: 'Каждая иконка useAnimations — это стандартная анимация Lottie JSON, поэтому её воспроизводит любой Lottie-плеер: lottie-web в браузере, lottie-ios, lottie-android, Flutter и React Native. Файлы бесплатны по лицензии MIT, без регистрации, и отдаются по постоянным URL с включённым CORS, так что их можно загружать напрямую или скачать в свой проект.', popular: 'Популярные Lottie-иконки', - allIcons: (count: number) => `Все ${icons(count)} →`, + allIcons: (count: number) => `Все ${icons(count)}`, filesTitle: 'Файлы и URL', filesIntroHtml: `У каждой иконки есть страница ${code('/icons/<id>')} с превью и файлами для скачивания (на английском). Сами файлы:`, files: { @@ -251,7 +251,7 @@ export const pages: Messages['pages'] = { `Иконки берут цвет текста, поэтому их перекрашивают ${code('text-primary')} или ${code('text-muted-foreground')}, а для тёмной темы ничего дополнительно делать не нужно. Остальные пропсы и приёмы — в ${link(docs, 'документации')}.`, codeText: { closeMenu: 'Закрыть меню', openMenu: 'Открыть меню' }, popular: 'Популярные иконки для shadcn/ui', - allIcons: (count: number) => `Все ${icons(count)} →`, + allIcons: (count: number) => `Все ${icons(count)}`, popularNote: 'Команда shadcn есть на странице каждой иконки.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/tr/islands.ts b/website/src/i18n/messages/tr/islands.ts index b30e005..8f71919 100644 --- a/website/src/i18n/messages/tr/islands.ts +++ b/website/src/i18n/messages/tr/islands.ts @@ -33,7 +33,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Dosyalar' }, copy: 'Kopyala', download: 'İndir', - iconPage: 'İkon sayfası →', + iconPage: 'İkon sayfası', }, interactionDemo: { click: 'Karta tıklayın', diff --git a/website/src/i18n/messages/tr/pages.ts b/website/src/i18n/messages/tr/pages.ts index 0a79238..fdb5137 100644 --- a/website/src/i18n/messages/tr/pages.ts +++ b/website/src/i18n/messages/tr/pages.ts @@ -26,7 +26,7 @@ export const pages: Messages['pages'] = { `Setteki her ikon, kategorilere göre gruplanmış hâlde. Canlı önizleme, React ve shadcn/ui kodu ile Lottie JSON, SVG ve ZIP indirmeleri için bir ikonu açın. Anahtar kelimeyle aramak ve animasyonları yerinde denemek için ${link(browser, 'ikon tarayıcısını')} kullanın.`, categoriesNav: 'Kategoriler', byInteraction: 'Etkileşime göre:', - categoryLink: (count: number, name: string) => `${name} kategorisindeki ${icons(count)} →`, + categoryLink: (count: number, name: string) => `${name} kategorisindeki ${icons(count)}`, }, categories: { @@ -102,7 +102,7 @@ export const pages: Messages['pages'] = { intro: 'Her useAnimations ikonu standart bir Lottie JSON animasyonudur, yani her Lottie oynatıcısı onu oynatabilir: web’de lottie-web, ayrıca lottie-ios, lottie-android, Flutter ve React Native. Dosyalar MIT lisansıyla ücretsizdir, kayıt gerektirmez ve CORS açık, kalıcı URL’lerden sunulur; doğrudan yükleyebilir ya da projenize indirebilirsiniz.', popular: 'Popüler Lottie ikonları', - allIcons: (count: number) => `${count} ikonun tamamı →`, + allIcons: (count: number) => `${count} ikonun tamamı`, filesTitle: 'Dosyalar ve URL’ler', filesIntroHtml: `Her ikonun ${code('/icons/<id>')} adresinde önizleme ve indirmeler içeren bir sayfası vardır (ikon sayfaları İngilizcedir). Dosyaların kendisi:`, files: { @@ -212,7 +212,7 @@ export const pages: Messages['pages'] = { `İkonlar metin rengini izler; ${code('text-primary')} ya da ${code('text-muted-foreground')} onları yeniden renklendirir ve karanlık mod için ek bir şey gerekmez. Diğer prop’lar ve kalıplar ${link(docs, 'dokümantasyonda')}.`, codeText: { closeMenu: 'Menüyü kapat', openMenu: 'Menüyü aç' }, popular: 'shadcn/ui için popüler ikonlar', - allIcons: (count: number) => `${count} ikonun tamamı →`, + allIcons: (count: number) => `${count} ikonun tamamı`, popularNote: 'Her ikon sayfası kendi shadcn komutunu gösterir.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/vi/islands.ts b/website/src/i18n/messages/vi/islands.ts index 563bfe8..b6c517c 100644 --- a/website/src/i18n/messages/vi/islands.ts +++ b/website/src/i18n/messages/vi/islands.ts @@ -34,7 +34,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: 'Tệp' }, copy: 'Sao chép', download: 'Tải xuống', - iconPage: 'Trang icon →', + iconPage: 'Trang icon', }, interactionDemo: { click: 'Click vào thẻ', diff --git a/website/src/i18n/messages/vi/pages.ts b/website/src/i18n/messages/vi/pages.ts index 1e697fc..365c2d4 100644 --- a/website/src/i18n/messages/vi/pages.ts +++ b/website/src/i18n/messages/vi/pages.ts @@ -25,7 +25,7 @@ export const pages: Messages['pages'] = { `Toàn bộ bộ icon, nhóm theo danh mục. Mở một icon để xem trước trực tiếp, lấy code React và shadcn/ui, và tải về Lottie JSON, SVG hoặc ZIP. Để tìm theo từ khóa và thử hiệu ứng ngay tại chỗ, hãy dùng ${link(browser, 'trình duyệt icon')}.`, categoriesNav: 'Danh mục', byInteraction: 'Theo tương tác:', - categoryLink: (count: number, name: string) => `${count} icon ${name.toLowerCase()} →`, + categoryLink: (count: number, name: string) => `${count} icon ${name.toLowerCase()}`, }, categories: { @@ -96,7 +96,7 @@ export const pages: Messages['pages'] = { intro: 'Mỗi icon của useAnimations là một animation Lottie JSON chuẩn, nên mọi Lottie player đều phát được: lottie-web trên web, lottie-ios, lottie-android, Flutter và React Native. Các tệp miễn phí theo giấy phép MIT, không cần đăng ký, được phục vụ từ URL cố định có bật CORS, nên bạn có thể dùng thẳng từ URL hoặc tải về đưa vào dự án.', popular: 'Icon Lottie phổ biến', - allIcons: (count: number) => `Tất cả ${count} icon →`, + allIcons: (count: number) => `Tất cả ${count} icon`, filesTitle: 'Tệp và URL', filesIntroHtml: `Mỗi icon có một trang tại ${code('/icons/<id>')} với bản xem trước và các tệp tải về. Bản thân các tệp:`, files: { @@ -206,7 +206,7 @@ export const pages: Messages['pages'] = { `Icon tự theo màu chữ, nên ${code('text-primary')} hay ${code('text-muted-foreground')} là đổi được màu, và dark mode không cần thêm gì. Các prop và cách dùng khác có trong ${link(docs, 'tài liệu')}.`, codeText: { closeMenu: 'Đóng menu', openMenu: 'Mở menu' }, popular: 'Icon phổ biến cho shadcn/ui', - allIcons: (count: number) => `Tất cả ${count} icon →`, + allIcons: (count: number) => `Tất cả ${count} icon`, popularNote: 'Trang của mỗi icon đều có lệnh shadcn tương ứng.', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/i18n/messages/zh/islands.ts b/website/src/i18n/messages/zh/islands.ts index 79751cd..59b7ba7 100644 --- a/website/src/i18n/messages/zh/islands.ts +++ b/website/src/i18n/messages/zh/islands.ts @@ -34,7 +34,7 @@ export const islands: Messages['islands'] = { tabs: { react: 'React', shadcn: 'shadcn', files: '文件' }, copy: '复制', download: '下载', - iconPage: '图标页面 →', + iconPage: '图标页面', }, interactionDemo: { click: '点击卡片', diff --git a/website/src/i18n/messages/zh/pages.ts b/website/src/i18n/messages/zh/pages.ts index 8fe00f4..0131677 100644 --- a/website/src/i18n/messages/zh/pages.ts +++ b/website/src/i18n/messages/zh/pages.ts @@ -20,7 +20,7 @@ export const pages: Messages['pages'] = { `整套图标按类别排列。打开任意图标,可以查看实时预览、React 和 shadcn/ui 代码,并下载 Lottie JSON、SVG 和 ZIP。如果想按关键词搜索并直接试玩动画,请使用${link(browser, '图标浏览器')}。`, categoriesNav: '类别', byInteraction: '按交互方式:', - categoryLink: (count: number, name: string) => `${count} 个${name}图标 →`, + categoryLink: (count: number, name: string) => `${count} 个${name}图标`, }, categories: { @@ -89,7 +89,7 @@ export const pages: Messages['pages'] = { intro: '每个 useAnimations 图标都是标准的 Lottie JSON 动画,任何 Lottie 播放器都能播放:网页上的 lottie-web,以及 lottie-ios、lottie-android、Flutter 和 React Native。这些文件依据 MIT 许可证免费提供,无需注册,托管在固定网址上并已开启 CORS,既可以直接加载,也可以下载到项目中。', popular: '热门 Lottie 图标', - allIcons: (count: number) => `全部 ${count} 个图标 →`, + allIcons: (count: number) => `全部 ${count} 个图标`, filesTitle: '文件与网址', filesIntroHtml: `每个图标在 ${code('/icons/<id>')} 都有一个页面,提供预览和下载。文件本身的地址如下:`, files: { @@ -197,7 +197,7 @@ export const pages: Messages['pages'] = { `图标跟随文字颜色,因此 ${code('text-primary')} 或 ${code('text-muted-foreground')} 就能给它们改色,深色模式也无需额外处理。更多属性和用法请参阅${link(docs, '文档')}。`, codeText: { closeMenu: '关闭菜单', openMenu: '打开菜单' }, popular: '热门 shadcn/ui 图标', - allIcons: (count: number) => `全部 ${count} 个图标 →`, + allIcons: (count: number) => `全部 ${count} 个图标`, popularNote: '每个图标页面都提供对应的 shadcn 命令。', faq: (f: { count: number; version: string }): Faq => [ { diff --git a/website/src/pages/404.astro b/website/src/pages/404.astro index 07b8911..0c07402 100644 --- a/website/src/pages/404.astro +++ b/website/src/pages/404.astro @@ -1,18 +1,20 @@ --- +import ControlIcon from '../components/ControlIcon'; import Layout from '../layouts/Layout.astro'; -import { icons } from '../lib/icons'; - -const icon = icons.find((item) => item.name === 'alertTriangle')!; --- - + Page not found The page you're looking for doesn't exist or has moved. - Home - Browse icons + Home + Browse icons All animated icons diff --git a/website/src/pages/icons/[id].astro b/website/src/pages/icons/[id].astro index d70a560..3028073 100644 --- a/website/src/pages/icons/[id].astro +++ b/website/src/pages/icons/[id].astro @@ -1,6 +1,8 @@ --- import type { GetStaticPaths } from 'astro'; +import ControlIcon from '../../components/ControlIcon'; import IconPreview from '../../components/IconPreview'; +import IconStill from '../../components/IconStill.astro'; import IconUsageTabs from '../../components/IconUsageTabs.astro'; import Layout from '../../layouts/Layout.astro'; import { lastModifiedOf } from '../../lib/git'; @@ -144,8 +146,8 @@ const jsonLd = { Icons - ›{icon.category} - ›{icon.title} + {icon.category} + {icon.title} @@ -197,7 +199,8 @@ const jsonLd = { data-track="download" data-track-icon={icon.id} data-track-format="zip" - class="rounded-full bg-fg px-5 py-2.5 text-sm font-medium text-bg hover:opacity-85">Download ZIPDownload ZIP Lottie JSONLottie JSON { states.map((state) => ( @@ -216,8 +220,9 @@ const jsonLd = { data-track-icon={icon.id} data-track-format="svg" data-track-file={`${state}.svg`} - class="rounded-full border border-line-strong px-5 py-2.5 text-sm hover:bg-surface-2" + class="inline-flex items-center gap-2 rounded-full border border-line-strong px-5 py-2.5 text-sm hover:bg-surface-2" > + {states.length > 1 ? `${state}.svg` : 'SVG'} )) @@ -272,7 +277,7 @@ const jsonLd = { href={`/icons/${other.id}`} class="flex items-center gap-4 rounded-2xl border border-line bg-surface p-4 transition hover:border-line-strong" > - + {other.title} {variantNote(icon, other)} @@ -291,11 +296,11 @@ const jsonLd = { { faq.map(({ question, answer }) => ( - - › + + {question} - {answer} + {answer} )) } @@ -309,8 +314,9 @@ const jsonLd = { Related icons - - All {icon.category.toLowerCase()} icons → + + All {icon.category.toLowerCase()} icons + @@ -320,7 +326,7 @@ const jsonLd = { href={`/icons/${other.id}`} class="flex aspect-[1/1.08] flex-col items-center justify-center gap-3 rounded-2xl border border-line bg-surface text-[13px] text-muted transition hover:border-line-strong hover:text-fg" > - + {other.title} @@ -331,8 +337,24 @@ const jsonLd = { } - {previous ? ← {previous.title} : } - {next && {next.title} →} + { + previous ? ( + + + {previous.title} + + ) : ( + + ) + } + { + next && ( + + {next.title} + + + ) + } diff --git a/website/src/scripts/copy.ts b/website/src/scripts/copy.ts index f236105..74cd713 100644 --- a/website/src/scripts/copy.ts +++ b/website/src/scripts/copy.ts @@ -1,10 +1,11 @@ // Copy buttons: or . The feedback // text comes from (Layout.astro), in the page's language. -const label = (button: HTMLElement, text: string) => { +// A successful copy also sets `data-copied` for a moment (accent color, copy icon to checkmark). +const label = (button: HTMLElement, text: string, copied: boolean) => { const target = button.querySelector('[data-copy-label]') ?? button; const previous = target.textContent; target.textContent = text; - button.dataset.copied = ''; + if (copied) button.dataset.copied = ''; setTimeout(() => { target.textContent = previous; delete button.dataset.copied; @@ -20,8 +21,8 @@ document.addEventListener('click', async (event) => { const text = button.dataset.copy ?? source?.textContent ?? ''; try { await navigator.clipboard.writeText(text.trim()); - label(button, document.body.dataset.copiedText ?? 'Copied'); + label(button, document.body.dataset.copiedText ?? 'Copied', true); } catch { - label(button, document.body.dataset.copyFailedText ?? 'Press Ctrl+C'); + label(button, document.body.dataset.copyFailedText ?? 'Press Ctrl+C', false); } }); diff --git a/website/src/scripts/hover-icons.ts b/website/src/scripts/hover-icons.ts new file mode 100644 index 0000000..4708a75 --- /dev/null +++ b/website/src/scripts/hover-icons.ts @@ -0,0 +1,34 @@ +// Icon stills (components/IconStill.astro) come alive on the first hover or keyboard focus of +// their link: the icon's Lottie JSON loads then, so a page of stills costs nothing until used. +import { createElement } from 'react'; +import { createRoot } from 'react-dom/client'; +import type { Interaction } from 'react-useanimations'; +import HoverIcon from '../components/HoverIcon'; +import { loadIconData } from '../lib/icon-data'; + +const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); + +const wake = (trigger: HTMLElement) => { + // Hover animations don't play with reduced motion, so the stills stay. + if (reducedMotion.matches) return; + // Icons whose first frame is empty keep their still: at rest they would show nothing. + const stills = trigger.querySelectorAll( + '[data-hover-icon]:not([data-live], [data-blank-start])' + ); + for (const still of stills) { + still.dataset.live = ''; + void loadIconData(still.dataset.hoverIcon!).then((animationData) => { + const interaction = still.dataset.interaction as Interaction; + createRoot(still).render(createElement(HoverIcon, { animationData, interaction, trigger })); + }); + } +}; + +document.addEventListener('pointerover', (event) => { + const trigger = (event.target as Element).closest('a'); + if (trigger && event.pointerType === 'mouse') wake(trigger); +}); +document.addEventListener('focusin', (event) => { + const trigger = (event.target as Element).closest('a'); + if (trigger?.matches(':focus-visible')) wake(trigger); +}); diff --git a/website/src/views/CategoryIndexPage.astro b/website/src/views/CategoryIndexPage.astro index 613a84c..208a58e 100644 --- a/website/src/views/CategoryIndexPage.astro +++ b/website/src/views/CategoryIndexPage.astro @@ -1,6 +1,7 @@ --- // The list of categories (/categories) in any language; src/pages routes render it. import Breadcrumbs from '../components/Breadcrumbs.astro'; +import IconStill from '../components/IconStill.astro'; import { alternatesOf, type Locale, localize, t } from '../i18n'; import Layout from '../layouts/Layout.astro'; import { breadcrumbNode, categories, categoryPath, localDescription, localTitle } from '../lib/hubs'; @@ -70,7 +71,7 @@ const jsonLd = { > {group.slice(0, 5).map((icon) => ( - + ))} diff --git a/website/src/views/DocumentationPage.astro b/website/src/views/DocumentationPage.astro index b36b25d..85269d8 100644 --- a/website/src/views/DocumentationPage.astro +++ b/website/src/views/DocumentationPage.astro @@ -1,6 +1,7 @@ --- // The documentation (/documentation) in any language; src/pages routes render it. import { alternatesOf, type Locale, localize, t } from '../i18n'; +import ControlIcon from '../components/ControlIcon'; import Layout from '../layouts/Layout.astro'; import { lastModifiedOf } from '../lib/git'; import { icons, SITE } from '../lib/icons'; @@ -56,7 +57,11 @@ const markdown = locale.prefix ? [] : [{ rel: 'alternate', type: 'text/markdown' diff --git a/website/src/views/InteractionIndexPage.astro b/website/src/views/InteractionIndexPage.astro index b7417fd..c66f719 100644 --- a/website/src/views/InteractionIndexPage.astro +++ b/website/src/views/InteractionIndexPage.astro @@ -1,6 +1,7 @@ --- // The list of interactions (/interactions) in any language; src/pages routes render it. import Breadcrumbs from '../components/Breadcrumbs.astro'; +import IconStill from '../components/IconStill.astro'; import { alternatesOf, type Locale, localize, t } from '../i18n'; import Layout from '../layouts/Layout.astro'; import { breadcrumbNode, interactionPath, interactions, localDescription, localTitle } from '../lib/hubs'; @@ -70,7 +71,7 @@ const jsonLd = { > {group.slice(0, 5).map((icon) => ( - + ))} interaction="{interaction}" diff --git a/website/src/views/LottieIconsPage.astro b/website/src/views/LottieIconsPage.astro index 5fac26a..32da28f 100644 --- a/website/src/views/LottieIconsPage.astro +++ b/website/src/views/LottieIconsPage.astro @@ -1,6 +1,7 @@ --- // The Lottie hub (/lottie-icons) in any language: the files and code for each Lottie player. // The code examples stay English. +import ControlIcon from '../components/ControlIcon'; import Breadcrumbs from '../components/Breadcrumbs.astro'; import CodeBlock from '../components/CodeBlock.astro'; import FaqList from '../components/FaqList.astro'; @@ -250,7 +251,7 @@ const jsonLd = { {p.popular} - {p.allIcons(icons.length)} + {p.allIcons(icons.length)} diff --git a/website/src/views/ShadcnIconsPage.astro b/website/src/views/ShadcnIconsPage.astro index 8233849..3ca97bd 100644 --- a/website/src/views/ShadcnIconsPage.astro +++ b/website/src/views/ShadcnIconsPage.astro @@ -1,5 +1,6 @@ --- // The shadcn/ui hub (/shadcn-animated-icons) in any language. The code examples stay English. +import ControlIcon from '../components/ControlIcon'; import Breadcrumbs from '../components/Breadcrumbs.astro'; import CodeBlock from '../components/CodeBlock.astro'; import FaqList from '../components/FaqList.astro'; @@ -129,7 +130,7 @@ const jsonLd = { {p.popular} - {p.allIcons(icons.length)} + {p.allIcons(icons.length)} {p.popularNote}
The page you're looking for doesn't exist or has moved.
{answer}
{p.popularNote}