mirror of
https://github.com/Xevion/grain.git
synced 2025-12-06 01:15:10 -06:00
Compare commits
2 Commits
e632e69b91
...
f5090b9c10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5090b9c10 | ||
|
|
6834aa308f |
39
README.md
39
README.md
@@ -1,29 +1,32 @@
|
||||
![Grain Project - Banner Image][grain-banner]
|
||||
[![Grain Project - Banner Image][grain-banner]][grain-website]
|
||||
|
||||
A small experiment on creating beautiful, dynamic backgrounds with
|
||||
colorful gradients & film grain. Built in <b>React</b> & <b>Vite</b> with <b>SVGs</b> and layers of <b>Radial Gradients</b>
|
||||
[](LICENSE)
|
||||
[](https://nodejs.org/)
|
||||
[](https://pnpm.io/)
|
||||
|
||||
### Dependencies Used
|
||||
Create beautiful, dynamic backgrounds with colorful gradients and film grain. Built with modern web technologies for smooth performance and stunning visuals.
|
||||
|
||||
- Hero Icons
|
||||
- React
|
||||
- Typescript
|
||||
- Vite
|
||||
- Sass
|
||||
- **Customizable gradients & noise** via SVG filters
|
||||
- **Preset palettes** for quick styling
|
||||
- **PNG export** with a single click
|
||||
|
||||
### Installation
|
||||
Built with [Preact](https://preactjs.com/), [Vite](https://vite.dev/), [Tailwind CSS](https://tailwindcss.com/), [Radix UI](https://www.radix-ui.com/), and [Lucide Icons](https://lucide.dev/).
|
||||
|
||||
- Built on Node v16, packages managed with Yarn.
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npm install --global yarn # If you don't have yarn installed
|
||||
yarn # Run inside root directory to install all dependencies.
|
||||
git clone https://github.com/Xevion/grain.git # Clone the repository
|
||||
cd grain # Navigate to the repository
|
||||
npm install --global pnpm # Install pnpm if needed
|
||||
pnpm install # Install dependencies
|
||||
pnpm dev # Start development server
|
||||
pnpm build # Build for production
|
||||
pnpm preview # Preview the production build
|
||||
```
|
||||
|
||||
### Development
|
||||
## License
|
||||
|
||||
```bash
|
||||
yarn dev # Starts a development server with Hot Module Replacement
|
||||
```
|
||||
Licensed under the [GNU General Public License v3.0](LICENSE).
|
||||
|
||||
[grain-banner]: ./.media/banner.jpeg
|
||||
[grain-banner]: ./.media/banner.jpeg
|
||||
[grain-website]: https://grain.xevion.dev/
|
||||
|
||||
10
index.html
10
index.html
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Grain | Dynamic Gradients & Noise</title>
|
||||
<title>Grain | Neural Gradient Explorer</title>
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:title" content="Grain" />
|
||||
<meta property="og:title" content="Grain - Neural Gradient Explorer" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A simple demonstration of a dynamically scaled SVG-based noise & radial gradients."
|
||||
content="Interactive gradient explorer with neural noise textures. Create, customize, and export beautiful gradients with glassmorphic UI."
|
||||
/>
|
||||
<meta property="og:url" content="https://grain.xevion.dev/" />
|
||||
<meta property="og:site_name" content="Grain" />
|
||||
@@ -35,10 +35,10 @@
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://grain.xevion.dev/" />
|
||||
<meta name="twitter:domain" content="https://grain.xevion.dev/" />
|
||||
<meta name="twitter:title" content="Grain" />
|
||||
<meta name="twitter:title" content="Grain - Neural Gradient Explorer" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="A simple demonstration of a dynamically scaled SVG-based noise & stacked radial gradients."
|
||||
content="Interactive gradient explorer with neural noise textures. Create, customize, and export beautiful gradients with glassmorphic UI."
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
10
package.json
10
package.json
@@ -12,9 +12,19 @@
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/roboto-mono": "^5.2.8",
|
||||
"@fontsource/space-grotesk": "^5.2.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"cssnano": "^7.1.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-preact": "^0.468.0",
|
||||
"modern-screenshot": "^4.6.6",
|
||||
"preact": "^10.27.2",
|
||||
"preact-iso": "^2.11.0",
|
||||
"preact-render-to-string": "^6.6.3",
|
||||
|
||||
822
pnpm-lock.yaml
generated
822
pnpm-lock.yaml
generated
@@ -8,15 +8,45 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@fontsource/inter':
|
||||
specifier: ^5.2.8
|
||||
version: 5.2.8
|
||||
'@fontsource/roboto-mono':
|
||||
specifier: ^5.2.8
|
||||
version: 5.2.8
|
||||
'@fontsource/space-grotesk':
|
||||
specifier: ^5.2.10
|
||||
version: 5.2.10
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.1.8
|
||||
version: 1.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-slider':
|
||||
specifier: ^1.3.6
|
||||
version: 1.3.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.17
|
||||
version: 4.1.17(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(yaml@2.8.1))
|
||||
cssnano:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0(postcss@8.5.6)
|
||||
framer-motion:
|
||||
specifier: ^12.23.24
|
||||
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
lucide-preact:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0(preact@10.27.2)
|
||||
modern-screenshot:
|
||||
specifier: ^4.6.6
|
||||
version: 4.6.6
|
||||
preact:
|
||||
specifier: ^10.27.2
|
||||
version: 10.27.2
|
||||
@@ -386,6 +416,30 @@ packages:
|
||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
|
||||
|
||||
'@floating-ui/react-dom@2.1.6':
|
||||
resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@fontsource/inter@5.2.8':
|
||||
resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==}
|
||||
|
||||
'@fontsource/roboto-mono@5.2.8':
|
||||
resolution: {integrity: sha512-M0XPRcvkOKiTPooXwX7yJSNpuad3CPcQwsADBfnopz3apBs+VKI47pc42mlvAxJjNTRauu5rndSxHc8F52bR7Q==}
|
||||
|
||||
'@fontsource/space-grotesk@5.2.10':
|
||||
resolution: {integrity: sha512-XNXEbT74OIITPqw2H6HXwPDp85fy43uxfBwFR5PU+9sLnjuLj12KlhVM9nZVN6q6dlKjkuN8JisW/OBxwxgUew==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -545,6 +599,345 @@ packages:
|
||||
preact: ^10.4.0
|
||||
vite: '>=2.0.0'
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7':
|
||||
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collection@1.1.7':
|
||||
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.2':
|
||||
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dialog@1.1.15':
|
||||
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-direction@1.1.1':
|
||||
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.11':
|
||||
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.3':
|
||||
resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.7':
|
||||
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-id@1.1.1':
|
||||
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popper@1.2.8':
|
||||
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-portal@1.1.9':
|
||||
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-presence@1.1.5':
|
||||
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3':
|
||||
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.4':
|
||||
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-select@2.2.6':
|
||||
resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.8':
|
||||
resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slider@1.3.6':
|
||||
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.3':
|
||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.4':
|
||||
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-tooltip@1.2.8':
|
||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.2.2':
|
||||
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.2':
|
||||
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.1.1':
|
||||
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1':
|
||||
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.1':
|
||||
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1':
|
||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1':
|
||||
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.3':
|
||||
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@rollup/pluginutils@4.2.1':
|
||||
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -842,6 +1235,10 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-query@5.1.3:
|
||||
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
|
||||
|
||||
@@ -1086,6 +1483,9 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-node-es@1.1.0:
|
||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
|
||||
doctrine@2.1.0:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1309,6 +1709,20 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
framer-motion@12.23.24:
|
||||
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1336,6 +1750,10 @@ packages:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-nonce@1.0.1:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1730,6 +2148,15 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
modern-screenshot@4.6.6:
|
||||
resolution: {integrity: sha512-8tF0xEpe7yx37mK95UcIghSCWYeu628K2hLJl+ZNY2ANmRzYLlRLpquPHAQcL8keF6BoeEzTEw4GrgmUpGuZ8w==}
|
||||
|
||||
motion-dom@12.23.23:
|
||||
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
|
||||
|
||||
motion-utils@12.23.6:
|
||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2047,12 +2474,51 @@ packages:
|
||||
random-js@2.1.0:
|
||||
resolution: {integrity: sha512-CRUyWmnzmZBA7RZSVGq0xMqmgCyPPxbiKNLFA5ud7KenojVX2s7Gv+V7eB52beKTPGxWRnVZ7D/tCIgYJJ8vNQ==}
|
||||
|
||||
react-dom@19.2.0:
|
||||
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
|
||||
peerDependencies:
|
||||
react: ^19.2.0
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-remove-scroll-bar@2.3.8:
|
||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-remove-scroll@2.7.1:
|
||||
resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-style-singleton@2.2.3:
|
||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react@19.2.0:
|
||||
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
@@ -2119,6 +2585,9 @@ packages:
|
||||
sax@1.4.1:
|
||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -2279,6 +2748,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2323,6 +2795,26 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-callback-ref@1.3.3:
|
||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sidecar@1.1.3:
|
||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -2746,6 +3238,29 @@ snapshots:
|
||||
'@eslint/core': 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.4
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@fontsource/inter@5.2.8': {}
|
||||
|
||||
'@fontsource/roboto-mono@5.2.8': {}
|
||||
|
||||
'@fontsource/space-grotesk@5.2.10': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@@ -2895,6 +3410,245 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-collection@1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-context@1.1.2(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-dialog@1.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.3(react@19.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-id': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-portal': 1.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-presence': 1.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(react@19.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.0)
|
||||
aria-hidden: 1.2.6
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
react-remove-scroll: 2.7.1(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-direction@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-escape-keydown': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.3(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-id@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-popper@1.2.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-arrow': 1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-rect': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-size': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/rect': 1.1.1
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-portal@1.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-presence@1.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.3(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.4(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-select@2.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.1
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-collection': 1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-direction': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.3(react@19.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-id': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-popper': 1.2.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-portal': 1.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(react@19.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-previous': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
aria-hidden: 1.2.6
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
react-remove-scroll: 2.7.1(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-separator@1.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-slider@1.3.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.1
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-collection': 1.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-direction': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-previous': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-use-size': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-slot@1.2.3(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-slot@1.2.4(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-tooltip@1.2.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(react@19.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-id': 1.1.1(react@19.2.0)
|
||||
'@radix-ui/react-popper': 1.2.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-portal': 1.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-presence': 1.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(react@19.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.0)
|
||||
'@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.2.2(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-effect-event': 0.0.2(react@19.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.2(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.1
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.0)
|
||||
react: 19.2.0
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@rollup/pluginutils@4.2.1':
|
||||
dependencies:
|
||||
estree-walker: 2.0.2
|
||||
@@ -3151,6 +3905,10 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
aria-query@5.1.3:
|
||||
dependencies:
|
||||
deep-equal: 2.2.3
|
||||
@@ -3461,6 +4219,8 @@ snapshots:
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
|
||||
doctrine@2.1.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
@@ -3827,6 +4587,15 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
motion-dom: 12.23.23
|
||||
motion-utils: 12.23.6
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -3860,6 +4629,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
@@ -4211,6 +4982,14 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
modern-screenshot@4.6.6: {}
|
||||
|
||||
motion-dom@12.23.23:
|
||||
dependencies:
|
||||
motion-utils: 12.23.6
|
||||
|
||||
motion-utils@12.23.6: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -4514,10 +5293,38 @@ snapshots:
|
||||
|
||||
random-js@2.1.0: {}
|
||||
|
||||
react-dom@19.2.0(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
react-style-singleton: 2.2.3(react@19.2.0)
|
||||
tslib: 2.8.1
|
||||
|
||||
react-remove-scroll@2.7.1(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
react-remove-scroll-bar: 2.3.8(react@19.2.0)
|
||||
react-style-singleton: 2.2.3(react@19.2.0)
|
||||
tslib: 2.8.1
|
||||
use-callback-ref: 1.3.3(react@19.2.0)
|
||||
use-sidecar: 1.1.3(react@19.2.0)
|
||||
|
||||
react-style-singleton@2.2.3(react@19.2.0):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
react: 19.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
react@19.2.0: {}
|
||||
|
||||
readdirp@4.1.2:
|
||||
optional: true
|
||||
|
||||
@@ -4621,6 +5428,8 @@ snapshots:
|
||||
|
||||
sax@1.4.1: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
@@ -4812,6 +5621,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -4872,6 +5683,17 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-callback-ref@1.3.3(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
use-sidecar@1.1.3(react@19.2.0):
|
||||
dependencies:
|
||||
detect-node-es: 1.1.0
|
||||
react: 19.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite-prerender-plugin@0.5.11(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(yaml@2.8.1)):
|
||||
|
||||
58
src/components/GlassButton.tsx
Normal file
58
src/components/GlassButton.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { type ComponentChildren } from "preact";
|
||||
import { motion } from "framer-motion";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
|
||||
interface GlassButtonProps {
|
||||
children: ComponentChildren;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
export function GlassButton({
|
||||
children,
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
tooltip,
|
||||
"aria-label": ariaLabel
|
||||
}: GlassButtonProps) {
|
||||
const button = (
|
||||
<motion.button
|
||||
className={`glass-button rounded-xl px-4 py-2 font-space-grotesk font-semibold text-zinc-900 outline-none focus:outline-2 outline-offset-2 focus:outline-fuchsia-500 disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
whileHover={!disabled ? { scale: 1.02, y: -2 } : {}}
|
||||
whileTap={!disabled ? { scale: 0.98, y: 0 } : {}}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
{button}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="glass-card px-3 py-2 rounded-lg text-sm text-zinc-900 font-inter"
|
||||
sideOffset={5}
|
||||
>
|
||||
{tooltip}
|
||||
<Tooltip.Arrow className="fill-white/20" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
32
src/components/GlassPanel.tsx
Normal file
32
src/components/GlassPanel.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type ComponentChildren } from "preact";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface GlassPanelProps {
|
||||
children: ComponentChildren;
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export function GlassPanel({ children, className = "", animate = true }: GlassPanelProps) {
|
||||
const Component = animate ? motion.div : "div";
|
||||
|
||||
const animationProps = animate ? {
|
||||
initial: { opacity: 0, x: -20 },
|
||||
animate: { opacity: 1, x: 0 },
|
||||
transition: {
|
||||
type: "spring" as const,
|
||||
stiffness: 200,
|
||||
damping: 25,
|
||||
duration: 0.4
|
||||
}
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={`glass-panel rounded-2xl ${className}`}
|
||||
{...animationProps}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
98
src/components/GradientControls.tsx
Normal file
98
src/components/GradientControls.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { RefreshCw, Download } from "lucide-preact";
|
||||
import { GlassButton } from "./GlassButton";
|
||||
import { SliderControl } from "./SliderControl";
|
||||
import { PaletteSelector } from "./PaletteSelector";
|
||||
import * as Separator from "@radix-ui/react-separator";
|
||||
|
||||
interface GradientControlsProps {
|
||||
paletteId: string;
|
||||
onPaletteChange: (paletteId: string) => void;
|
||||
gradientCount: number;
|
||||
onGradientCountChange: (count: number) => void;
|
||||
noiseIntensity: number;
|
||||
onNoiseIntensityChange: (intensity: number) => void;
|
||||
onRegenerate: () => void;
|
||||
onExport: () => void;
|
||||
}
|
||||
|
||||
export function GradientControls({
|
||||
paletteId,
|
||||
onPaletteChange,
|
||||
gradientCount,
|
||||
onGradientCountChange,
|
||||
noiseIntensity,
|
||||
onNoiseIntensityChange,
|
||||
onRegenerate,
|
||||
onExport,
|
||||
}: GradientControlsProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="pl-4 pt-3 pb-2">
|
||||
<h1 className="font-space-grotesk text-4xl font-bold tracking-tight text-zinc-50 text-shadow-md">
|
||||
Grain
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Separator.Root className="h-px bg-white/20" />
|
||||
|
||||
{/* Controls */}
|
||||
{/* Palette Selector */}
|
||||
<PaletteSelector
|
||||
className="px-6 py-4"
|
||||
selectedPaletteId={paletteId}
|
||||
onSelect={onPaletteChange}
|
||||
/>
|
||||
|
||||
<Separator.Root className="h-px bg-white/20" />
|
||||
|
||||
{/* Gradient Count Slider */}
|
||||
<SliderControl
|
||||
label="Gradients"
|
||||
value={gradientCount}
|
||||
onChange={onGradientCountChange}
|
||||
min={2}
|
||||
max={8}
|
||||
step={1}
|
||||
className="px-6 py-4"
|
||||
/>
|
||||
|
||||
{/* Noise Intensity Slider */}
|
||||
<SliderControl
|
||||
label="Noise"
|
||||
value={noiseIntensity}
|
||||
onChange={onNoiseIntensityChange}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.1}
|
||||
formatValue={(v) => `${Math.round(v * 100)}%`}
|
||||
className="px-6 py-4"
|
||||
/>
|
||||
|
||||
<Separator.Root className="h-px bg-white/20" />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-2 mx-6 my-4">
|
||||
<GlassButton
|
||||
onClick={onRegenerate}
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
tooltip="Regenerate gradient (R)"
|
||||
aria-label="Regenerate gradient"
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
<span>Regenerate</span>
|
||||
</GlassButton>
|
||||
|
||||
<GlassButton
|
||||
onClick={onExport}
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
tooltip="Export gradient (E)"
|
||||
aria-label="Export gradient"
|
||||
>
|
||||
<Download size={18} />
|
||||
<span>Export</span>
|
||||
</GlassButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
src/components/PaletteSelector.tsx
Normal file
63
src/components/PaletteSelector.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { COLOR_PALETTES, type ColorPalette } from "../utils/palettes";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface PaletteSelectorProps {
|
||||
selectedPaletteId: string;
|
||||
onSelect: (paletteId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PaletteSelector({
|
||||
selectedPaletteId,
|
||||
onSelect,
|
||||
className,
|
||||
}: PaletteSelectorProps) {
|
||||
return (
|
||||
<div className={`space-y-2 ${className}`}>
|
||||
<label className="font-space-grotesk text-sm font-semibold uppercase tracking-wide text-zinc-50 text-shadow-md">
|
||||
Color Palette
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{COLOR_PALETTES.map((palette) => (
|
||||
<PaletteSwatch
|
||||
key={palette.id}
|
||||
palette={palette}
|
||||
isSelected={palette.id === selectedPaletteId}
|
||||
onClick={() => onSelect(palette.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PaletteSwatchProps {
|
||||
palette: ColorPalette;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function PaletteSwatch({ palette, isSelected, onClick }: PaletteSwatchProps) {
|
||||
return (
|
||||
<motion.button
|
||||
className="glass-input rounded-lg p-2 flex flex-col gap-1.5 cursor-pointer outline-none transition-all"
|
||||
onClick={onClick}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<div className="flex gap-0.5 h-3">
|
||||
{palette.colors.slice(0, 6).map((color, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-1 rounded-sm"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs font-inter text-zinc-900 text-center">
|
||||
{palette.name}
|
||||
</span>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Sparkles, ShieldAlert } from "lucide-preact";
|
||||
|
||||
const Post = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="m-5 md:m-8 md:mt-5 w-[90%]">
|
||||
<div className="mb-5">
|
||||
<h2 className="text-3xl md:text-4xl pb-1 tracking-wide font-semibold drop-shadow-xl">
|
||||
Grain
|
||||
</h2>
|
||||
<span className="pl-1 py-1 text-zinc-500">
|
||||
Created by{" "}
|
||||
<a
|
||||
href="https://xevion.dev"
|
||||
target="_blank"
|
||||
className="transition-colors text-sky-800 hover:text-sky-600"
|
||||
>
|
||||
Ryan Walters
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/Xevion"
|
||||
target="_blank"
|
||||
className="hover:text-yellow-600 transition-colors cursor-pointer"
|
||||
>
|
||||
<Sparkles className="h-4 inline mb-2.5 m-2 " />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="semibold-children">
|
||||
A small experiment on creating beautiful, dynamic backgrounds with
|
||||
colorful gradients & film grain. Built in <b>React</b> & <b>Vite</b>{" "}
|
||||
with <b>SVGs</b> and layers of <b>Radial Gradients</b>.
|
||||
</p>
|
||||
<p>
|
||||
This app was inspired by the gradients used{" "}
|
||||
<a
|
||||
href="https://www.instagram.com/p/ClUe3ONJaER/"
|
||||
target="_blank"
|
||||
className="text-sky-800 hover:text-sky-600"
|
||||
>
|
||||
certain popular instagram post
|
||||
</a>{" "}
|
||||
with beautiful gradients and a slight film grain applied. I wanted
|
||||
to create something similar, but in a website form.
|
||||
</p>
|
||||
<p>
|
||||
By using a SVG with a{" "}
|
||||
<code><feTurbulence></code> filter inside,
|
||||
stacked upon several <code>radial-gradient</code>{" "}
|
||||
background images, the same effect can be created. Since SVGs do not
|
||||
naturally repeat internally, the SVG itself must be generated in
|
||||
such a way that the noise always displays the same way.
|
||||
</p>
|
||||
<p>
|
||||
React comes in handy here, allowing composition of an SVG, and then
|
||||
conversion to a <code>base64</code> encoded string.
|
||||
As a <code>base64</code> image, it can be fed into
|
||||
the <code>background</code> CSS property, allowing
|
||||
dynamic SVG generation.
|
||||
</p>
|
||||
<div className="pt-3">
|
||||
<a href="https://github.com/Xevion/grain">
|
||||
<div className="inline text-white text-medium drop-shadow-lg rounded border-2 shadow-xl border-zinc-600/75 m-2 p-2 bg-linear-to-r from-red-500 via-orange-500 to-orange-700">
|
||||
In Progress
|
||||
<ShieldAlert className="inline h-[1.4rem] ml-3 drop-shadow-2xl" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Post;
|
||||
52
src/components/SliderControl.tsx
Normal file
52
src/components/SliderControl.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as Slider from "@radix-ui/react-slider";
|
||||
|
||||
interface SliderControlProps {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
formatValue?: (value: number) => string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SliderControl({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
formatValue = (v) => v.toString(),
|
||||
className,
|
||||
}: SliderControlProps) {
|
||||
return (
|
||||
<div className={`space-y ${className}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="font-space-grotesk text-sm font-semibold uppercase tracking-wide text-zinc-50 text-shadow-md">
|
||||
{label}
|
||||
</label>
|
||||
<span className="font-inter text-sm text-zinc-100 text-shadow-md">
|
||||
{formatValue(value)}
|
||||
</span>
|
||||
</div>
|
||||
<Slider.Root
|
||||
className="relative flex items-center select-none touch-none w-full h-6"
|
||||
value={[value]}
|
||||
onValueChange={([newValue]) => onChange(newValue)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
>
|
||||
<Slider.Track className="glass-input relative grow rounded-full h-3">
|
||||
<Slider.Range className="absolute bg-white/40 rounded-full h-full" />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb
|
||||
className="block w-6 h-6 glass-button rounded-full focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2 cursor-grab active:cursor-grabbing"
|
||||
aria-label={label}
|
||||
/>
|
||||
</Slider.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap");
|
||||
@import "@fontsource/space-grotesk/400.css";
|
||||
@import "@fontsource/space-grotesk/600.css";
|
||||
@import "@fontsource/space-grotesk/700.css";
|
||||
@import "@fontsource/inter/400.css";
|
||||
@import "@fontsource/inter/500.css";
|
||||
@import "@fontsource/inter/600.css";
|
||||
@import "@fontsource/roboto-mono/400.css";
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-family-opensans: "Open Sans", sans-serif;
|
||||
--font-family-space-grotesk: "Space Grotesk", sans-serif;
|
||||
--font-family-inter: "Inter", sans-serif;
|
||||
--font-family-mono: "Roboto Mono", monospace;
|
||||
--font-family-raleway: "Raleway", sans-serif;
|
||||
--font-family-roboto: "Roboto";
|
||||
|
||||
--box-shadow-inner-md: inset 1px 4px 6px 0 rgb(0 0 0 / 0.1);
|
||||
--box-shadow-inner-md-2: inset 2px 2px 6px 0 rgb(0 0 0 / 0.15);
|
||||
@@ -60,3 +62,47 @@ body {
|
||||
pre.inline {
|
||||
@apply text-zinc-900;
|
||||
}
|
||||
|
||||
/* Glassmorphism utilities */
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: blur(50px) saturate(100%);
|
||||
/* -webkit-backdrop-filter: blur(10px) saturate(80%); */
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px) saturate(60%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(60%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.glass-button:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.glass-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: blur(8px) saturate(60%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(60%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: blur(15px) saturate(50%);
|
||||
-webkit-backdrop-filter: blur(15px) saturate(50%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
104
src/index.tsx
104
src/index.tsx
@@ -1,71 +1,83 @@
|
||||
import { hydrate, prerender as ssr } from "preact-iso";
|
||||
import { useMemo, useState, useEffect } from "preact/hooks";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
import { useViewportSize } from "./utils/useViewportSize";
|
||||
import { useBooleanToggle } from "./utils/useBooleanToggle";
|
||||
import useBackground from "./utils/useBackground";
|
||||
import Post from "./components/Post";
|
||||
import { RefreshCw, Eye, EyeOff } from "lucide-preact";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { GlassPanel } from "./components/GlassPanel";
|
||||
import { GradientControls } from "./components/GradientControls";
|
||||
import { getPaletteById } from "./utils/palettes";
|
||||
import { exportGradientAsPNG } from "./utils/exportGradient";
|
||||
|
||||
export function App() {
|
||||
const { width, height } = useViewportSize();
|
||||
|
||||
// Gradient state
|
||||
const [paletteId, setPaletteId] = useState("classic");
|
||||
const [gradientCount, setGradientCount] = useState(5);
|
||||
const [noiseIntensity, setNoiseIntensity] = useState(0.9);
|
||||
|
||||
const palette = getPaletteById(paletteId);
|
||||
|
||||
const { svg, backgrounds, regenerate } = useBackground({
|
||||
width,
|
||||
height,
|
||||
ratio: 0.4,
|
||||
paletteColors: palette.colors,
|
||||
gradientCount,
|
||||
noiseIntensity,
|
||||
});
|
||||
const [postHidden, toggleHidden] = useBooleanToggle(false);
|
||||
const [iconSpinning, toggleIconSpinning] = useBooleanToggle(false);
|
||||
|
||||
const style = useMemo(() => {
|
||||
const gradientStyle = useMemo(() => {
|
||||
return {
|
||||
background: [`url("${svg}")`, ...backgrounds].join(", "),
|
||||
};
|
||||
}, [svg, backgrounds]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === "r" || e.key === "R") {
|
||||
e.preventDefault();
|
||||
regenerate();
|
||||
} else if (e.key === "e" || e.key === "E") {
|
||||
e.preventDefault();
|
||||
exportGradientAsPNG();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyPress);
|
||||
return () => window.removeEventListener("keydown", handleKeyPress);
|
||||
}, [regenerate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className="text-zinc-800 gradient max-w-screen max-h-screen overflow-clip"
|
||||
>
|
||||
<div className="font-inter w-full h-full bg-zinc-800/50 bg-blend-overlay">
|
||||
<div className="grid grid-cols-12 w-full">
|
||||
<div className="col-span-3 sm:col-span-2">
|
||||
<button
|
||||
className="block p-2 w-10 h-10 rounded mx-auto xs:mx-0 xs:ml-5 mt-5 shadow-inner-md bg-zinc-700 text-zinc-100 button"
|
||||
onClick={() => {
|
||||
toggleIconSpinning(true);
|
||||
regenerate();
|
||||
setTimeout(() => toggleIconSpinning(false), 200);
|
||||
}}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`transition-transform duration-200 ${
|
||||
iconSpinning ? "rotate-180" : "rotate-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="block p-2 w-10 h-10 rounded mx-auto xs:mx-0 xs:ml-5 mt-5 shadow-inner-md bg-zinc-700 text-zinc-100 button"
|
||||
onClick={() => toggleHidden()}
|
||||
>
|
||||
{postHidden ? <Eye /> : <EyeOff />}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={`h-screen transition-opacity ease-in-out duration-75 ${
|
||||
postHidden ? "opacity-0 pointer-events-none" : ""
|
||||
} flex col-span-9 sm:col-span-6 md:col-span-5 w-full min-h-screen`}
|
||||
>
|
||||
<div className="bg-white overflow-y-auto">
|
||||
<Post />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{/* Full-screen gradient background */}
|
||||
<div
|
||||
id="gradient-container"
|
||||
style={gradientStyle}
|
||||
className="w-full h-screen gradient text-zinc-800 overflow-clip"
|
||||
>
|
||||
<div className="w-full h-full bg-zinc-800/50 bg-blend-overlay" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed overlay panel on left */}
|
||||
<div className="absolute top-0 left-0 w-[400px] h-screen pointer-events-none">
|
||||
<GlassPanel className="m-6 h-[calc(100vh-3rem)] pointer-events-auto">
|
||||
<GradientControls
|
||||
paletteId={paletteId}
|
||||
onPaletteChange={setPaletteId}
|
||||
gradientCount={gradientCount}
|
||||
onGradientCountChange={setGradientCount}
|
||||
noiseIntensity={noiseIntensity}
|
||||
onNoiseIntensityChange={setNoiseIntensity}
|
||||
onRegenerate={regenerate}
|
||||
onExport={() => exportGradientAsPNG()}
|
||||
/>
|
||||
</GlassPanel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@ describe("App", () => {
|
||||
expect(() => render(h(App, {}))).not.toThrow();
|
||||
});
|
||||
|
||||
it("renders the Post component with main heading", () => {
|
||||
it("renders the main heading", () => {
|
||||
const { getAllByText } = render(h(App, {}));
|
||||
expect(getAllByText("Grain").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders author information", () => {
|
||||
it("renders the tagline", () => {
|
||||
const { getAllByText } = render(h(App, {}));
|
||||
expect(getAllByText("Ryan Walters").length).toBeGreaterThan(0);
|
||||
expect(getAllByText("Neural Gradient Explorer").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders with gradient class", () => {
|
||||
|
||||
20
src/utils/exportGradient.ts
Normal file
20
src/utils/exportGradient.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { domToPng } from "modern-screenshot";
|
||||
|
||||
/**
|
||||
* Export the gradient as a PNG screenshot
|
||||
*/
|
||||
export async function exportGradientAsPNG(
|
||||
elementId: string = "gradient-container"
|
||||
): Promise<void> {
|
||||
const element = document.querySelector(`#${elementId}`);
|
||||
if (!element) {
|
||||
throw new Error("Gradient element not found");
|
||||
}
|
||||
|
||||
const dataUrl = await domToPng(element as HTMLElement);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = `grain-gradient-${Date.now()}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
}
|
||||
52
src/utils/palettes.ts
Normal file
52
src/utils/palettes.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export interface ColorPalette {
|
||||
id: string;
|
||||
name: string;
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
export const COLOR_PALETTES: ColorPalette[] = [
|
||||
{
|
||||
id: "classic",
|
||||
name: "Classic",
|
||||
colors: ["#ed625d", "#42b6c6", "#f79f88", "#446ba6", "#4b95f0", "#d16ba5"]
|
||||
},
|
||||
{
|
||||
id: "sunset",
|
||||
name: "Sunset",
|
||||
colors: ["#ff6b6b", "#f9ca24", "#ee5a6f", "#c56cf0", "#ff9ff3", "#feca57"]
|
||||
},
|
||||
{
|
||||
id: "ocean",
|
||||
name: "Ocean",
|
||||
colors: ["#0077be", "#00d4ff", "#0099cc", "#66d3ff", "#1e90ff", "#4ecdc4"]
|
||||
},
|
||||
{
|
||||
id: "forest",
|
||||
name: "Forest",
|
||||
colors: ["#2ecc71", "#27ae60", "#16a085", "#1abc9c", "#52c97f", "#00b894"]
|
||||
},
|
||||
{
|
||||
id: "aurora",
|
||||
name: "Aurora",
|
||||
colors: ["#a29bfe", "#6c5ce7", "#fd79a8", "#e17055", "#00b894", "#00cec9"]
|
||||
},
|
||||
{
|
||||
id: "fire",
|
||||
name: "Fire",
|
||||
colors: ["#ff4757", "#ff6348", "#ff7f50", "#ffa502", "#ff6b81", "#ee5a6f"]
|
||||
},
|
||||
{
|
||||
id: "cosmic",
|
||||
name: "Cosmic",
|
||||
colors: ["#5f27cd", "#341f97", "#ee5a6f", "#c44569", "#f368e0", "#ff9ff3"]
|
||||
},
|
||||
{
|
||||
id: "monochrome",
|
||||
name: "Monochrome",
|
||||
colors: ["#2d3436", "#636e72", "#b2bec3", "#dfe6e9", "#74b9ff", "#a29bfe"]
|
||||
}
|
||||
];
|
||||
|
||||
export function getPaletteById(id: string): ColorPalette {
|
||||
return COLOR_PALETTES.find(p => p.id === id) || COLOR_PALETTES[0];
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Random } from "random-js";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { useMemo, useState, useEffect } from "preact/hooks";
|
||||
import { getEdgePoint } from "./helpers";
|
||||
|
||||
interface useBackgroundProps {
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: number;
|
||||
paletteColors: string[];
|
||||
gradientCount?: number;
|
||||
noiseIntensity?: number;
|
||||
}
|
||||
|
||||
interface useBackgroundReturn {
|
||||
@@ -14,16 +18,11 @@ interface useBackgroundReturn {
|
||||
}
|
||||
|
||||
const random = new Random();
|
||||
const palettes = [
|
||||
// ["#5e1e1e", "#141414", "#400000", "#7a0000", "#2b0059", "#000c59", "#850082", "#850052"],
|
||||
["#ed625d", "#42b6c6", "#f79f88", "#446ba6", "#4b95f0", "#d16ba5"],
|
||||
];
|
||||
|
||||
const generateBackground = (): string[] => {
|
||||
const palette = random.pick(palettes);
|
||||
return Array(5)
|
||||
const generateBackground = (paletteColors: string[], count: number): string[] => {
|
||||
return Array(count)
|
||||
.fill(null)
|
||||
.map(() => random.pick(palette))
|
||||
.map(() => random.pick(paletteColors))
|
||||
.map((color) => {
|
||||
const [x, y] = getEdgePoint(random.integer(0, 400), 100, 100);
|
||||
return `radial-gradient(farthest-corner at ${x}% ${y}%, ${color}, transparent 100%)`;
|
||||
@@ -34,11 +33,19 @@ const useBackground = ({
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
paletteColors,
|
||||
gradientCount = 5,
|
||||
noiseIntensity = 0.9,
|
||||
}: useBackgroundProps): useBackgroundReturn => {
|
||||
const [background, setBackground] = useState(generateBackground());
|
||||
const [background, setBackground] = useState(() => generateBackground(paletteColors, gradientCount));
|
||||
|
||||
// Regenerate when palette or count changes
|
||||
useEffect(() => {
|
||||
setBackground(generateBackground(paletteColors, gradientCount));
|
||||
}, [paletteColors, gradientCount]);
|
||||
|
||||
const regenerate = () => {
|
||||
setBackground(generateBackground());
|
||||
setBackground(generateBackground(paletteColors, gradientCount));
|
||||
};
|
||||
|
||||
const noise = (): string => {
|
||||
@@ -46,7 +53,7 @@ const useBackground = ({
|
||||
const svgHeight = Math.ceil((height ?? 1080) * ratio);
|
||||
const seed = random.integer(0, 1000000);
|
||||
|
||||
return `<svg viewBox="0 0 ${svgWidth} ${svgHeight}" xmlns="http://www.w3.org/2000/svg"><filter id="noiseFilter"><feTurbulence type="fractalNoise" baseFrequency="2.1" numOctaves="2" seed="${seed}" stitchTiles="stitch"/></filter><g opacity="0.9"><rect width="100%" height="100%" filter="url(#noiseFilter)"/></g></svg>`;
|
||||
return `<svg viewBox="0 0 ${svgWidth} ${svgHeight}" xmlns="http://www.w3.org/2000/svg"><filter id="noiseFilter"><feTurbulence type="fractalNoise" baseFrequency="2.1" numOctaves="2" seed="${seed}" stitchTiles="stitch"/></filter><g opacity="${noiseIntensity}"><rect width="100%" height="100%" filter="url(#noiseFilter)"/></g></svg>`;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user