fix: improve the style for the web

This commit is contained in:
2026-08-28 21:07:04 +07:00
parent 3dcc812cd4
commit dfc7002135
14 changed files with 1232 additions and 119 deletions
+745 -1
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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
View File
@@ -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",
+73 -43
View File
@@ -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>
<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>
<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>
<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>
);
+3 -2
View File
@@ -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>
+19 -12
View File
@@ -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>
+12 -10
View File
@@ -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>
+9 -6
View File
@@ -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
View File
@@ -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);
}
+71
View File
@@ -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 };
+134
View File
@@ -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,
};
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+3
View File
@@ -5,6 +5,9 @@
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"paths": {
"@/*": ["./src/*"]
},
"resolveJsonModule": true,
"noEmit": true,
"isolatedModules": true,
+6
View File
@@ -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'),
},
},
});