├── .gitignore ├── .prettierrc ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── assets │ ├── profile-with-circle.jpg │ └── sheep.png ├── components │ ├── Accordion │ │ ├── Accordion.tsx │ │ └── index.ts │ ├── AnimatedEyesFollowMouseCursor │ │ ├── AnimatedEyesFollowMouseCursor.tsx │ │ └── index.ts │ ├── Banners │ │ ├── AnimatedBanner1.tsx │ │ ├── Banners.tsx │ │ ├── index.ts │ │ └── type.ts │ ├── Canvas │ │ ├── BouncingBall │ │ │ ├── Ball.ts │ │ │ ├── Block.ts │ │ │ ├── BouncingBall.tsx │ │ │ └── index.ts │ │ ├── MovingBox │ │ │ ├── Dialog.ts │ │ │ ├── MovingBox.tsx │ │ │ ├── Point.ts │ │ │ └── index.ts │ │ ├── MovingGradation │ │ │ ├── GlowParticle.ts │ │ │ ├── MovingGradation.tsx │ │ │ ├── constants.ts │ │ │ └── index.ts │ │ ├── RotatingPolygon │ │ │ ├── Polygon.ts │ │ │ ├── RotatingPolygon.tsx │ │ │ └── index.ts │ │ ├── Wavy │ │ │ ├── Point.ts │ │ │ ├── Wave.ts │ │ │ ├── WaveGroup.ts │ │ │ ├── Wavy.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── ClimbEffects │ │ ├── ClimbEffects.tsx │ │ ├── README.md │ │ └── index.ts │ ├── ClipPathCircle │ │ ├── ClipPathCircle.tsx │ │ └── index.ts │ ├── CustomScrollbar │ │ ├── CustomScrollbar.tsx │ │ └── index.ts │ ├── Drag │ │ ├── DragItem.tsx │ │ ├── DragItemList.tsx │ │ └── index.ts │ ├── Dropdown │ │ ├── Dropdown.tsx │ │ └── index.ts │ ├── FakeScrollSpy │ │ ├── FakeScrollSpy.tsx │ │ └── index.ts │ ├── GhostText │ │ ├── GhostText.tsx │ │ └── index.ts │ ├── GlowingBackground │ │ ├── GlowingBackground.tsx │ │ └── index.ts │ ├── GlowingCheckbox │ │ ├── GlowingCheckbox.tsx │ │ └── index.ts │ ├── GlowingRadio │ │ ├── GlowingRadio.tsx │ │ └── index.ts │ ├── GlowingText │ │ ├── GlowingText.tsx │ │ ├── README.md │ │ └── index.ts │ ├── HalfCircularBackground │ │ ├── HalfCircularBackground.tsx │ │ └── index.ts │ ├── HeartAnimation │ │ ├── HeartAnimation.tsx │ │ └── index.ts │ ├── HideScrollOnScrollDown │ │ ├── HideScrollOnScrollDown.tsx │ │ └── index.ts │ ├── HoverEffectCard │ │ ├── HoverEffectCard.tsx │ │ └── index.ts │ ├── ListHoverEffects │ │ ├── ListHoverEffects.tsx │ │ └── index.ts │ ├── Loader │ │ ├── GradientLoader.tsx │ │ ├── Loader.tsx │ │ ├── TwoRingLoader.tsx │ │ └── index.ts │ ├── LongTextShadow │ │ ├── LongTextShadow.tsx │ │ └── index.ts │ ├── Menu │ │ ├── Menu.tsx │ │ ├── MenuItem.tsx │ │ └── index.ts │ ├── MiniWavy │ │ ├── MiniWavy.tsx │ │ └── index.ts │ ├── Modal │ │ ├── Modal.tsx │ │ └── index.ts │ ├── MouseCursor │ │ ├── DotMouseCursor.tsx │ │ └── index.ts │ ├── MoveSheep │ │ ├── Hill.ts │ │ ├── MoveSheep.tsx │ │ ├── Sheep.ts │ │ ├── SheepController.ts │ │ ├── Sun.ts │ │ └── index.ts │ ├── MovingText │ │ ├── MovingText.tsx │ │ └── index.ts │ ├── NeonButton │ │ ├── NeonButton.tsx │ │ └── index.ts │ ├── NumberCounter │ │ ├── NumberCounter.tsx │ │ └── index.ts │ ├── PasswordInput │ │ ├── PasswordInput.tsx │ │ └── index.ts │ ├── Portal │ │ ├── Portal.tsx │ │ └── index.ts │ ├── RangeInput │ │ ├── RangeInput.tsx │ │ └── index.ts │ ├── RippleEffectsButton │ │ ├── RippleEffectsButton.tsx │ │ └── index.ts │ ├── ScrollDownIndicator │ │ ├── ScrollDownIndicator.tsx │ │ └── index.ts │ ├── ScrollProgressBar │ │ ├── ScrollProgressBar.tsx │ │ └── index.ts │ ├── ShadowEffects │ │ ├── ShadowEffects.tsx │ │ └── index.ts │ ├── SidebarMenu │ │ ├── SidebarMenu.tsx │ │ └── index.ts │ ├── Skeleton │ │ ├── Skeleton.tsx │ │ ├── Skeleton2.tsx │ │ └── index.ts │ ├── SlidingMenuIndicator │ │ ├── SlidingMenuIndicator.tsx │ │ └── index.ts │ ├── StickyElementsScrollingEffects │ │ ├── StickyElementsScrollingEffects.tsx │ │ └── index.ts │ ├── TextArea │ │ ├── TextArea.tsx │ │ └── index.ts │ ├── TextTyping │ │ ├── TextTyping.tsx │ │ └── index.ts │ ├── ThreeDimensionBackground │ │ ├── ThreeDimensionBackground.tsx │ │ └── index.ts │ ├── ThreeDimensionDebitCard │ │ ├── ThreeDimensionDebitCard.tsx │ │ └── index.ts │ ├── Timeline │ │ ├── Timeline.tsx │ │ └── index.ts │ ├── Tooltip │ │ ├── Tooltip.tsx │ │ ├── TooltipWithPortal.tsx │ │ └── index.ts │ ├── WaveBorderCard │ │ ├── WaveBorderCard.tsx │ │ └── index.ts │ ├── WavySection │ │ ├── WavySection.tsx │ │ └── index.ts │ ├── WebGlAndThreeJS │ │ ├── BasicScene.tsx │ │ ├── BasicScene2.tsx │ │ ├── BasicScene3.tsx │ │ └── index.ts │ └── index.ts ├── hooks │ └── useScreenSize.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── stories │ ├── Accordion.stories.tsx │ ├── AnimatedEyesFollowMouseCursor.stories.tsx │ ├── Banners.stories.tsx │ ├── ClimbEffects.stories.tsx │ ├── ClipPathCircle.stories.tsx │ ├── CustomScrollbar.stories.tsx │ ├── Drag.stories.tsx │ ├── DropDown.stories.tsx │ ├── FakeScrollSpy.stories.tsx │ ├── GhostText.stories.tsx │ ├── GlowingBackground.stories.tsx │ ├── GlowingCheckbox.stories.tsx │ ├── GlowingText.stories.tsx │ ├── HalfCircularBackground.stories.tsx │ ├── HeartAnimation.stories.tsx │ ├── HideScrollOnScrollDown.stories.tsx │ ├── HoverEffectCard.stories.tsx │ ├── Introduction.stories.mdx │ ├── ListHoverEffects.stories.tsx │ ├── Loader.stories.tsx │ ├── LongBoxShadow.stories.tsx │ ├── Menu.stories.tsx │ ├── MiniWavy.stories.tsx │ ├── Modal.stories.tsx │ ├── MouseCursor.stories.tsx │ ├── MovingText.stories.tsx │ ├── NeonButton.stories.tsx │ ├── NumberCounter.stories.tsx │ ├── PasswordInput.stories.tsx │ ├── RangeInput.stories.tsx │ ├── RippleEffectsButton.stories.tsx │ ├── ScrollDownIndicator.stories.tsx │ ├── ScrollProgressBar.stories.tsx │ ├── ShadowEffects.stories.tsx │ ├── SidebarMenu.stories.tsx │ ├── Skeleton.stories.tsx │ ├── SlidingMenuIndicator.stories.tsx │ ├── StickyElementsScrollingEffects.stories.tsx │ ├── TextArea.stories.tsx │ ├── TextTyping.stories.tsx │ ├── ThreeDimensionBackground.stories.tsx │ ├── ThreeDimensionDebitCard.stories.tsx │ ├── Timeline.stories.tsx │ ├── Tooltip.stories.tsx │ ├── WaveBorderCard.stories.tsx │ ├── WavySection.stories.tsx │ ├── assets │ │ ├── code-brackets.svg │ │ ├── colors.svg │ │ ├── comments.svg │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── plugin.svg │ │ ├── repo.svg │ │ └── stackalt.svg │ ├── canvasTutorial │ │ ├── BouncingBall.stories.tsx │ │ ├── MoveSheep.stories.tsx │ │ ├── MovingBox.stories.tsx │ │ ├── MovingGradation.stories.tsx │ │ ├── Readme.stories.mdx │ │ ├── RotatingPolygon.stories.tsx │ │ └── Wavy.stories.tsx │ ├── dummy │ │ └── dummyData.js │ └── threejs │ │ └── BasicScene.stories.tsx └── utils │ ├── DomUtils.ts │ ├── RefUtils.ts │ ├── WebGLUtils.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | typescript: { 3 | check: false, 4 | checkOptions: {}, 5 | reactDocgen: 'react-docgen-typescript', 6 | reactDocgenTypescriptOptions: { 7 | shouldExtractLiteralValuesFromEnum: true, 8 | propFilter: (prop) => 9 | prop.parent ? !/node_modules/.test(prop.parent.fileName) : true, 10 | }, 11 | }, 12 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 13 | addons: [ 14 | '@storybook/addon-links', 15 | '@storybook/addon-essentials', 16 | '@storybook/preset-create-react-app', 17 | '@storybook/addon-storysource', 18 | '@storybook/addon-knobs', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | Jacob Css 13 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | controls: { disable: true }, 4 | layout: 'fullscreen', 5 | backgrounds: { 6 | default: 'dark', 7 | values: [ 8 | { name: 'dark', value: 'black' }, 9 | { name: 'white', value: 'white' }, 10 | { name: 'dimgray', value: 'dimgray' }, 11 | { name: 'ghostwhite', value: 'ghostwhite' }, 12 | { name: 'whitesmoke', value: 'whitesmoke' }, 13 | ], 14 | }, 15 | options: { showPanel: false }, 16 | }; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jacob 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Css Collection 2 | 3 | [![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@master/badge/badge-storybook.svg)](https://css-collection-git-main.jungkyuhyun.vercel.app/) 4 | 5 | - 자연스러운 인터렉션으로 좋은 유저 경험을 주기 위한 평소에 사용하는 것 이상의 애니메이션 및 그래픽 공부 6 | - 공부용이므로 이미 기능이 모두 구현된 css third-party 라이브러리는 사용하지 않습니다. 7 | 8 | ## Storybook 9 | 10 | - vercel로 빌드되었습니다. 11 | - 라이브 환경에서의 확인은 [storybook](https://css-collection-git-main.jungkyuhyun.vercel.app/)에서 확인할 수 있습니다. 12 | - 데스크탑에서 확인해 주세요. 13 | 14 | 15 | ## Preview 16 | 17 | [스토리북](https://css-collection-git-main.jungkyuhyun.vercel.app/)에서 모두 확인할 수 있습니다. 18 | 19 | ### Only css 20 | 21 | `styled-components`, `threejs`, `fontawesome` 등의 라이브러리를 제외하고 모두 직접 구현하는데 목적이 있습니다. 22 | ![2021-01-11_13-43-20 (1)](https://user-images.githubusercontent.com/42884032/104147787-1ba76880-5413-11eb-8775-1d4639a08b03.gif) 23 | 24 | ### Canvas 25 | 26 | html5의 `canvas` api를 이용하여 복잡한 애니메이션 구현을 위한 연습을 합니다. 27 | ![2021-02-21_20-15-38 (1)](https://user-images.githubusercontent.com/42884032/108623346-e6fee600-7481-11eb-8799-5508c089c3e8.gif) 28 | 29 | 30 | ### Etc 31 | 32 | `d3`, `canvas`, `webGL` 등의 내용이 더 추가될 예정입니다. 33 | 34 | 35 | 36 | ## Command 37 | 38 | ```bash 39 | npm i && npm run storybook 40 | ``` 41 | 42 | 또는 43 | 44 | ``` 45 | yarn install && yarn storybook 46 | ``` 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-collection", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "storybook": "start-storybook -p 6006 -s public", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test", 10 | "eject": "react-scripts eject", 11 | "build-storybook": "build-storybook -c .storybook -o public" 12 | }, 13 | "eslintConfig": { 14 | "extends": [ 15 | "react-app", 16 | "react-app/jest" 17 | ] 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "dependencies": { 32 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 33 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 34 | "@fortawesome/react-fontawesome": "^0.1.14", 35 | "@testing-library/jest-dom": "^5.11.4", 36 | "@testing-library/react": "^11.1.0", 37 | "@testing-library/user-event": "^12.1.10", 38 | "@types/jest": "^26.0.15", 39 | "@types/node": "^12.0.0", 40 | "@types/react": "^17.0.0", 41 | "@types/react-dom": "^16.9.8", 42 | "@types/styled-components": "^5.1.7", 43 | "lodash": "^4.17.20", 44 | "prettier": "^2.2.1", 45 | "react": "^17.0.1", 46 | "react-dom": "^17.0.1", 47 | "react-scripts": "4.0.1", 48 | "styled-components": "^5.2.1", 49 | "three": "^0.124.0", 50 | "typescript": "^4.0.3", 51 | "web-vitals": "^0.2.4" 52 | }, 53 | "devDependencies": { 54 | "@fortawesome/free-brands-svg-icons": "^5.15.1", 55 | "@fortawesome/free-regular-svg-icons": "^5.15.1", 56 | "@storybook/addon-actions": "^6.1.11", 57 | "@storybook/addon-essentials": "^6.1.11", 58 | "@storybook/addon-knobs": "^6.1.11", 59 | "@storybook/addon-links": "^6.1.11", 60 | "@storybook/addon-storysource": "^6.1.11", 61 | "@storybook/addons": "^6.1.11", 62 | "@storybook/node-logger": "^6.1.11", 63 | "@storybook/preset-create-react-app": "^3.1.5", 64 | "@storybook/react": "^6.1.11", 65 | "@types/lodash": "^4.14.166", 66 | "react-docgen-typescript": "^1.20.5", 67 | "storybook-addon-jsx": "^7.3.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JungKyuHyun/css-collection/9e5a580f9b5413939c82adfa065cd2a88ea3a670/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Css Collection 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JungKyuHyun/css-collection/9e5a580f9b5413939c82adfa065cd2a88ea3a670/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JungKyuHyun/css-collection/9e5a580f9b5413939c82adfa065cd2a88ea3a670/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | function App() { 5 | return ; 6 | } 7 | 8 | const LayoutContainer = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | background-color: black; 14 | height: 100vh; 15 | overflow: hidden; 16 | `; 17 | 18 | export default memo(App); 19 | -------------------------------------------------------------------------------- /src/assets/profile-with-circle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JungKyuHyun/css-collection/9e5a580f9b5413939c82adfa065cd2a88ea3a670/src/assets/profile-with-circle.jpg -------------------------------------------------------------------------------- /src/assets/sheep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JungKyuHyun/css-collection/9e5a580f9b5413939c82adfa065cd2a88ea3a670/src/assets/sheep.png -------------------------------------------------------------------------------- /src/components/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useCallback, ReactText } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | interface AccordionItemProps { 5 | className?: string; 6 | label?: ReactText; 7 | } 8 | 9 | const AccordionItem = memo(({ className, label }) => { 10 | const [isOpen, setIsOpen] = useState(false); 11 | 12 | const handleClick = useCallback(() => { 13 | setIsOpen((v) => !v); 14 | }, []); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | 21 |

22 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos 23 | voluptates quae nostrum totam quaerat, commodi asperiores ullam. 24 | Neque enim beatae, cum, optio, tempora autem est a sint vitae ipsum 25 | debitis! Lorem ipsum dolor sit amet consectetur adipisicing elit. 26 | Quos voluptates quae nostrum totam quaerat, commodi asperiores 27 | ullam. Neque enim beatae, cum, optio, tempora autem est a sint vitae 28 | ipsum debitis! 29 |

30 |
31 |
32 | 33 | ); 34 | }); 35 | 36 | export const Accordion = memo(() => { 37 | const dummyList = [ 38 | 'Jacob Accordion One', 39 | 'Jacob Accordion Two', 40 | 'Jacob Accordion Three', 41 | 'Jacob Accordion Four', 42 | ]; 43 | 44 | return ( 45 | 46 | {dummyList.map((d) => ( 47 | 48 | ))} 49 | 50 | ); 51 | }); 52 | 53 | const AccordionContainer = styled.div` 54 | max-width: 600px; 55 | `; 56 | 57 | const ContentSection = styled.div` 58 | position: relative; 59 | margin: 10px 20px; 60 | background: white; 61 | `; 62 | 63 | const Label = styled.div<{ open: boolean }>` 64 | position: relative; 65 | padding: 10px; 66 | background: cyan; 67 | font-weight: 600; 68 | cursor: pointer; 69 | 70 | &::before { 71 | content: ${(props) => (props.open ? '"-"' : '"+"')}; 72 | position: absolute; 73 | top: 50%; 74 | right: 20px; 75 | transform: translateY(-50%); 76 | font-size: 1.5em; 77 | } 78 | `; 79 | 80 | const Contents = styled.div<{ open: boolean }>` 81 | position: relative; 82 | background: white; 83 | overflow-y: auto; 84 | transition: 0.5s; 85 | height: 0; 86 | 87 | ${(props) => 88 | props.open && 89 | css` 90 | height: 100px; 91 | `} 92 | `; 93 | 94 | const P = styled.p` 95 | padding: 0 8px; 96 | `; 97 | -------------------------------------------------------------------------------- /src/components/Accordion/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Accordion'; 2 | -------------------------------------------------------------------------------- /src/components/AnimatedEyesFollowMouseCursor/AnimatedEyesFollowMouseCursor.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useRef, useCallback } from 'react'; 2 | import styled from 'styled-components'; 3 | import throttle from 'lodash/throttle'; 4 | 5 | export const AnimatedEyesFollowMouseCursor = memo(() => { 6 | const leftEyeRef = useRef(null); 7 | const rightEyeRef = useRef(null); 8 | 9 | const eyeball = useCallback((event: MouseEvent) => { 10 | if ( 11 | leftEyeRef === null || 12 | leftEyeRef.current === null || 13 | rightEyeRef === null || 14 | rightEyeRef.current === null 15 | ) { 16 | return; 17 | } 18 | const leftX = 19 | leftEyeRef.current.getBoundingClientRect().left + 20 | leftEyeRef.current.clientWidth / 2; 21 | const leftY = 22 | leftEyeRef.current.getBoundingClientRect().top + 23 | leftEyeRef.current.clientHeight / 2; 24 | const rightX = 25 | rightEyeRef.current.getBoundingClientRect().left + 26 | rightEyeRef.current.clientWidth / 2; 27 | const rightY = 28 | rightEyeRef.current.getBoundingClientRect().top + 29 | rightEyeRef.current.clientHeight / 2; 30 | 31 | const radianLeft = Math.atan2(event.pageX - leftX, event.pageY - leftY); 32 | const radianRight = Math.atan2(event.pageX - rightX, event.pageY - rightY); 33 | 34 | const rotationLeft = radianLeft * (180 / Math.PI) * -1 + 270; 35 | const rotationRight = radianRight * (180 / Math.PI) * -1 + 270; 36 | 37 | leftEyeRef.current.style.transform = `rotate(${rotationLeft}deg)`; 38 | rightEyeRef.current.style.transform = `rotate(${rotationRight}deg)`; 39 | }, []); 40 | 41 | useEffect(() => { 42 | document.body.addEventListener('mousemove', throttle(eyeball, 50)); 43 | return () => 44 | document.body.removeEventListener('mousemove', throttle(eyeball, 50)); 45 | }, [eyeball]); 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | ); 53 | }); 54 | 55 | const Box = styled.div` 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | `; 60 | 61 | const Eye = styled.div` 62 | position: relative; 63 | width: 120px; 64 | height: 120px; 65 | display: block; 66 | background: white; 67 | margin: 0 20px; 68 | border-radius: 50%; 69 | box-shadow: 0 5px 45px rgba(0, 0, 0, 0.2), inset 0 0 15px black, 70 | inset 0 0 25px black; 71 | 72 | &::before { 73 | content: ''; 74 | position: absolute; 75 | top: 50%; 76 | left: 35px; 77 | transform: translate(-50%, -50%); 78 | width: 45px; 79 | height: 45px; 80 | border-radius: 50%; 81 | background: white; 82 | border: 14px solid black; 83 | box-sizing: border-box; 84 | } 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/AnimatedEyesFollowMouseCursor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AnimatedEyesFollowMouseCursor'; 2 | -------------------------------------------------------------------------------- /src/components/Banners/Banners.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { AnimatedBanner1 } from './AnimatedBanner1'; 3 | import { BannerTypes } from './type'; 4 | 5 | const BANNERS_LOOKUP_TABLE = { 6 | [BannerTypes.Animated1]: AnimatedBanner1, 7 | }; 8 | 9 | export interface BannersProps { 10 | type?: BannerTypes; 11 | } 12 | 13 | export const Banners = memo(({ type }) => { 14 | if (type === undefined) { 15 | return null; 16 | } 17 | return React.createElement(BANNERS_LOOKUP_TABLE[type]); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/Banners/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Banners'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/components/Banners/type.ts: -------------------------------------------------------------------------------- 1 | export enum BannerTypes { 2 | Animated1 = 'Animated1', 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Canvas/BouncingBall/Ball.ts: -------------------------------------------------------------------------------- 1 | import { Block } from './Block'; 2 | 3 | export class Ball { 4 | radius; 5 | x; 6 | y; 7 | vx; 8 | vy; 9 | 10 | constructor(stageWidth: number, stageHeight: number, radius: number, speed: number) { 11 | this.radius = radius; 12 | this.vx = speed; 13 | this.vy = speed; 14 | 15 | const diameter = this.radius * 2; 16 | this.x = this.radius + Math.random() * (stageWidth - diameter); 17 | this.y = this.radius + Math.random() * (stageHeight - diameter); 18 | } 19 | 20 | draw(ctx: CanvasRenderingContext2D | null, stageWidth: number, stageHeight: number, block: Block) { 21 | if (ctx === null) return; 22 | 23 | this.x += this.vx; 24 | this.y += this.vy; 25 | 26 | this.bounceWindow(stageWidth, stageHeight); 27 | 28 | this.bounceBlock(block); 29 | 30 | ctx.fillStyle = '#fdd700'; 31 | ctx.beginPath(); 32 | ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); 33 | ctx.fill(); 34 | } 35 | 36 | private bounceWindow(stageWidth: number, stageHeight: number) { 37 | const minX = this.radius; 38 | const maxX = stageWidth - this.radius; 39 | const minY = this.radius; 40 | const maxY = stageHeight - this.radius; 41 | 42 | if (this.x <= minX || this.x >= maxX) { 43 | this.vx *= -1; 44 | this.x += this.vx; 45 | } else if (this.y <= minY || this.y >= maxY) { 46 | this.vy *= -1; 47 | this.y += this.vy; 48 | } 49 | } 50 | 51 | private bounceBlock(block: Block) { 52 | const minX = block.x - this.radius; 53 | const maxX = block.maxX + this.radius; 54 | const minY = block.y - this.radius; 55 | const maxY = block.maxY + this.radius; 56 | 57 | if (this.x > minX && this.x < maxX && this.y > minY && this.y < maxY) { 58 | const x1 = Math.abs(minX - this.x); 59 | const x2 = Math.abs(this.x - maxX); 60 | const y1 = Math.abs(minY - this.y); 61 | const y2 = Math.abs(minX - this.x); 62 | const min1 = Math.min(x1, x2); 63 | const min2 = Math.min(y1, y2); 64 | const min = Math.min(min1, min2); 65 | 66 | if (min === min1) { 67 | this.vx *= -1; 68 | this.x += this.vx; 69 | } else if (min === min2) { 70 | this.vy *= -1; 71 | this.y += this.vy; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/Canvas/BouncingBall/Block.ts: -------------------------------------------------------------------------------- 1 | export class Block { 2 | width; 3 | height; 4 | x; 5 | y; 6 | 7 | // 공을 추적 하기 위한 값 8 | maxX; 9 | maxY; 10 | 11 | constructor(width: number, height: number, x: number, y: number) { 12 | this.width = width; 13 | this.height = height; 14 | this.x = x; 15 | this.y = y; 16 | this.maxX = width + x; 17 | this.maxY = height + y; 18 | } 19 | 20 | draw(ctx: CanvasRenderingContext2D | null) { 21 | if (ctx === null) return; 22 | 23 | const xGap = 80; 24 | const yGap = 60; 25 | 26 | ctx.fillStyle = '#ff384e'; 27 | ctx.beginPath(); 28 | ctx.rect(this.x, this.y, this.width, this.height); 29 | ctx.fill(); 30 | 31 | // 그림자 주기 32 | ctx.fillStyle = '#190f3a'; 33 | ctx.beginPath(); 34 | ctx.moveTo(this.maxX, this.maxY); 35 | ctx.lineTo(this.maxX - xGap, this.maxY + yGap); 36 | ctx.lineTo(this.x - xGap, this.maxY + yGap); 37 | ctx.lineTo(this.x, this.maxY); 38 | ctx.fill(); 39 | 40 | // 옆 그림자 주기 41 | ctx.fillStyle = '#9d0919'; 42 | ctx.beginPath(); 43 | ctx.moveTo(this.x, this.y); 44 | ctx.lineTo(this.x, this.maxY); 45 | ctx.lineTo(this.x - xGap, this.maxY + yGap); 46 | ctx.lineTo(this.x - xGap, this.maxY + yGap - this.height); 47 | ctx.fill(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Canvas/BouncingBall/BouncingBall.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; 2 | import useScreenSize from '../../../hooks/useScreenSize'; 3 | import { DomUtils } from '../../../utils'; 4 | import { Ball } from './Ball'; 5 | import { Block } from './Block'; 6 | 7 | export const BouncingBall = memo(() => { 8 | const ref = useRef(null); 9 | const { stageWidth, stageHeight } = useScreenSize(); 10 | 11 | const ball = useMemo(() => { 12 | return new Ball(stageWidth, stageHeight, 60, 8); 13 | }, [stageWidth, stageHeight]); 14 | 15 | const block = useMemo(() => { 16 | return new Block(700, 30, 300, 450); 17 | }, []); 18 | 19 | const resize = useCallback(() => { 20 | if (ref.current === null) return; 21 | 22 | const ctx = ref.current.getContext('2d'); 23 | const pixelRatio = window.devicePixelRatio > 1 ? 2 : 1; 24 | 25 | ref.current.width = stageWidth * pixelRatio; 26 | ref.current.height = stageHeight * pixelRatio; 27 | 28 | ctx?.scale(pixelRatio, pixelRatio); 29 | }, [stageWidth, stageHeight]); 30 | 31 | const animate = useCallback( 32 | (t: number) => { 33 | if (ref.current === null) return; 34 | 35 | const ctx = ref.current.getContext('2d'); 36 | ctx?.clearRect(0, 0, stageWidth, stageHeight); 37 | 38 | window.requestAnimationFrame(animate); 39 | 40 | block.draw(ctx); 41 | ball.draw(ctx, stageWidth, stageHeight, block); 42 | }, 43 | [stageWidth, stageHeight, ball, block] 44 | ); 45 | 46 | useEffect(() => { 47 | if (!DomUtils.usableWindow()) return; 48 | 49 | window.addEventListener('resize', resize, false); 50 | resize(); 51 | 52 | return () => window.removeEventListener('resize', resize, false); 53 | }, [resize]); 54 | 55 | useEffect(() => { 56 | if (!DomUtils.usableWindow()) return; 57 | 58 | const num = window.requestAnimationFrame(animate); 59 | 60 | return () => window.cancelAnimationFrame(num); 61 | }, [animate]); 62 | 63 | return ; 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/Canvas/BouncingBall/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BouncingBall'; 2 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingBox/Dialog.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './Point'; 2 | 3 | const FOLLOW_SPEED = 0.08; 4 | const ROTATE_SPEED = 0.12; 5 | const MAX_ANGLE = 30; 6 | const FPS = 1000 / 60; 7 | const WIDTH = 260; 8 | const HEIGHT = 260; 9 | 10 | export class Dialog { 11 | pos: Point; 12 | target: Point; 13 | prevPos: Point; 14 | downPos: Point; 15 | startPos: Point; 16 | mousePos: Point; 17 | centerPos: Point; 18 | origin: Point; 19 | rotation: number; 20 | sideValue: number; 21 | isDown: boolean; 22 | 23 | constructor() { 24 | this.pos = new Point(); 25 | this.target = new Point(); 26 | this.prevPos = new Point(); 27 | this.downPos = new Point(); 28 | this.startPos = new Point(); 29 | this.mousePos = new Point(); 30 | this.centerPos = new Point(); 31 | this.origin = new Point(); 32 | this.rotation = 0; 33 | this.sideValue = 0; 34 | this.isDown = false; 35 | } 36 | 37 | resize(stageWidth: number, stageHeight: number) { 38 | this.pos.x = Math.random() * (stageWidth - WIDTH); 39 | this.pos.y = Math.random() * (stageHeight - HEIGHT); 40 | this.target = this.pos.clone(); 41 | this.prevPos = this.pos.clone(); 42 | } 43 | 44 | animate(ctx: CanvasRenderingContext2D | null) { 45 | if (ctx === null) return; 46 | 47 | const move = this.target.clone().subtract(this.pos).reduce(FOLLOW_SPEED); 48 | this.pos.add(move); 49 | 50 | this.centerPos = this.pos.clone().add(this.mousePos); 51 | 52 | this.swingDrag(ctx); 53 | 54 | this.prevPos = this.pos.clone(); 55 | } 56 | 57 | swingDrag(ctx: CanvasRenderingContext2D | null) { 58 | if (ctx === null) return; 59 | 60 | const dx = this.pos.x - this.prevPos.x; 61 | const speedX = Math.abs(dx) / FPS; 62 | const speed = Math.min(Math.max(speedX, 0), 1); 63 | 64 | let rotation = (MAX_ANGLE / 1) * speed; 65 | rotation = rotation * (dx > 0 ? 1 : -1) - this.sideValue; 66 | 67 | this.rotation += (rotation - this.rotation) * ROTATE_SPEED; 68 | 69 | const temPos = this.pos.clone().add(this.origin); 70 | ctx.save(); 71 | ctx.translate(temPos.x, temPos.y); 72 | ctx.rotate((this.rotation * Math.PI) / 180); 73 | ctx.beginPath(); 74 | ctx.fillStyle = '#f4e55a'; 75 | ctx.fillRect(-this.origin.x, -this.origin.y, WIDTH, HEIGHT); 76 | ctx.restore(); 77 | } 78 | 79 | down(point: Point) { 80 | if (point.collide(this.pos, WIDTH, HEIGHT)) { 81 | this.isDown = true; 82 | this.startPos = this.pos.clone(); 83 | this.downPos = point.clone(); 84 | this.mousePos = point.clone().subtract(this.pos); 85 | 86 | const xRatioValue = this.mousePos.x / WIDTH; 87 | this.origin.x = WIDTH * xRatioValue; 88 | this.origin.y = (HEIGHT * this.mousePos.y) / HEIGHT; 89 | 90 | this.sideValue = xRatioValue - 0.5; 91 | 92 | return this; 93 | } 94 | return null; 95 | } 96 | 97 | move(point: Point) { 98 | if (this.isDown) { 99 | this.target = this.startPos.clone().add(point).subtract(this.downPos); 100 | } 101 | } 102 | 103 | up() { 104 | this.isDown = false; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingBox/Point.ts: -------------------------------------------------------------------------------- 1 | export class Point { 2 | x; 3 | y; 4 | 5 | constructor(x?: number, y?: number) { 6 | this.x = x || 0; 7 | this.y = y || 0; 8 | } 9 | 10 | add(point: Point) { 11 | this.x += point.x; 12 | this.y += point.y; 13 | return this; 14 | } 15 | 16 | subtract(point: Point) { 17 | this.x -= point.x; 18 | this.y -= point.y; 19 | return this; 20 | } 21 | 22 | reduce(value: number) { 23 | this.x *= value; 24 | this.y *= value; 25 | return this; 26 | } 27 | 28 | collide(point: Point, width: number, height: number) { 29 | if (this.x >= point.x && this.x <= point.x + width && this.y >= point.y && this.y <= point.y + height) { 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | clone() { 36 | return new Point(this.x, this.y); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingBox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MovingBox'; 2 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingGradation/GlowParticle.ts: -------------------------------------------------------------------------------- 1 | import { PI2, RGB_TYPE } from './constants'; 2 | 3 | export class GlowParticle { 4 | x; 5 | y; 6 | radius; 7 | rgb; 8 | 9 | vx = Math.random() * 4; 10 | vy = Math.random() * 4; 11 | 12 | sinValue = Math.random(); 13 | 14 | constructor(x: number, y: number, radius: number, rgb: RGB_TYPE) { 15 | this.x = x; 16 | this.y = y; 17 | this.radius = radius; 18 | this.rgb = rgb; 19 | } 20 | 21 | animate(ctx: CanvasRenderingContext2D | null, stageWidth: number, stageHeight: number) { 22 | if (ctx === null) return; 23 | 24 | this.sinValue += 0.01; 25 | 26 | this.radius += Math.sin(this.sinValue); 27 | 28 | this.x += this.vx; 29 | this.y += this.vy; 30 | 31 | if (this.x < 0) { 32 | this.vx *= -1; 33 | this.x += 10; 34 | } else if (this.x > stageWidth) { 35 | this.vx *= -1; 36 | this.x -= 10; 37 | } 38 | 39 | if (this.y < 0) { 40 | this.vy *= -1; 41 | this.y += 10; 42 | } else if (this.y > stageHeight) { 43 | this.vy *= -1; 44 | this.y -= 10; 45 | } 46 | 47 | ctx.beginPath(); 48 | const g = ctx.createRadialGradient(this.x, this.y, this.radius * 0.01, this.x, this.y, this.radius); 49 | g.addColorStop(0, `rgba(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}, 1)`); 50 | g.addColorStop(1, `rgba(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}, 0)`); 51 | 52 | ctx.fillStyle = g; 53 | ctx.arc(this.x, this.y, this.radius, 0, PI2, false); 54 | ctx.fill(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingGradation/MovingGradation.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; 2 | import useScreenSize from '../../../hooks/useScreenSize'; 3 | import { DomUtils } from '../../../utils'; 4 | import { COLORS, MAX_RADIUS, MIN_RADIUS, TOTAL_PARTICLES } from './constants'; 5 | import { GlowParticle } from './GlowParticle'; 6 | 7 | export const MovingGradation = memo(() => { 8 | const ref = useRef(null); 9 | const { stageWidth, stageHeight } = useScreenSize(); 10 | const [particles, setParticles] = useState>([]); 11 | 12 | const createParticles = useCallback(() => { 13 | let curColor = 0; 14 | setParticles([]); 15 | 16 | for (let i = 0; i < TOTAL_PARTICLES; i++) { 17 | const item = new GlowParticle( 18 | Math.random() * stageWidth, 19 | Math.random() * stageHeight, 20 | Math.random() * (MAX_RADIUS - MIN_RADIUS) + MIN_RADIUS, 21 | COLORS[curColor] 22 | ); 23 | 24 | if (++curColor >= COLORS.length) { 25 | curColor = 0; 26 | } 27 | 28 | setParticles((prev) => [...prev, item]); 29 | } 30 | }, [stageWidth, stageHeight]); 31 | 32 | const resize = useCallback(() => { 33 | if (ref.current === null) return; 34 | 35 | const ctx = ref.current.getContext('2d'); 36 | 37 | if (ctx === null) return; 38 | 39 | const pixelRatio = window.devicePixelRatio > 1 ? 2 : 1; 40 | 41 | ref.current.width = stageWidth * pixelRatio; 42 | ref.current.height = stageHeight * pixelRatio; 43 | ctx.scale(pixelRatio, pixelRatio); 44 | 45 | // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Compositing 46 | ctx.globalCompositeOperation = 'saturation'; 47 | 48 | createParticles(); 49 | }, [stageWidth, stageHeight, createParticles]); 50 | 51 | const animate = useCallback( 52 | (t: number) => { 53 | window.requestAnimationFrame(animate); 54 | if (ref.current === null) return; 55 | const ctx = ref.current.getContext('2d'); 56 | 57 | ctx?.clearRect(0, 0, stageWidth, stageHeight); 58 | 59 | particles.forEach((p) => p.animate(ctx, stageWidth, stageHeight)); 60 | }, 61 | [stageWidth, stageHeight, particles] 62 | ); 63 | 64 | useEffect(() => { 65 | if (!DomUtils.usableWindow()) return; 66 | 67 | window.addEventListener('resize', resize, false); 68 | resize(); 69 | 70 | return () => window.removeEventListener('resize', resize, false); 71 | }, [resize]); 72 | 73 | useEffect(() => { 74 | if (!DomUtils.usableWindow()) return; 75 | 76 | const num = window.requestAnimationFrame(animate); 77 | 78 | return () => window.cancelAnimationFrame(num); 79 | }, [animate]); 80 | 81 | return ; 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingGradation/constants.ts: -------------------------------------------------------------------------------- 1 | export type RGB_TYPE = { r: number; g: number; b: number }; 2 | 3 | const COLORS: ReadonlyArray = [ 4 | { r: 45, g: 74, b: 227 }, // blue 5 | { r: 250, g: 255, b: 89 }, // yellow 6 | { r: 255, g: 104, b: 248 }, // pupple 7 | { r: 44, g: 209, b: 252 }, // skyblue 8 | { r: 54, g: 233, b: 84 }, // green 9 | ]; 10 | 11 | const TOTAL_PARTICLES = 15; 12 | 13 | const MAX_RADIUS = 900; 14 | 15 | const MIN_RADIUS = 400; 16 | 17 | const PI2 = Math.PI * 2; 18 | 19 | export { COLORS, TOTAL_PARTICLES, MAX_RADIUS, MIN_RADIUS, PI2 }; 20 | -------------------------------------------------------------------------------- /src/components/Canvas/MovingGradation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MovingGradation'; 2 | -------------------------------------------------------------------------------- /src/components/Canvas/RotatingPolygon/Polygon.ts: -------------------------------------------------------------------------------- 1 | const COLORS = [ 2 | '#4b45ab', 3 | '#554fb8', 4 | '#605ac7', 5 | '#2a91a8', 6 | '#2e9ab2', 7 | '#32a5bf', 8 | '#81b144', 9 | '#85b944', 10 | '#8fc549', 11 | '#e0af27', 12 | '#eeba2a', 13 | '#fec72e', 14 | '#bf342d', 15 | '#ca3931', 16 | '#d7423a', 17 | ]; 18 | 19 | export class Polygon { 20 | x; 21 | y; 22 | radius; 23 | sides; 24 | 25 | rotate = 0; 26 | PI2 = Math.PI * 2; 27 | 28 | constructor(x: number, y: number, radius: number, sides: number) { 29 | this.x = x; 30 | this.y = y; 31 | this.radius = radius; 32 | this.sides = sides; 33 | } 34 | 35 | animate(ctx: CanvasRenderingContext2D | null, moveX: number) { 36 | if (ctx === null) return; 37 | 38 | ctx.save(); 39 | // ctx.fillStyle = '#000'; 40 | // ctx.beginPath(); 41 | 42 | const angle = this.PI2 / this.sides; 43 | const angle2 = this.PI2 / 4; 44 | 45 | ctx.translate(this.x, this.y); 46 | 47 | this.rotate += moveX * 0.008; 48 | ctx.rotate(this.rotate); 49 | 50 | for (let i = 0; i < this.sides; i++) { 51 | const x = this.radius * Math.cos(angle * i); 52 | const y = this.radius * Math.sin(angle * i); 53 | 54 | // i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); 55 | 56 | // ctx.beginPath(); 57 | // ctx.arc(x, y, 30, 0, this.PI2, false); 58 | // ctx.fill(); 59 | 60 | ctx.save(); 61 | ctx.fillStyle = COLORS[i]; 62 | ctx.translate(x, y); 63 | ctx.rotate((((360 / this.sides) * i + 45) * Math.PI) / 180); 64 | ctx.beginPath(); 65 | for (let j = 0; j < 4; j++) { 66 | const x2 = 140 * Math.cos(angle2 * j); 67 | const y2 = 140 * Math.sin(angle2 * j); 68 | j === 0 ? ctx.moveTo(x2, y2) : ctx.lineTo(x2, y2); 69 | } 70 | ctx.fill(); 71 | ctx.closePath(); 72 | ctx.restore(); 73 | } 74 | 75 | // ctx.fill(); 76 | // ctx.closePath(); 77 | ctx.restore(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Canvas/RotatingPolygon/RotatingPolygon.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; 2 | import useScreenSize from '../../../hooks/useScreenSize'; 3 | import { DomUtils } from '../../../utils'; 4 | import { Polygon } from './Polygon'; 5 | 6 | export const RotatingPolygon = memo(() => { 7 | const { stageWidth, stageHeight } = useScreenSize(); 8 | const ref = useRef(null); 9 | const [pointer, setPointer] = useState({ isDown: false, moveX: 0, offsetX: 0 }); 10 | 11 | const polygon = useMemo(() => { 12 | return new Polygon(stageWidth / 2, stageHeight + stageHeight / 4, stageHeight / 1.5, 15); 13 | }, [stageWidth, stageHeight]); 14 | 15 | const handlePointerDown = useCallback((event: React.PointerEvent) => { 16 | setPointer((prev) => ({ ...prev, isDown: true, moveX: 0, offsetX: event.clientX })); 17 | }, []); 18 | 19 | const handlePointerMove = useCallback( 20 | (event: React.PointerEvent) => { 21 | if (pointer.isDown) { 22 | setPointer((prev) => ({ ...prev, moveX: event.clientX - prev.offsetX, offsetX: event.clientX })); 23 | } 24 | }, 25 | [pointer] 26 | ); 27 | 28 | const handlePointerUp = useCallback((event: React.PointerEvent) => { 29 | setPointer((prev) => ({ ...prev, isDown: false })); 30 | }, []); 31 | 32 | const resize = useCallback(() => { 33 | if (ref.current === null) return; 34 | 35 | const ctx = ref.current.getContext('2d'); 36 | const pixelRatio = window.devicePixelRatio > 1 ? 2 : 1; 37 | 38 | ref.current.width = stageWidth * pixelRatio; 39 | ref.current.height = stageHeight * pixelRatio; 40 | 41 | ctx?.scale(pixelRatio, pixelRatio); 42 | }, [stageWidth, stageHeight]); 43 | 44 | const animate = useCallback( 45 | (t: number) => { 46 | if (ref.current === null) return; 47 | 48 | const ctx = ref.current.getContext('2d'); 49 | ctx?.clearRect(0, 0, stageWidth, stageHeight); 50 | 51 | setPointer((prev) => ({ ...prev, moveX: (prev.moveX *= 0.92) })); 52 | 53 | polygon.animate(ctx, pointer.moveX); 54 | }, 55 | [stageWidth, stageHeight, polygon, pointer.moveX] 56 | ); 57 | 58 | useEffect(() => { 59 | if (!DomUtils.usableWindow()) return; 60 | 61 | window.addEventListener('resize', resize, false); 62 | resize(); 63 | 64 | return () => window.removeEventListener('resize', resize, false); 65 | }, [resize]); 66 | 67 | useEffect(() => { 68 | if (!DomUtils.usableWindow()) return; 69 | 70 | const num = window.requestAnimationFrame(animate); 71 | 72 | return () => window.cancelAnimationFrame(num); 73 | }, [animate]); 74 | 75 | return ( 76 | 82 | ); 83 | }); 84 | -------------------------------------------------------------------------------- /src/components/Canvas/RotatingPolygon/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RotatingPolygon'; 2 | -------------------------------------------------------------------------------- /src/components/Canvas/Wavy/Point.ts: -------------------------------------------------------------------------------- 1 | export class Point { 2 | x; 3 | y; 4 | fixedY; 5 | speed = 0.01; 6 | cur; 7 | 8 | // 얼마나 움직일 것인가에 대한 프로퍼티 9 | max = Math.random() * 100 + 150; 10 | 11 | constructor(index: number, x: number, y: number) { 12 | this.x = x; 13 | this.y = y; 14 | this.fixedY = y; 15 | this.cur = index; 16 | } 17 | 18 | update() { 19 | this.cur += this.speed; 20 | this.y = this.fixedY + Math.sin(this.cur) * this.max; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Canvas/Wavy/Wave.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './Point'; 2 | 3 | export class Wave { 4 | stageWidth = 0; 5 | stageHeight = 0; 6 | 7 | // 이 웨이브는 화면 중간에 그려질 것이기 때문에 center 값을 정의 8 | centerX = 0; 9 | centerY = 0; 10 | 11 | point: Point | undefined; 12 | 13 | index; 14 | totalPoints; 15 | color; 16 | points: Array = []; 17 | pointGap = 0; 18 | 19 | constructor(index: number, totalPoints: number, color: string) { 20 | this.index = index; 21 | this.totalPoints = totalPoints; 22 | this.color = color; 23 | } 24 | 25 | resize(stageWidth: number, stageHeight: number) { 26 | this.stageWidth = stageWidth; 27 | this.stageHeight = stageHeight; 28 | 29 | this.centerX = stageWidth / 2; 30 | this.centerY = stageHeight / 2; 31 | 32 | this.pointGap = this.stageWidth / (this.totalPoints - 1); 33 | 34 | this.init(); 35 | } 36 | 37 | init() { 38 | for (let i = 0; i < this.totalPoints; i++) { 39 | const point = new Point(this.index + i, this.pointGap * i, this.centerY); 40 | this.points[i] = point; 41 | } 42 | } 43 | 44 | draw(ctx: CanvasRenderingContext2D | null) { 45 | if (ctx === null) return; 46 | 47 | ctx.beginPath(); 48 | ctx.fillStyle = this.color; 49 | 50 | let prevX = this.points[0].x; 51 | let prevY = this.points[0].y; 52 | 53 | ctx.moveTo(prevX, prevY); 54 | 55 | // 첫점과 끝점은 가만히 있고, 가운데만 업데이트 56 | for (let i = 1; i < this.totalPoints; i++) { 57 | if (i < this.totalPoints - 1) { 58 | this.points[i].update(); 59 | } 60 | 61 | const cx = (prevX + this.points[i].x) / 2; 62 | const cy = (prevY + this.points[i].y) / 2; 63 | 64 | ctx.quadraticCurveTo(prevX, prevY, cx, cy); 65 | 66 | prevX = this.points[i].x; 67 | prevY = this.points[i].y; 68 | } 69 | 70 | ctx.lineTo(prevX, prevY); 71 | ctx.lineTo(this.stageWidth, this.stageHeight); 72 | ctx.lineTo(this.points[0].x, this.stageHeight); 73 | ctx.fill(); 74 | ctx.closePath(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/Canvas/Wavy/WaveGroup.ts: -------------------------------------------------------------------------------- 1 | import { Wave } from './Wave'; 2 | 3 | export class WaveGroup { 4 | totalWaves = 3; 5 | totalPoints = 6; 6 | 7 | color = ['rgba(255, 0, 0, 0.4)', 'rgba(255, 255, 0, 0.4)', 'rgba(0, 255, 255, 0.4)']; 8 | 9 | waves: Array; 10 | 11 | constructor() { 12 | this.waves = []; 13 | for (let i = 0; i < this.totalWaves; i++) { 14 | const wave = new Wave(i, this.totalPoints, this.color[i]); 15 | this.waves[i] = wave; 16 | } 17 | } 18 | 19 | resize(stageWidth: number, stageHeight: number) { 20 | for (let i = 0; i < this.totalWaves; i++) { 21 | const wave = this.waves[i]; 22 | wave.resize(stageWidth, stageHeight); 23 | } 24 | } 25 | 26 | draw(ctx: CanvasRenderingContext2D | null) { 27 | if (ctx === null) return; 28 | 29 | for (let i = 0; i < this.totalWaves; i++) { 30 | const wave = this.waves[i]; 31 | wave.draw(ctx); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Canvas/Wavy/Wavy.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; 2 | import useScreenSize from '../../../hooks/useScreenSize'; 3 | import { DomUtils } from '../../../utils'; 4 | import { WaveGroup } from './WaveGroup'; 5 | 6 | export const Wavy = memo(() => { 7 | const ref = useRef(null); 8 | const { stageWidth, stageHeight } = useScreenSize(); 9 | 10 | const waveGroup = useMemo(() => { 11 | return new WaveGroup(); 12 | }, []); 13 | 14 | const resize = useCallback(() => { 15 | if (ref.current === null) return; 16 | 17 | const ctx = ref.current.getContext('2d'); 18 | const pixelRatio = window.devicePixelRatio > 1 ? 2 : 1; 19 | 20 | ref.current.width = stageWidth * pixelRatio; 21 | ref.current.height = stageHeight * pixelRatio; 22 | ctx?.scale(pixelRatio, pixelRatio); 23 | 24 | waveGroup.resize(stageWidth, stageHeight); 25 | }, [stageWidth, stageHeight, waveGroup]); 26 | 27 | const animate = useCallback( 28 | (t: number) => { 29 | if (ref.current === null) return; 30 | 31 | const ctx = ref.current.getContext('2d'); 32 | ctx?.clearRect(0, 0, stageWidth, stageHeight); 33 | 34 | waveGroup.draw(ctx); 35 | 36 | requestAnimationFrame(animate); 37 | }, 38 | [stageWidth, stageHeight, waveGroup] 39 | ); 40 | 41 | useEffect(() => { 42 | if (!DomUtils.usableWindow()) return; 43 | window.addEventListener('resize', resize, false); 44 | resize(); 45 | 46 | return () => window.removeEventListener('resize', resize, false); 47 | }, [resize]); 48 | 49 | useEffect(() => { 50 | if (!DomUtils.usableWindow()) return; 51 | 52 | const num = requestAnimationFrame(animate); 53 | 54 | return () => window.cancelAnimationFrame(num); 55 | }, [animate]); 56 | 57 | return ; 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/Canvas/Wavy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Wavy'; 2 | -------------------------------------------------------------------------------- /src/components/Canvas/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Wavy'; 2 | export * from './RotatingPolygon'; 3 | export * from './BouncingBall'; 4 | export * from './MovingGradation'; 5 | export * from './MovingBox'; 6 | -------------------------------------------------------------------------------- /src/components/ClimbEffects/ClimbEffects.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const ClimbEffects = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }); 15 | 16 | const Bg = styled.div` 17 | position: relative; 18 | overflow: hidden; 19 | width: 100%; 20 | height: 100%; 21 | `; 22 | const Container = styled.div` 23 | position: relative; 24 | top: 40%; 25 | width: 100%; 26 | transform: rotate(-35deg); 27 | `; 28 | 29 | const animateSurface = keyframes` 30 | 0% 31 | { 32 | transform: translateX(0px); 33 | } 34 | 100% 35 | { 36 | transform: translateX(-200px); 37 | } 38 | `; 39 | 40 | const Box = styled.div` 41 | position: relative; 42 | display: flex; 43 | left: -20px; 44 | justify-content: center; 45 | align-self: center; 46 | width: calc(100% + 200px); 47 | -webkit-box-reflect: below 1px linear-gradient(transparent, #0004); 48 | animation: ${animateSurface} 1.5s ease-in-out infinite; 49 | `; 50 | 51 | const animate = keyframes` 52 | 0% 53 | { 54 | transform: rotate(0deg) 55 | } 56 | 60% 57 | { 58 | transform: rotate(90deg) 59 | } 60 | 65% 61 | { 62 | transform: rotate(85deg) 63 | } 64 | 70% 65 | { 66 | transform: rotate(90deg) 67 | } 68 | 75% 69 | { 70 | transform: rotate(87.5deg) 71 | } 72 | 80%, 100% 73 | { 74 | transform: rotate(90deg) 75 | } 76 | `; 77 | 78 | const Cube = styled.div` 79 | position: relative; 80 | width: 200px; 81 | height: 200px; 82 | background-color: cyan; 83 | box-shadow: 0 0 5px cyan, 0 0 25px cyan, 0 0 50px cyan, 0 0 100px cyan, 84 | 0 0 200px cyan, 0 0 300px cyan; 85 | transform-origin: bottom right; 86 | animation: ${animate} 1.5s ease-in-out infinite; 87 | `; 88 | -------------------------------------------------------------------------------- /src/components/ClimbEffects/README.md: -------------------------------------------------------------------------------- 1 | ### `ClimbEffects` 2 | 3 | ![ClimbEffects](https://user-images.githubusercontent.com/42884032/103213269-82774d00-4950-11eb-8c61-386b43d9094f.gif) 4 | -------------------------------------------------------------------------------- /src/components/ClimbEffects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ClimbEffects'; 2 | -------------------------------------------------------------------------------- /src/components/ClipPathCircle/ClipPathCircle.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { css, keyframes } from 'styled-components'; 3 | 4 | export const ClipPathCircle = memo(() => { 5 | return ( 6 |
7 | 8 | Jacob's Playground! 9 | 10 | 11 | Jacob's Playground! 12 | 13 |
14 | ); 15 | }); 16 | 17 | const animation = keyframes` 18 | 0% { 19 | transform: translateX(48%); 20 | } 21 | 50% { 22 | transform: translateX(-80%); 23 | } 24 | 100% { 25 | transform: translateX(48%); 26 | } 27 | `; 28 | 29 | const commonTextStyle = css` 30 | position: absolute; 31 | left: 0; 32 | font-size: 10em; 33 | white-space: nowrap; 34 | line-height: 100vh; 35 | cursor: default; 36 | animation: ${animation} 10s linear infinite; 37 | margin: 0; 38 | `; 39 | 40 | const Text = styled.h2` 41 | ${commonTextStyle}; 42 | color: white; 43 | `; 44 | 45 | const Text2 = styled.h2` 46 | ${commonTextStyle}; 47 | 48 | -webkit-text-stroke: 2px; 49 | -webkit-text-stroke-color: white; 50 | color: black; 51 | `; 52 | 53 | const Section = styled.section` 54 | position: relative; 55 | width: 100%; 56 | height: 100vh; 57 | display: flex; 58 | overflow: hidden; 59 | `; 60 | 61 | const Container = styled.div` 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | height: 100%; 67 | 68 | &:nth-child(2) { 69 | background: yellow; 70 | clip-path: circle(200px at center); 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /src/components/ClipPathCircle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ClipPathCircle'; 2 | -------------------------------------------------------------------------------- /src/components/CustomScrollbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CustomScrollbar'; 2 | -------------------------------------------------------------------------------- /src/components/Drag/DragItemList.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; 2 | import { DragItem } from './DragItem'; 3 | 4 | export interface DragOptions { 5 | label?: ReactNode; 6 | value?: string; 7 | disabled?: boolean; 8 | } 9 | 10 | interface DragItemListProps { 11 | className?: string; 12 | options?: ReadonlyArray; 13 | isReset?: boolean; 14 | onChange?: (newOptions: ReadonlyArray) => void; 15 | } 16 | 17 | export const DragItemList = memo(({ className, options, onChange, isReset = false }) => { 18 | const [dargList, setDragList] = useState(options); 19 | const draggedValue = React.useRef(); 20 | const resetInitialValue = useRef>(); 21 | 22 | useEffect(() => { 23 | if (isReset && resetInitialValue.current !== undefined) { 24 | setDragList(resetInitialValue.current); 25 | } 26 | }, [isReset]); 27 | 28 | useEffect(() => { 29 | if (resetInitialValue.current === undefined) { 30 | resetInitialValue.current = options; 31 | } 32 | }, [options]); 33 | 34 | useEffect(() => { 35 | if (dargList && onChange !== undefined) { 36 | onChange(dargList); 37 | } 38 | }, [dargList, onChange]); 39 | 40 | const handleChange = useCallback((dragValue?: string, dropValue?: string) => { 41 | setDragList((preDragList) => { 42 | if (preDragList === undefined) { 43 | console.error('options is empty.'); 44 | return; 45 | } 46 | 47 | if (dragValue !== undefined) { 48 | draggedValue.current = dragValue; 49 | } 50 | 51 | const defineList = preDragList.filter((l) => l.value !== draggedValue.current); 52 | const dropValueIndex = preDragList.findIndex((d) => d.value === dropValue); 53 | const draggedOption = preDragList.find((d) => d.value === draggedValue.current); 54 | 55 | if (draggedOption === undefined) { 56 | console.error('The dragged value could not be found.'); 57 | return preDragList; 58 | } 59 | 60 | const forwardList = defineList.slice(0, dropValueIndex); 61 | const backwardList = defineList.slice(dropValueIndex); 62 | return [...forwardList, draggedOption, ...backwardList]; 63 | }); 64 | }, []); 65 | 66 | if (dargList === undefined) { 67 | return null; 68 | } 69 | 70 | return ( 71 | <> 72 | {dargList.map(({ label, value, disabled }) => ( 73 | 81 | ))} 82 | 83 | ); 84 | }); 85 | -------------------------------------------------------------------------------- /src/components/Drag/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DragItemList'; 2 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Dropdown'; 2 | -------------------------------------------------------------------------------- /src/components/FakeScrollSpy/FakeScrollSpy.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | export const FakeScrollSpy = memo(() => { 5 | return ( 6 | 7 | Home Section 8 | About Section 9 | Services Section 10 | Portfolio Section 11 | Contact Section 12 | 13 | 14 | Home 15 | About 16 | Services 17 | Portfolio 18 | Contact 19 | 20 | 21 | ); 22 | }); 23 | 24 | const commonSectionStyle = css` 25 | position: relative; 26 | width: 100%; 27 | height: 100vh; 28 | color: white; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | font-size: 6em; 33 | `; 34 | 35 | const commonHoverStyle = css` 36 | background: black; 37 | color: white; 38 | `; 39 | 40 | const NavBar = styled.nav` 41 | position: fixed; 42 | top: 0; 43 | width: 100%; 44 | background: white; 45 | box-sizing: border-box; 46 | display: flex; 47 | justify-content: space-between; 48 | align-items: center; 49 | z-index: 1; 50 | `; 51 | 52 | const NavItem = styled.a` 53 | text-decoration: none; 54 | color: black; 55 | font-weight: 600; 56 | padding: 10px 20px; 57 | font-size: 1.6em; 58 | display: inline-block; 59 | width: 100%; 60 | text-align: center; 61 | 62 | &:hover { 63 | ${commonHoverStyle}; 64 | } 65 | `; 66 | 67 | const Home = styled.section` 68 | ${commonSectionStyle}; 69 | 70 | &:hover ~ ${NavBar} > ${NavItem}[href="#home"] { 71 | ${commonHoverStyle}; 72 | } 73 | `; 74 | 75 | const About = styled.section` 76 | ${commonSectionStyle}; 77 | 78 | &:hover ~ ${NavBar} > ${NavItem}[href="#about"] { 79 | ${commonHoverStyle}; 80 | } 81 | `; 82 | 83 | const Services = styled.section` 84 | ${commonSectionStyle}; 85 | 86 | &:hover ~ ${NavBar} > ${NavItem}[href="#services"] { 87 | ${commonHoverStyle}; 88 | } 89 | `; 90 | 91 | const Portfolio = styled.section` 92 | ${commonSectionStyle}; 93 | 94 | &:hover ~ ${NavBar} > ${NavItem}[href="#portfolio"] { 95 | ${commonHoverStyle}; 96 | } 97 | `; 98 | 99 | const Contact = styled.section` 100 | ${commonSectionStyle}; 101 | 102 | &:hover ~ ${NavBar} > ${NavItem}[href="#contact"] { 103 | ${commonHoverStyle}; 104 | } 105 | `; 106 | 107 | const Container = styled.div` 108 | & > section:nth-child(even) { 109 | background: gray; 110 | } 111 | `; 112 | -------------------------------------------------------------------------------- /src/components/FakeScrollSpy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FakeScrollSpy'; 2 | -------------------------------------------------------------------------------- /src/components/GhostText/GhostText.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface GhostTextProps { 5 | className?: string; 6 | text?: string; 7 | } 8 | 9 | export const GhostText = memo(({ className, text = "jacob's playground" }) => { 10 | return ( 11 | 12 | {text.split('').map((t, i) => React.createElement(GhostEffects, { key: `ghost-${i}`, index: i }, t))} 13 | 14 | ); 15 | }); 16 | 17 | const GhostEffects = styled.span<{ index: number }>` 18 | transition: 1s; 19 | 20 | &:nth-child(${(props) => props.index + 1}) { 21 | transition-delay: ${(props) => props.index * 0.1}s; 22 | } 23 | `; 24 | 25 | const Text = styled.h2` 26 | text-transform: uppercase; 27 | font-size: 5em; 28 | color: white; 29 | display: flex; 30 | text-align: center; 31 | white-space: pre; 32 | 33 | &:hover { 34 | ${GhostEffects} { 35 | filter: blur(20px); 36 | opacity: 0; 37 | transform: scale(2); 38 | } 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /src/components/GhostText/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GhostText'; 2 | -------------------------------------------------------------------------------- /src/components/GlowingBackground/GlowingBackground.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const GlowingBackground = memo(() => { 5 | return ( 6 | 7 | {Array(6) 8 | .fill('') 9 | .map((_, i) => ( 10 | 11 | ))} 12 | 13 | ); 14 | }); 15 | 16 | const Container = styled.div` 17 | position: relative; 18 | width: 90%; 19 | height: 90%; 20 | `; 21 | 22 | const animation = keyframes` 23 | 0% { 24 | transform: rotate(0deg); 25 | } 26 | 100% { 27 | transform: rotate(360deg); 28 | } 29 | `; 30 | 31 | const animationReverse = keyframes` 32 | 0% { 33 | transform: rotate(360deg); 34 | } 35 | 100% { 36 | transform: rotate(0deg); 37 | } 38 | `; 39 | 40 | const GlowingPointer = styled.span<{ delay: number }>` 41 | position: absolute; 42 | top: calc(80px * ${(props) => props.delay}); 43 | left: calc(80px * ${(props) => props.delay}); 44 | right: calc(80px * ${(props) => props.delay}); 45 | bottom: calc(80px * ${(props) => props.delay}); 46 | 47 | &::before { 48 | content: ''; 49 | position: absolute; 50 | top: 50%; 51 | left: -8px; 52 | width: 15px; 53 | height: 15px; 54 | background: cyan; 55 | border-radius: 50%; 56 | } 57 | 58 | &:nth-child(3n + 1)::before { 59 | background: lime; 60 | box-shadow: 0 0 5px lime, 0 0 20px lime, 0 0 40px lime, 0 0 60px lime, 0 0 80px lime; 61 | } 62 | &:nth-child(3n + 2)::before { 63 | background: cyan; 64 | box-shadow: 0 0 5px cyan, 0 0 20px cyan, 0 0 40px cyan, 0 0 60px cyan, 0 0 80px cyan; 65 | } 66 | &:nth-child(3n + 3)::before { 67 | background: deeppink; 68 | box-shadow: 0 0 5px deeppink, 0 0 20px deeppink, 0 0 40px deeppink, 0 0 60px deeppink, 0 0 80px deeppink; 69 | } 70 | 71 | &:nth-child(1) { 72 | animation: ${animation} 10s alternate infinite; 73 | } 74 | &:nth-child(2) { 75 | animation: ${animationReverse} 3s alternate infinite; 76 | } 77 | &:nth-child(3) { 78 | animation: ${animation} 8s alternate infinite; 79 | } 80 | &:nth-child(4) { 81 | animation: ${animationReverse} 7s alternate infinite; 82 | } 83 | &:nth-child(5) { 84 | animation: ${animation} 5s alternate infinite; 85 | } 86 | &:nth-child(6) { 87 | animation: ${animationReverse} 3s alternate infinite; 88 | } 89 | `; 90 | -------------------------------------------------------------------------------- /src/components/GlowingBackground/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GlowingBackground'; 2 | -------------------------------------------------------------------------------- /src/components/GlowingCheckbox/GlowingCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const GlowingCheckbox = memo(() => { 5 | return ; 6 | }); 7 | 8 | const Checkbox = styled.input` 9 | position: relative; 10 | width: 120px; 11 | height: 40px; 12 | background: linear-gradient(0deg, black, white); 13 | -webkit-appearance: none; 14 | outline: none; 15 | border-radius: 20px; 16 | box-shadow: 0 0 0 4px #353535, 0 0 0 5px #3e3e3e, 17 | inset 0 0 10px rgba(0, 0, 0, 1), 0 5px 20px rgba(0, 0, 0, 0.5), 18 | inset 0 0 15px rgba(0, 0, 0, 0.2); 19 | 20 | &:checked { 21 | background: linear-gradient(0deg, cyan, white); 22 | box-shadow: 0 0 0 1px rgba(0, 255, 255, 0.3), 0 0 0 5px #3e3e3e, 23 | inset 0 0 10px rgba(0, 0, 0, 1), 0 5px 20px rgba(0, 0, 0, 0.5), 24 | inset 0 0 15px rgba(0, 0, 0, 0.2); 25 | } 26 | 27 | &::before { 28 | content: ''; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 80px; 33 | height: 40px; 34 | background: linear-gradient(0deg, #000, gray); 35 | border-radius: 20px; 36 | box-shadow: 0 0 0 1px #232323; 37 | transform: scale(0.98, 0.96); 38 | transition: 0.5s; 39 | } 40 | 41 | &:checked::before { 42 | left: 40px; 43 | } 44 | 45 | &::after { 46 | content: ''; 47 | position: absolute; 48 | top: calc(50% - 2px); 49 | left: 64px; 50 | width: 4px; 51 | height: 4px; 52 | background: linear-gradient(0deg, gray, #000); 53 | border-radius: 50%; 54 | transition: 0.5s; 55 | } 56 | 57 | &:checked::after { 58 | background: cyan; 59 | left: 104px; 60 | box-shadow: 0 0 5px aqua, 0 0 15px aqua; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /src/components/GlowingCheckbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GlowingCheckbox'; 2 | -------------------------------------------------------------------------------- /src/components/GlowingRadio/GlowingRadio.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export interface GlowingRadioProps { 5 | className?: string; 6 | onChange?: (event: React.ChangeEvent) => void; 7 | options: ReadonlyArray<{ label?: string; value: string; name: string }>; 8 | value?: string; 9 | } 10 | 11 | export const GlowingRadio = memo( 12 | ({ className, onChange, options, value }) => { 13 | return ( 14 |
15 | radio 16 | {options.map((option) => ( 17 |
18 | 25 | 26 |
27 | ))} 28 |
29 | ); 30 | }, 31 | ); 32 | 33 | const Fieldset = styled.fieldset` 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | border-radius: 4px; 38 | text-transform: uppercase; 39 | `; 40 | 41 | const Legend = styled.legend` 42 | color: white; 43 | padding: 16px 8px; 44 | font-weight: 600; 45 | font-size: 24px; 46 | margin: 0 auto; 47 | `; 48 | 49 | const Radio = styled.input` 50 | position: relative; 51 | width: 120px; 52 | height: 40px; 53 | margin: 10px; 54 | outline: none; 55 | background: black; 56 | cursor: pointer; 57 | border-radius: 20px; 58 | appearance: none; 59 | box-shadow: -5px -5px 20px rgba(255, 255, 255, 0.1), 60 | 5px 5px 20px rgba(0, 0, 0, 0.1), 61 | inset -2px -2px 5px rgba(255, 255, 255, 0.1), 62 | inset 2px 2px 5px rgba(0, 0, 0, 0.5), 0 0 0 2px #3e3e3e; 63 | transition: 0.5s; 64 | 65 | &:checked { 66 | background: cyan; 67 | } 68 | 69 | &::before { 70 | content: ''; 71 | position: absolute; 72 | top: 0; 73 | left: 0; 74 | width: 80px; 75 | height: 40px; 76 | background: linear-gradient(to top, black, gray); 77 | border-radius: 20px; 78 | box-shadow: 0 0 0 1px dimgray; 79 | transform: scale(0.98, 0.96); 80 | transition: 0.5s; 81 | } 82 | 83 | &:checked::before { 84 | left: 40px; 85 | } 86 | 87 | &::after { 88 | content: ''; 89 | position: absolute; 90 | top: calc(50% - 2px); 91 | left: 65px; 92 | width: 4px; 93 | height: 4px; 94 | background: gray; 95 | border-radius: 50%; 96 | transition: 0.5s; 97 | } 98 | 99 | &:checked::after { 100 | left: calc(65px + 40px); 101 | background: cyan; 102 | box-shadow: 0 0 5px cyan, 0 0 15px cyan, 0 0 30px cyan; 103 | } 104 | `; 105 | -------------------------------------------------------------------------------- /src/components/GlowingRadio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GlowingRadio'; 2 | -------------------------------------------------------------------------------- /src/components/GlowingText/GlowingText.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export interface GlowingTextProps { 5 | label?: string; 6 | } 7 | 8 | export const GlowingText = memo(({ label }) => { 9 | return ( 10 |

11 | {label 12 | ?.split('') 13 | .map((s, i) => 14 | React.createElement(GlowStyled, { key: `s-${i}`, index: i }, s), 15 | )} 16 |

17 | ); 18 | }); 19 | 20 | const H1 = styled.h1` 21 | font-size: 12em; 22 | text-transform: uppercase; 23 | `; 24 | 25 | const animate = keyframes` 26 | 0%, 100% 27 | { 28 | color: white; 29 | filter: blur(1px); 30 | text-shadow: 0 0 10px Aqua; 31 | text-shadow: 0 0 20px Aqua; 32 | text-shadow: 0 0 40px Aqua; 33 | text-shadow: 0 0 80px Aqua; 34 | text-shadow: 0 0 120px Aqua; 35 | text-shadow: 0 0 200px Aqua; 36 | text-shadow: 0 0 300px Aqua; 37 | text-shadow: 0 0 400px Aqua; 38 | } 39 | 5%, 95% { 40 | color: #111; 41 | filter: blur(0px); 42 | text-shadow: none; 43 | } 44 | `; 45 | 46 | const GlowStyled = styled.span<{ index: number }>` 47 | text-transform: uppercase; 48 | display: table-cell; 49 | margin: 0; 50 | padding: 0; 51 | animation: ${animate} 2s linear infinite; 52 | animation-delay: ${(props) => `${props.index * 0.25}s`}; 53 | `; 54 | -------------------------------------------------------------------------------- /src/components/GlowingText/README.md: -------------------------------------------------------------------------------- 1 | ### `Glowing Text` 2 | 3 | ![glowing-text](https://user-images.githubusercontent.com/42884032/103140359-02bb6980-4729-11eb-97d9-47c57d91ffa8.gif) 4 | -------------------------------------------------------------------------------- /src/components/GlowingText/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GlowingText'; 2 | -------------------------------------------------------------------------------- /src/components/HalfCircularBackground/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HalfCircularBackground'; 2 | -------------------------------------------------------------------------------- /src/components/HeartAnimation/HeartAnimation.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const HeartAnimation = memo(() => { 5 | return ; 6 | }); 7 | 8 | const animtaion = keyframes` 9 | 0% { 10 | transform: rotate(45deg) scale(1); 11 | } 12 | 20% { 13 | transform: rotate(45deg) scale(0.8); 14 | } 15 | 40% { 16 | transform: rotate(45deg) scale(1.2); 17 | } 18 | 60% { 19 | transform: rotate(45deg) scale(1); 20 | } 21 | 80% { 22 | transform: rotate(45deg) scale(1.3); 23 | } 24 | 100% { 25 | transform: rotate(45deg) scale(1); 26 | } 27 | `; 28 | 29 | const Heart = styled.div` 30 | position: relative; 31 | width: 200px; 32 | height: 200px; 33 | background: red; 34 | transform: rotate(45deg); 35 | border-bottom-right-radius: 20px; 36 | box-shadow: 10px 10px 80px red; 37 | animation: ${animtaion} 1.5s linear infinite; 38 | 39 | &::before { 40 | content: ''; 41 | position: absolute; 42 | top: -99px; 43 | width: 200px; 44 | height: 100px; 45 | background: red; 46 | border-top-left-radius: 100px; 47 | border-top-right-radius: 100px; 48 | } 49 | 50 | &::after { 51 | content: ''; 52 | position: absolute; 53 | left: -99px; 54 | width: 100px; 55 | height: 200px; 56 | background: red; 57 | border-top-left-radius: 100px; 58 | border-bottom-left-radius: 100px; 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/HeartAnimation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HeartAnimation'; 2 | -------------------------------------------------------------------------------- /src/components/HideScrollOnScrollDown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HideScrollOnScrollDown'; 2 | -------------------------------------------------------------------------------- /src/components/HoverEffectCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HoverEffectCard'; 2 | -------------------------------------------------------------------------------- /src/components/ListHoverEffects/ListHoverEffects.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const ListHoverEffects = memo(() => { 5 | return ( 6 | 7 | Jacob's Playground 8 |
    9 |
  • 10 | 제이콥이 배고프다. 11 |
  • 12 |
  • 13 | 제이콥은 개발자다. 14 |
  • 15 |
  • 16 | 제이콥은 먹는게 좋다. 17 |
  • 18 |
  • 19 | 제이콥은 옷이 좋다. 20 |
  • 21 |
  • 22 | 제이콥은 성격이 좋다. 23 |
  • 24 |
  • 25 | 제이콥은 일한다. 26 |
  • 27 |
  • 28 | 제이콥이 코딩한다. 29 |
  • 30 |
