feat(app): add copy unicode button

This commit is contained in:
rektdeckard
2024-01-10 01:06:34 -07:00
committed by Tobias Fried
parent 7502b8b3ce
commit 697c6c836c
8 changed files with 73 additions and 41 deletions

View File

@@ -27,13 +27,12 @@
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\"" "format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/core": "^2.0.2", "@phosphor-icons/core": "^2.0.4",
"@phosphor-icons/react": "^2.0.15", "@phosphor-icons/react": "^2.0.15",
"@recoiljs/refine": "^0.1.1", "@recoiljs/refine": "^0.1.1",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"framer-motion": "^9.0.1", "framer-motion": "^10.17.12",
"fuse.js": "^6.4.1", "fuse.js": "^6.4.1",
"prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropdown-select": "^4.4.2", "react-dropdown-select": "^4.4.2",

18
pnpm-lock.yaml generated
View File

@@ -6,7 +6,7 @@ settings:
dependencies: dependencies:
'@phosphor-icons/core': '@phosphor-icons/core':
specifier: ^2.0.2 specifier: ^2.0.4
version: 2.0.4 version: 2.0.4
'@phosphor-icons/react': '@phosphor-icons/react':
specifier: ^2.0.15 specifier: ^2.0.15
@@ -18,14 +18,11 @@ dependencies:
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.5 version: 2.0.5
framer-motion: framer-motion:
specifier: ^9.0.1 specifier: ^10.17.12
version: 9.1.7(react-dom@18.2.0)(react@18.2.0) version: 10.17.12(react-dom@18.2.0)(react@18.2.0)
fuse.js: fuse.js:
specifier: ^6.4.1 specifier: ^6.4.1
version: 6.6.2 version: 6.6.2
prop-types:
specifier: ^15.8.1
version: 15.8.1
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@@ -1043,11 +1040,16 @@ packages:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
dev: false dev: false
/framer-motion@9.1.7(react-dom@18.2.0)(react@18.2.0): /framer-motion@10.17.12(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-nKxBkIO4IPkMEqcBbbATxsVjwPYShKl051yhBv9628iAH6JLeHD0siBHxkL62oQzMC1+GNX73XtPjgP753ufuw==} resolution: {integrity: sha512-6aaBLN2EgH/GilXwOzEalTfw5Rx9DTQJJjTrxq5bfDbGtPCzXz2GCN6ePGRpTi1ZGugLHxdU273h38ENbcdFKQ==}
peerDependencies: peerDependencies:
react: ^18.0.0 react: ^18.0.0
react-dom: ^18.0.0 react-dom: ^18.0.0
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)

View File

@@ -8,7 +8,7 @@ import IconGrid from "@/components/IconGrid";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import Notice from "@/components/Notice"; import Notice from "@/components/Notice";
import Recipes from "@/components/Recipes"; // import Recipes from "@/components/Recipes";
import { useCSSVariables } from "@/hooks"; import { useCSSVariables } from "@/hooks";
import { isDarkThemeSelector } from "@/state"; import { isDarkThemeSelector } from "@/state";

View File

@@ -10,7 +10,7 @@
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
max-width: 1120px; max-width: 1152px;
margin: auto; margin: auto;
} }

View File

