Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
675c4fc364 | ||
|
|
85af26fd1c | ||
|
|
d379cea5bc | ||
|
|
baeec27267 | ||
|
|
345acafb45 | ||
|
|
eba876b3ea | ||
|
|
3756374140 | ||
|
|
5e7f85ffdc | ||
|
|
1b8d6c48fc | ||
|
|
ced8732edb | ||
|
|
c0d84d9aec | ||
|
|
2a030b08cd | ||
|
|
b708d823a7 | ||
|
|
59285244ac | ||
|
|
511c191a02 | ||
|
|
a9ce9faac8 | ||
|
|
bda9942fff | ||
|
|
c3cd3c1d80 | ||
|
|
4137a8b5c1 | ||
|
|
4d7f5ea100 | ||
|
|
3cdcdd4e0d | ||
|
|
c090531800 | ||
|
|
887617e523 | ||
|
|
e242bcc660 | ||
|
|
22b69c3ae6 | ||
|
|
e4b99d2ca9 | ||
|
|
14d8c9d0e7 | ||
|
|
cdcf38466e | ||
|
|
f256109ba4 | ||
|
|
bcff9fecb3 | ||
|
|
a218b632ba | ||
|
|
56dd2ba514 | ||
|
|
f5089e1c60 | ||
|
|
77d93e4038 | ||
|
|
b6e2ae7da5 | ||
|
|
a885931831 | ||
|
|
2eb51f7ca7 | ||
|
|
7e1bd3d18e | ||
|
|
94e5d9b305 | ||
|
|
02e70848b1 | ||
|
|
b8eac52689 | ||
|
|
73b66e2e86 | ||
|
|
0e50efb5ea | ||
|
|
dc6764e387 | ||
|
|
2ba5ac332b | ||
|
|
b9a0b93067 | ||
|
|
6596bce68a | ||
|
|
6d74c9f719 | ||
|
|
02525cabb5 | ||
|
|
8974b2de19 | ||
|
|
61fe3d9de8 | ||
|
|
62d0524f34 | ||
|
|
b3b328876f | ||
|
|
f598e3ab50 | ||
|
|
78ff8e4500 | ||
|
|
98ef9db51a | ||
|
|
14c8807234 | ||
|
|
c3787fcde0 | ||
|
|
5166b0345c | ||
|
|
4d602cfce2 | ||
|
|
e981a86dee | ||
|
|
1390dfae4d | ||
|
|
ce0d323bd5 | ||
|
|
7bf1833ea2 | ||
|
|
de97e02427 | ||
|
|
2e9c7870bd | ||
|
|
fe656731fd | ||
|
|
0ea6cdc930 | ||
|
|
71c6a6927c | ||
|
|
30e777f856 | ||
|
|
71eba27c92 | ||
|
|
1221b5628b | ||
|
|
6fdef06b75 | ||
|
|
e7865e25e7 | ||
|
|
8a30bc5990 | ||
|
|
d0c4891480 | ||
|
|
20b077a05f | ||
|
|
ea1793a0fa | ||
|
|
3b0d30ed04 | ||
|
|
3055fbb955 | ||
|
|
e38f82501d | ||
|
|
55931e9f13 | ||
|
|
40f345b0d2 | ||
|
|
60c3aecc34 | ||
|
|
cdc14e8ddd | ||
|
|
5207219415 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
// Place your phosphor-web workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
// Place your phosphor-home workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||||
@@ -21,10 +21,10 @@
|
|||||||
"{",
|
"{",
|
||||||
"\tname: \"${1:name}\",",
|
"\tname: \"${1:name}\",",
|
||||||
"\tcategories: [IconCategory${2:categories}],",
|
"\tcategories: [IconCategory${2:categories}],",
|
||||||
"\ttags: [${3:tags}],",
|
"\ttags: [\"*new*\", ${3:tags}],",
|
||||||
"\tIcon: ${4:icon},",
|
"\tIcon: Icons.${4:icon},",
|
||||||
"},"
|
"},"
|
||||||
],
|
],
|
||||||
"description": "Create an IconEntry for phosphor-web"
|
"description": "Create an IconEntry for phosphor-home"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
54
README.md
54
README.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really.
|
Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really.
|
||||||
|
|
||||||
- 588 icons and counting
|
- 1,248 icons and counting
|
||||||
- 6 weights: **Thin**, **Light**, **Regular**, **Bold**, **Fill**, and **Duotone**
|
- 6 weights: **Thin**, **Light**, **Regular**, **Bold**, **Fill**, and **Duotone**
|
||||||
- Designed at 16 x 16px to read well small and scale up big
|
- Designed at 16 x 16px to read well small and scale up big
|
||||||
- Raw stroke information retained to fine-tune the style
|
- Raw stroke information retained to fine-tune the style
|
||||||
@@ -13,9 +13,9 @@ More ways to use at [phosphoricons.com](https://phosphoricons.com).
|
|||||||
|
|
||||||
## For developers
|
## For developers
|
||||||
|
|
||||||
Phosphor is available as a [one-liner](https://github.com/phosphor-icons/phosphor-icons) script, [React package](https://github.com/phosphor-icons/phosphor-react), and [Vue package](https://github.com/phosphor-icons/phosphor-vue), all of which can be sourced from NPM or from a CDN.
|
Phosphor is available for [web](https://github.com/phosphor-icons/web), [React](https://github.com/phosphor-icons/react), [Vue](https://github.com/phosphor-icons/vue), [Flutter](https://github.com/phosphor-icons/flutter), [Elm](https://github.com/phosphor-icons/phosphor-elm), and other frameworks and platforms.
|
||||||
|
|
||||||
### HTML/CSS
|
### Vanilla Web
|
||||||
|
|
||||||
- **Simple to use** – We use a similar approach as many other icon sets out there, providing icons as a webfont that uses Unicode's Private Use Area character codes to map normally non-rendering characters to icons. But you don't need to know that. All you need to do is add the script to the document `<head>`, and drop in icons with an `<i/>` tag and the appropriate class:
|
- **Simple to use** – We use a similar approach as many other icon sets out there, providing icons as a webfont that uses Unicode's Private Use Area character codes to map normally non-rendering characters to icons. But you don't need to know that. All you need to do is add the script to the document `<head>`, and drop in icons with an `<i/>` tag and the appropriate class:
|
||||||
|
|
||||||
@@ -23,26 +23,26 @@ Phosphor is available as a [one-liner](https://github.com/phosphor-icons/phospho
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://unpkg.com/phosphor-icons"></script>
|
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<i class="ph-smiley"></i>
|
<i class="ph-smiley"></i>
|
||||||
<i class="ph-heart-fill" style="color: hotpink"></i>
|
<i class="ph-fill ph-heart" style="color: hotpink"></i>
|
||||||
<i class="ph-cube-thin"></i>
|
<i class="ph-thin ph-cube"></i>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
Check out the full documentation on the [phosphor-icons](https://github.com/phosphor-icons/phosphor-icons) repo page.
|
Check out the full documentation on the [@phosphor-icons/web](https://github.com/phosphor-icons/web) repo page.
|
||||||
|
|
||||||
### React
|
### React
|
||||||
|
|
||||||
- **Powerful** – Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, provide default styles to all icons via the Context API, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out the full documentation on the [phosphor-react](https://github.com/phosphor-icons/phosphor-react) repo page.
|
- **Powerful** – Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, provide default styles to all icons via the Context API, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out the full documentation on the [@phosphor-icons/react](https://github.com/phosphor-icons/react) repo page.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Smiley, Heart, Horse } from "phosphor-react";
|
import { Smiley, Heart, Horse } from "@phosphor-icons/react";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
@@ -62,14 +62,14 @@ ReactDOM.render(<App />, document.getElementById("root"));
|
|||||||
|
|
||||||
### Vue
|
### Vue
|
||||||
|
|
||||||
- **Parity** – As with React, you can manipulate the `color`, `size`, and `weight` of an icon with a few keystrokes, or provide default styles to all icons via the `provide/inject` API. It is fully tree-shakable and ready to use right away. Check out the full documentation on the [phosphor-vue](https://github.com/phosphor-icons/phosphor-vue) repo page.
|
- **Parity** – As with React, you can manipulate the `color`, `size`, and `weight` of an icon with a few keystrokes, or provide default styles to all icons via the `provide/inject` API. It is fully tree-shakable and ready to use right away. Check out the full documentation on the [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) repo page.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<PhHorse />
|
<ph-horse />
|
||||||
<PhHeart :size="32" color="hotpink" weight="fill" />
|
<ph-heart :size="32" color="hotpink" weight="fill" />
|
||||||
<PhCube />
|
<ph-cube />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -86,14 +86,30 @@ ReactDOM.render(<App />, document.getElementById("root"));
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions.
|
> **Note:** Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions. Both Pascal and kebab-case conventions can be used in templates.
|
||||||
|
|
||||||
## Related Projects
|
## Our Related Projects
|
||||||
|
|
||||||
- [phosphor-react](https://github.com/phosphor-icons/phosphor-react) ▲ Phosphor icon component library for React
|
- [@phosphor-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog
|
||||||
- [phosphor-vue](https://github.com/phosphor-icons/phosphor-vue) ▲ Phosphor icon component library for Vue
|
- [@phosphor-icons/react](https://github.com/phosphor-icons/react) ▲ Phosphor icon component library for React
|
||||||
- [phosphor-icons](https://github.com/phosphor-icons/phosphor-icons) ▲ Phosphor icons for Vanilla JS
|
- [@phosphor-icons/web](https://github.com/phosphor-icons/web) ▲ Phosphor icons for Vanilla JS
|
||||||
- [phosphor-figma](https://github.com/phosphor-icons/phosphor-figma) ▲ Phosphor icons Figma plugin
|
- [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) ▲ Phosphor icon component library for Vue
|
||||||
|
- [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm
|
||||||
|
- [@phosphor-icons/flutter](https://github.com/phosphor-icons/flutter) ▲ Phosphor IconData library for Flutter
|
||||||
|
- [@phosphor-icons/webcomponents](https://github.com/phosphor-icons/webcomponents) ▲ Phosphor icons as Web Components
|
||||||
|
- [@phosphor-icons/figma](https://github.com/phosphor-icons/figma) ▲ Phosphor icons Figma plugin
|
||||||
|
- [@phosphor-icons/sketch](https://github.com/phosphor-icons/sketch) ▲ Phosphor icons Sketch plugin
|
||||||
|
|
||||||
|
## Community Projects
|
||||||
|
|
||||||
|
- [phosphor-react-native](https://github.com/duongdev/phosphor-react-native) ▲ Phosphor icon component library for React Native
|
||||||
|
- [phosphor-svelte](https://github.com/haruaki07/phosphor-svelte) ▲ Phosphor icons for Svelte apps
|
||||||
|
- [phosphor-r](https://github.com/dreamRs/phosphoricons) ▲ Phosphor icon wrapper for R documents and applications
|
||||||
|
- [blade-phosphor-icons](https://github.com/codeat3/blade-phosphor-icons) ▲ Phosphor icons in your Laravel Blade views
|
||||||
|
- [wireui/phosphoricons](https://github.com/wireui/phosphoricons) ▲ Phosphor icons for Laravel
|
||||||
|
- [phosphor-css](https://github.com/lucagoslar/phosphor-css) ▲ CSS wrapper for Phosphor SVG icons
|
||||||
|
|
||||||
|
If you've made a port of Phosphor and you want to see it here, just open a PR [here](https://github.com/phosphor-icons/phosphor-home)!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Phosphor Icons</title>
|
<title>Phosphor Icons</title>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#35313D" />
|
<meta name="theme-color" content="#35313D" />
|
||||||
<meta
|
<meta
|
||||||
@@ -18,7 +18,21 @@
|
|||||||
itemprop="description"
|
itemprop="description"
|
||||||
content="A flexible icon family for interfaces, diagrams, presentations — whatever, really."
|
content="A flexible icon family for interfaces, diagrams, presentations — whatever, really."
|
||||||
/>
|
/>
|
||||||
<meta itemprop="image" content="https://phosphoricons.com/phosphor-opengraph.png" />
|
<meta
|
||||||
|
itemprop="image"
|
||||||
|
content="https://phosphoricons.com/phosphor-opengraph.png"
|
||||||
|
/>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Project",
|
||||||
|
"url": "https://phosphoricons.com",
|
||||||
|
"email": "hello@phosphoricons.com",
|
||||||
|
"location": "Brooklyn, NY",
|
||||||
|
"description": "A flexible icon family for interfaces, diagrams, presentations — whatever, really.",
|
||||||
|
"logo": "https://phosphoricons.com/favicon-512.png"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- FACEBOOK META -->
|
<!-- FACEBOOK META -->
|
||||||
<meta property="og:title" content="Phosphor Icons" />
|
<meta property="og:title" content="Phosphor Icons" />
|
||||||
@@ -29,7 +43,10 @@
|
|||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://phosphoricons.com" />
|
<meta property="og:url" content="https://phosphoricons.com" />
|
||||||
<meta property="og:image" content="https://phosphoricons.com/phosphor-opengraph.png" />
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://phosphoricons.com/phosphor-opengraph.png"
|
||||||
|
/>
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property="og:image:type" content="image/png" />
|
||||||
<meta property="og:image:alt" content="Phosphor Icons logo" />
|
<meta property="og:image:alt" content="Phosphor Icons logo" />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
@@ -42,24 +59,27 @@
|
|||||||
name="twitter:description"
|
name="twitter:description"
|
||||||
content="A flexible icon family for interfaces, diagrams, presentations — whatever, really."
|
content="A flexible icon family for interfaces, diagrams, presentations — whatever, really."
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:image" content="https://phosphoricons.com/phosphor-opengraph.png" />
|
<meta
|
||||||
|
name="twitter:image"
|
||||||
|
content="https://phosphoricons.com/phosphor-opengraph.png"
|
||||||
|
/>
|
||||||
<meta name="twitter:site" content="@_phosphoricons" />
|
<meta name="twitter:site" content="@_phosphoricons" />
|
||||||
<meta name="twitter:creator" content="@friedtm" />
|
<meta name="twitter:creator" content="@friedtm" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon-192.png" />
|
<link rel="apple-touch-icon" href="/favicon-192.png" />
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
sizes="32x32"
|
sizes="32x32"
|
||||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
href="/favicon-32x32.png"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
href="/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -92,5 +112,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
76
package.json
76
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "phosphor-web",
|
"name": "phosphor-home",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://phosphoricons.com",
|
"homepage": "https://phosphoricons.com",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -18,34 +18,42 @@
|
|||||||
"UI",
|
"UI",
|
||||||
"UX"
|
"UX"
|
||||||
],
|
],
|
||||||
"repository": "github:phosphor-icons/phosphor-web",
|
"repository": "github:phosphor-icons/homepage",
|
||||||
"private": false,
|
"private": true,
|
||||||
"dependencies": {
|
|
||||||
"file-saver": "^2.0.2",
|
|
||||||
"framer-motion": "^2.1.0",
|
|
||||||
"fuse.js": "^6.4.1",
|
|
||||||
"phosphor-icons": "^1.0.0",
|
|
||||||
"phosphor-react": "^0.6.0",
|
|
||||||
"react": "^17.0.0-rc.0",
|
|
||||||
"react-dom": "^17.0.0-rc.0",
|
|
||||||
"react-dropdown-select": "^4.4.2",
|
|
||||||
"react-ga": "^3.1.2",
|
|
||||||
"react-scripts": "3.4.1",
|
|
||||||
"react-use": "^15.3.2",
|
|
||||||
"recoil": "^0.0.13",
|
|
||||||
"svg2png-converter": "^1.0.0",
|
|
||||||
"tinycolor2": "^1.4.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"dev": "vite",
|
||||||
"start": "react-scripts start",
|
"build": "tsc && vite build",
|
||||||
"build": "react-scripts build",
|
"preview": "vite preview",
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"predeploy": "npm run build",
|
|
||||||
"deploy": "gh-pages -d build",
|
|
||||||
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
|
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@phosphor-icons/core": "^2.0.2",
|
||||||
|
"@phosphor-icons/react": "^2.0.4",
|
||||||
|
"file-saver": "^2.0.2",
|
||||||
|
"framer-motion": "^9.0.1",
|
||||||
|
"fuse.js": "^6.4.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropdown-select": "^4.4.2",
|
||||||
|
"react-ga4": "^2.0.0",
|
||||||
|
"react-hotkeys-hook": "^3.2.1",
|
||||||
|
"react-use": "^17.4.0",
|
||||||
|
"recoil": "^0.7.6",
|
||||||
|
"svg2png-converter": "^1.0.2",
|
||||||
|
"tinycolor2": "^1.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/react": "^18.0.27",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"@types/tinycolor2": "^1.4.3",
|
||||||
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "^4.1.1",
|
||||||
|
"vite-plugin-svgr": "^2.4.0"
|
||||||
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
},
|
},
|
||||||
@@ -60,19 +68,5 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
|
||||||
"@testing-library/react": "^9.3.2",
|
|
||||||
"@testing-library/user-event": "^7.1.2",
|
|
||||||
"@types/file-saver": "^2.0.1",
|
|
||||||
"@types/jest": "^24.0.0",
|
|
||||||
"@types/node": "^12.0.0",
|
|
||||||
"@types/react": "^16.9.46",
|
|
||||||
"@types/react-dom": "^16.9.8",
|
|
||||||
"@types/react-list": "^0.8.5",
|
|
||||||
"@types/react-virtualized": "^9.21.10",
|
|
||||||
"@types/tinycolor2": "^1.4.2",
|
|
||||||
"typescript": "^3.9.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
public/.htaccess
Normal file
9
public/.htaccess
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{HTTPS} !=on
|
||||||
|
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,NE]
|
||||||
|
|
||||||
|
Options +SymLinksIfOwnerMatch
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
242
public/assets/phosphor-android/cadmium/wallpaper.json
Normal file
242
public/assets/phosphor-android/cadmium/wallpaper.json
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"Collections": [
|
||||||
|
{
|
||||||
|
"name": "abstract",
|
||||||
|
"preview_url": "https://i.imgur.com/xBCTxrP.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/xBCTxrPl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "architecture",
|
||||||
|
"preview_url": "https://i.imgur.com/pdV0KQ1.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/pdV0KQ1l.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "landscape",
|
||||||
|
"preview_url": "https://i.imgur.com/EEBaeFE.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/EEBaeFEl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minimal",
|
||||||
|
"preview_url": "https://i.imgur.com/peX5qVO.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/peX5qVOl.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Wallpapers": [
|
||||||
|
{
|
||||||
|
"name": "Antelope Canyon",
|
||||||
|
"author": "Daniel Olah Nvez",
|
||||||
|
"url": "https://i.imgur.com/KYglyUn.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/KYglyUl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fractured Light",
|
||||||
|
"author": "Chris Limbrick",
|
||||||
|
"url": "https://i.imgur.com/MILbsga.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/MILbsgal.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "The Forest Path",
|
||||||
|
"author": "Kristaps Ungurs",
|
||||||
|
"url": "https://i.imgur.com/95QLblG.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/95QLblGl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Taken",
|
||||||
|
"author": "Touann Gatouillat",
|
||||||
|
"url": "https://i.imgur.com/otCPhH7.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/otCPhH7l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Construct",
|
||||||
|
"author": "Florian Olivo",
|
||||||
|
"url": "https://i.imgur.com/7CxJsXn.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/7CxJsXnl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Redline",
|
||||||
|
"author": "Sergio Ibannez",
|
||||||
|
"url": "https://i.imgur.com/uja4O1m.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/uja4O1ml.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "End Of Line",
|
||||||
|
"author": "Claudio Schwarz-Purzlbaum",
|
||||||
|
"url": "https://i.imgur.com/2ZiqhE3.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/2ZiqhE3l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Downtown",
|
||||||
|
"author": "Van Mendoza",
|
||||||
|
"url": "https://i.imgur.com/sKizrcp.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/sKizrcpl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Omnicorp",
|
||||||
|
"author": "Irina Iriser",
|
||||||
|
"url": "https://i.imgur.com/Zx11fXx.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Zx11fXxl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pinball Wizard",
|
||||||
|
"author": "Joey Banks",
|
||||||
|
"url": "https://i.imgur.com/XpVCRrj.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/XpVCRrjl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chinatown",
|
||||||
|
"author": "Donny Jiang",
|
||||||
|
"url": "https://i.imgur.com/mz8mTVC.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/mz8mTVCl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FDR Drive",
|
||||||
|
"author": "Donny Jiang",
|
||||||
|
"url": "https://i.imgur.com/elbdmZk.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/elbdmZkl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Through the Fog",
|
||||||
|
"author": "Zhimai Zhang",
|
||||||
|
"url": "https://i.imgur.com/KQIss5a.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/KQIss5al.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kids Will Play",
|
||||||
|
"author": "Perry C",
|
||||||
|
"url": "https://i.imgur.com/IieNbdS.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/IieNbdSl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Double Blind",
|
||||||
|
"author": "Pau Casals",
|
||||||
|
"url": "https://i.imgur.com/kKyn0dW.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/kKyn0dWl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All-Seeing Eye",
|
||||||
|
"author": "Alex Rainer",
|
||||||
|
"url": "https://i.imgur.com/HtyCalZ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/HtyCalZl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Methylene",
|
||||||
|
"author": "Pim Myten",
|
||||||
|
"url": "https://i.imgur.com/vOAYeEW.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/vOAYeEWl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nexus",
|
||||||
|
"author": "Alexander Popov",
|
||||||
|
"url": "https://i.imgur.com/3q2e7N2.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/3q2e7N2l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Synthetic",
|
||||||
|
"author": "Leyy M",
|
||||||
|
"url": "https://i.imgur.com/ASKhMFU.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/ASKhMFUl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nondeterminism",
|
||||||
|
"author": "Alex Perez",
|
||||||
|
"url": "https://i.imgur.com/6dtZBAW.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/6dtZBAWl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "In the Beginning",
|
||||||
|
"author": "Pawel Czerwinski",
|
||||||
|
"url": "https://i.imgur.com/gBSjMh2.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/gBSjMh2l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "The Swarm",
|
||||||
|
"author": "Vino Li",
|
||||||
|
"url": "https://i.imgur.com/yljYDqp.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/yljYDqpl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Moonrise",
|
||||||
|
"author": "Adrian Swancar",
|
||||||
|
"url": "https://i.imgur.com/NiSCb6W.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/NiSCb6Wl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cerro",
|
||||||
|
"author": "Tamas Tuzes Katai",
|
||||||
|
"url": "https://i.imgur.com/5u4aM1y.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/5u4aM1yl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
409
public/assets/phosphor-android/carbon/wallpaper.json
Normal file
409
public/assets/phosphor-android/carbon/wallpaper.json
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
{
|
||||||
|
"Collections": [
|
||||||
|
{
|
||||||
|
"name": "Abstract",
|
||||||
|
"preview_url": "https://i.imgur.com/8DTRYwb.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/8DTRYwbl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Architecture",
|
||||||
|
"preview_url":"https://i.imgur.com/t6BTASJ.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/t6BTASJl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Landscape",
|
||||||
|
"preview_url": "https://i.imgur.com/G8cgftc.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/G8cgftcl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Minimal",
|
||||||
|
"preview_url": "https://i.imgur.com/Srt9bP3.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/Srt9bP3l.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Solids",
|
||||||
|
"preview_url": "https://i.imgur.com/fzL4yoj.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/fzL4yojl.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Wallpapers": [
|
||||||
|
{
|
||||||
|
"name": "Arctic",
|
||||||
|
"author": "Phosphor",
|
||||||
|
"url": "https://i.imgur.com/4qopfYy.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/4qopfYyl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "solids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saffron",
|
||||||
|
"author": "Phosphor",
|
||||||
|
"url": "https://i.imgur.com/fzL4yoj.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/fzL4yojl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "solids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Heliotrope",
|
||||||
|
"author": "Phosphor",
|
||||||
|
"url": "https://i.imgur.com/QsrKIjp.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QsrKIjpl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "solids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jade",
|
||||||
|
"author": "Phosphor",
|
||||||
|
"url": "https://i.imgur.com/Z29e7f7.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Z29e7f7l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "solids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soft Dunes",
|
||||||
|
"author": "Kunj Parekh",
|
||||||
|
"url": "https://i.imgur.com/vIAE5jd.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/vIAE5jdl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscapes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lake at Dawn",
|
||||||
|
"author": "Max Fuchs",
|
||||||
|
"url": "https://i.imgur.com/4Nmefrv.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/4Nmefrvl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Doors of Perception",
|
||||||
|
"author": "Philipp Berndt",
|
||||||
|
"url": "https://i.imgur.com/Srt9bP3.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Srt9bP3l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TwoTone",
|
||||||
|
"author": "Samuel Zeller",
|
||||||
|
"url": "https://i.imgur.com/8DTRYwb.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/8DTRYwbl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Empty Court",
|
||||||
|
"author": "35mm",
|
||||||
|
"url": "https://i.imgur.com/rPSLdCD.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/rPSLdCDl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Edge of the Pool",
|
||||||
|
"author": "Autumn Studio",
|
||||||
|
"url": "https://i.imgur.com/qNXWTej.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/qNXWTejl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "One in the Hand",
|
||||||
|
"author": "Rainon Franco",
|
||||||
|
"url": "https://i.imgur.com/QhW4UDc.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QhW4UDcl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Solitude",
|
||||||
|
"author": "Braxton Stuntz",
|
||||||
|
"url": "https://i.imgur.com/QlQ9tMO.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QlQ9tMOl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Double Fault",
|
||||||
|
"author": "Dane Deaner",
|
||||||
|
"url": "https://i.imgur.com/ERMWJSe.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/ERMWJSel.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lomo 800",
|
||||||
|
"author": "Markus Spiske",
|
||||||
|
"url": "https://i.imgur.com/bHPWGPQ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/bHPWGPQl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Clothespins",
|
||||||
|
"author": "Plush Design Studio",
|
||||||
|
"url": "https://i.imgur.com/xPyXBL9.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/xPyXBL9l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Memory Tapes",
|
||||||
|
"author": "Imani Clovis",
|
||||||
|
"url": "https://i.imgur.com/cmWuvUT.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/cmWuvUTl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Contours in White",
|
||||||
|
"author": "Jean Philippe del Berghe",
|
||||||
|
"url": "https://i.imgur.com/wjxl9xT.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/wjxl9xTl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Awaiting Inspiration",
|
||||||
|
"author": "Joanna Kosinska",
|
||||||
|
"url": "https://i.imgur.com/SBtlZmn.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/SBtlZmnl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Succulent",
|
||||||
|
"author": "Khai Sze Ong",
|
||||||
|
"url": "https://i.imgur.com/yDD2wz8.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/yDD2wz8l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tracks in the Sand",
|
||||||
|
"author": "Ruben Bagues",
|
||||||
|
"url": "https://i.imgur.com/G8cgftc.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/G8cgftcl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Starched Linen",
|
||||||
|
"author": "Annie Spratt",
|
||||||
|
"url": "https://i.imgur.com/W9nAQVR.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/W9nAQVRl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delamination",
|
||||||
|
"author": "Wesley Tingey",
|
||||||
|
"url": "https://i.imgur.com/VNJ3xWG.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/VNJ3xWGl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Look Up",
|
||||||
|
"author": "Tony Dinh",
|
||||||
|
"url": "https://i.imgur.com/t6BTASJ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/t6BTASJl.jpgg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Keep to the Right",
|
||||||
|
"author": "hello i m nik",
|
||||||
|
"url": "https://i.imgur.com/SHp6pkv.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/SHp6pkvl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pink Paper",
|
||||||
|
"author": "Alex Koch",
|
||||||
|
"url": "https://i.imgur.com/OHPR80R.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/OHPR80Rl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lavender Dunes",
|
||||||
|
"author": "Alex Koch",
|
||||||
|
"url": "https://i.imgur.com/Sb50W4f.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Sb50W4fl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cotton Candy Clouds",
|
||||||
|
"author": "Autumn Studio",
|
||||||
|
"url": "https://i.imgur.com/43Vf2hT.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/43Vf2hTl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pineapple",
|
||||||
|
"author": "Pineaaple Supply Co.",
|
||||||
|
"url": "https://i.imgur.com/YK9TyNk.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/YK9TyNkl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cuttings",
|
||||||
|
"author": "Alex Loup",
|
||||||
|
"url": "https://i.imgur.com/wp2S7TK.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/wp2S7TKl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Textured Ombre Wall",
|
||||||
|
"author": "Bharath G.S.",
|
||||||
|
"url": "https://i.imgur.com/kxBGzva.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/kxBGzval.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "The Loading Dock",
|
||||||
|
"author": "Ph. B.",
|
||||||
|
"url": "https://i.imgur.com/NmL5Ldm.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/NmL5Ldml.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Leaning Dwarf Pine",
|
||||||
|
"author": "Caleb George",
|
||||||
|
"url": "https://i.imgur.com/crgckrr.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/crgckrrl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dunes in Hard Light",
|
||||||
|
"author": "Heather Shevlin",
|
||||||
|
"url": "https://i.imgur.com/LG0G1IO.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/LG0G1IOl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "3 Balloons",
|
||||||
|
"author": "Amy Shamblen",
|
||||||
|
"url": "https://i.imgur.com/5HHCh12.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/5HHCh12l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bicolor Bricks",
|
||||||
|
"author": "Pawel Czerwinski",
|
||||||
|
"url": "https://i.imgur.com/i4VrEDH.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/i4VrEDHl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acrid Sky",
|
||||||
|
"author": "W",
|
||||||
|
"url": "https://i.imgur.com/RUSz9mM.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/RUSz9mMl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Salmon Colored Smoke",
|
||||||
|
"author": "Pawel Czerwinski",
|
||||||
|
"url": "https://i.imgur.com/8wehlrT.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/8wehlrTl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polygonal Grid",
|
||||||
|
"author": "Scott Webb",
|
||||||
|
"url": "https://i.imgur.com/voUtZhY.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/voUtZhYl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Clay Courts",
|
||||||
|
"author": "Ph. B.",
|
||||||
|
"url": "https://i.imgur.com/yd7OmyV.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/yd7OmyVl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lavender on Yellow",
|
||||||
|
"author": "Mona Eendra",
|
||||||
|
"url": "https://i.imgur.com/p8T1V7N.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/p8T1V7Nl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal,landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Whispering Pines",
|
||||||
|
"author": "Mads Schmidt Rasmussen",
|
||||||
|
"url": "https://i.imgur.com/FLkrDVZ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/FLkrDVZl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All the Light Touches",
|
||||||
|
"author": "Eberhard Grossgasteiger",
|
||||||
|
"url": "https://i.imgur.com/cwUvsmS.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/cwUvsmSl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
public/assets/phosphor-android/classic/app_feature_mercury_a.jpg
Normal file
BIN
public/assets/phosphor-android/classic/app_feature_mercury_a.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
public/assets/phosphor-android/classic/app_feature_mercury_b.jpg
Normal file
BIN
public/assets/phosphor-android/classic/app_feature_mercury_b.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
350
public/assets/phosphor-android/classic/wallpaper.json
Normal file
350
public/assets/phosphor-android/classic/wallpaper.json
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
{
|
||||||
|
"Collections": [
|
||||||
|
{
|
||||||
|
"name": "abstract",
|
||||||
|
"preview_url": "https://i.imgur.com/xBCTxrP.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/xBCTxrPl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "architecture",
|
||||||
|
"preview_url": "https://i.imgur.com/pdV0KQ1.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/pdV0KQ1l.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "landscape",
|
||||||
|
"preview_url": "https://i.imgur.com/EEBaeFE.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/EEBaeFEl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minimal",
|
||||||
|
"preview_url": "https://i.imgur.com/peX5qVO.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/peX5qVOl.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Wallpapers": [
|
||||||
|
{
|
||||||
|
"name": "Dunes at Dusk",
|
||||||
|
"author": "Jeremy Bishop",
|
||||||
|
"url": "https://i.imgur.com/EEBaeFE.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/EEBaeFEl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Black Sand",
|
||||||
|
"author": "Adrien Olichon",
|
||||||
|
"url": "https://i.imgur.com/Yt8zaUn.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Yt8zaUnl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Disappearing Coastline",
|
||||||
|
"author": "Chris Coe",
|
||||||
|
"url": "https://i.imgur.com/hq4aENh.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/hq4aENhl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal,landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Setting Out",
|
||||||
|
"author": "Tim Trad",
|
||||||
|
"url": "https://i.imgur.com/42uDJkj.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/42uDJkjl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dark Tides",
|
||||||
|
"author": "Mike Yukhtenko",
|
||||||
|
"url": "https://i.imgur.com/XSIIUAQ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/XSIIUAQl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rogue Wave",
|
||||||
|
"author": "Jack B",
|
||||||
|
"url": "https://i.imgur.com/YVHtCTT.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/YVHtCTTl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Strata",
|
||||||
|
"author": "Joshua Oluwagbemiga",
|
||||||
|
"url": "https://i.imgur.com/uvRONTa.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/uvRONTal.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Condolences",
|
||||||
|
"author": "Annie Spratt",
|
||||||
|
"url": "https://i.imgur.com/peX5qVO.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/peX5qVOl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ferns",
|
||||||
|
"author": "Andras Vas",
|
||||||
|
"url": "https://i.imgur.com/wXvAISN.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/wXvAISNl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Blades of Grass",
|
||||||
|
"author": "Claudio Testa",
|
||||||
|
"url": "https://i.imgur.com/0QOoq8R.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/0QOoq8Rl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lush",
|
||||||
|
"author": "Ian Espinosa",
|
||||||
|
"url": "https://i.imgur.com/AumRAb4.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/AumRAb4l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Amethyst Bloom",
|
||||||
|
"author": "Alyssa Smith",
|
||||||
|
"url": "https://i.imgur.com/96JmZIF.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/96JmZIFl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Carbon",
|
||||||
|
"author": "David Jorre",
|
||||||
|
"url": "https://i.imgur.com/hkN3ioi.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/hkN3ioil.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Charred Remains",
|
||||||
|
"author": "Brian Patrick Tagalog",
|
||||||
|
"url": "https://i.imgur.com/SbyCpR2.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/SbyCpR2l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Make a Wish",
|
||||||
|
"author": "Wil Stewart",
|
||||||
|
"url": "https://i.imgur.com/VeuzvUA.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/VeuzvUAl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Resonance",
|
||||||
|
"author": "Luke Stackpoole",
|
||||||
|
"url": "https://i.imgur.com/94SjlPi.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/94SjlPil.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "An Endless Mist",
|
||||||
|
"author": "Yoal Desurmont",
|
||||||
|
"url": "https://i.imgur.com/Qkng6Dm.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Qkng6Dml.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elevation",
|
||||||
|
"author": "Manuel Will",
|
||||||
|
"url": "https://i.imgur.com/yQMGOAb.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/yQMGOAbl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rock Show",
|
||||||
|
"author": "Raphael Schaller",
|
||||||
|
"url": "https://i.imgur.com/DMi6ffB.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/DMi6ffBl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Motes",
|
||||||
|
"author": "Samuel Zeller",
|
||||||
|
"url": "https://i.imgur.com/kOIWSNJ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/kOIWSNJl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Birds on a Wire",
|
||||||
|
"author": "Adrian Kirkegaard",
|
||||||
|
"url": "https://i.imgur.com/M3dlKS4.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/M3dlKS4l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Interleaved",
|
||||||
|
"author": "Ron Whitaker",
|
||||||
|
"url": "https://i.imgur.com/QQ7WQvD.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QQ7WQvDl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elliptic",
|
||||||
|
"author": "Tobias van Schneider",
|
||||||
|
"url": "https://i.imgur.com/xBCTxrP.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/xBCTxrPl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Convergence",
|
||||||
|
"author": "Josh Rose",
|
||||||
|
"url": "https://i.imgur.com/uqBhWYH.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/uqBhWYHl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Baffles",
|
||||||
|
"author": "Elena Saharova",
|
||||||
|
"url": "https://i.imgur.com/l2L6u1E.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/l2L6u1El.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polygonal",
|
||||||
|
"author": "Carrie Yang",
|
||||||
|
"url": "https://i.imgur.com/pdV0KQ1.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/pdV0KQ1l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "S-Curve",
|
||||||
|
"author": "Tobias van Schneider",
|
||||||
|
"url": "https://i.imgur.com/Y1A9GTi.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Y1A9GTil.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Galaxy Swirls",
|
||||||
|
"author": "Pawel Czerwinski",
|
||||||
|
"url": "https://i.imgur.com/KrxLGxy.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/KrxLGxyl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prismatic Dreams",
|
||||||
|
"author": "Sean Sinclair",
|
||||||
|
"url": "https://i.imgur.com/byLsXXB.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/byLsXXBl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slot Canyon",
|
||||||
|
"author": "Meric Dagli",
|
||||||
|
"url": "https://i.imgur.com/hB90xvB.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/hB90xvBl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Crescent Moonrise",
|
||||||
|
"author": "Val Vesa",
|
||||||
|
"url": "https://i.imgur.com/nfoPdRd.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/nfoPdRdl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Eye of the Storm",
|
||||||
|
"author": "Breno Machado",
|
||||||
|
"url": "https://i.imgur.com/BliGpTl.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/BliGpTll.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Thundercloud",
|
||||||
|
"author": "Bryan Minear",
|
||||||
|
"url": "https://i.imgur.com/mK8JE0t.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/mK8JE0tl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Departmental Security",
|
||||||
|
"author": "Bartosz Wanot",
|
||||||
|
"url": "https://i.imgur.com/wRN7lDd.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/wRN7lDdl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CMatrix",
|
||||||
|
"author": "Maekus Spiske",
|
||||||
|
"url": "https://i.imgur.com/QAsjQkP.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QAsjQkPl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Neon Serif",
|
||||||
|
"author": "Zuzanna Adamcyzk",
|
||||||
|
"url": "https://i.imgur.com/3edF6DE.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/3edF6DEl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
422
public/assets/phosphor-android/helium/wallpaper.json
Normal file
422
public/assets/phosphor-android/helium/wallpaper.json
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
{
|
||||||
|
"Collections": [
|
||||||
|
{
|
||||||
|
"name": "abstract",
|
||||||
|
"preview_url": "",
|
||||||
|
"preview_thumbnail_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "architecture",
|
||||||
|
"preview_url": "",
|
||||||
|
"preview_thumbnail_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "landscape",
|
||||||
|
"preview_url": "",
|
||||||
|
"preview_thumbnail_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minimal",
|
||||||
|
"preview_url": "",
|
||||||
|
"preview_thumbnail_url": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Wallpapers": [
|
||||||
|
{
|
||||||
|
"name": "Retro Sunrise",
|
||||||
|
"author": "Maxim Medvedev",
|
||||||
|
"url": "https://i.imgur.com/kCtyGwT.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/kCtyGwTl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tawny Peak",
|
||||||
|
"author": "Andreas Kind",
|
||||||
|
"url": "https://i.imgur.com/fqxcpJU.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/fqxcpJUl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "First Light",
|
||||||
|
"author": "Adrien Olichon",
|
||||||
|
"url": "https://i.imgur.com/EakVjSC.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/EakVjSCl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rosy Dunes",
|
||||||
|
"author": "Paxel Nolbert",
|
||||||
|
"url": "https://i.imgur.com/lD06npE.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/lD06npEl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Waking Up",
|
||||||
|
"author": "Dhaval Parmar",
|
||||||
|
"url": "https://i.imgur.com/eBenVs8.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/eBenVs8l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "The Great Outdoors",
|
||||||
|
"author": "Gauravdeep Singh Bansal",
|
||||||
|
"url": "https://i.imgur.com/gJ40ptR.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/gJ40ptRl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Make a Wish",
|
||||||
|
"author": "Diego Ph",
|
||||||
|
"url": "https://i.imgur.com/vJagBNf.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/vJagBNfl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lateral Strike",
|
||||||
|
"author": "Damon Lam",
|
||||||
|
"url": "https://i.imgur.com/BeZHVcY.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/BeZHVcYl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Starfield",
|
||||||
|
"author": "Guilherme Stecanella",
|
||||||
|
"url": "https://i.imgur.com/pDjDeVr.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/pDjDeVrl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Smallness",
|
||||||
|
"author": "Melanie Magdalena",
|
||||||
|
"url": "https://i.imgur.com/658blVF.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/658blVFl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alone at the Summit",
|
||||||
|
"author": "Aleks Dahlberg",
|
||||||
|
"url": "https://i.imgur.com/YKeQ9sc.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/YKeQ9scl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Like Waves",
|
||||||
|
"author": "Calvin Ma",
|
||||||
|
"url": "https://i.imgur.com/wbNiiie.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/wbNiiiel.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cloud Cover",
|
||||||
|
"author": "Adrian",
|
||||||
|
"url": "https://i.imgur.com/02uf66E.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/02uf66El.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tiled",
|
||||||
|
"author": "Fabrizio Conti",
|
||||||
|
"url": "https://i.imgur.com/mAfbAsG.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/mAfbAsGl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Brutalism",
|
||||||
|
"author": "Andrej Lisakov",
|
||||||
|
"url": "https://i.imgur.com/9JlGo4m.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/9JlGo4ml.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Arrayed",
|
||||||
|
"author": "Robert Haverly",
|
||||||
|
"url": "https://i.imgur.com/SGA180g.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/SGA180gl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Placidity",
|
||||||
|
"author": "Julian Bock",
|
||||||
|
"url": "https://i.imgur.com/T7PwRnh.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/T7PwRnhl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sea Change",
|
||||||
|
"author": "Samara Doole",
|
||||||
|
"url": "https://i.imgur.com/ekcUJ2M.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/ekcUJ2Ml.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Choppy",
|
||||||
|
"author": "Imleedh Ali",
|
||||||
|
"url": "https://i.imgur.com/2bk3vxM.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/2bk3vxMl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Below the Crush",
|
||||||
|
"author": "Jeremy Bishop",
|
||||||
|
"url": "https://i.imgur.com/h244SGI.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/h244SGIl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Betta",
|
||||||
|
"author": "Aung Soe Min",
|
||||||
|
"url": "https://i.imgur.com/YOBYine.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/YOBYinel.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Last One in the Pack",
|
||||||
|
"author": "Alfaz Sayed",
|
||||||
|
"url": "https://i.imgur.com/tP0SOFS.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/tP0SOFSl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Basement Window",
|
||||||
|
"author": "Taylor Young",
|
||||||
|
"url": "https://i.imgur.com/V7srimE.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/V7srimEl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cellular",
|
||||||
|
"author": "Christina Kirschnerova",
|
||||||
|
"url": "https://i.imgur.com/lePTybt.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/lePTybtl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ignition",
|
||||||
|
"author": "Roland Larsson",
|
||||||
|
"url": "https://i.imgur.com/v4si6N2.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/v4si6N2l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Falling Light",
|
||||||
|
"author": "Rene Bohmer",
|
||||||
|
"url": "https://i.imgur.com/1U953Cb.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/1U953Cbl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vices",
|
||||||
|
"author": "Jaredd Craig",
|
||||||
|
"url": "https://i.imgur.com/a2xwnHK.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/a2xwnHKl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Your Worst Nightmare",
|
||||||
|
"author": "Elti Meshau",
|
||||||
|
"url": "https://i.imgur.com/9ajAMtL.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/9ajAMtLl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tail Lights",
|
||||||
|
"author": "Shaunak Mirashi",
|
||||||
|
"url": "https://i.imgur.com/tL6d50t.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/tL6d50tl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rapid Transit",
|
||||||
|
"author": "Kevin Clyde Berbano",
|
||||||
|
"url": "https://i.imgur.com/JxXJxDZ.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/JxXJxDZl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Storms Ahead",
|
||||||
|
"author": "Luke Stackpoole",
|
||||||
|
"url": "https://i.imgur.com/ZBMCKbt.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/ZBMCKbtl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Truss",
|
||||||
|
"author": "Phil Botha",
|
||||||
|
"url": "https://i.imgur.com/q4qNcui.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/q4qNcuil.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"author": "Joshua Rivera",
|
||||||
|
"url": "https://i.imgur.com/84O8dFa.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/84O8dFal.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shadows on the Wall",
|
||||||
|
"author": "Rene Bohmer",
|
||||||
|
"url": "https://i.imgur.com/09GgsVW.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/09GgsVWl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fluid Dynamics",
|
||||||
|
"author": "Pawel Czerwinski",
|
||||||
|
"url": "https://i.imgur.com/AaD8SLO.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/AaD8SLOl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ombre",
|
||||||
|
"author": "Elliott Engelmann",
|
||||||
|
"url": "https://i.imgur.com/5jsq9Jg.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/5jsq9Jgl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Black Sand",
|
||||||
|
"author": "Adrien Olichon",
|
||||||
|
"url": "https://i.imgur.com/Yt8zaUn.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Yt8zaUnl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Carbon",
|
||||||
|
"author": "David Jorre",
|
||||||
|
"url": "https://i.imgur.com/hkN3ioi.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/hkN3ioil.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Charred Remains",
|
||||||
|
"author": "Brian Patrick Tagalog",
|
||||||
|
"url": "https://i.imgur.com/SbyCpR2.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/SbyCpR2l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Water Fountain",
|
||||||
|
"author": "Quin Stevenson",
|
||||||
|
"url": "https://i.imgur.com/Bz5u4HA.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Bz5u4HAl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "minimal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Interleaved",
|
||||||
|
"author": "Ron Whitaker",
|
||||||
|
"url": "https://i.imgur.com/QQ7WQvD.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QQ7WQvDl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elliptic",
|
||||||
|
"author": "Tobias van Schneider",
|
||||||
|
"url": "https://i.imgur.com/xBCTxrP.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/xBCTxrPl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Convergence",
|
||||||
|
"author": "Josh Rose",
|
||||||
|
"url": "https://i.imgur.com/uqBhWYH.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/uqBhWYHl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "S-Curve",
|
||||||
|
"author": "Tobias van Schneider",
|
||||||
|
"url": "https://i.imgur.com/Y1A9GTi.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Y1A9GTil.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
207
public/assets/phosphor-android/krypton/wallpaper.json
Normal file
207
public/assets/phosphor-android/krypton/wallpaper.json
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
{
|
||||||
|
"Collections": [
|
||||||
|
{
|
||||||
|
"name": "abstract",
|
||||||
|
"preview_url": "https://i.imgur.com/xBCTxrP.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/xBCTxrPl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "architecture",
|
||||||
|
"preview_url": "https://i.imgur.com/pdV0KQ1.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/pdV0KQ1l.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "landscape",
|
||||||
|
"preview_url": "https://i.imgur.com/EEBaeFE.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/EEBaeFEl.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minimal",
|
||||||
|
"preview_url": "https://i.imgur.com/peX5qVO.png",
|
||||||
|
"preview_thumbnail_url": "https://i.imgur.com/peX5qVOl.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Wallpapers": [
|
||||||
|
{
|
||||||
|
"name": "Echoes Of",
|
||||||
|
"author": "Zoltan Tasi",
|
||||||
|
"url": "https://i.imgur.com/OVKso33.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/OVKso33l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gradient #4B",
|
||||||
|
"author": "Luke Chesser",
|
||||||
|
"url": "https://i.imgur.com/iFrJbfE.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/iFrJbfEl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Orthogonal",
|
||||||
|
"author": "Etienne Beauregard",
|
||||||
|
"url": "https://i.imgur.com/cFyjq7V.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/cFyjq7Vl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Filters Through",
|
||||||
|
"author": "Martin Adams",
|
||||||
|
"url": "https://i.imgur.com/r7MFJxM.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/r7MFJxMl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gehry",
|
||||||
|
"author": "Sascha Yeryomin",
|
||||||
|
"url": "https://i.imgur.com/jf5NX7F.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/jf5NX7Fl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "The Benevolent Corp.",
|
||||||
|
"author": "Alex Iby",
|
||||||
|
"url": "https://i.imgur.com/SrgJtef.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/SrgJtefl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bokehd",
|
||||||
|
"author": "Kristaps Solims",
|
||||||
|
"url": "https://i.imgur.com/HzU9KSK.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/HzU9KSKl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Confluence",
|
||||||
|
"author": "Ash Edmonds",
|
||||||
|
"url": "https://i.imgur.com/HWwYY1t.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/HWwYY1tl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "What Lies Beyond",
|
||||||
|
"author": "Cheng Feng",
|
||||||
|
"url": "https://i.imgur.com/BjdQkxh.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/BjdQkxhl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stairstep",
|
||||||
|
"author": "Dawid Sokolowski",
|
||||||
|
"url": "https://i.imgur.com/VXzW2tV.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/VXzW2tVl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bluesmoke",
|
||||||
|
"author": "Albert Bleeker",
|
||||||
|
"url": "https://i.imgur.com/mrltonO.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/mrltonOl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tensegrity",
|
||||||
|
"author": "Luca Bravo",
|
||||||
|
"url": "https://i.imgur.com/N69Gk81.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/N69Gk81l.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vertical Integration",
|
||||||
|
"author": "Pawl Czerwinski",
|
||||||
|
"url": "https://i.imgur.com/rWVitPA.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/rWVitPAl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"author": "Joshua Rivera",
|
||||||
|
"url": "https://i.imgur.com/84O8dFa.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/84O8dFal.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ripples",
|
||||||
|
"author": "Julian Bock",
|
||||||
|
"url": "https://i.imgur.com/T7PwRnh.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/T7PwRnhl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Light Speed",
|
||||||
|
"author": "Adrien Olichon",
|
||||||
|
"url": "https://i.imgur.com/cprtb1M.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/cprtb1Ml.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dunes at Dusk",
|
||||||
|
"author": "Jeremy Bishop",
|
||||||
|
"url": "https://i.imgur.com/EEBaeFE.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/EEBaeFEl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Black Sand",
|
||||||
|
"author": "Adrien Olichon",
|
||||||
|
"url": "https://i.imgur.com/Yt8zaUn.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/Yt8zaUnl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "landscape"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Interleaved",
|
||||||
|
"author": "Ron Whitaker",
|
||||||
|
"url": "https://i.imgur.com/QQ7WQvD.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/QQ7WQvDl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elliptic",
|
||||||
|
"author": "Tobias van Schneider",
|
||||||
|
"url": "https://i.imgur.com/xBCTxrP.png",
|
||||||
|
"thumbnail": "https://i.imgur.com/xBCTxrPl.jpg",
|
||||||
|
"downloadable": true,
|
||||||
|
"copyright": "CC0",
|
||||||
|
"collections": "abstract,architecture"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -18,6 +18,13 @@
|
|||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"permissions": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*",
|
||||||
|
"clipboardRead",
|
||||||
|
"clipboardWrite",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#35313D",
|
"theme_color": "#35313D",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="58px" height="58px" viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>u-arrow-up-left</title>
|
|
||||||
<g id="u-arrow-up-left" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<polygon id="bounding-box" transform="translate(29.200000, 29.200000) scale(1, -1) rotate(90.000000) translate(-29.200000, -29.200000) " points="0.4 58 58 58 58 0.4 0.4 0.4"></polygon>
|
|
||||||
<path d="M38.2,9.4 C39.1941125,9.4 40,10.2058875 40,11.2 C40,12.1444069 39.2726866,12.9189407 38.347628,12.9940331 L38.2,13 L18.4,13 C12.4353125,13 7.6,17.8353125 7.6,23.8 C7.6,29.6684828 12.2805926,34.4437284 18.112283,34.5962419 L18.4,34.6 L44.654,34.6 L36.9272078,26.8727922 C36.2633165,26.2089009 36.2264336,25.1554365 36.8165592,24.4482593 L36.9272078,24.3272078 C37.5910991,23.6633165 38.6445635,23.6264336 39.3517407,24.2165592 L39.4727922,24.3272078 L50.2727922,35.1272078 C50.3086835,35.1630991 50.3430661,35.200499 50.3758368,35.2393043 L50.2727922,35.1272078 C50.322523,35.1769386 50.3687356,35.2288554 50.4114298,35.2826489 C50.4303122,35.3066321 50.4485184,35.3308286 50.4661181,35.3554879 C50.4786457,35.3727743 50.4909825,35.3907223 50.5029573,35.4088392 C50.5204983,35.4356771 50.5373395,35.4626973 50.5534721,35.4901836 C50.5604155,35.501718 50.5673022,35.5137723 50.5740382,35.5258878 C50.5905898,35.5559164 50.606343,35.5862258 50.6212445,35.6170243 C50.6310894,35.6372379 50.6403272,35.6573209 50.6491756,35.6775332 C50.6615763,35.705848 50.6734867,35.7350368 50.6846414,35.7645963 C50.6896953,35.7781611 50.6944267,35.7911991 50.6990025,35.8042794 C50.7106655,35.837281 50.721476,35.8712903 50.7312871,35.9057207 C50.7358977,35.9224356 50.7403085,35.938831 50.7444838,35.9552738 C50.7807852,36.0974825 50.8,36.2464973 50.8,36.4 C50.8,36.6494086 50.7492745,36.8869696 50.6575775,37.1029292 C50.6430903,37.1371689 50.6273615,37.1712925 50.6105085,37.2049973 C50.5965597,37.232636 50.5820362,37.2598508 50.5668455,37.2866377 C50.566621,37.2873207 50.5664341,37.2876503 50.566247,37.2879798 C50.5435288,37.327754 50.5186404,37.3678619 50.492262,37.4068799 C50.4795991,37.4258795 50.4663526,37.4446817 50.4527038,37.463281 C50.4342916,37.488172 50.4153249,37.512692 50.3957513,37.5366974 C50.3915469,37.5419745 50.3875087,37.5468658 50.3834408,37.5517407 L50.3644975,37.5739923 C50.3400259,37.6024091 50.3146818,37.630053 50.2885067,37.6568825 L50.2727922,37.6727922 L39.4727922,48.4727922 C38.7698485,49.1757359 37.6301515,49.1757359 36.9272078,48.4727922 C36.2633165,47.8089009 36.2264336,46.7554365 36.8165592,46.0482593 L36.9272078,45.9272078 L44.654,38.2 L18.4,38.2 C10.4470875,38.2 4,31.7529125 4,23.8 C4,15.951731 10.2785434,9.5699209 18.0868626,9.4033373 L18.4,9.4 L38.2,9.4 Z" fill="#000000" fill-rule="nonzero" transform="translate(27.400000, 29.200000) scale(1, -1) rotate(90.000000) translate(-27.400000, -29.200000) "></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,3 +1,21 @@
|
|||||||
|
:root {
|
||||||
|
--red: #ff6e60;
|
||||||
|
--orange: #ff8e51;
|
||||||
|
--yellow: #ffd171;
|
||||||
|
--pale: #ffe8dc;
|
||||||
|
--peach: #ffd5c0;
|
||||||
|
--darkgreen: #245633;
|
||||||
|
--blue: #397fff;
|
||||||
|
--purple: #925bff;
|
||||||
|
--eggplant: #35313d;
|
||||||
|
--neutral: #86838b;
|
||||||
|
--translucent: rgba(163, 159, 171, 0.1);
|
||||||
|
--scrim: rgba(255, 255, 255, 0.05);
|
||||||
|
--sheer: rgba(194, 186, 196, 0.25);
|
||||||
|
--soft: rgba(194, 186, 196, 0.7);
|
||||||
|
--shadow: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
font-variant-ligatures: common-ligatures;
|
font-variant-ligatures: common-ligatures;
|
||||||
@@ -15,25 +33,23 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
code {
|
code {
|
||||||
font-family: "IBM Plex Mono", "Courier New", monospace;
|
font-family: "IBM Plex Mono", "Courier New", monospace;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 20px 16px 20px 24px;
|
margin: 0;
|
||||||
margin: 12px 0px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #e1d4d7;
|
font-size: 12x;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +63,7 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.main-button {
|
button.main-button {
|
||||||
@@ -65,9 +82,10 @@ button.main-button {
|
|||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
user-select: none;
|
||||||
margin: 0 24px 24px 0;
|
margin: 0 24px 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +94,6 @@ button.main-button:active {
|
|||||||
box-shadow: 0 0 0 0 black;
|
box-shadow: 0 0 0 0 black;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.main-button:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* button.main-button:not(:last-child) {
|
|
||||||
margin: 0 24px 24px 0;
|
|
||||||
} */
|
|
||||||
|
|
||||||
button.main-button svg {
|
button.main-button svg {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
@@ -108,8 +118,37 @@ a.main-link:after {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.main-link:hover:after {
|
a.main-link:hover:after {
|
||||||
width: 0%;
|
width: 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge.new {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.updated {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid var(--translucent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: var(--foreground);
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
color: var(--foreground-card);
|
||||||
|
background-color: var(--background-card);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
const { getByText } = render(<App />);
|
|
||||||
const linkElement = getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
@@ -1,20 +1,43 @@
|
|||||||
import React, { Suspense } from "react";
|
import { Fragment, Suspense, useMemo } from "react";
|
||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Header from "../Header/Header";
|
import Header from "@/components/Header";
|
||||||
import Toolbar from "../Toolbar/Toolbar";
|
import Toolbar from "@/components/Toolbar";
|
||||||
import IconGrid from "../IconGrid/IconGrid";
|
import IconGrid from "@/components/IconGrid";
|
||||||
import Footer from "../Footer/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import Notice from "../Notice/Notice";
|
import Notice from "@/components/Notice";
|
||||||
|
import {
|
||||||
|
useIconParameters,
|
||||||
|
usePersistSettings,
|
||||||
|
useCSSVariables,
|
||||||
|
} from "@/hooks";
|
||||||
|
import { isDarkThemeSelector } from "@/state";
|
||||||
|
|
||||||
const errorFallback = <Notice message="Search error" />;
|
const errorFallback = <Notice message="Search error" />;
|
||||||
// const waitingFallback = <Notice type="wait" message="Loading..." />;
|
|
||||||
const waitingFallback = <Notice type="none" message="" />;
|
const waitingFallback = <Notice type="none" message="" />;
|
||||||
|
|
||||||
const App: React.FC<any> = () => {
|
const App: React.FC<any> = () => {
|
||||||
|
useIconParameters();
|
||||||
|
usePersistSettings();
|
||||||
|
|
||||||
|
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||||
|
|
||||||
|
const properties = useMemo(
|
||||||
|
() => ({
|
||||||
|
"--foreground": isDark ? "white" : "black",
|
||||||
|
"--foreground-card": isDark ? "white" : "#35313D",
|
||||||
|
"--background": isDark ? "#35313D" : "white",
|
||||||
|
"--background-card": isDark ? "#413c48" : "#f6f5f6",
|
||||||
|
}),
|
||||||
|
[isDark]
|
||||||
|
);
|
||||||
|
|
||||||
|
useCSSVariables(properties);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<Fragment>
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
@@ -25,7 +48,7 @@ const App: React.FC<any> = () => {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</React.StrictMode>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
1
src/components/App/index.ts
Normal file
1
src/components/App/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./App";
|
||||||
55
src/components/Banner/Banner.css
Normal file
55
src/components/Banner/Banner.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.banner {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
color: white;
|
||||||
|
margin: auto;
|
||||||
|
background-color: var(--eggplant);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner .main-button {
|
||||||
|
height: unset;
|
||||||
|
min-height: 64px;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: auto;
|
||||||
|
font-family: "IBM Plex Mono";
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-button {
|
||||||
|
color: inherit;
|
||||||
|
background: var(--eggplant);
|
||||||
|
height: unset !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 48px !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-button:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 32px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
81
src/components/Banner/Banner.tsx
Normal file
81
src/components/Banner/Banner.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { ReactNode, Dispatch, SetStateAction } from "react";
|
||||||
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import { XCircle } from "@phosphor-icons/react";
|
||||||
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
|
import { useLocalStorage } from "@/hooks";
|
||||||
|
|
||||||
|
import "./Banner.css";
|
||||||
|
|
||||||
|
type BannerState = {
|
||||||
|
seen: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BannerProps = {
|
||||||
|
id: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
onClose?: (dispatch: Dispatch<SetStateAction<BannerState>>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
initial: { y: -120 },
|
||||||
|
animate: { y: 0 },
|
||||||
|
exit: { y: -120 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const BANNER_STATE_KEY = "banner_state";
|
||||||
|
|
||||||
|
const Banner = ({ id, children, onClose }: BannerProps) => {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
seen: { [id]: seen },
|
||||||
|
},
|
||||||
|
setBannerState,
|
||||||
|
] = useLocalStorage<BannerState>(BANNER_STATE_KEY, {
|
||||||
|
seen: { [id]: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
ReactGA.event({
|
||||||
|
category: "Banner",
|
||||||
|
action: "Dismiss",
|
||||||
|
label: id,
|
||||||
|
});
|
||||||
|
onClose
|
||||||
|
? onClose(setBannerState)
|
||||||
|
: setBannerState((state) => ({
|
||||||
|
...state,
|
||||||
|
seen: { ...state.seen, [id]: true },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={true}>
|
||||||
|
{!seen && (
|
||||||
|
<motion.aside
|
||||||
|
className="card banner"
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={variants}
|
||||||
|
>
|
||||||
|
<div className="banner-content">
|
||||||
|
{children}
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
className="banner-button"
|
||||||
|
onClick={handleClose}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.key === "Enter" && handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XCircle color="currentColor" size={28} weight="regular" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Banner;
|
||||||
1
src/components/Banner/index.ts
Normal file
1
src/components/Banner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Banner";
|
||||||
@@ -16,10 +16,11 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
-ms-transform: translate(-50%, -50%);
|
-ms-transform: translate(-50%, -50%);
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.color-input {
|
input.color-input {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRecoilState, useRecoilValue } from "recoil";
|
import { useRecoilState, useRecoilValue } from "recoil";
|
||||||
|
|
||||||
import { iconColorAtom } from "../../state/atoms";
|
import { useThrottled } from "@/hooks";
|
||||||
import { isDarkThemeSelector } from "../../state/selectors";
|
import { iconColorAtom, isDarkThemeSelector } from "@/state";
|
||||||
import useThrottled from "../../hooks/useThrottled";
|
|
||||||
import "./ColorInput.css";
|
import "./ColorInput.css";
|
||||||
|
|
||||||
type ColorInputProps = {};
|
type ColorInputProps = {};
|
||||||
|
|
||||||
const ColorInput: React.FC<ColorInputProps> = () => {
|
const ColorInput = (_: ColorInputProps) => {
|
||||||
const [color, setColor] = useRecoilState(iconColorAtom);
|
const [color, setColor] = useRecoilState(iconColorAtom);
|
||||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||||
|
|
||||||
|
|||||||
1
src/components/ColorInput/index.ts
Normal file
1
src/components/ColorInput/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./ColorInput";
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React, { ErrorInfo } from "react";
|
import { Component, ErrorInfo, ReactNode } from "react";
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
fallback?: JSX.Element | React.ReactNode;
|
fallback?: JSX.Element | ReactNode;
|
||||||
|
children?: JSX.Element | ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
errorMessage?: string
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
export default class ErrorBoundary extends Component<
|
||||||
|
ErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { errorMessage: "" }
|
this.state = { errorMessage: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(error: any) {
|
static getDerivedStateFromError(error: any) {
|
||||||
@@ -23,7 +27,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
|
|||||||
console.info(info);
|
console.info(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element | React.ReactNode {
|
render(): JSX.Element | ReactNode {
|
||||||
if (this.state.errorMessage) {
|
if (this.state.errorMessage) {
|
||||||
return this.props.fallback ?? <p>{this.state.errorMessage}</p>;
|
return this.props.fallback ?? <p>{this.state.errorMessage}</p>;
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/components/ErrorBoundary/index.ts
Normal file
1
src/components/ErrorBoundary/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./ErrorBoundary";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
footer {
|
footer {
|
||||||
background-color: #925bff;
|
background-color: var(--purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
#back-to-top-button {
|
#back-to-top-button {
|
||||||
@@ -8,6 +8,16 @@ footer {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
font-size: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#back-to-top-button:active {
|
||||||
|
transform: translate(4px, 4px) !important;
|
||||||
|
box-shadow: 0 0 0 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#back-to-top-button svg {
|
||||||
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -48,9 +58,9 @@ footer .links {
|
|||||||
|
|
||||||
.illustrations-footer {
|
.illustrations-footer {
|
||||||
display: none;
|
display: none;
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +75,7 @@ footer .links {
|
|||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
font-size: 28px;
|
||||||
|
|
||||||
#back-to-top-button img {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .links {
|
footer .links {
|
||||||
@@ -133,17 +139,11 @@ footer .links {
|
|||||||
top: 276px;
|
top: 276px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #command {
|
|
||||||
position: absolute;
|
|
||||||
left: 532px;
|
|
||||||
top: 150px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.illustrations-footer {
|
.illustrations-footer {
|
||||||
display: initial;
|
display: initial;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -240px;
|
left: -240px;
|
||||||
top: 656px;
|
top: 632px;
|
||||||
height: 584px;
|
height: 584px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,51 @@
|
|||||||
import React from "react";
|
import { useRecoilValue } from "recoil";
|
||||||
import { Coffee, Heart } from "phosphor-react";
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import { Coffee, Heart, ArrowULeftUp } from "@phosphor-icons/react";
|
||||||
|
|
||||||
import uArrowUpLeft from "../../assets/u-arrow-up-left.svg";
|
import Links from "@/components/Links/Links";
|
||||||
import markerGreen from "../../assets/marker-green.svg";
|
|
||||||
import postIt from "../../assets/footer-mobile.svg";
|
import { ReactComponent as MarkerGreen } from "@/assets/marker-green.svg";
|
||||||
import Links from "../Links/Links";
|
import { ReactComponent as PostIt } from "@/assets/footer-mobile.svg";
|
||||||
|
import { useMediaQuery } from "@/hooks";
|
||||||
|
import { selectionEntryAtom } from "@/state";
|
||||||
import "./Footer.css";
|
import "./Footer.css";
|
||||||
|
|
||||||
type FooterProps = {};
|
type FooterProps = {};
|
||||||
|
|
||||||
const Footer: React.FC<FooterProps> = () => {
|
const variants: Variants = {
|
||||||
|
initial: { y: 188 },
|
||||||
|
animate: { y: 0 },
|
||||||
|
exit: { y: 188 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const Footer = (_: FooterProps) => {
|
||||||
|
const isMobile = useMediaQuery("(max-width: 719px)");
|
||||||
|
const isViewing = !!useRecoilValue(selectionEntryAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<button
|
<AnimatePresence initial={false}>
|
||||||
|
{(!isMobile || !isViewing) && (
|
||||||
|
<motion.button
|
||||||
id="back-to-top-button"
|
id="back-to-top-button"
|
||||||
aria-label="back-to-top button"
|
aria-label="back-to-top button"
|
||||||
className="main-button"
|
className="main-button"
|
||||||
|
variants={variants}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document
|
document
|
||||||
.getElementById("root")
|
.getElementById("root")
|
||||||
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={uArrowUpLeft} alt="" />
|
<ArrowULeftUp size="1em" />
|
||||||
</button>
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<div className="outro">
|
<div className="outro">
|
||||||
<Links />
|
<Links />
|
||||||
<p>
|
<p>
|
||||||
@@ -34,14 +55,14 @@ const Footer: React.FC<FooterProps> = () => {
|
|||||||
a little quirky, too.
|
a little quirky, too.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
We're thankful for the tools we've benefited from and
|
We're thankful for the tools we've benefited from and this is our
|
||||||
this is our contribution towards a collaborative community.
|
contribution towards a collaborative community.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Phosphor is free and open-source, licensed under{" "}
|
Phosphor is free and open-source, licensed under{" "}
|
||||||
<a
|
<a
|
||||||
className="main-link"
|
className="main-link"
|
||||||
href="https://raw.githubusercontent.com/phosphor-icons/phosphor-web/master/LICENSE"
|
href="https://raw.githubusercontent.com/phosphor-icons/phosphor-home/master/LICENSE"
|
||||||
>
|
>
|
||||||
MIT
|
MIT
|
||||||
</a>
|
</a>
|
||||||
@@ -82,7 +103,7 @@ const Footer: React.FC<FooterProps> = () => {
|
|||||||
Helena Zhang
|
Helena Zhang
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
and built by{" "}
|
and built by{" "}
|
||||||
<a className="main-link" href="https://github.com/rektdeckard">
|
<a className="main-link" href="https://tobiasfried.com">
|
||||||
Toby Fried
|
Toby Fried
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
<span
|
<span
|
||||||
@@ -95,24 +116,17 @@ const Footer: React.FC<FooterProps> = () => {
|
|||||||
<a className="main-link" href="mailto:hello@phosphoricons.com">
|
<a className="main-link" href="mailto:hello@phosphoricons.com">
|
||||||
hello@phosphoricons.com
|
hello@phosphoricons.com
|
||||||
</a>
|
</a>
|
||||||
. Check out our sister project:{" "}
|
|
||||||
<a
|
|
||||||
className="main-link"
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.tobiasfried.phosphor"
|
|
||||||
>
|
|
||||||
Phosphor for Android
|
|
||||||
</a>
|
|
||||||
. Type set in{" "}
|
. Type set in{" "}
|
||||||
<a className="main-link" href="https://manropefont.com/">
|
<a className="main-link" href="https://manropefont.com/">
|
||||||
Manrope
|
Manrope
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
by Mikhail Sharanda.
|
by Mikhail Sharanda.
|
||||||
</p>
|
</p>
|
||||||
<img id="marker-green" src={markerGreen} alt="" />
|
<MarkerGreen id="marker-green" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="illustrations-footer">
|
<div className="illustrations-footer">
|
||||||
<img id="post-it" src={postIt} width="878" height="667" alt="" />
|
<PostIt id="post-it" width="878" height="667" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
1
src/components/Footer/index.ts
Normal file
1
src/components/Footer/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Footer";
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
header {
|
header {
|
||||||
width: 100%;
|
|
||||||
background-color: #ffd171;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-contents {
|
.header-contents {
|
||||||
@@ -60,7 +60,7 @@ header {
|
|||||||
top: -158px;
|
top: -158px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#billiard-ball {
|
.billiard-ball {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 132px;
|
left: 132px;
|
||||||
top: -98px;
|
top: -98px;
|
||||||
@@ -72,13 +72,13 @@ header {
|
|||||||
top: 152px;
|
top: 152px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#warning {
|
.warning {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 394px;
|
left: 394px;
|
||||||
top: -304px;
|
top: -304px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tablet {
|
.tablet {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 672px;
|
left: 672px;
|
||||||
top: -900px;
|
top: -900px;
|
||||||
@@ -94,18 +94,18 @@ header {
|
|||||||
height: 612px;
|
height: 612px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cutting-mat {
|
.cutting-mat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 96px;
|
left: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receipt {
|
.receipt {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -36px;
|
left: -36px;
|
||||||
top: 190px;
|
top: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#calculator {
|
.calculator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 632px;
|
left: 632px;
|
||||||
top: 170px;
|
top: 170px;
|
||||||
@@ -131,7 +131,7 @@ header {
|
|||||||
top: -158px;
|
top: -158px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#billiard-ball {
|
.billiard-ball {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 900px;
|
left: 900px;
|
||||||
top: 400px;
|
top: 400px;
|
||||||
@@ -148,30 +148,30 @@ header {
|
|||||||
top: 694px;
|
top: 694px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#warning {
|
.warning {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1170px;
|
left: 1170px;
|
||||||
top: 400px;
|
top: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tablet {
|
.tablet {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 578px;
|
left: 578px;
|
||||||
top: -900px;
|
top: -900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cutting-mat {
|
.cutting-mat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 120px;
|
left: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receipt {
|
.receipt {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -16px;
|
left: -16px;
|
||||||
top: 190px;
|
top: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#calculator {
|
.calculator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 924px;
|
left: 924px;
|
||||||
top: 114px;
|
top: 114px;
|
||||||
|
|||||||
@@ -1,29 +1,35 @@
|
|||||||
import React from "react";
|
import {
|
||||||
import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react";
|
ArrowCircleUpRight,
|
||||||
|
ArrowCircleDown,
|
||||||
|
Broadcast,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
import markerPurple from "../../assets/marker-purple.svg";
|
import Banner from "@/components/Banner";
|
||||||
import paperclips from "../../assets/paperclips-header-mobile.svg";
|
|
||||||
import paperclipsThree from "../../assets/paperclips-header.svg";
|
import { ReactComponent as MarkerPurple } from "@/assets/marker-purple.svg";
|
||||||
import tablet from "../../assets/tablet.svg";
|
import { ReactComponent as PaperClips } from "@/assets/paperclips-header-mobile.svg";
|
||||||
import tabletSpec from "../../assets/tablet-spec.svg";
|
import { ReactComponent as PaperClipsThree } from "@/assets/paperclips-header.svg";
|
||||||
import billiardBall from "../../assets/billiard-ball.svg";
|
import { ReactComponent as Tablet } from "@/assets/tablet.svg";
|
||||||
import billiardBallSpec from "../../assets/billiard-ball-spec.svg";
|
import { ReactComponent as TabletSpec } from "@/assets/tablet-spec.svg";
|
||||||
import warning from "../../assets/warning.svg";
|
import { ReactComponent as BilliardBall } from "@/assets/billiard-ball.svg";
|
||||||
import warningSpec from "../../assets/warning-spec.svg";
|
import { ReactComponent as BilliardBallSpec } from "@/assets/billiard-ball-spec.svg";
|
||||||
import cuttingMat from "../../assets/cutting-mat.svg";
|
import { ReactComponent as Warning } from "@/assets/warning.svg";
|
||||||
import cuttingMatSpec from "../../assets/cutting-mat-spec.svg";
|
import { ReactComponent as WarningSpec } from "@/assets/warning-spec.svg";
|
||||||
import receipt from "../../assets/receipt.svg";
|
import { ReactComponent as CuttingMat } from "@/assets/cutting-mat.svg";
|
||||||
import receiptSpec from "../../assets/receipt-spec.svg";
|
import { ReactComponent as CuttingMatSpec } from "@/assets/cutting-mat-spec.svg";
|
||||||
import calculator from "../../assets/calculator.svg";
|
import { ReactComponent as Receipt } from "@/assets/receipt.svg";
|
||||||
import calculatorSpec from "../../assets/calculator-spec.svg";
|
import { ReactComponent as ReceiptSpec } from "@/assets/receipt-spec.svg";
|
||||||
import Links from "../Links/Links";
|
import { ReactComponent as Calculator } from "@/assets/calculator.svg";
|
||||||
|
import { ReactComponent as CalculatorSpec } from "@/assets/calculator-spec.svg";
|
||||||
|
|
||||||
|
import Links from "@/components/Links";
|
||||||
import "./Header.css";
|
import "./Header.css";
|
||||||
|
|
||||||
type HeaderProps = {};
|
type HeaderProps = {};
|
||||||
|
|
||||||
const handleGetStarted = () =>
|
const handleGetStarted = () =>
|
||||||
window.open(
|
window.open(
|
||||||
"https://github.com/phosphor-icons/phosphor-web#phosphor-icons",
|
"https://github.com/phosphor-icons/homepage#phosphor-icons",
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener noreferrer"
|
"noopener noreferrer"
|
||||||
);
|
);
|
||||||
@@ -33,30 +39,39 @@ const handleScrollToIcons = () =>
|
|||||||
.getElementById("toolbar")
|
.getElementById("toolbar")
|
||||||
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = () => {
|
const Header = (_: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
|
<Banner id="2.0">
|
||||||
|
<div className="message">
|
||||||
|
<Broadcast size={32} weight="fill" />
|
||||||
|
<small>
|
||||||
|
Phosphor 2.0 is out, with some big updates and some small API
|
||||||
|
changes. Check our{" "}
|
||||||
|
<a href="https://github.com/phosphor-icons/homepage#readme">
|
||||||
|
documentation
|
||||||
|
</a>{" "}
|
||||||
|
to see what's new!
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
<div className="header-contents">
|
<div className="header-contents">
|
||||||
<div className="illustrations-top">
|
<div className="illustrations-top">
|
||||||
<img src={markerPurple} id="marker-purple" alt="" />
|
<MarkerPurple id="marker-purple" />
|
||||||
<img src={paperclips} id="paperclips" alt="" />
|
<PaperClips id="paperclips" />
|
||||||
<img src={paperclipsThree} id="paperclips-three" alt="" />
|
<PaperClipsThree id="paperclips-three" />
|
||||||
<img id="tablet" src={tabletSpec} alt="" />
|
<TabletSpec className="tablet" />
|
||||||
<img id="tablet" className="inspectable xray" src={tablet} alt="" />
|
<Tablet className="tablet inspectable xray" />
|
||||||
<img id="billiard-ball" src={billiardBallSpec} alt="" />
|
<BilliardBallSpec className="billiard-ball" />
|
||||||
<img
|
<BilliardBall className="billiard-ball inspectable xray" />
|
||||||
id="billiard-ball"
|
|
||||||
className="inspectable xray"
|
|
||||||
src={billiardBall}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img id="warning" src={warningSpec} alt="" />
|
<WarningSpec className="warning" />
|
||||||
<img id="warning" className="inspectable xray" src={warning} alt="" />
|
<Warning className="warning inspectable xray" />
|
||||||
</div>
|
</div>
|
||||||
<div className="intro">
|
<div className="intro">
|
||||||
<h2>
|
<h2>
|
||||||
Phosphor is a flexible icon family for interfaces, diagrams, presentations —
|
Phosphor is a flexible icon family for interfaces, diagrams,
|
||||||
|
presentations —
|
||||||
<wbr />
|
<wbr />
|
||||||
whatever, really.
|
whatever, really.
|
||||||
</h2>
|
</h2>
|
||||||
@@ -73,22 +88,12 @@ const Header: React.FC<HeaderProps> = () => {
|
|||||||
<Links />
|
<Links />
|
||||||
</div>
|
</div>
|
||||||
<div className="illustrations-bottom">
|
<div className="illustrations-bottom">
|
||||||
<img id="cutting-mat" src={cuttingMatSpec} alt="" />
|
<CuttingMatSpec className="cutting-mat" />
|
||||||
<img
|
<CuttingMat className="cutting-mat inspectable xray" />
|
||||||
id="cutting-mat"
|
<ReceiptSpec className="receipt" />
|
||||||
className="inspectable xray"
|
<Receipt className="receipt inspectable xray" />
|
||||||
src={cuttingMat}
|
<CalculatorSpec className="calculator" />
|
||||||
alt=""
|
<Calculator className="calculator inspectable xray" />
|
||||||
/>
|
|
||||||
<img id="receipt" src={receiptSpec} alt="" />
|
|
||||||
<img id="receipt" className="inspectable xray" src={receipt} alt="" />
|
|
||||||
<img id="calculator" src={calculatorSpec} alt="" />
|
|
||||||
<img
|
|
||||||
id="calculator"
|
|
||||||
className="inspectable xray"
|
|
||||||
src={calculator}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
1
src/components/Header/index.ts
Normal file
1
src/components/Header/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Header";
|
||||||
304
src/components/IconGrid/DetailFooter.tsx
Normal file
304
src/components/IconGrid/DetailFooter.tsx
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import React, { useRef, useEffect, CSSProperties, useMemo } from "react";
|
||||||
|
import { useRecoilValue, useRecoilState } from "recoil";
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import { Svg2Png } from "svg2png-converter";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
CheckCircle,
|
||||||
|
DownloadSimple,
|
||||||
|
XCircle,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
|
import Tabs, { Tab } from "@/components/Tabs";
|
||||||
|
import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks";
|
||||||
|
import { SnippetType } from "@/lib";
|
||||||
|
import {
|
||||||
|
iconWeightAtom,
|
||||||
|
iconSizeAtom,
|
||||||
|
iconColorAtom,
|
||||||
|
selectionEntryAtom,
|
||||||
|
isDarkThemeSelector,
|
||||||
|
} from "@/state";
|
||||||
|
import { getCodeSnippets, supportsWeight } from "@/utils";
|
||||||
|
|
||||||
|
import TagCloud from "./TagCloud";
|
||||||
|
|
||||||
|
const variants: Record<string, Variants> = {
|
||||||
|
desktop: {
|
||||||
|
initial: { y: 188 },
|
||||||
|
animate: { y: 0 },
|
||||||
|
exit: { y: 188 },
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
initial: { y: "60vh" },
|
||||||
|
animate: { y: 0 },
|
||||||
|
exit: { y: "60vh" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const RENDERED_SNIPPETS = [
|
||||||
|
SnippetType.REACT,
|
||||||
|
SnippetType.VUE,
|
||||||
|
SnippetType.HTML,
|
||||||
|
SnippetType.FLUTTER,
|
||||||
|
SnippetType.ELM,
|
||||||
|
];
|
||||||
|
|
||||||
|
const buttonColor = "#35313D";
|
||||||
|
const successColor = "#1FA647";
|
||||||
|
const disabledColor = "#B7B7B7";
|
||||||
|
|
||||||
|
function cloneWithSize(svg: SVGSVGElement, size: number): SVGSVGElement {
|
||||||
|
const sized = svg.cloneNode(true) as SVGSVGElement;
|
||||||
|
sized.setAttribute("width", `${size}`);
|
||||||
|
sized.setAttribute("height", `${size}`);
|
||||||
|
return sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DetailFooter = () => {
|
||||||
|
const [entry, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||||
|
|
||||||
|
const weight = useRecoilValue(iconWeightAtom);
|
||||||
|
const size = useRecoilValue(iconSizeAtom);
|
||||||
|
const color = useRecoilValue(iconColorAtom);
|
||||||
|
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||||
|
const [copied, setCopied] = useTransientState<SnippetType | "SVG" | false>(
|
||||||
|
false,
|
||||||
|
2000
|
||||||
|
);
|
||||||
|
const ref = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const [{ i }, setInitialTab] = useSessionStorage("tab", { i: 0 });
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 719px)");
|
||||||
|
|
||||||
|
const [snippets, tabs] = useMemo<
|
||||||
|
[Partial<Record<SnippetType, string>>, Tab[]]
|
||||||
|
>(() => {
|
||||||
|
if (!entry) return [{}, []];
|
||||||
|
|
||||||
|
const snippets = getCodeSnippets({
|
||||||
|
displayName: entry?.pascal_name!,
|
||||||
|
name: entry.name,
|
||||||
|
weight,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snippetButtonStyle: CSSProperties =
|
||||||
|
weight === "duotone"
|
||||||
|
? { color: disabledColor, userSelect: "none" }
|
||||||
|
: { color: "currentcolor" };
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
header: "Tags",
|
||||||
|
content: (
|
||||||
|
<TagCloud
|
||||||
|
name={entry.name}
|
||||||
|
tags={Array.from(
|
||||||
|
new Set<string>([
|
||||||
|
...entry.categories,
|
||||||
|
...entry.name.split("-"),
|
||||||
|
...entry.tags,
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
].concat(
|
||||||
|
RENDERED_SNIPPETS.map((type) => {
|
||||||
|
const isWeightSupported = supportsWeight({ type, weight });
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: type,
|
||||||
|
content: (
|
||||||
|
<div className="snippet" key={type}>
|
||||||
|
<pre style={isWeightSupported ? undefined : snippetButtonStyle}>
|
||||||
|
<span>
|
||||||
|
{isWeightSupported
|
||||||
|
? snippets[type]
|
||||||
|
: "This weight is not yet supported"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
title="Copy snippet"
|
||||||
|
onClick={(e) => handleCopySnippet(e, type)}
|
||||||
|
disabled={!isWeightSupported}
|
||||||
|
style={
|
||||||
|
isWeightSupported
|
||||||
|
? { color: "currentColor" }
|
||||||
|
: snippetButtonStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copied === type ? (
|
||||||
|
<CheckCircle size={24} color={successColor} weight="fill" />
|
||||||
|
) : (
|
||||||
|
<Copy
|
||||||
|
size={24}
|
||||||
|
color={
|
||||||
|
isWeightSupported
|
||||||
|
? "currentColor"
|
||||||
|
: snippetButtonStyle.color
|
||||||
|
}
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return [snippets, tabs];
|
||||||
|
}, [entry, weight, size, copied, isDark]);
|
||||||
|
|
||||||
|
useHotkeys("esc", () => setSelectionEntry(null));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!entry) return;
|
||||||
|
ReactGA.event({
|
||||||
|
category: "Grid",
|
||||||
|
action: "Details",
|
||||||
|
label: entry.name,
|
||||||
|
});
|
||||||
|
}, [entry]);
|
||||||
|
|
||||||
|
const buttonBarStyle: CSSProperties = {
|
||||||
|
color: isDark ? "white" : buttonColor,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySnippet = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
|
type: SnippetType
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
setCopied(type);
|
||||||
|
const data = snippets[type];
|
||||||
|
data && void navigator.clipboard?.writeText(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySVG = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
navigator.clipboard?.writeText(cloneWithSize(ref.current, size).outerHTML);
|
||||||
|
setCopied("SVG");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSVG = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
const blob = new Blob([cloneWithSize(ref.current, size).outerHTML]);
|
||||||
|
saveAs(
|
||||||
|
blob,
|
||||||
|
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadPNG = async (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
Svg2Png.save(
|
||||||
|
cloneWithSize(ref.current, size),
|
||||||
|
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={true}>
|
||||||
|
{!!entry && (
|
||||||
|
<motion.aside
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={isMobile ? variants.mobile : variants.desktop}
|
||||||
|
className="secondary detail-footer card"
|
||||||
|
transition={isMobile ? { duration: 0.25 } : { duration: 0.1 }}
|
||||||
|
>
|
||||||
|
<div className="detail-preview">
|
||||||
|
<figure>
|
||||||
|
<entry.Icon ref={ref} size={64}></entry.Icon>
|
||||||
|
<figcaption>
|
||||||
|
<p>{entry.name}</p>
|
||||||
|
<small className="versioning">
|
||||||
|
available in v{entry.published_in.toFixed(1)}.0+
|
||||||
|
</small>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
<div className="detail-actions">
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={buttonBarStyle}
|
||||||
|
onClick={handleDownloadPNG}
|
||||||
|
>
|
||||||
|
<DownloadSimple size={20} color="currentColor" weight="fill" />{" "}
|
||||||
|
PNG
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={buttonBarStyle}
|
||||||
|
onClick={handleDownloadSVG}
|
||||||
|
>
|
||||||
|
<DownloadSimple size={20} color="currentColor" weight="fill" />{" "}
|
||||||
|
SVG
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={buttonBarStyle}
|
||||||
|
onClick={handleCopySVG}
|
||||||
|
>
|
||||||
|
{copied === "SVG" ? (
|
||||||
|
<CheckCircle size={20} color={successColor} weight="fill" />
|
||||||
|
) : (
|
||||||
|
<Copy size={20} color="currentColor" weight="fill" />
|
||||||
|
)}
|
||||||
|
{copied === "SVG" ? "Copied!" : " SVG"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
tabs={tabs}
|
||||||
|
initialIndex={i}
|
||||||
|
onTabChange={(i) => setInitialTab({ i })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
className="close-button"
|
||||||
|
onClick={() => setSelectionEntry(null)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.key === "Enter" && setSelectionEntry(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XCircle color="currentColor" size={28} weight="fill" />
|
||||||
|
</button>
|
||||||
|
</motion.aside>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailFooter;
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { Svg2Png } from "svg2png-converter";
|
|
||||||
import { saveAs } from "file-saver";
|
|
||||||
import { Copy, X, CheckCircle, Download } from "phosphor-react";
|
|
||||||
import ReactGA from "react-ga";
|
|
||||||
|
|
||||||
import {
|
|
||||||
iconWeightAtom,
|
|
||||||
iconSizeAtom,
|
|
||||||
iconColorAtom,
|
|
||||||
iconPreviewOpenAtom,
|
|
||||||
} from "../../state/atoms";
|
|
||||||
import useTransientState from "../../hooks/useTransientState";
|
|
||||||
import TagCloud from "./TagCloud";
|
|
||||||
import { IconEntry } from "../../lib";
|
|
||||||
|
|
||||||
const panelVariants = {
|
|
||||||
open: {
|
|
||||||
opacity: 1,
|
|
||||||
height: "100%",
|
|
||||||
marginTop: 4,
|
|
||||||
marginBottom: 4,
|
|
||||||
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
|
|
||||||
},
|
|
||||||
collapsed: {
|
|
||||||
opacity: 0,
|
|
||||||
height: 0,
|
|
||||||
marginTop: 0,
|
|
||||||
marginBottom: 0,
|
|
||||||
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentVariants = {
|
|
||||||
open: { opacity: 1, transition: { duration: 0.2 } },
|
|
||||||
collapsed: { opacity: 0, transition: { duration: 0.1 } },
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonColor = "#35313D";
|
|
||||||
const successColor = "#1FA647";
|
|
||||||
const disabledColor = "#B7B7B7";
|
|
||||||
|
|
||||||
interface InfoPanelProps {
|
|
||||||
index: number;
|
|
||||||
spans: number;
|
|
||||||
isDark: boolean;
|
|
||||||
entry: IconEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DetailsPanel: React.FC<InfoPanelProps> = (props) => {
|
|
||||||
const { index, spans, isDark, entry } = props;
|
|
||||||
const { name, Icon, categories, tags } = entry;
|
|
||||||
const weight = useRecoilValue(iconWeightAtom);
|
|
||||||
const size = useRecoilValue(iconSizeAtom);
|
|
||||||
const color = useRecoilValue(iconColorAtom);
|
|
||||||
const setOpen = useSetRecoilState(iconPreviewOpenAtom);
|
|
||||||
const [copied, setCopied] = useTransientState<string | false>(false, 2000);
|
|
||||||
const ref = useRef<SVGSVGElement>(null);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => ReactGA.event({ category: "Grid", action: "Details", label: name }),
|
|
||||||
[name]
|
|
||||||
);
|
|
||||||
|
|
||||||
const buttonBarStyle: React.CSSProperties = {
|
|
||||||
color: isDark ? "white" : buttonColor,
|
|
||||||
};
|
|
||||||
const snippetButtonStyle: React.CSSProperties =
|
|
||||||
weight === "duotone"
|
|
||||||
? { color: disabledColor, userSelect: "none" }
|
|
||||||
: { color: buttonColor };
|
|
||||||
|
|
||||||
const snippets = {
|
|
||||||
html:
|
|
||||||
weight === "duotone"
|
|
||||||
? "This weight is not yet supported"
|
|
||||||
: `<i class="ph-${name}${
|
|
||||||
weight === "regular" ? "" : `-${weight}`
|
|
||||||
}"></i>`,
|
|
||||||
react: `<${Icon.displayName} size={${size}} ${
|
|
||||||
color !== "#000000" ? `color="${color}" ` : ""
|
|
||||||
}${weight === "regular" ? "" : `weight="${weight}" `}/>`,
|
|
||||||
vue: `<Ph${Icon.displayName} :size="${size}" ${
|
|
||||||
color !== "#000000" ? `color="${color}" ` : ""
|
|
||||||
}${weight === "regular" ? "" : `weight="${weight}" `}/>`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopySnippet = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
||||||
type: "html" | "react" | "vue"
|
|
||||||
) => {
|
|
||||||
event.currentTarget.blur();
|
|
||||||
setCopied(type);
|
|
||||||
const data = snippets[type];
|
|
||||||
data && void navigator.clipboard?.writeText(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopySVG = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
event.currentTarget.blur();
|
|
||||||
setCopied("svg");
|
|
||||||
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownloadSVG = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
event.currentTarget.blur();
|
|
||||||
if (!ref.current?.outerHTML) return;
|
|
||||||
const blob = new Blob([ref.current.outerHTML]);
|
|
||||||
saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownloadPNG = async (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
event.currentTarget.blur();
|
|
||||||
if (!ref.current?.outerHTML) return;
|
|
||||||
Svg2Png.save(
|
|
||||||
ref.current,
|
|
||||||
`${name}${weight === "regular" ? "" : `-${weight}`}.png`,
|
|
||||||
{ scaleX: 1.334, scaleY: 1.334 }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.section
|
|
||||||
className="info-box"
|
|
||||||
animate="open"
|
|
||||||
exit="collapsed"
|
|
||||||
variants={panelVariants}
|
|
||||||
style={{
|
|
||||||
order: index + (spans - (index % spans)),
|
|
||||||
color: isDark ? "white" : "black",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial="collapsed"
|
|
||||||
animate="open"
|
|
||||||
exit="collapsed"
|
|
||||||
variants={contentVariants}
|
|
||||||
className="icon-preview"
|
|
||||||
>
|
|
||||||
<Icon ref={ref} color={color} weight={weight} size={192} />
|
|
||||||
<p>{name}</p>
|
|
||||||
<TagCloud
|
|
||||||
name={name}
|
|
||||||
tags={Array.from(
|
|
||||||
new Set<string>([...categories, ...name.split("-"), ...tags])
|
|
||||||
)}
|
|
||||||
isDark={isDark}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div
|
|
||||||
initial="collapsed"
|
|
||||||
animate="open"
|
|
||||||
exit="collapsed"
|
|
||||||
variants={contentVariants}
|
|
||||||
className="icon-usage"
|
|
||||||
>
|
|
||||||
<div className="snippet">
|
|
||||||
React
|
|
||||||
<pre tabIndex={0}>
|
|
||||||
<span>{snippets.react}</span>
|
|
||||||
<button
|
|
||||||
title="Copy snippet"
|
|
||||||
onClick={(e) => handleCopySnippet(e, "react")}
|
|
||||||
>
|
|
||||||
{copied === "react" ? (
|
|
||||||
<CheckCircle size={24} color={successColor} weight="fill" />
|
|
||||||
) : (
|
|
||||||
<Copy size={24} color={buttonColor} weight="fill" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<div className="snippet">
|
|
||||||
Vue
|
|
||||||
<pre tabIndex={0}>
|
|
||||||
<span>{snippets.vue}</span>
|
|
||||||
<button
|
|
||||||
title="Copy snippet"
|
|
||||||
onClick={(e) => handleCopySnippet(e, "vue")}
|
|
||||||
>
|
|
||||||
{copied === "vue" ? (
|
|
||||||
<CheckCircle size={24} color={successColor} weight="fill" />
|
|
||||||
) : (
|
|
||||||
<Copy size={24} color={buttonColor} weight="fill" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<div className="snippet">
|
|
||||||
HTML/CSS
|
|
||||||
<pre tabIndex={0} style={snippetButtonStyle}>
|
|
||||||
<span>{snippets.html}</span>
|
|
||||||
<button
|
|
||||||
title="Copy snippet"
|
|
||||||
onClick={(e) => handleCopySnippet(e, "html")}
|
|
||||||
disabled={weight === "duotone"}
|
|
||||||
style={snippetButtonStyle}
|
|
||||||
>
|
|
||||||
{copied === "html" ? (
|
|
||||||
<CheckCircle size={24} color={successColor} weight="fill" />
|
|
||||||
) : (
|
|
||||||
<Copy
|
|
||||||
size={24}
|
|
||||||
color={snippetButtonStyle.color}
|
|
||||||
weight="fill"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<div className="button-row">
|
|
||||||
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
|
|
||||||
<Download size={32} color="currentColor" weight="fill" /> Download
|
|
||||||
PNG
|
|
||||||
</button>
|
|
||||||
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
|
|
||||||
<Download size={32} color="currentColor" weight="fill" /> Download
|
|
||||||
SVG
|
|
||||||
</button>
|
|
||||||
<button style={buttonBarStyle} onClick={handleCopySVG}>
|
|
||||||
{copied === "svg" ? (
|
|
||||||
<CheckCircle size={32} color={successColor} weight="fill" />
|
|
||||||
) : (
|
|
||||||
<Copy size={32} color="currentColor" weight="fill" />
|
|
||||||
)}
|
|
||||||
{copied === "svg" ? "Copied!" : "Copy to design software"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
<motion.span
|
|
||||||
initial="collapsed"
|
|
||||||
animate="open"
|
|
||||||
exit="collapsed"
|
|
||||||
variants={contentVariants}
|
|
||||||
>
|
|
||||||
<X
|
|
||||||
className="close-icon"
|
|
||||||
tabIndex={0}
|
|
||||||
color={buttonBarStyle.color}
|
|
||||||
size={32}
|
|
||||||
weight="fill"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
e.key === "Enter" && setOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.span>
|
|
||||||
</motion.section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DetailsPanel;
|
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
.grid-container {
|
.grid-container {
|
||||||
|
position: relative;
|
||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
min-height: 80vh;
|
/* min-height: 80vh; */
|
||||||
|
z-index: 1;
|
||||||
|
content-visibility: auto;
|
||||||
|
color: var(--foreground);
|
||||||
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
@@ -21,82 +26,63 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* transition: background-color 100ms ease; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item:hover {
|
.grid-item:hover {
|
||||||
background-color: rgba(163, 159, 171, 0.1);
|
background-color: var(--translucent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item:focus {
|
.grid-item:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 2px solid rgba(163, 159, 171, 0.1);
|
border: 2px solid var(--translucent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item p {
|
.grid-item p {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: #86838b;
|
color: var(--neutral);
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
@media screen and (max-width: 536px) {
|
||||||
position: relative;
|
.grid-container {
|
||||||
display: flex;
|
padding: 32px 8px;
|
||||||
width: 100%;
|
|
||||||
height: 0px;
|
|
||||||
margin: 0 4px;
|
|
||||||
border-radius: 16px;
|
|
||||||
background-color: rgba(163, 159, 171, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
|
||||||
.icon-preview {
|
|
||||||
display: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-usage {
|
.grid-item {
|
||||||
padding-left: 10% !important;
|
width: 108px;
|
||||||
|
height: unset;
|
||||||
|
padding: 4px 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item p {
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-preview {
|
.versioning {
|
||||||
width: 30%;
|
margin-top: 2px;
|
||||||
display: flex;
|
opacity: 0.6;
|
||||||
text-align: center;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-preview p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-usage {
|
|
||||||
flex: 1;
|
|
||||||
padding: 56px 10% 56px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snippet {
|
.snippet {
|
||||||
margin-bottom: 24px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snippet pre {
|
.snippet pre {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
color: black;
|
|
||||||
user-select: all;
|
|
||||||
-moz-user-select: all;
|
-moz-user-select: all;
|
||||||
-webkit-user-select: all;
|
-webkit-user-select: all;
|
||||||
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snippet pre:focus {
|
.snippet pre:focus {
|
||||||
@@ -105,9 +91,9 @@
|
|||||||
|
|
||||||
@keyframes select {
|
@keyframes select {
|
||||||
to {
|
to {
|
||||||
user-select: text;
|
|
||||||
-moz-user-select: text;
|
-moz-user-select: text;
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
|
user-select: text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +134,42 @@
|
|||||||
|
|
||||||
.close-icon {
|
.close-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 24px;
|
top: 12px;
|
||||||
right: 24px;
|
right: 12px;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
height: unset !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 48px !important;
|
||||||
|
position: absolute;
|
||||||
|
top: -14px;
|
||||||
|
right: -18px;
|
||||||
|
text-align: end;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button::before {
|
||||||
|
content: "";
|
||||||
|
background: var(--background);
|
||||||
|
position: absolute;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-list {
|
.empty-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -178,3 +194,66 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: -96px;
|
top: -96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aside.detail-footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 16px;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 1120px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 232px 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
height: 136px;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption > p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 719px) {
|
||||||
|
.close-button {
|
||||||
|
top: 4px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.detail-footer {
|
||||||
|
top: 16px;
|
||||||
|
bottom: -4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 440px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button svg {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
import React, { useRef, useEffect } from "react";
|
import { useRef, useEffect } from "react";
|
||||||
import { useRecoilValue } from "recoil";
|
import { useRecoilValue } from "recoil";
|
||||||
import { motion, useAnimation } from "framer-motion";
|
import { motion, useAnimation } from "framer-motion";
|
||||||
import { IconContext } from "phosphor-react";
|
import { IconContext } from "@phosphor-icons/react";
|
||||||
|
|
||||||
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
|
|
||||||
import {
|
import {
|
||||||
|
iconWeightAtom,
|
||||||
|
iconSizeAtom,
|
||||||
|
iconColorAtom,
|
||||||
filteredQueryResultsSelector,
|
filteredQueryResultsSelector,
|
||||||
isDarkThemeSelector,
|
isDarkThemeSelector,
|
||||||
} from "../../state/selectors";
|
} from "@/state";
|
||||||
import useGridSpans from "../../hooks/useGridSpans";
|
import Notice from "@/components/Notice";
|
||||||
|
|
||||||
|
import DetailFooter from "./DetailFooter";
|
||||||
import IconGridItem from "./IconGridItem";
|
import IconGridItem from "./IconGridItem";
|
||||||
import TagCloud from "./TagCloud";
|
import TagCloud from "./TagCloud";
|
||||||
import Notice from "../Notice/Notice";
|
|
||||||
import "./IconGrid.css";
|
import "./IconGrid.css";
|
||||||
|
|
||||||
const defaultSearchTags = ["communication", "editor", "emoji", "maps", "weather"];
|
const defaultSearchTags = [
|
||||||
|
"*new*",
|
||||||
|
"*updated*",
|
||||||
|
"communication",
|
||||||
|
"editor",
|
||||||
|
"emoji",
|
||||||
|
"maps",
|
||||||
|
"weather",
|
||||||
|
];
|
||||||
|
|
||||||
type IconGridProps = {};
|
type IconGridProps = {};
|
||||||
|
|
||||||
const IconGrid: React.FC<IconGridProps> = () => {
|
const IconGrid = (_: IconGridProps) => {
|
||||||
const weight = useRecoilValue(iconWeightAtom);
|
const weight = useRecoilValue(iconWeightAtom);
|
||||||
const size = useRecoilValue(iconSizeAtom);
|
const size = useRecoilValue(iconSizeAtom);
|
||||||
const color = useRecoilValue(iconColorAtom);
|
const color = useRecoilValue(iconColorAtom);
|
||||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||||
const spans = useGridSpans();
|
|
||||||
|
|
||||||
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
|
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
|
||||||
|
|
||||||
const originOffset = useRef({ top: 0, left: 0 });
|
const originOffset = useRef({ top: 0, left: 0 });
|
||||||
@@ -38,34 +47,26 @@ const IconGrid: React.FC<IconGridProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<Notice>
|
<Notice>
|
||||||
<span>Try searching a category or keyword:</span>
|
<span>Try searching a category or keyword:</span>
|
||||||
<TagCloud name="empty-state" isDark={isDark} tags={defaultSearchTags} />
|
<TagCloud name="empty-state" tags={defaultSearchTags} />
|
||||||
</Notice>
|
</Notice>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>
|
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>
|
||||||
<div
|
<div className="grid-container">
|
||||||
className="grid-container"
|
|
||||||
style={{ backgroundColor: isDark ? "#35313D" : "" }}
|
|
||||||
>
|
|
||||||
<i id="beacon" className="beacon" />
|
<i id="beacon" className="beacon" />
|
||||||
<motion.div
|
<motion.div className="grid" initial="hidden" animate={controls}>
|
||||||
className="grid"
|
|
||||||
initial="hidden"
|
|
||||||
animate={controls}
|
|
||||||
variants={{}}
|
|
||||||
>
|
|
||||||
{filteredQueryResults.map((iconEntry, index) => (
|
{filteredQueryResults.map((iconEntry, index) => (
|
||||||
<IconGridItem
|
<IconGridItem
|
||||||
key={index}
|
key={index}
|
||||||
index={index}
|
index={index}
|
||||||
spans={spans}
|
|
||||||
isDark={isDark}
|
isDark={isDark}
|
||||||
entry={iconEntry}
|
entry={iconEntry}
|
||||||
originOffset={originOffset}
|
originOffset={originOffset}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
<DetailFooter />
|
||||||
</div>
|
</div>
|
||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import React, {
|
import {
|
||||||
useRef,
|
useRef,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useEffect,
|
useEffect,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
|
HTMLAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { iconPreviewOpenAtom } from "../../state/atoms";
|
import { IconEntry } from "@/lib";
|
||||||
import DetailsPanel from "./DetailsPanel";
|
import { selectionEntryAtom } from "@/state";
|
||||||
import { IconEntry } from "../../lib";
|
|
||||||
|
|
||||||
interface IconGridItemProps {
|
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
index: number;
|
index: number;
|
||||||
spans: number;
|
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
entry: IconEntry;
|
entry: IconEntry;
|
||||||
originOffset: MutableRefObject<{ top: number; left: number }>;
|
originOffset: MutableRefObject<{ top: number; left: number }>;
|
||||||
@@ -25,22 +24,24 @@ const delayPerPixel = 0.0004;
|
|||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: (delayRef: any) => ({
|
visible: (delayRef: MutableRefObject<number>) => ({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { delay: delayRef.current },
|
transition: { delay: delayRef.current },
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const IconGridItem: React.FC<IconGridItemProps> = (props) => {
|
const IconGridItem = (props: IconGridItemProps) => {
|
||||||
const { index, originOffset, entry } = props;
|
const { index, originOffset, entry, style } = props;
|
||||||
const { name, Icon } = entry;
|
const { name, Icon } = entry;
|
||||||
const [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
|
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||||
const isOpen = open === name;
|
const isOpen = selection?.name === name;
|
||||||
|
const isNew = entry.tags.includes("*new*");
|
||||||
|
const isUpdated = entry.tags.includes("*updated*");
|
||||||
const delayRef = useRef<number>(0);
|
const delayRef = useRef<number>(0);
|
||||||
const offset = useRef({ top: 0, left: 0 });
|
const offset = useRef({ top: 0, left: 0 });
|
||||||
const ref = useRef<any>();
|
const ref = useRef<any>();
|
||||||
|
|
||||||
const handleOpen = () => setOpen(isOpen ? false : name);
|
const handleOpen = () => setSelectionEntry(isOpen ? null : entry);
|
||||||
|
|
||||||
// The measurement for all elements happens in the layoutEffect cycle
|
// The measurement for all elements happens in the layoutEffect cycle
|
||||||
// This ensures that when we calculate distance in the effect cycle
|
// This ensures that when we calculate distance in the effect cycle
|
||||||
@@ -74,8 +75,8 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{
|
style={{
|
||||||
order: index,
|
...style,
|
||||||
backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined,
|
backgroundColor: isOpen ? "var(--translucent)" : undefined,
|
||||||
}}
|
}}
|
||||||
custom={delayRef}
|
custom={delayRef}
|
||||||
transition={transition}
|
transition={transition}
|
||||||
@@ -84,11 +85,12 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
|
|||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
>
|
>
|
||||||
<Icon />
|
<Icon />
|
||||||
<p>{name}</p>
|
<p>
|
||||||
|
{name}
|
||||||
|
{isNew && <span className="badge new">•</span>}
|
||||||
|
{isUpdated && <span className="badge updated">•</span>}
|
||||||
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{isOpen && <DetailsPanel {...props} />}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,31 +2,27 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.tag-button {
|
button.tag-button {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: rgba(194, 186, 196, 0.25);
|
background-color: var(--sheer);
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 200ms ease, box-shadow 200ms ease;
|
transition: background-color 200ms ease, box-shadow 200ms ease;
|
||||||
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.tag-button:hover {
|
button.tag-button:hover {
|
||||||
background-color: rgba(194, 186, 196, 0.7);
|
background-color: var(--soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.tag-button:focus {
|
button.tag-button:focus-visible {
|
||||||
box-shadow: 0 0 0 1px rgba(194, 186, 196, 0.7);
|
box-shadow: 0 0 0 1px var(--soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-button code {
|
.tag-button code {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
import React, { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useSetRecoilState } from "recoil";
|
import { useSetRecoilState } from "recoil";
|
||||||
|
|
||||||
import { searchQueryAtom } from "../../state/atoms";
|
import { useMediaQuery } from "@/hooks";
|
||||||
|
import { searchQueryAtom } from "@/state";
|
||||||
import "./TagCloud.css";
|
import "./TagCloud.css";
|
||||||
|
|
||||||
interface TagCloudProps {
|
interface TagCloudProps {
|
||||||
name: string;
|
name: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
isDark: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagCloud: React.FC<TagCloudProps> = ({ name, tags, isDark }) => {
|
const TagCloud = ({ name, tags }: TagCloudProps) => {
|
||||||
|
const isMobile = useMediaQuery("(max-width: 719px)");
|
||||||
const setQuery = useSetRecoilState(searchQueryAtom);
|
const setQuery = useSetRecoilState(searchQueryAtom);
|
||||||
const handleTagClick = useCallback(
|
const handleTagClick = useCallback(
|
||||||
(tag: string) => {
|
(tag: string) => {
|
||||||
setQuery(tag);
|
setQuery(tag);
|
||||||
document.getElementById("search-input")?.focus();
|
!isMobile && document.getElementById("search-input")?.focus();
|
||||||
},
|
},
|
||||||
[setQuery]
|
[setQuery, isMobile]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +29,9 @@ const TagCloud: React.FC<TagCloudProps> = ({ name, tags, isDark }) => {
|
|||||||
className="tag-button"
|
className="tag-button"
|
||||||
onClick={() => void handleTagClick(tag)}
|
onClick={() => void handleTagClick(tag)}
|
||||||
>
|
>
|
||||||
<code className={`${isDark ? "dark" : ""}`}>{tag}</code>
|
<code>{tag}</code>
|
||||||
|
{tag === "*new*" && <span className="badge new">•</span>}
|
||||||
|
{tag === "*updated*" && <span className="badge updated">•</span>}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
src/components/IconGrid/index.ts
Normal file
1
src/components/IconGrid/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./IconGrid";
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
a.nav-link {
|
a.nav-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import React from "react";
|
import { ArrowElbowDownRight } from "@phosphor-icons/react";
|
||||||
import { OutboundLink } from "react-ga";
|
|
||||||
import { ArrowElbowDownRight } from "phosphor-react";
|
|
||||||
|
|
||||||
import { iconCount } from "../../lib/icons";
|
import { iconCount } from "@/lib/icons";
|
||||||
|
import OutboundLink from "@/components/OutboundLink";
|
||||||
|
|
||||||
import "./Links.css";
|
import "./Links.css";
|
||||||
|
|
||||||
interface LinksProps {}
|
interface LinksProps {}
|
||||||
|
|
||||||
const Links: React.FC<LinksProps> = () => {
|
const Links = (_: LinksProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="links">
|
<div className="links">
|
||||||
<div>
|
<div>
|
||||||
<ArrowElbowDownRight size={24} />
|
<ArrowElbowDownRight size={24} />
|
||||||
<OutboundLink
|
<OutboundLink
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
to="https://phosphoricons.com/assets/phosphor-icons.zip"
|
href="https://phosphoricons.com/assets/phosphor-icons.zip"
|
||||||
eventLabel="Download all"
|
eventLabel="Download all"
|
||||||
download
|
download
|
||||||
type="application/zip"
|
type="application/zip"
|
||||||
@@ -23,67 +22,71 @@ const Links: React.FC<LinksProps> = () => {
|
|||||||
Download all ({iconCount})
|
Download all ({iconCount})
|
||||||
</OutboundLink>
|
</OutboundLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ArrowElbowDownRight size={24} />
|
<ArrowElbowDownRight size={24} />
|
||||||
|
<span>
|
||||||
<OutboundLink
|
<OutboundLink
|
||||||
className="nav-link"
|
href="https://www.figma.com/community/file/903830135544202908/Phosphor-Icons"
|
||||||
to="https://www.figma.com/file/xMCDSp5g0g7Fw8aMyAdVVr/Phosphor-Icon-Library-0.6.0"
|
|
||||||
eventLabel="Figma library"
|
eventLabel="Figma library"
|
||||||
>
|
>
|
||||||
Figma library
|
Figma library
|
||||||
</OutboundLink>
|
</OutboundLink>
|
||||||
</div>
|
{" / "}
|
||||||
{/* <div>
|
|
||||||
<ArrowElbowDownRight size={24} />
|
|
||||||
<OutboundLink
|
<OutboundLink
|
||||||
className="nav-link"
|
href="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
|
||||||
to="https://www.figma.com/community/plugin/892854133443228626/Phosphor-Icons"
|
|
||||||
eventLabel="Figma plugin"
|
eventLabel="Figma plugin"
|
||||||
>
|
>
|
||||||
Add Figma plugin
|
plugin
|
||||||
</OutboundLink>
|
</OutboundLink>
|
||||||
</div> */}
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ArrowElbowDownRight size={24} />
|
<ArrowElbowDownRight size={24} />
|
||||||
<a
|
<OutboundLink
|
||||||
className="nav-link"
|
href="https://phosphoricons.com/assets/phosphor-icons.sketchplugin.zip"
|
||||||
href="https://github.com/phosphor-icons/phosphor-web/issues"
|
eventLabel="Download sketch plugin"
|
||||||
|
download
|
||||||
|
type="application/zip"
|
||||||
>
|
>
|
||||||
Request an icon
|
Sketch plugin
|
||||||
</a>
|
</OutboundLink>
|
||||||
</div>
|
</div>
|
||||||
{/* <div>
|
|
||||||
|
<div>
|
||||||
<ArrowElbowDownRight size={24} />
|
<ArrowElbowDownRight size={24} />
|
||||||
<span>
|
<span>
|
||||||
<a className="nav-link" href="https://paypal.me/minoraxis">
|
<OutboundLink href="https://paypal.me/minoraxis" eventLabel="Donate">
|
||||||
Donate on PayPal
|
Donate on PayPal
|
||||||
</a>
|
</OutboundLink>
|
||||||
{" / "}
|
{" / "}
|
||||||
<a className="nav-link" href="https://patreon.com/phosphoricons">
|
<OutboundLink
|
||||||
Patreon
|
href="https://patreon.com/phosphoricons"
|
||||||
</a>
|
eventLabel="Patreon"
|
||||||
</span>
|
|
||||||
</div> */}
|
|
||||||
<div>
|
|
||||||
<ArrowElbowDownRight size={24} />
|
|
||||||
<a className="nav-link" href="https://paypal.me/minoraxis">
|
|
||||||
Donate on PayPal
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ArrowElbowDownRight size={24} />
|
|
||||||
<a className="nav-link" href="https://patreon.com/phosphoricons">
|
|
||||||
Support us on Patreon
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ArrowElbowDownRight size={24} />
|
|
||||||
<a
|
|
||||||
className="nav-link"
|
|
||||||
href="https://github.com/phosphor-icons/phosphor-web"
|
|
||||||
>
|
>
|
||||||
Github
|
Patreon
|
||||||
</a>
|
</OutboundLink>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ArrowElbowDownRight size={24} />
|
||||||
|
<OutboundLink
|
||||||
|
href="https://github.com/phosphor-icons/homepage"
|
||||||
|
eventLabel="GitHub"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</OutboundLink>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ArrowElbowDownRight size={24} />
|
||||||
|
<OutboundLink
|
||||||
|
href="https://github.com/phosphor-icons/homepage/issues"
|
||||||
|
eventLabel="Request"
|
||||||
|
>
|
||||||
|
Request an icon
|
||||||
|
</OutboundLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
1
src/components/Links/index.ts
Normal file
1
src/components/Links/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Links";
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
import React from "react";
|
import { ReactNode } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useRecoilValue } from "recoil";
|
import { useRecoilValue } from "recoil";
|
||||||
|
import { HourglassMedium, Question, SmileyXEyes } from "@phosphor-icons/react";
|
||||||
|
|
||||||
import { isDarkThemeSelector } from "../../state/selectors";
|
import { searchQueryAtom } from "@/state";
|
||||||
import { searchQueryAtom } from "../../state/atoms";
|
|
||||||
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
|
|
||||||
|
|
||||||
interface NoticeProps {
|
interface NoticeProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
type?: "wait" | "help" | "warn" | "none";
|
type?: "wait" | "help" | "warn" | "none";
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notice: React.FC<NoticeProps> = ({ message, type = "warn", children }) => {
|
const Notice = ({ message, type = "warn", children }: NoticeProps) => {
|
||||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
|
||||||
const query = useRecoilValue(searchQueryAtom);
|
const query = useRecoilValue(searchQueryAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={isDark ? { backgroundColor: "#35313D", color: "white" } : {}}>
|
<div className="primary">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="empty-list"
|
className="empty-list"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|||||||
1
src/components/Notice/index.ts
Normal file
1
src/components/Notice/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Notice";
|
||||||
64
src/components/OutboundLink/OutboundLink.tsx
Normal file
64
src/components/OutboundLink/OutboundLink.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
AnchorHTMLAttributes,
|
||||||
|
useCallback,
|
||||||
|
MouseEventHandler,
|
||||||
|
} from "react";
|
||||||
|
import ReactGA from "react-ga4";
|
||||||
|
import { UaEventOptions } from "react-ga4/types/ga4";
|
||||||
|
|
||||||
|
interface OutboundLinkProps
|
||||||
|
extends DetailedHTMLProps<
|
||||||
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
HTMLAnchorElement
|
||||||
|
> {
|
||||||
|
eventLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEWTAB = "_blank";
|
||||||
|
const MIDDLECLICK = 1;
|
||||||
|
const DEFAULT_META: UaEventOptions = {
|
||||||
|
category: "Outbound",
|
||||||
|
action: "Click",
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutboundLink = ({
|
||||||
|
eventLabel,
|
||||||
|
target,
|
||||||
|
href,
|
||||||
|
...props
|
||||||
|
}: OutboundLinkProps) => {
|
||||||
|
const handleClick: MouseEventHandler<HTMLAnchorElement> = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const eventMeta = { ...DEFAULT_META, label: eventLabel };
|
||||||
|
const sameTarget = target !== NEWTAB;
|
||||||
|
const normalClick = !(
|
||||||
|
event.ctrlKey ||
|
||||||
|
event.shiftKey ||
|
||||||
|
event.metaKey ||
|
||||||
|
event.button === MIDDLECLICK
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!!href && sameTarget && normalClick) {
|
||||||
|
event.preventDefault();
|
||||||
|
ReactGA.event(eventMeta);
|
||||||
|
window.location.href = href;
|
||||||
|
} else {
|
||||||
|
ReactGA.event(eventMeta);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[href, eventLabel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
href={href}
|
||||||
|
onClick={handleClick}
|
||||||
|
rel={target === NEWTAB ? "noopener noreferrer" : ""}
|
||||||
|
className="nav-link"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OutboundLink;
|
||||||
1
src/components/OutboundLink/index.ts
Normal file
1
src/components/OutboundLink/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./OutboundLink";
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--scrim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar:focus-within {
|
.search-bar:focus-within {
|
||||||
@@ -15,11 +15,15 @@
|
|||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-bar:focus-within .keys {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.search-bar input {
|
.search-bar input {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: none;
|
border: none;
|
||||||
margin-left: 12px;
|
margin: 0 12px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -48,6 +52,17 @@
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keys {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
min-width: 42px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 558px) {
|
@media screen and (max-width: 558px) {
|
||||||
#search-icon {
|
#search-icon {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -58,3 +73,9 @@
|
|||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 858px) and (max-width: 1100px) {
|
||||||
|
.search-bar {
|
||||||
|
flex-basis: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,40 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
MutableRefObject,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { useDebounce } from "react-use";
|
import { useDebounce } from "react-use";
|
||||||
import { MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import ReactGA from "react-ga";
|
import { Command, MagnifyingGlass, X, HourglassHigh } from "@phosphor-icons/react";
|
||||||
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
import { searchQueryAtom } from "../../state/atoms";
|
import { searchQueryAtom } from "@/state";
|
||||||
import "./SearchInput.css";
|
import "./SearchInput.css";
|
||||||
|
|
||||||
|
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
|
||||||
|
const isApple = apple.test(window.navigator.platform);
|
||||||
|
|
||||||
|
const mobile = /Android|iPhone|iPod|iPad|Opera Mini|IEMobile/i;
|
||||||
|
const isMobile = mobile.test(window.navigator.userAgent);
|
||||||
|
|
||||||
type SearchInputProps = {};
|
type SearchInputProps = {};
|
||||||
|
|
||||||
const SearchInput: React.FC<SearchInputProps> = () => {
|
const SearchInput = (_: SearchInputProps) => {
|
||||||
const [value, setValue] = useState<string>("");
|
const [value, setValue] = useState<string>("");
|
||||||
const [query, setQuery] = useRecoilState(searchQueryAtom);
|
const [query, setQuery] = useRecoilState(searchQueryAtom);
|
||||||
|
const inputRef =
|
||||||
|
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
|
||||||
|
|
||||||
|
useHotkeys("ctrl+k,cmd+k", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.repeat) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -28,13 +51,15 @@ const SearchInput: React.FC<SearchInputProps> = () => {
|
|||||||
() => {
|
() => {
|
||||||
if (value !== query) {
|
if (value !== query) {
|
||||||
setQuery(value);
|
setQuery(value);
|
||||||
!!value && ReactGA.event({ category: "Search", action: "Query", label: value });
|
!!value &&
|
||||||
|
ReactGA.event({ category: "Search", action: "Query", label: value });
|
||||||
}
|
}
|
||||||
!!value && void document
|
!!value &&
|
||||||
|
void document
|
||||||
.getElementById("beacon")
|
.getElementById("beacon")
|
||||||
?.scrollIntoView({ block: "start", behavior: "smooth" });
|
?.scrollIntoView({ block: "start", behavior: "smooth" });
|
||||||
},
|
},
|
||||||
250,
|
500,
|
||||||
[value]
|
[value]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -49,6 +74,7 @@ const SearchInput: React.FC<SearchInputProps> = () => {
|
|||||||
<div className="search-bar">
|
<div className="search-bar">
|
||||||
<MagnifyingGlass id="search-icon" size={24} />
|
<MagnifyingGlass id="search-icon" size={24} />
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
id="search-input"
|
id="search-input"
|
||||||
title="Search for icon names, categories, or keywords"
|
title="Search for icon names, categories, or keywords"
|
||||||
aria-label="Search for an icon"
|
aria-label="Search for an icon"
|
||||||
@@ -58,10 +84,11 @@ const SearchInput: React.FC<SearchInputProps> = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
onChange={({ currentTarget }) => setValue(currentTarget.value)}
|
onChange={({ currentTarget }) => setValue(currentTarget.value)}
|
||||||
onKeyPress={({ currentTarget, key }) =>
|
onKeyDown={({ currentTarget, key }) =>
|
||||||
key === "Enter" && currentTarget.blur()
|
(key === "Enter" || key === "Escape") && currentTarget.blur()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
|
||||||
{value ? (
|
{value ? (
|
||||||
isReady() ? (
|
isReady() ? (
|
||||||
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
|
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
|
||||||
@@ -73,4 +100,8 @@ const SearchInput: React.FC<SearchInputProps> = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Keys = ({ children }: { children?: ReactNode }) => (
|
||||||
|
<div className="keys">{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default SearchInput;
|
export default SearchInput;
|
||||||
|
|||||||
1
src/components/SearchInput/index.ts
Normal file
1
src/components/SearchInput/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./SearchInput";
|
||||||
17
src/components/SettingsActions/SettingsActions.css
Normal file
17
src/components/SettingsActions/SettingsActions.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
button.action-button {
|
||||||
|
background-color: var(--scrim);
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.action-button:active {
|
||||||
|
background-color: var(--sheer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 558px) {
|
||||||
|
.action-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/components/SettingsActions/SettingsActions.tsx
Normal file
62
src/components/SettingsActions/SettingsActions.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useRecoilValue, useResetRecoilState } from "recoil";
|
||||||
|
import { ArrowCounterClockwise, CheckCircle, Link } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
import { useTransientState } from "@/hooks";
|
||||||
|
import {
|
||||||
|
iconWeightAtom,
|
||||||
|
iconSizeAtom,
|
||||||
|
iconColorAtom,
|
||||||
|
resetSettingsSelector,
|
||||||
|
} from "@/state";
|
||||||
|
|
||||||
|
import "./SettingsActions.css";
|
||||||
|
|
||||||
|
const SettingsActions = () => {
|
||||||
|
const weight = useRecoilValue(iconWeightAtom);
|
||||||
|
const size = useRecoilValue(iconSizeAtom);
|
||||||
|
const color = useRecoilValue(iconColorAtom);
|
||||||
|
const reset = useResetRecoilState(resetSettingsSelector);
|
||||||
|
|
||||||
|
const [copied, setCopied] = useTransientState<boolean>(false, 2000);
|
||||||
|
|
||||||
|
const copyDeepLinkToClipboard = () => {
|
||||||
|
const paramString = new URLSearchParams([
|
||||||
|
["weight", weight.toString()],
|
||||||
|
["size", size.toString()],
|
||||||
|
["color", color.replace("#", "")],
|
||||||
|
]).toString();
|
||||||
|
void navigator.clipboard
|
||||||
|
?.writeText(`${window.location.host}?${paramString}`)
|
||||||
|
.then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert("Clipboard permissions must be enabled to copy links!");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
title="Restore default settings"
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
<ArrowCounterClockwise size={24} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
title="Copy URL for current settings"
|
||||||
|
onClick={copyDeepLinkToClipboard}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckCircle size={24} color="#1FA647" weight="fill" />
|
||||||
|
) : (
|
||||||
|
<Link size={24} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsActions;
|
||||||
1
src/components/SettingsActions/index.ts
Normal file
1
src/components/SettingsActions/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./SettingsActions";
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--scrim);
|
||||||
font-family: "Manrope", sans-serif;
|
font-family: "Manrope", sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -68,12 +68,12 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
width: 24px; /* Set a specific slider handle width */
|
width: 24px; /* Set a specific slider handle width */
|
||||||
height: 24px; /* Slider handle height */
|
height: 24px; /* Slider handle height */
|
||||||
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
|
box-shadow: 0 0 0 6px var(--sheer);
|
||||||
}
|
}
|
||||||
|
|
||||||
.size-bar input:focus::-webkit-slider-thumb {
|
.size-bar input:focus::-webkit-slider-thumb {
|
||||||
outline: none;
|
outline: none;
|
||||||
width: 24px; /* Set a specific slider handle width */
|
width: 24px; /* Set a specific slider handle width */
|
||||||
height: 24px; /* Slider handle height */
|
height: 24px; /* Slider handle height */
|
||||||
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
|
box-shadow: 0 0 0 6px var(--sheer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
|
|
||||||
import { iconSizeAtom } from "../../state/atoms";
|
import { iconSizeAtom } from "@/state";
|
||||||
import "./SizeInput.css";
|
import "./SizeInput.css";
|
||||||
|
|
||||||
type SizeInputProps = {};
|
type SizeInputProps = {};
|
||||||
@@ -14,7 +14,7 @@ const handleBlur = (event: React.UIEvent<HTMLInputElement>) => {
|
|||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
const SizeInput: React.FC<SizeInputProps> = () => {
|
const SizeInput = (_: SizeInputProps) => {
|
||||||
const [size, setSize] = useRecoilState(iconSizeAtom);
|
const [size, setSize] = useRecoilState(iconSizeAtom);
|
||||||
|
|
||||||
const handleSizeChange = useCallback(
|
const handleSizeChange = useCallback(
|
||||||
|
|||||||
1
src/components/SizeInput/index.ts
Normal file
1
src/components/SizeInput/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./SizeInput";
|
||||||
@@ -1,20 +1,3 @@
|
|||||||
/* .style-select {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.style-select {
|
|
||||||
background-color: gold;
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 4px 4px #ccc;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.style-select option {
|
|
||||||
background-color: gold;
|
|
||||||
border-radius: 24px;
|
|
||||||
display: none;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.react-dropdown-select {
|
.react-dropdown-select {
|
||||||
width: 176px !important;
|
width: 176px !important;
|
||||||
height: 48px !important;
|
height: 48px !important;
|
||||||
@@ -22,7 +5,7 @@
|
|||||||
padding: 0 24px !important;
|
padding: 0 24px !important;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--scrim);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
@@ -50,19 +33,6 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .react-dropdown-select-type-single {
|
|
||||||
height: 100% !important;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .react-dropdown-select-clear,
|
|
||||||
.react-dropdown-select-dropdown-handle {
|
|
||||||
color: #fff;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .react-dropdown-select-option {
|
|
||||||
border: 1px solid #000;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.react-dropdown-select-item {
|
.react-dropdown-select-item {
|
||||||
color: #333;
|
color: #333;
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
@@ -89,25 +59,24 @@
|
|||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
/* background: rgb(29, 20, 20) !important; */
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.react-dropdown-select-item {
|
.react-dropdown-select-item {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.react-dropdown-select-item:hover {
|
.react-dropdown-select-item:hover {
|
||||||
background-color: #ffd171 !important;
|
background-color: var(--yellow) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-dropdown-select-item.react-dropdown-select-item-selected,
|
.react-dropdown-select-item.react-dropdown-select-item-selected,
|
||||||
.react-dropdown-select-item.react-dropdown-select-item-active {
|
.react-dropdown-select-item.react-dropdown-select-item-active {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
background-color: #ffd171 !important;
|
background-color: var(--yellow) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-dropdown-select-item:focus {
|
.react-dropdown-select-item:focus {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
background-color: #ffd171 !important;
|
background-color: var(--yellow) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-dropdown-select-item.react-dropdown-select-item-disabled {
|
.react-dropdown-select-item.react-dropdown-select-item-disabled {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
import { useMemo } from "react";
|
||||||
import { useSetRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import Select from "react-dropdown-select";
|
import Select from "react-dropdown-select";
|
||||||
import { PencilLine } from "phosphor-react";
|
import { PencilLine } from "@phosphor-icons/react";
|
||||||
|
import { IconStyle } from "@phosphor-icons/core";
|
||||||
|
|
||||||
|
import { iconWeightAtom } from "@/state";
|
||||||
|
|
||||||
import { iconWeightAtom } from "../../state/atoms";
|
|
||||||
import { IconStyle } from "../../lib";
|
|
||||||
import "./StyleInput.css";
|
import "./StyleInput.css";
|
||||||
|
|
||||||
type WeightOption = { key: string; value: IconStyle; icon: JSX.Element };
|
type WeightOption = { key: string; value: IconStyle; icon: JSX.Element };
|
||||||
@@ -44,8 +45,13 @@ const options: WeightOption[] = [
|
|||||||
|
|
||||||
type StyleInputProps = {};
|
type StyleInputProps = {};
|
||||||
|
|
||||||
const StyleInput: React.FC<StyleInputProps> = () => {
|
const StyleInput = (_: StyleInputProps) => {
|
||||||
const setStyle = useSetRecoilState(iconWeightAtom);
|
const [style, setStyle] = useRecoilState(iconWeightAtom);
|
||||||
|
|
||||||
|
const currentStyle = useMemo(
|
||||||
|
() => [options.find((option) => option.value === style)!!],
|
||||||
|
[style]
|
||||||
|
);
|
||||||
|
|
||||||
const handleStyleChange = (values: WeightOption[]) =>
|
const handleStyleChange = (values: WeightOption[]) =>
|
||||||
setStyle(values[0].value as IconStyle);
|
setStyle(values[0].value as IconStyle);
|
||||||
@@ -53,7 +59,7 @@ const StyleInput: React.FC<StyleInputProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
options={options}
|
options={options}
|
||||||
values={[options[2]]}
|
values={currentStyle}
|
||||||
searchable={false}
|
searchable={false}
|
||||||
labelField="key"
|
labelField="key"
|
||||||
onChange={handleStyleChange}
|
onChange={handleStyleChange}
|
||||||
|
|||||||
1
src/components/StyleInput/index.ts
Normal file
1
src/components/StyleInput/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./StyleInput";
|
||||||
56
src/components/Tabs/Tabs.css
Normal file
56
src/components/Tabs/Tabs.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tab {
|
||||||
|
all: unset;
|
||||||
|
padding: 6px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tab:focus-visible {
|
||||||
|
outline: 1px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tab:hover:not(.active) {
|
||||||
|
background-color: var(--sheer);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tab.active {
|
||||||
|
background-color: var(--background);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 77px;
|
||||||
|
max-height: 77px;
|
||||||
|
padding: 20px 20px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--background);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 719px) {
|
||||||
|
.tabs {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
height: unset;
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/components/Tabs/Tabs.tsx
Normal file
59
src/components/Tabs/Tabs.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { CSSProperties, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import "./Tabs.css";
|
||||||
|
|
||||||
|
export type Tab = {
|
||||||
|
header: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabsProps = {
|
||||||
|
tabs: Tab[];
|
||||||
|
initialIndex?: number;
|
||||||
|
onTabChange?: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentStyles: Record<string, CSSProperties> = {
|
||||||
|
activeLeft: { borderTopLeftRadius: 0 },
|
||||||
|
activeRight: { borderTopRightRadius: 0 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const Tabs = ({ tabs, initialIndex = 0, onTabChange }: TabsProps) => {
|
||||||
|
const [activeIndex, setActiveIndex] = useState<number>(
|
||||||
|
!!tabs[initialIndex] ? initialIndex : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="secondary tabs" tabIndex={0}>
|
||||||
|
<div className="tabs-header">
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
tabIndex={0}
|
||||||
|
className={`tab ${activeIndex === i ? "active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveIndex(i);
|
||||||
|
onTabChange?.(i);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.header}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="tab-content"
|
||||||
|
style={
|
||||||
|
activeIndex === 0
|
||||||
|
? contentStyles.activeLeft
|
||||||
|
: activeIndex === tabs.length - 1
|
||||||
|
? contentStyles.activeRight
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tabs[activeIndex]?.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
2
src/components/Tabs/index.ts
Normal file
2
src/components/Tabs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./Tabs";
|
||||||
|
export type { Tab } from "./Tabs";
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
menu.toolbar {
|
nav.toolbar {
|
||||||
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #35313d;
|
background-color: var(--eggplant);
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 0 0 var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-contents {
|
.toolbar-contents {
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import StyleInput from "@/components/StyleInput";
|
||||||
|
import SearchInput from "@/components/SearchInput";
|
||||||
|
import SizeInput from "@/components/SizeInput";
|
||||||
|
import ColorInput from "@/components/ColorInput";
|
||||||
|
import SettingsActions from "@/components/SettingsActions";
|
||||||
import "./Toolbar.css";
|
import "./Toolbar.css";
|
||||||
import StyleInput from "../StyleInput/StyleInput";
|
|
||||||
import SearchInput from "../SearchInput/SearchInput";
|
|
||||||
import SizeInput from "../SizeInput/SizeInput";
|
|
||||||
import ColorInput from "../ColorInput/ColorInput";
|
|
||||||
|
|
||||||
type ToolbarProps = {};
|
type ToolbarProps = {};
|
||||||
|
|
||||||
const Toolbar: React.FC<ToolbarProps> = () => {
|
const Toolbar: React.FC<ToolbarProps> = () => {
|
||||||
return (
|
return (
|
||||||
<menu className="toolbar" id="toolbar">
|
<nav className="toolbar" id="toolbar">
|
||||||
<div className="toolbar-contents">
|
<div className="toolbar-contents">
|
||||||
<StyleInput />
|
<StyleInput />
|
||||||
<SearchInput />
|
<SearchInput />
|
||||||
<SizeInput />
|
<SizeInput />
|
||||||
<ColorInput />
|
<ColorInput />
|
||||||
|
<SettingsActions />
|
||||||
</div>
|
</div>
|
||||||
</menu>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
1
src/components/Toolbar/index.ts
Normal file
1
src/components/Toolbar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Toolbar";
|
||||||
13
src/hooks/index.ts
Normal file
13
src/hooks/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export { default as useCSSVariables } from "./useCSSVariables";
|
||||||
|
export { default as useDebounce } from "./useDebounce";
|
||||||
|
export { default as useEvent } from "./useEvent";
|
||||||
|
export { default as useIconParameters } from "./useIconParameters";
|
||||||
|
export { default as useLocalStorage } from "./useLocalStorage";
|
||||||
|
export { default as useMediaQuery } from "./useMediaQuery";
|
||||||
|
export { default as usePersistSettings } from "./usePersistSettings";
|
||||||
|
export { default as useSessionStorage } from "./useSessionStorage";
|
||||||
|
export { default as useThrottle } from "./useThrottle";
|
||||||
|
export { default as useThrottled } from "./useThrottled";
|
||||||
|
export { default as useTimeoutFn } from "./useTimeoutFn";
|
||||||
|
export { default as useTransientState } from "./useTransientState";
|
||||||
|
export { default as useUnmount } from "./useUnmount";
|
||||||
40
src/hooks/useCSSVariables.ts
Normal file
40
src/hooks/useCSSVariables.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
type CSSCustomPropertyName = `--${string}`;
|
||||||
|
|
||||||
|
type CSSCustomProperties = {
|
||||||
|
[property: CSSCustomPropertyName]: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function simpleDiff(prev: CSSCustomProperties, next: CSSCustomProperties) {
|
||||||
|
const merge = { ...prev, ...next };
|
||||||
|
return Object.entries(merge).reduce<
|
||||||
|
[property: CSSCustomPropertyName, value: string | null][]
|
||||||
|
>((acc, [k, val]) => {
|
||||||
|
let key = k as CSSCustomPropertyName;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!prev[key as CSSCustomPropertyName] ||
|
||||||
|
prev[key as CSSCustomPropertyName] !== val
|
||||||
|
) {
|
||||||
|
acc.push([key, val]);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useCSSVariables(properties: CSSCustomProperties) {
|
||||||
|
const p = useRef<CSSCustomProperties>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const diff = simpleDiff(p.current, properties);
|
||||||
|
|
||||||
|
if (diff.length > 0) {
|
||||||
|
diff.forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
p.current = properties;
|
||||||
|
}
|
||||||
|
}, [properties]);
|
||||||
|
}
|
||||||
16
src/hooks/useDebounce.ts
Normal file
16
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { DependencyList, useEffect } from "react";
|
||||||
|
import useTimeoutFn from "./useTimeoutFn";
|
||||||
|
|
||||||
|
export type UseDebounceReturn = [() => boolean | null, () => void];
|
||||||
|
|
||||||
|
export default function useDebounce(
|
||||||
|
fn: Function,
|
||||||
|
ms: number = 0,
|
||||||
|
deps: DependencyList = []
|
||||||
|
): UseDebounceReturn {
|
||||||
|
const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
|
||||||
|
|
||||||
|
useEffect(reset, deps);
|
||||||
|
|
||||||
|
return [isReady, cancel];
|
||||||
|
}
|
||||||
45
src/hooks/useEvent.ts
Normal file
45
src/hooks/useEvent.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export type UseEventTarget = HTMLElement | SVGElement | Document | Window;
|
||||||
|
|
||||||
|
export type UseEventMap<E extends UseEventTarget> = E extends HTMLElement
|
||||||
|
? HTMLElementEventMap
|
||||||
|
: E extends SVGElement
|
||||||
|
? SVGElementEventMap
|
||||||
|
: E extends Document
|
||||||
|
? DocumentEventMap
|
||||||
|
: WindowEventMap;
|
||||||
|
|
||||||
|
export type UseEventType<E extends UseEventTarget> = keyof UseEventMap<E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach event listeners to arbitrary targets, and perform necessary cleanup
|
||||||
|
* when unmounting. Provides type inference for the listener based on the
|
||||||
|
* provided event name (currently supports {@link Window}, {@link Document},
|
||||||
|
* and subclasses of {@link HTMLElement} and {@link SVGElement}).
|
||||||
|
*
|
||||||
|
* @param type an {@link https://developer.mozilla.org/en-US/docs/Web/Events#event_listing event type}
|
||||||
|
* @param listener a callback to be fired on the event
|
||||||
|
* @param options {@link AddEventListenerOptions}
|
||||||
|
* @param el the target element to attack the listener. Defaults to
|
||||||
|
* {@link Document} when omitted.
|
||||||
|
*/
|
||||||
|
export default function useEvent<
|
||||||
|
K extends UseEventType<T>,
|
||||||
|
M extends UseEventMap<T>,
|
||||||
|
T extends UseEventTarget = Document
|
||||||
|
>(
|
||||||
|
type: K,
|
||||||
|
listener: (this: T, ev: M[K]) => any,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
el?: T
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const target = el ?? document;
|
||||||
|
// @ts-ignore
|
||||||
|
target.addEventListener(type, listener, options);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return () => target.removeEventListener(type, listener);
|
||||||
|
}, [el, type]);
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { useWindowSize } from "react-use";
|
|
||||||
|
|
||||||
const GRID_PADDING = 32; // .grid-container { padding }
|
|
||||||
const TOOLBAR_WIDTH = 17; // IS THIS BROWSER-SPECIFIC?
|
|
||||||
const MAX_GRID_WIDTH = 1120; // .grid { max-width }
|
|
||||||
const ITEM_WIDTH = 168; // .grid-item { width; height; margin }
|
|
||||||
|
|
||||||
export default (): number => {
|
|
||||||
const { width } = useWindowSize();
|
|
||||||
return Math.floor(
|
|
||||||
Math.min(width - GRID_PADDING - TOOLBAR_WIDTH, MAX_GRID_WIDTH) / ITEM_WIDTH
|
|
||||||
);
|
|
||||||
};
|
|
||||||
64
src/hooks/useIconParameters.ts
Normal file
64
src/hooks/useIconParameters.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSearchParam } from "react-use";
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
|
import TinyColor from "tinycolor2";
|
||||||
|
import { IconStyle } from "@phosphor-icons/core";
|
||||||
|
|
||||||
|
import { iconColorAtom, iconWeightAtom, iconSizeAtom } from "../state/atoms";
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const weight = useSearchParam("weight")?.replace(/["']/g, "");
|
||||||
|
const size = useSearchParam("size")?.replace(/["']/g, "");
|
||||||
|
const color = useSearchParam("color")?.replace(/["']/g, "");
|
||||||
|
|
||||||
|
const setColor = useSetRecoilState(iconColorAtom);
|
||||||
|
const setWeight = useSetRecoilState(iconWeightAtom);
|
||||||
|
const setSize = useSetRecoilState(iconSizeAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (weight) {
|
||||||
|
if (weight.toUpperCase() in IconStyle) setWeight(weight as IconStyle);
|
||||||
|
}
|
||||||
|
}, [weight, setWeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (size) {
|
||||||
|
const normalizedSize = parseInt(size);
|
||||||
|
if (typeof normalizedSize === "number" && isFinite(normalizedSize))
|
||||||
|
setSize(Math.min(Math.max(normalizedSize, 16), 96));
|
||||||
|
}
|
||||||
|
}, [size, setSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (color) {
|
||||||
|
const normalizedColor = TinyColor(color);
|
||||||
|
if (normalizedColor.isValid()) setColor(normalizedColor.toHexString());
|
||||||
|
}
|
||||||
|
}, [color, setColor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!weight && !size && !color) {
|
||||||
|
const persistedState = JSON.parse(
|
||||||
|
window.localStorage.getItem("__phosphor_settings__") || "null"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!!persistedState) {
|
||||||
|
const { weight, size, color } = persistedState;
|
||||||
|
if (weight) {
|
||||||
|
if (weight.toUpperCase() in IconStyle) setWeight(weight as IconStyle);
|
||||||
|
}
|
||||||
|
if (size) {
|
||||||
|
const normalizedSize = parseInt(size);
|
||||||
|
if (typeof normalizedSize === "number" && isFinite(normalizedSize))
|
||||||
|
setSize(Math.min(Math.max(normalizedSize, 16), 96));
|
||||||
|
}
|
||||||
|
if (color) {
|
||||||
|
const normalizedColor = TinyColor(color);
|
||||||
|
if (normalizedColor.isValid())
|
||||||
|
setColor(normalizedColor.toHexString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
40
src/hooks/useLocalStorage.ts
Normal file
40
src/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useState, Dispatch, SetStateAction } from "react";
|
||||||
|
import { STORAGE_KEY } from "@/state";
|
||||||
|
|
||||||
|
type Initializer<S> = () => S;
|
||||||
|
type Setter<S> = (prev: S) => S;
|
||||||
|
type Action<S> = S | Setter<S> | Initializer<S>;
|
||||||
|
|
||||||
|
function expand<S extends object>(action: Action<S>, prev?: S) {
|
||||||
|
if (typeof action === "function") {
|
||||||
|
return (action as Setter<S>)(prev!);
|
||||||
|
} else {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useLocalStorage<S extends object>(
|
||||||
|
key: string,
|
||||||
|
fallbackState: S | (() => S)
|
||||||
|
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
|
||||||
|
const [value, setValue] = useState<S>(() => {
|
||||||
|
let val = localStorage.getItem(STORAGE_KEY + key);
|
||||||
|
if (val) return JSON.parse(val) as S;
|
||||||
|
return expand(fallbackState);
|
||||||
|
});
|
||||||
|
|
||||||
|
const set: Dispatch<SetStateAction<S>> = useCallback((val) => {
|
||||||
|
setValue((prev) => {
|
||||||
|
const next = expand(val, prev);
|
||||||
|
localStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
(partial: Partial<S>) => set((value) => ({ ...value, ...partial })),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, set, insert];
|
||||||
|
}
|
||||||
12
src/hooks/useMediaQuery.ts
Normal file
12
src/hooks/useMediaQuery.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useMemo, useReducer, Reducer } from "react";
|
||||||
|
import useEvent from "./useEvent";
|
||||||
|
|
||||||
|
const updater: Reducer<number, void> = (s) => (s + 1) % 1_000_000;
|
||||||
|
|
||||||
|
export default function useMediaQuery(query: string) {
|
||||||
|
const mq = useMemo(() => window.matchMedia(query), [query]);
|
||||||
|
const [, update] = useReducer(updater, 0);
|
||||||
|
|
||||||
|
useEvent("resize", update, { passive: true });
|
||||||
|
return mq.matches;
|
||||||
|
}
|
||||||
23
src/hooks/usePersistSettings.ts
Normal file
23
src/hooks/usePersistSettings.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
import {
|
||||||
|
iconWeightAtom,
|
||||||
|
iconSizeAtom,
|
||||||
|
iconColorAtom,
|
||||||
|
STORAGE_KEY,
|
||||||
|
} from "@/state";
|
||||||
|
import useDebounce from "./useDebounce";
|
||||||
|
|
||||||
|
export default function usePersistSettings() {
|
||||||
|
const weight = useRecoilValue(iconWeightAtom);
|
||||||
|
const size = useRecoilValue(iconSizeAtom);
|
||||||
|
const color = useRecoilValue(iconColorAtom);
|
||||||
|
|
||||||
|
useDebounce(
|
||||||
|
() => {
|
||||||
|
const serializedState = JSON.stringify({ weight, size, color });
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, serializedState);
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
[weight, size, color]
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/hooks/useSessionStorage.ts
Normal file
40
src/hooks/useSessionStorage.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useState, Dispatch, SetStateAction } from "react";
|
||||||
|
import { STORAGE_KEY } from "@/state";
|
||||||
|
|
||||||
|
type Initializer<S> = () => S;
|
||||||
|
type Setter<S> = (prev: S) => S;
|
||||||
|
type Action<S> = S | Setter<S> | Initializer<S>;
|
||||||
|
|
||||||
|
function expand<S extends object>(action: Action<S>, prev?: S) {
|
||||||
|
if (typeof action === "function") {
|
||||||
|
return (action as Setter<S>)(prev!);
|
||||||
|
} else {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSessionStorage<S extends object>(
|
||||||
|
key: string,
|
||||||
|
fallbackState: S | (() => S)
|
||||||
|
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
|
||||||
|
const [value, setValue] = useState<S>(() => {
|
||||||
|
let val = sessionStorage.getItem(STORAGE_KEY + key);
|
||||||
|
if (val) return JSON.parse(val) as S;
|
||||||
|
return expand(fallbackState);
|
||||||
|
});
|
||||||
|
|
||||||
|
const set: Dispatch<SetStateAction<S>> = useCallback((val) => {
|
||||||
|
setValue((prev) => {
|
||||||
|
const next = expand(val, prev);
|
||||||
|
sessionStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
(partial: Partial<S>) => set((value) => ({ ...value, ...partial })),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, set, insert];
|
||||||
|
}
|
||||||
37
src/hooks/useThrottle.ts
Normal file
37
src/hooks/useThrottle.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import useUnmount from "./useUnmount";
|
||||||
|
|
||||||
|
const useThrottle = <T>(value: T, ms: number = 200) => {
|
||||||
|
const [state, setState] = useState<T>(value);
|
||||||
|
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const nextValue = useRef(null) as any;
|
||||||
|
const hasNextValue = useRef(0) as any;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!timeout.current) {
|
||||||
|
setState(value);
|
||||||
|
const timeoutCallback = () => {
|
||||||
|
if (hasNextValue.current) {
|
||||||
|
hasNextValue.current = false;
|
||||||
|
setState(nextValue.current);
|
||||||
|
timeout.current = setTimeout(timeoutCallback, ms);
|
||||||
|
} else {
|
||||||
|
timeout.current = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timeout.current = setTimeout(timeoutCallback, ms);
|
||||||
|
} else {
|
||||||
|
nextValue.current = value;
|
||||||
|
hasNextValue.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useUnmount(() => {
|
||||||
|
timeout.current && clearTimeout(timeout.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useThrottle;
|
||||||
44
src/hooks/useTimeoutFn.ts
Normal file
44
src/hooks/useTimeoutFn.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
|
||||||
|
|
||||||
|
export default function useTimeoutFn(
|
||||||
|
fn: Function,
|
||||||
|
ms: number = 0
|
||||||
|
): UseTimeoutFnReturn {
|
||||||
|
const ready = useRef<boolean | null>(false);
|
||||||
|
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const callback = useRef(fn);
|
||||||
|
|
||||||
|
const isReady = useCallback(() => ready.current, []);
|
||||||
|
|
||||||
|
const set = useCallback(() => {
|
||||||
|
ready.current = false;
|
||||||
|
timeout.current && clearTimeout(timeout.current);
|
||||||
|
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
ready.current = true;
|
||||||
|
callback.current();
|
||||||
|
}, ms);
|
||||||
|
}, [ms]);
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
ready.current = null;
|
||||||
|
timeout.current && clearTimeout(timeout.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// update ref when function changes
|
||||||
|
useEffect(() => {
|
||||||
|
callback.current = fn;
|
||||||
|
}, [fn]);
|
||||||
|
|
||||||
|
// set on mount, clear on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
set();
|
||||||
|
|
||||||
|
return clear;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [ms]);
|
||||||
|
|
||||||
|
return [isReady, clear, set];
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTimeoutFn } from "react-use";
|
import { useTimeoutFn } from "react-use";
|
||||||
|
|
||||||
export default <T>(baseState: T, ms: number = 1000): [T, (transientValue: T) => void] => {
|
export default <T>(
|
||||||
|
baseState: T,
|
||||||
|
ms: number = 1000
|
||||||
|
): [T, (transientValue: T) => void] => {
|
||||||
const [value, setValue] = useState<T>(baseState);
|
const [value, setValue] = useState<T>(baseState);
|
||||||
const [, cancel, restart] = useTimeoutFn(() => setValue(baseState), ms);
|
const [, cancel, restart] = useTimeoutFn(() => setValue(baseState), ms);
|
||||||
|
|
||||||
|
|||||||
12
src/hooks/useUnmount.ts
Normal file
12
src/hooks/useUnmount.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
const useUnmount = (fn: () => any): void => {
|
||||||
|
const fnRef = useRef(fn);
|
||||||
|
|
||||||
|
// update the ref each render so if it change the newest callback will be invoked
|
||||||
|
fnRef.current = fn;
|
||||||
|
|
||||||
|
useEffect(() => () => fnRef.current(), []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUnmount;
|
||||||
@@ -1,27 +1,23 @@
|
|||||||
import React from "react";
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import { createRoot } from "react-dom/client";
|
||||||
import { RecoilRoot } from "recoil";
|
import { RecoilRoot } from "recoil";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import App from "./components/App";
|
||||||
import App from "./components/App/App";
|
import ReactGA from "react-ga4";
|
||||||
import ReactGA from "react-ga";
|
|
||||||
|
|
||||||
ReactGA.initialize("UA-179205759-1", { titleCase: false });
|
const GA_MEASUREMENT_ID = 'G-1C1REQCLFB'
|
||||||
ReactGA.pageview(window.location.pathname);
|
ReactGA.initialize(GA_MEASUREMENT_ID);
|
||||||
|
|
||||||
ReactDOM.render(
|
const container = document.getElementById("root");
|
||||||
<React.StrictMode>
|
const root = createRoot(container!);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<App />
|
<App />
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
</React.StrictMode>,
|
</StrictMode>
|
||||||
document.getElementById("root")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
|
||||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
|
||||||
serviceWorker.unregister();
|
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
|
|
||||||
%c sphorphosphor %co%cspho
|
%c sphorphosphor %co%cspho
|
||||||
|
|||||||
5567
src/lib/icons.ts
5567
src/lib/icons.ts
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,14 @@
|
|||||||
import { Icon } from "phosphor-react";
|
import { Icon } from "@phosphor-icons/react";
|
||||||
|
import { IconEntry as CoreEntry } from "@phosphor-icons/core";
|
||||||
|
|
||||||
export enum IconStyle {
|
export interface IconEntry extends CoreEntry {
|
||||||
THIN = "thin",
|
|
||||||
LIGHT = "light",
|
|
||||||
REGULAR = "regular",
|
|
||||||
BOLD = "bold",
|
|
||||||
FILL = "fill",
|
|
||||||
DUOTONE = "duotone",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum IconCategory {
|
|
||||||
ARROWS = "arrows",
|
|
||||||
BRAND = "brands",
|
|
||||||
COMMERCE = "commerce",
|
|
||||||
COMMUNICATION = "communications",
|
|
||||||
DESIGN = "design",
|
|
||||||
DEVELOPMENT = "development",
|
|
||||||
OFFICE = "office",
|
|
||||||
EDITOR = "editor",
|
|
||||||
FINANCE = "finances",
|
|
||||||
GAMES = "games",
|
|
||||||
HEALTH = "health & wellness",
|
|
||||||
MAP = "maps & navigation",
|
|
||||||
MEDIA = "media",
|
|
||||||
NATURE = "nature",
|
|
||||||
OBJECTS = "objects",
|
|
||||||
PEOPLE = "people",
|
|
||||||
SYSTEM = "system",
|
|
||||||
WEATHER = "weather",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IconEntry {
|
|
||||||
name: string;
|
|
||||||
categories: IconCategory[];
|
|
||||||
tags: string[];
|
|
||||||
Icon: Icon;
|
Icon: Icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SnippetType {
|
||||||
|
REACT = "React",
|
||||||
|
VUE = "Vue",
|
||||||
|
HTML = "HTML/CSS",
|
||||||
|
FLUTTER = "Flutter",
|
||||||
|
ELM = "Elm",
|
||||||
|
}
|
||||||
|
|||||||
1
src/react-app-env.d.ts
vendored
1
src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="react-scripts" />
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
// This optional code is used to register a service worker.
|
|
||||||
// register() is not called by default.
|
|
||||||
|
|
||||||
// This lets the app load faster on subsequent visits in production, and gives
|
|
||||||
// it offline capabilities. However, it also means that developers (and users)
|
|
||||||
// will only see deployed updates on subsequent visits to a page, after all the
|
|
||||||
// existing tabs open on the page have been closed, since previously cached
|
|
||||||
// resources are updated in the background.
|
|
||||||
|
|
||||||
// To learn more about the benefits of this model and instructions on how to
|
|
||||||
// opt-in, read https://bit.ly/CRA-PWA
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(
|
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
type Config = {
|
|
||||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
|
||||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function register(config?: Config) {
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(
|
|
||||||
process.env.PUBLIC_URL,
|
|
||||||
window.location.href
|
|
||||||
);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if (isLocalhost) {
|
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
|
||||||
checkValidServiceWorker(swUrl, config);
|
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
|
||||||
// service worker/PWA documentation.
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
console.log(
|
|
||||||
'This web app is being served cache-first by a service ' +
|
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Is not localhost. Just register service worker
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl: string, config?: Config) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then(registration => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if (installingWorker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === 'installed') {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the updated precached content has been fetched,
|
|
||||||
// but the previous service worker will still serve the older
|
|
||||||
// content until all client tabs are closed.
|
|
||||||
console.log(
|
|
||||||
'New content is available and will be used when all ' +
|
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onUpdate) {
|
|
||||||
config.onUpdate(registration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// "Content is cached for offline use." message.
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onSuccess) {
|
|
||||||
config.onSuccess(registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
|
||||||
fetch(swUrl, {
|
|
||||||
headers: { 'Service-Worker': 'script' }
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
if (
|
|
||||||
response.status === 404 ||
|
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready
|
|
||||||
.then(registration => {
|
|
||||||
registration.unregister();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
import { atom } from "recoil";
|
import { atom } from "recoil";
|
||||||
import { IconStyle } from "../lib";
|
import { IconStyle } from "@phosphor-icons/core";
|
||||||
|
import { IconEntry } from "@/lib";
|
||||||
|
|
||||||
export const searchQueryAtom = atom<string>({
|
export const searchQueryAtom = atom<string>({
|
||||||
key: "searchQueryAtom",
|
key: "searchQuery",
|
||||||
default: "",
|
default: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconWeightAtom = atom<IconStyle>({
|
export const iconWeightAtom = atom<IconStyle>({
|
||||||
key: "iconWeightAtom",
|
key: "iconWeight",
|
||||||
default: IconStyle.REGULAR,
|
default: IconStyle.REGULAR,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconSizeAtom = atom<number>({
|
export const iconSizeAtom = atom<number>({
|
||||||
key: "iconSizeAtom",
|
key: "iconSize",
|
||||||
default: 48,
|
default: 32,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconColorAtom = atom<string>({
|
export const iconColorAtom = atom<string>({
|
||||||
key: "iconColorAtom",
|
key: "iconColor",
|
||||||
default: "#000000",
|
default: "#000000",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconPreviewOpenAtom = atom<string | false>({
|
export const iconPreviewOpenAtom = atom<string | false>({
|
||||||
key: "iconPreviewOpenAtom",
|
key: "iconPreviewOpen",
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectionEntryAtom = atom<IconEntry | null>({
|
||||||
|
key: "selectionEntry",
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|||||||
4
src/state/index.ts
Normal file
4
src/state/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./atoms";
|
||||||
|
export * from "./selectors";
|
||||||
|
|
||||||
|
export const STORAGE_KEY = "__phosphor_settings__";
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { selector, selectorFamily } from "recoil";
|
import { selector, selectorFamily } from "recoil";
|
||||||
import TinyColor from "tinycolor2";
|
import TinyColor from "tinycolor2";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
import { IconCategory } from "@phosphor-icons/core";
|
||||||
|
|
||||||
import { searchQueryAtom, iconColorAtom } from "./atoms";
|
import {
|
||||||
import { IconEntry, IconCategory } from "../lib";
|
searchQueryAtom,
|
||||||
import { icons } from "../lib/icons";
|
iconWeightAtom,
|
||||||
|
iconSizeAtom,
|
||||||
|
iconColorAtom,
|
||||||
|
} from "./atoms";
|
||||||
|
import { IconEntry } from "@/lib";
|
||||||
|
import { icons } from "@/lib/icons";
|
||||||
|
|
||||||
const fuse = new Fuse(icons, {
|
const fuse = new Fuse(icons, {
|
||||||
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
|
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
|
||||||
@@ -14,7 +20,7 @@ const fuse = new Fuse(icons, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
|
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
|
||||||
key: "filteredQueryResultsSelector",
|
key: "filteredQueryResults",
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
const query = get(searchQueryAtom).trim().toLowerCase();
|
const query = get(searchQueryAtom).trim().toLowerCase();
|
||||||
if (!query) return icons;
|
if (!query) return icons;
|
||||||
@@ -30,7 +36,7 @@ type CategorizedIcons = Partial<Record<IconCategory, IconEntry[]>>;
|
|||||||
export const categorizedQueryResultsSelector = selector<
|
export const categorizedQueryResultsSelector = selector<
|
||||||
Readonly<CategorizedIcons>
|
Readonly<CategorizedIcons>
|
||||||
>({
|
>({
|
||||||
key: "categorizedQueryResultsSelector",
|
key: "categorizedQueryResults",
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
const filteredResults = get(filteredQueryResultsSelector);
|
const filteredResults = get(filteredQueryResultsSelector);
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
@@ -51,8 +57,10 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
|
|||||||
ReadonlyArray<IconEntry>,
|
ReadonlyArray<IconEntry>,
|
||||||
IconCategory
|
IconCategory
|
||||||
>({
|
>({
|
||||||
key: "singleCategoryQueryResultsSelector",
|
key: "singleCategoryQueryResults",
|
||||||
get: (category: IconCategory) => ({ get }) => {
|
get:
|
||||||
|
(category: IconCategory) =>
|
||||||
|
({ get }) => {
|
||||||
const filteredResults = get(filteredQueryResultsSelector);
|
const filteredResults = get(filteredQueryResultsSelector);
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
resolve(
|
resolve(
|
||||||
@@ -63,6 +71,16 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const isDarkThemeSelector = selector<boolean>({
|
export const isDarkThemeSelector = selector<boolean>({
|
||||||
key: "isDarkThemeSelector",
|
key: "isDarkTheme",
|
||||||
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resetSettingsSelector = selector<null>({
|
||||||
|
key: "resetSettings",
|
||||||
|
get: () => null,
|
||||||
|
set: ({ reset }) => {
|
||||||
|
reset(iconWeightAtom);
|
||||||
|
reset(iconSizeAtom);
|
||||||
|
reset(iconColorAtom);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
61
src/utils/index.ts
Normal file
61
src/utils/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { IconStyle } from "@phosphor-icons/core";
|
||||||
|
|
||||||
|
import { SnippetType } from "@/lib";
|
||||||
|
|
||||||
|
export function getCodeSnippets({
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
weight,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
weight: IconStyle;
|
||||||
|
size: number;
|
||||||
|
color: string;
|
||||||
|
}): Record<SnippetType, string> {
|
||||||
|
const isDefaultWeight = weight === "regular";
|
||||||
|
const isDefaultColor = color === "#000000";
|
||||||
|
const elmName = displayName.replace(/^\w/, (c) => c.toLowerCase());
|
||||||
|
const elmWeight = weight.replace(/^\w/, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
return {
|
||||||
|
[SnippetType.HTML]: `<i class="ph${
|
||||||
|
isDefaultWeight ? "" : `-${weight}`
|
||||||
|
} ph-${name}"></i>`,
|
||||||
|
[SnippetType.REACT]: `<${displayName} size={${size}} ${
|
||||||
|
!isDefaultColor ? `color="${color}" ` : ""
|
||||||
|
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
|
||||||
|
[SnippetType.VUE]: `<ph${displayName
|
||||||
|
.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2")
|
||||||
|
.toLowerCase()} :size="${size}" ${
|
||||||
|
!isDefaultColor ? `color="${color}" ` : ""
|
||||||
|
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
|
||||||
|
[SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace(
|
||||||
|
/^\w/,
|
||||||
|
(c) => c.toLowerCase()
|
||||||
|
)}${
|
||||||
|
isDefaultWeight ? "" : weight.replace(/^\w/, (c) => c.toUpperCase())
|
||||||
|
},\n size: ${size.toFixed(1)},\n${
|
||||||
|
!isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : ""
|
||||||
|
})`,
|
||||||
|
[SnippetType.ELM]: `Phosphor.${elmName}${
|
||||||
|
isDefaultWeight ? "" : " " + elmWeight
|
||||||
|
}
|
||||||
|
|> withSize ${size}
|
||||||
|
|> withSizeUnit "px"
|
||||||
|
|> toHtml []`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsWeight({
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
}: {
|
||||||
|
type: SnippetType;
|
||||||
|
weight: IconStyle;
|
||||||
|
}): boolean {
|
||||||
|
if (type !== SnippetType.FLUTTER) return true;
|
||||||
|
return weight !== IconStyle.DUOTONE;
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"baseUrl": "./",
|
||||||
"lib": [
|
"paths": {
|
||||||
"es6",
|
"@/*": ["./src/*"]
|
||||||
"dom",
|
},
|
||||||
"dom.iterable",
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
"esnext"
|
"target": "ESNext",
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "esnext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react",
|
"jsx": "react-jsx",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
@@ -26,11 +25,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"],
|
||||||
"src"
|
"exclude": ["node_modules", "build"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"build"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import svgr from 'vite-plugin-svgr'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), svgr()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"~": path.resolve(__dirname, "./public"),
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user