chore: update dependencies and add new mobile-first features page with interactive demos
This commit is contained in:
2005
package-lock.json
generated
2005
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -10,33 +10,34 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"framer-motion": "^11.18.0",
|
||||
"@headlessui/react": "^2.1.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@types/node": "^20.10.8",
|
||||
"@types/react": "^18.2.47",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"clsx": "^2.1.1",
|
||||
"react-type-animation": "^3.2.0",
|
||||
"cobe": "^0.6.5",
|
||||
"framer-motion": "^10.15.0",
|
||||
"react": "^18.2.0",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"cobe": "^0.6.4"
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-type-animation": "^3.2.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"typescript": "^5.3.3",
|
||||
"use-debounce": "^10.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"sharp": "0.33.1",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
231
public/images/phone-frame.svg
Normal file
231
public/images/phone-frame.svg
Normal file
@@ -0,0 +1,231 @@
|
||||
<svg width="366" height="729" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g mask="url(#mask)">
|
||||
<g filter="url(#a)">
|
||||
<path
|
||||
d="M363.315 64.213C363.315 22.99 341.312 1 300.092 1H66.751C25.53 1 3.528 22.99 3.528 64.213v44.68l-.857.143A2 2 0 0 0 1 111.009v24.611a2 2 0 0 0 1.671 1.973l.95.158a2.26 2.26 0 0 1-.093.236v26.173c.212.1.398.296.541.643l-1.398.233A2 2 0 0 0 1 167.009v47.611a2 2 0 0 0 1.671 1.973l1.368.228c-.139.319-.314.533-.511.653v16.637c.221.104.414.313.56.689l-1.417.236A2 2 0 0 0 1 237.009v47.611a2 2 0 0 0 1.671 1.973l1.347.225c-.135.294-.302.493-.49.607v377.681c0 41.213 22 63.208 63.223 63.208h95.074c.947-.504 2.717-.843 4.745-.843l.141.001h.194l.086-.001 33.704.005c1.849.043 3.442.37 4.323.838h95.074c41.222 0 63.223-21.999 63.223-63.212v-394.63c-.259-.275-.48-.796-.63-1.47l-.011-.133 1.655-.276A2 2 0 0 0 366 266.62v-77.611a2 2 0 0 0-1.671-1.973l-1.712-.285c.148-.839.396-1.491.698-1.811V64.213Z"
|
||||
fill="url(#b)" />
|
||||
<path
|
||||
d="M363.315 64.213C363.315 22.99 341.312 1 300.092 1H66.751C25.53 1 3.528 22.99 3.528 64.213v44.68l-.857.143A2 2 0 0 0 1 111.009v24.611a2 2 0 0 0 1.671 1.973l.95.158a2.26 2.26 0 0 1-.093.236v26.173c.212.1.398.296.541.643l-1.398.233A2 2 0 0 0 1 167.009v47.611a2 2 0 0 0 1.671 1.973l1.368.228c-.139.319-.314.533-.511.653v16.637c.221.104.414.313.56.689l-1.417.236A2 2 0 0 0 1 237.009v47.611a2 2 0 0 0 1.671 1.973l1.347.225c-.135.294-.302.493-.49.607v377.681c0 41.213 22 63.208 63.223 63.208h95.074c.947-.504 2.717-.843 4.745-.843l.141.001h.194l.086-.001 33.704.005c1.849.043 3.442.37 4.323.838h95.074c41.222 0 63.223-21.999 63.223-63.212v-394.63c-.259-.275-.48-.796-.63-1.47l-.011-.133 1.655-.276A2 2 0 0 0 366 266.62v-77.611a2 2 0 0 0-1.671-1.973l-1.712-.285c.148-.839.396-1.491.698-1.811V64.213Z"
|
||||
fill="url(#c)" />
|
||||
</g>
|
||||
<g filter="url(#d)">
|
||||
<path
|
||||
d="M5 133.772v-21.15c0-1.359-.54-2.661-1.5-3.622-.844-.073-2.496.257-2.496 2.157v24.562c.406 2.023 2.605 2.023 2.605 2.023A6.363 6.363 0 0 0 5 133.772Z"
|
||||
fill="url(#e)" />
|
||||
<path
|
||||
d="M5 133.772v-21.15c0-1.359-.54-2.661-1.5-3.622-.844-.073-2.496.257-2.496 2.157v24.562c.406 2.023 2.605 2.023 2.605 2.023A6.363 6.363 0 0 0 5 133.772Z"
|
||||
fill="url(#f)" fill-opacity=".1" />
|
||||
</g>
|
||||
<g filter="url(#g)">
|
||||
<path
|
||||
d="M5 213.772v-46.15c0-1.359-.54-2.661-1.5-3.622-.844-.073-2.496.257-2.496 2.157v49.562c.406 2.023 2.605 2.023 2.605 2.023A6.363 6.363 0 0 0 5 213.772Z"
|
||||
fill="url(#h)" />
|
||||
<path
|
||||
d="M5 213.772v-46.15c0-1.359-.54-2.661-1.5-3.622-.844-.073-2.496.257-2.496 2.157v49.562c.406 2.023 2.605 2.023 2.605 2.023A6.363 6.363 0 0 0 5 213.772Z"
|
||||
fill="url(#i)" fill-opacity=".1" />
|
||||
</g>
|
||||
<g filter="url(#j)">
|
||||
<path
|
||||
d="M5 283.772v-46.15c0-1.359-.54-2.661-1.5-3.622-.844-.073-2.496.257-2.496 2.157v49.562c.406 2.023 2.605 2.023 2.605 2.023A6.363 6.363 0 0 0 5 283.772Z"
|
||||
fill="url(#k)" />
|
||||
<path
|
||||
d="M5 283.772v-46.15c0-1.359-.54-2.661-1.5-3.622-.844-.073-2.496.257-2.496 2.157v49.562c.406 2.023 2.605 2.023 2.605 2.023A6.363 6.363 0 0 0 5 283.772Z"
|
||||
fill="url(#l)" fill-opacity=".1" />
|
||||
</g>
|
||||
<g filter="url(#m)">
|
||||
<path
|
||||
d="M362.004 266.772v-78.15a5.12 5.12 0 0 1 1.5-3.622c.844-.073 2.496.257 2.496 2.157v81.562c-.406 2.023-2.605 2.023-2.605 2.023a6.359 6.359 0 0 1-1.391-3.97Z"
|
||||
fill="url(#n)" />
|
||||
<path
|
||||
d="M362.004 266.772v-78.15a5.12 5.12 0 0 1 1.5-3.622c.844-.073 2.496.257 2.496 2.157v81.562c-.406 2.023-2.605 2.023-2.605 2.023a6.359 6.359 0 0 1-1.391-3.97Z"
|
||||
fill="url(#o)" fill-opacity=".1" />
|
||||
</g>
|
||||
<path
|
||||
d="M305 14.5H59c-24.577 0-44.5 19.923-44.5 44.5v615c0 23.472 19.028 42.5 42.5 42.5h250c23.472 0 42.5-19.028 42.5-42.5V59c0-24.577-19.923-44.5-44.5-44.5Z"
|
||||
stroke="url(#p)" stroke-opacity=".5" />
|
||||
<g filter="url(#q)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16 59c0-23.748 19.252-43 43-43h246c23.748 0 43 19.252 43 43v615c0 23.196-18.804 42-42 42H58c-23.196 0-42-18.804-42-42V59Z"
|
||||
fill="url(#r)" fill-opacity=".3" />
|
||||
<path
|
||||
d="M305 15.5H59c-24.024 0-43.5 19.476-43.5 43.5v615c0 23.472 19.028 42.5 42.5 42.5h248c23.472 0 42.5-19.028 42.5-42.5V59c0-24.024-19.476-43.5-43.5-43.5Z"
|
||||
stroke="#000" stroke-opacity=".07" />
|
||||
</g>
|
||||
<g filter="url(#s)">
|
||||
<rect x="154" y="29" width="56" height="5" rx="2.5" fill="#D4D4D4" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<mask id="mask">
|
||||
<rect width="366" height="729" fill="#fff" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M89.728 24a4.213 4.213 0 0 1 4.213 4.212v2.527c0 10.235 8.3 18.532 18.539 18.532h139.04c10.239 0 18.539-8.297 18.539-18.532v-2.527A4.212 4.212 0 0 1 274.272 24h32.864C325.286 24 340 38.71 340 56.853v618.295c0 18.144-14.714 32.853-32.864 32.853H56.864c-18.15 0-32.864-14.709-32.864-32.853V56.853C24 38.709 38.714 24 56.864 24h32.864Z"
|
||||
fill="#000" />
|
||||
</mask>
|
||||
<linearGradient id="e" x1="1.004" y1="123.367" x2="5" y2="123.367" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D4D4D4" />
|
||||
<stop offset="1" stop-color="#E6E6E6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="f" x1="3.002" y1="108.991" x2="3.002" y2="116.75" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" />
|
||||
<stop offset=".783" stop-color="#171717" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="h" x1="1.004" y1="190.867" x2="5" y2="190.867" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D4D4D4" />
|
||||
<stop offset="1" stop-color="#E6E6E6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="i" x1="3.002" y1="163.991" x2="3.002" y2="178.497" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" />
|
||||
<stop offset=".783" stop-color="#171717" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="k" x1="1.004" y1="260.867" x2="5" y2="260.867" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D4D4D4" />
|
||||
<stop offset="1" stop-color="#E6E6E6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="l" x1="3.002" y1="233.991" x2="3.002" y2="248.497" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" />
|
||||
<stop offset=".783" stop-color="#171717" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="n" x1="362.004" y1="226.25" x2="366" y2="226.25" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".124" stop-color="#E6E6E6" />
|
||||
<stop offset="1" stop-color="#D4D4D4" />
|
||||
</linearGradient>
|
||||
<linearGradient id="o" x1="364.002" y1="184.991" x2="364.002" y2="208.134" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" />
|
||||
<stop offset=".783" stop-color="#171717" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="p" x1="182" y1="15" x2="182" y2="716" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff" />
|
||||
<stop offset=".381" stop-color="#fff" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<filter id="a" x="-1" y="-1" width="367" height="730.314" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="-2" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend in2="shape" result="effect1_innerShadow_104_2007" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="-2" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0.0901961 0 0 0 0 0.0901961 0 0 0 0 0.0901961 0 0 0 0.17 0" />
|
||||
<feBlend in2="effect1_innerShadow_104_2007" result="effect2_innerShadow_104_2007" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2" />
|
||||
<feGaussianBlur stdDeviation=".5" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.6 0" />
|
||||
<feBlend in2="effect2_innerShadow_104_2007" result="effect3_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<filter id="d" x="1.004" y="108.991" width="4.996" height="28.751" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="1" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_104_2007" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_104_2007" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="-1" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0" />
|
||||
<feBlend in2="shape" result="effect2_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<filter id="g" x="1.004" y="163.991" width="4.996" height="53.751" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="1" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_104_2007" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_104_2007" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="-1" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0" />
|
||||
<feBlend in2="shape" result="effect2_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<filter id="j" x="1.004" y="233.991" width="4.996" height="53.751" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="1" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_104_2007" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_104_2007" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="-1" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0" />
|
||||
<feBlend in2="shape" result="effect2_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<filter id="m" x="361.004" y="184.991" width="4.996" height="85.751" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="-1" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_104_2007" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_104_2007" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="1" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0" />
|
||||
<feBlend in2="shape" result="effect2_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<filter id="q" x="15" y="15" width="334" height="703" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_104_2007" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_104_2007" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="2.5" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0" />
|
||||
<feBlend in2="shape" result="effect2_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<filter id="s" x="154" y="29" width="56" height="6" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_104_2007" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_104_2007" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation=".5" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="shape" result="effect2_innerShadow_104_2007" />
|
||||
</filter>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0 727 -642 0 184 1)">
|
||||
<stop stop-color="#FAFAFA" />
|
||||
<stop offset="1" stop-color="#E6E6E6" />
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0 319 -295.5 0 183.5 1)">
|
||||
<stop stop-color="#fff" />
|
||||
<stop offset=".533" stop-color="#fff" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient id="r" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0 689 -326.783 0 182 27)">
|
||||
<stop offset=".319" stop-color="#D4D4D4" />
|
||||
<stop offset="1" stop-color="#E6E6E6" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/phoneframe.png
Normal file
BIN
public/images/phoneframe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
22
src/components/PhoneFrame.tsx
Normal file
22
src/components/PhoneFrame.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import phoneFrame from '@/images/phone-frame.svg'
|
||||
|
||||
export function PhoneFrame({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div className={clsx('relative aspect-[366/729]', className)} {...props}>
|
||||
<img
|
||||
src={phoneFrame}
|
||||
alt=""
|
||||
className="pointer-events-none absolute inset-0 w-full h-full"
|
||||
/>
|
||||
<div className="absolute inset-x-[6.3%] top-[3.15%] bottom-[2.75%] rounded-3xl overflow-y-auto bg-gray-900">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
147
src/components/Texts.tsx
Normal file
147
src/components/Texts.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const colorVariants = {
|
||||
primary: 'text-gray-900',
|
||||
secondary: 'text-gray-600',
|
||||
light: 'text-gray-50',
|
||||
accent: 'text-cyan-500',
|
||||
white: 'text-white',
|
||||
dark: 'text-gray-950',
|
||||
tertiary: 'text-gray-700',
|
||||
lightSecondary: 'text-gray-300',
|
||||
} as const
|
||||
|
||||
type TextOwnProps = {
|
||||
color?: keyof typeof colorVariants
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Polymorphic helpers
|
||||
type PolymorphicProps<E extends React.ElementType, P> = P & {
|
||||
as?: E
|
||||
} & Omit<React.ComponentPropsWithoutRef<E>, keyof P | 'as'>
|
||||
|
||||
const createTextComponent = <DefaultElement extends React.ElementType>(
|
||||
defaultElement: DefaultElement,
|
||||
defaultClassName: string
|
||||
) => {
|
||||
type Props<E extends React.ElementType = DefaultElement> = PolymorphicProps<
|
||||
E,
|
||||
TextOwnProps
|
||||
>
|
||||
|
||||
function Text<E extends React.ElementType = DefaultElement>({
|
||||
as,
|
||||
color = 'primary',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: Props<E>) {
|
||||
const Tag = (as || defaultElement) as React.ElementType
|
||||
return (
|
||||
<Tag
|
||||
className={cn(defaultClassName, colorVariants[color], className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
;(Text as any).displayName = `Text(${typeof defaultElement === 'string' ? defaultElement : 'Component'
|
||||
})`
|
||||
return Text
|
||||
}
|
||||
|
||||
// Exports based on your tailwind.css and the example
|
||||
export const H1 = createTextComponent(
|
||||
'h1',
|
||||
'text-6xl lg:text-7xl font-medium leading-tight tracking-tight'
|
||||
)
|
||||
export const H2 = createTextComponent(
|
||||
'h2',
|
||||
'text-4xl lg:text-6xl font-medium leading-tight tracking-tight'
|
||||
)
|
||||
export const H3 = createTextComponent(
|
||||
'h3',
|
||||
'text-3xl lg:text-5xl font-medium leading-tight tracking-tight'
|
||||
)
|
||||
export const H4 = createTextComponent(
|
||||
'h4',
|
||||
'text-2xl lg:text-4xl font-medium leading-snug tracking-tight'
|
||||
)
|
||||
export const P = createTextComponent(
|
||||
'p',
|
||||
'text-base lg:text-lg leading-relaxed'
|
||||
)
|
||||
export const Small = createTextComponent(
|
||||
'small',
|
||||
'text-sm font-medium leading-normal tracking-normal'
|
||||
)
|
||||
export const Subtle = createTextComponent(
|
||||
'p',
|
||||
'text-sm leading-normal tracking-normal text-gray-500'
|
||||
)
|
||||
export const H5 = createTextComponent(
|
||||
'h5',
|
||||
'text-xl lg:text-2xl font-semibold leading-snug tracking-tight'
|
||||
)
|
||||
export const Eyebrow = createTextComponent(
|
||||
'h2',
|
||||
'text-base/7 font-semibold tracking-wide'
|
||||
)
|
||||
export const SectionHeader = createTextComponent(
|
||||
'p',
|
||||
'text-3xl lg:text-4xl font-medium leading-tight tracking-tight'
|
||||
)
|
||||
export const CardEyebrow = createTextComponent(
|
||||
'h3',
|
||||
'text-sm/4 font-semibold tracking-wide'
|
||||
)
|
||||
export const CardTitle = createTextComponent(
|
||||
'p',
|
||||
'text-lg font-medium leading-snug tracking-tight'
|
||||
)
|
||||
export const CardDescription = createTextComponent(
|
||||
'p',
|
||||
'text-sm/6 leading-normal tracking-normal'
|
||||
)
|
||||
export const FeatureTitle = createTextComponent(
|
||||
'h3',
|
||||
'text-lg font-semibold leading-snug tracking-tight'
|
||||
)
|
||||
export const FeatureDescription = createTextComponent(
|
||||
'p',
|
||||
'text-sm leading-normal tracking-normal'
|
||||
)
|
||||
export const MobileFeatureTitle = createTextComponent(
|
||||
'h3',
|
||||
'text-sm font-semibold sm:text-lg leading-snug tracking-tight'
|
||||
)
|
||||
export const SecondaryFeatureTitle = createTextComponent(
|
||||
'h3',
|
||||
'text-base font-semibold leading-snug tracking-tight'
|
||||
)
|
||||
export const Question = createTextComponent(
|
||||
'h3',
|
||||
'text-lg/6 font-semibold tracking-tight'
|
||||
)
|
||||
export const Answer = createTextComponent(
|
||||
'p',
|
||||
'mt-4 text-sm leading-normal tracking-normal'
|
||||
)
|
||||
export const PageHeader = createTextComponent(
|
||||
'h2',
|
||||
'text-5xl lg:text-6xl font-medium leading-tight tracking-tight'
|
||||
)
|
||||
export const DownloadCardTitle = createTextComponent(
|
||||
'dt',
|
||||
'text-base/7 font-semibold tracking-wide'
|
||||
)
|
||||
export const DownloadCardDescription = createTextComponent(
|
||||
'dd',
|
||||
'text-base/7 leading-normal tracking-normal'
|
||||
)
|
||||
BIN
src/images/linux copy.png
Normal file
BIN
src/images/linux copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
109
src/pages/network/AppScreen.tsx
Normal file
109
src/pages/network/AppScreen.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { forwardRef } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 79 24" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M12 24C5.373 24 0 18.627 0 12S5.373 0 12 0s12 5.373 12 12-5.373 12-12 12ZM2.4 12a9.004 9.004 0 0 0 6.055 8.507c1.565.542 2.945-.85 2.945-2.507V6c0-1.657-1.38-3.049-2.945-2.507A9.004 9.004 0 0 0 2.4 12Z"
|
||||
fill="#06B6D4"
|
||||
/>
|
||||
<path
|
||||
d="M33.004 17V6.818h3.818c.783 0 1.439.146 1.97.438.533.291.935.692 1.207 1.203.275.507.413 1.084.413 1.73 0 .653-.138 1.233-.413 1.74a2.948 2.948 0 0 1-1.218 1.198c-.537.288-1.198.433-1.983.433h-2.531v-1.517h2.282c.457 0 .832-.08 1.124-.238.291-.16.507-.378.646-.657.142-.278.214-.598.214-.96 0-.36-.072-.679-.214-.954a1.452 1.452 0 0 0-.651-.641c-.292-.156-.668-.234-1.129-.234h-1.69V17h-1.845Zm12.152.15c-.746 0-1.392-.165-1.939-.493a3.343 3.343 0 0 1-1.273-1.377c-.298-.59-.447-1.28-.447-2.068 0-.79.15-1.48.447-2.073a3.335 3.335 0 0 1 1.273-1.383c.547-.328 1.193-.492 1.94-.492.745 0 1.391.164 1.938.492.547.329.97.79 1.268 1.383.301.593.452 1.284.452 2.073 0 .789-.15 1.478-.452 2.068a3.309 3.309 0 0 1-1.268 1.377c-.547.328-1.193.492-1.939.492Zm.01-1.443c.404 0 .742-.11 1.014-.333.272-.225.474-.527.607-.905.136-.377.204-.798.204-1.262 0-.468-.068-.89-.204-1.268a2.007 2.007 0 0 0-.607-.91c-.272-.225-.61-.338-1.014-.338-.414 0-.759.113-1.034.338a2.041 2.041 0 0 0-.612.91 3.81 3.81 0 0 0-.198 1.268c0 .464.066.885.198 1.262.136.378.34.68.612.905.275.222.62.333 1.034.333Zm8.508 1.442c-.763 0-1.417-.167-1.964-.502a3.352 3.352 0 0 1-1.258-1.387c-.292-.593-.437-1.276-.437-2.048 0-.776.149-1.46.447-2.054a3.34 3.34 0 0 1 1.263-1.392c.547-.334 1.193-.502 1.939-.502.62 0 1.168.115 1.645.343.48.226.864.546 1.149.96.285.41.447.891.487 1.441h-1.72a1.644 1.644 0 0 0-.497-.92c-.259-.248-.605-.372-1.04-.372-.367 0-.69.1-.969.298-.278.196-.495.478-.651.845-.153.368-.229.81-.229 1.323 0 .52.076.968.229 1.342.152.371.366.658.641.86.279.2.605.298.98.298.265 0 .502-.05.71-.149.213-.102.39-.25.532-.442.143-.192.24-.426.294-.701h1.72a2.999 2.999 0 0 1-.477 1.437c-.275.414-.65.739-1.124.974-.474.232-1.03.348-1.67.348Zm6.39-2.545-.006-2.173h.289l2.744-3.067h2.103l-3.376 3.758h-.372l-1.383 1.482ZM58.422 17V6.818h1.8V17h-1.8Zm4.792 0-2.485-3.475 1.213-1.268L65.368 17h-2.153Zm6.245.15c-.766 0-1.427-.16-1.984-.478a3.233 3.233 0 0 1-1.278-1.362c-.298-.59-.447-1.285-.447-2.083 0-.786.149-1.475.447-2.069a3.384 3.384 0 0 1 1.263-1.392c.54-.334 1.175-.502 1.904-.502.47 0 .915.076 1.333.229.42.149.792.381 1.113.696.325.315.58.716.766 1.203.186.484.278 1.06.278 1.73v.552h-6.259v-1.213h4.534a1.935 1.935 0 0 0-.224-.92 1.625 1.625 0 0 0-.611-.641 1.719 1.719 0 0 0-.905-.234c-.368 0-.691.09-.97.269a1.848 1.848 0 0 0-.65.696c-.153.285-.231.598-.234.94v1.058c0 .444.08.825.243 1.144.163.315.39.556.681.726.292.165.634.248 1.025.248.261 0 .498-.036.71-.11.213-.075.397-.187.552-.332.156-.146.274-.327.353-.542l1.68.189a2.62 2.62 0 0 1-.606 1.163 2.958 2.958 0 0 1-1.133.766c-.461.179-.988.268-1.581.268Zm8.731-7.786v1.392h-4.39V9.364h4.39Zm-3.306-1.83h1.8v7.17c0 .241.036.427.109.556a.59.59 0 0 0 .298.258c.123.047.259.07.408.07.113 0 .215-.008.308-.025.096-.016.17-.031.219-.045l.303 1.407c-.096.034-.233.07-.412.11-.176.04-.392.063-.647.07a2.934 2.934 0 0 1-1.218-.204 1.895 1.895 0 0 1-.86-.706c-.209-.319-.311-.716-.308-1.194V7.534Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M5 6h14M5 18h14M5 12h14"
|
||||
stroke="#fff"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function UserIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M15 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.696 19h10.608c1.175 0 2.08-.935 1.532-1.897C18.028 15.69 16.187 14 12 14s-6.028 1.689-6.836 3.103C4.616 18.065 5.521 19 6.696 19Z"
|
||||
stroke="#fff"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppScreen({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div className={clsx('flex flex-col', className)} {...props}>
|
||||
<div className="flex justify-between px-4 pt-0">
|
||||
<MenuIcon className="h-6 w-6 flex-none" />
|
||||
<Logo className="h-6 flex-none" />
|
||||
<UserIcon className="h-6 w-6 flex-none" />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
AppScreen.Header = forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
{ children: React.ReactNode }
|
||||
>(function AppScreenHeader({ children }, ref) {
|
||||
return (
|
||||
<div ref={ref} className="mt-6 px-4 text-white">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
AppScreen.Title = forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
{ children: React.ReactNode }
|
||||
>(function AppScreenTitle({ children }, ref) {
|
||||
return (
|
||||
<div ref={ref} className="text-2xl text-white">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
AppScreen.Subtitle = forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
{ children: React.ReactNode }
|
||||
>(function AppScreenSubtitle({ children }, ref) {
|
||||
return (
|
||||
<div ref={ref} className="text-sm text-gray-500">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
AppScreen.Body = forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
{ className?: string; children: React.ReactNode }
|
||||
>(function AppScreenBody({ children, className }, ref) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx('mt-6 flex-auto rounded-t-2xl bg-white', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,28 +1,440 @@
|
||||
import { Container } from '../../components/Container'
|
||||
'use client'
|
||||
|
||||
import { Fragment, useEffect, useId, useRef, useState } from 'react'
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
type MotionProps,
|
||||
type Variant,
|
||||
type Variants,
|
||||
AnimatePresence,
|
||||
motion,
|
||||
} from 'framer-motion'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
import { AppScreen } from './AppScreen'
|
||||
import {
|
||||
Eyebrow,
|
||||
FeatureDescription,
|
||||
FeatureTitle,
|
||||
MobileFeatureTitle,
|
||||
P,
|
||||
SectionHeader,
|
||||
} from '@/components/Texts'
|
||||
import { CircleBackground } from '@/components/CircleBackground'
|
||||
import { Container } from '@/components/Container'
|
||||
|
||||
import connectorImg from '@/images/connector.png'
|
||||
import peersImg from '@/images/peers.png'
|
||||
import settingImg from '@/images/setting.png'
|
||||
import { PhoneFrame } from '@/components/PhoneFrame'
|
||||
|
||||
|
||||
interface CustomAnimationProps {
|
||||
isForwards: boolean
|
||||
changeCount: number
|
||||
}
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: 'Mycelium Connector',
|
||||
description:
|
||||
"Start (and stop) your Mycelium connector to gain access to sites, apps, and workloads available exclusively on the Mycelium Network. View statistics around peers and traffic.",
|
||||
icon: DeviceUserIcon,
|
||||
screen: InviteScreen,
|
||||
},
|
||||
{
|
||||
name: 'Mycelium Peers',
|
||||
description:
|
||||
'Search and discover active peers on the Mycelium Network, or add your own.',
|
||||
icon: DeviceNotificationIcon,
|
||||
screen: StocksScreen,
|
||||
},
|
||||
{
|
||||
name: 'Network Setting',
|
||||
description:
|
||||
'Find version and network information and trigger light or dark mode.',
|
||||
icon: DeviceTouchIcon,
|
||||
screen: InvestScreen,
|
||||
},
|
||||
]
|
||||
|
||||
function DeviceUserIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true" {...props}>
|
||||
<circle cx={16} cy={16} r={16} fill="#A3A3A3" fillOpacity={0.2} />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 23a3 3 0 100-6 3 3 0 000 6zm-1 2a4 4 0 00-4 4v1a2 2 0 002 2h6a2 2 0 002-2v-1a4 4 0 00-4-4h-2z"
|
||||
fill="#737373"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 4a4 4 0 014-4h14a4 4 0 014 4v24a4.002 4.002 0 01-3.01 3.877c-.535.136-.99-.325-.99-.877s.474-.98.959-1.244A2 2 0 0025 28V4a2 2 0 00-2-2h-1.382a1 1 0 00-.894.553l-.448.894a1 1 0 01-.894.553h-6.764a1 1 0 01-.894-.553l-.448-.894A1 1 0 0010.382 2H9a2 2 0 00-2 2v24a2 2 0 001.041 1.756C8.525 30.02 9 30.448 9 31s-.455 1.013-.99.877A4.002 4.002 0 015 28V4z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DeviceNotificationIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true" {...props}>
|
||||
<circle cx={16} cy={16} r={16} fill="#A3A3A3" fillOpacity={0.2} />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 0a4 4 0 00-4 4v24a4 4 0 004 4h14a4 4 0 004-4V4a4 4 0 00-4-4H9zm0 2a2 2 0 00-2 2v24a2 2 0 002 2h14a2 2 0 002-2V4a2 2 0 00-2-2h-1.382a1 1 0 00-.894.553l-.448.894a1 1 0 01-.894.553h-6.764a1 1 0 01-.894-.553l-.448-.894A1 1 0 0010.382 2H9z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
<path
|
||||
d="M9 8a2 2 0 012-2h10a2 2 0 012 2v2a2 2 0 01-2 2H11a2 2 0 01-2-2V8z"
|
||||
fill="#737373"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DeviceTouchIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
let id = useId()
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 32 32" fill="none" aria-hidden="true" {...props}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`${id}-gradient`}
|
||||
x1={14}
|
||||
y1={14.5}
|
||||
x2={7}
|
||||
y2={17}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#737373" />
|
||||
<stop offset={1} stopColor="#D4D4D4" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx={16} cy={16} r={16} fill="#A3A3A3" fillOpacity={0.2} />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 4a4 4 0 014-4h14a4 4 0 014 4v13h-2V4a2 2 0 00-2-2h-1.382a1 1 0 00-.894.553l-.448.894a1 1 0 01-.894.553h-6.764a1 1 0 01-.894-.553l-.448-.894A1 1 0 0010.382 2H9a2 2 0 00-2 2v24a2 2 0 002 2h4v2H9a4 4 0 01-4-4V4z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
<path
|
||||
d="M7 22c0-4.694 3.5-8 8-8"
|
||||
stroke={`url(#${id}-gradient)`}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M21 20l.217-5.513a1.431 1.431 0 00-2.85-.226L17.5 21.5l-1.51-1.51a2.107 2.107 0 00-2.98 0 .024.024 0 00-.005.024l3.083 9.25A4 4 0 0019.883 32H25a4 4 0 004-4v-5a3 3 0 00-3-3h-5z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const headerAnimation: Variants = {
|
||||
initial: { opacity: 0, transition: { duration: 0.3 } },
|
||||
animate: { opacity: 1, transition: { duration: 0.3, delay: 0.3 } },
|
||||
exit: { opacity: 0, transition: { duration: 0.3 } },
|
||||
}
|
||||
|
||||
const maxZIndex = 2147483647
|
||||
|
||||
const bodyVariantBackwards: Variant = {
|
||||
opacity: 0.4,
|
||||
scale: 0.8,
|
||||
zIndex: 0,
|
||||
filter: 'blur(4px)',
|
||||
transition: { duration: 0.4 },
|
||||
}
|
||||
|
||||
|
||||
const bodyAnimation: MotionProps = {
|
||||
initial: 'initial',
|
||||
animate: 'animate',
|
||||
exit: 'exit',
|
||||
variants: {
|
||||
initial: (custom: CustomAnimationProps) => (
|
||||
custom.isForwards
|
||||
? {
|
||||
y: '100%',
|
||||
zIndex: maxZIndex - custom.changeCount,
|
||||
transition: { duration: 0.4 },
|
||||
}
|
||||
: bodyVariantBackwards
|
||||
),
|
||||
animate: (custom: CustomAnimationProps) => ({
|
||||
y: '0%',
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
zIndex: maxZIndex / 2 - custom.changeCount,
|
||||
filter: 'blur(0px)',
|
||||
transition: { duration: 0.4 },
|
||||
}),
|
||||
exit: (custom: CustomAnimationProps) => (
|
||||
custom.isForwards
|
||||
? bodyVariantBackwards
|
||||
: {
|
||||
y: '100%',
|
||||
zIndex: maxZIndex - custom.changeCount,
|
||||
transition: { duration: 0.4 },
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
function InviteScreen() {
|
||||
return (
|
||||
<AppScreen className="w-full">
|
||||
<img src={connectorImg} alt="Mycelium Connector" width="366" height="732" className="mt-[-2rem]" />
|
||||
</AppScreen>
|
||||
)
|
||||
}
|
||||
|
||||
function StocksScreen() {
|
||||
return (
|
||||
<AppScreen className="w-full">
|
||||
<img src={peersImg} alt="Mycelium Peers" width="366" height="732" className="mt-[-2rem]" />
|
||||
</AppScreen>
|
||||
)
|
||||
}
|
||||
|
||||
function InvestScreen() {
|
||||
return (
|
||||
<AppScreen className="w-full">
|
||||
<img src={settingImg} alt="Mycelium Settings" width="366" height="732" className="mt-[-2rem]" />
|
||||
</AppScreen>
|
||||
)
|
||||
}
|
||||
|
||||
function usePrevious<T>(value: T) {
|
||||
const ref = useRef<T>()
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
|
||||
return ref.current
|
||||
}
|
||||
|
||||
function FeaturesDesktop() {
|
||||
let [changeCount, setChangeCount] = useState(0)
|
||||
let [selectedIndex, setSelectedIndex] = useState(0)
|
||||
let prevIndex = usePrevious(selectedIndex)
|
||||
let isForwards = prevIndex === undefined ? true : selectedIndex > prevIndex
|
||||
|
||||
let onChange = useDebouncedCallback(
|
||||
(selectedIndex: number) => {
|
||||
setSelectedIndex(selectedIndex)
|
||||
setChangeCount((changeCount) => changeCount + 1)
|
||||
},
|
||||
100,
|
||||
{ leading: true },
|
||||
)
|
||||
|
||||
return (
|
||||
<TabGroup
|
||||
className="grid grid-cols-12 items-center gap-8 lg:gap-16"
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={onChange}
|
||||
vertical
|
||||
>
|
||||
<TabList className="z-10 order-last col-span-6 space-y-6">
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={feature.name}
|
||||
className={clsx(
|
||||
'relative rounded-2xl outline-2 transition-all duration-300 ease-in-out hover:scale-105 hover:bg-gray-800/30',
|
||||
selectedIndex === featureIndex
|
||||
? 'outline-cyan-500'
|
||||
: 'outline-transparent hover:outline-cyan-500',
|
||||
)}
|
||||
>
|
||||
{featureIndex === selectedIndex && (
|
||||
<motion.div
|
||||
layoutId="activeBackground"
|
||||
className="absolute inset-0 bg-gray-800"
|
||||
initial={{ borderRadius: 16 }}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10 p-8">
|
||||
<feature.icon className="h-8 w-8" />
|
||||
<FeatureTitle as="h3" color="white" className="mt-6">
|
||||
<Tab className="text-left data-selected:not-data-focus:outline-hidden">
|
||||
<span className="absolute inset-0 rounded-2xl" />
|
||||
{feature.name}
|
||||
</Tab>
|
||||
</FeatureTitle>
|
||||
<FeatureDescription color="secondary" className="mt-2">
|
||||
{feature.description}
|
||||
</FeatureDescription>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabList>
|
||||
<div className="relative col-span-6">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<CircleBackground id="primaryfeatures_desktop_circle" color="#13B5C8" className="animate-spin-slower" />
|
||||
</div>
|
||||
<PhoneFrame className="z-10 mx-auto w-full max-w-[366px]">
|
||||
<TabPanels as={Fragment}>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
custom={{ isForwards, changeCount }}
|
||||
>
|
||||
{features.map((feature, featureIndex) =>
|
||||
selectedIndex === featureIndex ? (
|
||||
<TabPanel
|
||||
static
|
||||
key={feature.name + changeCount}
|
||||
className="col-start-1 row-start-1 flex focus:outline-offset-32 data-selected:not-data-focus:outline-hidden"
|
||||
>
|
||||
<motion.div {...bodyAnimation} custom={{ isForwards, changeCount }}>
|
||||
<feature.screen />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
) : null,
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</TabPanels>
|
||||
</PhoneFrame>
|
||||
</div>
|
||||
</TabGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturesMobile() {
|
||||
let [activeIndex, setActiveIndex] = useState(0)
|
||||
let slideContainerRef = useRef<React.ElementRef<'div'>>(null)
|
||||
let slideRefs = useRef<Array<React.ElementRef<'div'>>>([])
|
||||
|
||||
useEffect(() => {
|
||||
let observer = new window.IntersectionObserver(
|
||||
(entries) => {
|
||||
for (let entry of entries) {
|
||||
if (entry.isIntersecting && entry.target instanceof HTMLDivElement) {
|
||||
setActiveIndex(slideRefs.current.indexOf(entry.target))
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: slideContainerRef.current,
|
||||
threshold: 0.6,
|
||||
},
|
||||
)
|
||||
|
||||
for (let slide of slideRefs.current) {
|
||||
if (slide) {
|
||||
observer.observe(slide)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [slideContainerRef, slideRefs])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={slideContainerRef}
|
||||
className="-mb-4 flex snap-x snap-mandatory -space-x-4 overflow-x-auto overscroll-x-contain scroll-smooth pb-4 [scrollbar-width:none] sm:-space-x-6 [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
ref={(ref) => ref && (slideRefs.current[featureIndex] = ref)}
|
||||
className="w-full flex-none snap-center px-4 sm:px-6 transition-all duration-300 ease-in-out hover:scale-105"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative transform overflow-hidden rounded-2xl bg-gray-800 px-5 py-6 outline-2 transition-colors',
|
||||
activeIndex === featureIndex
|
||||
? 'outline-transparent' // Remove outline for active mobile slide
|
||||
: 'outline-transparent hover:outline-cyan-500',
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<CircleBackground
|
||||
id={`primaryfeatures_mobile_circle_${featureIndex}`}
|
||||
color="#13B5C8"
|
||||
className={featureIndex % 2 === 1 ? 'rotate-180' : undefined}
|
||||
/>
|
||||
</div>
|
||||
<PhoneFrame className="relative mx-auto w-full max-w-[366px]">
|
||||
<feature.screen />
|
||||
</PhoneFrame>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gray-800/95 p-6 backdrop-blur-sm sm:p-10">
|
||||
<feature.icon className="h-8 w-8" />
|
||||
<MobileFeatureTitle color="white" className="mt-6">
|
||||
{feature.name}
|
||||
</MobileFeatureTitle>
|
||||
<FeatureDescription color="secondary" className="mt-2">
|
||||
{feature.description}
|
||||
</FeatureDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
{features.map((_, featureIndex) => (
|
||||
<button
|
||||
type="button"
|
||||
key={featureIndex}
|
||||
className={clsx(
|
||||
'relative h-0.5 w-4 rounded-full',
|
||||
featureIndex === activeIndex ? 'bg-gray-300' : 'bg-gray-500',
|
||||
)}
|
||||
aria-label={`Go to slide ${featureIndex + 1}`}
|
||||
onClick={() => {
|
||||
slideRefs.current[featureIndex].scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className="absolute -inset-x-1.5 -inset-y-3" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function PrimaryFeatures() {
|
||||
return (
|
||||
<section
|
||||
id="howitworks"
|
||||
aria-label="How Mycelium works"
|
||||
aria-label="Features for investing all your money"
|
||||
className="bg-gray-900 py-20 sm:py-32"
|
||||
>
|
||||
<Container>
|
||||
<div className="mx-auto max-w-2xl lg:mx-0 lg:max-w-3xl">
|
||||
<h2 className="text-base/7 font-semibold text-cyan-500">How It Works</h2>
|
||||
<p className="text-3xl lg:text-4xl font-medium tracking-tight text-white">
|
||||
<Eyebrow color="accent">How It Works</Eyebrow>
|
||||
<SectionHeader color="white" className="mt-2">
|
||||
How Mycelium Operates
|
||||
</p>
|
||||
<p className="mt-6 text-lg text-gray-300">
|
||||
Mycelium, like its natural namesake, thrives on decentralization, efficiency, and security, making it a truly powerful force in the world of decentralized networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-16 text-center">
|
||||
<p className="text-lg text-gray-400">
|
||||
Interactive features demonstration coming soon...
|
||||
</p>
|
||||
</SectionHeader>
|
||||
<P color="light" className="mt-6">
|
||||
Mycelium, like its natural namesake, thrives on decentralization,
|
||||
efficiency, and security, making it a truly powerful force in the world
|
||||
of decentralized networks.
|
||||
</P>
|
||||
</div>
|
||||
</Container>
|
||||
<div className="mt-16 md:hidden">
|
||||
<FeaturesMobile />
|
||||
</div>
|
||||
<Container className="hidden md:mt-20 md:block">
|
||||
<FeaturesDesktop />
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'node:path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user