feat: Add logo with text to navigation menu

This commit is contained in:
mauro 🤙 2024-07-04 07:58:14 +00:00
parent cb35ed18c9
commit 5396e42be8
12 changed files with 1250 additions and 47 deletions

458
package-lock.json generated
View file

@ -8,8 +8,13 @@
"name": "zen-website", "name": "zen-website",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.7.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -20,9 +25,11 @@
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-spring": "^9.7.3", "react-spring": "^9.7.3",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@ -2348,6 +2355,40 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
"integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
"dependencies": {
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
"integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
"integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA=="
},
"node_modules/@hapi/hoek": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -2363,6 +2404,14 @@
"@hapi/hoek": "^9.0.0" "@hapi/hoek": "^9.0.0"
} }
}, },
"node_modules/@hookform/resolvers": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.7.0.tgz",
"integrity": "sha512-42p5X18noBV3xqOpTlf2V5qJZwzNgO4eLzHzmKGh/w7z4+4XqRw5AsESVkqE+qwAuRRlg2QG12EVEjPkrRIbeg==",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.14", "version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -2764,11 +2813,67 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="
},
"node_modules/@radix-ui/primitive": { "node_modules/@radix-ui/primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
}, },
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz",
"integrity": "sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
@ -2862,6 +2967,44 @@
} }
} }
}, },
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
"integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
"integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-icons": { "node_modules/@radix-ui/react-icons": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
@ -2887,6 +3030,28 @@
} }
} }
}, },
"node_modules/@radix-ui/react-label": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz",
"integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-navigation-menu": { "node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz",
@ -2922,6 +3087,60 @@
} }
} }
}, },
"node_modules/@radix-ui/react-popper": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz",
"integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-presence": { "node_modules/@radix-ui/react-presence": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
@ -2997,6 +3216,48 @@
} }
} }
}, },
"node_modules/@radix-ui/react-select": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz",
"integrity": "sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-dismissable-layer": "1.1.0",
"@radix-ui/react-focus-guards": "1.1.0",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.7"
},
"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
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
@ -3119,6 +3380,40 @@
} }
} }
}, },
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
"dependencies": {
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": { "node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
@ -3141,6 +3436,11 @@
} }
} }
}, },
"node_modules/@radix-ui/rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
},
"node_modules/@react-native-community/cli": { "node_modules/@react-native-community/cli": {
"version": "13.6.9", "version": "13.6.9",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.9.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.9.tgz",
@ -4321,6 +4621,17 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true "dev": true
}, },
"node_modules/aria-hidden": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-query": { "node_modules/aria-query": {
"version": "5.1.3", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
@ -5588,6 +5899,11 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -6748,6 +7064,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -7158,7 +7482,6 @@
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
@ -10035,6 +10358,21 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-hook-form": {
"version": "7.52.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz",
"integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==",
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -10130,6 +10468,51 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-remove-scroll": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
"integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
"dependencies": {
"react-remove-scroll-bar": "^2.3.4",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
"dependencies": {
"react-style-singleton": "^2.2.1",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-shallow-renderer": { "node_modules/react-shallow-renderer": {
"version": "16.15.0", "version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@ -10273,6 +10656,28 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"dependencies": {
"get-nonce": "^1.0.0",
"invariant": "^2.2.4",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-use-measure": { "node_modules/react-use-measure": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz",
@ -11925,6 +12330,47 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-callback-ref": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -12319,6 +12765,14 @@
"integrity": "sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==", "integrity": "sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==",
"peer": true "peer": true
}, },
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zustand": { "node_modules/zustand": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",

View file

@ -9,8 +9,13 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.7.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -21,9 +26,11 @@
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-spring": "^9.7.3", "react-spring": "^9.7.3",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

View file

