├── ss.jpeg ├── header.png ├── src ├── logo.png ├── favicon.ico ├── assets │ ├── ss.jpeg │ ├── default.webp │ ├── mountain.webp │ ├── box.svg │ ├── github.svg │ ├── fence.svg │ ├── download.svg │ ├── edit.svg │ ├── upload.svg │ ├── reset.svg │ ├── random.svg │ ├── top.svg │ └── wand.svg ├── styles │ ├── tailwind.css │ ├── slider.css │ ├── scroll.css │ └── main.css ├── utils │ ├── getFileType.js │ ├── loadElement.js │ ├── dynamicCanvasResize.js │ ├── generateColors.js │ ├── generateRandomURL.js │ ├── hslToRgb.js │ ├── downscaleCanvasImage.js │ └── downloadImage.js ├── index.jsx ├── App.jsx ├── Components │ ├── UI │ │ ├── Slider.jsx │ │ ├── Checkbox.jsx │ │ ├── AnimatedText.jsx │ │ ├── Spinner.jsx │ │ ├── Button.jsx │ │ ├── Navbar.jsx │ │ ├── Toast.jsx │ │ └── Modal.jsx │ ├── OutputSection │ │ ├── OutputGrid.jsx │ │ └── AdjustedOutput.jsx │ ├── Main │ │ └── ImageSection.jsx │ ├── AbstractArt │ │ ├── GlowEffect.jsx │ │ └── AbstractArt.jsx │ ├── Home.jsx │ ├── ImageButtons │ │ └── ImageButtons.jsx │ ├── CanvasComponents │ │ ├── MainCanvas.jsx │ │ └── OuputCanvas.jsx │ └── MainConfig │ │ └── ConfigBar.jsx ├── constants.js ├── hooks │ ├── useCanvas.jsx │ ├── useWorker.jsx │ ├── useWindowSize.jsx │ └── useStore.jsx ├── worker │ ├── runWorker.js │ └── tintWorker.js ├── loader.svg └── index.html ├── postcss.config.js ├── .prettierrc.json ├── .babelrc ├── webpack.dev.js ├── .gitignore ├── .eslintrc.js ├── webpack.prod.js ├── LICENSE ├── README.md ├── webpack.common.js ├── package.json ├── tailwind.config.js └── lib └── webcomponent ├── banner.js └── float-menu.js /ss.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/ss.jpeg -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/header.png -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/src/logo.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/assets/ss.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/src/assets/ss.jpeg -------------------------------------------------------------------------------- /src/assets/default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/src/assets/default.webp -------------------------------------------------------------------------------- /src/assets/mountain.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/HEAD/src/assets/mountain.webp -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getFileType.js: -------------------------------------------------------------------------------- 1 | export const getFileType = (fname) => 2 | fname.slice(((fname.lastIndexOf('.') - 1) >>> 0) + 2) 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["@babel/plugin-transform-react-jsx", { "pragma": "h" }] 5 | ] 6 | 7 | } 8 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | }) 7 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact' 2 | import App from './App.jsx' 3 | import './styles/main.css' 4 | import './styles/slider.css' 5 | import './styles/scroll.css' 6 | 7 | render(, document.getElementById('app')) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | */package-lock.json 6 | .vscode 7 | .idea 8 | test/ts/**/*.js 9 | coverage 10 | *.sw[op] 11 | *.log 12 | package/ 13 | preact-*.tgz 14 | preact.tgz 15 | jsx-csstype.d.ts 16 | 17 | .vercel 18 | -------------------------------------------------------------------------------- /src/utils/loadElement.js: -------------------------------------------------------------------------------- 1 | // Returns a promise - use to load image 2 | export function loadElement(src) { 3 | return new Promise((resolve) => { 4 | const e = new Image() 5 | e.addEventListener('load', () => { 6 | resolve(e) 7 | }) 8 | e.src = src 9 | e.crossOrigin = 'Anonymous' 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/slider.css: -------------------------------------------------------------------------------- 1 | @media screen and (-webkit-min-device-pixel-ratio: 0) { 2 | 3 | input[type="range"]::-webkit-slider-thumb { 4 | width: 15px; 5 | -webkit-appearance: none; 6 | appearance: none; 7 | height: 15px; 8 | cursor: ew-resize; 9 | background: #FFF; 10 | /* box-shadow: -405px 0 0 400px #605E5C; */ 11 | border-radius: 50%; 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/scroll.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body ::-webkit-scrollbar { 6 | width: 0.4em; 7 | } 8 | 9 | body ::-webkit-scrollbar-track { 10 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 11 | } 12 | 13 | body ::-webkit-scrollbar-thumb { 14 | background-color: rgb(73, 73, 73); 15 | border-radius: 8px; 16 | } 17 | 18 | #out-grid-section:hover { 19 | overflow: auto; 20 | transition: all 1s ease; 21 | } -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import Home from './Components/Home' 3 | import './../lib/webcomponent/float-menu' 4 | import './../lib/webcomponent/banner' 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ) 14 | } 15 | 16 | export default App 17 | -------------------------------------------------------------------------------- /src/assets/box.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/dynamicCanvasResize.js: -------------------------------------------------------------------------------- 1 | export const dynamicCanvasResize = (cv, img, scale) => { 2 | cv.width = cv.offsetWidth * scale 3 | cv.height = cv.offsetHeight * scale 4 | 5 | const cvProportion = (cv.height * 1.0) / cv.width 6 | const res = (img.height * 1.0) / img.width 7 | 8 | if (cvProportion > res) { 9 | cv.height = res * cv.width 10 | } else { 11 | cv.width = (cv.height * 1.0) / res 12 | } 13 | cv.style.width = 'auto' 14 | cv.style.height = 'auto' 15 | return cv 16 | } 17 | -------------------------------------------------------------------------------- /src/Components/UI/Slider.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | const Slider = ({ adjustedHue, onCustomHueAdjust }) => { 4 | return ( 5 | 18 | ) 19 | } 20 | 21 | export default Slider 22 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const FILTERS = [ 2 | 'DELPHOX', 3 | 'BAYLEEF', 4 | 'AZELF', 5 | 'Lunala', 6 | 'Archeops', 7 | 'Virizion', 8 | 'Deino', 9 | 'Corsola', 10 | ] 11 | export const DEFAULT_HUE = 15 12 | export const OUTPUT_IMAGES = 8 13 | 14 | export const getParams = (canvas) => ({ 15 | x: 0, 16 | y: 0, 17 | width: canvas.width * 2, 18 | height: canvas.height * 2, 19 | }) 20 | 21 | export const UNSPLASH_URI = 22 | 'https://source.unsplash.com/random/600x350/?abstract,nature' 23 | -------------------------------------------------------------------------------- /src/Components/UI/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | const Checkbox = ({ text, checked, onChange }) => { 4 | return ( 5 | 14 | ) 15 | } 16 | 17 | export default Checkbox 18 | -------------------------------------------------------------------------------- /src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /src/utils/generateColors.js: -------------------------------------------------------------------------------- 1 | import { hslToRgb } from './hslToRgb' 2 | 3 | export const generateColors = (n) => { 4 | const colors = [] 5 | let abs = Math.floor(Math.abs(n)) 6 | let part = 1 / abs 7 | 8 | for (let i = 0; i < n; i++) { 9 | let color = { h: i * part, s: 0.9, l: 0.6 } 10 | let hsl = color 11 | let rgb = hslToRgb(color.h, color.s, color.l) 12 | let rgbString = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` 13 | colors.push({ 14 | hsl, 15 | rgb, 16 | rgbString, 17 | }) 18 | } 19 | 20 | return colors 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['plugin:import/recommended', 'preact'], 7 | parserOptions: { 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | ecmaVersion: 12, 12 | sourceType: 'module', 13 | }, 14 | plugins: [], 15 | settings: { 16 | 'import/resolver': { 17 | node: { 18 | extensions: ['.js', '.jsx'], 19 | }, 20 | }, 21 | }, 22 | rules: { 23 | semi: 'off', 24 | 'linebreak-style': 'off', 25 | 'import/prefer-default-export': 'off', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/fence.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Components/UI/AnimatedText.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { useEffect, useState } from 'preact/hooks' 3 | 4 | const AnimatedText = ({ textList }) => { 5 | const [textIdx, setTextIdx] = useState(0) 6 | 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | if (textIdx < 2) { 10 | setTextIdx((i) => i + 1) 11 | } else { 12 | clearInterval(interval) 13 | } 14 | }, 2000) 15 | return () => clearInterval(interval) 16 | }, [textIdx]) 17 | return

{textList[textIdx]}