@@ -64,10 +64,15 @@ const IconGrid = (_: IconGridProps) => {
<IconContext.Provider value={{ weight, size, color, mirrored: false }}> <IconContext.Provider value={{ weight, size, color, mirrored: false }}>
<div className="grid-container"> <div className="grid-container">
<i id="beacon" className="beacon" /> <i id="beacon" className="beacon" />
<motion.div className="grid" initial="hidden" animate={controls}> <motion.div
key={query}
className="grid"
initial="hidden"
animate={controls}
>
{filteredQueryResults.map((iconEntry, index) => ( {filteredQueryResults.map((iconEntry, index) => (
<IconGridItem <IconGridItem
key={index} key={iconEntry.name}
index={index} index={index}
isDark={isDark} isDark={isDark}
entry={iconEntry} entry={iconEntry}

View File

@@ -20,7 +20,7 @@ interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
const transition = { duration: 0.2 }; const transition = { duration: 0.2 };
const originIndex = 0; const originIndex = 0;
const delayPerPixel = 0.0004; const delayPerPixel = 0.0003;
const itemVariants = { const itemVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
@@ -68,30 +68,28 @@ const IconGridItem = (props: IconGridItemProps) => {
}, [originOffset]); }, [originOffset]);
return ( return (
<> <motion.div
<motion.div className="grid-item"
className="grid-item" key={name}
key={name} ref={ref}
ref={ref} tabIndex={0}
tabIndex={0} style={{
style={{ ...style,
...style, backgroundColor: isOpen ? "var(--background-layer)" : undefined,
backgroundColor: isOpen ? "var(--background-layer)" : undefined, }}
}} custom={delayRef}
custom={delayRef} transition={transition}
transition={transition} variants={itemVariants}
variants={itemVariants} onKeyPress={(e) => e.key === "Enter" && handleOpen()}
onKeyPress={(e) => e.key === "Enter" && handleOpen()} onClick={handleOpen}
onClick={handleOpen} >
> <Icon />
<Icon /> <p>
<p> <span className="name">{name}</span>
<span className="name">{name}</span> {isNew && <span className="badge new"></span>}
{isNew && <span className="badge new"></span>} {isUpdated && <span className="badge updated"></span>}
{isUpdated && <span className="badge updated"></span>} </p>
</p> </motion.div>
</motion.div>
</>
); );
}; };

View File

@@ -18,6 +18,7 @@ import {
CaretDoubleLeft, CaretDoubleLeft,
CaretDoubleRight, CaretDoubleRight,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import Tabs, { Tab } from "@/components/Tabs"; import Tabs, { Tab } from "@/components/Tabs";
@@ -61,6 +62,7 @@ enum CopyType {
SVG_DATA, SVG_DATA,
PNG, PNG,
PNG_DATA, PNG_DATA,
UNICODE,
} }
function cloneWithSize(svg: SVGSVGElement, size: number): SVGSVGElement { function cloneWithSize(svg: SVGSVGElement, size: number): SVGSVGElement {
@@ -75,12 +77,18 @@ const ActionButton = (
active?: boolean; active?: boolean;
label: string; label: string;
download?: boolean; download?: boolean;
disabled?: boolean;
} & HTMLAttributes<HTMLButtonElement> } & HTMLAttributes<HTMLButtonElement>
) => { ) => {
const { active, download, label, ...rest } = props; const { active, download, label, ...rest } = props;
const Icon = download ? ArrowFatLinesDown : Copy; const Icon = download ? ArrowFatLinesDown : Copy;
return ( return (
<button {...rest} className="action-button text" tabIndex={0}> <button
{...rest}
className={`action-button text ${props.disabled ? "disabled" : ""}`}
aria-disabled={props.disabled}
tabIndex={0}
>
{active ? ( {active ? (
<CheckCircle size={20} color="var(--olive)" weight="fill" /> <CheckCircle size={20} color="var(--olive)" weight="fill" />
) : ( ) : (
@@ -246,6 +254,14 @@ const Panel = () => {
setCopied(CopyType.SVG_RAW); setCopied(CopyType.SVG_RAW);
}; };
const handleCopyUnicode = async () => {
if (!entry) return;
const content = String.fromCharCode(entry.codepoint);
navigator.clipboard?.writeText(content);
setCopied(CopyType.UNICODE);
};
const handleDownloadSVG = ( const handleDownloadSVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
@@ -335,6 +351,9 @@ const Panel = () => {
<entry.Icon ref={ref} size={64}></entry.Icon> <entry.Icon ref={ref} size={64}></entry.Icon>
<figcaption> <figcaption>
<p>{entry.name}</p> <p>{entry.name}</p>
<small className="versioning">
U+{entry.codepoint.toString(16).toUpperCase()}
</small>
<small className="versioning"> <small className="versioning">
available in v{entry.published_in.toFixed(1)}.0+ available in v{entry.published_in.toFixed(1)}.0+
</small> </small>
@@ -395,6 +414,14 @@ const Panel = () => {
active={copied === CopyType.SVG_DATA} active={copied === CopyType.SVG_DATA}
onClick={handleCopyDataSVG} onClick={handleCopyDataSVG}
/> />
<ActionButton
label="Unicode"
title="Copy Unicode character (v2.1.0 or newer)"
active={copied === CopyType.UNICODE}
disabled={weight === IconStyle.DUOTONE}
onClick={handleCopyUnicode}
/>
</> </>
)} )}
</div> </div>

View File

@@ -42,6 +42,7 @@ export default ({ children }: { children: ReactNode }) => {
const listen: ListenToItems = useCallback( const listen: ListenToItems = useCallback(
({ updateItem, updateAllKnownItems }) => { ({ updateItem, updateAllKnownItems }) => {
void updateAllKnownItems;
const onStorage = (event: StorageEvent) => { const onStorage = (event: StorageEvent) => {
// ignore clear() calls // ignore clear() calls
if (event.storageArea === localStorage && event.key !== null) { if (event.storageArea === localStorage && event.key !== null) {