webui: keystore — expandable row detail view for key data

Expandable detail row per key.  Clicking the ▶ arrow left of the name
unfolds a full-width row showing labeled key fields (Value / Public Key
/ Private Key) as word-wrapped monospace blocks (max-width: 72ch),
making base64 data easy to read and copy.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-14 22:07:28 +02:00
parent b2bf034a91
commit 613ec53fdf
3 changed files with 98 additions and 19 deletions
+50 -4
View File
@@ -61,6 +61,8 @@
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-mono: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
--surface-alt: var(--slate-100);
/* Legacy variable mapping */
--card-bg: var(--surface);
}
@@ -74,6 +76,7 @@
--fg-muted: var(--slate-400);
--border: var(--slate-700);
--border-subtle: var(--slate-800);
--surface-alt: var(--slate-900);
--sidebar-bg: var(--slate-800);
--sidebar-fg: var(--slate-200);
--sidebar-hover: rgba(255, 255, 255, 0.1);
@@ -92,6 +95,7 @@
--fg-muted: var(--slate-400);
--border: var(--slate-700);
--border-subtle: var(--slate-800);
--surface-alt: var(--slate-900);
--zebra-stripe: rgba(255,255,255,0.03);
--sidebar-bg: var(--slate-800);
--sidebar-fg: var(--slate-200);
@@ -110,6 +114,7 @@
--fg-muted: var(--slate-500);
--border: var(--slate-200);
--border-subtle: var(--slate-100);
--surface-alt: var(--slate-100);
--sidebar-bg: var(--slate-100);
--sidebar-fg: var(--slate-700);
--sidebar-hover: rgba(0, 0, 0, 0.07);
@@ -132,7 +137,6 @@
.light .badge-reject { background: #fef2f2; color: #991b1b; }
.light .badge-drop { background: var(--slate-100); color: var(--slate-600); }
.light .badge-continue { background: #dbeafe; color: #1e40af; }
.light .key-full { background: var(--slate-100); }
/* ==========================================================================
Reset & Base
@@ -1264,9 +1268,51 @@ details.fw-hint[open] summary::before { transform: rotate(90deg); }
/* Keystore */
.empty-message { color: var(--fg-muted); font-style: italic; padding: 1rem; text-align: center; }
.key-full { font-family: var(--font-mono); font-size: 0.8rem; word-break: break-all; color: var(--fg); background: var(--slate-100); padding: 0.25rem; border-radius: 4px; }
@media (prefers-color-scheme: dark) { .key-full { background: var(--slate-800); } }
.dark .key-full { background: var(--slate-800); }
.key-row-toggle {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--fg);
font: inherit;
display: inline-flex;
align-items: center;
gap: 0.4rem;
text-align: left;
}
.key-row-arrow {
font-size: 0.6rem;
color: var(--fg-muted);
display: inline-block;
transition: transform 0.15s ease;
flex-shrink: 0;
}
.key-row-toggle[aria-expanded="true"] .key-row-arrow { transform: rotate(90deg); }
.key-detail-row { display: none; }
.key-detail-row.is-open { display: table-row; }
.key-detail-cell {
background: var(--surface-alt);
padding: 0.75rem 1rem 0.75rem 1.5rem !important;
border-top: none !important;
}
.key-detail-body { display: flex; flex-direction: column; gap: 0.6rem; }
.key-field { display: flex; flex-direction: column; gap: 0.2rem; }
.key-field-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-muted); }
.key-field-value {
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap;
color: var(--fg);
background: var(--surface);
padding: 0.4rem 0.6rem;
border-radius: 4px;
max-width: 72ch;
display: block;
}
/* Survey */
.survey-chart { max-width: 100%; height: auto; display: block; }
+13
View File
@@ -432,6 +432,19 @@
});
})();
// Keystore key detail row toggle
(function() {
document.addEventListener('click', function(e) {
var btn = e.target.closest('.key-row-toggle');
if (!btn) return;
var target = btn.getAttribute('data-target');
var row = document.getElementById('key-detail-' + target);
if (!row) return;
var open = row.classList.toggle('is-open');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
});
})();
// Sidebar toggle (mobile)
(function() {
var MOBILE_BP = 1024;
+35 -15
View File
@@ -23,15 +23,23 @@
<tr>
<th>Name</th>
<th>Format</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{{range .SymmetricKeys}}
<tr>
<td>{{.Name}}</td>
{{range $i, $k := .SymmetricKeys}}
<tr class="key-summary-row">
<td><button class="key-row-toggle" type="button" aria-expanded="false" data-target="sym-{{$i}}"><span class="key-row-arrow" aria-hidden="true"></span>{{.Name}}</button></td>
<td>{{.Format}}</td>
<td><code class="key-full">{{.Value}}</code></td>
</tr>
<tr class="key-detail-row" id="key-detail-sym-{{$i}}">
<td colspan="2" class="key-detail-cell">
<div class="key-detail-body">
<div class="key-field">
<span class="key-field-label">Value</span>
<code class="key-field-value">{{.Value}}</code>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
@@ -49,21 +57,33 @@
<tr>
<th>Name</th>
<th>Algorithm</th>
<th>Public Key</th>
{{if (index .AsymmetricKeys 0).PrivateKeyFull}}<th>Private Key</th>{{end}}
<th>Certificates</th>
</tr>
</thead>
<tbody>
{{range .AsymmetricKeys}}
<tr>
<td>{{.Name}}</td>
{{range $i, $k := .AsymmetricKeys}}
<tr class="key-summary-row">
<td><button class="key-row-toggle" type="button" aria-expanded="false" data-target="asym-{{$i}}"><span class="key-row-arrow" aria-hidden="true"></span>{{.Name}}</button></td>
<td>{{.Algorithm}}</td>
<td>{{if .PublicKeyFull}}<code class="key-full">{{.PublicKeyFull}}</code>{{else}}-{{end}}</td>
{{if .PrivateKeyFull}}
<td><code class="key-full">{{.PrivateKeyFull}}</code></td>
{{end}}
<td>{{range $i, $c := .Certificates}}{{if $i}}, {{end}}{{$c}}{{end}}</td>
<td>{{range $j, $c := .Certificates}}{{if $j}}, {{end}}{{$c}}{{end}}</td>
</tr>
<tr class="key-detail-row" id="key-detail-asym-{{$i}}">
<td colspan="4" class="key-detail-cell">
<div class="key-detail-body">
{{if .PublicKeyFull}}
<div class="key-field">
<span class="key-field-label">Public Key</span>
<code class="key-field-value">{{.PublicKeyFull}}</code>
</div>
{{end}}
{{if .PrivateKeyFull}}
<div class="key-field">
<span class="key-field-label">Private Key</span>
<code class="key-field-value">{{.PrivateKeyFull}}</code>
</div>
{{end}}
</div>
</td>
</tr>
{{end}}
</tbody>