first
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
After Width: | Height: | Size: 735 KiB |
After Width: | Height: | Size: 241 KiB |
After Width: | Height: | Size: 253 KiB |
After Width: | Height: | Size: 664 KiB |
After Width: | Height: | Size: 333 KiB |
After Width: | Height: | Size: 339 KiB |
After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 113 KiB |
After Width: | Height: | Size: 356 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 324 KiB |
After Width: | Height: | Size: 247 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 289 KiB |
After Width: | Height: | Size: 774 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 201 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 545 KiB |
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "healthy-oil",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/cli": "^3.12.0",
|
||||||
|
"@chakra-ui/react": "^3.12.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"add": "^2.0.6",
|
||||||
|
"color-mode": "^0.1.0",
|
||||||
|
"framer-motion": "^12.5.0",
|
||||||
|
"motion": "^12.5.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"snippet": "^0.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,42 @@
|
||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
SimpleGrid,
|
||||||
|
Center,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
|
||||||
|
import Header from './components/header'
|
||||||
|
import Hero1 from './components/hero1'
|
||||||
|
import Hero2 from './components/hero2'
|
||||||
|
import Compare from './components/compare'
|
||||||
|
import Qa from './components/qa'
|
||||||
|
import Oil_info from './components/oil_info'
|
||||||
|
import Bestoil from './components/bestoil'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container maxW="100vw" p={0} m={0}>
|
||||||
|
<Header />
|
||||||
|
{/* Hero Section */}
|
||||||
|
<Box position="relative">
|
||||||
|
<Hero1 />
|
||||||
|
<Box mt={{ base: "-5vw", sm: "-5vw", md: "-5vw", lg: "-5vw", xl: "-5vw" }}>
|
||||||
|
<Hero2 />
|
||||||
|
</Box>
|
||||||
|
<Compare />
|
||||||
|
{/* Q&A Section */}
|
||||||
|
<Box mt={{ base: "-15vw", sm: "-15vw", md: "0vw", lg: "0vw", xl: "0vw" }}>
|
||||||
|
<Qa />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Oil_info />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Bestoil />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* Info Section */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Box bg="#7BC142" color="white" py={6} w="full">
|
||||||
|
<Box maxW="100%" px={0} mx={0} textAlign="center">
|
||||||
|
<Text mb={2}>服務專線:29437810</Text>
|
||||||
|
<Box w="150px" h="40px" bg="gray.300" borderRadius="md" mx="auto" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
|
@ -0,0 +1,78 @@
|
||||||
|
:root {
|
||||||
|
/* Primary colors */
|
||||||
|
--color-primary-50: #e5f4ea;
|
||||||
|
--color-primary-100: #c1e3cd;
|
||||||
|
--color-primary-200: #9cd2ae;
|
||||||
|
--color-primary-300: #78c18f;
|
||||||
|
--color-primary-400: #54b070;
|
||||||
|
--color-primary-500: #007934; /* Main primary color */
|
||||||
|
--color-primary-600: #006e2f;
|
||||||
|
--color-primary-700: #005e28;
|
||||||
|
--color-primary-800: #004f22;
|
||||||
|
--color-primary-900: #00401b;
|
||||||
|
|
||||||
|
/* Secondary colors (yellow) */
|
||||||
|
--color-secondary-50: #fefae8;
|
||||||
|
--color-secondary-100: #fcf3c4;
|
||||||
|
--color-secondary-200: #fbec9f;
|
||||||
|
--color-secondary-300: #f9e57a;
|
||||||
|
--color-secondary-400: #f8df55;
|
||||||
|
--color-secondary-500: #F7E8A5; /* Main secondary color */
|
||||||
|
--color-secondary-600: #e0d294;
|
||||||
|
--color-secondary-700: #c9bc84;
|
||||||
|
--color-secondary-800: #b2a773;
|
||||||
|
--color-secondary-900: #9c9162;
|
||||||
|
|
||||||
|
/* Accent colors */
|
||||||
|
--color-accent-green: #7BC142;
|
||||||
|
--color-accent-blue: #E3F2FD;
|
||||||
|
--color-accent-orange: #FFF3E0;
|
||||||
|
--color-accent-purple: #F3E5F5;
|
||||||
|
--color-accent-red: #FFEBEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes for backgrounds */
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--color-primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: var(--color-secondary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-green {
|
||||||
|
background-color: var(--color-accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-blue {
|
||||||
|
background-color: var(--color-accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-orange {
|
||||||
|
background-color: var(--color-accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-purple {
|
||||||
|
background-color: var(--color-accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-red {
|
||||||
|
background-color: var(--color-accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes for text colors */
|
||||||
|
.text-primary {
|
||||||
|
color: var(--color-primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--color-secondary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-accent-green {
|
||||||
|
color: var(--color-accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-normalGreen{
|
||||||
|
color: "#075C39",
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const colors = {
|
||||||
|
textColor: '#075C39',
|
||||||
|
backgroundColor: '#FFFBCE',
|
||||||
|
topBarColor: '#92C000',
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { Box, Stack, Image, Flex, useBreakpointValue, Text, SimpleGrid } from '@chakra-ui/react'
|
||||||
|
import { colors } from '../colors';
|
||||||
|
|
||||||
|
function Bestoil() {
|
||||||
|
const cook = useBreakpointValue({
|
||||||
|
base: "/images/cook_mb.png",
|
||||||
|
sm: "/images/cook_mb.png",
|
||||||
|
md: "/images/cook_pc.png",
|
||||||
|
|
||||||
|
});
|
||||||
|
const oilCube = [
|
||||||
|
{
|
||||||
|
bgColor: 'linear-gradient(to right,#00609E ,#008FD0,#00609E )',
|
||||||
|
title: '米糠油',
|
||||||
|
text: '含天然谷維素,具抗氧化作用,有助維持血管健康'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: 'linear-gradient(to right,#D44E2D ,#EAA72E,#D44E2D )',
|
||||||
|
title: '芥花籽油',
|
||||||
|
text: '富含多元不飽和脂肪Omega-3、6,有助降低心血管疾病風險'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: 'linear-gradient(to right,#7DAE3A ,#AFC226,#7DAE3A )',
|
||||||
|
title: '橄欖油',
|
||||||
|
text: '富含單元不飽和脂肪酸Omega-9,有助保護心臟健康'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: 'linear-gradient(to right,#0098D2 ,#00AFE6,#0098D2 )',
|
||||||
|
title: '亞麻籽油',
|
||||||
|
text: '是亞麻酸(Omega-3) 的良好來源,有助維持細胞健康'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: 'linear-gradient(to right,#D23431 ,#E28C24,#D23431 )',
|
||||||
|
title: '高油酸葵花籽油',
|
||||||
|
text: '低飽和脂肪,穩定性高,適合高溫烹調'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const formatText = (text: string) => {
|
||||||
|
return text.split('\n').map((line, i) => (
|
||||||
|
<Box key={i}
|
||||||
|
color={'white'}
|
||||||
|
fontSize='9px'
|
||||||
|
mt={-2}
|
||||||
|
className='font-noto-sans font-regular'
|
||||||
|
>
|
||||||
|
{line.split('\t').map((segment, j) => (
|
||||||
|
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||||
|
))}
|
||||||
|
{i < text.split('\n').length - 1 && <br />}
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
position="relative"
|
||||||
|
w="100%"
|
||||||
|
overflow="hidden"
|
||||||
|
|
||||||
|
bgColor={colors.backgroundColor}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
w='100%'
|
||||||
|
justify={"center"}
|
||||||
|
>
|
||||||
|
<Image src="/images/best5.png"
|
||||||
|
fit='contain'
|
||||||
|
width={'500px'} />
|
||||||
|
<Image src="/images/oilchart.png"
|
||||||
|
fit='contain'
|
||||||
|
width={'500px'} />
|
||||||
|
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
w='100%'
|
||||||
|
align={"center"}
|
||||||
|
|
||||||
|
>
|
||||||
|
<Image src={cook}
|
||||||
|
w={{ base: "100%", sm: "100%", md: '600px' }}
|
||||||
|
fit='contain'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
w={{ base: "90%", sm: "90%", md: '600px' }}
|
||||||
|
fontSize={'lg'}
|
||||||
|
className='NotoSansCJKtc font-regular'
|
||||||
|
color={colors.textColor}
|
||||||
|
textAlign={"center"}>
|
||||||
|
{"不同食油的脂肪酸組合各有優勢,而長期使用單一油種則可能令營養失衡。營萃護心油融合5種優質食油,發揮更全面的健康效益:"}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
|
||||||
|
<Box width="100%" display="flex" flexDirection="column" alignItems="center" mt={5}>
|
||||||
|
<Flex flexWrap="wrap" justifyContent="center" gap="15px" maxWidth="945px">
|
||||||
|
{oilCube.map((item, index) => (
|
||||||
|
<Stack
|
||||||
|
key={index}
|
||||||
|
w='270px'
|
||||||
|
h='90px'
|
||||||
|
bgImage={item.bgColor}
|
||||||
|
roundedTopLeft={'30px'}
|
||||||
|
roundedBottomRight={'30px'}
|
||||||
|
p={4}
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
|
||||||
|
color="white"
|
||||||
|
className='font-melle font-black'
|
||||||
|
fontSize="2xl"
|
||||||
|
mb={-1}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
|
||||||
|
color={colors.backgroundColor}
|
||||||
|
className='font-melle font-medium'
|
||||||
|
mt={-1}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bestoil
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Box, Stack, Image, Text, Flex, SimpleGrid } from '@chakra-ui/react'
|
||||||
|
import { colors } from '../colors';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { motion, useInView } from 'framer-motion';
|
||||||
|
|
||||||
|
// Create motion versions of Chakra components
|
||||||
|
const MotionStack = motion(Stack);
|
||||||
|
const MotionImage = motion(Image);
|
||||||
|
const MotionBox = motion(Box);
|
||||||
|
|
||||||
|
function Compare() {
|
||||||
|
const compareRef = useRef(null);
|
||||||
|
const isInView = useInView(compareRef, { once: true, amount: 0.3 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={compareRef}
|
||||||
|
w="100%"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<SimpleGrid columns={2}
|
||||||
|
maxW="full"
|
||||||
|
h={{ base: '140vw', sm: '140vw', md: '120vw', lg: '50vw', xl: '55vw' }}
|
||||||
|
mb="20px" // Add 20px bottom margin
|
||||||
|
position="relative"
|
||||||
|
zIndex="1"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
position="relative"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
bgImage={`
|
||||||
|
linear-gradient(to bottom,
|
||||||
|
rgba(255, 251, 210, 1) 4%,
|
||||||
|
rgba(255, 251, 210, 0.1) 10%),
|
||||||
|
url(/images/bgyellow.jpg)
|
||||||
|
`}
|
||||||
|
bgSize="cover"
|
||||||
|
bgPos="center"
|
||||||
|
|
||||||
|
>
|
||||||
|
<Box bgColor="rgba(255, 251, 210, 0.4)" w="100%" h="100%" />
|
||||||
|
{/* Content container with z-index to appear above the background */}
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
bgImage="linear-gradient(to bottom, #FFFBD2 4%, #E1E1E2 10%)"
|
||||||
|
>
|
||||||
|
{/* Content here */}
|
||||||
|
<MotionImage
|
||||||
|
src='/images/conpareheart.png'
|
||||||
|
width={{
|
||||||
|
base: '95%', sm: '90% ', md: '80% ', lg: '35% '
|
||||||
|
}}
|
||||||
|
position="absolute"
|
||||||
|
top="0%"
|
||||||
|
left="50%"
|
||||||
|
transform="translateX(-51%)"
|
||||||
|
zIndex="1"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 1.5 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* Use a larger negative margin to pull the flex up into the grid area */}
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
width="100%"
|
||||||
|
mt={{ base: '-25vw', sm: '-25vw', md: '-20vw', lg: '-10vw' }}
|
||||||
|
position="relative"
|
||||||
|
zIndex="2"
|
||||||
|
>
|
||||||
|
<MotionImage
|
||||||
|
src="/images/bigheart.png"
|
||||||
|
width={{
|
||||||
|
base: '100%%', sm: '95% ', md: '80% ', lg: '35% '
|
||||||
|
}}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 1.5 }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Compare;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Box, Flex, Image, Text, } from '@chakra-ui/react';
|
||||||
|
import {colors} from '../colors';
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<Box bg={colors.topBarColor} py={4} w="full">
|
||||||
|
<Flex alignItems={'center'} justifyContent={'center'} direction="column">
|
||||||
|
<Image src="/images/headerlogo.png" alt="Logo" width="150px" />
|
||||||
|
<Text textAlign={'center'} marginLeft={3} marginTop={2} className="font-melle font-medium" color={'#075C39'}>
|
||||||
|
{"【積極求變 健康向前】"}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { Box, Stack, SimpleGrid, Image, Image as ChakraImage, Flex, useBreakpointValue } from '@chakra-ui/react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
function Hero1() {
|
||||||
|
const MotionImage = motion(ChakraImage);
|
||||||
|
const oilImage = useBreakpointValue({
|
||||||
|
base: "/images/oilmobile.png",
|
||||||
|
sm: "/images/oil.png",
|
||||||
|
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
w="100%"
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent={"center"}
|
||||||
|
overflow="hidden"
|
||||||
|
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
overflow="hidden"
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
//top={{ base: "-3%", sm: "-3%" }}
|
||||||
|
left="0"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
pointerEvents="none"
|
||||||
|
zIndex="1"
|
||||||
|
>
|
||||||
|
<MotionImage
|
||||||
|
src={oilImage}
|
||||||
|
objectFit="contain"
|
||||||
|
marginTop={{ base: "30vw", sm: "5vw", md: "5vw", lg: "0vw", xl: "0vw" }}
|
||||||
|
marginRight={{ base: "20vw", sm: "0vw", md: "0vw", lg: "0vw", xl: "0vw" }}
|
||||||
|
height={{ base: "85vw", sm: "45vw", md: "32vw", lg: "25vw", xl: "25vw" }}
|
||||||
|
maxW="100%" // Prevents the image from being too wide
|
||||||
|
// Adjust vertical position as needed
|
||||||
|
pointerEvents="none"
|
||||||
|
initial={{ opacity: 0, x: -100 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Stack gap={0} >
|
||||||
|
<Box
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
w="100%"
|
||||||
|
bgImage={"url('/images/background.png')"}
|
||||||
|
bgSize="cover"
|
||||||
|
backgroundPosition="center"
|
||||||
|
bgRepeat="no-repeat"
|
||||||
|
h={{ base: "125vw", sm: "70vw", md: "50vw", lg: "40vw", xl: "35vw" }}
|
||||||
|
|
||||||
|
>
|
||||||
|
<SimpleGrid columns={{ base: 1, sm: 2, md: 2, lg: 2, xl: 2 }} marginTop={10} >
|
||||||
|
<Flex
|
||||||
|
justify={{ base: "center", sm: "flex-end", md: "flex-end", lg: "flex-end", xl: "flex-end" }}
|
||||||
|
align={"flex-start"}
|
||||||
|
h="fit-content" // or a specific height
|
||||||
|
alignSelf="flex-start" // This constrains the Flex to its content height
|
||||||
|
>
|
||||||
|
<MotionImage src="/images/text1.png"
|
||||||
|
width={{ base: "90%", sm: "80%", md: "70%", lg: "70%", xl: "60%" }}
|
||||||
|
fit="contain"
|
||||||
|
h="auto"
|
||||||
|
marginRight={{ sm: "-10%", md: "-10%", lg: "-10%", xl: "-10%" }}
|
||||||
|
marginTop={{ base: 0, sm: 5, md: 5, lg: 5, xl: 5 }}
|
||||||
|
initial={{ opacity: 0, x: -100 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }} />
|
||||||
|
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
|
||||||
|
overflow="hidden"
|
||||||
|
h="auto"
|
||||||
|
alignItems={{ base: "flex-end", sm: "flex-start", md: "flex-start", lg: "flex-start", xl: "flex-start" }}
|
||||||
|
justifyContent={{ base: "center", sm: "flex-start", md: "flex-start", lg: "flex-start", xl: "flex-start" }}
|
||||||
|
>
|
||||||
|
<MotionImage
|
||||||
|
src="/images/people2.png"
|
||||||
|
h={{ base: "120vw", sm: "70vw", md: "50vw", lg: "50vw", xl: "50vw" }}
|
||||||
|
// maxH={"50%"}
|
||||||
|
marginRight={{ base: "-30%", sm: "0", md: "0", lg: "0", xl: "0" }}
|
||||||
|
objectFit="cover"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
h={{ base: "30vw", sm: "30vw", md: "30vw", lg: "30vw", xl: "20vw" }}
|
||||||
|
w="100%"
|
||||||
|
bgImage={"url('/images/woodtable.png')"}
|
||||||
|
bgSize="cover"
|
||||||
|
backgroundPosition="center"
|
||||||
|
justifyContent={"center"}
|
||||||
|
alignSelf={"center"}
|
||||||
|
bgRepeat="no-repeat"
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
} export default Hero1;
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { Box, Stack, Image, Text, Flex, SimpleGrid } from '@chakra-ui/react'
|
||||||
|
import { colors } from '../colors';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { motion, useInView, useAnimation } from 'framer-motion'
|
||||||
|
|
||||||
|
// Create motion components from Chakra UI components
|
||||||
|
const MotionBox = motion(Box);
|
||||||
|
const MotionFlex = motion(Flex);
|
||||||
|
const MotionImage = motion(Image);
|
||||||
|
const MotionText = motion(Text);
|
||||||
|
const MotionStack = motion(Stack);
|
||||||
|
|
||||||
|
function Hero2() {
|
||||||
|
const textStackRef = useRef<HTMLDivElement>(null);
|
||||||
|
const imageStackRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [textStackHeight, setTextStackHeight] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Create refs and animation controls for scroll-based animations
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isInView = useInView(containerRef, { once: true, amount: 0.2 });
|
||||||
|
const textControls = useAnimation();
|
||||||
|
const imageControls = useAnimation();
|
||||||
|
|
||||||
|
// Trigger animations when component comes into view
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInView) {
|
||||||
|
textControls.start("visible");
|
||||||
|
imageControls.start("visible");
|
||||||
|
} else {
|
||||||
|
textControls.start("hidden");
|
||||||
|
imageControls.start("hidden");
|
||||||
|
}
|
||||||
|
}, [isInView, textControls, imageControls]);
|
||||||
|
|
||||||
|
// Add this useEffect to measure and update heights
|
||||||
|
useEffect(() => {
|
||||||
|
const updateHeight = () => {
|
||||||
|
if (textStackRef.current) {
|
||||||
|
const height = textStackRef.current.offsetHeight;
|
||||||
|
setTextStackHeight(height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial measurement
|
||||||
|
updateHeight();
|
||||||
|
|
||||||
|
// Setup resize observer for responsive adjustments
|
||||||
|
const resizeObserver = new ResizeObserver(updateHeight);
|
||||||
|
if (textStackRef.current) {
|
||||||
|
resizeObserver.observe(textStackRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (textStackRef.current) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={containerRef}
|
||||||
|
w="100%"
|
||||||
|
position="relative"
|
||||||
|
zIndex="1"
|
||||||
|
bgColor={colors.backgroundColor}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
justify={"flex-start"}
|
||||||
|
align={"center"}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/images/title1.png"
|
||||||
|
w={{base:"420px",sm:"450px",md:"450px",lg:"350px",xl:"350px"}}
|
||||||
|
maxW={"95%"}
|
||||||
|
/>
|
||||||
|
<SimpleGrid
|
||||||
|
gap="25px"
|
||||||
|
columns={{ base: 1, md: 1, lg: 2, xl: 2 }}
|
||||||
|
px="4"
|
||||||
|
maxW="full"
|
||||||
|
mx="0"
|
||||||
|
marginTop={{ base: -7, md: -3, lg: 0, xl: 0 }}
|
||||||
|
height={{ base: "auto", md: "auto", lg: "auto", xl: "25vw", '2xl': "15vw" }}
|
||||||
|
>
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<Stack
|
||||||
|
id='image_stack'
|
||||||
|
ref={imageStackRef}
|
||||||
|
alignItems='flex-end'
|
||||||
|
display={{ base: "none", md: "none", lg: "flex" }}
|
||||||
|
w={{ md: "80%", lg: "70%", xl: "60%" }}
|
||||||
|
h={"auto"}
|
||||||
|
>
|
||||||
|
<MotionImage
|
||||||
|
src="/images/oldman.jpg"
|
||||||
|
rounded="4xl"
|
||||||
|
marginTop={'10px'}
|
||||||
|
height={{ md: "100%", lg: "85%", xl: "65%" }}
|
||||||
|
fit="cover"
|
||||||
|
objectPosition="center"
|
||||||
|
variants={{
|
||||||
|
hidden: { scale: 0.95, opacity: 0 },
|
||||||
|
visible: { scale: 1, opacity: 1 }
|
||||||
|
}}
|
||||||
|
initial="hidden"
|
||||||
|
animate={imageControls}
|
||||||
|
transition={{ duration: 0.7, delay: 0.4 }}
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex justify={{ sm: 'center', md: "center", lg: "flex-start" }}>
|
||||||
|
<Stack
|
||||||
|
w={{ sm: "100%", md: "80%", lg: "90%", xl: "60%" }}
|
||||||
|
h="auto"
|
||||||
|
id="text_stack"
|
||||||
|
ref={textStackRef}
|
||||||
|
align={{ sm: 'flex-start', md: "flex-start", lg: "flex-start" }}
|
||||||
|
>
|
||||||
|
<Flex direction={"column"}>
|
||||||
|
<MotionText
|
||||||
|
color={colors.textColor}
|
||||||
|
className="font-melle font-medium"
|
||||||
|
fontSize={{ base: "2xl", sm: "3xl", md: "3xl", lg: "3xl" }}
|
||||||
|
variants={{
|
||||||
|
hidden: { x: -10, opacity: 0 },
|
||||||
|
visible: { x: 0, opacity: 1 }
|
||||||
|
}}
|
||||||
|
initial="hidden"
|
||||||
|
animate={textControls}
|
||||||
|
transition={{ duration: 0.5, delay: 0.6 }}
|
||||||
|
>
|
||||||
|
<strong>外出用餐</strong>難控用油,
|
||||||
|
</MotionText>
|
||||||
|
<MotionText
|
||||||
|
color={colors.textColor}
|
||||||
|
className="font-melle font-medium"
|
||||||
|
fontSize={{ base: "2xl", sm: "3xl", md: "3xl", lg: "3xl" }}
|
||||||
|
marginTop={-3}
|
||||||
|
variants={{
|
||||||
|
hidden: { x: -10, opacity: 0 },
|
||||||
|
visible: { x: 0, opacity: 1 }
|
||||||
|
}}
|
||||||
|
initial="hidden"
|
||||||
|
animate={textControls}
|
||||||
|
transition={{ duration: 0.5, delay: 0.7 }}
|
||||||
|
>
|
||||||
|
不健康油脂或增<strong>「三高」</strong>風險
|
||||||
|
</MotionText>
|
||||||
|
</Flex>
|
||||||
|
<MotionImage
|
||||||
|
src="/images/oldman.jpg"
|
||||||
|
rounded="4xl"
|
||||||
|
marginTop={'3px'}
|
||||||
|
h={{ md: "90%", lg: "65%", xl: "65%" }}
|
||||||
|
display={{ base: "flex", md: "flex", lg: "none" }}
|
||||||
|
fit="cover"
|
||||||
|
objectPosition="center"
|
||||||
|
variants={{
|
||||||
|
hidden: { scale: 0.95, opacity: 0 },
|
||||||
|
visible: { scale: 1, opacity: 1 }
|
||||||
|
}}
|
||||||
|
initial="hidden"
|
||||||
|
animate={imageControls}
|
||||||
|
transition={{ duration: 0.7, delay: 0.4 }}
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MotionText
|
||||||
|
color={colors.textColor}
|
||||||
|
className="font-noto-sans font-bold"
|
||||||
|
lineHeight={2}
|
||||||
|
variants={{
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
visible: { y: 0, opacity: 1 }
|
||||||
|
}}
|
||||||
|
initial="hidden"
|
||||||
|
animate={textControls}
|
||||||
|
transition={{ duration: 0.8, delay: 0.8 }}
|
||||||
|
>
|
||||||
|
{"都市人難免外出用餐,即使精選食材,也無法控制下鍋的食油,一天吃進多少隱形壞油難以估算。在家煮食雖可自選用油,但你確定自己用的是「好油」嗎?長期攝取高飽和脂肪、反式脂肪(如牛油、豬油、加工食品、重複使用的煎炸油),會提升壞膽固醇,導致血管堵塞,增加高血壓、高血脂及心血管疾病風險。"}
|
||||||
|
</MotionText>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Hero2;
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { Box, Stack, Image, Flex, useBreakpointValue, Text } from '@chakra-ui/react'
|
||||||
|
import { motion, useInView, useAnimation } from 'framer-motion'
|
||||||
|
import { useRef, useEffect } from 'react'
|
||||||
|
const MotionFlex = motion(Flex);
|
||||||
|
function Oil_info() {
|
||||||
|
const oilinfotitle = useBreakpointValue({
|
||||||
|
base: "/images/mboilinfotitle.png",
|
||||||
|
sm: "/images/mboilinfotitle.png",
|
||||||
|
md: "/images/oilinfotitle.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
const oilinfogroup = useBreakpointValue({
|
||||||
|
base: "/images/mboilinfogroup.png",
|
||||||
|
sm: "/images/mboilinfogroup.png",
|
||||||
|
md: "/images/pcoilinfogroup.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation controls for the header section
|
||||||
|
const headerControls = useAnimation();
|
||||||
|
const headerRef = useRef(null);
|
||||||
|
const headerInView = useInView(headerRef, { once: true, amount: 0.3 });
|
||||||
|
|
||||||
|
// Animation controls for the footer group image
|
||||||
|
const footerControls = useAnimation();
|
||||||
|
const footerRef = useRef(null);
|
||||||
|
const footerInView = useInView(footerRef, { once: true, amount: 0.3 });
|
||||||
|
|
||||||
|
// Trigger animations when elements come into view
|
||||||
|
useEffect(() => {
|
||||||
|
if (headerInView) {
|
||||||
|
headerControls.start({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: { duration: 0.8, ease: "easeOut" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [headerInView, headerControls]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (footerInView) {
|
||||||
|
footerControls.start({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: { duration: 0.8, ease: "easeOut" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [footerInView, footerControls]);
|
||||||
|
|
||||||
|
const formatText = (text: string) => {
|
||||||
|
return text.split('\n').map((line, i) => (
|
||||||
|
<Box key={i}
|
||||||
|
color={'white'}
|
||||||
|
fontSize='9px'
|
||||||
|
mt={-2}
|
||||||
|
className='font-noto-sans font-regular'
|
||||||
|
>
|
||||||
|
{line.split('\t').map((segment, j) => (
|
||||||
|
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||||
|
))}
|
||||||
|
{i < text.split('\n').length - 1 && <br />}
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
position="relative"
|
||||||
|
w="100%"
|
||||||
|
overflow="hidden"
|
||||||
|
bgColor={'#4E8C34'}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<MotionFlex
|
||||||
|
as={motion.div}
|
||||||
|
ref={headerRef}
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={headerControls}
|
||||||
|
direction="column"
|
||||||
|
bgImage={"url('/images/oilinfobg.png')"}
|
||||||
|
bgSize="cover"
|
||||||
|
backgroundPosition="center"
|
||||||
|
bgRepeat="no-repeat"
|
||||||
|
w='100%'
|
||||||
|
h='300px'
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
mt={{ base: 10, sm: 10, md: 10 }}
|
||||||
|
justify={"center"}
|
||||||
|
align={"center"}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
w={{ base: '75%', sm: '75%', md: '300px' }}
|
||||||
|
h='auto'
|
||||||
|
alignItems={"center"}
|
||||||
|
justify={"center"}
|
||||||
|
align={"center"}
|
||||||
|
>
|
||||||
|
<Image src="/images/oilinfoheadertitle.png"
|
||||||
|
fit='contain'
|
||||||
|
w='100%'
|
||||||
|
h='auto'
|
||||||
|
></Image>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
display={{ base: 'none', sm: 'none', md: 'flex' }}
|
||||||
|
w='500px'
|
||||||
|
align={"flex-end"}
|
||||||
|
justify={"center"}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color='white'
|
||||||
|
className='NotoSansCJKtc font-regular'
|
||||||
|
mb={10}
|
||||||
|
lineHeight={1.5}
|
||||||
|
fontSize={'lg'}
|
||||||
|
>
|
||||||
|
{"市場上食油選擇繁多,卻缺少一款真正符合人體對脂肪酸需求的「黃金比例」食油。長期依賴單一油種(如橄欖油、葵花籽油),可能導致脂肪酸及 Omega-3、6、9 攝取失衡。研究顯示,當人體吸收過多Omega-6,發炎風險可增 3 倍,無形中加重心血管負擔。"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
</MotionFlex>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Rest of the component remains the same until the footer image */}
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
display={{ base: 'flex', sm: 'flex', md: 'none' }}
|
||||||
|
mt={{ base: "-40px", sm: 0, md: 0 }}
|
||||||
|
w="100%"
|
||||||
|
align={"center"}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
w="80%"
|
||||||
|
color='white'
|
||||||
|
className='NotoSansCJKtc font-regular'
|
||||||
|
lineHeight={1.5}
|
||||||
|
fontSize={'lg'}
|
||||||
|
>
|
||||||
|
{"市場上食油選擇繁多,卻缺少一款真正符合人體對脂肪酸需求的「黃金比例」食油。長期依賴單一油種(如橄欖油、葵花籽油),可能導致脂肪酸及 Omega-3、6、9 攝取失衡。研究顯示,當人體吸收過多Omega-6,發炎風險可增 3 倍,無形中加重心血管負擔。"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
w='100%'
|
||||||
|
gap={0}
|
||||||
|
align='center'
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
w={{ base: '80%', sm: '80%', md: "100%" }}
|
||||||
|
justify={{ base: 'flex-start', sm: 'flex-start', md: 'center' }}
|
||||||
|
>
|
||||||
|
<Image src={oilinfotitle}
|
||||||
|
w='400px'
|
||||||
|
mt={{ base: 2, sm: 2, md: -12 }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
mt={{ base: 0, sm: 0, md: "-60px" }}
|
||||||
|
direction={{ base: 'column', sm: 'column', md: 'row' }}
|
||||||
|
gap={3}
|
||||||
|
align={"center"}
|
||||||
|
>
|
||||||
|
<Image src="/images/oilinfooil.png"
|
||||||
|
w={{ base: '70%', sm: '80%', md: '350px' }}
|
||||||
|
/>
|
||||||
|
<Stack
|
||||||
|
justify={"center"}
|
||||||
|
w={{ base: '80%', sm: '80%', md: '500px' }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
display={{ base: 'flex', sm: 'flex', md: 'none' }}
|
||||||
|
>
|
||||||
|
{formatText(`*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||||
|
^根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算`)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
color='white'
|
||||||
|
className='NotoSansCJKtc font-medium'
|
||||||
|
mb={10}
|
||||||
|
lineHeight={1.5}
|
||||||
|
fontSize={'lg'}
|
||||||
|
>
|
||||||
|
<Box as="span" className='NotoSansCJKtc font-xbold' display="inline">世界衛生組織建議,</Box>
|
||||||
|
{"人體應攝取良好的脂肪酸比例,這可由食油中攝取,從而調節膽固醇,保護心血管健康。"}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
color='white'
|
||||||
|
className='NotoSansCJKtc font-medium'
|
||||||
|
mb={10}
|
||||||
|
lineHeight={1.5}
|
||||||
|
fontSize={'lg'}
|
||||||
|
>
|
||||||
|
{"獅球嘜「營萃護心油」採用黃金比例調配,讓每一滴都成為心血管的隱形防護網。"}
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
display={{ base: 'none', sm: 'none', md: 'flex' }}
|
||||||
|
mt={-5}>
|
||||||
|
{formatText(`*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||||
|
^根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算`)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
bgImage={"url('/images/oilinfofooterbg.png')"}
|
||||||
|
bgSize="cover"
|
||||||
|
backgroundPosition="center"
|
||||||
|
bgRepeat="no-repeat"
|
||||||
|
w='100%'
|
||||||
|
h='auto'
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
w='100%'
|
||||||
|
mb={10}
|
||||||
|
align={"center"}
|
||||||
|
ref={footerRef}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={footerControls}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={oilinfogroup}
|
||||||
|
w={{ base: "80%", sm: "80%", md: '850px' }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default Oil_info;
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { Box, Image, Text, Flex, Accordion } from '@chakra-ui/react'
|
||||||
|
import { colors } from '../colors';
|
||||||
|
import { motion, useInView } from 'framer-motion';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
const MotionBox = motion(Box);
|
||||||
|
const questions = [
|
||||||
|
{
|
||||||
|
image: '/images/q1.png',
|
||||||
|
question: "煮餸落少啲油,甚至唔落油就等於健康?",
|
||||||
|
answer: "錯!油脂是身體合成荷爾蒙的重要材料,若完全不攝取油脂,可能會導致皮膚變乾、容易脫髮,對女生來說更可能影響生理週期。簡單來說,身體需要「好脂肪」來幫助吸收營養,適量攝取優質油脂更有助維持細胞健康!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/images/q2.png',
|
||||||
|
question: "用橄欖油煮食就一定最健康?",
|
||||||
|
answer: "未必!橄欖油雖富含單元不飽和脂肪酸如Omega-9,但人體可自行合成,而其 Omega-3 和 Omega-6 含量偏低,僅約 1% 和 7%,脂肪酸比例並不均衡,對小孩及孕婦來說,長期單一使用可能無法滿足他們對營養素的需求。而初榨橄欖油煙點低(190°C),高溫煮食容易產生有害物質,反成健康負擔。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/images/q3.png',
|
||||||
|
question: "使用單一油種的食用油較好?",
|
||||||
|
answer: "未必!橄欖油雖富含單元不飽和脂肪酸如Omega-9,但人體可自行合成,而其Omega-3和 Omega-6 含量偏低,脂肪酸比例並不均衡,對小孩及孕婦來說,長期單一使用可能無法滿足他們對營養素的需求。而初榨橄欖油煙點較低(約190°C),高溫煮食容易產生有害物質,反成健康負擔。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/images/q4.png',
|
||||||
|
question: "貴價油 = 健康油?",
|
||||||
|
answer: `健康關鍵在於「脂肪酸比例」,而非價格!
|
||||||
|
不同脂肪酸對健康影響各異:
|
||||||
|
\t• 單元不飽和脂肪酸(MUFA / 例如Omega-9) - 有助降低壞膽固醇
|
||||||
|
\t• 多元不飽和脂肪酸(PUFA / 例如Omega-3、6) - 需攝取適當比例,Omega-6過多會引發炎症
|
||||||
|
\t• 飽和脂肪(SFA) - 過量攝取會提升壞膽固醇,影響心血管健康`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/images/q5.png',
|
||||||
|
question: "食油種類太多,點揀先唔會中伏?",
|
||||||
|
answer: `市面上的食油種類繁多,從花生油、粟米油、芥花籽油、橄欖油、米糠油到牛油果油,每種油都有不同的營養價值和適用範圍。要選擇真正健康和適合日常使用的食油,只需記住 3 個關鍵原則:
|
||||||
|
|
||||||
|
看煙點:高溫煎炸選用煙點高於200°C的食油,可減少有害物質產生
|
||||||
|
看脂肪酸比例:單元不飽和脂肪酸(MUFA)與多元不飽和脂肪酸(PUFA)比例,確保均衡攝取營養
|
||||||
|
看認證:符合國際營養機構推薦,如世界衛生組織(WHO)建議的脂肪酸攝取比例`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function Qa() {
|
||||||
|
const formatText = (text: string) => {
|
||||||
|
return text.split('\n').map((line, i) => (
|
||||||
|
<Box key={i}
|
||||||
|
color={colors.textColor}
|
||||||
|
fontSize='lg'
|
||||||
|
ml={1}
|
||||||
|
className='font-noto-sans font-regular'
|
||||||
|
>
|
||||||
|
{line.split('\t').map((segment, j) => (
|
||||||
|
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||||
|
))}
|
||||||
|
{i < text.split('\n').length - 1 && <br />}
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w="100%"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Accordion.Root variant="plain" multiple={true} >
|
||||||
|
{questions.map((item, index) => {
|
||||||
|
// Create a ref for each question item
|
||||||
|
const questionRef = useRef(null);
|
||||||
|
// Use useInView with once:true to trigger animation only once when scrolled into view
|
||||||
|
const isInView = useInView(questionRef, {
|
||||||
|
once: true, // Only trigger once
|
||||||
|
amount: 0.3 // Trigger when 30% of element is in viewport
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionBox
|
||||||
|
ref={questionRef}
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.6,
|
||||||
|
delay: index * 0.15,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Accordion.Item value={item.question} width={'full'} bgColor='white'>
|
||||||
|
<Accordion.ItemTrigger
|
||||||
|
_hover={{
|
||||||
|
boxShadow: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
_focus={{
|
||||||
|
boxShadow: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
_active={{
|
||||||
|
boxShadow: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
bgColor={index % 2 == 0 ? 'white' : '#FBFCF3'} height='auto'
|
||||||
|
justifyContent='center'
|
||||||
|
w='full'
|
||||||
|
alignItems={'center'}>
|
||||||
|
<Flex w={{ base: '95%', sm: '95%', md: '80%', lg: '70%', xl: '50%' }}
|
||||||
|
direction={{ base: 'column', sm: "column", md: 'row' }}
|
||||||
|
marginY={4}
|
||||||
|
>
|
||||||
|
<Flex w={{ base: '15%', sm: '15%', md: '10%', lg: '10%', xl: '10%' }}
|
||||||
|
justifyContent={{ base: 'flex-start', sm: 'flex-start', md: 'flex-end' }}
|
||||||
|
alignItems='center'>
|
||||||
|
<Image src={item.image}
|
||||||
|
mt={2.5}
|
||||||
|
mr={{ base: 0, sm: 0, md: 1.5 }}
|
||||||
|
w={{ base: '70px', sm: '70px', md: '45px', lg: '45px', xl: '45px' }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex w='90%'
|
||||||
|
alignItems={'center'}>
|
||||||
|
<Text
|
||||||
|
color={colors.textColor}
|
||||||
|
fontSize='2xl'
|
||||||
|
className='font-noto-sans font-black'
|
||||||
|
>{item.question}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Accordion.ItemTrigger>
|
||||||
|
<Accordion.ItemContent>
|
||||||
|
<Accordion.ItemBody
|
||||||
|
justifyItems={'center'}>
|
||||||
|
<Flex w={{ base: '95%', sm: '95%', md: '80%', lg: '70%', xl: '50%' }}>
|
||||||
|
<Flex w={{ base: '3%', sm: '2%', md: '10%', lg: '10%', xl: '10%' }}
|
||||||
|
justifyContent={{ base: 'flex-start', sm: 'flex-start', md: 'flex-end' }}
|
||||||
|
alignItems='center'>
|
||||||
|
</Flex>
|
||||||
|
<Flex w={{ base: '97%', sm: '98%', md: '90%', lg: '90%', xl: '90%' }}
|
||||||
|
direction={'column'}>
|
||||||
|
{formatText(item.answer)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Accordion.ItemBody>
|
||||||
|
</Accordion.ItemContent>
|
||||||
|
</Accordion.Item>
|
||||||
|
</MotionBox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion.Root>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default Qa;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
Avatar as ChakraAvatar,
|
||||||
|
AvatarGroup as ChakraAvatarGroup,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
|
||||||
|
|
||||||
|
export interface AvatarProps extends ChakraAvatar.RootProps {
|
||||||
|
name?: string
|
||||||
|
src?: string
|
||||||
|
srcSet?: string
|
||||||
|
loading?: ImageProps["loading"]
|
||||||
|
icon?: React.ReactElement
|
||||||
|
fallback?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||||
|
function Avatar(props, ref) {
|
||||||
|
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||||
|
props
|
||||||
|
return (
|
||||||
|
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||||
|
<ChakraAvatar.Fallback name={name}>
|
||||||
|
{icon || fallback}
|
||||||
|
</ChakraAvatar.Fallback>
|
||||||
|
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||||
|
{children}
|
||||||
|
</ChakraAvatar.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const AvatarGroup = ChakraAvatarGroup
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
rootRef?: React.Ref<HTMLLabelElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
|
function Checkbox(props, ref) {
|
||||||
|
const { icon, children, inputProps, rootRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||||
|
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraCheckbox.Control>
|
||||||
|
{icon || <ChakraCheckbox.Indicator />}
|
||||||
|
</ChakraCheckbox.Control>
|
||||||
|
{children != null && (
|
||||||
|
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||||
|
)}
|
||||||
|
</ChakraCheckbox.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { ButtonProps } from "@chakra-ui/react"
|
||||||
|
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuX } from "react-icons/lu"
|
||||||
|
|
||||||
|
export type CloseButtonProps = ButtonProps
|
||||||
|
|
||||||
|
export const CloseButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
CloseButtonProps
|
||||||
|
>(function CloseButton(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||||
|
{props.children ?? <LuX />}
|
||||||
|
</ChakraIconButton>
|
||||||
|
)
|
||||||
|
})
|
|
@ -0,0 +1,107 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
|
||||||
|
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
|
||||||
|
import { ThemeProvider, useTheme } from "next-themes"
|
||||||
|
import type { ThemeProviderProps } from "next-themes"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuMoon, LuSun } from "react-icons/lu"
|
||||||
|
|
||||||
|
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||||
|
|
||||||
|
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="light" disableTransitionOnChange {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorMode = "light" | "dark"
|
||||||
|
|
||||||
|
export interface UseColorModeReturn {
|
||||||
|
colorMode: ColorMode
|
||||||
|
setColorMode: (colorMode: ColorMode) => void
|
||||||
|
toggleColorMode: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorMode(): UseColorModeReturn {
|
||||||
|
const { resolvedTheme, setTheme } = useTheme()
|
||||||
|
const toggleColorMode = () => {
|
||||||
|
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
colorMode: resolvedTheme as ColorMode,
|
||||||
|
setColorMode: setTheme,
|
||||||
|
toggleColorMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorModeValue<T>(light: T, dark: T) {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === "dark" ? dark : light
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColorModeIcon() {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||||
|
|
||||||
|
export const ColorModeButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ColorModeButtonProps
|
||||||
|
>(function ColorModeButton(props, ref) {
|
||||||
|
const { toggleColorMode } = useColorMode()
|
||||||
|
return (
|
||||||
|
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Toggle color mode"
|
||||||
|
size="sm"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
css={{
|
||||||
|
_icon: {
|
||||||
|
width: "5",
|
||||||
|
height: "5",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorModeIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ClientOnly>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||||
|
function LightMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color="fg"
|
||||||
|
display="contents"
|
||||||
|
className="chakra-theme light"
|
||||||
|
colorPalette="gray"
|
||||||
|
colorScheme="light"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||||
|
function DarkMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color="fg"
|
||||||
|
display="contents"
|
||||||
|
className="chakra-theme dark"
|
||||||
|
colorPalette="gray"
|
||||||
|
colorScheme="dark"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface DialogContentProps extends ChakraDialog.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
backdrop?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DialogContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
DialogContentProps
|
||||||
|
>(function DialogContent(props, ref) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
portalled = true,
|
||||||
|
portalRef,
|
||||||
|
backdrop = true,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
{backdrop && <ChakraDialog.Backdrop />}
|
||||||
|
<ChakraDialog.Positioner>
|
||||||
|
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
|
||||||
|
{children}
|
||||||
|
</ChakraDialog.Content>
|
||||||
|
</ChakraDialog.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DialogCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraDialog.CloseTriggerProps
|
||||||
|
>(function DialogCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraDialog.CloseTrigger
|
||||||
|
position="absolute"
|
||||||
|
top="2"
|
||||||
|
insetEnd="2"
|
||||||
|
{...props}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<CloseButton size="sm" ref={ref}>
|
||||||
|
{props.children}
|
||||||
|
</CloseButton>
|
||||||
|
</ChakraDialog.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DialogRoot = ChakraDialog.Root
|
||||||
|
export const DialogFooter = ChakraDialog.Footer
|
||||||
|
export const DialogHeader = ChakraDialog.Header
|
||||||
|
export const DialogBody = ChakraDialog.Body
|
||||||
|
export const DialogBackdrop = ChakraDialog.Backdrop
|
||||||
|
export const DialogTitle = ChakraDialog.Title
|
||||||
|
export const DialogDescription = ChakraDialog.Description
|
||||||
|
export const DialogTrigger = ChakraDialog.Trigger
|
||||||
|
export const DialogActionTrigger = ChakraDialog.ActionTrigger
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
offset?: ChakraDrawer.ContentProps["padding"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrawerContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
DrawerContentProps
|
||||||
|
>(function DrawerContent(props, ref) {
|
||||||
|
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraDrawer.Positioner padding={offset}>
|
||||||
|
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
|
||||||
|
{children}
|
||||||
|
</ChakraDrawer.Content>
|
||||||
|
</ChakraDrawer.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DrawerCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraDrawer.CloseTriggerProps
|
||||||
|
>(function DrawerCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraDrawer.CloseTrigger
|
||||||
|
position="absolute"
|
||||||
|
top="2"
|
||||||
|
insetEnd="2"
|
||||||
|
{...props}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<CloseButton size="sm" ref={ref} />
|
||||||
|
</ChakraDrawer.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DrawerTrigger = ChakraDrawer.Trigger
|
||||||
|
export const DrawerRoot = ChakraDrawer.Root
|
||||||
|
export const DrawerFooter = ChakraDrawer.Footer
|
||||||
|
export const DrawerHeader = ChakraDrawer.Header
|
||||||
|
export const DrawerBody = ChakraDrawer.Body
|
||||||
|
export const DrawerBackdrop = ChakraDrawer.Backdrop
|
||||||
|
export const DrawerDescription = ChakraDrawer.Description
|
||||||
|
export const DrawerTitle = ChakraDrawer.Title
|
||||||
|
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Field as ChakraField } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||||
|
label?: React.ReactNode
|
||||||
|
helperText?: React.ReactNode
|
||||||
|
errorText?: React.ReactNode
|
||||||
|
optionalText?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||||
|
function Field(props, ref) {
|
||||||
|
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||||
|
props
|
||||||
|
return (
|
||||||
|
<ChakraField.Root ref={ref} {...rest}>
|
||||||
|
{label && (
|
||||||
|
<ChakraField.Label>
|
||||||
|
{label}
|
||||||
|
<ChakraField.RequiredIndicator fallback={optionalText} />
|
||||||
|
</ChakraField.Label>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{helperText && (
|
||||||
|
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
|
||||||
|
)}
|
||||||
|
{errorText && (
|
||||||
|
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||||
|
)}
|
||||||
|
</ChakraField.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
|
||||||
|
import { Group, InputElement } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface InputGroupProps extends BoxProps {
|
||||||
|
startElementProps?: InputElementProps
|
||||||
|
endElementProps?: InputElementProps
|
||||||
|
startElement?: React.ReactNode
|
||||||
|
endElement?: React.ReactNode
|
||||||
|
children: React.ReactElement<InputElementProps>
|
||||||
|
startOffset?: InputElementProps["paddingStart"]
|
||||||
|
endOffset?: InputElementProps["paddingEnd"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||||
|
function InputGroup(props, ref) {
|
||||||
|
const {
|
||||||
|
startElement,
|
||||||
|
startElementProps,
|
||||||
|
endElement,
|
||||||
|
endElementProps,
|
||||||
|
children,
|
||||||
|
startOffset = "6px",
|
||||||
|
endOffset = "6px",
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const child =
|
||||||
|
React.Children.only<React.ReactElement<InputElementProps>>(children)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group ref={ref} {...rest}>
|
||||||
|
{startElement && (
|
||||||
|
<InputElement pointerEvents="none" {...startElementProps}>
|
||||||
|
{startElement}
|
||||||
|
</InputElement>
|
||||||
|
)}
|
||||||
|
{React.cloneElement(child, {
|
||||||
|
...(startElement && {
|
||||||
|
ps: `calc(var(--input-height) - ${startOffset})`,
|
||||||
|
}),
|
||||||
|
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
|
||||||
|
...children.props,
|
||||||
|
})}
|
||||||
|
{endElement && (
|
||||||
|
<InputElement placement="end" {...endElementProps}>
|
||||||
|
{endElement}
|
||||||
|
</InputElement>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface PopoverContentProps extends ChakraPopover.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PopoverContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PopoverContentProps
|
||||||
|
>(function PopoverContent(props, ref) {
|
||||||
|
const { portalled = true, portalRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraPopover.Positioner>
|
||||||
|
<ChakraPopover.Content ref={ref} {...rest} />
|
||||||
|
</ChakraPopover.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PopoverArrow = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraPopover.ArrowProps
|
||||||
|
>(function PopoverArrow(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraPopover.Arrow {...props} ref={ref}>
|
||||||
|
<ChakraPopover.ArrowTip />
|
||||||
|
</ChakraPopover.Arrow>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PopoverCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraPopover.CloseTriggerProps
|
||||||
|
>(function PopoverCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraPopover.CloseTrigger
|
||||||
|
position="absolute"
|
||||||
|
top="1"
|
||||||
|
insetEnd="1"
|
||||||
|
{...props}
|
||||||
|
asChild
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<CloseButton size="sm" />
|
||||||
|
</ChakraPopover.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PopoverTitle = ChakraPopover.Title
|
||||||
|
export const PopoverDescription = ChakraPopover.Description
|
||||||
|
export const PopoverFooter = ChakraPopover.Footer
|
||||||
|
export const PopoverHeader = ChakraPopover.Header
|
||||||
|
export const PopoverRoot = ChakraPopover.Root
|
||||||
|
export const PopoverBody = ChakraPopover.Body
|
||||||
|
export const PopoverTrigger = ChakraPopover.Trigger
|
|
@ -0,0 +1,15 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
ColorModeProvider,
|
||||||
|
type ColorModeProviderProps,
|
||||||
|
} from "./color-mode"
|
||||||
|
|
||||||
|
export function Provider(props: ColorModeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ChakraProvider value={defaultSystem}>
|
||||||
|
<ColorModeProvider {...props} />
|
||||||
|
</ChakraProvider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface RadioProps extends ChakraRadioGroup.ItemProps {
|
||||||
|
rootRef?: React.Ref<HTMLDivElement>
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||||
|
function Radio(props, ref) {
|
||||||
|
const { children, inputProps, rootRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
|
||||||
|
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraRadioGroup.ItemIndicator />
|
||||||
|
{children && (
|
||||||
|
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
|
||||||
|
)}
|
||||||
|
</ChakraRadioGroup.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const RadioGroup = ChakraRadioGroup.Root
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface SliderProps extends ChakraSlider.RootProps {
|
||||||
|
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||||
|
label?: React.ReactNode
|
||||||
|
showValue?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||||
|
function Slider(props, ref) {
|
||||||
|
const { marks: marksProp, label, showValue, ...rest } = props
|
||||||
|
const value = props.defaultValue ?? props.value
|
||||||
|
|
||||||
|
const marks = marksProp?.map((mark) => {
|
||||||
|
if (typeof mark === "number") return { value: mark, label: undefined }
|
||||||
|
return mark
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
||||||
|
{label && !showValue && (
|
||||||
|
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
||||||
|
)}
|
||||||
|
{label && showValue && (
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
||||||
|
<ChakraSlider.ValueText />
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
|
||||||
|
<ChakraSlider.Track>
|
||||||
|
<ChakraSlider.Range />
|
||||||
|
</ChakraSlider.Track>
|
||||||
|
<SliderThumbs value={value} />
|
||||||
|
<SliderMarks marks={marks} />
|
||||||
|
</ChakraSlider.Control>
|
||||||
|
</ChakraSlider.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function SliderThumbs(props: { value?: number[] }) {
|
||||||
|
const { value } = props
|
||||||
|
return (
|
||||||
|
<For each={value}>
|
||||||
|
{(_, index) => (
|
||||||
|
<ChakraSlider.Thumb key={index} index={index}>
|
||||||
|
<ChakraSlider.HiddenInput />
|
||||||
|
</ChakraSlider.Thumb>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SliderMarksProps {
|
||||||
|
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
|
||||||
|
function SliderMarks(props, ref) {
|
||||||
|
const { marks } = props
|
||||||
|
if (!marks?.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraSlider.MarkerGroup ref={ref}>
|
||||||
|
{marks.map((mark, index) => {
|
||||||
|
const value = typeof mark === "number" ? mark : mark.value
|
||||||
|
const label = typeof mark === "number" ? undefined : mark.label
|
||||||
|
return (
|
||||||
|
<ChakraSlider.Marker key={index} value={value}>
|
||||||
|
<ChakraSlider.MarkerIndicator />
|
||||||
|
{label}
|
||||||
|
</ChakraSlider.Marker>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ChakraSlider.MarkerGroup>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||||
|
showArrow?: boolean
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
content: React.ReactNode
|
||||||
|
contentProps?: ChakraTooltip.ContentProps
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||||
|
function Tooltip(props, ref) {
|
||||||
|
const {
|
||||||
|
showArrow,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
portalled = true,
|
||||||
|
content,
|
||||||
|
contentProps,
|
||||||
|
portalRef,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if (disabled) return children
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraTooltip.Root {...rest}>
|
||||||
|
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraTooltip.Positioner>
|
||||||
|
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||||
|
{showArrow && (
|
||||||
|
<ChakraTooltip.Arrow>
|
||||||
|
<ChakraTooltip.ArrowTip />
|
||||||
|
</ChakraTooltip.Arrow>
|
||||||
|
)}
|
||||||
|
{content}
|
||||||
|
</ChakraTooltip.Content>
|
||||||
|
</ChakraTooltip.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</ChakraTooltip.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,49 @@
|
||||||
|
/* Font Family Utility Classes */
|
||||||
|
.font-melle {
|
||||||
|
font-family: 'MElleHK', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-noto-sans {
|
||||||
|
font-family: 'NotoSansCJKtc', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-noto-mono {
|
||||||
|
font-family: 'NotoSansMonoCJKtc', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-noto-serif {
|
||||||
|
font-family: 'NotoSerifCJKjp', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font Weight Utility Classes */
|
||||||
|
.font-thin {
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-light {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-demi-light {
|
||||||
|
font-weight: 350;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-regular {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-xbold {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-black {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
/* MElleHK Font Family */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MElleHK';
|
||||||
|
src: url('../assets/fonts/MElleHK-Light.OTF') format('opentype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MElleHK';
|
||||||
|
src: url('../assets/fonts/MElleHK-Medium.OTF') format('opentype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MElleHK';
|
||||||
|
src: url('../assets/fonts/MElleHK-Xbold.otf') format('opentype');
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NotoSansCJKtc Font Family */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-Thin.otf') format('opentype');
|
||||||
|
font-weight: 100;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-Light.otf') format('opentype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-DemiLight.otf') format('opentype');
|
||||||
|
font-weight: 350;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-Regular.otf') format('opentype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-Medium.otf') format('opentype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-Bold.otf') format('opentype');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansCJKtc-Black.otf') format('opentype');
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NotoSansMonoCJKtc Font Family */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansMonoCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansMonoCJKtc-Regular.otf') format('opentype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSansMonoCJKtc';
|
||||||
|
src: url('../assets/fonts/NotoSansMonoCJKtc-Bold.otf') format('opentype');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NotoSerifCJKjp Font Family */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'NotoSerifCJKjp';
|
||||||
|
src: url('../assets/fonts/NotoSerifCJKjp-Black.otf') format('opentype');
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: 'NotoSansCJKtc', 'MElleHK', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Provider } from "@/components/ui/provider"
|
||||||
|
import React from "react"
|
||||||
|
import ReactDOM from "react-dom/client"
|
||||||
|
import App from "./App"
|
||||||
|
import "./index.css"
|
||||||
|
import "./fonts.css"
|
||||||
|
import "./colors.css"
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths"
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(),tsconfigPaths()],
|
||||||
|
})
|