31 |
32 | ); 33 | }); 34 | 35 | const List = styled.div` 36 | position: relative; 37 | `; 38 | 39 | const Title = styled.h2` 40 | position: relative; 41 | color: white; 42 | font-weight: 600; 43 | letter-spacing: 1px; 44 | text-transform: uppercase; 45 | `; 46 | 47 | const Ul = styled.ul` 48 | position: relative; 49 | width: 100%; 50 | padding: 0; 51 | `; 52 | 53 | const Li = styled.li` 54 | position: relative; 55 | left: 0; 56 | color: gray; 57 | list-style: none; 58 | margin: 4px 0; 59 | border-left: 2px solid cyan; 60 | transition: 0.1s; 61 | cursor: pointer; 62 | 63 | &:hover { 64 | left: 10px; 65 | } 66 | 67 | &::before { 68 | content: ''; 69 | position: absolute; 70 | width: 100%; 71 | height: 100%; 72 | background: cyan; 73 | transform: scaleX(0); 74 | transform-origin: left; 75 | transition: 0.5s; 76 | } 77 | 78 | &:hover::before { 79 | transform: scaleX(1); 80 | } 81 | `; 82 | 83 | const Item = styled.span` 84 | position: relative; 85 | padding: 8px; 86 | display: inline-block; 87 | z-index: 1; 88 | transition: 0.5s; 89 | 90 | &:hover { 91 | color: white; 92 | } 93 | `; 94 | -------------------------------------------------------------------------------- /src/components/ListHoverEffects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ListHoverEffects'; 2 | -------------------------------------------------------------------------------- /src/components/Loader/GradientLoader.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const GradientLoader = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }); 15 | 16 | const Loader = styled.div` 17 | position: relative; 18 | overflow: hidden; 19 | width: 200px; 20 | height: 200px; 21 | border: 4px solid white; 22 | border-radius: 50%; 23 | box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.1), inset -10px -10px 15px rgba(255, 255, 255, 0.5), 24 | inset 10px 10px 10px rgba(0, 0, 0, 0.1); 25 | 26 | &::before { 27 | content: ''; 28 | position: absolute; 29 | top: 25px; 30 | left: 25px; 31 | bottom: 25px; 32 | right: 25px; 33 | background: white; 34 | border-radius: 50%; 35 | border: 4px solid white; 36 | box-shadow: inset -10px -10px 15px rgba(255, 255, 255, 0.5), inset 10px 10px 10px rgba(0, 0, 0, 0.1); 37 | } 38 | `; 39 | 40 | const animation = keyframes` 41 | 0% { 42 | transform: rotate(0deg); 43 | } 44 | 100% { 45 | transform: rotate(360deg); 46 | } 47 | `; 48 | 49 | const Gradient = styled.span` 50 | position: absolute; 51 | width: 100%; 52 | height: 100%; 53 | border: 50%; 54 | background: linear-gradient(cyan, lime, yellow); 55 | z-index: -1; 56 | filter: blur(20px); 57 | animation: ${animation} 0.6s linear infinite; 58 | `; 59 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const Loader = memo(() => { 5 | return ; 6 | }); 7 | 8 | const animate = keyframes` 9 | 0% { 10 | transform: rotate(0deg); 11 | filter: hue-rotate(0deg); 12 | } 13 | 100% { 14 | transform: rotate(360deg); 15 | filter: hue-rotate(360deg); 16 | } 17 | `; 18 | 19 | const GradientLoader = styled.div` 20 | position: relative; 21 | width: 150px; 22 | height: 150px; 23 | border-radius: 50%; 24 | background: linear-gradient(45deg, transparent, transparent 40%, cyan); 25 | animation: ${animate} 2s linear infinite; 26 | 27 | &::before { 28 | content: ''; 29 | position: absolute; 30 | top: 6px; 31 | left: 6px; 32 | bottom: 6px; 33 | right: 6px; 34 | background: black; 35 | border-radius: 50%; 36 | z-index: 1000; 37 | } 38 | &::after { 39 | content: ''; 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | bottom: 0; 44 | right: 0; 45 | background: linear-gradient(45deg, transparent, transparent 40%, cyan); 46 | border-radius: 50%; 47 | z-index: 1; 48 | filter: blur(30px); 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /src/components/Loader/TwoRingLoader.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { css, keyframes } from 'styled-components'; 3 | 4 | export const TwoRingLoader = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }); 16 | 17 | const LoaderContainer = styled.div` 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | transform: translate(-50%, -50%); 22 | width: 200px; 23 | height: 200px; 24 | box-sizing: border-box; 25 | `; 26 | 27 | const ringCommonStyle = css` 28 | position: absolute; 29 | border: 2px solid dimgray; 30 | background: black; 31 | border-radius: 50%; 32 | `; 33 | 34 | const animation = keyframes` 35 | 0% { 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | transform: rotate(360deg); 40 | } 41 | `; 42 | 43 | const OuterRing = styled.div` 44 | ${ringCommonStyle}; 45 | top: 0; 46 | left: 0; 47 | right: 0; 48 | bottom: 0; 49 | border-left: 2px solid lime; 50 | border-top: 2px solid lime; 51 | animation: ${animation} 3s linear infinite; 52 | `; 53 | 54 | const InnerRing = styled.div` 55 | ${ringCommonStyle}; 56 | top: 30px; 57 | left: 30px; 58 | right: 30px; 59 | bottom: 30px; 60 | border-right: 2px solid deeppink; 61 | border-bottom: 2px solid deeppink; 62 | animation: ${animation} 3s reverse linear infinite; 63 | `; 64 | 65 | const circleCommonStyle = css` 66 | position: absolute; 67 | top: calc(50% - 1px); 68 | left: 50%; 69 | width: 50%; 70 | height: 2px; 71 | transform: rotate(-45deg); 72 | transform-origin: left; 73 | 74 | &::before { 75 | content: ''; 76 | position: absolute; 77 | width: 10px; 78 | height: 10px; 79 | border-radius: 50%; 80 | background: white; 81 | top: -4px; 82 | right: -6px; 83 | } 84 | `; 85 | 86 | const OuterRingCircle = styled.div` 87 | ${circleCommonStyle}; 88 | 89 | &::before { 90 | background: lime; 91 | box-shadow: 0 0 5px lime, 0 0 20px lime, 0 0 40px lime, 0 0 60px lime, 0 0 80px lime, 0 0 100px lime; 92 | } 93 | `; 94 | 95 | const InnerRingCircle = styled.div` 96 | ${circleCommonStyle}; 97 | 98 | &::before { 99 | background: deeppink; 100 | box-shadow: 0 0 5px deeppink, 0 0 20px deeppink, 0 0 40px deeppink, 0 0 60px deeppink, 0 0 80px deeppink, 101 | 0 0 100px deeppink; 102 | } 103 | `; 104 | -------------------------------------------------------------------------------- /src/components/Loader/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Loader'; 2 | export * from './TwoRingLoader'; 3 | export * from './GradientLoader'; 4 | -------------------------------------------------------------------------------- /src/components/LongTextShadow/LongTextShadow.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo, ReactText } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export interface LongTextShadowProps { 5 | label?: ReactText; 6 | } 7 | 8 | export const LongTextShadow = memo(({ label }) => { 9 | const getShadow = useMemo(() => { 10 | let shadow = ''; 11 | for (let i = 0; i < 400; i++) { 12 | shadow += (i !== 0 ? ',' : '') + i + 'px ' + i + 'px 0 gray'; 13 | } 14 | return shadow; 15 | }, []); 16 | 17 | return {label}; 18 | }); 19 | 20 | const Text = styled.h2<{ shadow: string }>` 21 | font-size: 5.5em; 22 | color: white; 23 | font-weight: 600; 24 | text-shadow: ${(props) => props.shadow}; 25 | text-transform: uppercase; 26 | `; 27 | -------------------------------------------------------------------------------- /src/components/LongTextShadow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LongTextShadow'; 2 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect, useCallback } from 'react'; 2 | import styled from 'styled-components'; 3 | import { MenuItem } from './MenuItem'; 4 | 5 | export type ItemsTypes = ReadonlyArray<{ 6 | label: string; 7 | value?: number | string | string[]; 8 | fn: (v?: number | string | string[]) => void; 9 | }>; 10 | 11 | type Props = { 12 | className?: string; 13 | list: ReadonlyArray<{ 14 | text: string; 15 | items?: ItemsTypes; 16 | }>; 17 | }; 18 | 19 | export const Menu = memo(({ className, list }) => { 20 | const [openedIndex, setOpenedIndex] = useState(null); 21 | 22 | const togglePopover = useCallback( 23 | (index: number) => (e: React.MouseEvent) => { 24 | e.preventDefault(); 25 | e.stopPropagation(); 26 | setOpenedIndex(openedIndex === index ? null : index); 27 | }, 28 | [openedIndex] 29 | ); 30 | 31 | const closeAll = useCallback((e: MouseEvent) => { 32 | if (e.target !== null && (e.target as HTMLElement).id !== 'jacob-context-menu') { 33 | setOpenedIndex(null); 34 | } 35 | }, []); 36 | 37 | useEffect(() => { 38 | document.body.addEventListener('click', closeAll); 39 | return () => { 40 | document.body.removeEventListener('click', closeAll); 41 | }; 42 | }); 43 | 44 | return ( 45 |
46 | {list.map(({ text, items = [] }, i) => ( 47 | 48 | {items.length > 0 && 49 | items.map(({ label, value, fn }) => { 50 | const handleClick = () => { 51 | fn(value); 52 | setOpenedIndex(null); 53 | }; 54 | return ( 55 | // https://courses.cs.washington.edu/courses/cse154/17au/styleguide/html-css/naming-conventions-html.html#class-id-names 56 | 57 | {label} 58 | 59 | ); 60 | })} 61 | 62 | ))} 63 |
64 | ); 65 | }); 66 | 67 | const ContextMenu = styled.span` 68 | display: flex; 69 | flex-direction: column; 70 | padding: 8px 0; 71 | margin: 0 16px; 72 | transition: all 0.3s ease; 73 | border-radius: 4px; 74 | cursor: pointer; 75 | 76 | &:hover { 77 | margin: 0; 78 | padding-left: 16px; 79 | padding-right: 16px; 80 | background: lightgray; 81 | } 82 | `; 83 | -------------------------------------------------------------------------------- /src/components/Menu/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | type Props = { 5 | text: string; 6 | open?: boolean; 7 | onToggle?: (event: React.MouseEvent) => void; 8 | }; 9 | 10 | export const MenuItem: FC = memo(({ text, children, open = false, onToggle }) => { 11 | return ( 12 | 13 | 14 | <Summary>{text}</Summary> 15 | 16 | {children && {children}} 17 | 18 | ); 19 | }); 20 | 21 | const MenuItemContainer = styled.div<{ open: boolean }>` 22 | position: relative; 23 | width: 100%; 24 | margin: 2px 0; 25 | 26 | &::after { 27 | position: absolute; 28 | border-radius: 50%; 29 | bottom: -4px; 30 | right: -4px; 31 | border: 1px solid rgba(0, 0, 0, 0.2); 32 | padding: 2px; 33 | width: 18px; 34 | height: 18px; 35 | font-size: 12px; 36 | text-align: center; 37 | font-weight: 800; 38 | background: #fff; 39 | z-index: 2; 40 | } 41 | 42 | &:hover::after { 43 | content: '+'; 44 | } 45 | 46 | &[open]::after { 47 | content: 'ㅡ'; 48 | } 49 | `; 50 | 51 | const HiddenContextMenuItem = styled.span<{ open: boolean }>` 52 | position: absolute; 53 | color: black; 54 | background: #fff; 55 | border: 1px solid lightgray; 56 | border-radius: 4px; 57 | top: 100%; 58 | right: 10px; 59 | /* fallback for ie */ 60 | width: 60%; 61 | width: min(60%, 100px); 62 | padding: 8px; 63 | margin: 0; 64 | display: ${(props) => (props.open ? 'block' : 'none')}; 65 | z-index: 1; 66 | `; 67 | 68 | const Title = styled.details` 69 | position: relative; 70 | color: white; 71 | padding: 8px 0; 72 | margin: 0 24px; 73 | transition: all 0.3s ease; 74 | 75 | &[open], 76 | &:hover { 77 | padding-left: 24px; 78 | padding-right: 24px; 79 | margin: 0; 80 | background: gray; 81 | border-radius: 8px; 82 | } 83 | 84 | @media (pointer: fine) { 85 | cursor: pointer; 86 | } 87 | `; 88 | 89 | const Summary = styled.summary` 90 | padding: 8px; 91 | outline: none; 92 | font-weight: 600; 93 | 94 | &::-webkit-details-marker { 95 | display: none; 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /src/components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Menu'; 2 | -------------------------------------------------------------------------------- /src/components/MiniWavy/MiniWavy.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const MiniWavy = memo(() => { 5 | return ; 6 | }); 7 | 8 | const animation = keyframes` 9 | 0% { 10 | transform: rotate(0deg); 11 | } 12 | 100% { 13 | transform: rotate(360deg); 14 | } 15 | `; 16 | 17 | const animation2 = keyframes` 18 | 0% { 19 | transform: rotate(360deg); 20 | } 21 | 100% { 22 | transform: rotate(0deg); 23 | } 24 | `; 25 | const MiniCircle = styled.div` 26 | position: relative; 27 | width: 200px; 28 | height: 200px; 29 | background: aqua; 30 | border-radius: 50%; 31 | box-shadow: inset 0 0 50px black, inset 0 0 1px black, inset 0 0 2px black, inset 0 0 3px black, inset 0 0 4px black; 32 | 33 | overflow: hidden; 34 | 35 | &::before { 36 | content: ''; 37 | position: absolute; 38 | top: -150%; 39 | left: -50%; 40 | width: 200%; 41 | height: 200%; 42 | border-radius: 40%; 43 | background: #111; 44 | opacity: 0.8; 45 | animation: ${animation} 12s linear infinite; 46 | } 47 | 48 | &::after { 49 | content: ''; 50 | position: absolute; 51 | top: -150%; 52 | left: -50%; 53 | width: 200%; 54 | height: 200%; 55 | border-radius: 40%; 56 | background: #111; 57 | opacity: 0.9; 58 | animation: ${animation2} 5s linear infinite; 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/MiniWavy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MiniWavy'; 2 | -------------------------------------------------------------------------------- /src/components/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Modal'; 2 | -------------------------------------------------------------------------------- /src/components/MouseCursor/DotMouseCursor.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const DotMouseCursor = memo(() => { 5 | const ref = useRef(null); 6 | 7 | const handleMousemove = useCallback((event: MouseEvent) => { 8 | if (ref === null || ref.current === null) { 9 | return; 10 | } 11 | ref.current.style.left = event.clientX + 'px'; 12 | ref.current.style.top = event.clientY + 'px'; 13 | }, []); 14 | 15 | useEffect(() => { 16 | if (typeof window === 'undefined') { 17 | return; 18 | } 19 | window.addEventListener('mousemove', handleMousemove); 20 | return () => window.removeEventListener('mousemove', handleMousemove); 21 | }, [handleMousemove]); 22 | 23 | const handleMouseOver = useCallback(() => { 24 | if (ref === null || ref.current === null) { 25 | return; 26 | } 27 | ref.current.style.cssText = `width: 100px; height: 100px; border: 2px dashed white`; 28 | }, []); 29 | 30 | const handleMouseOut = useCallback(() => { 31 | if (ref === null || ref.current === null) { 32 | return; 33 | } 34 | ref.current.style.cssText = `width: 20px; height: 20px`; 35 | }, []); 36 | 37 | return ( 38 | <> 39 | 40 | Jacob's Playground! 41 | 42 | 43 | 44 | ); 45 | }); 46 | 47 | const Cursor = styled.span` 48 | position: absolute; 49 | width: 20px; 50 | height: 20px; 51 | border: 2px solid white; 52 | box-sizing: border-box; 53 | transition: 0.1s; 54 | transform: translate(-50%, -50%); 55 | border-radius: 50%; 56 | pointer-events: none; 57 | `; 58 | 59 | const Title = styled.h2` 60 | color: white; 61 | 62 | &:hover { 63 | color: red; 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /src/components/MouseCursor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DotMouseCursor'; 2 | -------------------------------------------------------------------------------- /src/components/MoveSheep/Hill.ts: -------------------------------------------------------------------------------- 1 | export type DotType = { x1: number, y1: number, x2: number, y2: number, x3: number, y3: number } 2 | export type DotsType = Array 3 | 4 | export class Hill { 5 | color: string; 6 | speed: number; 7 | total: number; 8 | stageWidth: number = 0; 9 | stageHeight: number = 0; 10 | points: Array<{ x: number, y: number}> = []; 11 | gap: number = 0; 12 | 13 | constructor(color: string, speed: number, total: number) { 14 | this.color = color; 15 | this.speed = speed; 16 | this.total = total; 17 | } 18 | 19 | resize(stageWidth: number, stageHeight: number) { 20 | this.stageWidth = stageWidth; 21 | this.stageHeight = stageHeight; 22 | 23 | this.gap = Math.ceil(this.stageWidth / (this.total -2)); 24 | 25 | for(let i =0; i < this.total; i++) { 26 | this.points[i] = { 27 | x: i*this.gap, 28 | y: this.getY(), 29 | } 30 | } 31 | } 32 | 33 | draw(ctx: CanvasRenderingContext2D | null) { 34 | if(ctx === null) return; 35 | 36 | ctx.fillStyle = this.color; 37 | ctx.beginPath(); 38 | 39 | let cur = this.points[0]; 40 | let prev = cur; 41 | 42 | let dots: DotsType = []; 43 | 44 | if(cur.x > -this.gap) { 45 | this.points.unshift({ 46 | x: -(this.gap * 2), 47 | y: this.getY() 48 | }) 49 | } else if(cur.x > this.stageWidth + this.gap) { 50 | this.points.splice(-1); 51 | } 52 | 53 | ctx.moveTo(cur.x, cur.y); 54 | 55 | let prevCx = cur.x; 56 | let prevCy = cur.y; 57 | 58 | this.points.forEach((hill) => { 59 | hill.x += this.speed 60 | const cx = (prev.x + hill.x) / 2; 61 | const cy = (prev.y + hill.y) / 2; 62 | ctx.quadraticCurveTo(prev.x, prev.y, cx,cy); 63 | 64 | dots.push({ 65 | x1: prevCx, 66 | y1: prevCy, 67 | x2: prev.x, 68 | y2: prev.y, 69 | x3: cx, 70 | y3: cy 71 | }) 72 | 73 | prev = hill; 74 | prevCx = cx; 75 | prevCy = cy; 76 | }); 77 | 78 | ctx.lineTo(prev.x, prev.y); 79 | ctx.lineTo(this.stageWidth, this.stageHeight); 80 | ctx.lineTo(this.points[0].x, this.stageHeight) 81 | ctx.fill(); 82 | 83 | return dots; 84 | } 85 | 86 | getY() { 87 | const min = this.stageHeight / 8; 88 | const max = this.stageHeight - min; 89 | return min + Math.random() * max; 90 | } 91 | } -------------------------------------------------------------------------------- /src/components/MoveSheep/MoveSheep.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import useScreenSize from '../../hooks/useScreenSize'; 4 | import { DotsType, Hill } from './Hill'; 5 | import { SheepController } from './SheepController'; 6 | import { Sun } from './Sun'; 7 | 8 | export const MoveSheep = memo(() => { 9 | const ref = useRef(null); 10 | const { stageWidth, stageHeight } = useScreenSize(); 11 | 12 | useEffect(() => { 13 | if (ref.current === null) return; 14 | const ctx = ref.current.getContext('2d'); 15 | 16 | const hills = [new Hill('#fd6bea', 0.2, 12), new Hill('#ff59c2', 0.5, 8), new Hill('#ff4674', 1.4, 6)]; 17 | const sheepController = new SheepController(); 18 | const sun = new Sun(); 19 | 20 | const resize = () => { 21 | if (ref.current === null) return; 22 | 23 | ref.current.width = stageWidth * 2; 24 | ref.current.height = stageHeight * 2; 25 | ctx?.scale(2, 2); 26 | 27 | sun.resize(stageWidth, stageHeight); 28 | 29 | hills.forEach((hill) => hill.resize(stageWidth, stageHeight)); 30 | sheepController.resize(stageWidth, stageHeight); 31 | }; 32 | 33 | const animate = (t: number) => { 34 | requestAnimationFrame(animate); 35 | 36 | ctx?.clearRect(0, 0, stageWidth, stageHeight); 37 | 38 | sun.draw(ctx, t); 39 | 40 | let dots: DotsType | undefined; 41 | hills.forEach((hill) => { 42 | dots = hill.draw(ctx); 43 | }); 44 | 45 | if (dots !== undefined) { 46 | sheepController.draw(ctx, t, dots); 47 | } 48 | }; 49 | 50 | window.addEventListener('resize', resize, false); 51 | resize(); 52 | requestAnimationFrame(animate); 53 | return () => { 54 | window.removeEventListener('resize', resize, false); 55 | }; 56 | }, [stageWidth, stageHeight]); 57 | 58 | return ; 59 | }); 60 | 61 | const Canvas = styled.canvas` 62 | width: 100%; 63 | height: 100%; 64 | `; 65 | -------------------------------------------------------------------------------- /src/components/MoveSheep/SheepController.ts: -------------------------------------------------------------------------------- 1 | import { DotsType } from './Hill'; 2 | import { Sheep } from './Sheep' 3 | import SheepImage from '../../assets/sheep.png' 4 | 5 | export class SheepController { 6 | stageWidth = 0; 7 | stageHeight = 0; 8 | img = new Image(); 9 | items: Array = []; 10 | cur = 0; 11 | isLoaded = false; 12 | 13 | constructor() { 14 | this.img.onload = () => { 15 | this.loaded(); 16 | } 17 | this.img.src = SheepImage; 18 | } 19 | 20 | resize(stageWidth: number, stageHeight: number) { 21 | this.stageWidth = stageWidth; 22 | this.stageHeight = stageHeight; 23 | } 24 | 25 | loaded() { 26 | this.isLoaded = true; 27 | this.addSheep(); 28 | } 29 | 30 | addSheep() { 31 | this.items.push(new Sheep(this.img, this.stageWidth)); 32 | } 33 | 34 | draw(ctx: CanvasRenderingContext2D | null, t: number, dots: DotsType) { 35 | if(!this.isLoaded) return; 36 | 37 | this.cur += 1; 38 | if(this.cur > 200) { 39 | this.cur = 0; 40 | this.addSheep(); 41 | } 42 | 43 | for(let i = this.items.length -1; i >= 0; i--) { 44 | const item = this.items[i]; 45 | if(item.x < -item.width) { 46 | this.items.splice(i, 1); 47 | } else { 48 | item.draw(ctx, t, dots) 49 | } 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/components/MoveSheep/Sun.ts: -------------------------------------------------------------------------------- 1 | type Position = { x: number; y: number }; 2 | 3 | export class Sun { 4 | radius = 200; 5 | stageWidth: number = 0; 6 | stageHeight: number = 0; 7 | x: number = 0; 8 | y: number = 0; 9 | 10 | total = 60; 11 | gap = 1 / this.total; 12 | orginPos: Array = []; 13 | pos: Array = []; 14 | 15 | fps = 30; 16 | fpsTime = 1000 / this.fps; 17 | time: number | undefined; 18 | 19 | constructor() { 20 | for (let i=0; i < this.total; i++) { 21 | const pos = this.getCirclePoint(this.radius, this.gap * i); 22 | this.orginPos[i] = pos; 23 | this.pos[i] = pos; 24 | } 25 | } 26 | 27 | 28 | resize(stageWidth: number, stageHeight: number) { 29 | this.stageWidth = stageWidth; 30 | this.stageHeight = stageHeight; 31 | 32 | this.x = this.stageWidth - this.radius -140; 33 | this.y = this.radius + 100; 34 | } 35 | 36 | draw(ctx: CanvasRenderingContext2D | null, t: number) { 37 | if(ctx === null) return; 38 | 39 | if(this.time === undefined) { 40 | this.time = t; 41 | } 42 | const now = t - this.time; 43 | if(now > this.fpsTime) { 44 | this.time = t; 45 | this.updatePoins(); 46 | } 47 | 48 | ctx.fillStyle = '#ffb200'; 49 | ctx.beginPath(); 50 | 51 | let pos = this.pos[0]; 52 | ctx.moveTo(pos.x + this.x, pos.y + this.y); 53 | for(let i=1; i(({ label }) => { 9 | return ( 10 | 11 | {label 12 | ?.split('') 13 | .map((s, i) => 14 | React.createElement(MoveText, { key: `s-${i}`, delay: i }, s), 15 | )} 16 | 17 | ); 18 | }); 19 | 20 | const Container = styled.h1` 21 | position: relative; 22 | `; 23 | 24 | const animate = keyframes` 25 | 0%, 40%, 100% 26 | { 27 | transform: translateY(0); 28 | } 29 | 20% { 30 | transform: translateY(-50px); 31 | } 32 | `; 33 | 34 | const MoveText = styled.span<{ delay: number }>` 35 | position: relative; 36 | font-size: 2em; 37 | color: white; 38 | text-transform: uppercase; 39 | display: inline-block; 40 | 41 | animation: ${animate} 2s ease-in-out infinite; 42 | animation-delay: ${(props) => `${props.delay * 0.1}s`}; 43 | `; 44 | -------------------------------------------------------------------------------- /src/components/MovingText/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MovingText'; 2 | -------------------------------------------------------------------------------- /src/components/NeonButton/NeonButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, ReactText } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export interface NeonButtonProps { 5 | label?: ReactText; 6 | } 7 | 8 | export const NeonButton = memo(({ label }) => { 9 | return ( 10 | 11 | {label} 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }); 19 | 20 | const NeonButtonContainer = styled.a` 21 | position: relative; 22 | display: inline-block; 23 | color: cyan; 24 | padding: 20px 24px; 25 | font-size: 24px; 26 | text-decoration: none; 27 | text-transform: uppercase; 28 | transition: 0.5s; 29 | letter-spacing: 0.5px; 30 | overflow: hidden; 31 | -webkit-box-reflect: below 1px linear-gradient(transparent, #0004); 32 | 33 | &:hover { 34 | background-color: cyan; 35 | color: black; 36 | box-shadow: 0 0 5px cyan, 0 0 25px cyan, 0 0 50px cyan, 0 0 100px cyan; 37 | } 38 | `; 39 | 40 | const topAnimate = keyframes` 41 | 0% { 42 | left: -100%; 43 | } 44 | 50%, 100% { 45 | left: 100%; 46 | } 47 | `; 48 | 49 | const rightAnimate = keyframes` 50 | 0% { 51 | top: -100%; 52 | } 53 | 50%, 100% { 54 | top: 100%; 55 | } 56 | `; 57 | 58 | const bottomAnimate = keyframes` 59 | 0% { 60 | right: -100%; 61 | } 62 | 50%, 100% { 63 | right: 100%; 64 | } 65 | `; 66 | 67 | const leftAnimate = keyframes` 68 | 0% { 69 | bottom: -100%; 70 | } 71 | 50%, 100% { 72 | bottom: 100%; 73 | } 74 | `; 75 | 76 | const Border = styled.span` 77 | position: absolute; 78 | display: block; 79 | 80 | &:nth-child(1) { 81 | top: 0; 82 | left: -100%; 83 | width: 100%; 84 | height: 2px; 85 | background: linear-gradient(90deg, transparent, cyan); 86 | animation: ${topAnimate} 1s linear infinite; 87 | } 88 | 89 | &:nth-child(2) { 90 | top: -100%; 91 | right: 0; 92 | width: 2px; 93 | height: 100%; 94 | background: linear-gradient(180deg, transparent, cyan); 95 | animation: ${rightAnimate} 1s linear infinite; 96 | animation-delay: 0.25s; 97 | } 98 | 99 | &:nth-child(3) { 100 | right: -100%; 101 | bottom: 0; 102 | width: 100%; 103 | height: 2px; 104 | background: linear-gradient(270deg, transparent, cyan); 105 | animation: ${bottomAnimate} 1s linear infinite; 106 | animation-delay: 0.5s; 107 | } 108 | 109 | &:nth-child(4) { 110 | bottom: -100%; 111 | left: 0; 112 | width: 2px; 113 | height: 100%; 114 | background: linear-gradient(360deg, transparent, cyan); 115 | animation: ${leftAnimate} 1s linear infinite; 116 | animation-delay: 0.75s; 117 | } 118 | `; 119 | -------------------------------------------------------------------------------- /src/components/NeonButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NeonButton'; 2 | -------------------------------------------------------------------------------- /src/components/NumberCounter/NumberCounter.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const NumberCounter = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | }); 13 | 14 | const Container = styled.div` 15 | color: white; 16 | border: 5px solid gray; 17 | padding: 0 28px; 18 | border-radius: 8px; 19 | margin: 0 10px; 20 | `; 21 | 22 | const animation = keyframes` 23 | 0% { 24 | top: 0; 25 | } 26 | 100% { 27 | top: -10em; 28 | } 29 | `; 30 | 31 | const NumberBox = styled.span` 32 | display: inline-block; 33 | overflow: hidden; 34 | height: 1em; 35 | line-height: 1em; 36 | font-weight: bold; 37 | font-size: 16em; 38 | 39 | text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0, 0, 0, 0.1), 40 | 0 0 5px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.3), 0 3px 5px rgba(0, 0, 0, 0.2), 41 | 0 5px 10px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.2), 0 20px 20px rgba(0, 0, 0, 0.15); 42 | 43 | &::after { 44 | position: relative; 45 | white-space: pre; 46 | content: '0\\A 1\\A 2\\A 3\\A 4\\A 5\\A 6\\A 7\\A 8\\A 9\\A'; 47 | } 48 | 49 | &:nth-child(3)::after { 50 | animation: ${animation} 1s steps(10) infinite; 51 | } 52 | 53 | &:nth-child(2)::after { 54 | animation: ${animation} 10s steps(10) infinite; 55 | } 56 | 57 | &:nth-child(1)::after { 58 | animation: ${animation} 100s steps(10) infinite; 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/NumberCounter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NumberCounter'; 2 | -------------------------------------------------------------------------------- /src/components/PasswordInput/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import React, { memo, useCallback, useState } from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | export const PasswordInput = memo(() => { 7 | const [value, setValue] = useState(); 8 | const [type, setType] = useState('password'); 9 | 10 | const handleChange = useCallback((event: React.ChangeEvent) => { 11 | setValue(event.target.value); 12 | }, []); 13 | 14 | const handleIconClick = useCallback(() => { 15 | setType((t) => (t === 'password' ? 'text' : 'password')); 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }); 27 | 28 | const Container = styled.div` 29 | position: relative; 30 | width: 400px; 31 | height: 60px; 32 | `; 33 | 34 | const Input = styled.input` 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 100%; 40 | border: none; 41 | background: transparent; 42 | padding: 0 20px; 43 | font-size: 18px; 44 | box-sizing: border-box; 45 | outline: none; 46 | border-radius: 8px; 47 | box-shadow: -4px -4px 10px white, inset 4px 4px 10px rgba(0, 0, 0, 0.05), inset -4px -4px 10px white, 48 | 4px 4px 10px rgba(0, 0, 0, 0.05); 49 | 50 | &::placeholder { 51 | color: lightgray; 52 | } 53 | `; 54 | 55 | const Toggle = styled.div` 56 | position: absolute; 57 | top: 50%; 58 | right: 20px; 59 | transform: translateY(-50%); 60 | width: 30px; 61 | height: 30px; 62 | cursor: pointer; 63 | `; 64 | 65 | const Icon = styled(FontAwesomeIcon)` 66 | color: gray; 67 | `; 68 | -------------------------------------------------------------------------------- /src/components/PasswordInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PasswordInput'; 2 | -------------------------------------------------------------------------------- /src/components/Portal/Portal.tsx: -------------------------------------------------------------------------------- 1 | import { memo, FC, useState, useEffect } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | interface PortalProps { 5 | className?: string; 6 | element?: string; 7 | } 8 | 9 | export const Portal: FC = memo(({ children, className = 'jacob-portal', element = 'div' }) => { 10 | const [container] = useState(() => { 11 | const el = document.createElement(element); 12 | el.classList.add(className); 13 | return el; 14 | }); 15 | 16 | useEffect(() => { 17 | if (typeof window === 'undefined') { 18 | return; 19 | } 20 | document.body.appendChild(container); 21 | return () => { 22 | document.body.removeChild(container); 23 | }; 24 | }, [container]); 25 | 26 | return ReactDOM.createPortal(children, container); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/Portal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Portal'; 2 | -------------------------------------------------------------------------------- /src/components/RangeInput/RangeInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useRef, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const RangeInput = memo(() => { 5 | const [value, setValue] = useState('0'); 6 | const ref = useRef(null); 7 | 8 | const handleChange = useCallback( 9 | (event: React.ChangeEvent) => { 10 | setValue(event.target.value); 11 | if (ref !== null && ref.current !== null) { 12 | ref.current.style.width = event.target.value + '%'; 13 | } 14 | }, 15 | [], 16 | ); 17 | return ( 18 | 19 | 20 | {value} 21 | 28 | 29 | ); 30 | }); 31 | 32 | const Container = styled.div` 33 | position: relative; 34 | `; 35 | 36 | const RangeValue = styled.h2` 37 | color: white; 38 | position: relative; 39 | display: block; 40 | font-size: 6em; 41 | z-index: 2; 42 | text-align: center; 43 | 44 | &::after { 45 | content: '%'; 46 | } 47 | `; 48 | 49 | const FillArea = styled.div` 50 | position: fixed; 51 | top: 0; 52 | left: 0; 53 | width: 0; 54 | height: 100%; 55 | background: cyan; 56 | z-index: 1; 57 | `; 58 | 59 | const Input = styled.input` 60 | position: relative; 61 | width: 400px; 62 | height: 16px; 63 | -webkit-appearance: none; 64 | background: whitesmoke; 65 | outline: none; 66 | border-radius: 8px; 67 | box-shadow: 0 0 0 1px dimgray, inset 0 0 5px black; 68 | z-index: 2; 69 | overflow: hidden; 70 | 71 | &::-webkit-slider-thumb { 72 | -webkit-appearance: none; 73 | width: 16px; 74 | height: 16px; 75 | border-radius: 50%; 76 | background: aqua; 77 | border: 1px solid gray; 78 | z-index: 2; 79 | box-shadow: -406px 0 0 400px aquamarine; 80 | } 81 | `; 82 | -------------------------------------------------------------------------------- /src/components/RangeInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RangeInput'; 2 | -------------------------------------------------------------------------------- /src/components/RippleEffectsButton/RippleEffectsButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | import throttle from 'lodash/throttle'; 4 | 5 | export const RippleEffectsButton = memo(() => { 6 | const ref = useRef(null); 7 | const [ripples, setRipples] = useState([]); 8 | 9 | const handleMouseEnter = useMemo( 10 | () => 11 | throttle((event: React.MouseEvent) => { 12 | event.stopPropagation(); 13 | if (ref === null || ref.current === null) { 14 | return; 15 | } 16 | if (event.currentTarget === null) { 17 | return; 18 | } 19 | const x = event.clientX - event.currentTarget.offsetLeft; 20 | const y = event.clientY - event.currentTarget.offsetTop; 21 | const Ripples = React.createElement(Ripple, { 22 | style: { top: `${y}px`, left: `${x}px` }, 23 | }); 24 | setRipples((s) => [...s, Ripples]); 25 | setTimeout(() => { 26 | setRipples((s) => s.filter((r) => r !== Ripples)); 27 | }, 1000); 28 | }, 500), 29 | [], 30 | ); 31 | 32 | const handleClick = useCallback(() => { 33 | alert('Hello, Jacob!'); 34 | }, []); 35 | 36 | return ( 37 | 41 | ); 42 | }); 43 | 44 | const animate = keyframes` 45 | 0% { 46 | width: 0; 47 | height: 0; 48 | opacity: 0.2; 49 | } 50 | 100% { 51 | width: 500px; 52 | height: 500px; 53 | opacity: 0.2; 54 | } 55 | `; 56 | 57 | const Ripple = styled.span` 58 | position: absolute; 59 | background: white; 60 | transform: translate(-50%, -50%); 61 | pointer-events: none; 62 | border-radius: 50%; 63 | animation: ${animate} 0.8s linear infinite; 64 | `; 65 | 66 | const Button = styled.button` 67 | position: relative; 68 | display: inline-block; 69 | padding: 12px 36px; 70 | margin: 10px; 71 | color: white; 72 | background: rgba(0, 0, 0, 0.2); 73 | border-radius: 4px; 74 | text-transform: uppercase; 75 | outline-style: none; 76 | border: 1px solid white; 77 | overflow: hidden; 78 | cursor: pointer; 79 | `; 80 | -------------------------------------------------------------------------------- /src/components/RippleEffectsButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RippleEffectsButton'; 2 | -------------------------------------------------------------------------------- /src/components/ScrollDownIndicator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ScrollDownIndicator'; 2 | -------------------------------------------------------------------------------- /src/components/ScrollProgressBar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ScrollProgressBar'; 2 | -------------------------------------------------------------------------------- /src/components/ShadowEffects/ShadowEffects.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const ShadowEffects = memo(() => { 5 | return ; 6 | }); 7 | 8 | const Circle = styled.div` 9 | position: relative; 10 | width: 200px; 11 | height: 200px; 12 | background: transparent; 13 | border-radius: 50%; 14 | box-shadow: 0 0 0 40px rgba(0, 255, 255, 0.3), 15 | 0 0 0 80px rgba(0, 255, 255, 0.3), 0 0 0 120px rgba(0, 255, 255, 0.3), 16 | 0 0 0 160px rgba(0, 255, 255, 0.3), 0 0 0 200px rgba(0, 255, 255, 0.3), 17 | 0 0 0 200px rgba(0, 255, 255, 0.1); 18 | `; 19 | -------------------------------------------------------------------------------- /src/components/ShadowEffects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ShadowEffects'; 2 | -------------------------------------------------------------------------------- /src/components/SidebarMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SidebarMenu'; 2 | -------------------------------------------------------------------------------- /src/components/Skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { css, keyframes } from 'styled-components'; 3 | 4 | export const Skeleton = memo(() => { 5 | return ( 6 | 7 |
8 | 9 |
10 | 11 | 12 |
13 |
14 |
15 | ); 16 | }); 17 | 18 | const animation = keyframes` 19 | 0% { 20 | opacity: 1; 21 | } 22 | 50% { 23 | opacity: 0.4; 24 | } 25 | 100% { 26 | opacity: 1; 27 | } 28 | `; 29 | 30 | export const SKELETON_ANIMATION = css` 31 | animation: ${animation} 1.5s ease-in-out 0.5s infinite; 32 | `; 33 | 34 | const Card = styled.div` 35 | max-width: 320px; 36 | background: whitesmoke; 37 | padding: 28px; 38 | border-radius: 8px; 39 | `; 40 | 41 | const Header = styled.div` 42 | display: flex; 43 | align-items: center; 44 | `; 45 | 46 | const Img = styled.div` 47 | height: 64px; 48 | width: 64px; 49 | background: lightgray; 50 | border-radius: 50%; 51 | 52 | ${SKELETON_ANIMATION}; 53 | `; 54 | 55 | const Details = styled.div` 56 | margin-left: 12px; 57 | `; 58 | 59 | const Line = styled.div` 60 | width: 100%; 61 | margin: 8px 0; 62 | background: lightgray; 63 | border-radius: 8px; 64 | 65 | &:nth-child(1) { 66 | width: 80px; 67 | height: 16px; 68 | } 69 | 70 | &:nth-child(2) { 71 | width: 160px; 72 | height: 12px; 73 | } 74 | 75 | ${SKELETON_ANIMATION}; 76 | `; 77 | -------------------------------------------------------------------------------- /src/components/Skeleton/Skeleton2.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const Skeleton2 = memo(() => { 5 | return ; 6 | }); 7 | 8 | const animate = keyframes` 9 | 0% { 10 | transform: translateX(-80px); 11 | } 12 | 100% { 13 | transform: translateX(160px); 14 | } 15 | `; 16 | 17 | const Skeleton = styled.span` 18 | position: relative; 19 | height: 48px; 20 | width: 160px; 21 | background-color: rgb(211, 211, 211); 22 | overflow: hidden; 23 | border-radius: 4px; 24 | 25 | &::before { 26 | content: ''; 27 | position: absolute; 28 | transform: translateX(-80px); 29 | height: 48px; 30 | width: 100px; 31 | animation: ${animate} 1s infinite; 32 | z-index: 4; 33 | background-image: linear-gradient( 34 | to left, 35 | rgba(251, 251, 251, 0.05), 36 | rgba(251, 251, 251, 0.3), 37 | rgba(251, 251, 251, 0.6), 38 | rgba(251, 251, 251, 0.3), 39 | rgba(251, 251, 251, 0.05) 40 | ); 41 | } 42 | `; 43 | -------------------------------------------------------------------------------- /src/components/Skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Skeleton'; 2 | export * from './Skeleton2'; 3 | -------------------------------------------------------------------------------- /src/components/SlidingMenuIndicator/SlidingMenuIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useRef, useMemo } from 'react'; 2 | import styled from 'styled-components'; 3 | import throttle from 'lodash/throttle'; 4 | 5 | export const SlidingMenuIndicator = memo(() => { 6 | const markerRef = useRef(null); 7 | const linkRefs = useRef([]); 8 | 9 | const handleLinkRef = useCallback((ref: HTMLAnchorElement | null) => { 10 | if (ref !== null && !linkRefs.current.includes(ref)) { 11 | linkRefs.current.push(ref); 12 | } 13 | }, []); 14 | 15 | const moveIndicator = useMemo( 16 | () => 17 | throttle((ref: HTMLAnchorElement) => { 18 | if (markerRef === null || markerRef.current === null) { 19 | return; 20 | } 21 | markerRef.current.style.top = ref.offsetTop + 'px'; 22 | markerRef.current.style.width = ref.offsetWidth + 'px'; 23 | }, 150), 24 | [], 25 | ); 26 | 27 | useEffect(() => { 28 | linkRefs.current.forEach((ref) => { 29 | ref.addEventListener('mousemove', () => moveIndicator(ref)); 30 | 31 | return () => 32 | ref.removeEventListener('mousemove', () => moveIndicator(ref)); 33 | }); 34 | }, [moveIndicator]); 35 | 36 | return ( 37 | 38 | 39 | {['Home', 'About', 'Services', 'Portfolio', 'Team', 'Contact'].map( 40 | (el) => { 41 | return ( 42 | 43 | 44 | {el} 45 | 46 | 47 | ); 48 | }, 49 | )} 50 | 51 | ); 52 | }); 53 | 54 | const Menu = styled.ul` 55 | list-style: none; 56 | position: relative; 57 | display: flex; 58 | flex-direction: column; 59 | align-items: center; 60 | justify-content: center; 61 | `; 62 | 63 | const Marker = styled.div` 64 | position: absolute; 65 | top: 0; 66 | height: 50px; 67 | background: cyan; 68 | border-radius: 4px; 69 | transition: 0.2s; 70 | `; 71 | 72 | const Item = styled.li` 73 | list-style: none; 74 | `; 75 | 76 | const Link = styled.a` 77 | position: relative; 78 | font-size: 2em; 79 | color: white; 80 | text-decoration: none; 81 | display: inline-block; 82 | padding: 0 20px; 83 | margin: 10px 0; 84 | `; 85 | -------------------------------------------------------------------------------- /src/components/SlidingMenuIndicator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SlidingMenuIndicator'; 2 | -------------------------------------------------------------------------------- /src/components/StickyElementsScrollingEffects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StickyElementsScrollingEffects'; 2 | -------------------------------------------------------------------------------- /src/components/TextArea/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useRef, ReactText, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export interface TextAreaProp { 5 | className?: string; 6 | value?: ReactText | ReadonlyArray; 7 | placeholder?: string; 8 | onChange?: (event: React.ChangeEvent) => void; 9 | } 10 | 11 | export const TextArea = memo( 12 | ({ className, value, onChange, placeholder }) => { 13 | const ref = useRef(null); 14 | 15 | useEffect(() => { 16 | if (ref === null || ref.current === null) { 17 | return; 18 | } 19 | ref.current.style.height = '38px'; 20 | ref.current.style.height = ref.current.scrollHeight + 'px'; 21 | }, []); 22 | 23 | const handleResizeHeight = useCallback(() => { 24 | if (ref === null || ref.current === null) { 25 | return; 26 | } 27 | ref.current.style.height = '38px'; 28 | ref.current.style.height = ref.current.scrollHeight + 'px'; 29 | }, []); 30 | 31 | return ( 32 | <> 33 | 34 | Automatic height-adjusted TextArea 35 | <br /> 36 | <Sub>by JACOB</Sub> 37 | 38 | 47 | 48 | ); 49 | }, 50 | ); 51 | 52 | const Title = styled.h2` 53 | color: white; 54 | text-align: center; 55 | `; 56 | 57 | const Sub = styled.span` 58 | color: dimgray; 59 | font-weight: 600; 60 | `; 61 | 62 | const AutoResizeTextArea = styled.textarea` 63 | resize: none; 64 | overflow: hidden; 65 | padding: 12px; 66 | display: block; 67 | outline: none; 68 | min-height: 38px; 69 | border-radius: 4px; 70 | caret-color: lightskyblue; 71 | box-sizing: border-box; 72 | line-height: 20px; 73 | 74 | &:focus { 75 | background: azure; 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /src/components/TextArea/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextArea'; 2 | -------------------------------------------------------------------------------- /src/components/TextTyping/TextTyping.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const TextTyping = memo(() => { 5 | const text = 'jacob css text typing...'; 6 | 7 | return ( 8 | 9 | {text} 10 | 11 | ); 12 | }); 13 | 14 | const animation = keyframes` 15 | 0% { 16 | width: 0; 17 | } 18 | 50% { 19 | width: 100%; 20 | } 21 | 100% { 22 | width: 0; 23 | } 24 | `; 25 | 26 | const TypingAnimation = styled.h1<{ textLength: number }>` 27 | margin: 0; 28 | position: absolute; 29 | top: 40%; 30 | text-transform: uppercase; 31 | font-family: consolas; 32 | letter-spacing: 5px; 33 | color: transparent; 34 | white-space: nowrap; 35 | 36 | &::before { 37 | content: attr(data-text); 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | color: white; 44 | overflow: hidden; 45 | border-right: 1px solid white; 46 | animation: ${animation} 8s steps(${(props) => props.textLength}) infinite; 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/components/TextTyping/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextTyping'; 2 | -------------------------------------------------------------------------------- /src/components/ThreeDimensionBackground/ThreeDimensionBackground.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const ThreeDimensionBackground = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }); 44 | 45 | const Background = styled.div` 46 | position: fixed; 47 | width: 100%; 48 | height: 100%; 49 | overflow: hidden; 50 | `; 51 | 52 | const animation = keyframes` 53 | 0% { 54 | transform: rotateX(0deg) rotateY(0deg); 55 | } 56 | 100% { 57 | transform: rotateX(360deg) rotateY(360deg); 58 | } 59 | `; 60 | 61 | const Rotate = styled.div` 62 | position: absolute; 63 | top: calc(50% - 200px); 64 | left: calc(50% - 200px); 65 | width: 400px; 66 | height: 400px; 67 | transform-style: preserve-3d; 68 | animation: ${animation} 20s linear infinite; 69 | zoom: 5; 70 | `; 71 | 72 | const Sphere = styled.div` 73 | position: absolute; 74 | top: 0; 75 | left: 0; 76 | width: 100%; 77 | height: 100%; 78 | transform-style: preserve-3d; 79 | 80 | &:nth-child(2) { 81 | transform: rotate(90deg); 82 | } 83 | &:nth-child(3) { 84 | transform: rotate(45deg); 85 | } 86 | &:nth-child(4) { 87 | transform: rotate(-45deg); 88 | } 89 | `; 90 | 91 | const SphereItem = styled.span` 92 | position: absolute; 93 | top: 0; 94 | left: 0; 95 | width: 100%; 96 | height: 100%; 97 | transform-style: preserve-3d; 98 | background: radial-gradient(lightgray, white); 99 | border-radius: 50%; 100 | 101 | &:nth-child(1) { 102 | transform: rotateY(0deg); 103 | } 104 | &:nth-child(2) { 105 | transform: rotateY(30deg); 106 | } 107 | &:nth-child(3) { 108 | transform: rotateY(60deg); 109 | } 110 | &:nth-child(4) { 111 | transform: rotateY(90deg); 112 | } 113 | &:nth-child(5) { 114 | transform: rotateY(120deg); 115 | } 116 | &:nth-child(6) { 117 | transform: rotateY(150deg); 118 | } 119 | `; 120 | -------------------------------------------------------------------------------- /src/components/ThreeDimensionBackground/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ThreeDimensionBackground'; 2 | -------------------------------------------------------------------------------- /src/components/ThreeDimensionDebitCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ThreeDimensionDebitCard'; 2 | -------------------------------------------------------------------------------- /src/components/Timeline/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Timeline'; 2 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const Tooltip = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | Jacob One 10 | 11 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Deserunt atque voluptatibus possimus esse officia. 12 | 13 | 14 | 15 | 16 | 17 | 18 | Jacob Two 19 | 20 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Deserunt atque voluptatibus possimus esse officia. 21 | 22 | 23 | 24 | 25 | 26 | 27 | Jacob Three 28 | 29 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Deserunt atque voluptatibus possimus esse officia. 30 | 31 | 32 | 33 | 34 | ); 35 | }); 36 | 37 | const List = styled.ul` 38 | margin: 0; 39 | padding: 0; 40 | display: flex; 41 | `; 42 | 43 | const ContentsContainer = styled.div` 44 | position: absolute; 45 | bottom: 56px; 46 | width: 400px; 47 | background: white; 48 | padding: 12px 16px; 49 | box-sizing: border-box; 50 | border-radius: 4px; 51 | visibility: hidden; 52 | transition: 0.5s; 53 | opacity: 0; 54 | transform: translate(-50%, -50px); 55 | 56 | &::before { 57 | content: ''; 58 | position: absolute; 59 | width: 28px; 60 | height: 28px; 61 | background: white; 62 | bottom: -14px; 63 | right: 178px; 64 | transform: rotate(45deg); 65 | } 66 | `; 67 | 68 | const ListItem = styled.li` 69 | position: relative; 70 | list-style: none; 71 | width: 20px; 72 | height: 20px; 73 | background: lightgray; 74 | margin: 0 20px; 75 | border-radius: 50%; 76 | transition: 0.5s; 77 | cursor: pointer; 78 | 79 | &:hover { 80 | background: cyan; 81 | box-shadow: 0 0 0 4px black, 0 0 0 6px cyan; 82 | 83 | ${ContentsContainer} { 84 | visibility: visible; 85 | opacity: 1; 86 | transform: translate(-50%, 0); 87 | } 88 | } 89 | `; 90 | 91 | const Title = styled.h2` 92 | margin: 0 0 8px; 93 | `; 94 | 95 | const Contents = styled.p` 96 | margin: 0 0 8px; 97 | `; 98 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Tooltip'; 2 | export * from './TooltipWithPortal'; 3 | -------------------------------------------------------------------------------- /src/components/WaveBorderCard/WaveBorderCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const WaveBorderCard = memo(() => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | Jacob's Card 12 |

13 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit 14 | odit porro ex voluptatum dolorum deserunt eligendi nostrum quisquam 15 | tempora earum nam alias error, aliquam nisi, odio autem? Neque, 16 | deleniti libero! 17 |

18 |
19 |
20 | ); 21 | }); 22 | 23 | const animate = keyframes` 24 | 0% { 25 | transform: rotate(0deg); 26 | } 27 | 100% { 28 | transform: rotate(360deg); 29 | } 30 | `; 31 | 32 | const animate2 = keyframes` 33 | 0% { 34 | transform: rotate(360deg); 35 | } 36 | 100% { 37 | transform: rotate(0deg); 38 | } 39 | `; 40 | 41 | const Border = styled.span` 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | height: 100%; 47 | border: 2px solid white; 48 | border-radius: 36% 52% 56% 40% / 40% 56% 60% 54%; 49 | transition: 0.5s; 50 | 51 | &:nth-child(1) { 52 | animation: ${animate} 6s linear infinite; 53 | } 54 | 55 | &:nth-child(2) { 56 | animation: ${animate2} 4s linear infinite; 57 | } 58 | 59 | &:nth-child(3) { 60 | animation: ${animate} 10s linear infinite; 61 | } 62 | `; 63 | 64 | const Title = styled.h2``; 65 | 66 | const P = styled.p``; 67 | 68 | const Card = styled.div` 69 | position: relative; 70 | width: 400px; 71 | height: 400px; 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | 76 | &:hover { 77 | ${Border} { 78 | border: none; 79 | background: cyan; 80 | } 81 | ${Title}, ${P} { 82 | text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, 83 | -1px 1px 0 black; 84 | } 85 | } 86 | `; 87 | 88 | const ContentContainer = styled.div` 89 | position: relative; 90 | padding: 40px 60px; 91 | color: white; 92 | text-align: center; 93 | z-index: 1000; 94 | `; 95 | -------------------------------------------------------------------------------- /src/components/WaveBorderCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './WaveBorderCard'; 2 | -------------------------------------------------------------------------------- /src/components/WavySection/WavySection.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | export const WavySection = memo(() => { 5 | return ( 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | Jacob's Wavy 14 | 15 |
16 | ); 17 | }); 18 | 19 | const Section = styled.section` 20 | position: relative; 21 | width: 100%; 22 | height: 100vh; 23 | overflow: hidden; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | `; 28 | 29 | const Wave = styled.div` 30 | position: absolute; 31 | left: 0; 32 | width: 100%; 33 | height: 100%; 34 | background: blue; 35 | `; 36 | 37 | const animate = keyframes` 38 | 0% { 39 | transform: translate(-50%, -75%) rotate(0deg); 40 | } 41 | 100% { 42 | transform: translate(-50%, -75%) rotate(360deg); 43 | } 44 | `; 45 | 46 | const Curve = styled.span` 47 | position: absolute; 48 | width: 325vh; 49 | height: 325vh; 50 | left: 50%; 51 | background: black; 52 | transform: translate(-50%, -75%); 53 | 54 | &:nth-child(1) { 55 | animation: ${animate} 5s linear infinite; 56 | border-radius: 45%; 57 | background: rgba(20, 20, 20, 1); 58 | } 59 | 60 | &:nth-child(2) { 61 | animation: ${animate} 10s linear infinite; 62 | border-radius: 40%; 63 | background: rgba(20, 20, 20, 0.5); 64 | } 65 | 66 | &:nth-child(3) { 67 | animation: ${animate} 16s linear infinite; 68 | border-radius: 42.5%; 69 | background: rgba(20, 20, 20, 0.5); 70 | } 71 | `; 72 | 73 | const Contents = styled.div` 74 | position: relative; 75 | z-index: 1; 76 | color: white; 77 | font-size: 4em; 78 | `; 79 | 80 | const Title = styled.h2``; 81 | -------------------------------------------------------------------------------- /src/components/WavySection/index.ts: -------------------------------------------------------------------------------- 1 | export * from './WavySection'; 2 | -------------------------------------------------------------------------------- /src/components/WebGlAndThreeJS/BasicScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useRef } from 'react'; 2 | import { BoxGeometry, Mesh, MeshBasicMaterial, PerspectiveCamera, Scene, WebGLRenderer } from 'three'; 3 | import { DomUtils, RefUtils } from '../../utils'; 4 | import { WebGLUtils } from '../../utils/WebGLUtils'; 5 | 6 | export const BasicScene = memo(() => { 7 | const ref = useRef(null); 8 | 9 | useEffect(() => { 10 | if (!RefUtils.notNull(ref) || !DomUtils.usableWindow()) { 11 | return; 12 | } 13 | const scene = new Scene(); 14 | const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 15 | 16 | const renderer = new WebGLRenderer(); 17 | renderer.setSize(window.innerWidth, window.innerHeight); 18 | ref.current!.appendChild(renderer.domElement); 19 | 20 | const geometry = new BoxGeometry(1, 1, 1); 21 | const material = new MeshBasicMaterial({ color: 0x00ff00 }); 22 | const cube = new Mesh(geometry, material); 23 | scene.add(cube); 24 | 25 | camera.position.z = 5; 26 | 27 | const animate = () => { 28 | requestAnimationFrame(animate); 29 | cube.rotation.x += 0.01; 30 | cube.rotation.y += 0.01; 31 | 32 | renderer.render(scene, camera); 33 | }; 34 | 35 | if (WebGLUtils.isWebGLAvailable()) { 36 | animate(); 37 | } else { 38 | const warning = WebGLUtils.getWebGLErrorMessage(); 39 | ref.current!.appendChild(warning); 40 | } 41 | }, []); 42 | 43 | return
; 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/WebGlAndThreeJS/BasicScene2.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useMemo, useRef } from 'react'; 2 | import { BoxGeometry, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three'; 3 | import { DomUtils, RefUtils } from '../../utils'; 4 | import { WebGLUtils } from '../../utils/WebGLUtils'; 5 | 6 | /** 7 | * BasicScene을 개선. 광원 추가, 메테리얼 변경 8 | */ 9 | 10 | export const BasicScene2 = memo(() => { 11 | const ref = useRef(null); 12 | const scene = useMemo(() => { 13 | return new Scene(); 14 | }, []); 15 | 16 | const cubes = useMemo(() => { 17 | const boxWidth = 1; 18 | const boxHeight = 1; 19 | const boxDepth = 1; 20 | const geometry = new BoxGeometry(boxWidth, boxHeight, boxDepth); 21 | 22 | const instansA = WebGLUtils.makeInstance(scene, geometry, 0x44aa88, 0); 23 | const instansB = WebGLUtils.makeInstance(scene, geometry, 0x8844aa, -2); 24 | const instansC = WebGLUtils.makeInstance(scene, geometry, 0xaa8844, 2); 25 | 26 | return [instansA.cube, instansB.cube, instansC.cube]; 27 | }, [scene]); 28 | 29 | useEffect(() => { 30 | if (!RefUtils.notNull(ref) || !DomUtils.usableWindow()) { 31 | return; 32 | } 33 | 34 | const fov = 100; 35 | const aspect = 2; // the canvas default 36 | const near = 0.1; 37 | const far = 5; 38 | const camera = new PerspectiveCamera(fov, aspect, near, far); 39 | camera.position.z = 3; 40 | 41 | const renderer = new WebGLRenderer(); 42 | renderer.setSize(window.innerWidth, window.innerHeight); 43 | ref.current!.appendChild(renderer.domElement); 44 | 45 | // 광원 추가 46 | const color = 0xffffff; 47 | const intensity = 1; 48 | const light = new DirectionalLight(color, intensity); 49 | light.position.set(-1, 2, 4); 50 | scene.add(light); 51 | 52 | const animate = (time: number) => { 53 | time *= 0.001; // convert time to seconds 54 | 55 | cubes.forEach((cube, ndx) => { 56 | const speed = 1 + ndx * 0.1; 57 | const rot = time * speed; 58 | cube.rotation.x = rot; 59 | cube.rotation.y = rot; 60 | }); 61 | renderer.render(scene, camera); 62 | requestAnimationFrame(animate); 63 | }; 64 | 65 | if (WebGLUtils.isWebGLAvailable()) { 66 | requestAnimationFrame(animate); 67 | } else { 68 | const warning = WebGLUtils.getWebGLErrorMessage(); 69 | ref.current!.appendChild(warning); 70 | } 71 | }, [cubes, scene]); 72 | 73 | return
; 74 | }); 75 | -------------------------------------------------------------------------------- /src/components/WebGlAndThreeJS/BasicScene3.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useMemo, useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { BoxGeometry, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three'; 4 | import { DomUtils, RefUtils } from '../../utils'; 5 | import { WebGLUtils } from '../../utils/WebGLUtils'; 6 | 7 | /** 8 | * 반응형 디자인 적용 9 | */ 10 | 11 | export const BasicScene3 = memo(() => { 12 | const ref = useRef(null); 13 | const scene = useMemo(() => { 14 | return new Scene(); 15 | }, []); 16 | 17 | const cubes = useMemo(() => { 18 | const boxWidth = 1; 19 | const boxHeight = 1; 20 | const boxDepth = 1; 21 | const geometry = new BoxGeometry(boxWidth, boxHeight, boxDepth); 22 | 23 | const instansA = WebGLUtils.makeInstance(scene, geometry, 0x44aa88, 0); 24 | const instansB = WebGLUtils.makeInstance(scene, geometry, 0x8844aa, -2); 25 | const instansC = WebGLUtils.makeInstance(scene, geometry, 0xaa8844, 2); 26 | 27 | return [instansA.cube, instansB.cube, instansC.cube]; 28 | }, [scene]); 29 | 30 | useEffect(() => { 31 | if (!RefUtils.notNull(ref) || !DomUtils.usableWindow()) { 32 | return; 33 | } 34 | 35 | const canvas = ref.current!; 36 | const renderer = new WebGLRenderer({ canvas }); 37 | 38 | const fov = 75; 39 | const aspect = 2; // the canvas default 40 | const near = 0.1; 41 | const far = 5; 42 | const camera = new PerspectiveCamera(fov, aspect, near, far); 43 | camera.position.z = 3; 44 | 45 | // 광원 추가 46 | const color = 0xffffff; 47 | const intensity = 1; 48 | const light = new DirectionalLight(color, intensity); 49 | light.position.set(-1, 2, 4); 50 | scene.add(light); 51 | 52 | const animate = (time: number) => { 53 | time *= 0.001; // convert time to seconds 54 | 55 | if (WebGLUtils.resizeRendererToDisplaySize(renderer)) { 56 | const canvas = renderer.domElement; 57 | camera.aspect = canvas.clientWidth / canvas.clientHeight; 58 | camera.updateProjectionMatrix(); 59 | } 60 | 61 | cubes.forEach((cube, ndx) => { 62 | const speed = 1 + ndx * 0.1; 63 | const rot = time * speed; 64 | cube.rotation.x = rot; 65 | cube.rotation.y = rot; 66 | }); 67 | renderer.render(scene, camera); 68 | requestAnimationFrame(animate); 69 | }; 70 | 71 | if (WebGLUtils.isWebGLAvailable()) { 72 | requestAnimationFrame(animate); 73 | } else { 74 | const warning = WebGLUtils.getWebGLErrorMessage(); 75 | ref.current!.appendChild(warning); 76 | } 77 | }, [cubes, scene]); 78 | 79 | return ; 80 | }); 81 | 82 | const Canvas = styled.canvas` 83 | display: block; 84 | width: 200px; 85 | height: 120px; 86 | float: left; 87 | `; 88 | -------------------------------------------------------------------------------- /src/components/WebGlAndThreeJS/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BasicScene'; 2 | export * from './BasicScene2'; 3 | export * from './BasicScene3'; 4 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GlowingText'; 2 | export * from './ClimbEffects'; 3 | export * from './LongTextShadow'; 4 | export * from './NeonButton'; 5 | export * from './GlowingCheckbox'; 6 | export * from './Banners'; 7 | export * from './Dropdown'; 8 | export * from './AnimatedEyesFollowMouseCursor'; 9 | export * from './WavySection'; 10 | export * from './SlidingMenuIndicator'; 11 | export * from './StickyElementsScrollingEffects'; 12 | export * from './ShadowEffects'; 13 | export * from './ListHoverEffects'; 14 | export * from './HalfCircularBackground'; 15 | export * from './RippleEffectsButton'; 16 | export * from './HideScrollOnScrollDown'; 17 | export * from './FakeScrollSpy'; 18 | export * from './MovingText'; 19 | export * from './CustomScrollbar'; 20 | export * from './ScrollProgressBar'; 21 | export * from './Modal'; 22 | export * from './ThreeDimensionBackground'; 23 | export * from './Accordion'; 24 | export * from './Loader'; 25 | export * from './ScrollDownIndicator'; 26 | export * from './RangeInput'; 27 | export * from './WaveBorderCard'; 28 | export * from './TextArea'; 29 | export * from './SidebarMenu'; 30 | export * from './Skeleton'; 31 | export * from './GlowingRadio'; 32 | export * from './Drag'; 33 | export * from './Timeline'; 34 | export * from './GlowingBackground'; 35 | export * from './Tooltip'; 36 | export * from './Portal'; 37 | export * from './ThreeDimensionDebitCard'; 38 | export * from './TextTyping'; 39 | export * from './PasswordInput'; 40 | export * from './HoverEffectCard'; 41 | export * from './GhostText'; 42 | export * from './MiniWavy'; 43 | export * from './HeartAnimation'; 44 | export * from './MouseCursor'; 45 | export * from './ClipPathCircle'; 46 | export * from './NumberCounter'; 47 | export * from './WebGlAndThreeJS'; 48 | export * from './Menu'; 49 | export * from './MoveSheep'; 50 | export * from './Canvas'; 51 | -------------------------------------------------------------------------------- /src/hooks/useScreenSize.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | import { useEffect, useState } from 'react'; 3 | import { DomUtils } from '../utils'; 4 | 5 | function useScreenSize() { 6 | const [size, setSize] = useState({ width: document.body.clientWidth, height: document.body.clientHeight }); 7 | 8 | useEffect(() => { 9 | if (!DomUtils.usableWindow()) return; 10 | 11 | const resize = debounce(() => { 12 | setSize((pre) => ({ ...pre, width: document.body.clientWidth, height: document.body.clientHeight })); 13 | }, 400); 14 | 15 | window.addEventListener('resize', resize, false); 16 | resize(); 17 | return () => window.removeEventListener('resize', resize, false); 18 | }, []); 19 | 20 | return { stageWidth: size.width, stageHeight: size.height }; 21 | } 22 | 23 | export default useScreenSize; 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | outline: 0; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | body { 13 | width: 100%; 14 | height: 100%; 15 | overflow: hidden; 16 | margin: 0; 17 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 18 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 19 | sans-serif; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | 24 | code { 25 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 26 | monospace; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/stories/Accordion.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Accordion } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/Accordion', 7 | component: Accordion, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | justifyContent: 'center', 13 | alignItems: 'center', 14 | height: '100vh', 15 | boxSizing: 'border-box', 16 | } as CSSProperties; 17 | 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }, 24 | ], 25 | } as Meta; 26 | 27 | const Template: Story = (args) => ; 28 | 29 | export const BasicCss = Template.bind({}); 30 | -------------------------------------------------------------------------------- /src/stories/AnimatedEyesFollowMouseCursor.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { AnimatedEyesFollowMouseCursor } from '../components'; 4 | import { CSSProperties } from 'styled-components'; 5 | 6 | export default { 7 | title: 'jacob-css/AnimatedEyesFollowMouseCursor', 8 | component: AnimatedEyesFollowMouseCursor, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'auto', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | parameters: { 29 | docs: { 30 | description: { component: '마우스 포인터를 따라 눈동자가 움직입니다.' }, 31 | }, 32 | }, 33 | } as Meta; 34 | 35 | const Template: Story = (args) => ; 36 | 37 | export const BasicCss = Template.bind({}); 38 | BasicCss.args = {}; 39 | -------------------------------------------------------------------------------- /src/stories/Banners.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Banners, BannerTypes } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/Banners', 7 | component: Banners, 8 | parameters: { 9 | backgrounds: { 10 | default: 'white', 11 | }, 12 | docs: { 13 | description: { component: '코카콜라 느낌의 배너와 배경입니다.' }, 14 | }, 15 | }, 16 | decorators: [ 17 | (Story) => { 18 | const style = { 19 | display: 'flex', 20 | flexDirection: 'column', 21 | justifyContent: 'center', 22 | alignItems: 'center', 23 | height: '100vh', 24 | overflow: 'auto', 25 | boxSizing: 'border-box', 26 | } as CSSProperties; 27 | 28 | return ( 29 |
30 | 31 |
32 | ); 33 | }, 34 | ], 35 | } as Meta; 36 | 37 | const Template: Story = (args) => ; 38 | 39 | export const AnimatedBanner1 = Template.bind({}); 40 | AnimatedBanner1.args = { type: BannerTypes.Animated1 }; 41 | -------------------------------------------------------------------------------- /src/stories/ClimbEffects.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { ClimbEffects } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/ClimbEffects', 7 | component: ClimbEffects, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'auto', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { 30 | component: '보이지 않는 영역을 힘겹게 개척하는 네온사인 사각형입니다.', 31 | }, 32 | }, 33 | }, 34 | } as Meta; 35 | 36 | const Template: Story = (args) => ; 37 | 38 | export const BasicCss = Template.bind({}); 39 | BasicCss.args = {}; 40 | -------------------------------------------------------------------------------- /src/stories/ClipPathCircle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { ClipPathCircle } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/ClipPathCircle', 7 | component: ClipPathCircle, 8 | parameters: { 9 | docs: { 10 | description: { component: 'clip-path을 이용한 scroll text 애니메이션' }, 11 | }, 12 | }, 13 | } as Meta; 14 | 15 | const Template: Story = (args) => ; 16 | 17 | export const BasicCss = Template.bind({}); 18 | -------------------------------------------------------------------------------- /src/stories/CustomScrollbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { CustomScrollbar } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/CustomScrollbar', 7 | component: CustomScrollbar, 8 | parameters: { 9 | docs: { 10 | description: { component: '스크롤을 커스텀하였습니다.' }, 11 | }, 12 | }, 13 | } as Meta; 14 | 15 | const Template: Story = (args) => ; 16 | 17 | export const BasicCss = Template.bind({}); 18 | -------------------------------------------------------------------------------- /src/stories/DropDown.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Dropdown } from '../components'; 4 | import styled from 'styled-components'; 5 | 6 | export default { 7 | title: 'jacob-css/Dropdown', 8 | component: Dropdown, 9 | parameters: { 10 | docs: { 11 | description: { 12 | component: '오른쪽 위에 프로필 이미지를 누르면 드롭다운이 나옵니다.', 13 | }, 14 | }, 15 | }, 16 | } as Meta; 17 | 18 | const Template: Story = (args) => ( 19 | <> 20 | {`Click Here! -->`} 21 | 22 | 23 | ); 24 | 25 | const IndicateText = styled.h2` 26 | color: white; 27 | position: absolute; 28 | right: 120px; 29 | top: 12px; 30 | `; 31 | 32 | export const UserInfoDropdown = Template.bind({}); 33 | -------------------------------------------------------------------------------- /src/stories/FakeScrollSpy.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { FakeScrollSpy } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/FakeScrollSpy', 7 | component: FakeScrollSpy, 8 | parameters: { 9 | docs: { 10 | description: { component: '페이지 후버에 따라 네비게이션이 변합니다.' }, 11 | }, 12 | }, 13 | } as Meta; 14 | 15 | const Template: Story = (args) => ; 16 | 17 | export const BasicCss = Template.bind({}); 18 | -------------------------------------------------------------------------------- /src/stories/GhostText.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { GhostText } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/GhostText', 7 | component: GhostText, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: '고스트 효과' }, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | const Template: Story = (args) => ; 35 | 36 | export const BasicCss = Template.bind({}); 37 | -------------------------------------------------------------------------------- /src/stories/GlowingBackground.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { GlowingBackground } from '../components'; 4 | import styled from 'styled-components'; 5 | 6 | export default { 7 | title: 'jacob-css/GlowingBackground', 8 | component: GlowingBackground, 9 | parameters: { 10 | docs: { 11 | description: { component: '빛나는 포인터가 있는 배경입니다.' }, 12 | }, 13 | }, 14 | decorators: [ 15 | (Story) => { 16 | const style = { 17 | display: 'flex', 18 | flexDirection: 'column', 19 | justifyContent: 'center', 20 | alignItems: 'center', 21 | height: '100vh', 22 | overflow: 'hidden', 23 | boxSizing: 'border-box', 24 | } as CSSProperties; 25 | 26 | return ( 27 |
28 | 29 |
30 | ); 31 | }, 32 | ], 33 | } as Meta; 34 | 35 | const Template: Story = (args) => ( 36 | <> 37 | Jacob Css 38 | 39 | 40 | ); 41 | 42 | const Title = styled.h1` 43 | position: absolute; 44 | color: white; 45 | font-size: 8vw; 46 | z-index: 1; 47 | `; 48 | 49 | export const BasicCss = Template.bind({}); 50 | -------------------------------------------------------------------------------- /src/stories/GlowingCheckbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, useCallback, useState } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { GlowingCheckbox, GlowingRadio } from '../components'; 4 | import styled, { keyframes } from 'styled-components'; 5 | 6 | export default { 7 | title: 'jacob-css/GlowingCheckbox', 8 | component: GlowingCheckbox, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'auto', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | parameters: { 29 | docs: { 30 | description: { 31 | component: 32 | '(checkbox/radio)타입의 input과 스타일을 조작하는 기초를 제공합니다.', 33 | }, 34 | }, 35 | }, 36 | } as Meta; 37 | 38 | const options = [ 39 | { value: 'Jacob ...', name: 'test' }, 40 | { value: 'Jacob is good!', name: 'test' }, 41 | ]; 42 | 43 | const Template: Story = () => { 44 | const [radioValue, setRadioValue] = useState('Jacob ...'); 45 | 46 | const handleRadioChange = useCallback( 47 | (event: React.ChangeEvent) => { 48 | setRadioValue(event.target.value); 49 | }, 50 | [] 51 | ); 52 | 53 | return ( 54 | <> 55 | checkbox 56 | 57 | 58 | 63 | {radioValue} 64 | 65 | ); 66 | }; 67 | 68 | const Title = styled.h2` 69 | color: white; 70 | font-weight: 600; 71 | font-size: 24px; 72 | text-transform: uppercase; 73 | `; 74 | 75 | const Divider = styled.hr` 76 | margin: 28px 0 16px; 77 | width: 32%; 78 | background: white; 79 | `; 80 | 81 | const animation = keyframes` 82 | 0% { 83 | transform: scaleX(0); 84 | } 85 | 100% { 86 | transform: scaleX(1); 87 | } 88 | `; 89 | const RadioValue = styled.span` 90 | color: whitesmoke; 91 | position: relative; 92 | margin-top: 8px; 93 | 94 | &::before { 95 | content: ''; 96 | position: absolute; 97 | height: 2px; 98 | bottom: 0; 99 | left: 0; 100 | background: cyan; 101 | width: 100%; 102 | transform: scaleX(0); 103 | transform-origin: bottom left; 104 | animation: ${animation} 0.5s ease-out infinite; 105 | } 106 | `; 107 | 108 | export const BasicCss = Template.bind({}); 109 | -------------------------------------------------------------------------------- /src/stories/GlowingText.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { GlowingText, GlowingTextProps } from '../components'; 4 | import { text } from '@storybook/addon-knobs'; 5 | 6 | export default { 7 | title: 'jacob-css/GlowingText', 8 | component: GlowingText, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'auto', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | parameters: { 29 | docs: { 30 | description: { component: '반짝 반짝.' }, 31 | }, 32 | }, 33 | } as Meta; 34 | 35 | const Template: Story = () => { 36 | const label = text('label', 'jacob'); 37 | return ; 38 | }; 39 | 40 | export const BasicCss = Template.bind({}); 41 | -------------------------------------------------------------------------------- /src/stories/HalfCircularBackground.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HalfCircularBackground } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/HalfCircularBackground', 7 | component: HalfCircularBackground, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: '컨텐츠 아래쪽에 라운드된 배경을 제공합니다.' }, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | const Template: Story = (args) => ; 35 | 36 | export const BasicCss = Template.bind({}); 37 | -------------------------------------------------------------------------------- /src/stories/HeartAnimation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HeartAnimation } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/HeartAnimation', 7 | component: HeartAnimation, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: '하트 하트~' }, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | const Template: Story = (args) => ; 35 | 36 | export const BasicCss = Template.bind({}); 37 | -------------------------------------------------------------------------------- /src/stories/HideScrollOnScrollDown.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HideScrollOnScrollDown } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/HideScrollOnScrollDown', 7 | component: HideScrollOnScrollDown, 8 | parameters: { 9 | docs: { 10 | description: { 11 | component: '스크롤의 방향에 따라 헤더의 존재 유무가 결정됩니다.', 12 | }, 13 | }, 14 | }, 15 | } as Meta; 16 | 17 | const Template: Story = (args) => ; 18 | 19 | export const BasicCss = Template.bind({}); 20 | -------------------------------------------------------------------------------- /src/stories/HoverEffectCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HoverEffectCard } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/HoverEffectCard', 7 | component: HoverEffectCard, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: 'hover 효과가 들어간 카드입니다..' }, 30 | }, 31 | backgrounds: { 32 | default: 'whitesmoke', 33 | }, 34 | }, 35 | } as Meta; 36 | 37 | const Template: Story = (args) => ; 38 | 39 | export const BasicCss = Template.bind({}); 40 | -------------------------------------------------------------------------------- /src/stories/ListHoverEffects.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { ListHoverEffects } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/ListHoverEffects', 7 | component: ListHoverEffects, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'auto', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | } as Meta; 28 | 29 | const Template: Story = () => { 30 | return ; 31 | }; 32 | 33 | export const BasicCss = Template.bind({}); 34 | -------------------------------------------------------------------------------- /src/stories/Loader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { GradientLoader, Loader, TwoRingLoader } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/Loader', 7 | component: Loader, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'auto', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: '그라데이션 로더(스피너)입니다.' }, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | const Template: Story = () => { 35 | return ; 36 | }; 37 | 38 | const Template2: Story = () => { 39 | return ; 40 | }; 41 | 42 | const Template3: Story = () => { 43 | return ; 44 | }; 45 | 46 | export const BasicCss = Template.bind({}); 47 | 48 | export const TwoRing = Template2.bind({}); 49 | 50 | export const Gradient = Template3.bind({}); 51 | -------------------------------------------------------------------------------- /src/stories/LongBoxShadow.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { LongTextShadow, LongTextShadowProps } from '../components'; 4 | import { text } from '@storybook/addon-knobs'; 5 | 6 | export default { 7 | title: 'jacob-css/LongTextShadow', 8 | component: LongTextShadow, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'auto', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | } as Meta; 29 | 30 | const Template: Story = (args) => { 31 | const label = text('label', 'jacob'); 32 | return ; 33 | }; 34 | 35 | export const BasicCss = Template.bind({}); 36 | -------------------------------------------------------------------------------- /src/stories/Menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Menu } from '../components'; 4 | import dummyData from './dummy/dummyData'; 5 | 6 | export default { 7 | title: 'jacob-css/Menu', 8 | component: Menu, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | padding: '40px', 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | boxSizing: 'border-box', 17 | height: '100vh', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: '리스트를 클릭했을때 나오는 메뉴.' }, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | const Template: Story = () => { 35 | return ( 36 | <> 37 |

Context Menu

38 | 39 | 40 | ); 41 | }; 42 | 43 | export const ContextMenu = Template.bind({}); 44 | -------------------------------------------------------------------------------- /src/stories/MiniWavy.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { MiniWavy } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/MiniWavy', 7 | component: MiniWavy, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | } as Meta; 28 | 29 | const Template: Story = () => { 30 | return ; 31 | }; 32 | 33 | export const BasicCss = Template.bind({}); 34 | -------------------------------------------------------------------------------- /src/stories/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Modal } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/Modal', 7 | component: Modal, 8 | parameters: { 9 | backgrounds: { 10 | default: 'dark', 11 | }, 12 | }, 13 | decorators: [ 14 | (Story) => { 15 | const style = { 16 | boxSizing: 'border-box', 17 | } as CSSProperties; 18 | 19 | return ( 20 |
21 | 22 |
23 | ); 24 | }, 25 | ], 26 | } as Meta; 27 | 28 | const Template: Story = (args) => ; 29 | 30 | export const BasicCss = Template.bind({}); 31 | -------------------------------------------------------------------------------- /src/stories/MouseCursor.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { DotMouseCursor } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/DotMouseCursor', 7 | component: DotMouseCursor, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | cursor: 'none', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | } as Meta; 29 | 30 | const Template: Story = () => { 31 | return ; 32 | }; 33 | 34 | export const BasicCss = Template.bind({}); 35 | -------------------------------------------------------------------------------- /src/stories/MovingText.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { MovingText, MovingTextProps } from '../components'; 4 | import { text } from '@storybook/addon-knobs'; 5 | 6 | export default { 7 | title: 'jacob-css/MovingText', 8 | component: MovingText, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'auto', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | } as Meta; 29 | 30 | const Template: Story = () => { 31 | const label = text('label', 'jacob-css'); 32 | return ; 33 | }; 34 | 35 | export const BasicCss = Template.bind({}); 36 | -------------------------------------------------------------------------------- /src/stories/NeonButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { text } from '@storybook/addon-knobs'; 4 | import { NeonButton, NeonButtonProps } from '../components'; 5 | 6 | export default { 7 | title: 'jacob-css/NeonButton', 8 | component: NeonButton, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'auto', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | } as Meta; 29 | 30 | const Template: Story = () => { 31 | const label = text('label', 'jacob button'); 32 | return ; 33 | }; 34 | 35 | export const BasicCss = Template.bind({}); 36 | -------------------------------------------------------------------------------- /src/stories/NumberCounter.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { NumberCounter } from '../components'; 4 | import styled from 'styled-components'; 5 | 6 | export default { 7 | title: 'jacob-css/NumberCounter', 8 | component: NumberCounter, 9 | decorators: [ 10 | (Story) => { 11 | const style = { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | height: '100vh', 17 | overflow: 'hidden', 18 | boxSizing: 'border-box', 19 | } as CSSProperties; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }, 27 | ], 28 | } as Meta; 29 | 30 | const Template: Story = () => { 31 | return ( 32 | <> 33 |

Only css!

34 | 35 | 36 | ); 37 | }; 38 | 39 | const H2 = styled.h2` 40 | margin: 0; 41 | color: gray; 42 | margin: 16px 0; 43 | `; 44 | 45 | export const BasicCss = Template.bind({}); 46 | -------------------------------------------------------------------------------- /src/stories/PasswordInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { PasswordInput } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/PasswordInput', 7 | component: PasswordInput, 8 | parameters: { 9 | backgrounds: { 10 | default: 'whitesmoke', 11 | }, 12 | docs: { 13 | description: { 14 | component: '암호를 입력하는 Input 컴포넌트 입니다. ', 15 | }, 16 | }, 17 | }, 18 | decorators: [ 19 | (Story) => { 20 | const style = { 21 | display: 'flex', 22 | flexDirection: 'column', 23 | justifyContent: 'center', 24 | alignItems: 'center', 25 | height: '100vh', 26 | overflow: 'hidden', 27 | boxSizing: 'border-box', 28 | } as CSSProperties; 29 | 30 | return ( 31 |
32 | 33 |
34 | ); 35 | }, 36 | ], 37 | } as Meta; 38 | 39 | const Template: Story = () => { 40 | return ; 41 | }; 42 | 43 | export const BasicCss = Template.bind({}); 44 | -------------------------------------------------------------------------------- /src/stories/RangeInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { RangeInput } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/RangeInput', 7 | component: RangeInput, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'auto', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { 30 | component: 'range타입 input의 스타일을 조작하는 기초를 제공합니다. ', 31 | }, 32 | }, 33 | }, 34 | } as Meta; 35 | 36 | const Template: Story = () => { 37 | return ; 38 | }; 39 | 40 | export const BasicCss = Template.bind({}); 41 | -------------------------------------------------------------------------------- /src/stories/RippleEffectsButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { RippleEffectsButton } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/RippleEffectsButton', 7 | component: RippleEffectsButton, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { component: '버튼의 hover에 ripple효과를 줍니다.' }, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | const Template: Story = (args) => ; 35 | 36 | export const BasicCss = Template.bind({}); 37 | -------------------------------------------------------------------------------- /src/stories/ScrollDownIndicator.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { ScrollDownIndicator } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/ScrollDownIndicator', 7 | component: ScrollDownIndicator, 8 | parameters: { 9 | docs: { 10 | description: { 11 | component: '화면 아래로 스크롤을 유도하는 표시자입니다.', 12 | }, 13 | }, 14 | }, 15 | } as Meta; 16 | 17 | const Template: Story = (args) => ; 18 | 19 | export const BasicCss = Template.bind({}); 20 | -------------------------------------------------------------------------------- /src/stories/ScrollProgressBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { ScrollProgressBar } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/ScrollProgressBar', 7 | component: ScrollProgressBar, 8 | parameters: { 9 | docs: { 10 | description: { 11 | component: 12 | '스크롤의 높이에 따라 커스텀된 진행바를 스크롤바 형식으로 제공합니다.', 13 | }, 14 | }, 15 | }, 16 | } as Meta; 17 | 18 | const Template: Story = (args) => ; 19 | 20 | export const BasicCss = Template.bind({}); 21 | -------------------------------------------------------------------------------- /src/stories/ShadowEffects.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { ShadowEffects } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/ShadowEffects', 7 | component: ShadowEffects, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'auto', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { 30 | component: '그림자 효과를 확인하기 위한 단순 도넛입니다.', 31 | }, 32 | }, 33 | }, 34 | } as Meta; 35 | 36 | const Template: Story = () => { 37 | return ; 38 | }; 39 | 40 | export const BasicCss = Template.bind({}); 41 | -------------------------------------------------------------------------------- /src/stories/SidebarMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { SidebarMenu } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/SidebarMenu', 7 | component: SidebarMenu, 8 | parameters: { 9 | docs: { 10 | description: { component: '좌측 사이드바 컴포넌트입니다.' }, 11 | }, 12 | }, 13 | } as Meta; 14 | 15 | const Template: Story = (args) => ; 16 | 17 | export const BasicCss = Template.bind({}); 18 | -------------------------------------------------------------------------------- /src/stories/Skeleton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Skeleton, Skeleton2 } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/Skeleton', 7 | component: Skeleton, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'hidden', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | parameters: { 28 | docs: { 29 | description: { 30 | component: '데이터가 로드되기 전에 미리 보기를 표시하여 로드시간에 대한 불만을 줄입니다.', 31 | }, 32 | }, 33 | }, 34 | } as Meta; 35 | 36 | const Template: Story = (args) => ; 37 | 38 | const Template2: Story = (args) => ; 39 | 40 | export const style1 = Template.bind({}); 41 | 42 | export const style2 = Template2.bind({}); 43 | -------------------------------------------------------------------------------- /src/stories/SlidingMenuIndicator.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { SlidingMenuIndicator } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/SlidingMenuIndicator', 7 | component: SlidingMenuIndicator, 8 | decorators: [ 9 | (Story) => { 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | height: '100vh', 16 | overflow: 'auto', 17 | boxSizing: 'border-box', 18 | } as CSSProperties; 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }, 26 | ], 27 | } as Meta; 28 | 29 | const Template: Story = (args) => ; 30 | 31 | export const BasicCss = Template.bind({}); 32 | -------------------------------------------------------------------------------- /src/stories/StickyElementsScrollingEffects.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { StickyElementsScrollingEffects } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/StickyElementsScrollingEffects', 7 | component: StickyElementsScrollingEffects, 8 | parameters: { 9 | docs: { 10 | description: { 11 | component: '현재 페이지에 따라 sticky되는 헤더가 달라집니다.', 12 | }, 13 | }, 14 | }, 15 | } as Meta; 16 | 17 | const Template: Story = (args) => ; 18 | 19 | export const BasicCss = Template.bind({}); 20 | -------------------------------------------------------------------------------- /src/stories/TextArea.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { TextArea } from '../components'; 4 | 5 | export default { 6 | title: 'jacob-css/TextArea', 7 | component: TextArea, 8 | parameters: { 9 | docs: { 10 | description: { component: '애니메이션이 살짝 추가된 Textarea' }, 11 | }, 12 | }, 13 | decorators: [ 14 | (Story) => { 15 | const style = { 16 | display: 'flex', 17 | flexDirection: 'column', 18 | top: '180px', 19 | position: 'absolute', 20 | left: 0, 21 | right: 0, 22 | alignItems: 'center', 23 | height: '100vh', 24 | overflow: 'auto', 25 | boxSizing: 'border-box', 26 | } as CSSProperties; 27 | 28 | return ( 29 |
30 | 31 |
32 | ); 33 | }, 34 | ], 35 | } as Meta; 36 | 37 | const Template: Story = (args) =>