Split embedded and standalone chart surfaces

This commit is contained in:
Mikhail Chusavitin
2026-03-15 21:41:38 +03:00
parent df91e24fea
commit 5ce37f9997
15 changed files with 1039 additions and 86 deletions

View File

@@ -16,6 +16,8 @@ var pageTemplate = template.Must(template.New("view.html").Funcs(template.FuncMa
"joinLines": joinLines,
}).ParseFS(content, "templates/view.html"))
var uploadTemplate = template.Must(template.New("upload.html").ParseFS(content, "templates/upload.html"))
func Render(data any) ([]byte, error) {
var out strings.Builder
if err := pageTemplate.ExecuteTemplate(&out, "view.html", data); err != nil {
@@ -24,6 +26,14 @@ func Render(data any) ([]byte, error) {
return []byte(out.String()), nil
}
func RenderUpload(data any) ([]byte, error) {
var out strings.Builder
if err := uploadTemplate.ExecuteTemplate(&out, "upload.html", data); err != nil {
return nil, err
}
return []byte(out.String()), nil
}
func Static() http.Handler {
sub, err := fs.Sub(content, "static")
if err != nil {

View File

@@ -52,9 +52,10 @@ body {
padding: 20px 0 32px;
}
.input-panel,
.empty-panel,
.meta-panel,
.section-card {
.section-card,
.upload-panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
@@ -63,30 +64,63 @@ body {
margin-bottom: 16px;
}
.input-label,
.empty-panel h2,
.section-card h2,
.meta-panel h2 {
.meta-panel h2,
.upload-panel h2 {
display: block;
margin: 0 0 12px;
font-size: 18px;
}
textarea {
.empty-panel p,
.upload-panel p {
margin: 0;
color: var(--muted);
}
.upload-panel {
width: min(680px, 100%);
margin-left: auto;
margin-right: auto;
}
.upload-dropzone {
display: block;
margin-top: 16px;
border: 1px dashed var(--border);
border-radius: 12px;
padding: 18px;
background: #faf6ef;
}
.upload-dropzone input {
display: block;
width: 100%;
min-height: 220px;
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
background: #fcfaf6;
color: var(--ink);
font: 13px/1.5 "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
margin-bottom: 12px;
}
.input-actions {
margin-top: 12px;
.upload-eyebrow {
display: block;
margin-bottom: 6px;
color: var(--accent);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
button {
.upload-dropzone strong {
display: block;
margin-bottom: 4px;
font-size: 18px;
}
.upload-actions {
margin-top: 14px;
}
.upload-actions button {
border: 0;
border-radius: 999px;
background: var(--accent);
@@ -98,7 +132,7 @@ button {
}
.error-box {
margin-top: 12px;
margin-bottom: 16px;
border: 1px solid var(--crit-fg);
border-radius: 10px;
padding: 12px;
@@ -121,6 +155,24 @@ button {
border-radius: 999px;
}
.sections-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.section-card {
margin-bottom: 0;
}
.section-card-half {
grid-column: span 1;
}
.section-card-full {
grid-column: 1 / -1;
}
.kv-table,
.data-table {
width: 100%;
@@ -154,6 +206,19 @@ button {
overflow-x: auto;
}
.table-group + .table-group {
margin-top: 18px;
}
.table-group h3 {
margin: 0 0 8px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.data-table thead th {
position: sticky;
top: 0;
@@ -198,6 +263,10 @@ button {
width: min(100vw - 20px, 1500px);
}
.sections-grid {
grid-template-columns: 1fr;
}
.page-header {
padding: 18px 14px 14px;
}
@@ -206,9 +275,15 @@ button {
font-size: 26px;
}
.input-panel,
.empty-panel,
.meta-panel,
.section-card {
.section-card,
.upload-panel {
padding: 14px;
}
.section-card-half,
.section-card-full {
grid-column: auto;
}
}

38
web/templates/upload.html Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/view.css">
</head>
<body>
<header class="page-header">
<div>
<h1>{{ .Title }}</h1>
<p>Read-only viewer for Reanimator JSON snapshots</p>
</div>
</header>
<main class="page-main">
<section class="upload-panel">
<h2>Open Snapshot</h2>
<p>Select a Reanimator JSON snapshot to render.</p>
<form method="post" action="/render" enctype="multipart/form-data">
<label class="upload-dropzone" for="snapshot_file">
<input id="snapshot_file" name="snapshot_file" type="file" accept=".json,application/json" required>
<span class="upload-eyebrow">Standalone Mode</span>
<strong>Choose a snapshot JSON file</strong>
<span>The file is rendered read-only and not modified.</span>
</label>
<div class="upload-actions">
<button type="submit">Render Snapshot</button>
</div>
</form>
{{ if .Error }}
<div class="error-box">{{ .Error }}</div>
{{ end }}
</section>
</main>
</body>
</html>

View File

@@ -15,19 +15,6 @@
</header>
<main class="page-main">
<section class="input-panel">
<form method="post" action="/render">
<label for="snapshot" class="input-label">Snapshot JSON</label>
<textarea id="snapshot" name="snapshot" spellcheck="false" placeholder='{"target_host":"...","hardware":{...}}'>{{ .InputJSON }}</textarea>
<div class="input-actions">
<button type="submit">Render Snapshot</button>
</div>
</form>
{{ if .Error }}
<div class="error-box">{{ .Error }}</div>
{{ end }}
</section>
{{ if .HasSnapshot }}
<section class="meta-panel">
<h2>Snapshot Metadata</h2>
@@ -49,8 +36,9 @@
{{ end }}
</nav>
<div class="sections-grid">
{{ range .Sections }}
<section class="section-card" id="{{ .ID }}">
<section class="section-card {{ if or (eq .ID "board") (eq .ID "firmware") }}section-card-half{{ else }}section-card-full{{ end }}" id="{{ .ID }}">
<h2>{{ .Title }}</h2>
{{ if eq .Kind "object" }}
@@ -103,8 +91,59 @@
</table>
</div>
{{ end }}
{{ if eq .Kind "grouped_tables" }}
{{ range .Groups }}
<div class="table-group">
<h3>{{ .Title }}</h3>
{{ $group := . }}
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
{{ range .Columns }}
<th>{{ . }}</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range .Items }}
<tr>
{{ $row := . }}
{{ range $group.Columns }}
<td>
{{ $value := index $row.Cells . }}
{{ if eq . "status" }}
<span class="status-badge {{ statusClass $value }}">{{ $value }}</span>
{{ else }}
{{ range joinLines $value }}
<div>{{ . }}</div>
{{ end }}
{{ end }}
</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
{{ end }}
{{ end }}
</section>
{{ end }}
</div>
{{ end }}
{{ if .Error }}
<section class="error-box">{{ .Error }}</section>
{{ end }}
{{ if not .HasSnapshot }}
<section class="empty-panel">
<h2>Snapshot Viewer</h2>
<p>This page renders one Reanimator snapshot provided by the embedding application.</p>
</section>
{{ end }}
</main>
</body>