@ -2,19 +2,20 @@
import { ny } from "@/lib/utils"; import { ny } from "@/lib/utils";
import GridPattern from "./ui/grid-pattern"; import GridPattern from "./ui/grid-pattern";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "./ui/select";
import { DownloadIcon } from "lucide-react"; import { Button } from "./ui/button";
import { Form, FormField, FormItem, FormLabel } from "./ui/form";
import { useForm } from "react-hook-form";
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'
const BASE_URL = "https://github.com/zen-browser/desktop/releases/download/latest"; const BASE_URL = "https://github.com/zen-browser/desktop/releases/download/latest";
const releases: any = { const releases: any = {
Windows: [ Windows: "zen.installer.exe",
"zen.win64.zip", WindowsZip: "zen.win64.zip",
],
//MacOS: [], //MacOS: [],
Linux: [ Linux: "zen.linux.tar.bz2",
"zen.linux.tar.bz2",
],
}; };
function getDefaultPlatformBasedOnUserAgent() { function getDefaultPlatformBasedOnUserAgent() {
@ -34,7 +35,30 @@ function getDefaultPlatformBasedOnUserAgent() {
return "Windows"; return "Windows";
} }
const formSchema = z.object({
platform: z.string().nonempty(),
});
export default function DownloadPage() { export default function DownloadPage() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
platform: getDefaultPlatformBasedOnUserAgent(),
},
});
const onSubmit = async (data: any) => {
const platform = data.platform;
console.log("Data: ", data)
console.log("Platform: ", platform)
console.log("Releases: ", releases)
const releasesForPlatform = releases[platform];
console.log("Releases for platform: ", releasesForPlatform)
const url = `${BASE_URL}/${releasesForPlatform}`;
console.log("URL: ", url)
window.location.href = url;
}
return ( return (
<div className="w-full relative h-screen flex items-center justify-center"> <div className="w-full relative h-screen flex items-center justify-center">
<div className="w-1/2 relative h-full px-64 flex items-cetner justify-center flex-col"> <div className="w-1/2 relative h-full px-64 flex items-cetner justify-center flex-col">
@ -60,38 +84,37 @@ export default function DownloadPage() {
Get started with Zen Browser today. Get back to browsing the web with peace of mind. Get started with Zen Browser today. Get back to browsing the web with peace of mind.
</p> </p>
</div> </div>
<div className="w-1/2 relative flex items-cetner justify-start"> <div className="w-1/2 relative flex flex-col relative items-cetner justify-start">
<Tabs defaultValue={getDefaultPlatformBasedOnUserAgent()} className="mx-auto w-fit flex flex-col items-start justify-center"> <div className="w-1/2 relative">
<TabsList> <Form {...form}>
{Object.keys(releases).map((platform) => ( <form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<TabsTrigger key={platform} value={platform}>{platform}</TabsTrigger> <FormField
))} control={form.control}
</TabsList> name="platform"
{Object.keys(releases).map((platform) => ( render={({ field }) => (
<TabsContent key={platform} value={platform} className="border rounded-md p-5 border-gray"> <FormItem>
<table> <FormLabel>Select your operating system</FormLabel>
<thead className=""> <Select onValueChange={field.onChange} defaultValue={field.value}>
<tr> <SelectTrigger className="w-full mb-5">
<th className="text-start pb-4 min-w-64">File</th> <SelectValue placeholder="Operating System" />
<th className="text-start pb-4">Download</th> </SelectTrigger>
</tr> <SelectContent>
</thead> <SelectGroup>
<tbody> <SelectLabel>Operating System</SelectLabel>
{releases[platform].map((release: string) => ( <SelectItem value="Windows">Windows Installer</SelectItem>
<tr key={release} className="relative w-full"> <SelectItem value="WindowsZip">Windows (Zip)</SelectItem>
<td className="min-w-64">{release}</td> <SelectItem value="MacOS" disabled>MacOS</SelectItem>
<td className="flex items-center w-full justify-center"> <SelectItem value="Linux">Linux</SelectItem>
<a href={`${BASE_URL}/${release}`} className="text-blue-500"> </SelectGroup>
<DownloadIcon size={24} /> </SelectContent>
</a> </Select>
</td> </FormItem>
</tr> )}
))} />
</tbody> <Button type="submit">Download Zen 🎉</Button>
</table> </form>
</TabsContent> </Form>
))} </div>
</Tabs>
</div> </div>
</div> </div>
); );

View file

@ -63,7 +63,9 @@ export default function Header() {
text="Documentation" text="Documentation"
/> />
</a> </a>
<a href="/download">
<ShinyButton text="Download now" /> <ShinyButton text="Download now" />
</a>
</div> </div>
<ChevronDown className="absolute bottom-5 left-1/2 size-7 mb-10 animate-bounce" style={{ <ChevronDown className="absolute bottom-5 left-1/2 size-7 mb-10 animate-bounce" style={{
transform: 'translateX(-50%)', transform: 'translateX(-50%)',

View file

@ -1,6 +1,9 @@
export default function Logo() { export default function Logo({ withText, ...props }: any) {
return ( return (
<div className="flex items-center m-0" {...props}>
<img src="/logo.png" alt="Zen Logo" className="w-12 h-12" /> <img src="/logo.png" alt="Zen Logo" className="w-12 h-12" />
{withText && <span className="text-2xl font-bold ml-2">Zen</span>}
</div>
); );
} }

View file

@ -32,7 +32,12 @@ export function Navigation() {
return ( return (
<div className="backdrop-blur fixed z-10 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center"> <div className="backdrop-blur fixed z-10 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
<NavigationMenu> <NavigationMenu>
<NavigationMenuList> <NavigationMenuList className="w-full">
<NavigationMenuItem className="cursor-pointer mr-20">
<NavigationMenuLink href="/" asChild>
<Logo withText />
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuTrigger>Getting started</NavigationMenuTrigger> <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
<NavigationMenuContent> <NavigationMenuContent>

View file

@ -0,0 +1,57 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import { ny } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={ny(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = 'Button'
export { Button, buttonVariants }

View file

@ -0,0 +1,30 @@
'use client'
import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from '@radix-ui/react-icons'
import { ny } from '@/lib/utils'
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={ny(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={ny('flex items-center justify-center text-current')}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View file

@ -0,0 +1,239 @@
'use client'
import type { ReactNode } from 'react'
import React, { useEffect, useRef } from 'react'
export interface BaseParticle {
element: HTMLElement | SVGSVGElement
left: number
size: number
top: number
}
export interface BaseParticleOptions {
particle?: string
size?: number
}
export interface CoolParticle extends BaseParticle {
direction: number
speedHorz: number
speedUp: number
spinSpeed: number
spinVal: number
}
export interface CoolParticleOptions extends BaseParticleOptions {
particleCount?: number
speedHorz?: number
speedUp?: number
}
function getContainer() {
const id = '_coolMode_effect'
const existingContainer = document.getElementById(id)
if (existingContainer)
return existingContainer
const container = document.createElement('div')
container.setAttribute('id', id)
container.setAttribute(
'style',
'overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647',
)
document.body.appendChild(container)
return container
}
let instanceCounter = 0
function applyParticleEffect(
element: HTMLElement,
options?: CoolParticleOptions,
): () => void {
instanceCounter++
const defaultParticle = 'circle'
const particleType = options?.particle || defaultParticle
const sizes = [15, 20, 25, 35, 45]
const limit = 45
let particles: CoolParticle[] = []
let autoAddParticle = false
let mouseX = 0
let mouseY = 0
const container = getContainer()
function generateParticle() {
const size
= options?.size || sizes[Math.floor(Math.random() * sizes.length)]
const speedHorz = options?.speedHorz || Math.random() * 10
const speedUp = options?.speedUp || Math.random() * 25
const spinVal = Math.random() * 360
const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1)
const top = mouseY - size / 2
const left = mouseX - size / 2
const direction = Math.random() <= 0.5 ? -1 : 1
const particle = document.createElement('div')
if (particleType === 'circle') {
const svgNS = 'http://www.w3.org/2000/svg'
const circleSVG = document.createElementNS(svgNS, 'svg')
const circle = document.createElementNS(svgNS, 'circle')
circle.setAttributeNS(null, 'cx', (size / 2).toString())
circle.setAttributeNS(null, 'cy', (size / 2).toString())
circle.setAttributeNS(null, 'r', (size / 2).toString())
circle.setAttributeNS(
null,
'fill',
`hsl(${Math.random() * 360}, 70%, 50%)`,
)
circleSVG.appendChild(circle)
circleSVG.setAttribute('width', size.toString())
circleSVG.setAttribute('height', size.toString())
particle.appendChild(circleSVG)
}
else {
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`
}
particle.style.position = 'absolute'
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`
container.appendChild(particle)
particles.push({
direction,
element: particle,
left,
size,
speedHorz,
speedUp,
spinSpeed,
spinVal,
top,
})
}
function refreshParticles() {
particles.forEach((p) => {
p.left = p.left - p.speedHorz * p.direction
p.top = p.top - p.speedUp
p.speedUp = Math.min(p.size, p.speedUp - 1)
p.spinVal = p.spinVal + p.spinSpeed
if (
p.top
>= Math.max(window.innerHeight, document.body.clientHeight) + p.size
) {
particles = particles.filter(o => o !== p)
p.element.remove()
}
p.element.setAttribute(
'style',
[
'position:absolute',
'will-change:transform',
`top:${p.top}px`,
`left:${p.left}px`,
`transform:rotate(${p.spinVal}deg)`,
].join(';'),
)
})
}
let animationFrame: number | undefined
let lastParticleTimestamp = 0
const particleGenerationDelay = 30
function loop() {
const currentTime = performance.now()
if (
autoAddParticle
&& particles.length < limit
&& currentTime - lastParticleTimestamp > particleGenerationDelay
) {
generateParticle()
lastParticleTimestamp = currentTime
}
refreshParticles()
animationFrame = requestAnimationFrame(loop)
}
loop()
const isTouchInteraction = 'ontouchstart' in window
const tap = isTouchInteraction ? 'touchstart' : 'mousedown'
const tapEnd = isTouchInteraction ? 'touchend' : 'mouseup'
const move = isTouchInteraction ? 'touchmove' : 'mousemove'
const updateMousePosition = (e: MouseEvent | TouchEvent) => {
if ('touches' in e) {
mouseX = e.touches?.[0].clientX
mouseY = e.touches?.[0].clientY
}
else {
mouseX = e.clientX
mouseY = e.clientY
}
}
const tapHandler = (e: MouseEvent | TouchEvent) => {
updateMousePosition(e)
autoAddParticle = true
}
const disableAutoAddParticle = () => {
autoAddParticle = false
}
element.addEventListener(move, updateMousePosition, { passive: true })
element.addEventListener(tap, tapHandler, { passive: true })
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true })
element.addEventListener('mouseleave', disableAutoAddParticle, {
passive: true,
})
return () => {
element.removeEventListener(move, updateMousePosition)
element.removeEventListener(tap, tapHandler)
element.removeEventListener(tapEnd, disableAutoAddParticle)
element.removeEventListener('mouseleave', disableAutoAddParticle)
const interval = setInterval(() => {
if (animationFrame && particles.length === 0) {
cancelAnimationFrame(animationFrame)
clearInterval(interval)
if (--instanceCounter === 0)
container.remove()
}
}, 500)
}
}
interface CoolModeProps {
children: ReactNode
options?: CoolParticleOptions
}
export const CoolMode: React.FC<CoolModeProps> = ({ children, options }) => {
const ref = useRef<HTMLElement>(null)
useEffect(() => {
if (ref.current)
return applyParticleEffect(ref.current, options)
}, [options])
return React.cloneElement(children as React.ReactElement, { ref })
}

189
src/components/ui/form.tsx Normal file
View file

@ -0,0 +1,189 @@
import * as React from 'react'
import type * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'
import { Controller, FormProvider, useFormContext, useFormState } from 'react-hook-form'
import { ny } from '@/lib/utils'
import { Label } from '@/components/ui/label'
const Form = FormProvider
interface FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
)
function FormField<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ ...props }: ControllerProps<TFieldValues, TName>) {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
function useFormField() {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext)
throw new Error('useFormField should be used within <FormField>')
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
interface FormItemContextValue {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={ny('space-y-2', className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={ny(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId }
= useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={ny('text-[0.8rem] text-muted-foreground', className)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body)
return null
return (
<p
ref={ref}
id={formMessageId}
className={ny('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'
const FormGlobalError = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { errors } = useFormState()
const rootError = errors.root
if (!rootError)
return null
return (
<p
ref={ref}
className={ny('text-sm font-medium text-destructive', className)}
{...props}
>
{rootError.message}
</p>
)
})
FormGlobalError.displayName = 'FormGlobalError'
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormGlobalError,
FormField,
}

View file

@ -0,0 +1,26 @@
'use client'
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { type VariantProps, cva } from 'class-variance-authority'
import { ny } from '@/lib/utils'
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={ny(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,168 @@
'use client'
import * as React from 'react'
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@radix-ui/react-icons'
import * as SelectPrimitive from '@radix-ui/react-select'
import { ny } from '@/lib/utils'
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={ny(
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-left text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 [&>span]:text-left',
className,
)}
onPointerDown={(e) => {
if (e.pointerType === 'touch')
e.preventDefault()
}}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={ny(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={ny(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName
= SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={ny(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={ny(
'p-1',
position === 'popper'
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={ny('px-2 py-1.5 text-sm font-semibold', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={ny(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={ny('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}