18 | } 19 | 20 | export default AnimatedText 21 | -------------------------------------------------------------------------------- /src/utils/generateRandomURL.js: -------------------------------------------------------------------------------- 1 | import { UNSPLASH_URI } from '../constants' 2 | 3 | export const generateRandomURL = () => 4 | new Promise((resolve) => { 5 | try { 6 | fetch(UNSPLASH_URI).then((response) => { 7 | if (response.ok) { 8 | resolve({ 9 | ok: true, 10 | url: response.url, 11 | }) 12 | } else { 13 | resolve({ 14 | ok: false, 15 | err: 'Error Fetching', 16 | }) 17 | } 18 | }) 19 | } catch (e) { 20 | resolve({ 21 | ok: false, 22 | err: e.message, 23 | }) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/assets/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | const CompressionPlugin = require('compression-webpack-plugin') 4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 5 | const webpack = require('webpack') 6 | 7 | module.exports = merge(common, { 8 | mode: 'production', 9 | plugins: [ 10 | new webpack.DefinePlugin({ 11 | // <-- key to reducing React's size 12 | 'process.env': { 13 | NODE_ENV: JSON.stringify('production'), 14 | }, 15 | }), 16 | new CompressionPlugin(), 17 | new OptimizeCSSAssetsPlugin({}), 18 | ], 19 | }) 20 | -------------------------------------------------------------------------------- /src/Components/OutputSection/OutputGrid.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { useState } from 'preact/hooks' 3 | import { generateColors } from '../../utils/generateColors' 4 | import { AdjustedOutput } from './AdjustedOutput' 5 | import { OUTPUT_IMAGES } from '../../constants' 6 | 7 | const OutputGrid = () => { 8 | const [colors] = useState(() => generateColors(OUTPUT_IMAGES).reverse()) 9 | 10 | return ( 11 |
12 | {colors.map((c, i) => ( 13 | 14 | ))} 15 |
16 | ) 17 | } 18 | 19 | export default OutputGrid 20 | -------------------------------------------------------------------------------- /src/assets/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/hslToRgb.js: -------------------------------------------------------------------------------- 1 | export function hslToRgb(h, s, l) { 2 | let r, g, b 3 | 4 | function hue2rgb(p, q, t) { 5 | if (t < 0) t += 1 6 | if (t > 1) t -= 1 7 | if (t < 1 / 6) return p + (q - p) * 6 * t 8 | if (t < 1 / 2) return q 9 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 10 | return p 11 | } 12 | 13 | if (s == 0) { 14 | r = g = b = l // achromatic 15 | } else { 16 | let q = l < 0.5 ? l * (1 + s) : l + s - l * s 17 | let p = 2 * l - q 18 | r = hue2rgb(p, q, h + 1 / 3) 19 | g = hue2rgb(p, q, h) 20 | b = hue2rgb(p, q, h - 1 / 3) 21 | } 22 | 23 | return { 24 | r: Math.floor(r * 255), 25 | g: Math.floor(g * 255), 26 | b: Math.floor(b * 255), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useCanvas.jsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'preact/hooks' 2 | 3 | const useCanvas = (draw, options = {}) => { 4 | const canvasRef = useRef(null) 5 | 6 | useEffect(() => { 7 | const canvas = canvasRef.current 8 | const context = canvas.getContext(options.context || '2d') 9 | let frameCount = 0 10 | let animationFrameId 11 | const render = () => { 12 | frameCount++ 13 | if (draw) { 14 | draw(context, frameCount) 15 | } 16 | animationFrameId = window.requestAnimationFrame(render) 17 | } 18 | render() 19 | return () => { 20 | window.cancelAnimationFrame(animationFrameId) 21 | } 22 | }, [draw, options.context]) 23 | return canvasRef 24 | } 25 | export default useCanvas 26 | -------------------------------------------------------------------------------- /src/Components/UI/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | const Spinner = () => { 4 | return ( 5 | 11 | 22 | 27 | 28 | ) 29 | } 30 | 31 | export default Spinner 32 | -------------------------------------------------------------------------------- /src/worker/runWorker.js: -------------------------------------------------------------------------------- 1 | export const runWorker = ( 2 | ctx, 3 | imgData, 4 | params, 5 | color, 6 | monoTone, 7 | custom, 8 | adjustedHue, 9 | ) => { 10 | return new Promise((resolve) => { 11 | if (window.Worker) { 12 | const worker = new Worker(new URL('./tintWorker.js', import.meta.url)) 13 | worker.postMessage({ imgData, color, monoTone, custom, adjustedHue }, [ 14 | imgData.data.buffer, 15 | ]) 16 | worker.onerror = (err) => resolve({ ok: false, err }) 17 | worker.onmessage = (e) => { 18 | const imgData = e.data 19 | ctx.putImageData( 20 | imgData, 21 | (params.x - params.width / 2) * 1, 22 | (params.y - params.height / 2) * 1, 23 | ) 24 | worker.terminate() 25 | resolve({ ok: true }) 26 | } 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/useWorker.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'preact/hooks' 2 | 3 | export const useWorker = (file, onMessageCallback, postMessage, buffer) => { 4 | const [status, setStatus] = useState('loading') 5 | 6 | useEffect(() => { 7 | if (!postMessage || !postMessage.imgData || !buffer) { 8 | return 9 | } 10 | 11 | const runWorker = () => { 12 | const worker = new Worker(new URL(file, import.meta.url)) 13 | worker.postMessage(postMessage, buffer) 14 | worker.onerror = (err) => { 15 | setStatus('error') 16 | throw err 17 | } 18 | worker.onmessage = (e) => { 19 | onMessageCallback(e) 20 | setStatus('success') 21 | worker.terminate() 22 | } 23 | } 24 | if (window.Worker) { 25 | runWorker() 26 | } 27 | }, [buffer, file, onMessageCallback, postMessage]) 28 | 29 | return status 30 | } 31 | -------------------------------------------------------------------------------- /src/Components/Main/ImageSection.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import ConfigBar from '../MainConfig/ConfigBar' 3 | import MainCanvas from '../CanvasComponents/MainCanvas' 4 | import { useStore } from '../../hooks/useStore' 5 | import FenceImg from '../../assets/fence.svg' 6 | 7 | const ImageSection = () => { 8 | const selectedImage = useStore((state) => state.selectedImage) 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | ) 28 | } 29 | 30 | export default ImageSection 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Uxie.io 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 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'preact/hooks' 2 | 3 | // Hook usehooks.com 4 | export const useWindowSize = () => { 5 | // Initialize state with undefined width/height so server and client renders match 6 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 7 | const [windowSize, setWindowSize] = useState({ 8 | width: undefined, 9 | height: undefined, 10 | }) 11 | useEffect(() => { 12 | // Handler to call on window resize 13 | function handleResize() { 14 | // Set window width/height to state 15 | setWindowSize({ 16 | width: window.innerWidth, 17 | height: window.innerHeight, 18 | }) 19 | } 20 | // Add event listener 21 | window.addEventListener('resize', handleResize) 22 | // Call handler right away so state gets updated with initial window size 23 | handleResize() 24 | // Remove event listener on cleanup 25 | return () => window.removeEventListener('resize', handleResize) 26 | }, []) // Empty array ensures that effect is only run on mount 27 | return windowSize 28 | } 29 | -------------------------------------------------------------------------------- /src/Components/UI/Button.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | const Button = ({ 4 | variant, 5 | href, 6 | children, 7 | fontWeight, 8 | colorRGB = '#ffffff', 9 | textSize = 'sm', 10 | onClick, 11 | }) => { 12 | const color = 13 | variant === 'white' 14 | ? 'text-black bg-white' 15 | : variant === 'glass' 16 | ? 'text-white bg-grey-light' 17 | : 'text-white bg-darkish-black' 18 | 19 | const text = textSize === 'lg' ? 'text-md lg:text-md' : 'text-sm lg:text-md' 20 | const map = { glass: 'rgba(245,245,245,.1)', solid: `${colorRGB}b2` } 21 | const customColor = map[variant] 22 | 23 | return ( 24 | 35 | {children} 36 | 37 | ) 38 | } 39 | 40 | export default Button 41 | -------------------------------------------------------------------------------- /src/hooks/useStore.jsx: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { DEFAULT_HUE } from '../constants' 3 | import img from './../assets/default.webp' 4 | 5 | export const useStore = create((set) => ({ 6 | showTints: true, 7 | toggleTints: () => set((state) => ({ showTints: !state.showTints })), 8 | monoTone: false, 9 | toggleTone: () => set((state) => ({ monoTone: !state.monoTone })), 10 | customize: false, 11 | toggleCustomize: () => set((state) => ({ customize: !state.customize })), 12 | selectedImage: img, 13 | selectImage: (src) => set(() => ({ selectedImage: src })), 14 | adjustedHue: DEFAULT_HUE, 15 | adjustHue: (num) => set(() => ({ adjustedHue: num })), 16 | toast: false, 17 | toastContent: { 18 | spinner: true, 19 | animated: true, 20 | texts: ['Processing', 'Generating', 'Downloading..'], 21 | }, 22 | toggleToast: (content) => 23 | set((state) => { 24 | if (content) { 25 | return { 26 | toast: !state.toast, 27 | toastContent: content, 28 | } 29 | } 30 | return { 31 | toast: !state.toast, 32 | } 33 | }), 34 | 35 | selectedFileExt: 'png', 36 | setSelectedFileExt: (ext) => set(() => ({ selectedFileExt: ext })), 37 | })) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinter 2 | 3 | ![header](./header.png) 4 | 5 | 6 | Buy Me A Coffee
7 | 8 | Tinter is tiny web tool to generate color variation of images. We often use photoshop, just to render multiple hue variants of image and fine grain which works for us. This tool also generate monochrome colors of images with multiple variants, without hampering the quality of image. 9 | 10 | # 11 | 12 | ![image](./ss.jpeg) 13 | --- 14 | ## Features 15 | 16 | - Generate color variations of images. 17 | - Generate monoTone hues of images 18 | - Supports png, jpeg, webp. 19 | - Privacy focused. No image uploading to servers. 20 | - Customize hue to control and fine tune your images. 21 | - No Loss in Quality. 22 | 23 | --- 24 | ## Contributions 25 | 26 | We whole heartedly welcome new contributions either fixing a issue, adding a new customization or simply improving the stylings. 27 | 28 | We truly ❤️ pull requests! If you wish to help, you can learn more about how you can contribute to this project in the contribution guide. 29 | 30 | Give a star if you like It.👍 31 | 32 | --- 33 | 34 | ## Credits 35 | 36 | [Anup A.](https://github.com/anup-a) 37 | -------------------------------------------------------------------------------- /src/Components/OutputSection/AdjustedOutput.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { downloadImage } from '../../utils/downloadImage' 3 | import { useStore } from '../../hooks/useStore' 4 | import OutputCanvas from '../CanvasComponents/OuputCanvas' 5 | import DownloadIcon from '../../assets/download.svg' 6 | import { DEFAULT_HUE } from '../../constants' 7 | 8 | export const AdjustedOutput = ({ color }) => { 9 | const selectedImage = useStore((state) => state.selectedImage) 10 | const monoTone = useStore((state) => state.monoTone) 11 | const toggleToast = useStore((state) => state.toggleToast) 12 | const selectedFileExt = useStore((state) => state.selectedFileExt) 13 | 14 | const download = () => { 15 | toggleToast() 16 | downloadImage( 17 | selectedImage, 18 | color, 19 | monoTone, 20 | false, 21 | DEFAULT_HUE, 22 | selectedFileExt, 23 | ).then(() => { 24 | toggleToast() 25 | }) 26 | } 27 | 28 | return ( 29 |
35 | 36 | Download icon 37 | 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/Components/AbstractArt/GlowEffect.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | const GlowEffect = () => { 4 | return ( 5 |
19 |
33 | 34 |
48 |
49 | ) 50 | } 51 | 52 | export default GlowEffect 53 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HTMLWebpackPlugin = require('html-webpack-plugin') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | 5 | module.exports = { 6 | entry: './src/index.jsx', 7 | output: { 8 | path: path.join(__dirname, '/dist'), 9 | filename: 'index_bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(js|jsx)$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: 'babel-loader', 18 | }, 19 | }, 20 | { 21 | test: /\.css$/, 22 | use: [ 23 | 'style-loader', 24 | { 25 | loader: MiniCssExtractPlugin.loader, 26 | }, 27 | { 28 | loader: 'css-loader', 29 | options: { 30 | importLoaders: 1, 31 | }, 32 | }, 33 | 'postcss-loader', 34 | ], 35 | }, 36 | { 37 | test: /\.(png|jp(e*)g|svg|gif|webp)$/, 38 | use: [ 39 | { 40 | loader: 'file-loader', 41 | options: { 42 | name: 'images/[hash]-[name].[ext]', 43 | }, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | resolve: { 50 | extensions: ['.js', '.jsx'], 51 | alias: { 52 | react: 'preact/compat', 53 | 'react-dom': 'preact/compat', 54 | }, 55 | }, 56 | plugins: [ 57 | new MiniCssExtractPlugin({ 58 | filename: '[name].bundle.css', 59 | chunkFilename: '[id].css', 60 | }), 61 | new HTMLWebpackPlugin({ 62 | template: './src/index.html', 63 | favicon: "./src/favicon.ico", 64 | }), 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /src/Components/Home.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment, h } from 'preact' 2 | import Navbar from './UI/Navbar' 3 | import OutputGrid from './OutputSection/OutputGrid' 4 | import { useStore } from '../hooks/useStore' 5 | import { Modal } from './UI/Modal' 6 | import { useState } from 'preact/hooks' 7 | import ImageSection from './Main/ImageSection' 8 | import ImageButtons from './ImageButtons/ImageButtons' 9 | import AbstractArt from './AbstractArt/AbstractArt' 10 | import Toast from './UI/Toast' 11 | import GlowEffect from './AbstractArt/GlowEffect' 12 | 13 | function Home() { 14 | const showTints = useStore((state) => state.showTints) 15 | const toast = useStore((state) => state.toast) 16 | let [isOpen, setIsOpen] = useState(false) 17 | 18 | function closeModal() { 19 | setIsOpen(false) 20 | } 21 | 22 | function openModal() { 23 | setIsOpen(true) 24 | } 25 | 26 | return ( 27 | 28 |
33 | 34 |
35 | 36 |
40 | 41 | 42 | {showTints && } 43 |
44 |
45 |
46 | 47 | {toast && } 48 | 49 |
50 | ) 51 | } 52 | 53 | export default Home 54 | -------------------------------------------------------------------------------- /src/utils/downscaleCanvasImage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://stackoverflow.com/questions/21961839/simulation-background-size-cover-in-canvas/21961894#21961894 3 | * 4 | * Credits - Ken Fyrstenberg Nilsen 5 | * 6 | * drawImageProp(context, image [, x, y, width, height [,offsetX, offsetY]]) 7 | * 8 | * If image and context are only arguments rectangle will equal canvas 9 | */ 10 | 11 | export function downscaleDrawCanvasImage( 12 | ctx, 13 | img, 14 | x, 15 | y, 16 | w, 17 | h, 18 | offsetX, 19 | offsetY, 20 | ) { 21 | if (arguments.length === 2) { 22 | x = y = 0 23 | w = ctx.canvas.width 24 | h = ctx.canvas.height 25 | } 26 | 27 | /// default offset is center 28 | offsetX = typeof offsetX === 'number' ? offsetX : 0.5 29 | offsetY = typeof offsetY === 'number' ? offsetY : 0.5 30 | 31 | /// keep bounds [0.0, 1.0] 32 | if (offsetX < 0) offsetX = 0 33 | if (offsetY < 0) offsetY = 0 34 | if (offsetX > 1) offsetX = 1 35 | if (offsetY > 1) offsetY = 1 36 | 37 | let iw = img.width, 38 | ih = img.height, 39 | r = Math.min(w / iw, h / ih), 40 | nw = iw * r, /// new prop. width 41 | nh = ih * r, /// new prop. height 42 | cx, 43 | cy, 44 | cw, 45 | ch, 46 | ar = 1 47 | 48 | /// decide which gap to fill 49 | if (nw < w) ar = w / nw 50 | if (nh < h) ar = h / nh 51 | nw *= ar 52 | nh *= ar 53 | 54 | /// calc source rectangle 55 | cw = iw / (nw / w) 56 | ch = ih / (nh / h) 57 | 58 | cx = (iw - cw) * offsetX 59 | cy = (ih - ch) * offsetY 60 | 61 | /// make sure source rectangle is valid 62 | if (cx < 0) cx = 0 63 | if (cy < 0) cy = 0 64 | if (cw > iw) cw = iw 65 | if (ch > ih) ch = ih 66 | 67 | /// fill image in dest. rectangle 68 | ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h) 69 | } 70 | -------------------------------------------------------------------------------- /src/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 17 | 18 | 19 | 24 | 28 | 32 | 33 | 34 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Components/ImageButtons/ImageButtons.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import Button from '../UI/Button' 3 | import UploadIcon from '../../assets/upload.svg' 4 | import RandomIcon from '../../assets/random.svg' 5 | import { generateRandomURL } from '../../utils/generateRandomURL' 6 | import { useStore } from '../../hooks/useStore' 7 | 8 | const ImageButtons = ({ openModal }) => { 9 | const { 10 | showTints, 11 | toggleTints, 12 | toggleCustomize, 13 | toggleToast, 14 | selectImage, 15 | customize, 16 | toggleTone, 17 | monoTone, 18 | } = useStore() 19 | 20 | const handleLoadRandomImage = () => { 21 | toggleToast({ 22 | spinner: true, 23 | animated: false, 24 | texts: ['Loading ...'], 25 | }) 26 | generateRandomURL().then((res) => { 27 | if (res.ok) { 28 | selectImage(res.url) 29 | toggleToast() 30 | } else { 31 | toggleToast() 32 | toggleToast({ 33 | spinner: false, 34 | animated: false, 35 | texts: ['Error loading image.'], 36 | }) 37 | } 38 | 39 | if (!monoTone) { 40 | toggleTone() 41 | } 42 | 43 | if (showTints) { 44 | toggleTints() 45 | 46 | if (customize) { 47 | toggleCustomize() 48 | } 49 | } 50 | }) 51 | } 52 | 53 | return ( 54 |
55 | 59 | 63 |
64 | ) 65 | } 66 | 67 | export default ImageButtons 68 | -------------------------------------------------------------------------------- /src/assets/random.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinter", 3 | "version": "1.0.0", 4 | "description": "Generate tints of images", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "NODE_OPTIONS=--openssl-legacy-provider && npm run watch:css && webpack serve --config webpack.dev.js --open --hot ", 9 | "build": "set NODE_ENV=production&& npm run build:css && webpack --mode=production --devtool source-map --config webpack.prod.js", 10 | "build:css": "postcss src/styles/tailwind.css -o src/styles/main.css", 11 | "watch:css": "postcss src/styles/tailwind.css -o src/styles/main.css" 12 | }, 13 | "keywords": [ 14 | "tinter", 15 | "image", 16 | "hue", 17 | "color" 18 | ], 19 | "author": "Anup Aglawe", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@headlessui/react": "^1.2.0", 23 | "@tailwindcss/forms": "^0.3.2", 24 | "file-saver": "^2.0.5", 25 | "preact": "^10.5.4", 26 | "zustand": "^3.5.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.11.6", 30 | "@babel/plugin-transform-react-jsx": "^7.10.4", 31 | "@babel/preset-env": "^7.11.5", 32 | "@babel/preset-react": "^7.10.4", 33 | "autoprefixer": "^9.8.6", 34 | "babel-loader": "^8.1.0", 35 | "compression-webpack-plugin": "^6.0.3", 36 | "css-loader": "^4.3.0", 37 | "eslint": "^7.29.0", 38 | "eslint-config-airbnb": "^18.2.1", 39 | "eslint-config-airbnb-base": "^14.2.1", 40 | "eslint-config-preact": "^1.1.4", 41 | "eslint-plugin-import": "^2.23.4", 42 | "file-loader": "^6.1.0", 43 | "html-webpack-plugin": "^5.3.1", 44 | "mini-css-extract-plugin": "^0.11.3", 45 | "optimize-css-assets-webpack-plugin": "^5.0.4", 46 | "postcss": "^8.2.15", 47 | "postcss-cli": "^8.0.0", 48 | "postcss-loader": "^4.0.3", 49 | "style-loader": "^1.3.0", 50 | "tailwindcss": "^2.1.2", 51 | "webpack": "^5.89.0", 52 | "webpack-cli": "^4.7.0", 53 | "webpack-dev-server": "^3.11.2", 54 | "webpack-merge": "^5.7.3" 55 | }, 56 | "eslintConfig": { 57 | "extends": "preact" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | enabled: true, 4 | content: ['./src/**/*.html', './src/**/*.jsx'], 5 | }, 6 | darkMode: 'class', 7 | variants: { 8 | extend: { 9 | backgroundColor: ['checked'], 10 | borderColor: ['checked'], 11 | }, 12 | }, 13 | 14 | theme: { 15 | extend: { 16 | colors: { 17 | 'darkish-blue': '#182635', 18 | 'darkish-black': '#1C1C1F', 19 | 'grey-light': 'F5F5F5', 20 | }, 21 | width: { 22 | '7/10': '70%', 23 | '3/10': '30%', 24 | }, 25 | height: { 26 | '7/10': '70%', 27 | '3/10': '30%', 28 | }, 29 | screens: { 30 | xs: '362px', 31 | }, 32 | }, 33 | fontFamily: { 34 | plex: ['"IBM Plex Sans"', 'sans-serif'], 35 | sans: [ 36 | 'Poppins', 37 | 'system-ui', 38 | '-apple-system', 39 | 'BlinkMacSystemFont', 40 | '"Segoe UI"', 41 | 'Roboto', 42 | '"Helvetica Neue"', 43 | 'Arial', 44 | '"Noto Sans"', 45 | 'sans-serif', 46 | '"Apple Color Emoji"', 47 | '"Segoe UI Emoji"', 48 | '"Segoe UI Symbol"', 49 | '"Noto Color Emoji"', 50 | ], 51 | serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'], 52 | mono: [ 53 | 'Menlo', 54 | 'Monaco', 55 | 'Consolas', 56 | '"Liberation Mono"', 57 | '"Courier New"', 58 | 'monospace', 59 | ], 60 | }, 61 | }, 62 | plugins: [ 63 | require('@tailwindcss/forms'), 64 | // ... 65 | ], 66 | 67 | // variants: { 68 | // backgroundColor: [ 69 | // 'dark', 70 | // 'dark-hover', 71 | // 'dark-group-hover', 72 | // 'dark-even', 73 | // 'dark-odd', 74 | // 'hover', 75 | // 'responsive', 76 | // ], 77 | // borderColor: [ 78 | // 'dark', 79 | // 'dark-focus', 80 | // 'dark-focus-within', 81 | // 'hover', 82 | // 'responsive', 83 | // ], 84 | // textColor: ['dark', 'dark-hover', 'dark-active', 'hover', 'responsive'], 85 | // }, 86 | } 87 | -------------------------------------------------------------------------------- /src/assets/top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Components/UI/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment, h } from 'preact' 2 | import TopImg from './../../assets/top.svg' 3 | import Button from './Button' 4 | import { useWindowSize } from './../../hooks/useWindowSize' 5 | // import GithubIcon from './../../assets/github.svg' 6 | // import BoxIcon from './../../assets/box.svg' 7 | 8 | function Navbar() { 9 | const { width } = useWindowSize() 10 | const mobileSize = width < 715 11 | 12 | return ( 13 |
14 |
15 |
16 |
17 | top abstract illustration 22 |
23 |
24 |
25 | Tinter 26 |
27 |
28 |
29 | {!mobileSize ? ( 30 | 31 | 39 | 47 | 48 | ) : ( 49 | 50 | {/* 55 | 56 | 57 | 62 | 63 | */} 64 | 65 | )} 66 |
67 |
68 | ) 69 | } 70 | 71 | export default Navbar 72 | -------------------------------------------------------------------------------- /src/Components/AbstractArt/AbstractArt.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import MountainImage from '../../assets/mountain.webp' 3 | 4 | const AbstractArt = () => ( 5 |
6 | 97 | 98 | 30 | 39 | 40 | 69 | 70 | 71 | 72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 89 | 90 | -------------------------------------------------------------------------------- /lib/webcomponent/float-menu.js: -------------------------------------------------------------------------------- 1 | const template = document.createElement('template') 2 | 3 | template.innerHTML = ` 4 | 93 | 94 | 96 | 97 |
98 |
99 |
100 | 101 |
102 |
103 | 131 |
132 | ` 133 | 134 | class FloatMenu extends HTMLElement { 135 | constructor() { 136 | super() 137 | this.attachShadow({ mode: 'open' }) 138 | this.shadowRoot.appendChild(template.content.cloneNode(true)) 139 | 140 | const homeBtn = this.shadowRoot.querySelector('.btn-wrapper') 141 | const iconsDiv = this.shadowRoot.querySelector('.icons') 142 | 143 | homeBtn.addEventListener('click', () => { 144 | homeBtn.classList.toggle('active') 145 | iconsDiv.classList.toggle('open') 146 | }) 147 | 148 | const screenWidth = window.innerWidth 149 | 150 | if (screenWidth < 2000) { 151 | homeBtn.classList.toggle('active') 152 | iconsDiv.classList.toggle('open') 153 | } 154 | } 155 | } 156 | 157 | window.customElements.define('float-menu', FloatMenu) 158 | 159 | export default FloatMenu 160 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com */ 2 | 3 | /*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ 4 | 5 | /* 6 | Document 7 | ======== 8 | */ 9 | 10 | /** 11 | Use a better box model (opinionated). 12 | */ 13 | 14 | *, 15 | ::before, 16 | ::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | /** 21 | Use a more readable tab size (opinionated). 22 | */ 23 | 24 | html { 25 | -moz-tab-size: 4; 26 | -o-tab-size: 4; 27 | tab-size: 4; 28 | } 29 | 30 | /** 31 | 1. Correct the line height in all browsers. 32 | 2. Prevent adjustments of font size after orientation changes in iOS. 33 | */ 34 | 35 | html { 36 | line-height: 1.15; /* 1 */ 37 | -webkit-text-size-adjust: 100%; /* 2 */ 38 | } 39 | 40 | /* 41 | Sections 42 | ======== 43 | */ 44 | 45 | /** 46 | Remove the margin in all browsers. 47 | */ 48 | 49 | body { 50 | margin: 0; 51 | } 52 | 53 | /** 54 | Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 55 | */ 56 | 57 | body { 58 | font-family: 59 | system-ui, 60 | -apple-system, /* Firefox supports this but not yet `system-ui` */ 61 | 'Segoe UI', 62 | Roboto, 63 | Helvetica, 64 | Arial, 65 | sans-serif, 66 | 'Apple Color Emoji', 67 | 'Segoe UI Emoji'; 68 | } 69 | 70 | /* 71 | Grouping content 72 | ================ 73 | */ 74 | 75 | /** 76 | 1. Add the correct height in Firefox. 77 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 78 | */ 79 | 80 | hr { 81 | height: 0; /* 1 */ 82 | color: inherit; /* 2 */ 83 | } 84 | 85 | /* 86 | Text-level semantics 87 | ==================== 88 | */ 89 | 90 | /** 91 | Add the correct text decoration in Chrome, Edge, and Safari. 92 | */ 93 | 94 | abbr[title] { 95 | -webkit-text-decoration: underline dotted; 96 | text-decoration: underline dotted; 97 | } 98 | 99 | /** 100 | Add the correct font weight in Edge and Safari. 101 | */ 102 | 103 | b, 104 | strong { 105 | font-weight: bolder; 106 | } 107 | 108 | /** 109 | 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 110 | 2. Correct the odd 'em' font sizing in all browsers. 111 | */ 112 | 113 | code, 114 | kbd, 115 | samp, 116 | pre { 117 | font-family: 118 | ui-monospace, 119 | SFMono-Regular, 120 | Consolas, 121 | 'Liberation Mono', 122 | Menlo, 123 | monospace; /* 1 */ 124 | font-size: 1em; /* 2 */ 125 | } 126 | 127 | /** 128 | Add the correct font size in all browsers. 129 | */ 130 | 131 | small { 132 | font-size: 80%; 133 | } 134 | 135 | /** 136 | Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. 137 | */ 138 | 139 | sub, 140 | sup { 141 | font-size: 75%; 142 | line-height: 0; 143 | position: relative; 144 | vertical-align: baseline; 145 | } 146 | 147 | sub { 148 | bottom: -0.25em; 149 | } 150 | 151 | sup { 152 | top: -0.5em; 153 | } 154 | 155 | /* 156 | Tabular data 157 | ============ 158 | */ 159 | 160 | /** 161 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 162 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 163 | */ 164 | 165 | table { 166 | text-indent: 0; /* 1 */ 167 | border-color: inherit; /* 2 */ 168 | } 169 | 170 | /* 171 | Forms 172 | ===== 173 | */ 174 | 175 | /** 176 | 1. Change the font styles in all browsers. 177 | 2. Remove the margin in Firefox and Safari. 178 | */ 179 | 180 | button, 181 | input, 182 | optgroup, 183 | select, 184 | textarea { 185 | font-family: inherit; /* 1 */ 186 | font-size: 100%; /* 1 */ 187 | line-height: 1.15; /* 1 */ 188 | margin: 0; /* 2 */ 189 | } 190 | 191 | /** 192 | Remove the inheritance of text transform in Edge and Firefox. 193 | 1. Remove the inheritance of text transform in Firefox. 194 | */ 195 | 196 | button, 197 | select { /* 1 */ 198 | text-transform: none; 199 | } 200 | 201 | /** 202 | Correct the inability to style clickable types in iOS and Safari. 203 | */ 204 | 205 | button, 206 | [type='button'], 207 | [type='reset'] { 208 | -webkit-appearance: button; 209 | } 210 | 211 | /** 212 | Remove the inner border and padding in Firefox. 213 | */ 214 | 215 | /** 216 | Restore the focus styles unset by the previous rule. 217 | */ 218 | 219 | /** 220 | Remove the additional ':invalid' styles in Firefox. 221 | See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737 222 | */ 223 | 224 | /** 225 | Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. 226 | */ 227 | 228 | legend { 229 | padding: 0; 230 | } 231 | 232 | /** 233 | Add the correct vertical alignment in Chrome and Firefox. 234 | */ 235 | 236 | progress { 237 | vertical-align: baseline; 238 | } 239 | 240 | /** 241 | Correct the cursor style of increment and decrement buttons in Safari. 242 | */ 243 | 244 | /** 245 | 1. Correct the odd appearance in Chrome and Safari. 246 | 2. Correct the outline style in Safari. 247 | */ 248 | 249 | /** 250 | Remove the inner padding in Chrome and Safari on macOS. 251 | */ 252 | 253 | /** 254 | 1. Correct the inability to style clickable types in iOS and Safari. 255 | 2. Change font properties to 'inherit' in Safari. 256 | */ 257 | 258 | /* 259 | Interactive 260 | =========== 261 | */ 262 | 263 | /* 264 | Add the correct display in Chrome and Safari. 265 | */ 266 | 267 | summary { 268 | display: list-item; 269 | } 270 | 271 | /** 272 | * Manually forked from SUIT CSS Base: https://github.com/suitcss/base 273 | * A thin layer on top of normalize.css that provides a starting point more 274 | * suitable for web applications. 275 | */ 276 | 277 | /** 278 | * Removes the default spacing and border for appropriate elements. 279 | */ 280 | 281 | blockquote, 282 | dl, 283 | dd, 284 | h1, 285 | h2, 286 | h3, 287 | h4, 288 | h5, 289 | h6, 290 | hr, 291 | figure, 292 | p, 293 | pre { 294 | margin: 0; 295 | } 296 | 297 | button { 298 | background-color: transparent; 299 | background-image: none; 300 | } 301 | 302 | /** 303 | * Work around a Firefox/IE bug where the transparent `button` background 304 | * results in a loss of the default `button` focus styles. 305 | */ 306 | 307 | button:focus { 308 | outline: 1px dotted; 309 | outline: 5px auto -webkit-focus-ring-color; 310 | } 311 | 312 | fieldset { 313 | margin: 0; 314 | padding: 0; 315 | } 316 | 317 | ol, 318 | ul { 319 | list-style: none; 320 | margin: 0; 321 | padding: 0; 322 | } 323 | 324 | /** 325 | * Tailwind custom reset styles 326 | */ 327 | 328 | /** 329 | * 1. Use the user's configured `sans` font-family (with Tailwind's default 330 | * sans-serif font stack as a fallback) as a sane default. 331 | * 2. Use Tailwind's default "normal" line-height so the user isn't forced 332 | * to override it to ensure consistency even when using the default theme. 333 | */ 334 | 335 | html { 336 | font-family: Poppins, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ 337 | line-height: 1.5; /* 2 */ 338 | } 339 | 340 | /** 341 | * Inherit font-family and line-height from `html` so users can set them as 342 | * a class directly on the `html` element. 343 | */ 344 | 345 | body { 346 | font-family: inherit; 347 | line-height: inherit; 348 | } 349 | 350 | /** 351 | * 1. Prevent padding and border from affecting element width. 352 | * 353 | * We used to set this in the html element and inherit from 354 | * the parent element for everything else. This caused issues 355 | * in shadow-dom-enhanced elements like
where the content 356 | * is wrapped by a div with box-sizing set to `content-box`. 357 | * 358 | * https://github.com/mozdevs/cssremedy/issues/4 359 | * 360 | * 361 | * 2. Allow adding a border to an element by just adding a border-width. 362 | * 363 | * By default, the way the browser specifies that an element should have no 364 | * border is by setting it's border-style to `none` in the user-agent 365 | * stylesheet. 366 | * 367 | * In order to easily add borders to elements by just setting the `border-width` 368 | * property, we change the default border-style for all elements to `solid`, and 369 | * use border-width to hide them instead. This way our `border` utilities only 370 | * need to set the `border-width` property instead of the entire `border` 371 | * shorthand, making our border utilities much more straightforward to compose. 372 | * 373 | * https://github.com/tailwindcss/tailwindcss/pull/116 374 | */ 375 | 376 | *, 377 | ::before, 378 | ::after { 379 | box-sizing: border-box; /* 1 */ 380 | border-width: 0; /* 2 */ 381 | border-style: solid; /* 2 */ 382 | border-color: #e5e7eb; /* 2 */ 383 | } 384 | 385 | /* 386 | * Ensure horizontal rules are visible by default 387 | */ 388 | 389 | hr { 390 | border-top-width: 1px; 391 | } 392 | 393 | /** 394 | * Undo the `border-style: none` reset that Normalize applies to images so that 395 | * our `border-{width}` utilities have the expected effect. 396 | * 397 | * The Normalize reset is unnecessary for us since we default the border-width 398 | * to 0 on all elements. 399 | * 400 | * https://github.com/tailwindcss/tailwindcss/issues/362 401 | */ 402 | 403 | img { 404 | border-style: solid; 405 | } 406 | 407 | textarea { 408 | resize: vertical; 409 | } 410 | 411 | input::-moz-placeholder, textarea::-moz-placeholder { 412 | opacity: 1; 413 | color: #9ca3af; 414 | } 415 | 416 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 417 | opacity: 1; 418 | color: #9ca3af; 419 | } 420 | 421 | input::placeholder, 422 | textarea::placeholder { 423 | opacity: 1; 424 | color: #9ca3af; 425 | } 426 | 427 | button { 428 | cursor: pointer; 429 | } 430 | 431 | table { 432 | border-collapse: collapse; 433 | } 434 | 435 | h1, 436 | h2, 437 | h3, 438 | h4, 439 | h5, 440 | h6 { 441 | font-size: inherit; 442 | font-weight: inherit; 443 | } 444 | 445 | /** 446 | * Reset links to optimize for opt-in styling instead of 447 | * opt-out. 448 | */ 449 | 450 | a { 451 | color: inherit; 452 | text-decoration: inherit; 453 | } 454 | 455 | /** 456 | * Reset form element properties that are easy to forget to 457 | * style explicitly so you don't inadvertently introduce 458 | * styles that deviate from your design system. These styles 459 | * supplement a partial reset that is already applied by 460 | * normalize.css. 461 | */ 462 | 463 | button, 464 | input, 465 | optgroup, 466 | select, 467 | textarea { 468 | padding: 0; 469 | line-height: inherit; 470 | color: inherit; 471 | } 472 | 473 | /** 474 | * Use the configured 'mono' font family for elements that 475 | * are expected to be rendered with a monospace font, falling 476 | * back to the system monospace stack if there is no configured 477 | * 'mono' font family. 478 | */ 479 | 480 | pre, 481 | code, 482 | kbd, 483 | samp { 484 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 485 | } 486 | 487 | /** 488 | * Make replaced elements `display: block` by default as that's 489 | * the behavior you want almost all of the time. Inspired by 490 | * CSS Remedy, with `svg` added as well. 491 | * 492 | * https://github.com/mozdevs/cssremedy/issues/14 493 | */ 494 | 495 | img, 496 | svg, 497 | video, 498 | canvas, 499 | audio, 500 | iframe, 501 | embed, 502 | object { 503 | display: block; 504 | vertical-align: middle; 505 | } 506 | 507 | /** 508 | * Constrain images and videos to the parent width and preserve 509 | * their intrinsic aspect ratio. 510 | * 511 | * https://github.com/mozdevs/cssremedy/issues/14 512 | */ 513 | 514 | img, 515 | video { 516 | max-width: 100%; 517 | height: auto; 518 | } 519 | 520 | [type='text'],[type='url'],[type='time'],textarea,select { 521 | -webkit-appearance: none; 522 | -moz-appearance: none; 523 | appearance: none; 524 | background-color: #fff; 525 | border-color: #6b7280; 526 | border-width: 1px; 527 | border-radius: 0px; 528 | padding-top: 0.5rem; 529 | padding-right: 0.75rem; 530 | padding-bottom: 0.5rem; 531 | padding-left: 0.75rem; 532 | font-size: 1rem; 533 | line-height: 1.5rem; 534 | } 535 | 536 | [type='text']:focus, [type='url']:focus, [type='time']:focus, textarea:focus, select:focus { 537 | outline: 2px solid transparent; 538 | outline-offset: 2px; 539 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 540 | --tw-ring-offset-width: 0px; 541 | --tw-ring-offset-color: #fff; 542 | --tw-ring-color: #2563eb; 543 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 544 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 545 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 546 | border-color: #2563eb; 547 | } 548 | 549 | input::-moz-placeholder, textarea::-moz-placeholder { 550 | color: #6b7280; 551 | opacity: 1; 552 | } 553 | 554 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 555 | color: #6b7280; 556 | opacity: 1; 557 | } 558 | 559 | input::placeholder,textarea::placeholder { 560 | color: #6b7280; 561 | opacity: 1; 562 | } 563 | 564 | select { 565 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); 566 | background-position: right 0.5rem center; 567 | background-repeat: no-repeat; 568 | background-size: 1.5em 1.5em; 569 | padding-right: 2.5rem; 570 | -webkit-print-color-adjust: exact; 571 | color-adjust: exact; 572 | } 573 | 574 | [type='checkbox'] { 575 | -webkit-appearance: none; 576 | -moz-appearance: none; 577 | appearance: none; 578 | padding: 0; 579 | -webkit-print-color-adjust: exact; 580 | color-adjust: exact; 581 | display: inline-block; 582 | vertical-align: middle; 583 | background-origin: border-box; 584 | -webkit-user-select: none; 585 | -moz-user-select: none; 586 | -ms-user-select: none; 587 | user-select: none; 588 | flex-shrink: 0; 589 | height: 1rem; 590 | width: 1rem; 591 | color: #2563eb; 592 | background-color: #fff; 593 | border-color: #6b7280; 594 | border-width: 1px; 595 | } 596 | 597 | [type='checkbox'] { 598 | border-radius: 0px; 599 | } 600 | 601 | [type='checkbox']:focus { 602 | outline: 2px solid transparent; 603 | outline-offset: 2px; 604 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 605 | --tw-ring-offset-width: 2px; 606 | --tw-ring-offset-color: #fff; 607 | --tw-ring-color: #2563eb; 608 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 609 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 610 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 611 | } 612 | 613 | [type='checkbox']:checked { 614 | border-color: transparent; 615 | background-color: currentColor; 616 | background-size: 100% 100%; 617 | background-position: center; 618 | background-repeat: no-repeat; 619 | } 620 | 621 | [type='checkbox']:checked { 622 | background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); 623 | } 624 | 625 | [type='checkbox']:checked:hover,[type='checkbox']:checked:focus { 626 | border-color: transparent; 627 | background-color: currentColor; 628 | } 629 | 630 | [type='checkbox']:indeterminate { 631 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); 632 | border-color: transparent; 633 | background-color: currentColor; 634 | background-size: 100% 100%; 635 | background-position: center; 636 | background-repeat: no-repeat; 637 | } 638 | 639 | [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { 640 | border-color: transparent; 641 | background-color: currentColor; 642 | } 643 | 644 | [type='file'] { 645 | background: unset; 646 | border-color: inherit; 647 | border-width: 0; 648 | border-radius: 0; 649 | padding: 0; 650 | font-size: unset; 651 | line-height: inherit; 652 | } 653 | 654 | [type='file']:focus { 655 | outline: 1px auto -webkit-focus-ring-color; 656 | } 657 | 658 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 659 | --tw-space-y-reverse: 0; 660 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 661 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 662 | } 663 | 664 | .sr-only { 665 | position: absolute; 666 | width: 1px; 667 | height: 1px; 668 | padding: 0; 669 | margin: -1px; 670 | overflow: hidden; 671 | clip: rect(0, 0, 0, 0); 672 | white-space: nowrap; 673 | border-width: 0; 674 | } 675 | 676 | .appearance-none { 677 | -webkit-appearance: none; 678 | -moz-appearance: none; 679 | appearance: none; 680 | } 681 | 682 | .bg-black { 683 | --tw-bg-opacity: 1; 684 | background-color: rgba(0, 0, 0, var(--tw-bg-opacity)); 685 | } 686 | 687 | .bg-white { 688 | --tw-bg-opacity: 1; 689 | background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); 690 | } 691 | 692 | .bg-blue-100 { 693 | --tw-bg-opacity: 1; 694 | background-color: rgba(219, 234, 254, var(--tw-bg-opacity)); 695 | } 696 | 697 | .bg-darkish-black { 698 | --tw-bg-opacity: 1; 699 | background-color: rgba(28, 28, 31, var(--tw-bg-opacity)); 700 | } 701 | 702 | .bg-grey-light { 703 | background-color: F5F5F5; 704 | } 705 | 706 | .hover\:bg-blue-200:hover { 707 | --tw-bg-opacity: 1; 708 | background-color: rgba(191, 219, 254, var(--tw-bg-opacity)); 709 | } 710 | 711 | .bg-gradient-to-br { 712 | background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); 713 | } 714 | 715 | .from-yellow-400 { 716 | --tw-gradient-from: #fbbf24; 717 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(251, 191, 36, 0)); 718 | } 719 | 720 | .via-red-500 { 721 | --tw-gradient-stops: var(--tw-gradient-from), #ef4444, var(--tw-gradient-to, rgba(239, 68, 68, 0)); 722 | } 723 | 724 | .to-pink-500 { 725 | --tw-gradient-to: #ec4899; 726 | } 727 | 728 | .border-transparent { 729 | border-color: transparent; 730 | } 731 | 732 | .border-gray-500 { 733 | --tw-border-opacity: 1; 734 | border-color: rgba(107, 114, 128, var(--tw-border-opacity)); 735 | } 736 | 737 | .rounded { 738 | border-radius: 0.25rem; 739 | } 740 | 741 | .rounded-md { 742 | border-radius: 0.375rem; 743 | } 744 | 745 | .rounded-lg { 746 | border-radius: 0.5rem; 747 | } 748 | 749 | .rounded-2xl { 750 | border-radius: 1rem; 751 | } 752 | 753 | .rounded-full { 754 | border-radius: 9999px; 755 | } 756 | 757 | .border-dashed { 758 | border-style: dashed; 759 | } 760 | 761 | .border { 762 | border-width: 1px; 763 | } 764 | 765 | .cursor-pointer { 766 | cursor: pointer; 767 | } 768 | 769 | .block { 770 | display: block; 771 | } 772 | 773 | .inline-block { 774 | display: inline-block; 775 | } 776 | 777 | .flex { 778 | display: flex; 779 | } 780 | 781 | .inline-flex { 782 | display: inline-flex; 783 | } 784 | 785 | .table { 786 | display: table; 787 | } 788 | 789 | .grid { 790 | display: grid; 791 | } 792 | 793 | .contents { 794 | display: contents; 795 | } 796 | 797 | .hidden { 798 | display: none; 799 | } 800 | 801 | .flex-col { 802 | flex-direction: column; 803 | } 804 | 805 | .items-start { 806 | align-items: flex-start; 807 | } 808 | 809 | .items-end { 810 | align-items: flex-end; 811 | } 812 | 813 | .items-center { 814 | align-items: center; 815 | } 816 | 817 | .justify-start { 818 | justify-content: flex-start; 819 | } 820 | 821 | .justify-end { 822 | justify-content: flex-end; 823 | } 824 | 825 | .justify-center { 826 | justify-content: center; 827 | } 828 | 829 | .justify-between { 830 | justify-content: space-between; 831 | } 832 | 833 | .justify-around { 834 | justify-content: space-around; 835 | } 836 | 837 | .justify-evenly { 838 | justify-content: space-evenly; 839 | } 840 | 841 | .flex-1 { 842 | flex: 1 1 0%; 843 | } 844 | 845 | .flex-shrink-0 { 846 | flex-shrink: 0; 847 | } 848 | 849 | .float-right { 850 | float: right; 851 | } 852 | 853 | .font-plex { 854 | font-family: "IBM Plex Sans", sans-serif; 855 | } 856 | 857 | .font-medium { 858 | font-weight: 500; 859 | } 860 | 861 | .font-semibold { 862 | font-weight: 600; 863 | } 864 | 865 | .font-bold { 866 | font-weight: 700; 867 | } 868 | 869 | .font-extrabold { 870 | font-weight: 800; 871 | } 872 | 873 | .h-0 { 874 | height: 0px; 875 | } 876 | 877 | .h-2 { 878 | height: 0.5rem; 879 | } 880 | 881 | .h-4 { 882 | height: 1rem; 883 | } 884 | 885 | .h-5 { 886 | height: 1.25rem; 887 | } 888 | 889 | .h-6 { 890 | height: 1.5rem; 891 | } 892 | 893 | .h-8 { 894 | height: 2rem; 895 | } 896 | 897 | .h-20 { 898 | height: 5rem; 899 | } 900 | 901 | .h-36 { 902 | height: 9rem; 903 | } 904 | 905 | .h-full { 906 | height: 100%; 907 | } 908 | 909 | .h-screen { 910 | height: 100vh; 911 | } 912 | 913 | .text-sm { 914 | font-size: 0.875rem; 915 | line-height: 1.25rem; 916 | } 917 | 918 | .text-lg { 919 | font-size: 1.125rem; 920 | line-height: 1.75rem; 921 | } 922 | 923 | .text-3xl { 924 | font-size: 1.875rem; 925 | line-height: 2.25rem; 926 | } 927 | 928 | .m-1 { 929 | margin: 0.25rem; 930 | } 931 | 932 | .m-2 { 933 | margin: 0.5rem; 934 | } 935 | 936 | .m-auto { 937 | margin: auto; 938 | } 939 | 940 | .mx-0 { 941 | margin-left: 0px; 942 | margin-right: 0px; 943 | } 944 | 945 | .my-2 { 946 | margin-top: 0.5rem; 947 | margin-bottom: 0.5rem; 948 | } 949 | 950 | .my-4 { 951 | margin-top: 1rem; 952 | margin-bottom: 1rem; 953 | } 954 | 955 | .mx-4 { 956 | margin-left: 1rem; 957 | margin-right: 1rem; 958 | } 959 | 960 | .my-8 { 961 | margin-top: 2rem; 962 | margin-bottom: 2rem; 963 | } 964 | 965 | .mx-auto { 966 | margin-left: auto; 967 | margin-right: auto; 968 | } 969 | 970 | .mr-2 { 971 | margin-right: 0.5rem; 972 | } 973 | 974 | .ml-2 { 975 | margin-left: 0.5rem; 976 | } 977 | 978 | .mr-3 { 979 | margin-right: 0.75rem; 980 | } 981 | 982 | .ml-3 { 983 | margin-left: 0.75rem; 984 | } 985 | 986 | .mr-4 { 987 | margin-right: 1rem; 988 | } 989 | 990 | .mb-4 { 991 | margin-bottom: 1rem; 992 | } 993 | 994 | .ml-4 { 995 | margin-left: 1rem; 996 | } 997 | 998 | .mr-6 { 999 | margin-right: 1.5rem; 1000 | } 1001 | 1002 | .ml-6 { 1003 | margin-left: 1.5rem; 1004 | } 1005 | 1006 | .mt-8 { 1007 | margin-top: 2rem; 1008 | } 1009 | 1010 | .mb-8 { 1011 | margin-bottom: 2rem; 1012 | } 1013 | 1014 | .mb-24 { 1015 | margin-bottom: 6rem; 1016 | } 1017 | 1018 | .-ml-1 { 1019 | margin-left: -0.25rem; 1020 | } 1021 | 1022 | .max-w-sm { 1023 | max-width: 24rem; 1024 | } 1025 | 1026 | .max-w-lg { 1027 | max-width: 32rem; 1028 | } 1029 | 1030 | .max-w-full { 1031 | max-width: 100%; 1032 | } 1033 | 1034 | .min-h-screen { 1035 | min-height: 100vh; 1036 | } 1037 | 1038 | .opacity-0 { 1039 | opacity: 0; 1040 | } 1041 | 1042 | .opacity-25 { 1043 | opacity: 0.25; 1044 | } 1045 | 1046 | .opacity-30 { 1047 | opacity: 0.3; 1048 | } 1049 | 1050 | .opacity-75 { 1051 | opacity: 0.75; 1052 | } 1053 | 1054 | .opacity-100 { 1055 | opacity: 1; 1056 | } 1057 | 1058 | .focus\:outline-none:focus { 1059 | outline: 2px solid transparent; 1060 | outline-offset: 2px; 1061 | } 1062 | 1063 | .overflow-hidden { 1064 | overflow: hidden; 1065 | } 1066 | 1067 | .overflow-y-auto { 1068 | overflow-y: auto; 1069 | } 1070 | 1071 | .p-2 { 1072 | padding: 0.5rem; 1073 | } 1074 | 1075 | .p-4 { 1076 | padding: 1rem; 1077 | } 1078 | 1079 | .p-6 { 1080 | padding: 1.5rem; 1081 | } 1082 | 1083 | .p-10 { 1084 | padding: 2.5rem; 1085 | } 1086 | 1087 | .p-20 { 1088 | padding: 5rem; 1089 | } 1090 | 1091 | .py-2 { 1092 | padding-top: 0.5rem; 1093 | padding-bottom: 0.5rem; 1094 | } 1095 | 1096 | .px-4 { 1097 | padding-left: 1rem; 1098 | padding-right: 1rem; 1099 | } 1100 | 1101 | .py-6 { 1102 | padding-top: 1.5rem; 1103 | padding-bottom: 1.5rem; 1104 | } 1105 | 1106 | .py-8 { 1107 | padding-top: 2rem; 1108 | padding-bottom: 2rem; 1109 | } 1110 | 1111 | .pt-0 { 1112 | padding-top: 0px; 1113 | } 1114 | 1115 | .pt-0\.5 { 1116 | padding-top: 0.125rem; 1117 | } 1118 | 1119 | .pointer-events-none { 1120 | pointer-events: none; 1121 | } 1122 | 1123 | .pointer-events-auto { 1124 | pointer-events: auto; 1125 | } 1126 | 1127 | .fixed { 1128 | position: fixed; 1129 | } 1130 | 1131 | .absolute { 1132 | position: absolute; 1133 | } 1134 | 1135 | .relative { 1136 | position: relative; 1137 | } 1138 | 1139 | .inset-0 { 1140 | top: 0px; 1141 | right: 0px; 1142 | bottom: 0px; 1143 | left: 0px; 1144 | } 1145 | 1146 | .top-0 { 1147 | top: 0px; 1148 | } 1149 | 1150 | .right-0 { 1151 | right: 0px; 1152 | } 1153 | 1154 | .left-0 { 1155 | left: 0px; 1156 | } 1157 | 1158 | .bottom-3 { 1159 | bottom: 0.75rem; 1160 | } 1161 | 1162 | .left-3 { 1163 | left: 0.75rem; 1164 | } 1165 | 1166 | .resize { 1167 | resize: both; 1168 | } 1169 | 1170 | * { 1171 | --tw-shadow: 0 0 #0000; 1172 | } 1173 | 1174 | .shadow-lg { 1175 | --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 1176 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1177 | } 1178 | 1179 | .shadow-xl { 1180 | --tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 1181 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1182 | } 1183 | 1184 | * { 1185 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 1186 | --tw-ring-offset-width: 0px; 1187 | --tw-ring-offset-color: #fff; 1188 | --tw-ring-color: rgba(59, 130, 246, 0.5); 1189 | --tw-ring-offset-shadow: 0 0 #0000; 1190 | --tw-ring-shadow: 0 0 #0000; 1191 | } 1192 | 1193 | .ring-1 { 1194 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1195 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1196 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1197 | } 1198 | 1199 | .focus\:ring-2:focus { 1200 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1201 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1202 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1203 | } 1204 | 1205 | .focus\:ring-offset-2:focus { 1206 | --tw-ring-offset-width: 2px; 1207 | } 1208 | 1209 | .ring-black { 1210 | --tw-ring-opacity: 1; 1211 | --tw-ring-color: rgba(0, 0, 0, var(--tw-ring-opacity)); 1212 | } 1213 | 1214 | .focus\:ring-indigo-500:focus { 1215 | --tw-ring-opacity: 1; 1216 | --tw-ring-color: rgba(99, 102, 241, var(--tw-ring-opacity)); 1217 | } 1218 | 1219 | .focus\:ring-pink-500:focus { 1220 | --tw-ring-opacity: 1; 1221 | --tw-ring-color: rgba(236, 72, 153, var(--tw-ring-opacity)); 1222 | } 1223 | 1224 | .ring-opacity-5 { 1225 | --tw-ring-opacity: 0.05; 1226 | } 1227 | 1228 | .text-left { 1229 | text-align: left; 1230 | } 1231 | 1232 | .text-center { 1233 | text-align: center; 1234 | } 1235 | 1236 | .text-black { 1237 | --tw-text-opacity: 1; 1238 | color: rgba(0, 0, 0, var(--tw-text-opacity)); 1239 | } 1240 | 1241 | .text-white { 1242 | --tw-text-opacity: 1; 1243 | color: rgba(255, 255, 255, var(--tw-text-opacity)); 1244 | } 1245 | 1246 | .text-gray-400 { 1247 | --tw-text-opacity: 1; 1248 | color: rgba(156, 163, 175, var(--tw-text-opacity)); 1249 | } 1250 | 1251 | .text-gray-900 { 1252 | --tw-text-opacity: 1; 1253 | color: rgba(17, 24, 39, var(--tw-text-opacity)); 1254 | } 1255 | 1256 | .text-blue-900 { 1257 | --tw-text-opacity: 1; 1258 | color: rgba(30, 58, 138, var(--tw-text-opacity)); 1259 | } 1260 | 1261 | .text-pink-500 { 1262 | --tw-text-opacity: 1; 1263 | color: rgba(236, 72, 153, var(--tw-text-opacity)); 1264 | } 1265 | 1266 | .hover\:text-gray-500:hover { 1267 | --tw-text-opacity: 1; 1268 | color: rgba(107, 114, 128, var(--tw-text-opacity)); 1269 | } 1270 | 1271 | .align-middle { 1272 | vertical-align: middle; 1273 | } 1274 | 1275 | .align-bottom { 1276 | vertical-align: bottom; 1277 | } 1278 | 1279 | .w-0 { 1280 | width: 0px; 1281 | } 1282 | 1283 | .w-4 { 1284 | width: 1rem; 1285 | } 1286 | 1287 | .w-5 { 1288 | width: 1.25rem; 1289 | } 1290 | 1291 | .w-6 { 1292 | width: 1.5rem; 1293 | } 1294 | 1295 | .w-8 { 1296 | width: 2rem; 1297 | } 1298 | 1299 | .w-36 { 1300 | width: 9rem; 1301 | } 1302 | 1303 | .w-full { 1304 | width: 100%; 1305 | } 1306 | 1307 | .z-10 { 1308 | z-index: 10; 1309 | } 1310 | 1311 | .z-50 { 1312 | z-index: 50; 1313 | } 1314 | 1315 | .grid-cols-2 { 1316 | grid-template-columns: repeat(2, minmax(0, 1fr)); 1317 | } 1318 | 1319 | .transform { 1320 | --tw-translate-x: 0; 1321 | --tw-translate-y: 0; 1322 | --tw-rotate: 0; 1323 | --tw-skew-x: 0; 1324 | --tw-skew-y: 0; 1325 | --tw-scale-x: 1; 1326 | --tw-scale-y: 1; 1327 | transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1328 | } 1329 | 1330 | .scale-95 { 1331 | --tw-scale-x: .95; 1332 | --tw-scale-y: .95; 1333 | } 1334 | 1335 | .scale-100 { 1336 | --tw-scale-x: 1; 1337 | --tw-scale-y: 1; 1338 | } 1339 | 1340 | .translate-y-0 { 1341 | --tw-translate-y: 0px; 1342 | } 1343 | 1344 | .translate-y-2 { 1345 | --tw-translate-y: 0.5rem; 1346 | } 1347 | 1348 | .transition-all { 1349 | transition-property: all; 1350 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1351 | transition-duration: 150ms; 1352 | } 1353 | 1354 | .transition { 1355 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; 1356 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 1357 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; 1358 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1359 | transition-duration: 150ms; 1360 | } 1361 | 1362 | .ease-in { 1363 | transition-timing-function: cubic-bezier(0.4, 0, 1, 1); 1364 | } 1365 | 1366 | .ease-out { 1367 | transition-timing-function: cubic-bezier(0, 0, 0.2, 1); 1368 | } 1369 | 1370 | .duration-100 { 1371 | transition-duration: 100ms; 1372 | } 1373 | 1374 | .duration-200 { 1375 | transition-duration: 200ms; 1376 | } 1377 | 1378 | .duration-300 { 1379 | transition-duration: 300ms; 1380 | } 1381 | 1382 | @-webkit-keyframes spin { 1383 | to { 1384 | transform: rotate(360deg); 1385 | } 1386 | } 1387 | 1388 | @keyframes spin { 1389 | to { 1390 | transform: rotate(360deg); 1391 | } 1392 | } 1393 | 1394 | @-webkit-keyframes ping { 1395 | 75%, 100% { 1396 | transform: scale(2); 1397 | opacity: 0; 1398 | } 1399 | } 1400 | 1401 | @keyframes ping { 1402 | 75%, 100% { 1403 | transform: scale(2); 1404 | opacity: 0; 1405 | } 1406 | } 1407 | 1408 | @-webkit-keyframes pulse { 1409 | 50% { 1410 | opacity: .5; 1411 | } 1412 | } 1413 | 1414 | @keyframes pulse { 1415 | 50% { 1416 | opacity: .5; 1417 | } 1418 | } 1419 | 1420 | @-webkit-keyframes bounce { 1421 | 0%, 100% { 1422 | transform: translateY(-25%); 1423 | -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1); 1424 | animation-timing-function: cubic-bezier(0.8,0,1,1); 1425 | } 1426 | 1427 | 50% { 1428 | transform: none; 1429 | -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1); 1430 | animation-timing-function: cubic-bezier(0,0,0.2,1); 1431 | } 1432 | } 1433 | 1434 | @keyframes bounce { 1435 | 0%, 100% { 1436 | transform: translateY(-25%); 1437 | -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1); 1438 | animation-timing-function: cubic-bezier(0.8,0,1,1); 1439 | } 1440 | 1441 | 50% { 1442 | transform: none; 1443 | -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1); 1444 | animation-timing-function: cubic-bezier(0,0,0.2,1); 1445 | } 1446 | } 1447 | 1448 | .animate-spin { 1449 | -webkit-animation: spin 1s linear infinite; 1450 | animation: spin 1s linear infinite; 1451 | } 1452 | 1453 | .filter { 1454 | --tw-blur: var(--tw-empty,/*!*/ /*!*/); 1455 | --tw-brightness: var(--tw-empty,/*!*/ /*!*/); 1456 | --tw-contrast: var(--tw-empty,/*!*/ /*!*/); 1457 | --tw-grayscale: var(--tw-empty,/*!*/ /*!*/); 1458 | --tw-hue-rotate: var(--tw-empty,/*!*/ /*!*/); 1459 | --tw-invert: var(--tw-empty,/*!*/ /*!*/); 1460 | --tw-saturate: var(--tw-empty,/*!*/ /*!*/); 1461 | --tw-sepia: var(--tw-empty,/*!*/ /*!*/); 1462 | --tw-drop-shadow: var(--tw-empty,/*!*/ /*!*/); 1463 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 1464 | } 1465 | 1466 | .blur { 1467 | --tw-blur: blur(8px); 1468 | } 1469 | 1470 | @media (min-width: 640px) { 1471 | .sm\:items-start { 1472 | align-items: flex-start; 1473 | } 1474 | 1475 | .sm\:items-end { 1476 | align-items: flex-end; 1477 | } 1478 | 1479 | .sm\:p-6 { 1480 | padding: 1.5rem; 1481 | } 1482 | 1483 | .sm\:translate-x-0 { 1484 | --tw-translate-x: 0px; 1485 | } 1486 | 1487 | .sm\:translate-x-2 { 1488 | --tw-translate-x: 0.5rem; 1489 | } 1490 | 1491 | .sm\:translate-y-0 { 1492 | --tw-translate-y: 0px; 1493 | } 1494 | } 1495 | 1496 | @media (min-width: 768px) { 1497 | .md\:h-12 { 1498 | height: 3rem; 1499 | } 1500 | 1501 | .md\:h-auto { 1502 | height: auto; 1503 | } 1504 | 1505 | .md\:mx-16 { 1506 | margin-left: 4rem; 1507 | margin-right: 4rem; 1508 | } 1509 | 1510 | .md\:w-12 { 1511 | width: 3rem; 1512 | } 1513 | } 1514 | 1515 | @media (min-width: 1024px) { 1516 | .lg\:flex-row { 1517 | flex-direction: row; 1518 | } 1519 | 1520 | .lg\:h-2\/5 { 1521 | height: 40%; 1522 | } 1523 | 1524 | .lg\:h-3\/5 { 1525 | height: 60%; 1526 | } 1527 | 1528 | .lg\:h-screen { 1529 | height: 100vh; 1530 | } 1531 | 1532 | .lg\:text-5xl { 1533 | font-size: 3rem; 1534 | line-height: 1; 1535 | } 1536 | 1537 | .lg\:mx-8 { 1538 | margin-left: 2rem; 1539 | margin-right: 2rem; 1540 | } 1541 | 1542 | .lg\:mx-16 { 1543 | margin-left: 4rem; 1544 | margin-right: 4rem; 1545 | } 1546 | 1547 | .lg\:mb-0 { 1548 | margin-bottom: 0px; 1549 | } 1550 | 1551 | .lg\:mb-72 { 1552 | margin-bottom: 18rem; 1553 | } 1554 | 1555 | .lg\:overflow-hidden { 1556 | overflow: hidden; 1557 | } 1558 | 1559 | .lg\:py-1 { 1560 | padding-top: 0.25rem; 1561 | padding-bottom: 0.25rem; 1562 | } 1563 | 1564 | .lg\:px-4 { 1565 | padding-left: 1rem; 1566 | padding-right: 1rem; 1567 | } 1568 | 1569 | .lg\:fixed { 1570 | position: fixed; 1571 | } 1572 | 1573 | .lg\:w-1\/2 { 1574 | width: 50%; 1575 | } 1576 | } 1577 | 1578 | @media (min-width: 1280px) { 1579 | .xl\:w-2\/5 { 1580 | width: 40%; 1581 | } 1582 | 1583 | .xl\:w-3\/5 { 1584 | width: 60%; 1585 | } 1586 | } 1587 | 1588 | @media (min-width: 1536px) { 1589 | } 1590 | 1591 | @media (min-width: 362px) { 1592 | } 1593 | --------------------------------------------------------------------------------