Redesign following Web Interface Guidelines

- Added skip link for accessibility
- Proper semantic HTML with <table>, <th scope>, <main>
- All icon buttons have aria-label
- Focus-visible rings on all interactive elements
- Proper form labels with htmlFor
- Input autocomplete and type attributes
- Role and aria attributes for modal
- Visual focus states without outline-none
- Better color contrast and status indicators
- Hover and transition improvements
This commit is contained in:
OpenClaw Bot 2026-02-18 13:49:59 -06:00
parent 28cd726320
commit 796a760dc3
19 changed files with 182 additions and 88 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -130,7 +130,7 @@
/******/ /******/
/******/ /* webpack/runtime/getFullHash */ /******/ /* webpack/runtime/getFullHash */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.h = () => ("175a446eeb5052e2") /******/ __webpack_require__.h = () => ("1930de2390f27e98")
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ /* webpack/runtime/hasOwnProperty shorthand */

File diff suppressed because one or more lines are too long

View File

@ -190,7 +190,7 @@
/******/ /******/
/******/ /* webpack/runtime/getFullHash */ /******/ /* webpack/runtime/getFullHash */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.h = () => ("1122623f302a8ef0") /******/ __webpack_require__.h = () => ("946779c75e05dcfd")
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/global */ /******/ /* webpack/runtime/global */

View File

@ -0,0 +1 @@
{"c":["app/page","webpack"],"r":[],"m":[]}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("946779c75e05dcfd")
/******/ })();
/******/
/******/ }
);

File diff suppressed because one or more lines are too long

View File

