mirror of
https://github.com/tormachris/cf-workers-status-page.git
synced 2024-11-23 22:45:43 +01:00
Merge pull request #15 from aexvir/aexvir/tailwind
refactor: switch css framework to tailwind
This commit is contained in:
commit
35c620f485
Binary file not shown.
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 450 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
@ -133,3 +133,4 @@ worker/
|
||||
.direnv/
|
||||
out/
|
||||
package-lock.json
|
||||
public/style.css
|
||||
|
4
index.js
4
index.js
@ -10,7 +10,7 @@ import { processCronTrigger } from './src/functions/cronTrigger'
|
||||
*/
|
||||
const DEBUG = false
|
||||
|
||||
addEventListener('fetch', event => {
|
||||
addEventListener('fetch', (event) => {
|
||||
try {
|
||||
event.respondWith(
|
||||
handleEvent(event, require.context('./pages/', true, /\.js$/), DEBUG),
|
||||
@ -27,6 +27,6 @@ addEventListener('fetch', event => {
|
||||
}
|
||||
})
|
||||
|
||||
addEventListener('scheduled', event => {
|
||||
addEventListener('scheduled', (event) => {
|
||||
event.waitUntil(processCronTrigger(event))
|
||||
})
|
||||
|
17
package.json
17
package.json
@ -7,21 +7,26 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "flareact dev",
|
||||
"build": "flareact build",
|
||||
"deploy": "flareact publish",
|
||||
"build": "yarn css && flareact build",
|
||||
"deploy": "yarn build && flareact publish",
|
||||
"kv-gc": "node ./src/cli/gcMonitors.js",
|
||||
"format": "prettier --write '**/*.{js,css,json,md}'"
|
||||
"format": "prettier --write '**/*.{js,css,json,md}'",
|
||||
"css": "postcss public/tailwind.css -o public/style.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"flareact": "^0.9.0",
|
||||
"laco": "^1.2.1",
|
||||
"laco-react": "^1.1.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.0.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prettier": "^1.18.2",
|
||||
"postcss": "^8.1.8",
|
||||
"postcss-cli": "^8.3.0",
|
||||
"prettier": "^2.2.0",
|
||||
"tailwindcss": "^2.0.1",
|
||||
"yaml-loader": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { processCronTrigger } from '../../src/functions/cronTrigger'
|
||||
|
||||
export default async event => {
|
||||
export default async (event) => {
|
||||
// used only for local debugging
|
||||
//return processCronTrigger(event)
|
||||
}
|
||||
|
155
pages/index.js
155
pages/index.js
@ -1,115 +1,97 @@
|
||||
import Head from 'flareact/head'
|
||||
import MonitorHistogram from '../src/components/monitorHistogram'
|
||||
|
||||
import {
|
||||
getMonitors,
|
||||
useKeyPress,
|
||||
} from '../src/functions/helpers'
|
||||
|
||||
import config from '../config.yaml'
|
||||
import MonitorStatusLabel from '../src/components/monitorStatusLabel'
|
||||
import MonitorStatusHeader from '../src/components/monitorStatusHeader'
|
||||
import MonitorFilter from '../src/components/monitorFilter'
|
||||
|
||||
import { Store } from 'laco'
|
||||
import { useStore } from 'laco-react'
|
||||
import Head from 'flareact/head'
|
||||
|
||||
const MonitorStore = new Store(
|
||||
{
|
||||
monitors: config.monitors,
|
||||
visible: config.monitors,
|
||||
activeFilter: false
|
||||
}
|
||||
)
|
||||
import { getMonitors, useKeyPress, switchTheme } from '../src/functions/helpers'
|
||||
import config from '../config.yaml'
|
||||
import MonitorCard from '../src/components/monitorCard'
|
||||
import MonitorFilter from '../src/components/monitorFilter'
|
||||
import MonitorStatusHeader from '../src/components/monitorStatusHeader'
|
||||
import ThemeSwitcher from '../src/components/themeSwitcher'
|
||||
|
||||
const filterByTerm = (term) => MonitorStore.set(
|
||||
state => ({ visible: state.monitors.filter((monitor) => monitor.name.toLowerCase().includes(term)) })
|
||||
)
|
||||
const MonitorStore = new Store({
|
||||
monitors: config.monitors,
|
||||
visible: config.monitors,
|
||||
activeFilter: false,
|
||||
})
|
||||
|
||||
const filterByTerm = (term) =>
|
||||
MonitorStore.set((state) => ({
|
||||
visible: state.monitors.filter((monitor) =>
|
||||
monitor.name.toLowerCase().includes(term),
|
||||
),
|
||||
}))
|
||||
|
||||
export async function getEdgeProps() {
|
||||
// get KV data
|
||||
const {value: kvMonitors, metadata: kvMonitorsMetadata } = await getMonitors()
|
||||
const {
|
||||
value: kvMonitors,
|
||||
metadata: kvMonitorsMetadata,
|
||||
} = await getMonitors()
|
||||
|
||||
return {
|
||||
props: {
|
||||
config,
|
||||
kvMonitors: kvMonitors || {},
|
||||
kvMonitorsMetadata: kvMonitorsMetadata || {}
|
||||
kvMonitorsMetadata: kvMonitorsMetadata || {},
|
||||
},
|
||||
// Revalidate these props once every x seconds
|
||||
revalidate: 5,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Index({
|
||||
config,
|
||||
kvMonitors,
|
||||
kvMonitorsMetadata,
|
||||
}) {
|
||||
export default function Index({ config, kvMonitors, kvMonitorsMetadata }) {
|
||||
const state = useStore(MonitorStore)
|
||||
const slash = useKeyPress('/')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="min-h-screen">
|
||||
<Head>
|
||||
<title>{config.settings.title}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/semantic.min.css"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link rel="stylesheet" href="./main.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script>
|
||||
{`
|
||||
function setTheme(theme) {
|
||||
document.documentElement.classList.remove("dark", "light")
|
||||
document.documentElement.classList.add(theme)
|
||||
localStorage.theme = theme
|
||||
}
|
||||
(() => {
|
||||
const query = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
query.addListener(() => {
|
||||
setTheme(query.matches ? "dark" : "light")
|
||||
})
|
||||
if (["dark", "light"].includes(localStorage.theme)) {
|
||||
setTheme(localStorage.theme)
|
||||
} else {
|
||||
setTheme(query.matches ? "dark" : "light")
|
||||
}
|
||||
})()
|
||||
`}
|
||||
</script>
|
||||
</Head>
|
||||
<div className="ui basic segment container">
|
||||
<div className="horizontal flex between">
|
||||
<h1 className="ui huge marginless title header">
|
||||
<img
|
||||
className="ui middle aligned tiny image"
|
||||
src={config.settings.logo}
|
||||
/>
|
||||
{config.settings.title}
|
||||
</h1>
|
||||
<MonitorFilter
|
||||
active={slash}
|
||||
callback={filterByTerm}
|
||||
/>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-row justify-between items-center p-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<img className="h-8 w-auto" src={config.settings.logo} />
|
||||
<h1 className="ml-4 text-3xl">{config.settings.title}</h1>
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
{typeof window !== 'undefined' && <ThemeSwitcher />}
|
||||
<MonitorFilter active={slash} callback={filterByTerm} />
|
||||
</div>
|
||||
</div>
|
||||
<MonitorStatusHeader
|
||||
kvMonitorsMetadata={kvMonitorsMetadata}
|
||||
/>
|
||||
<MonitorStatusHeader kvMonitorsMetadata={kvMonitorsMetadata} />
|
||||
{state.visible.map((monitor, key) => {
|
||||
return (
|
||||
<div key={key} className="ui segment">
|
||||
<div
|
||||
className="ui horizontal flex between"
|
||||
style={{ marginBottom: '8px' }}
|
||||
>
|
||||
<div className="ui marginless header">
|
||||
{monitor.description && (
|
||||
<span data-tooltip={monitor.description}>
|
||||
<i className="blue small info circle icon" />
|
||||
</span>
|
||||
)}
|
||||
<div className="content">{monitor.name}</div>
|
||||
</div>
|
||||
<MonitorStatusLabel
|
||||
kvMonitor={kvMonitors[monitor.id]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MonitorHistogram
|
||||
monitorId={monitor.id}
|
||||
kvMonitor={kvMonitors[monitor.id]}
|
||||
/>
|
||||
|
||||
<div className="horizontal flex between grey-text">
|
||||
<div>{config.settings.daysInHistogram} days ago</div>
|
||||
<div>Today</div>
|
||||
</div>
|
||||
</div>
|
||||
<MonitorCard
|
||||
key={key}
|
||||
monitor={monitor}
|
||||
data={kvMonitors[monitor.id]}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className="horizontal flex between grey-text">
|
||||
<div className="flex flex-row justify-between mt-4 text-sm">
|
||||
<div>
|
||||
Powered by{' '}
|
||||
<a href="https://workers.cloudflare.com/" target="_blank">
|
||||
@ -129,6 +111,15 @@ export default function Index({
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
{`
|
||||
function setTheme(theme) {
|
||||
document.documentElement.classList.remove("dark", "light")
|
||||
document.documentElement.classList.add(theme)
|
||||
localStorage.theme = theme
|
||||
}
|
||||
`}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
body {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.marginless {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.paddingless {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.black-text {
|
||||
color: #000 !important;
|
||||
}
|
||||
.grey-text {
|
||||
color: #a0a0a0 !important;
|
||||
}
|
||||
.white-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
.histogram {
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.hitbox {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 1px;
|
||||
border-radius: 3.75px;
|
||||
}
|
||||
.bar {
|
||||
background: #dcddde;
|
||||
padding-bottom: 1px;
|
||||
height: 100%;
|
||||
width: 85%;
|
||||
border-radius: 100px;
|
||||
}
|
||||
.bar.green {
|
||||
background: #21ba45;
|
||||
}
|
||||
.bar.red {
|
||||
background: #db2828;
|
||||
}
|
||||
.bar.orange {
|
||||
background: #f2711c;
|
||||
}
|
||||
span i.icon {
|
||||
margin: 0 .25em .25em 0 !important;
|
||||
}
|
||||
.ui.title.header .ui.image {
|
||||
margin-top: -.5em !important;
|
||||
}
|
68
public/tailwind.css
Normal file
68
public/tailwind.css
Normal file
@ -0,0 +1,68 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-50;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-blue-500 dark:text-blue-400;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply p-4 bg-white border border-gray-200 dark:bg-gray-700 dark:border-gray-600 shadow rounded-lg p-4 mb-2;
|
||||
}
|
||||
|
||||
.pill {
|
||||
@apply px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full;
|
||||
}
|
||||
|
||||
.histogram {
|
||||
@apply h-6 w-full mx-auto;
|
||||
}
|
||||
.hitbox {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 1px;
|
||||
border-radius: 3.75px;
|
||||
}
|
||||
.bar {
|
||||
@apply bg-gray-300 dark:bg-gray-600;
|
||||
padding-bottom: 1px;
|
||||
height: 100%;
|
||||
width: 85%;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.bar.green {
|
||||
@apply bg-green-400 dark:bg-green-700;
|
||||
}
|
||||
|
||||
.bar.red {
|
||||
@apply bg-red-400 dark:bg-red-700;
|
||||
}
|
||||
|
||||
.bar.yellow {
|
||||
@apply bg-yellow-400 dark:bg-yellow-700;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.tooltip .content {
|
||||
@apply invisible absolute z-50 inline-block;
|
||||
@apply rounded-full py-1 px-2 bg-gray-100 dark:bg-gray-800 shadow;
|
||||
@apply opacity-0 transition-all duration-200 scale-50;
|
||||
}
|
||||
|
||||
.tooltip:hover .content {
|
||||
@apply visible opacity-100 scale-100;
|
||||
}
|
||||
}
|
@ -9,7 +9,9 @@ const apiToken = process.env.CF_API_TOKEN
|
||||
const kvPrefix = 's_'
|
||||
|
||||
if (!accountId || !namespaceId || !apiToken) {
|
||||
console.error("Missing required environment variables: CF_ACCOUNT_ID, KV_NAMESPACE_ID, CF_API_TOKEN")
|
||||
console.error(
|
||||
'Missing required environment variables: CF_ACCOUNT_ID, KV_NAMESPACE_ID, CF_API_TOKEN',
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@ -51,24 +53,26 @@ function loadConfig() {
|
||||
return JSON.parse(config)
|
||||
}
|
||||
|
||||
getKvMonitors(kvPrefix).then(async kvMonitors => {
|
||||
const config = loadConfig()
|
||||
const monitors = config.monitors.map(key => {
|
||||
return key.id
|
||||
})
|
||||
const kvState = kvMonitors.map(key => {
|
||||
return key.name
|
||||
})
|
||||
const keysForRemoval = kvState.filter(
|
||||
x => !monitors.includes(x.replace(kvPrefix, '')),
|
||||
)
|
||||
|
||||
if (keysForRemoval.length > 0) {
|
||||
console.log(
|
||||
`Removing following keys from KV storage as they are no longer in the config: ${keysForRemoval.join(
|
||||
', ',
|
||||
)}`,
|
||||
getKvMonitors(kvPrefix)
|
||||
.then(async (kvMonitors) => {
|
||||
const config = loadConfig()
|
||||
const monitors = config.monitors.map((key) => {
|
||||
return key.id
|
||||
})
|
||||
const kvState = kvMonitors.map((key) => {
|
||||
return key.name
|
||||
})
|
||||
const keysForRemoval = kvState.filter(
|
||||
(x) => !monitors.includes(x.replace(kvPrefix, '')),
|
||||
)
|
||||
await deleteKvBulk(keysForRemoval)
|
||||
}
|
||||
}).catch(e => console.log(e))
|
||||
|
||||
if (keysForRemoval.length > 0) {
|
||||
console.log(
|
||||
`Removing following keys from KV storage as they are no longer in the config: ${keysForRemoval.join(
|
||||
', ',
|
||||
)}`,
|
||||
)
|
||||
await deleteKvBulk(keysForRemoval)
|
||||
}
|
||||
})
|
||||
.catch((e) => console.log(e))
|
||||
|
46
src/components/monitorCard.js
Normal file
46
src/components/monitorCard.js
Normal file
@ -0,0 +1,46 @@
|
||||
import config from '../../config.yaml'
|
||||
import MonitorStatusLabel from './monitorStatusLabel'
|
||||
import MonitorHistogram from './monitorHistogram'
|
||||
|
||||
const infoIcon = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="h-5 mr-2 mx-auto text-blue-500 dark:text-blue-400"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function MonitorCard({ key, monitor, data }) {
|
||||
return (
|
||||
<div key={key} className="card">
|
||||
<div className="flex flex-row justify-between items-center mb-2">
|
||||
<div className="flex flex-row items-center align-center">
|
||||
{monitor.description && (
|
||||
<div className="tooltip">
|
||||
{infoIcon}
|
||||
<div className="content text-center transform -translate-y-1/2 top-1/2 ml-8 w-72 text-sm object-left">
|
||||
{monitor.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xl">{monitor.name}</div>
|
||||
</div>
|
||||
<MonitorStatusLabel kvMonitor={data} />
|
||||
</div>
|
||||
|
||||
<MonitorHistogram monitorId={monitor.id} kvMonitor={data} />
|
||||
|
||||
<div className="flex flex-row justify-between items-center text-gray-400 text-sm">
|
||||
<div>{config.settings.daysInHistogram} days ago</div>
|
||||
<div>Today</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,6 +1,21 @@
|
||||
import config from '../../config.yaml'
|
||||
import { useState } from 'react'
|
||||
|
||||
const searchIcon = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
className="h-7 mx-auto text-gray-300 dark:text-gray-600"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 9a2 2 0 114 0 2 2 0 01-4 0z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a4 4 0 00-3.446 6.032l-2.261 2.26a1 1 0 101.414 1.415l2.261-2.261A4 4 0 1011 5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function MonitorFilter({ active, callback }) {
|
||||
const [input, setInput] = useState('')
|
||||
|
||||
@ -21,21 +36,19 @@ export default function MonitorFilter({ active, callback }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ui search">
|
||||
<div className="ui icon input">
|
||||
<input
|
||||
className="prompt"
|
||||
type="text"
|
||||
value={input}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Tap '/' to search"
|
||||
tabIndex={0}
|
||||
ref={
|
||||
(e) => e && active && e.focus()
|
||||
}
|
||||
/>
|
||||
<i className="search icon"></i>
|
||||
<div className="col-span-6 sm:col-span-3 relative">
|
||||
<input
|
||||
className="block w-full py-2 px-3 border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-full shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
type="text"
|
||||
value={input}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Tap '/' to search"
|
||||
tabIndex={0}
|
||||
ref={(e) => e && active && e.focus()}
|
||||
/>
|
||||
<div className="absolute inset-y-1 right-1 flex z-1 items-center">
|
||||
{searchIcon}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,56 +1,54 @@
|
||||
import config from '../../config.yaml'
|
||||
|
||||
export default function MonitorHistogram({
|
||||
monitorId,
|
||||
kvMonitor,
|
||||
}) {
|
||||
export default function MonitorHistogram({ monitorId, kvMonitor }) {
|
||||
// create date and set date - daysInHistogram for the first day of the histogram
|
||||
let date = new Date()
|
||||
date.setDate(date.getDate() - config.settings.daysInHistogram)
|
||||
|
||||
let content = null
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
return (
|
||||
<div
|
||||
key={`${monitorId}-histogram`}
|
||||
className="horizontal flex histogram"
|
||||
>
|
||||
{Array.from(Array(config.settings.daysInHistogram).keys()).map(key => {
|
||||
date.setDate(date.getDate() + 1)
|
||||
const dayInHistogram = date.toISOString().split('T')[0]
|
||||
content = Array.from(Array(config.settings.daysInHistogram).keys()).map(
|
||||
(key) => {
|
||||
date.setDate(date.getDate() + 1)
|
||||
const dayInHistogram = date.toISOString().split('T')[0]
|
||||
|
||||
let bg = ''
|
||||
let dayInHistogramLabel = config.settings.dayInHistogramNoData
|
||||
let bg = ''
|
||||
let dayInHistogramLabel = config.settings.dayInHistogramNoData
|
||||
|
||||
// filter all dates before first check, check the rest
|
||||
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
|
||||
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
|
||||
bg = 'green'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramOperational
|
||||
} else {
|
||||
bg = 'orange'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramNotOperational
|
||||
}
|
||||
// filter all dates before first check, check the rest
|
||||
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
|
||||
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
|
||||
bg = 'green'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramOperational
|
||||
} else {
|
||||
bg = 'yellow'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramNotOperational
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} className="hitbox">
|
||||
<div
|
||||
className={`${bg} bar`}
|
||||
data-tooltip={`${dayInHistogram} - ${dayInHistogramLabel}`}
|
||||
/>
|
||||
return (
|
||||
<div key={key} className="hitbox tooltip">
|
||||
<div className={`${bg} bar`} />
|
||||
<div className="content text-center py-1 px-2 mt-2 left-1/2 -ml-20 w-40 text-xs">
|
||||
{dayInHistogram}
|
||||
<br />
|
||||
<span className="font-semibold text-sm">
|
||||
{dayInHistogramLabel}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
key={`${monitorId}-histogram`}
|
||||
className="horizontal flex histogram"
|
||||
>
|
||||
<div className="grey-text">Loading histogram ...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${monitorId}-histogram`}
|
||||
className="flex flex-row items-center histogram"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,28 +1,34 @@
|
||||
import config from '../../config.yaml'
|
||||
|
||||
export default function MonitorStatusHeader({kvMonitorsMetadata}) {
|
||||
let backgroundColor = 'green'
|
||||
let headerText = config.settings.allmonitorsOperational
|
||||
let textColor = 'black'
|
||||
const classes = {
|
||||
green:
|
||||
'bg-green-200 text-green-700 dark:bg-green-700 dark:text-green-200 border-green-300 dark:border-green-600',
|
||||
yellow:
|
||||
'bg-yellow-200 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-200 border-yellow-300 dark:border-yellow-600',
|
||||
}
|
||||
|
||||
export default function MonitorStatusHeader({ kvMonitorsMetadata }) {
|
||||
let color = 'green'
|
||||
let text = config.settings.allmonitorsOperational
|
||||
|
||||
if (!kvMonitorsMetadata.monitorsOperational) {
|
||||
backgroundColor = 'yellow'
|
||||
headerText = config.settings.notAllmonitorsOperational
|
||||
color = 'yellow'
|
||||
text = config.settings.notAllmonitorsOperational
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`ui inverted segment ${backgroundColor}`}>
|
||||
<div className="horizontal flex between">
|
||||
<div className={`ui marginless header ${textColor}-text`}>
|
||||
{headerText}
|
||||
</div>
|
||||
{
|
||||
kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
|
||||
<div className={`${textColor}-text`}>
|
||||
checked {Math.round((Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000)} sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
|
||||
<div className={`card mb-4 font-semibold ${classes[color]}`}>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div>{text}</div>
|
||||
{kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
|
||||
<div className="text-xs font-light">
|
||||
checked{' '}
|
||||
{Math.round(
|
||||
(Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000,
|
||||
)}{' '}
|
||||
sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,18 +1,25 @@
|
||||
import config from '../../config.yaml'
|
||||
|
||||
const classes = {
|
||||
gray: 'bg-gray-200 text-gray-800 dark:bg-gray-800 dark:text-gray-200',
|
||||
green: 'bg-green-200 text-green-800 dark:bg-green-800 dark:text-green-200',
|
||||
yellow:
|
||||
'bg-yellow-200 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-200',
|
||||
}
|
||||
|
||||
export default function MonitorStatusLabel({ kvMonitor }) {
|
||||
let labelColor = 'grey'
|
||||
let labelText = 'No data'
|
||||
let color = 'gray'
|
||||
let text = 'No data'
|
||||
|
||||
if (typeof kvMonitor !== 'undefined') {
|
||||
if (kvMonitor.operational) {
|
||||
labelColor = 'green'
|
||||
labelText = config.settings.monitorLabelOperational
|
||||
color = 'green'
|
||||
text = config.settings.monitorLabelOperational
|
||||
} else {
|
||||
labelColor = 'orange'
|
||||
labelText = config.settings.monitorLabelNotOperational
|
||||
color = 'yellow'
|
||||
text = config.settings.monitorLabelNotOperational
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={`ui ${labelColor} horizontal label`}>{labelText}</div>
|
||||
return <div className={`pill leading-5 ${classes[color]}`}>{text}</div>
|
||||
}
|
||||
|
58
src/components/themeSwitcher.js
Normal file
58
src/components/themeSwitcher.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const moonIcon = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="h-5 mx-auto"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const sunIcon = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="h-5 mx-auto"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const [darkmode, setDark] = useState(localStorage.getItem('theme') === 'dark')
|
||||
|
||||
useEffect(() => {
|
||||
setTheme(darkmode ? 'dark' : 'light')
|
||||
}, [darkmode])
|
||||
|
||||
const changeTheme = () => {
|
||||
setDark(!darkmode)
|
||||
}
|
||||
|
||||
const buttonColor = darkmode ? 'bg-gray-700' : 'bg-gray-200'
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${buttonColor} rounded-full h-7 w-7 mr-4`}
|
||||
onClick={changeTheme}
|
||||
>
|
||||
{darkmode ? sunIcon : moonIcon}
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,10 +1,6 @@
|
||||
import config from '../../config.yaml'
|
||||
|
||||
import {
|
||||
setKV,
|
||||
getKVWithMetadata,
|
||||
notifySlack,
|
||||
} from './helpers'
|
||||
import { setKV, getKVWithMetadata, notifySlack } from './helpers'
|
||||
|
||||
function getDate() {
|
||||
return new Date().toISOString().split('T')[0]
|
||||
@ -12,7 +8,10 @@ function getDate() {
|
||||
|
||||
export async function processCronTrigger(event) {
|
||||
// Get monitors state from KV
|
||||
let {value: monitorsState, metadata: monitorsStateMetadata} = await getKVWithMetadata('monitors_data', 'json')
|
||||
let {
|
||||
value: monitorsState,
|
||||
metadata: monitorsStateMetadata,
|
||||
} = await getKVWithMetadata('monitors_data', 'json')
|
||||
|
||||
// Create empty state objects if not exists in KV storage yet
|
||||
if (!monitorsState) {
|
||||
@ -28,7 +27,7 @@ export async function processCronTrigger(event) {
|
||||
for (const monitor of config.monitors) {
|
||||
// Create default monitor state if does not exist yet
|
||||
if (typeof monitorsState[monitor.id] === 'undefined') {
|
||||
monitorsState[monitor.id] = {failedDays: []}
|
||||
monitorsState[monitor.id] = { failedDays: [] }
|
||||
}
|
||||
|
||||
console.log(`Checking ${monitor.name} ...`)
|
||||
@ -43,15 +42,22 @@ export async function processCronTrigger(event) {
|
||||
}
|
||||
|
||||
const checkResponse = await fetch(monitor.url, init)
|
||||
const monitorOperational = checkResponse.status === (monitor.expectStatus || 200)
|
||||
const monitorOperational =
|
||||
checkResponse.status === (monitor.expectStatus || 200)
|
||||
|
||||
// Send Slack message on monitor change
|
||||
if (monitorsState[monitor.id].operational !== monitorOperational && typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') {
|
||||
event.waitUntil(notifySlack(monitor, monitorOperational))
|
||||
if (
|
||||
monitorsState[monitor.id].operational !== monitorOperational &&
|
||||
typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' &&
|
||||
SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret'
|
||||
) {
|
||||
event.waitUntil(notifySlack(monitor, monitorOperational))
|
||||
}
|
||||
|
||||
monitorsState[monitor.id].operational = checkResponse.status === (monitor.expectStatus || 200)
|
||||
monitorsState[monitor.id].firstCheck = monitorsState[monitor.id].firstCheck || getDate()
|
||||
monitorsState[monitor.id].operational =
|
||||
checkResponse.status === (monitor.expectStatus || 200)
|
||||
monitorsState[monitor.id].firstCheck =
|
||||
monitorsState[monitor.id].firstCheck || getDate()
|
||||
|
||||
// Set monitorsOperational and push current day to failedDays
|
||||
if (!monitorOperational) {
|
||||
@ -72,11 +78,15 @@ export async function processCronTrigger(event) {
|
||||
const loc = res.headers.get('cf-ray').split('-')[1]
|
||||
monitorsStateMetadata.lastUpdate = {
|
||||
loc,
|
||||
time: Date.now()
|
||||
time: Date.now(),
|
||||
}
|
||||
|
||||
// Save monitorsState and monitorsStateMetadata to KV storage
|
||||
await setKV('monitors_data', JSON.stringify(monitorsState), monitorsStateMetadata)
|
||||
await setKV(
|
||||
'monitors_data',
|
||||
JSON.stringify(monitorsState),
|
||||
monitorsStateMetadata,
|
||||
)
|
||||
|
||||
return new Response('OK')
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import config from '../../config.yaml'
|
||||
import {useEffect, useState} from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export async function getMonitors() {
|
||||
return await getKVWithMetadata('monitors_data', "json")
|
||||
return await getKVWithMetadata('monitors_data', 'json')
|
||||
}
|
||||
|
||||
export async function setKV(key, value, metadata, expirationTtl) {
|
||||
@ -23,11 +23,10 @@ export async function notifySlack(monitor, operational) {
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `Monitor *${monitor.name}* changed status to *${
|
||||
operational
|
||||
text: `Monitor *${monitor.name}* changed status to *${operational
|
||||
? config.settings.monitorLabelOperational
|
||||
: config.settings.monitorLabelNotOperational
|
||||
}*`,
|
||||
}*`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -35,11 +34,9 @@ export async function notifySlack(monitor, operational) {
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `${
|
||||
operational ? ':white_check_mark:' : ':x:'
|
||||
} \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${
|
||||
config.settings.url
|
||||
}|Status Page>`,
|
||||
text: `${operational ? ':white_check_mark:' : ':x:'} \`${monitor.method ? monitor.method : 'GET'
|
||||
} ${monitor.url}\` - :eyes: <${config.settings.url
|
||||
}|Status Page>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -59,24 +56,24 @@ export function useKeyPress(targetKey) {
|
||||
|
||||
function downHandler({ key }) {
|
||||
if (key === targetKey) {
|
||||
setKeyPressed(true);
|
||||
setKeyPressed(true)
|
||||
}
|
||||
}
|
||||
|
||||
const upHandler = ({ key }) => {
|
||||
if (key === targetKey) {
|
||||
setKeyPressed(false);
|
||||
setKeyPressed(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', downHandler);
|
||||
window.addEventListener('keyup', upHandler);
|
||||
window.addEventListener('keydown', downHandler)
|
||||
window.addEventListener('keyup', upHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler);
|
||||
window.removeEventListener('keyup', upHandler);
|
||||
};
|
||||
window.removeEventListener('keydown', downHandler)
|
||||
window.removeEventListener('keyup', upHandler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return keyPressed
|
||||
|
918
tailwind.config.js
Normal file
918
tailwind.config.js
Normal file
@ -0,0 +1,918 @@
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: ['./src/**/*.js', './pages/**/*.js', './public/tailwind.css'],
|
||||
},
|
||||
presets: [],
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
|
||||
black: colors.black,
|
||||
white: colors.white,
|
||||
gray: colors.coolGray,
|
||||
red: colors.red,
|
||||
yellow: colors.yellow,
|
||||
green: colors.green,
|
||||
blue: colors.lightBlue,
|
||||
indigo: colors.indigo,
|
||||
purple: colors.violet,
|
||||
pink: colors.pink,
|
||||
},
|
||||
spacing: {
|
||||
px: '1px',
|
||||
0: '0px',
|
||||
0.5: '0.125rem',
|
||||
1: '0.25rem',
|
||||
1.5: '0.375rem',
|
||||
2: '0.5rem',
|
||||
2.5: '0.625rem',
|
||||
3: '0.75rem',
|
||||
3.5: '0.875rem',
|
||||
4: '1rem',
|
||||
5: '1.25rem',
|
||||
6: '1.5rem',
|
||||
7: '1.75rem',
|
||||
8: '2rem',
|
||||
9: '2.25rem',
|
||||
10: '2.5rem',
|
||||
11: '2.75rem',
|
||||
12: '3rem',
|
||||
14: '3.5rem',
|
||||
16: '4rem',
|
||||
20: '5rem',
|
||||
24: '6rem',
|
||||
28: '7rem',
|
||||
32: '8rem',
|
||||
36: '9rem',
|
||||
40: '10rem',
|
||||
44: '11rem',
|
||||
48: '12rem',
|
||||
52: '13rem',
|
||||
56: '14rem',
|
||||
60: '15rem',
|
||||
64: '16rem',
|
||||
72: '18rem',
|
||||
80: '20rem',
|
||||
96: '24rem',
|
||||
},
|
||||
animation: {
|
||||
none: 'none',
|
||||
spin: 'spin 1s linear infinite',
|
||||
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
|
||||
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
bounce: 'bounce 1s infinite',
|
||||
},
|
||||
backgroundColor: (theme) => theme('colors'),
|
||||
backgroundImage: {
|
||||
none: 'none',
|
||||
'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
|
||||
'gradient-to-tr':
|
||||
'linear-gradient(to top right, var(--tw-gradient-stops))',
|
||||
'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
|
||||
'gradient-to-br':
|
||||
'linear-gradient(to bottom right, var(--tw-gradient-stops))',
|
||||
'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
|
||||
'gradient-to-bl':
|
||||
'linear-gradient(to bottom left, var(--tw-gradient-stops))',
|
||||
'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
|
||||
'gradient-to-tl':
|
||||
'linear-gradient(to top left, var(--tw-gradient-stops))',
|
||||
},
|
||||
backgroundOpacity: (theme) => theme('opacity'),
|
||||
backgroundPosition: {
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
'left-bottom': 'left bottom',
|
||||
'left-top': 'left top',
|
||||
right: 'right',
|
||||
'right-bottom': 'right bottom',
|
||||
'right-top': 'right top',
|
||||
top: 'top',
|
||||
},
|
||||
backgroundSize: {
|
||||
auto: 'auto',
|
||||
cover: 'cover',
|
||||
contain: 'contain',
|
||||
},
|
||||
borderColor: (theme) => ({
|
||||
...theme('colors'),
|
||||
DEFAULT: theme('colors.gray.200', 'currentColor'),
|
||||
}),
|
||||
borderOpacity: (theme) => theme('opacity'),
|
||||
borderRadius: {
|
||||
none: '0px',
|
||||
sm: '0.125rem',
|
||||
DEFAULT: '0.25rem',
|
||||
md: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
'2xl': '1rem',
|
||||
'3xl': '1.5rem',
|
||||
full: '9999px',
|
||||
},
|
||||
borderWidth: {
|
||||
DEFAULT: '1px',
|
||||
0: '0px',
|
||||
2: '2px',
|
||||
4: '4px',
|
||||
8: '8px',
|
||||
},
|
||||
boxShadow: {
|
||||
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
DEFAULT:
|
||||
'0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
||||
md:
|
||||
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
lg:
|
||||
'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
||||
xl:
|
||||
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
|
||||
none: 'none',
|
||||
},
|
||||
container: {},
|
||||
cursor: {
|
||||
auto: 'auto',
|
||||
default: 'default',
|
||||
pointer: 'pointer',
|
||||
wait: 'wait',
|
||||
text: 'text',
|
||||
move: 'move',
|
||||
'not-allowed': 'not-allowed',
|
||||
},
|
||||
divideColor: (theme) => theme('borderColor'),
|
||||
divideOpacity: (theme) => theme('borderOpacity'),
|
||||
divideWidth: (theme) => theme('borderWidth'),
|
||||
fill: { current: 'currentColor' },
|
||||
flex: {
|
||||
1: '1 1 0%',
|
||||
auto: '1 1 auto',
|
||||
initial: '0 1 auto',
|
||||
none: 'none',
|
||||
},
|
||||
flexGrow: {
|
||||
0: '0',
|
||||
DEFAULT: '1',
|
||||
},
|
||||
flexShrink: {
|
||||
0: '0',
|
||||
DEFAULT: '1',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'ui-sans-serif',
|
||||
'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"',
|
||||
],
|
||||
serif: [
|
||||
'ui-serif',
|
||||
'Georgia',
|
||||
'Cambria',
|
||||
'"Times New Roman"',
|
||||
'Times',
|
||||
'serif',
|
||||
],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace',
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
base: ['1rem', { lineHeight: '1.5rem' }],
|
||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
||||
'5xl': ['3rem', { lineHeight: '1' }],
|
||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||
'7xl': ['4.5rem', { lineHeight: '1' }],
|
||||
'8xl': ['6rem', { lineHeight: '1' }],
|
||||
'9xl': ['8rem', { lineHeight: '1' }],
|
||||
},
|
||||
fontWeight: {
|
||||
thin: '100',
|
||||
extralight: '200',
|
||||
light: '300',
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
semibold: '600',
|
||||
bold: '700',
|
||||
extrabold: '800',
|
||||
black: '900',
|
||||
},
|
||||
gap: (theme) => theme('spacing'),
|
||||
gradientColorStops: (theme) => theme('colors'),
|
||||
gridAutoColumns: {
|
||||
auto: 'auto',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
fr: 'minmax(0, 1fr)',
|
||||
},
|
||||
gridAutoRows: {
|
||||
auto: 'auto',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
fr: 'minmax(0, 1fr)',
|
||||
},
|
||||
gridColumn: {
|
||||
auto: 'auto',
|
||||
'span-1': 'span 1 / span 1',
|
||||
'span-2': 'span 2 / span 2',
|
||||
'span-3': 'span 3 / span 3',
|
||||
'span-4': 'span 4 / span 4',
|
||||
'span-5': 'span 5 / span 5',
|
||||
'span-6': 'span 6 / span 6',
|
||||
'span-7': 'span 7 / span 7',
|
||||
'span-8': 'span 8 / span 8',
|
||||
'span-9': 'span 9 / span 9',
|
||||
'span-10': 'span 10 / span 10',
|
||||
'span-11': 'span 11 / span 11',
|
||||
'span-12': 'span 12 / span 12',
|
||||
'span-full': '1 / -1',
|
||||
},
|
||||
gridColumnEnd: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
11: '11',
|
||||
12: '12',
|
||||
13: '13',
|
||||
},
|
||||
gridColumnStart: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
11: '11',
|
||||
12: '12',
|
||||
13: '13',
|
||||
},
|
||||
gridRow: {
|
||||
auto: 'auto',
|
||||
'span-1': 'span 1 / span 1',
|
||||
'span-2': 'span 2 / span 2',
|
||||
'span-3': 'span 3 / span 3',
|
||||
'span-4': 'span 4 / span 4',
|
||||
'span-5': 'span 5 / span 5',
|
||||
'span-6': 'span 6 / span 6',
|
||||
'span-full': '1 / -1',
|
||||
},
|
||||
gridRowStart: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
},
|
||||
gridRowEnd: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
},
|
||||
transformOrigin: {
|
||||
center: 'center',
|
||||
top: 'top',
|
||||
'top-right': 'top right',
|
||||
right: 'right',
|
||||
'bottom-right': 'bottom right',
|
||||
bottom: 'bottom',
|
||||
'bottom-left': 'bottom left',
|
||||
left: 'left',
|
||||
'top-left': 'top left',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
none: 'none',
|
||||
1: 'repeat(1, minmax(0, 1fr))',
|
||||
2: 'repeat(2, minmax(0, 1fr))',
|
||||
3: 'repeat(3, minmax(0, 1fr))',
|
||||
4: 'repeat(4, minmax(0, 1fr))',
|
||||
5: 'repeat(5, minmax(0, 1fr))',
|
||||
6: 'repeat(6, minmax(0, 1fr))',
|
||||
7: 'repeat(7, minmax(0, 1fr))',
|
||||
8: 'repeat(8, minmax(0, 1fr))',
|
||||
9: 'repeat(9, minmax(0, 1fr))',
|
||||
10: 'repeat(10, minmax(0, 1fr))',
|
||||
11: 'repeat(11, minmax(0, 1fr))',
|
||||
12: 'repeat(12, minmax(0, 1fr))',
|
||||
},
|
||||
gridTemplateRows: {
|
||||
none: 'none',
|
||||
1: 'repeat(1, minmax(0, 1fr))',
|
||||
2: 'repeat(2, minmax(0, 1fr))',
|
||||
3: 'repeat(3, minmax(0, 1fr))',
|
||||
4: 'repeat(4, minmax(0, 1fr))',
|
||||
5: 'repeat(5, minmax(0, 1fr))',
|
||||
6: 'repeat(6, minmax(0, 1fr))',
|
||||
},
|
||||
height: (theme) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
'1/5': '20%',
|
||||
'2/5': '40%',
|
||||
'3/5': '60%',
|
||||
'4/5': '80%',
|
||||
'1/6': '16.666667%',
|
||||
'2/6': '33.333333%',
|
||||
'3/6': '50%',
|
||||
'4/6': '66.666667%',
|
||||
'5/6': '83.333333%',
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
inset: (theme, { negative }) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
full: '100%',
|
||||
'-1/2': '-50%',
|
||||
'-1/3': '-33.333333%',
|
||||
'-2/3': '-66.666667%',
|
||||
'-1/4': '-25%',
|
||||
'-2/4': '-50%',
|
||||
'-3/4': '-75%',
|
||||
'-full': '-100%',
|
||||
}),
|
||||
keyframes: {
|
||||
spin: {
|
||||
to: {
|
||||
transform: 'rotate(360deg)',
|
||||
},
|
||||
},
|
||||
ping: {
|
||||
'75%, 100%': {
|
||||
transform: 'scale(2)',
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
pulse: {
|
||||
'50%': {
|
||||
opacity: '.5',
|
||||
},
|
||||
},
|
||||
bounce: {
|
||||
'0%, 100%': {
|
||||
transform: 'translateY(-25%)',
|
||||
animationTimingFunction: 'cubic-bezier(0.8,0,1,1)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'none',
|
||||
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
letterSpacing: {
|
||||
tighter: '-0.05em',
|
||||
tight: '-0.025em',
|
||||
normal: '0em',
|
||||
wide: '0.025em',
|
||||
wider: '0.05em',
|
||||
widest: '0.1em',
|
||||
},
|
||||
lineHeight: {
|
||||
none: '1',
|
||||
tight: '1.25',
|
||||
snug: '1.375',
|
||||
normal: '1.5',
|
||||
relaxed: '1.625',
|
||||
loose: '2',
|
||||
3: '.75rem',
|
||||
4: '1rem',
|
||||
5: '1.25rem',
|
||||
6: '1.5rem',
|
||||
7: '1.75rem',
|
||||
8: '2rem',
|
||||
9: '2.25rem',
|
||||
10: '2.5rem',
|
||||
},
|
||||
listStyleType: {
|
||||
none: 'none',
|
||||
disc: 'disc',
|
||||
decimal: 'decimal',
|
||||
},
|
||||
margin: (theme, { negative }) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
}),
|
||||
maxHeight: (theme) => ({
|
||||
...theme('spacing'),
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
maxWidth: (theme, { breakpoints }) => ({
|
||||
none: 'none',
|
||||
0: '0rem',
|
||||
xs: '20rem',
|
||||
sm: '24rem',
|
||||
md: '28rem',
|
||||
lg: '32rem',
|
||||
xl: '36rem',
|
||||
'2xl': '42rem',
|
||||
'3xl': '48rem',
|
||||
'4xl': '56rem',
|
||||
'5xl': '64rem',
|
||||
'6xl': '72rem',
|
||||
'7xl': '80rem',
|
||||
full: '100%',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
prose: '65ch',
|
||||
...breakpoints(theme('screens')),
|
||||
}),
|
||||
minHeight: {
|
||||
0: '0px',
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
},
|
||||
minWidth: {
|
||||
0: '0px',
|
||||
full: '100%',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
},
|
||||
objectPosition: {
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
'left-bottom': 'left bottom',
|
||||
'left-top': 'left top',
|
||||
right: 'right',
|
||||
'right-bottom': 'right bottom',
|
||||
'right-top': 'right top',
|
||||
top: 'top',
|
||||
},
|
||||
opacity: {
|
||||
0: '0',
|
||||
5: '0.05',
|
||||
10: '0.1',
|
||||
20: '0.2',
|
||||
25: '0.25',
|
||||
30: '0.3',
|
||||
40: '0.4',
|
||||
50: '0.5',
|
||||
60: '0.6',
|
||||
70: '0.7',
|
||||
75: '0.75',
|
||||
80: '0.8',
|
||||
90: '0.9',
|
||||
95: '0.95',
|
||||
100: '1',
|
||||
},
|
||||
order: {
|
||||
first: '-9999',
|
||||
last: '9999',
|
||||
none: '0',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
11: '11',
|
||||
12: '12',
|
||||
},
|
||||
outline: {
|
||||
none: ['2px solid transparent', '2px'],
|
||||
white: ['2px dotted white', '2px'],
|
||||
black: ['2px dotted black', '2px'],
|
||||
},
|
||||
padding: (theme) => theme('spacing'),
|
||||
placeholderColor: (theme) => theme('colors'),
|
||||
placeholderOpacity: (theme) => theme('opacity'),
|
||||
ringColor: (theme) => ({
|
||||
DEFAULT: theme('colors.blue.500', '#3b82f6'),
|
||||
...theme('colors'),
|
||||
}),
|
||||
ringOffsetColor: (theme) => theme('colors'),
|
||||
ringOffsetWidth: {
|
||||
0: '0px',
|
||||
1: '1px',
|
||||
2: '2px',
|
||||
4: '4px',
|
||||
8: '8px',
|
||||
},
|
||||
ringOpacity: (theme) => ({
|
||||
DEFAULT: '0.5',
|
||||
...theme('opacity'),
|
||||
}),
|
||||
ringWidth: {
|
||||
DEFAULT: '3px',
|
||||
0: '0px',
|
||||
1: '1px',
|
||||
2: '2px',
|
||||
4: '4px',
|
||||
8: '8px',
|
||||
},
|
||||
rotate: {
|
||||
'-180': '-180deg',
|
||||
'-90': '-90deg',
|
||||
'-45': '-45deg',
|
||||
'-12': '-12deg',
|
||||
'-6': '-6deg',
|
||||
'-3': '-3deg',
|
||||
'-2': '-2deg',
|
||||
'-1': '-1deg',
|
||||
0: '0deg',
|
||||
1: '1deg',
|
||||
2: '2deg',
|
||||
3: '3deg',
|
||||
6: '6deg',
|
||||
12: '12deg',
|
||||
45: '45deg',
|
||||
90: '90deg',
|
||||
180: '180deg',
|
||||
},
|
||||
scale: {
|
||||
0: '0',
|
||||
50: '.5',
|
||||
75: '.75',
|
||||
90: '.9',
|
||||
95: '.95',
|
||||
100: '1',
|
||||
105: '1.05',
|
||||
110: '1.1',
|
||||
125: '1.25',
|
||||
150: '1.5',
|
||||
},
|
||||
skew: {
|
||||
'-12': '-12deg',
|
||||
'-6': '-6deg',
|
||||
'-3': '-3deg',
|
||||
'-2': '-2deg',
|
||||
'-1': '-1deg',
|
||||
0: '0deg',
|
||||
1: '1deg',
|
||||
2: '2deg',
|
||||
3: '3deg',
|
||||
6: '6deg',
|
||||
12: '12deg',
|
||||
},
|
||||
space: (theme, { negative }) => ({
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
}),
|
||||
stroke: {
|
||||
current: 'currentColor',
|
||||
},
|
||||
strokeWidth: {
|
||||
0: '0',
|
||||
1: '1',
|
||||
2: '2',
|
||||
},
|
||||
textColor: (theme) => theme('colors'),
|
||||
textOpacity: (theme) => theme('opacity'),
|
||||
transitionDuration: {
|
||||
DEFAULT: '150ms',
|
||||
75: '75ms',
|
||||
100: '100ms',
|
||||
150: '150ms',
|
||||
200: '200ms',
|
||||
300: '300ms',
|
||||
500: '500ms',
|
||||
700: '700ms',
|
||||
1000: '1000ms',
|
||||
},
|
||||
transitionDelay: {
|
||||
75: '75ms',
|
||||
100: '100ms',
|
||||
150: '150ms',
|
||||
200: '200ms',
|
||||
300: '300ms',
|
||||
500: '500ms',
|
||||
700: '700ms',
|
||||
1000: '1000ms',
|
||||
},
|
||||
transitionProperty: {
|
||||
none: 'none',
|
||||
all: 'all',
|
||||
DEFAULT:
|
||||
'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
|
||||
colors: 'background-color, border-color, color, fill, stroke',
|
||||
opacity: 'opacity',
|
||||
shadow: 'box-shadow',
|
||||
transform: 'transform',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
linear: 'linear',
|
||||
in: 'cubic-bezier(0.4, 0, 1, 1)',
|
||||
out: 'cubic-bezier(0, 0, 0.2, 1)',
|
||||
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
},
|
||||
translate: (theme, { negative }) => ({
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
full: '100%',
|
||||
'-1/2': '-50%',
|
||||
'-1/3': '-33.333333%',
|
||||
'-2/3': '-66.666667%',
|
||||
'-1/4': '-25%',
|
||||
'-2/4': '-50%',
|
||||
'-3/4': '-75%',
|
||||
'-full': '-100%',
|
||||
}),
|
||||
width: (theme) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
'1/5': '20%',
|
||||
'2/5': '40%',
|
||||
'3/5': '60%',
|
||||
'4/5': '80%',
|
||||
'1/6': '16.666667%',
|
||||
'2/6': '33.333333%',
|
||||
'3/6': '50%',
|
||||
'4/6': '66.666667%',
|
||||
'5/6': '83.333333%',
|
||||
'1/12': '8.333333%',
|
||||
'2/12': '16.666667%',
|
||||
'3/12': '25%',
|
||||
'4/12': '33.333333%',
|
||||
'5/12': '41.666667%',
|
||||
'6/12': '50%',
|
||||
'7/12': '58.333333%',
|
||||
'8/12': '66.666667%',
|
||||
'9/12': '75%',
|
||||
'10/12': '83.333333%',
|
||||
'11/12': '91.666667%',
|
||||
full: '100%',
|
||||
screen: '100vw',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
}),
|
||||
zIndex: {
|
||||
auto: 'auto',
|
||||
0: '0',
|
||||
10: '10',
|
||||
20: '20',
|
||||
30: '30',
|
||||
40: '40',
|
||||
50: '50',
|
||||
},
|
||||
},
|
||||
variantOrder: [
|
||||
'first',
|
||||
'last',
|
||||
'odd',
|
||||
'even',
|
||||
'visited',
|
||||
'checked',
|
||||
'group-hover',
|
||||
'group-focus',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
'focus-visible',
|
||||
'active',
|
||||
'disabled',
|
||||
],
|
||||
variants: {
|
||||
accessibility: ['responsive', 'focus-within', 'focus'],
|
||||
alignContent: ['responsive'],
|
||||
alignItems: ['responsive'],
|
||||
alignSelf: ['responsive'],
|
||||
animation: ['responsive'],
|
||||
appearance: ['responsive'],
|
||||
backgroundAttachment: ['responsive'],
|
||||
backgroundClip: ['responsive'],
|
||||
backgroundColor: [
|
||||
'responsive',
|
||||
'dark',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
backgroundImage: ['responsive'],
|
||||
backgroundOpacity: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
backgroundPosition: ['responsive'],
|
||||
backgroundRepeat: ['responsive'],
|
||||
backgroundSize: ['responsive'],
|
||||
borderCollapse: ['responsive'],
|
||||
borderColor: [
|
||||
'responsive',
|
||||
'dark',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
borderOpacity: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
borderRadius: ['responsive'],
|
||||
borderStyle: ['responsive'],
|
||||
borderWidth: ['responsive'],
|
||||
boxShadow: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
|
||||
boxSizing: ['responsive'],
|
||||
clear: ['responsive'],
|
||||
container: ['responsive'],
|
||||
cursor: ['responsive'],
|
||||
display: ['responsive'],
|
||||
divideColor: ['responsive', 'dark'],
|
||||
divideOpacity: ['responsive'],
|
||||
divideStyle: ['responsive'],
|
||||
divideWidth: ['responsive'],
|
||||
fill: ['responsive'],
|
||||
flex: ['responsive'],
|
||||
flexDirection: ['responsive'],
|
||||
flexGrow: ['responsive'],
|
||||
flexShrink: ['responsive'],
|
||||
flexWrap: ['responsive'],
|
||||
float: ['responsive'],
|
||||
fontFamily: ['responsive'],
|
||||
fontSize: ['responsive'],
|
||||
fontSmoothing: ['responsive'],
|
||||
fontStyle: ['responsive'],
|
||||
fontVariantNumeric: ['responsive'],
|
||||
fontWeight: ['responsive'],
|
||||
gap: ['responsive'],
|
||||
gradientColorStops: ['responsive', 'dark', 'hover', 'focus'],
|
||||
gridAutoColumns: ['responsive'],
|
||||
gridAutoFlow: ['responsive'],
|
||||
gridAutoRows: ['responsive'],
|
||||
gridColumn: ['responsive'],
|
||||
gridColumnEnd: ['responsive'],
|
||||
gridColumnStart: ['responsive'],
|
||||
gridRow: ['responsive'],
|
||||
gridRowEnd: ['responsive'],
|
||||
gridRowStart: ['responsive'],
|
||||
gridTemplateColumns: ['responsive'],
|
||||
gridTemplateRows: ['responsive'],
|
||||
height: ['responsive'],
|
||||
inset: ['responsive'],
|
||||
justifyContent: ['responsive'],
|
||||
justifyItems: ['responsive'],
|
||||
justifySelf: ['responsive'],
|
||||
letterSpacing: ['responsive'],
|
||||
lineHeight: ['responsive'],
|
||||
listStylePosition: ['responsive'],
|
||||
listStyleType: ['responsive'],
|
||||
margin: ['responsive'],
|
||||
maxHeight: ['responsive'],
|
||||
maxWidth: ['responsive'],
|
||||
minHeight: ['responsive'],
|
||||
minWidth: ['responsive'],
|
||||
objectFit: ['responsive'],
|
||||
objectPosition: ['responsive'],
|
||||
opacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
|
||||
order: ['responsive'],
|
||||
outline: ['responsive', 'focus-within', 'focus'],
|
||||
overflow: ['responsive'],
|
||||
overscrollBehavior: ['responsive'],
|
||||
padding: ['responsive'],
|
||||
placeContent: ['responsive'],
|
||||
placeItems: ['responsive'],
|
||||
placeSelf: ['responsive'],
|
||||
placeholderColor: ['responsive', 'dark', 'focus'],
|
||||
placeholderOpacity: ['responsive', 'focus'],
|
||||
pointerEvents: ['responsive'],
|
||||
position: ['responsive'],
|
||||
resize: ['responsive'],
|
||||
ringColor: ['responsive', 'dark', 'focus-within', 'focus'],
|
||||
ringOffsetColor: ['responsive', 'dark', 'focus-within', 'focus'],
|
||||
ringOffsetWidth: ['responsive', 'focus-within', 'focus'],
|
||||
ringOpacity: ['responsive', 'focus-within', 'focus'],
|
||||
ringWidth: ['responsive', 'focus-within', 'focus'],
|
||||
rotate: ['responsive', 'hover', 'focus'],
|
||||
scale: ['responsive', 'hover', 'focus'],
|
||||
skew: ['responsive', 'hover', 'focus'],
|
||||
space: ['responsive'],
|
||||
stroke: ['responsive'],
|
||||
strokeWidth: ['responsive'],
|
||||
tableLayout: ['responsive'],
|
||||
textAlign: ['responsive'],
|
||||
textColor: [
|
||||
'responsive',
|
||||
'dark',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
textDecoration: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
textOpacity: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
textOverflow: ['responsive'],
|
||||
textTransform: ['responsive'],
|
||||
transform: ['responsive'],
|
||||
transformOrigin: ['responsive'],
|
||||
transitionDelay: ['responsive'],
|
||||
transitionDuration: ['responsive'],
|
||||
transitionProperty: ['responsive'],
|
||||
transitionTimingFunction: ['responsive'],
|
||||
translate: ['responsive', 'hover', 'focus'],
|
||||
userSelect: ['responsive'],
|
||||
verticalAlign: ['responsive'],
|
||||
visibility: ['responsive'],
|
||||
whitespace: ['responsive'],
|
||||
width: ['responsive'],
|
||||
wordBreak: ['responsive'],
|
||||
zIndex: ['responsive', 'focus-within', 'focus'],
|
||||
},
|
||||
plugins: [],
|
||||
}
|
Loading…
Reference in New Issue
Block a user