mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 05:41:59 +00:00
fix: improve the style for the web
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/client/styles.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
+7
-1
@@ -39,12 +39,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.102.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"hono": "^4.13.5",
|
||||
"lucide-react": "^1.34.0",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "19",
|
||||
"react-dom": "19",
|
||||
"recharts": "^3"
|
||||
"recharts": "^3",
|
||||
"shadcn": "^4.19.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"workerd",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { ArrowRight, Database, History, Pencil, Power, PowerOff, RefreshCw, Trash2, Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import type { Monitor, MonitorInput, MonitorMethod } from '../api/monitors';
|
||||
import { SiteIcon } from '../components/SiteIcon';
|
||||
import { useLogoutMutation } from '../queries/auth';
|
||||
@@ -112,12 +114,18 @@ export function DashboardPage() {
|
||||
<a className="nav-auth" href="/">
|
||||
View status page
|
||||
</a>
|
||||
<button className="nav-auth" type="button" onClick={() => navigate('/settings')}>
|
||||
<Button variant="unstyled" className="nav-auth" type="button" onClick={() => navigate('/settings')}>
|
||||
Settings
|
||||
</button>
|
||||
<button className="nav-auth" type="button" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="nav-auth"
|
||||
type="button"
|
||||
onClick={() => logoutMutation.mutate()}
|
||||
disabled={logoutMutation.isPending}
|
||||
>
|
||||
{logoutMutation.isPending ? 'Signing out…' : 'Sign out'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -129,14 +137,10 @@ export function DashboardPage() {
|
||||
<h1>Monitors</h1>
|
||||
<p>Track endpoint availability from Cloudflare's edge every five minutes.</p>
|
||||
</div>
|
||||
{formOpen ? (
|
||||
<button className="secondary-button" type="button" onClick={closeForm}>
|
||||
Close form
|
||||
</button>
|
||||
) : monitorsQuery.isSuccess && monitors.length > 0 ? (
|
||||
<button className="primary-button" type="button" onClick={openCreateForm}>
|
||||
{monitorsQuery.isSuccess && monitors.length > 0 ? (
|
||||
<Button variant="unstyled" className="primary-button" type="button" onClick={openCreateForm}>
|
||||
Add monitor <ArrowRight />
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -158,15 +162,26 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{formOpen && (
|
||||
<section className="monitor-form-panel" aria-labelledby="monitor-form-title">
|
||||
<div className="form-panel-heading">
|
||||
<div>
|
||||
<p className="overline">Configuration</p>
|
||||
<h2 id="monitor-form-title">{editing ? `Edit ${editing.name}` : 'Add a monitor'}</h2>
|
||||
</div>
|
||||
<p>Checks run on the configured schedule, with a minimum interval of five minutes.</p>
|
||||
</div>
|
||||
<Dialog
|
||||
open={formOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) closeForm();
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-h-[85vh] w-[calc(100%-2rem)] overflow-y-auto sm:max-w-3xl"
|
||||
onEscapeKeyDown={(event) => {
|
||||
if (formMutation.isPending) event.preventDefault();
|
||||
}}
|
||||
onInteractOutside={(event) => {
|
||||
if (formMutation.isPending) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<p className="overline">Configuration</p>
|
||||
<DialogTitle>{editing ? `Edit ${editing.name}` : 'Add a monitor'}</DialogTitle>
|
||||
<DialogDescription>Checks run on the configured schedule, with a minimum interval of five minutes.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form className="monitor-form" onSubmit={handleSubmit}>
|
||||
<label className="field field-name">
|
||||
<span>Name</span>
|
||||
@@ -243,12 +258,12 @@ export function DashboardPage() {
|
||||
<span>Enable incident alerts</span>
|
||||
</label>
|
||||
<div className="form-actions compact-actions">
|
||||
<button className="secondary-button" type="button" onClick={closeForm}>
|
||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={closeForm}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="primary-button" type="submit" disabled={formMutation.isPending}>
|
||||
</Button>
|
||||
<Button variant="unstyled" className="primary-button" type="submit" disabled={formMutation.isPending}>
|
||||
{formMutation.isPending ? 'Saving…' : editing ? 'Save changes' : 'Add monitor'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{formMutation.isError && (
|
||||
<p className="form-error" role="alert">
|
||||
@@ -256,8 +271,8 @@ export function DashboardPage() {
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<section className="services-panel" aria-labelledby="monitor-list-title">
|
||||
<div className="panel-heading">
|
||||
@@ -265,7 +280,8 @@ export function DashboardPage() {
|
||||
<h2 id="monitor-list-title">Configured sites</h2>
|
||||
<p>Latest result for each monitored endpoint.</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="icon-button"
|
||||
type="button"
|
||||
onClick={() => void monitorsQuery.refetch()}
|
||||
@@ -273,7 +289,7 @@ export function DashboardPage() {
|
||||
aria-label="Refresh monitors"
|
||||
>
|
||||
<RefreshCw className={monitorsQuery.isFetching ? 'is-spinning' : ''} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{monitorsQuery.isPending ? (
|
||||
@@ -290,9 +306,9 @@ export function DashboardPage() {
|
||||
<div className="panel-state error-state">
|
||||
<strong>Monitors could not be loaded</strong>
|
||||
<p>{errorMessage(monitorsQuery.error, 'Unknown request error')}</p>
|
||||
<button className="secondary-button" type="button" onClick={() => void monitorsQuery.refetch()}>
|
||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={() => void monitorsQuery.refetch()}>
|
||||
Try again
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : monitors.length === 0 ? (
|
||||
<div className="panel-state empty-state">
|
||||
@@ -302,9 +318,9 @@ export function DashboardPage() {
|
||||
<strong>No monitors yet</strong>
|
||||
<p>Add the first endpoint to start collecting availability checks.</p>
|
||||
{!formOpen && (
|
||||
<button className="primary-button" type="button" onClick={openCreateForm}>
|
||||
<Button variant="unstyled" className="primary-button" type="button" onClick={openCreateForm}>
|
||||
Add first site <ArrowRight />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -326,9 +342,14 @@ export function DashboardPage() {
|
||||
<SiteIcon key={monitor.url} monitorId={monitor.id} />
|
||||
</span>
|
||||
<div>
|
||||
<button className="monitor-name-link" type="button" onClick={() => navigate(`/monitors/${monitor.id}`)}>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="monitor-name-link"
|
||||
type="button"
|
||||
onClick={() => navigate(`/monitors/${monitor.id}`)}
|
||||
>
|
||||
{monitor.name}
|
||||
</button>
|
||||
</Button>
|
||||
<small title={monitor.url}>{monitor.url}</small>
|
||||
<span className="monitor-meta">
|
||||
{monitor.method} · expect {monitor.expectedStatus} · every {monitor.intervalSeconds / 60}m
|
||||
@@ -347,11 +368,17 @@ export function DashboardPage() {
|
||||
<small title={monitor.lastError ?? undefined}>{monitor.lastError ?? formatCheckedAt(monitor.lastCheckedAt)}</small>
|
||||
</div>
|
||||
<div className="row-actions" aria-label={`Actions for ${monitor.name}`}>
|
||||
<button className="row-action row-action-labeled" type="button" onClick={() => navigate(`/monitors/${monitor.id}`)}>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="row-action row-action-labeled"
|
||||
type="button"
|
||||
onClick={() => navigate(`/monitors/${monitor.id}`)}
|
||||
>
|
||||
<History aria-hidden="true" />
|
||||
<span>History</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="row-action row-action-labeled row-action-accent"
|
||||
type="button"
|
||||
onClick={() => checkMutation.mutate(monitor.id)}
|
||||
@@ -360,8 +387,9 @@ export function DashboardPage() {
|
||||
>
|
||||
<RefreshCw className={checking ? 'is-spinning' : ''} aria-hidden="true" />
|
||||
<span>{checking ? 'Checking' : 'Check now'}</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="row-action row-action-icon"
|
||||
type="button"
|
||||
onClick={() => openEditForm(monitor)}
|
||||
@@ -369,8 +397,9 @@ export function DashboardPage() {
|
||||
title="Edit monitor"
|
||||
>
|
||||
<Pencil aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="row-action row-action-icon"
|
||||
type="button"
|
||||
onClick={() => updateMutation.mutate({ id: monitor.id, input: { enabled: !monitor.enabled } })}
|
||||
@@ -379,8 +408,9 @@ export function DashboardPage() {
|
||||
title={`${monitor.enabled ? 'Disable' : 'Enable'} monitor`}
|
||||
>
|
||||
{monitor.enabled ? <PowerOff aria-hidden="true" /> : <Power aria-hidden="true" />}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="row-action row-action-icon danger-action"
|
||||
type="button"
|
||||
onClick={() => handleDelete(monitor)}
|
||||
@@ -390,7 +420,7 @@ export function DashboardPage() {
|
||||
aria-busy={deleting}
|
||||
>
|
||||
<Trash2 aria-hidden="true" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { navigate } from '../lib/router';
|
||||
import { useLoginMutation, useSessionQuery } from '../queries/auth';
|
||||
|
||||
@@ -53,9 +54,9 @@ export function LoginPage() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button className="auth-submit" type="submit" disabled={loginMutation.isPending}>
|
||||
<Button variant="unstyled" className="auth-submit" type="submit" disabled={loginMutation.isPending}>
|
||||
{loginMutation.isPending ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArrowLeft, BellOff, CheckCircle2, Clock3, ExternalLink, RefreshCw, Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LatencySparkline } from '../components/charts/LatencySparkline';
|
||||
import { UptimeBar } from '../components/charts/UptimeBar';
|
||||
import { navigate } from '../lib/router';
|
||||
@@ -54,9 +55,9 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
<div className="detail-error">
|
||||
<strong>Monitor not found</strong>
|
||||
<p>The requested monitor could not be loaded.</p>
|
||||
<button className="secondary-button" onClick={() => navigate('/dashboard')}>
|
||||
<Button variant="unstyled" className="secondary-button" onClick={() => navigate('/dashboard')}>
|
||||
Return to dashboard
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,24 +65,24 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
<div className="dashboard-shell">
|
||||
<header className="dashboard-header">
|
||||
<div className="dashboard-header-inner">
|
||||
<button className="brand brand-button" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<Button variant="unstyled" className="brand brand-button" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<Zap className="brand-mark" fill="currentColor" />
|
||||
<span>upwatch</span>
|
||||
</button>
|
||||
</Button>
|
||||
<div className="nav-actions">
|
||||
<button className="nav-auth" onClick={() => navigate('/settings')}>
|
||||
<Button variant="unstyled" className="nav-auth" onClick={() => navigate('/settings')}>
|
||||
Settings
|
||||
</button>
|
||||
<button className="nav-auth" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
|
||||
</Button>
|
||||
<Button variant="unstyled" className="nav-auth" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
|
||||
Sign out
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="dashboard-main detail-main">
|
||||
<button className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<Button variant="unstyled" className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<ArrowLeft /> All monitors
|
||||
</button>
|
||||
</Button>
|
||||
<section className="detail-hero">
|
||||
<div className="detail-title">
|
||||
<span className={`status-orb ${status.className}`} />
|
||||
@@ -104,9 +105,15 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
<BellOff /> Alerts muted
|
||||
</span>
|
||||
)}
|
||||
<button className="primary-button" type="button" onClick={() => checkMutation.mutate(id)} disabled={checkMutation.isPending}>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="primary-button"
|
||||
type="button"
|
||||
onClick={() => checkMutation.mutate(id)}
|
||||
disabled={checkMutation.isPending}
|
||||
>
|
||||
{checkMutation.isPending ? 'Checking…' : 'Check now'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { ArrowLeft, BellRing, Send, Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { NotificationSettings } from '../api/settings';
|
||||
import { navigate } from '../lib/router';
|
||||
import { useLogoutMutation } from '../queries/auth';
|
||||
@@ -45,17 +46,18 @@ function SettingsForm({ settings }: { settings: NotificationSettings }) {
|
||||
</span>
|
||||
</label>
|
||||
<div className="settings-actions">
|
||||
<button
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => testMutation.mutate()}
|
||||
disabled={!settings.webhookUrl || testMutation.isPending}
|
||||
>
|
||||
<Send /> {testMutation.isPending ? 'Sending…' : 'Send test'}
|
||||
</button>
|
||||
<button className="primary-button" type="submit" disabled={updateMutation.isPending}>
|
||||
</Button>
|
||||
<Button variant="unstyled" className="primary-button" type="submit" disabled={updateMutation.isPending}>
|
||||
{updateMutation.isPending ? 'Saving…' : 'Save settings'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{updateMutation.isSuccess && <p className="settings-success">Notification settings saved.</p>}
|
||||
{testMutation.isSuccess && <p className="settings-success">Test webhook delivered successfully.</p>}
|
||||
@@ -73,22 +75,22 @@ export function SettingsPage() {
|
||||
<div className="dashboard-shell">
|
||||
<header className="dashboard-header">
|
||||
<div className="dashboard-header-inner">
|
||||
<button className="brand brand-button" onClick={() => navigate('/dashboard')}>
|
||||
<Button variant="unstyled" className="brand brand-button" onClick={() => navigate('/dashboard')}>
|
||||
<Zap className="brand-mark" fill="currentColor" />
|
||||
<span>upwatch</span>
|
||||
</button>
|
||||
</Button>
|
||||
<div className="nav-actions">
|
||||
<span className="header-context">Settings</span>
|
||||
<button className="nav-auth" onClick={() => logoutMutation.mutate()}>
|
||||
<Button variant="unstyled" className="nav-auth" onClick={() => logoutMutation.mutate()}>
|
||||
Sign out
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="settings-main">
|
||||
<button className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<Button variant="unstyled" className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<ArrowLeft /> Dashboard
|
||||
</button>
|
||||
</Button>
|
||||
<section className="settings-heading">
|
||||
<p className="overline">Integrations</p>
|
||||
<h1>Notifications</h1>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Activity, CircleCheck, Database, RefreshCw, TriangleAlert, Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { PublicOverallStatus, PublicServiceStatus } from '../api/status';
|
||||
import { SiteIcon } from '../components/SiteIcon';
|
||||
import { StatusHistoryBar } from '../components/StatusHistoryBar';
|
||||
@@ -65,13 +66,14 @@ export function StatusPage() {
|
||||
<Zap className="brand-mark" fill="currentColor" />
|
||||
<span>upwatch</span>
|
||||
</a>
|
||||
<button
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="status-header-action"
|
||||
type="button"
|
||||
onClick={() => navigate(sessionQuery.data?.authenticated ? '/dashboard' : '/login')}
|
||||
>
|
||||
{sessionQuery.data?.authenticated ? 'Dashboard' : 'Sign in'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -102,9 +104,9 @@ export function StatusPage() {
|
||||
</span>
|
||||
<strong>Status could not be loaded</strong>
|
||||
<p>{errorMessage(statusQuery.error)}</p>
|
||||
<button className="secondary-button" type="button" onClick={() => void statusQuery.refetch()}>
|
||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={() => void statusQuery.refetch()}>
|
||||
Try again
|
||||
</button>
|
||||
</Button>
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
@@ -125,7 +127,8 @@ export function StatusPage() {
|
||||
<h2 id="public-services-title">Services</h2>
|
||||
<p>Availability is calculated from checks collected over the last 90 days.</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="icon-button"
|
||||
type="button"
|
||||
onClick={() => void statusQuery.refetch()}
|
||||
@@ -133,7 +136,7 @@ export function StatusPage() {
|
||||
aria-label="Refresh service status"
|
||||
>
|
||||
<RefreshCw className={statusQuery.isFetching ? 'is-spinning' : ''} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{status.services.length === 0 ? (
|
||||
|
||||
+118
-43
@@ -1,5 +1,9 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import 'shadcn/tailwind.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
font-family: 'DM Sans', 'Helvetica Neue', sans-serif;
|
||||
@@ -8,13 +12,45 @@
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
--primary: #3ecf8e;
|
||||
--shadcn-primary: oklch(0.205 0 0);
|
||||
--primary-deep: #24b47e;
|
||||
--ink: #171717;
|
||||
--muted: #707070;
|
||||
--shadcn-muted: oklch(0.97 0 0);
|
||||
--faint: #9a9a9a;
|
||||
--hairline: #dfdfdf;
|
||||
--soft: #fafafa;
|
||||
--night: #1c1c1c;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -253,35 +289,6 @@ button {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.monitor-form-panel {
|
||||
margin-top: 24px;
|
||||
padding: 28px;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 28px rgb(29 74 54 / 0.06);
|
||||
}
|
||||
.form-panel-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
.form-panel-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.4px;
|
||||
}
|
||||
.form-panel-heading > p {
|
||||
max-width: 480px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--muted);
|
||||
}
|
||||
.monitor-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -575,7 +582,7 @@ button {
|
||||
}
|
||||
.row-actions {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto repeat(3, 34px);
|
||||
grid-template-columns: auto auto repeat(3, 38px);
|
||||
justify-content: space-between;
|
||||
justify-self: stretch;
|
||||
width: 100%;
|
||||
@@ -587,11 +594,11 @@ button {
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 6px 10px;
|
||||
min-height: 38px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: #626262;
|
||||
@@ -606,12 +613,12 @@ button {
|
||||
}
|
||||
.row-action svg {
|
||||
flex: 0 0 auto;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
.row-action-icon {
|
||||
width: 34px;
|
||||
width: 38px;
|
||||
padding: 0;
|
||||
}
|
||||
.row-action:hover:not(:disabled) {
|
||||
@@ -1920,13 +1927,6 @@ button {
|
||||
.metric-card p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.monitor-form-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
.form-panel-heading {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.monitor-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -2138,3 +2138,78 @@ button {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--shadcn-muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--shadcn-primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--shadcn-primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--shadcn-muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Slot } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-oklch(0.922 0 0) border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-oklch(0.708 0 0) focus-visible:ring-3 focus-visible:ring-oklch(0.708 0 0)/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-oklch(0.577 0.245 27.325) aria-invalid:ring-3 aria-invalid:ring-oklch(0.577 0.245 27.325)/20 dark:aria-invalid:border-oklch(0.577 0.245 27.325)/50 dark:aria-invalid:ring-oklch(0.577 0.245 27.325)/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 dark:border-oklch(1 0 0 / 10%) dark:focus-visible:border-oklch(0.556 0 0) dark:focus-visible:ring-oklch(0.556 0 0)/50 dark:aria-invalid:border-oklch(0.704 0.191 22.216) dark:aria-invalid:ring-oklch(0.704 0.191 22.216)/20 dark:dark:aria-invalid:border-oklch(0.704 0.191 22.216)/50 dark:dark:aria-invalid:ring-oklch(0.704 0.191 22.216)/40 cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
unstyled: null,
|
||||
default:
|
||||
'bg-oklch(0.205 0 0) text-oklch(0.985 0 0) hover:bg-oklch(0.205 0 0)/80 dark:bg-oklch(0.922 0 0) dark:text-oklch(0.205 0 0) dark:hover:bg-oklch(0.922 0 0)/80',
|
||||
outline:
|
||||
'border-oklch(0.922 0 0) bg-oklch(1 0 0) hover:bg-oklch(0.97 0 0) hover:text-oklch(0.145 0 0) aria-expanded:bg-oklch(0.97 0 0) aria-expanded:text-oklch(0.145 0 0) dark:border-oklch(0.922 0 0) dark:bg-oklch(0.922 0 0)/30 dark:hover:bg-oklch(0.922 0 0)/50 dark:border-oklch(1 0 0 / 10%) dark:bg-oklch(0.145 0 0) dark:hover:bg-oklch(0.269 0 0) dark:hover:text-oklch(0.985 0 0) dark:aria-expanded:bg-oklch(0.269 0 0) dark:aria-expanded:text-oklch(0.985 0 0) dark:dark:border-oklch(1 0 0 / 15%) dark:dark:bg-oklch(1 0 0 / 15%)/30 dark:dark:hover:bg-oklch(1 0 0 / 15%)/50',
|
||||
secondary:
|
||||
'bg-oklch(0.97 0 0) text-oklch(0.205 0 0) hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-oklch(0.97 0 0) aria-expanded:text-oklch(0.205 0 0) dark:bg-oklch(0.269 0 0) dark:text-oklch(0.985 0 0) dark:aria-expanded:bg-oklch(0.269 0 0) dark:aria-expanded:text-oklch(0.985 0 0)',
|
||||
ghost:
|
||||
'hover:bg-oklch(0.97 0 0) hover:text-oklch(0.145 0 0) aria-expanded:bg-oklch(0.97 0 0) aria-expanded:text-oklch(0.145 0 0) dark:hover:bg-oklch(0.97 0 0)/50 dark:hover:bg-oklch(0.269 0 0) dark:hover:text-oklch(0.985 0 0) dark:aria-expanded:bg-oklch(0.269 0 0) dark:aria-expanded:text-oklch(0.985 0 0) dark:dark:hover:bg-oklch(0.269 0 0)/50',
|
||||
destructive:
|
||||
'bg-oklch(0.577 0.245 27.325)/10 text-oklch(0.577 0.245 27.325) hover:bg-oklch(0.577 0.245 27.325)/20 focus-visible:border-oklch(0.577 0.245 27.325)/40 focus-visible:ring-oklch(0.577 0.245 27.325)/20 dark:bg-oklch(0.577 0.245 27.325)/20 dark:hover:bg-oklch(0.577 0.245 27.325)/30 dark:focus-visible:ring-oklch(0.577 0.245 27.325)/40 dark:bg-oklch(0.704 0.191 22.216)/10 dark:text-oklch(0.704 0.191 22.216) dark:hover:bg-oklch(0.704 0.191 22.216)/20 dark:focus-visible:border-oklch(0.704 0.191 22.216)/40 dark:focus-visible:ring-oklch(0.704 0.191 22.216)/20 dark:dark:bg-oklch(0.704 0.191 22.216)/20 dark:dark:hover:bg-oklch(0.704 0.191 22.216)/30 dark:dark:focus-visible:ring-oklch(0.704 0.191 22.216)/40',
|
||||
link: 'text-oklch(0.205 0 0) underline-offset-4 hover:underline dark:text-oklch(0.922 0 0)',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
icon: 'size-8',
|
||||
'icon-xs':
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
||||
'icon-lg': 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={
|
||||
variant === 'unstyled'
|
||||
? cn('cursor-pointer disabled:cursor-not-allowed disabled:opacity-50', className)
|
||||
: cn(buttonVariants({ variant, size, className }))
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,134 @@
|
||||
import * as React from 'react';
|
||||
import { Dialog as DialogPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-white p-6 text-sm text-neutral-900 shadow-xl ring-1 ring-black/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 dark:bg-neutral-900 dark:text-neutral-50 dark:ring-white/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="absolute top-4 right-4 inline-flex size-8 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-500 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-50"
|
||||
type="button"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<XIcon className="size-4" aria-hidden="true" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />;
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
'-mx-6 -mb-6 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-neutral-100/50 p-6 sm:flex-row sm:justify-end dark:bg-neutral-800/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return <DialogPrimitive.Title data-slot="dialog-title" className={cn('text-base leading-none font-medium', className)} {...props} />;
|
||||
}
|
||||
|
||||
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
'text-sm text-neutral-500 *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-neutral-900 dark:text-neutral-400 dark:*:[a]:hover:text-neutral-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { cloudflare } from '@cloudflare/vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss(), cloudflare()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(import.meta.dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user