feat: add hwlab cli and web skeleton
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# HWLAB L6 MVP Runtime Plan
|
||||
|
||||
This evidence fixture is a mock artifact used by the CLI and cloud web skeleton.
|
||||
|
||||
- Environment: dev
|
||||
- Endpoint: http://74.48.78.17:6667
|
||||
- Scope: browser/CLI/gateway through cloud-web/cloud-api
|
||||
- Status: dry-run only
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"project": {
|
||||
"projectId": "proj_mvp-l6",
|
||||
"name": "HWLAB L6 MVP",
|
||||
"description": "Mock project for the HWLAB CLI and cloud web skeleton.",
|
||||
"status": "active",
|
||||
"environment": "dev",
|
||||
"labels": {
|
||||
"track": "mvp",
|
||||
"surface": "cli-web"
|
||||
},
|
||||
"createdBy": "service_hwlab-cli",
|
||||
"createdAt": "2026-05-21T00:00:00.000Z",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"projectId": "proj_mvp-l6",
|
||||
"name": "HWLAB L6 MVP",
|
||||
"status": "active",
|
||||
"environment": "dev",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"projectId": "proj_cloud-web",
|
||||
"name": "Cloud Web Skeleton",
|
||||
"status": "active",
|
||||
"environment": "dev",
|
||||
"updatedAt": "2026-05-20T12:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"serviceRoute": [
|
||||
"browser/CLI/gateway",
|
||||
"master hwlab-edge-proxy",
|
||||
"frp",
|
||||
"D601 hwlab-dev/hwlab-router",
|
||||
"cloud-web/cloud-api"
|
||||
],
|
||||
"mvpSteps": [
|
||||
"browser/CLI/gateway",
|
||||
"master hwlab-edge-proxy",
|
||||
"frp",
|
||||
"D601 hwlab-dev/hwlab-router",
|
||||
"cloud-web/cloud-api",
|
||||
"hardware trusted closed loop",
|
||||
"agent automation closed loop",
|
||||
"evidence record",
|
||||
"worker cleanup"
|
||||
],
|
||||
"endpoints": {
|
||||
"dev": "http://74.48.78.17:6667"
|
||||
},
|
||||
"loops": [
|
||||
{
|
||||
"name": "hardware trusted closed loop",
|
||||
"description": "Route a validated wiring config to the gateway and observe the corresponding hardware operation lifecycle."
|
||||
},
|
||||
{
|
||||
"name": "agent automation closed loop",
|
||||
"description": "Schedule agent and worker sessions, execute a scoped operation, and persist the resulting audit and evidence trail."
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"evidenceId": "evidence_mvp-runtime-plan",
|
||||
"projectId": "proj_mvp-l6",
|
||||
"operationId": "op_mvp-0001",
|
||||
"kind": "report",
|
||||
"uri": "fixtures/mvp/evidence/mvp-runtime-plan.md",
|
||||
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"serviceId": "hwlab-cli",
|
||||
"environment": "dev",
|
||||
"createdAt": "2026-05-21T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"source": "mock"
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": [
|
||||
"release worker sessions",
|
||||
"clear transient gateway state",
|
||||
"archive evidence references",
|
||||
"close the CLI-runner task"
|
||||
]
|
||||
}
|
||||
+6
-1
@@ -5,6 +5,11 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"validate": "node scripts/validate-contract.mjs",
|
||||
"check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs"
|
||||
"check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs",
|
||||
"cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health",
|
||||
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
|
||||
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
|
||||
"web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
|
||||
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DEV_ENDPOINT } from "../../../internal/protocol/index.mjs";
|
||||
|
||||
const DEFAULT_PROJECTS = [
|
||||
{
|
||||
projectId: "proj_mvp-l6",
|
||||
name: "HWLAB L6 MVP",
|
||||
status: "active",
|
||||
environment: "dev",
|
||||
updatedAt: "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
projectId: "proj_cloud-web",
|
||||
name: "Cloud Web Skeleton",
|
||||
status: "active",
|
||||
environment: "dev",
|
||||
updatedAt: "2026-05-20T12:00:00.000Z"
|
||||
}
|
||||
];
|
||||
|
||||
function loadRuntimeFixture(cwd) {
|
||||
const fixturePath = path.resolve(cwd, "fixtures/mvp/runtime.json");
|
||||
if (!fs.existsSync(fixturePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(fixturePath, "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function resolveRuntime(cwd) {
|
||||
const fixture = loadRuntimeFixture(cwd);
|
||||
if (fixture) {
|
||||
return fixture;
|
||||
}
|
||||
return {
|
||||
endpoints: { dev: DEV_ENDPOINT },
|
||||
projects: DEFAULT_PROJECTS,
|
||||
mvpSteps: [
|
||||
"browser/CLI/gateway",
|
||||
"master hwlab-edge-proxy",
|
||||
"frp",
|
||||
"D601 hwlab-dev/hwlab-router",
|
||||
"cloud-web/cloud-api",
|
||||
"hardware trusted closed loop",
|
||||
"agent automation closed loop",
|
||||
"evidence record",
|
||||
"worker cleanup"
|
||||
],
|
||||
loops: [
|
||||
{ name: "hardware trusted closed loop" },
|
||||
{ name: "agent automation closed loop" }
|
||||
],
|
||||
evidence: [],
|
||||
cleanup: []
|
||||
};
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function writeLine(stream, line = "") {
|
||||
stream.write(`${line}\n`);
|
||||
}
|
||||
|
||||
function writeProjectList(runtime, stdout) {
|
||||
writeLine(stdout, "hwlab-cli project list");
|
||||
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
|
||||
for (const project of runtime.projects ?? []) {
|
||||
writeLine(
|
||||
stdout,
|
||||
`${project.projectId} | ${project.name} | ${project.status} | ${project.environment} | ${project.updatedAt}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function writeHealth(runtime, stdout) {
|
||||
writeLine(stdout, "hwlab-cli health");
|
||||
writeLine(stdout, "status: ok");
|
||||
writeLine(stdout, `environment: dev`);
|
||||
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
|
||||
writeLine(stdout, `projects: ${(runtime.projects ?? []).length}`);
|
||||
}
|
||||
|
||||
function writeDryRun(runtime, stdout) {
|
||||
writeLine(stdout, "hwlab-cli test e2e --env dev --mvp --dry-run");
|
||||
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
|
||||
writeLine(stdout, "mvp route:");
|
||||
writeLine(
|
||||
stdout,
|
||||
` ${runtime.serviceRoute?.join(" -> ") ?? "browser/CLI/gateway -> master hwlab-edge-proxy -> frp -> D601 hwlab-dev/hwlab-router -> cloud-web/cloud-api"}`
|
||||
);
|
||||
for (const step of runtime.mvpSteps ?? []) {
|
||||
writeLine(stdout, `- ${step}`);
|
||||
}
|
||||
writeLine(stdout, "closed loops:");
|
||||
for (const loop of runtime.loops ?? []) {
|
||||
writeLine(stdout, `- ${loop.name}${loop.description ? `: ${loop.description}` : ""}`);
|
||||
}
|
||||
writeLine(stdout, "evidence record:");
|
||||
for (const evidence of runtime.evidence ?? []) {
|
||||
writeLine(
|
||||
stdout,
|
||||
`- ${evidence.evidenceId} | ${evidence.kind} | ${evidence.serviceId} | ${evidence.uri}`
|
||||
);
|
||||
}
|
||||
writeLine(stdout, "worker cleanup:");
|
||||
for (const step of runtime.cleanup ?? []) {
|
||||
writeLine(stdout, `- ${step}`);
|
||||
}
|
||||
writeLine(stdout, "dry-run only: no DEV/PROD changes were made");
|
||||
}
|
||||
|
||||
function writeHelp(stdout) {
|
||||
writeLine(stdout, "hwlab-cli");
|
||||
writeLine(stdout, "commands:");
|
||||
writeLine(stdout, " health");
|
||||
writeLine(stdout, " project list");
|
||||
writeLine(stdout, " test e2e --env dev --mvp --dry-run");
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const flags = new Set();
|
||||
const options = new Map();
|
||||
const rest = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg.startsWith("--")) {
|
||||
flags.add(arg);
|
||||
const next = args[index + 1];
|
||||
if (next && !next.startsWith("--")) {
|
||||
options.set(arg, next);
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
rest.push(arg);
|
||||
}
|
||||
}
|
||||
return { flags, options, rest };
|
||||
}
|
||||
|
||||
export async function runCli(argv, io) {
|
||||
const stdout = io.stdout ?? process.stdout;
|
||||
const stderr = io.stderr ?? process.stderr;
|
||||
const runtime = resolveRuntime(io.cwd ?? process.cwd());
|
||||
const [command = "help", subcommand, ...rest] = argv;
|
||||
const { flags, options } = parseArgs(rest);
|
||||
|
||||
if (command === "health") {
|
||||
writeHealth(runtime, stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "project" && subcommand === "list") {
|
||||
writeProjectList(runtime, stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "test" && subcommand === "e2e") {
|
||||
const env = options.get("--env") ?? null;
|
||||
const isDryRun = flags.has("--dry-run");
|
||||
const isMvp = flags.has("--mvp");
|
||||
if (env !== "dev") {
|
||||
writeLine(stderr, "only --env dev is supported in the skeleton");
|
||||
return 1;
|
||||
}
|
||||
if (!isMvp || !isDryRun) {
|
||||
writeLine(stderr, "use --mvp --dry-run for the skeleton");
|
||||
return 1;
|
||||
}
|
||||
writeDryRun(runtime, stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "project" && !subcommand) {
|
||||
writeLine(stderr, "usage: hwlab-cli project list");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (command === "test" && !subcommand) {
|
||||
writeLine(stderr, "usage: hwlab-cli test e2e --env dev --mvp --dry-run");
|
||||
return 1;
|
||||
}
|
||||
|
||||
writeHelp(stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function formatRuntime(runtime) {
|
||||
return formatJson(runtime);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "hwlab-cli",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"hwlab-cli": "./bin/hwlab-cli.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs",
|
||||
"health": "node bin/hwlab-cli.mjs health"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { runtime } from "./runtime.mjs";
|
||||
|
||||
const runtimeGrid = document.querySelector("#runtime-grid");
|
||||
const steps = document.querySelector("#mvp-steps");
|
||||
const projectsBody = document.querySelector("#projects-body");
|
||||
const status = document.querySelector("#status");
|
||||
|
||||
status.textContent = "dev skeleton";
|
||||
|
||||
const runtimePairs = [
|
||||
["Endpoint", runtime.endpoints.dev],
|
||||
["Route", runtime.serviceRoute.join(" -> ")],
|
||||
["Closed loops", runtime.closedLoops.join(" / ")],
|
||||
["Evidence", runtime.evidence[0].uri]
|
||||
];
|
||||
|
||||
for (const [label, value] of runtimePairs) {
|
||||
const dt = document.createElement("dt");
|
||||
dt.textContent = label;
|
||||
const dd = document.createElement("dd");
|
||||
dd.textContent = value;
|
||||
runtimeGrid.append(dt, dd);
|
||||
}
|
||||
|
||||
for (const step of runtime.mvpSteps) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = step;
|
||||
steps.append(li);
|
||||
}
|
||||
|
||||
for (const project of runtime.projects) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${project.projectId}<br /><strong>${project.name}</strong></td>
|
||||
<td>${project.status}</td>
|
||||
<td>${project.environment}</td>
|
||||
<td>${project.updatedAt}</td>
|
||||
`;
|
||||
projectsBody.append(row);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>HWLAB Cloud Web</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
<script type="module" src="./app.mjs"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">HWLAB</p>
|
||||
<h1>Cloud Web</h1>
|
||||
</div>
|
||||
<div class="status" id="status">dev skeleton</div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Runtime</h2>
|
||||
<p>Mock dashboard for the L6 CLI and cloud-web surface.</p>
|
||||
</div>
|
||||
<dl class="grid" id="runtime-grid"></dl>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>MVP route</h2>
|
||||
<p>Fixed dry-run path shared with the CLI skeleton.</p>
|
||||
</div>
|
||||
<ol class="steps" id="mvp-steps"></ol>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Projects</h2>
|
||||
<p>Static project list seeded from the mock fixture.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Status</th>
|
||||
<th>Environment</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projects-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "hwlab-cloud-web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check": "node scripts/check.mjs",
|
||||
"build": "node scripts/build.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export const runtime = {
|
||||
endpoints: {
|
||||
dev: "http://74.48.78.17:6667"
|
||||
},
|
||||
serviceRoute: [
|
||||
"browser/CLI/gateway",
|
||||
"master hwlab-edge-proxy",
|
||||
"frp",
|
||||
"D601 hwlab-dev/hwlab-router",
|
||||
"cloud-web/cloud-api"
|
||||
],
|
||||
mvpSteps: [
|
||||
"browser/CLI/gateway",
|
||||
"master hwlab-edge-proxy",
|
||||
"frp",
|
||||
"D601 hwlab-dev/hwlab-router",
|
||||
"cloud-web/cloud-api",
|
||||
"hardware trusted closed loop",
|
||||
"agent automation closed loop",
|
||||
"evidence record",
|
||||
"worker cleanup"
|
||||
],
|
||||
closedLoops: [
|
||||
"hardware trusted closed loop",
|
||||
"agent automation closed loop"
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
projectId: "proj_mvp-l6",
|
||||
name: "HWLAB L6 MVP",
|
||||
status: "active",
|
||||
environment: "dev",
|
||||
updatedAt: "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
projectId: "proj_cloud-web",
|
||||
name: "Cloud Web Skeleton",
|
||||
status: "active",
|
||||
environment: "dev",
|
||||
updatedAt: "2026-05-20T12:00:00.000Z"
|
||||
}
|
||||
],
|
||||
evidence: [
|
||||
{
|
||||
evidenceId: "evidence_mvp-runtime-plan",
|
||||
kind: "report",
|
||||
uri: "fixtures/mvp/evidence/mvp-runtime-plan.md",
|
||||
serviceId: "hwlab-cli"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||||
const distDir = path.resolve(rootDir, "dist");
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
|
||||
for (const file of ["index.html", "styles.css", "app.mjs", "runtime.mjs"]) {
|
||||
fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
|
||||
}
|
||||
|
||||
console.log(`hwlab-cloud-web build ok: ${distDir}`);
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||||
const requiredFiles = ["index.html", "styles.css", "app.mjs", "runtime.mjs"];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
const filePath = path.resolve(rootDir, file);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`missing web asset: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
|
||||
if (!html.includes("HWLAB Cloud Web")) {
|
||||
throw new Error("index.html missing title");
|
||||
}
|
||||
|
||||
console.log("hwlab-cloud-web check ok");
|
||||
@@ -0,0 +1,154 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface-2: #1f2630;
|
||||
--text: #e6edf3;
|
||||
--muted: #9aa4b2;
|
||||
--accent: #58a6ff;
|
||||
--border: #30363d;
|
||||
--good: #3fb950;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 4px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
dl,
|
||||
ol,
|
||||
table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
color: var(--good);
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-head p {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 18px;
|
||||
}
|
||||
|
||||
.grid dt {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.grid dd {
|
||||
margin: 2px 0 0;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
body {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user