@ -151,123 +151,148 @@ export default function HeartbeatMonitor() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center"> <div className="min-h-screen bg-slate-950 flex items-center justify-center" role="status" aria-live="polite">
<div className="text-slate-400">Loading...</div> <div className="text-slate-400">Loading monitor...</div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-slate-950 text-slate-100 p-6"> <div className="min-h-screen bg-slate-950 text-slate-100">
{/* Skip Link for Accessibility */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-emerald-500 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg"
>
Skip to main content
</a>
{/* Header */} {/* Header */}
<div className="max-w-6xl mx-auto mb-6"> <header className="border-b border-slate-800 bg-slate-900">
<div className="flex items-center justify-between"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Activity className="w-8 h-8 text-emerald-500" /> <Activity className="w-7 h-7 text-emerald-500" aria-hidden="true" />
<div> <div>
<h1 className="text-2xl font-bold">Heartbeat Monitor</h1> <h1 className="text-xl font-semibold text-white">Heartbeat Monitor</h1>
<p className="text-slate-400">{onlineApps} of {totalApps} services online</p> <p className="text-sm text-slate-400" aria-live="polite">
{onlineApps} of {totalApps} services online
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={fetchData} onClick={fetchData}
className="p-2 bg-slate-800 rounded-lg text-slate-400 hover:text-white" className="inline-flex items-center justify-center p-2.5 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
aria-label="Refresh data"
> >
<RefreshCw className="w-5 h-5" /> <RefreshCw className="w-5 h-5" aria-hidden="true" />
</button> </button>
<button <button
onClick={() => setShowAddApp(true)} onClick={() => setShowAddApp(true)}
className="flex items-center gap-2 bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-2 rounded-lg" className="inline-flex items-center gap-2 bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2.5 rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" aria-hidden="true" />
Add App Add App
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</header>
{/* Main Content */}
<main id="main-content" className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Table */} {/* Table */}
<div className="max-w-6xl mx-auto">
<div className="bg-slate-900 rounded-lg border border-slate-800 overflow-hidden"> <div className="bg-slate-900 rounded-lg border border-slate-800 overflow-hidden">
<table className="w-full"> <table className="w-full text-left">
<thead> <thead>
<tr className="bg-slate-800 text-left"> <tr className="bg-slate-800 border-b border-slate-700">
<th className="px-4 py-3 text-sm font-medium text-slate-400">Status</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-sm font-medium text-slate-400">Name</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Name</th>
<th className="px-4 py-3 text-sm font-medium text-slate-400">URL</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">URL</th>
<th className="px-4 py-3 text-sm font-medium text-slate-400">Port</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Port</th>
<th className="px-4 py-3 text-sm font-medium text-slate-400">Uptime</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Uptime</th>
<th className="px-4 py-3 text-sm font-medium text-slate-400">Response</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Response</th>
<th className="px-4 py-3 text-sm font-medium text-slate-400">Actions</th> <th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">
<span className="sr-only">Actions</span>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody className="divide-y divide-slate-800">
{apps.map((app) => { {apps.map((app) => {
const { isUp, uptime, latest } = getAppStatus(app.id); const { isUp, uptime, latest } = getAppStatus(app.id);
return ( return (
<tr key={app.id} className="border-t border-slate-800 hover:bg-slate-800/50"> <tr key={app.id} className="hover:bg-slate-800/50 transition-colors">
<td className="px-4 py-3"> <td className="px-4 py-4">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${ <span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
isUp isUp
? "bg-emerald-500/20 text-emerald-400" ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
: "bg-red-500/20 text-red-400" : "bg-red-500/10 text-red-400 border border-red-500/20"
}`}> }`}
<span className={`w-1.5 h-1.5 rounded-full ${isUp ? "bg-emerald-400" : "bg-red-400"}`} /> >
{isUp ? "ONLINE" : "OFFLINE"} <span className={`w-1.5 h-1.5 rounded-full ${isUp ? "bg-emerald-400" : "bg-red-400"}`} aria-hidden="true" />
{isUp ? "Online" : "Offline"}
</span> </span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-4">
<div> <div>
<p className="font-medium text-white">{app.name}</p> <p className="font-medium text-white">{app.name}</p>
{app.description && (
<p className="text-sm text-slate-500">{app.description}</p> <p className="text-sm text-slate-500">{app.description}</p>
)}
</div> </div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-4">
<a <a
href={app.url} href={app.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-emerald-400 hover:text-emerald-300 text-sm" className="text-emerald-400 hover:text-emerald-300 hover:underline text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 rounded"
> >
{app.url} {app.url}
</a> </a>
</td> </td>
<td className="px-4 py-3 text-slate-300 font-mono">{app.port}</td> <td className="px-4 py-4">
<td className="px-4 py-3"> <code className="text-sm text-slate-300 bg-slate-800 px-2 py-1 rounded">{app.port}</code>
<span className={`font-mono ${uptime >= 90 ? "text-emerald-400" : "text-yellow-400"}`}> </td>
<td className="px-4 py-4">
<span className={`text-sm font-medium ${uptime >= 90 ? "text-emerald-400" : uptime >= 50 ? "text-yellow-400" : "text-red-400"}`}>
{uptime}% {uptime}%
</span> </span>
</td> </td>
<td className="px-4 py-3 text-slate-300 font-mono"> <td className="px-4 py-4">
{latest?.responseTime ? `${latest.responseTime}ms` : "--"} <span className="text-sm text-slate-300">
{latest?.responseTime ? `${latest.responseTime}ms` : "—"}
</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<button <button
onClick={() => checkApp(app)} onClick={() => checkApp(app)}
disabled={checking === app.id} disabled={checking === app.id}
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700 rounded" className="inline-flex items-center justify-center p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 disabled:opacity-50 disabled:cursor-not-allowed"
title="Check now" aria-label={`Check ${app.name} status`}
> >
<RefreshCw className={`w-4 h-4 ${checking === app.id ? "animate-spin" : ""}`} /> <RefreshCw className={`w-4 h-4 ${checking === app.id ? "animate-spin" : ""}`} aria-hidden="true" />
</button> </button>
<a <a
href={app.url} href={app.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700 rounded" className="inline-flex items-center justify-center p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
title="Open app" aria-label={`Open ${app.name}`}
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" aria-hidden="true" />
</a> </a>
<button <button
onClick={() => deleteApp(app.id)} onClick={() => deleteApp(app.id)}
className="p-1.5 text-slate-400 hover:text-red-400 hover:bg-slate-700 rounded" className="inline-flex items-center justify-center p-2 text-slate-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
title="Delete" aria-label={`Delete ${app.name}`}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" aria-hidden="true" />
</button> </button>
</div> </div>
</td> </td>
@ -277,66 +302,91 @@ export default function HeartbeatMonitor() {
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </main>
{/* Add App Modal */} {/* Add App Modal */}
{showAddApp && ( {showAddApp && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div
<div className="bg-slate-900 rounded-lg p-6 w-full max-w-md border border-slate-800"> className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4"
<h2 className="text-xl font-bold text-white mb-4">Add New App</h2> role="dialog"
<form onSubmit={addApp} className="space-y-4"> aria-modal="true"
aria-labelledby="add-app-title"
>
<div className="bg-slate-900 rounded-lg border border-slate-800 p-6 w-full max-w-md shadow-2xl">
<h2 id="add-app-title" className="text-xl font-semibold text-white mb-6">Add New App</h2>
<form onSubmit={addApp} className="space-y-5">
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Name</label> <label htmlFor="app-name" className="block text-sm font-medium text-slate-400 mb-2">
Name <span className="text-red-400">*</span>
</label>
<input <input
id="app-name"
type="text" type="text"
value={newApp.name} value={newApp.name}
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })} onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
placeholder="My Application"
required required
autoComplete="off"
/> />
</div> </div>
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Description</label> <label htmlFor="app-desc" className="block text-sm font-medium text-slate-400 mb-2">
Description
</label>
<input <input
id="app-desc"
type="text" type="text"
value={newApp.description} value={newApp.description}
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })} onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
placeholder="Brief description…"
autoComplete="off"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm text-slate-400 mb-1">URL</label> <label htmlFor="app-url" className="block text-sm font-medium text-slate-400 mb-2">
URL <span className="text-red-400">*</span>
</label>
<input <input
type="text" id="app-url"
type="url"
value={newApp.url} value={newApp.url}
onChange={(e) => setNewApp({ ...newApp, url: e.target.value })} onChange={(e) => setNewApp({ ...newApp, url: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
placeholder="http://localhost:3000"
required required
autoComplete="off"
/> />
</div> </div>
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Port</label> <label htmlFor="app-port" className="block text-sm font-medium text-slate-400 mb-2">
Port <span className="text-red-400">*</span>
</label>
<input <input
id="app-port"
type="number" type="number"
value={newApp.port} value={newApp.port}
onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })} onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })}
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
required required
min="1"
max="65535"
/> />
</div> </div>
</div> </div>
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-4">
<button <button
type="button" type="button"
onClick={() => setShowAddApp(false)} onClick={() => setShowAddApp(false)}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2 rounded-lg" className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2.5 rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white py-2 rounded-lg font-medium" className="flex-1 bg-emerald-600 hover:bg-emerald-500 text-white py-2.5 rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
> >
Add App Add App
</button> </button>