feat(app): add copy unicode button
This commit is contained in:
committed by
Tobias Fried
parent
7502b8b3ce
commit
697c6c836c
@@ -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
18
pnpm-lock.yaml
generated
@@ -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)
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,7 +68,6 @@ const IconGridItem = (props: IconGridItemProps) => {
|
|||||||
}, [originOffset]);
|
}, [originOffset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid-item"
|
className="grid-item"
|
||||||
key={name}
|
key={name}
|
||||||
@@ -91,7 +90,6 @@ const IconGridItem = (props: IconGridItemProps) => {
|
|||||||
{isUpdated && <span className="badge updated">•</span>}
|
{isUpdated && <span className="badge updated">•</span>}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user