mirror of
https://github.com/tormachris/cf-workers-status-page.git
synced 2024-11-23 22:45:43 +01:00
commit
a89a3737e5
Binary file not shown.
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 450 KiB |
7
.github/workflows/deploy.yml
vendored
7
.github/workflows/deploy.yml
vendored
@ -28,11 +28,18 @@ jobs:
|
|||||||
echo "[env.production]" >> wrangler.toml
|
echo "[env.production]" >> wrangler.toml
|
||||||
echo "kv_namespaces = [{binding=\"KV_STATUS_PAGE\", id=\"${KV_NAMESPACE_ID}\"}]" >> wrangler.toml
|
echo "kv_namespaces = [{binding=\"KV_STATUS_PAGE\", id=\"${KV_NAMESPACE_ID}\"}]" >> wrangler.toml
|
||||||
[ -z "$SECRET_SLACK_WEBHOOK_URL" ] && echo "Secret SECRET_SLACK_WEBHOOK_URL not set, creating dummy one..." && SECRET_SLACK_WEBHOOK_URL="default-gh-action-secret" || true
|
[ -z "$SECRET_SLACK_WEBHOOK_URL" ] && echo "Secret SECRET_SLACK_WEBHOOK_URL not set, creating dummy one..." && SECRET_SLACK_WEBHOOK_URL="default-gh-action-secret" || true
|
||||||
|
[ -z "$SECRET_TELEGRAM_API_TOKEN" ] && echo "Secret SECRET_TELEGRAM_API_TOKEN not set, creating dummy one..." && SECRET_TELEGRAM_API_TOKEN="default-gh-action-secret" || true
|
||||||
|
[ -z "$SECRET_TELEGRAM_CHAT_ID" ] && echo "Secret SECRET_TELEGRAM_CHAT_ID not set, creating dummy one..." && SECRET_TELEGRAM_CHAT_ID="default-gh-action-secret" || true
|
||||||
postCommands: |
|
postCommands: |
|
||||||
yarn kv-gc
|
yarn kv-gc
|
||||||
secrets: |
|
secrets: |
|
||||||
SECRET_SLACK_WEBHOOK_URL
|
SECRET_SLACK_WEBHOOK_URL
|
||||||
|
SECRET_TELEGRAM_API_TOKEN
|
||||||
|
SECRET_TELEGRAM_CHAT_ID
|
||||||
environment: production
|
environment: production
|
||||||
env:
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
SECRET_SLACK_WEBHOOK_URL: ${{secrets.SECRET_SLACK_WEBHOOK_URL}}
|
SECRET_SLACK_WEBHOOK_URL: ${{secrets.SECRET_SLACK_WEBHOOK_URL}}
|
||||||
|
SECRET_TELEGRAM_API_TOKEN: ${{secrets.SECRET_TELEGRAM_API_TOKEN}}
|
||||||
|
SECRET_TELEGRAM_CHAT_ID: ${{secrets.SECRET_TELEGRAM_CHAT_ID}}
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -133,3 +133,4 @@ worker/
|
|||||||
.direnv/
|
.direnv/
|
||||||
out/
|
out/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
public/style.css
|
||||||
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Ignore generated files
|
||||||
|
out
|
||||||
|
public
|
@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
|
|||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to creating a positive environment
|
||||||
include:
|
include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
- Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
- Being respectful of differing viewpoints and experiences
|
||||||
* Gracefully accepting constructive criticism
|
- Gracefully accepting constructive criticism
|
||||||
* Focusing on what is best for the community
|
- Focusing on what is best for the community
|
||||||
* Showing empathy towards other community members
|
- Showing empathy towards other community members
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
advances
|
advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
- Publishing others' private information, such as a physical or electronic
|
||||||
address, without explicit permission
|
address, without explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Our Responsibilities
|
||||||
|
|
||||||
|
50
README.md
50
README.md
@ -10,15 +10,15 @@ Monitor your websites, showcase status including daily history, and get Slack no
|
|||||||
|
|
||||||
You'll need a [Cloudflare Workers account](https://dash.cloudflare.com/sign-up/workers) with
|
You'll need a [Cloudflare Workers account](https://dash.cloudflare.com/sign-up/workers) with
|
||||||
|
|
||||||
* A workers domain set up
|
- A workers domain set up
|
||||||
* The Workers Bundled subscription \($5/mo\)
|
- The Workers Bundled subscription \($5/mo\)
|
||||||
* [It works with Workers Free!](https://blog.cloudflare.com/workers-kv-free-tier/) Check [more info](#workers-kv-free-tier) on how to run on Workers Free.
|
- [It works with Workers Free!](https://blog.cloudflare.com/workers-kv-free-tier/) Check [more info](#workers-kv-free-tier) on how to run on Workers Free.
|
||||||
* Some websites/APIs to watch 🙂
|
- Some websites/APIs to watch 🙂
|
||||||
|
|
||||||
Also, prepare the following secrets
|
Also, prepare the following secrets
|
||||||
|
|
||||||
* Cloudflare API token with `Edit Cloudflare Workers` permissions
|
- Cloudflare API token with `Edit Cloudflare Workers` permissions
|
||||||
* Slack incoming webhook \(optional\)
|
- Slack incoming webhook \(optional\)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
|
|||||||
- Name: SECRET_SLACK_WEBHOOK_URL (optional)
|
- Name: SECRET_SLACK_WEBHOOK_URL (optional)
|
||||||
- Value: your-slack-webhook-url
|
- Value: your-slack-webhook-url
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Navigate to the **Actions** settings in your repository and enable them
|
3. Navigate to the **Actions** settings in your repository and enable them
|
||||||
4. Edit [config.yaml](./config.yaml) to adjust configuration and list all of your websites/APIs you want to monitor
|
4. Edit [config.yaml](./config.yaml) to adjust configuration and list all of your websites/APIs you want to monitor
|
||||||
|
|
||||||
@ -48,6 +49,7 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
|
|||||||
url: 'https://status-page.eidam.dev' # used for Slack messages
|
url: 'https://status-page.eidam.dev' # used for Slack messages
|
||||||
logo: logo-192x192.png # image in ./public/ folder
|
logo: logo-192x192.png # image in ./public/ folder
|
||||||
daysInHistogram: 90 # number of days you want to display in histogram
|
daysInHistogram: 90 # number of days you want to display in histogram
|
||||||
|
collectResponseTimes: false # experimental feature, enable only for <5 monitors or on paid plans
|
||||||
|
|
||||||
# configurable texts across the status page
|
# configurable texts across the status page
|
||||||
allmonitorsOperational: 'All Systems Operational'
|
allmonitorsOperational: 'All Systems Operational'
|
||||||
@ -73,32 +75,46 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
|
|||||||
5. Push to `main` branch to trigger the deployment
|
5. Push to `main` branch to trigger the deployment
|
||||||
6. 🎉
|
6. 🎉
|
||||||
7. _\(optional\)_ Go to [Cloudflare Workers settings](https://dash.cloudflare.com/?to=/workers) and assign custom domain/route
|
7. _\(optional\)_ Go to [Cloudflare Workers settings](https://dash.cloudflare.com/?to=/workers) and assign custom domain/route
|
||||||
* e.g. `status-page.eidam.dev/*` _\(make sure you include `/*` as the Worker also serve static files\)_
|
- e.g. `status-page.eidam.dev/*` _\(make sure you include `/*` as the Worker also serve static files\)_
|
||||||
8. _\(optional\)_ Edit [wrangler.toml](./wrangler.toml) to adjust Worker settings or CRON Trigger schedule, especially if you are on [Workers Free plan](#workers-kv-free-tier)
|
8. _\(optional\)_ Edit [wrangler.toml](./wrangler.toml) to adjust Worker settings or CRON Trigger schedule, especially if you are on [Workers Free plan](#workers-kv-free-tier)
|
||||||
|
|
||||||
|
### Telegram notifications
|
||||||
|
|
||||||
|
To enable telegram notifications, you'll need to take a few additional steps.
|
||||||
|
|
||||||
|
1. [Create a new Bot](https://core.telegram.org/bots#creating-a-new-bot)
|
||||||
|
2. Set the api token you received when creating the bot as content of the `SECRET_TELEGRAM_API_TOKEN` secret in your github repository.
|
||||||
|
3. Send a message to the bot from the telegram account which should receive the alerts (Something more than `/start`)
|
||||||
|
4. Get the chat id with `curl https://api.telegram.org/bot<YOUR TELEGRAM API TOKEN>/getUpdates | jq '.result[0] .message .chat .id'`
|
||||||
|
5. Set the retrieved chat id in the `SECRET_TELEGRAM_CHAT_ID` secret variable
|
||||||
|
6. Redeploy the status page using the github action
|
||||||
|
|
||||||
### Deploy on your own
|
### Deploy on your own
|
||||||
|
|
||||||
You can clone the repository yourself and use Wrangler CLI to develop/deploy, extra list of things you need to take care of:
|
You can clone the repository yourself and use Wrangler CLI to develop/deploy, extra list of things you need to take care of:
|
||||||
|
|
||||||
* create KV namespace and add the `KV_STATUS_PAGE` binding to [wrangler.toml](./wrangler.toml)
|
- create KV namespace and add the `KV_STATUS_PAGE` binding to [wrangler.toml](./wrangler.toml)
|
||||||
* create Worker secrets _\(optional\)_
|
- create Worker secrets _\(optional\)_
|
||||||
* `SECRET_SLACK_WEBHOOK_URL`
|
- `SECRET_SLACK_WEBHOOK_URL`
|
||||||
|
|
||||||
## Workers KV free tier
|
## Workers KV free tier
|
||||||
The Workers Free plan includes limited KV usage, but the quota is sufficient for 2-minute checks only
|
|
||||||
* Change the CRON trigger to 2 minutes interval (`crons = ["*/2 * * * *"]`) in [wrangler.toml](./wrangler.toml)
|
The Workers Free plan includes limited KV usage, but the quota is sufficient for 2-minute checks only
|
||||||
|
|
||||||
|
- Change the CRON trigger to 2 minutes interval (`crons = ["*/2 * * * *"]`) in [wrangler.toml](./wrangler.toml)
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
* **Max 25 monitors to watch in case you are using Slack notifications**, due to the limit of subrequests Cloudflare Worker can make \(50\).
|
- **Max 25 monitors to watch in case you are using Slack notifications**, due to the limit of subrequests Cloudflare Worker can make \(50\).
|
||||||
|
|
||||||
The plan is to support up to 49 by sending only one Slack notification per scheduled run.
|
The plan is to support up to 49 by sending only one Slack notification per scheduled run.
|
||||||
|
|
||||||
* **KV replication lag** - You might get Slack notification instantly, however it may take couple of more seconds to see the change on your status page as [Cron Triggers are usually running on underutilized quiet hours machines](https://blog.cloudflare.com/introducing-cron-triggers-for-cloudflare-workers/#how-are-you-able-to-offer-this-feature-at-no-additional-cost).
|
- **KV replication lag** - You might get Slack notification instantly, however it may take couple of more seconds to see the change on your status page as [Cron Triggers are usually running on underutilized quiet hours machines](https://blog.cloudflare.com/introducing-cron-triggers-for-cloudflare-workers/#how-are-you-able-to-offer-this-feature-at-no-additional-cost).
|
||||||
|
|
||||||
* **Initial delay (no data)** - It takes couple of minutes to schedule and run CRON Triggers for the first time
|
- **Initial delay (no data)** - It takes couple of minutes to schedule and run CRON Triggers for the first time
|
||||||
|
|
||||||
## Future plans
|
## Future plans
|
||||||
|
|
||||||
Stay tuned for more features coming in, like leveraging the fact that CRON instances are scheduled around the world during the day
|
Stay tuned for more features coming in, like leveraging the fact that CRON instances are scheduled around the world during the day
|
||||||
so we can monitor the response times. However, we will most probably wait for the [Durable Objects](https://blog.cloudflare.com/introducing-workers-durable-objects/) to be in open beta
|
so we can monitor the response times. However, we will most probably wait for the [Durable Objects](https://blog.cloudflare.com/introducing-workers-durable-objects/) to be in open beta
|
||||||
as they are better fit to reliably store such info.
|
as they are better fit to reliably store such info.
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# Table of contents
|
# Table of contents
|
||||||
|
|
||||||
* [Cloudflare Worker - Status Page](README.md)
|
- [Cloudflare Worker - Status Page](README.md)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ settings:
|
|||||||
url: 'https://status-page.eidam.dev' # used for Slack messages
|
url: 'https://status-page.eidam.dev' # used for Slack messages
|
||||||
logo: logo-192x192.png # image in ./public/ folder
|
logo: logo-192x192.png # image in ./public/ folder
|
||||||
daysInHistogram: 90 # number of days you want to display in histogram
|
daysInHistogram: 90 # number of days you want to display in histogram
|
||||||
|
collectResponseTimes: false # experimental feature, enable only for <5 monitors or on paid plans
|
||||||
|
|
||||||
allmonitorsOperational: 'All Systems Operational'
|
allmonitorsOperational: 'All Systems Operational'
|
||||||
notAllmonitorsOperational: 'Not All Systems Operational'
|
notAllmonitorsOperational: 'Not All Systems Operational'
|
||||||
@ -11,7 +12,7 @@ settings:
|
|||||||
monitorLabelNoData: 'No data'
|
monitorLabelNoData: 'No data'
|
||||||
dayInHistogramNoData: 'No data'
|
dayInHistogramNoData: 'No data'
|
||||||
dayInHistogramOperational: 'All good'
|
dayInHistogramOperational: 'All good'
|
||||||
dayInHistogramNotOperational: 'Some checks failed'
|
dayInHistogramNotOperational: ' incident(s)' # xx incident(s) recorded
|
||||||
|
|
||||||
monitors:
|
monitors:
|
||||||
- id: workers-cloudflare-com # unique identifier
|
- id: workers-cloudflare-com # unique identifier
|
||||||
|
4
index.js
4
index.js
@ -10,7 +10,7 @@ import { processCronTrigger } from './src/functions/cronTrigger'
|
|||||||
*/
|
*/
|
||||||
const DEBUG = false
|
const DEBUG = false
|
||||||
|
|
||||||
addEventListener('fetch', event => {
|
addEventListener('fetch', (event) => {
|
||||||
try {
|
try {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
handleEvent(event, require.context('./pages/', true, /\.js$/), DEBUG),
|
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))
|
event.waitUntil(processCronTrigger(event))
|
||||||
})
|
})
|
||||||
|
24
package.json
24
package.json
@ -7,21 +7,29 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "flareact dev",
|
"dev": "flareact dev",
|
||||||
"build": "flareact build",
|
"build": "yarn css && flareact build",
|
||||||
"deploy": "flareact publish",
|
"deploy": "yarn build && flareact publish",
|
||||||
"kv-gc": "node ./src/cli/gcMonitors.js",
|
"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",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flareact": "^0.9.0",
|
"flareact": "0.9.0",
|
||||||
"laco": "^1.2.1",
|
"laco": "^1.2.1",
|
||||||
"laco-react": "^1.1.0",
|
"laco-react": "^1.1.0",
|
||||||
"react": "^16.13.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^16.13.1"
|
"react-dom": "^17.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.0.2",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"prettier": "^1.18.2",
|
"postcss": "^8.1.8",
|
||||||
"yaml-loader": "^0.6.0"
|
"postcss-cli": "^8.3.0",
|
||||||
|
"prettier": "^2.2.0",
|
||||||
|
"tailwindcss": "^2.0.1",
|
||||||
|
"yaml-loader": "^0.6.0",
|
||||||
|
"patch-package": "^6.2.2",
|
||||||
|
"postinstall-postinstall": "^2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { processCronTrigger } from '../../src/functions/cronTrigger'
|
import { processCronTrigger } from '../../src/functions/cronTrigger'
|
||||||
|
|
||||||
export default async event => {
|
export default async (event) => {
|
||||||
// used only for local debugging
|
// used only for local debugging
|
||||||
//return processCronTrigger(event)
|
//return processCronTrigger(event)
|
||||||
}
|
}
|
||||||
|
145
pages/index.js
145
pages/index.js
@ -1,115 +1,94 @@
|
|||||||
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 { Store } from 'laco'
|
||||||
import { useStore } from 'laco-react'
|
import { useStore } from 'laco-react'
|
||||||
|
import Head from 'flareact/head'
|
||||||
|
|
||||||
const MonitorStore = new Store(
|
import { getKVMonitors, useKeyPress } from '../src/functions/helpers'
|
||||||
{
|
import config from '../config.yaml'
|
||||||
monitors: config.monitors,
|
import MonitorCard from '../src/components/monitorCard'
|
||||||
visible: config.monitors,
|
import MonitorFilter from '../src/components/monitorFilter'
|
||||||
activeFilter: false
|
import MonitorStatusHeader from '../src/components/monitorStatusHeader'
|
||||||
}
|
import ThemeSwitcher from '../src/components/themeSwitcher'
|
||||||
)
|
|
||||||
|
|
||||||
const filterByTerm = (term) => MonitorStore.set(
|
const MonitorStore = new Store({
|
||||||
state => ({ visible: state.monitors.filter((monitor) => monitor.name.toLowerCase().includes(term)) })
|
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() {
|
export async function getEdgeProps() {
|
||||||
// get KV data
|
// get KV data
|
||||||
const {value: kvMonitors, metadata: kvMonitorsMetadata } = await getMonitors()
|
const kvMonitors = await getKVMonitors()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
config,
|
config,
|
||||||
kvMonitors: kvMonitors || {},
|
kvMonitors: kvMonitors ? kvMonitors.monitors : {},
|
||||||
kvMonitorsMetadata: kvMonitorsMetadata || {}
|
kvMonitorsLastUpdate: kvMonitors ? kvMonitors.lastUpdate : {},
|
||||||
},
|
},
|
||||||
// Revalidate these props once every x seconds
|
// Revalidate these props once every x seconds
|
||||||
revalidate: 5,
|
revalidate: 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Index({
|
export default function Index({ config, kvMonitors, kvMonitorsLastUpdate }) {
|
||||||
config,
|
|
||||||
kvMonitors,
|
|
||||||
kvMonitorsMetadata,
|
|
||||||
}) {
|
|
||||||
const state = useStore(MonitorStore)
|
const state = useStore(MonitorStore)
|
||||||
const slash = useKeyPress('/')
|
const slash = useKeyPress('/')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="min-h-screen">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{config.settings.title}</title>
|
<title>{config.settings.title}</title>
|
||||||
<link
|
<link rel="stylesheet" href="./style.css" />
|
||||||
rel="stylesheet"
|
<script>
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/semantic.min.css"
|
{`
|
||||||
crossOrigin="anonymous"
|
function setTheme(theme) {
|
||||||
/>
|
document.documentElement.classList.remove("dark", "light")
|
||||||
<link rel="stylesheet" href="./main.css" />
|
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>
|
</Head>
|
||||||
<div className="ui basic segment container">
|
<div className="container mx-auto px-4">
|
||||||
<div className="horizontal flex between">
|
<div className="flex flex-row justify-between items-center p-4">
|
||||||
<h1 className="ui huge marginless title header">
|
<div className="flex flex-row items-center">
|
||||||
<img
|
<img className="h-8 w-auto" src={config.settings.logo} />
|
||||||
className="ui middle aligned tiny image"
|
<h1 className="ml-4 text-3xl">{config.settings.title}</h1>
|
||||||
src={config.settings.logo}
|
</div>
|
||||||
/>
|
<div className="flex flex-row items-center">
|
||||||
{config.settings.title}
|
{typeof window !== 'undefined' && <ThemeSwitcher />}
|
||||||
</h1>
|
<MonitorFilter active={slash} callback={filterByTerm} />
|
||||||
<MonitorFilter
|
</div>
|
||||||
active={slash}
|
|
||||||
callback={filterByTerm}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<MonitorStatusHeader
|
<MonitorStatusHeader kvMonitorsLastUpdate={kvMonitorsLastUpdate} />
|
||||||
kvMonitorsMetadata={kvMonitorsMetadata}
|
|
||||||
/>
|
|
||||||
{state.visible.map((monitor, key) => {
|
{state.visible.map((monitor, key) => {
|
||||||
return (
|
return (
|
||||||
<div key={key} className="ui segment">
|
<MonitorCard
|
||||||
<div
|
key={key}
|
||||||
className="ui horizontal flex between"
|
monitor={monitor}
|
||||||
style={{ marginBottom: '8px' }}
|
data={kvMonitors[monitor.id]}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className="horizontal flex between grey-text">
|
<div className="flex flex-row justify-between mt-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
<a href="https://workers.cloudflare.com/" target="_blank">
|
<a href="https://workers.cloudflare.com/" target="_blank">
|
||||||
|
12
patches/flareact+0.9.0.patch
Normal file
12
patches/flareact+0.9.0.patch
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/node_modules/flareact/src/components/_document.js b/node_modules/flareact/src/components/_document.js
|
||||||
|
index 3494b60..206b493 100644
|
||||||
|
--- a/node_modules/flareact/src/components/_document.js
|
||||||
|
+++ b/node_modules/flareact/src/components/_document.js
|
||||||
|
@@ -61,6 +61,7 @@ export function FlareactHead({ helmet, page, buildManifest }) {
|
||||||
|
{helmet.title.toComponent()}
|
||||||
|
{helmet.meta.toComponent()}
|
||||||
|
{helmet.link.toComponent()}
|
||||||
|
+ {helmet.script.toComponent()}
|
||||||
|
|
||||||
|
{[...links].map((link) => (
|
||||||
|
<link href={`/_flareact/static/${link}`} rel="stylesheet" />
|
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-lg 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_'
|
const kvPrefix = 's_'
|
||||||
|
|
||||||
if (!accountId || !namespaceId || !apiToken) {
|
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)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,24 +53,26 @@ function loadConfig() {
|
|||||||
return JSON.parse(config)
|
return JSON.parse(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
getKvMonitors(kvPrefix).then(async kvMonitors => {
|
getKvMonitors(kvPrefix)
|
||||||
const config = loadConfig()
|
.then(async (kvMonitors) => {
|
||||||
const monitors = config.monitors.map(key => {
|
const config = loadConfig()
|
||||||
return key.id
|
const monitors = config.monitors.map((key) => {
|
||||||
})
|
return key.id
|
||||||
const kvState = kvMonitors.map(key => {
|
})
|
||||||
return key.name
|
const kvState = kvMonitors.map((key) => {
|
||||||
})
|
return key.name
|
||||||
const keysForRemoval = kvState.filter(
|
})
|
||||||
x => !monitors.includes(x.replace(kvPrefix, '')),
|
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(
|
|
||||||
', ',
|
|
||||||
)}`,
|
|
||||||
)
|
)
|
||||||
await deleteKvBulk(keysForRemoval)
|
|
||||||
}
|
if (keysForRemoval.length > 0) {
|
||||||
}).catch(e => console.log(e))
|
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'
|
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 }) {
|
export default function MonitorFilter({ active, callback }) {
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
|
|
||||||
@ -21,21 +36,19 @@ export default function MonitorFilter({ active, callback }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ui search">
|
<div className="col-span-6 sm:col-span-3 relative">
|
||||||
<div className="ui icon input">
|
<input
|
||||||
<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"
|
||||||
className="prompt"
|
type="text"
|
||||||
type="text"
|
value={input}
|
||||||
value={input}
|
onInput={handleInput}
|
||||||
onInput={handleInput}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyDown={handleKeyDown}
|
placeholder="Tap '/' to search"
|
||||||
placeholder="Tap '/' to search"
|
tabIndex={0}
|
||||||
tabIndex={0}
|
ref={(e) => e && active && e.focus()}
|
||||||
ref={
|
/>
|
||||||
(e) => e && active && e.focus()
|
<div className="absolute inset-y-1 right-1 flex z-1 items-center">
|
||||||
}
|
{searchIcon}
|
||||||
/>
|
|
||||||
<i className="search icon"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,56 +1,67 @@
|
|||||||
import config from '../../config.yaml'
|
import config from '../../config.yaml'
|
||||||
|
|
||||||
export default function MonitorHistogram({
|
export default function MonitorHistogram({ monitorId, kvMonitor }) {
|
||||||
monitorId,
|
|
||||||
kvMonitor,
|
|
||||||
}) {
|
|
||||||
// create date and set date - daysInHistogram for the first day of the histogram
|
// create date and set date - daysInHistogram for the first day of the histogram
|
||||||
let date = new Date()
|
let date = new Date()
|
||||||
date.setDate(date.getDate() - config.settings.daysInHistogram)
|
date.setDate(date.getDate() - config.settings.daysInHistogram)
|
||||||
|
|
||||||
|
let content = null
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
return (
|
content = Array.from(Array(config.settings.daysInHistogram).keys()).map(
|
||||||
<div
|
(key) => {
|
||||||
key={`${monitorId}-histogram`}
|
date.setDate(date.getDate() + 1)
|
||||||
className="horizontal flex histogram"
|
const dayInHistogram = date.toISOString().split('T')[0]
|
||||||
>
|
|
||||||
{Array.from(Array(config.settings.daysInHistogram).keys()).map(key => {
|
|
||||||
date.setDate(date.getDate() + 1)
|
|
||||||
const dayInHistogram = date.toISOString().split('T')[0]
|
|
||||||
|
|
||||||
let bg = ''
|
let bg = ''
|
||||||
let dayInHistogramLabel = config.settings.dayInHistogramNoData
|
let dayInHistogramLabel = config.settings.dayInHistogramNoData
|
||||||
|
|
||||||
// filter all dates before first check, check the rest
|
// filter all dates before first check, then check the rest
|
||||||
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
|
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
|
||||||
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
|
if (
|
||||||
bg = 'green'
|
kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||||
dayInHistogramLabel = config.settings.dayInHistogramOperational
|
kvMonitor.checks[dayInHistogram].fails > 0
|
||||||
} else {
|
) {
|
||||||
bg = 'orange'
|
bg = 'yellow'
|
||||||
dayInHistogramLabel = config.settings.dayInHistogramNotOperational
|
dayInHistogramLabel = `${kvMonitor.checks[dayInHistogram].fails} ${config.settings.dayInHistogramNotOperational}`
|
||||||
}
|
} else {
|
||||||
|
bg = 'green'
|
||||||
|
dayInHistogramLabel = config.settings.dayInHistogramOperational
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="hitbox">
|
<div key={key} className="hitbox tooltip">
|
||||||
<div
|
<div className={`${bg} bar`} />
|
||||||
className={`${bg} bar`}
|
<div className="content text-center py-1 px-2 mt-2 left-1/2 -ml-20 w-40 text-xs">
|
||||||
data-tooltip={`${dayInHistogram} - ${dayInHistogramLabel}`}
|
{dayInHistogram}
|
||||||
/>
|
<br />
|
||||||
|
<span className="font-semibold text-sm">
|
||||||
|
{dayInHistogramLabel}
|
||||||
|
</span>
|
||||||
|
{kvMonitor &&
|
||||||
|
kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||||
|
Object.keys(kvMonitor.checks[dayInHistogram].res).map((key) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
{key}: {kvMonitor.checks[dayInHistogram].res[key].a}ms
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
})}
|
)
|
||||||
</div>
|
},
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${monitorId}-histogram`}
|
|
||||||
className="horizontal flex histogram"
|
|
||||||
>
|
|
||||||
<div className="grey-text">Loading histogram ...</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${monitorId}-histogram`}
|
||||||
|
className="flex flex-row items-center histogram"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
import config from '../../config.yaml'
|
import config from '../../config.yaml'
|
||||||
|
|
||||||
export default function MonitorStatusHeader({kvMonitorsMetadata}) {
|
const classes = {
|
||||||
let backgroundColor = 'green'
|
green:
|
||||||
let headerText = config.settings.allmonitorsOperational
|
'bg-green-200 text-green-700 dark:bg-green-700 dark:text-green-200 border-green-300 dark:border-green-600',
|
||||||
let textColor = 'black'
|
yellow:
|
||||||
|
'bg-yellow-200 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-200 border-yellow-300 dark:border-yellow-600',
|
||||||
|
}
|
||||||
|
|
||||||
if (!kvMonitorsMetadata.monitorsOperational) {
|
export default function MonitorStatusHeader({ kvMonitorsLastUpdate }) {
|
||||||
backgroundColor = 'yellow'
|
let color = 'green'
|
||||||
headerText = config.settings.notAllmonitorsOperational
|
let text = config.settings.allmonitorsOperational
|
||||||
|
|
||||||
|
if (!kvMonitorsLastUpdate.allOperational) {
|
||||||
|
color = 'yellow'
|
||||||
|
text = config.settings.notAllmonitorsOperational
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`ui inverted segment ${backgroundColor}`}>
|
<div className={`card mb-4 font-semibold ${classes[color]}`}>
|
||||||
<div className="horizontal flex between">
|
<div className="flex flex-row justify-between items-center">
|
||||||
<div className={`ui marginless header ${textColor}-text`}>
|
<div>{text}</div>
|
||||||
{headerText}
|
{kvMonitorsLastUpdate.time && typeof window !== 'undefined' && (
|
||||||
</div>
|
<div className="text-xs font-light">
|
||||||
{
|
checked{' '}
|
||||||
kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
|
{Math.round((Date.now() - kvMonitorsLastUpdate.time) / 1000)} sec
|
||||||
<div className={`${textColor}-text`}>
|
ago (from {kvMonitorsLastUpdate.loc})
|
||||||
checked {Math.round((Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000)} sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
import config from '../../config.yaml'
|
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 }) {
|
export default function MonitorStatusLabel({ kvMonitor }) {
|
||||||
let labelColor = 'grey'
|
let color = 'gray'
|
||||||
let labelText = 'No data'
|
let text = 'No data'
|
||||||
|
|
||||||
if (typeof kvMonitor !== 'undefined') {
|
if (typeof kvMonitor !== 'undefined') {
|
||||||
if (kvMonitor.operational) {
|
if (kvMonitor.lastCheck.operational) {
|
||||||
labelColor = 'green'
|
color = 'green'
|
||||||
labelText = config.settings.monitorLabelOperational
|
text = config.settings.monitorLabelOperational
|
||||||
} else {
|
} else {
|
||||||
labelColor = 'orange'
|
color = 'yellow'
|
||||||
labelText = config.settings.monitorLabelNotOperational
|
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,9 +1,11 @@
|
|||||||
import config from '../../config.yaml'
|
import config from '../../config.yaml'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setKV,
|
|
||||||
getKVWithMetadata,
|
|
||||||
notifySlack,
|
notifySlack,
|
||||||
|
notifyTelegram,
|
||||||
|
getCheckLocation,
|
||||||
|
getKVMonitors,
|
||||||
|
setKVMonitors,
|
||||||
} from './helpers'
|
} from './helpers'
|
||||||
|
|
||||||
function getDate() {
|
function getDate() {
|
||||||
@ -11,24 +13,29 @@ function getDate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function processCronTrigger(event) {
|
export async function processCronTrigger(event) {
|
||||||
|
// Get Worker PoP and save it to monitorsStateMetadata
|
||||||
|
const checkLocation = await getCheckLocation()
|
||||||
|
const checkDay = getDate()
|
||||||
|
|
||||||
// Get monitors state from KV
|
// Get monitors state from KV
|
||||||
let {value: monitorsState, metadata: monitorsStateMetadata} = await getKVWithMetadata('monitors_data', 'json')
|
let monitorsState = await getKVMonitors()
|
||||||
|
|
||||||
// Create empty state objects if not exists in KV storage yet
|
// Create empty state objects if not exists in KV storage yet
|
||||||
if (!monitorsState) {
|
if (!monitorsState) {
|
||||||
monitorsState = {}
|
monitorsState = { lastUpdate: {}, monitors: {} }
|
||||||
}
|
|
||||||
if (!monitorsStateMetadata) {
|
|
||||||
monitorsStateMetadata = {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset default all monitors state to true
|
// Reset default all monitors state to true
|
||||||
monitorsStateMetadata.monitorsOperational = true
|
monitorsState.lastUpdate.allOperational = true
|
||||||
|
|
||||||
for (const monitor of config.monitors) {
|
for (const monitor of config.monitors) {
|
||||||
// Create default monitor state if does not exist yet
|
// Create default monitor state if does not exist yet
|
||||||
if (typeof monitorsState[monitor.id] === 'undefined') {
|
if (typeof monitorsState.monitors[monitor.id] === 'undefined') {
|
||||||
monitorsState[monitor.id] = {failedDays: []}
|
monitorsState.monitors[monitor.id] = {
|
||||||
|
firstCheck: checkDay,
|
||||||
|
lastCheck: {},
|
||||||
|
checks: {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Checking ${monitor.name} ...`)
|
console.log(`Checking ${monitor.name} ...`)
|
||||||
@ -42,41 +49,101 @@ export async function processCronTrigger(event) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform a check and measure time
|
||||||
|
const requestStartTime = Date.now()
|
||||||
const checkResponse = await fetch(monitor.url, init)
|
const checkResponse = await fetch(monitor.url, init)
|
||||||
const monitorOperational = checkResponse.status === (monitor.expectStatus || 200)
|
const requestTime = Math.round(Date.now() - requestStartTime)
|
||||||
|
|
||||||
// Send Slack message on monitor change
|
// Determine whether operational and status changed
|
||||||
if (monitorsState[monitor.id].operational !== monitorOperational && typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') {
|
const monitorOperational =
|
||||||
event.waitUntil(notifySlack(monitor, monitorOperational))
|
checkResponse.status === (monitor.expectStatus || 200)
|
||||||
|
const monitorStatusChanged =
|
||||||
|
monitorsState.monitors[monitor.id].lastCheck.operational !==
|
||||||
|
monitorOperational
|
||||||
|
|
||||||
|
// Save monitor's last check response status
|
||||||
|
monitorsState.monitors[monitor.id].lastCheck = {
|
||||||
|
status: checkResponse.status,
|
||||||
|
statusText: checkResponse.statusText,
|
||||||
|
operational: monitorOperational,
|
||||||
}
|
}
|
||||||
|
|
||||||
monitorsState[monitor.id].operational = checkResponse.status === (monitor.expectStatus || 200)
|
// Send Slack message on monitor change
|
||||||
monitorsState[monitor.id].firstCheck = monitorsState[monitor.id].firstCheck || getDate()
|
if (
|
||||||
|
monitorStatusChanged &&
|
||||||
|
typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' &&
|
||||||
|
SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret'
|
||||||
|
) {
|
||||||
|
event.waitUntil(notifySlack(monitor, monitorOperational))
|
||||||
|
}
|
||||||
|
|
||||||
// Set monitorsOperational and push current day to failedDays
|
// Send Telegram message on monitor change
|
||||||
if (!monitorOperational) {
|
if (
|
||||||
monitorsStateMetadata.monitorsOperational = false
|
monitorStatusChanged &&
|
||||||
|
typeof SECRET_TELEGRAM_API_TOKEN !== 'undefined' &&
|
||||||
|
SECRET_TELEGRAM_API_TOKEN !== 'default-gh-action-secret' &&
|
||||||
|
typeof SECRET_TELEGRAM_CHAT_ID !== 'undefined' &&
|
||||||
|
SECRET_TELEGRAM_CHAT_ID !== 'default-gh-action-secret'
|
||||||
|
) {
|
||||||
|
event.waitUntil(notifyTelegram(monitor, monitorOperational))
|
||||||
|
}
|
||||||
|
|
||||||
const failedDay = getDate()
|
// make sure checkDay exists in checks in cases when needed
|
||||||
if (!monitorsState[monitor.id].failedDays.includes(failedDay)) {
|
if (
|
||||||
console.log('Saving new failed daily status ...')
|
(config.settings.collectResponseTimes || !monitorOperational) &&
|
||||||
monitorsState[monitor.id].failedDays.push(failedDay)
|
!monitorsState.monitors[monitor.id].checks.hasOwnProperty(checkDay)
|
||||||
|
) {
|
||||||
|
monitorsState.monitors[monitor.id].checks[checkDay] = {
|
||||||
|
fails: 0,
|
||||||
|
res: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.settings.collectResponseTimes && monitorOperational) {
|
||||||
|
// make sure location exists in current checkDay
|
||||||
|
if (
|
||||||
|
!monitorsState.monitors[monitor.id].checks[checkDay].res.hasOwnProperty(
|
||||||
|
checkLocation,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||||
|
checkLocation
|
||||||
|
] = {
|
||||||
|
n: 0,
|
||||||
|
ms: 0,
|
||||||
|
a: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment number of checks and sum of ms
|
||||||
|
const no = ++monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||||
|
checkLocation
|
||||||
|
].n
|
||||||
|
const ms = (monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||||
|
checkLocation
|
||||||
|
].ms += requestTime)
|
||||||
|
|
||||||
|
// save new average ms
|
||||||
|
monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||||
|
checkLocation
|
||||||
|
].a = Math.round(ms / no)
|
||||||
|
} else if (!monitorOperational) {
|
||||||
|
// Save allOperational to false
|
||||||
|
monitorsState.lastUpdate.allOperational = false
|
||||||
|
|
||||||
|
// Increment failed checks, only on status change (maybe call it .incidents instead?)
|
||||||
|
if (monitorStatusChanged) {
|
||||||
|
monitorsState.monitors[monitor.id].checks[checkDay].fails++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Worker PoP and save it to monitorsStateMetadata
|
// Save last update information
|
||||||
const res = await fetch('https://cloudflare-dns.com/dns-query', {
|
monitorsState.lastUpdate.time = Date.now()
|
||||||
method: 'OPTIONS',
|
monitorsState.lastUpdate.loc = checkLocation
|
||||||
})
|
|
||||||
const loc = res.headers.get('cf-ray').split('-')[1]
|
|
||||||
monitorsStateMetadata.lastUpdate = {
|
|
||||||
loc,
|
|
||||||
time: Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save monitorsState and monitorsStateMetadata to KV storage
|
// Save monitorsState to KV storage
|
||||||
await setKV('monitors_data', JSON.stringify(monitorsState), monitorsStateMetadata)
|
await setKVMonitors(monitorsState)
|
||||||
|
|
||||||
return new Response('OK')
|
return new Response('OK')
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import config from '../../config.yaml'
|
import config from '../../config.yaml'
|
||||||
import {useEffect, useState} from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export async function getMonitors() {
|
const kvDataKey = 'monitors_data_v1_1'
|
||||||
return await getKVWithMetadata('monitors_data', "json")
|
|
||||||
|
export async function getKVMonitors() {
|
||||||
|
// trying both to see performance difference
|
||||||
|
return KV_STATUS_PAGE.get(kvDataKey, 'json')
|
||||||
|
//return JSON.parse(await KV_STATUS_PAGE.get(kvDataKey, 'text'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setKVMonitors(data) {
|
||||||
|
return setKV(kvDataKey, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOperationalLabel = (operational) => {
|
||||||
|
return operational
|
||||||
|
? config.settings.monitorLabelOperational
|
||||||
|
: config.settings.monitorLabelNotOperational
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setKV(key, value, metadata, expirationTtl) {
|
export async function setKV(key, value, metadata, expirationTtl) {
|
||||||
return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl })
|
return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getKVWithMetadata(key, type = 'text') {
|
|
||||||
return KV_STATUS_PAGE.getWithMetadata(key, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function notifySlack(monitor, operational) {
|
export async function notifySlack(monitor, operational) {
|
||||||
const payload = {
|
const payload = {
|
||||||
attachments: [
|
attachments: [
|
||||||
@ -23,11 +33,9 @@ export async function notifySlack(monitor, operational) {
|
|||||||
type: 'section',
|
type: 'section',
|
||||||
text: {
|
text: {
|
||||||
type: 'mrkdwn',
|
type: 'mrkdwn',
|
||||||
text: `Monitor *${monitor.name}* changed status to *${
|
text: `Monitor *${
|
||||||
operational
|
monitor.name
|
||||||
? config.settings.monitorLabelOperational
|
}* changed status to *${getOperationalLabel(operational)}*`,
|
||||||
: config.settings.monitorLabelNotOperational
|
|
||||||
}*`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,9 +43,9 @@ export async function notifySlack(monitor, operational) {
|
|||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'mrkdwn',
|
type: 'mrkdwn',
|
||||||
text: `${
|
text: `${operational ? ':white_check_mark:' : ':x:'} \`${
|
||||||
operational ? ':white_check_mark:' : ':x:'
|
monitor.method ? monitor.method : 'GET'
|
||||||
} \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${
|
} ${monitor.url}\` - :eyes: <${
|
||||||
config.settings.url
|
config.settings.url
|
||||||
}|Status Page>`,
|
}|Status Page>`,
|
||||||
},
|
},
|
||||||
@ -54,30 +62,58 @@ export async function notifySlack(monitor, operational) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function notifyTelegram(monitor, operational) {
|
||||||
|
const text = `Monitor *${monitor.name.replace(
|
||||||
|
'-',
|
||||||
|
'\\-',
|
||||||
|
)}* changed status to *${getOperationalLabel(operational)}*
|
||||||
|
${operational ? '✅' : '❌'} \`${monitor.method ? monitor.method : 'GET'} ${
|
||||||
|
monitor.url
|
||||||
|
}\` \\- 👀 [Status Page](${config.settings.url})`
|
||||||
|
|
||||||
|
const payload = new FormData()
|
||||||
|
payload.append('chat_id', SECRET_TELEGRAM_CHAT_ID)
|
||||||
|
payload.append('parse_mode', 'MarkdownV2')
|
||||||
|
payload.append('text', text)
|
||||||
|
|
||||||
|
const telegramUrl = `https://api.telegram.org/bot${SECRET_TELEGRAM_API_TOKEN}/sendMessage`
|
||||||
|
return fetch(telegramUrl, {
|
||||||
|
body: payload,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useKeyPress(targetKey) {
|
export function useKeyPress(targetKey) {
|
||||||
const [keyPressed, setKeyPressed] = useState(false)
|
const [keyPressed, setKeyPressed] = useState(false)
|
||||||
|
|
||||||
function downHandler({ key }) {
|
function downHandler({ key }) {
|
||||||
if (key === targetKey) {
|
if (key === targetKey) {
|
||||||
setKeyPressed(true);
|
setKeyPressed(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const upHandler = ({ key }) => {
|
const upHandler = ({ key }) => {
|
||||||
if (key === targetKey) {
|
if (key === targetKey) {
|
||||||
setKeyPressed(false);
|
setKeyPressed(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('keydown', downHandler);
|
window.addEventListener('keydown', downHandler)
|
||||||
window.addEventListener('keyup', upHandler);
|
window.addEventListener('keyup', upHandler)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', downHandler);
|
window.removeEventListener('keydown', downHandler)
|
||||||
window.removeEventListener('keyup', upHandler);
|
window.removeEventListener('keyup', upHandler)
|
||||||
};
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return keyPressed
|
return keyPressed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCheckLocation() {
|
||||||
|
const res = await fetch('https://cloudflare-dns.com/dns-query', {
|
||||||
|
method: 'OPTIONS',
|
||||||
|
})
|
||||||
|
return res.headers.get('cf-ray').split('-')[1]
|
||||||
|
}
|
||||||
|
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