🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@eldrex/dashboard

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@eldrex/dashboard - npm Package Compare versions

Comparing version
1.0.2
to
1.0.3
dist/assets/index-Dk4e-dWH.js

Sorry, the diff of this file is too big to display

+37
# @eldrex/dashboard
## Visual, offline-first dashboard for DevDiff changelog management
[![npm version](https://img.shields.io/npm/v/@eldrex/dashboard)](https://npmjs.com/package/@eldrex/dashboard)
[![Socket Score](https://img.shields.io/badge/Socket-76-blue)](https://socket.dev/npm/package/@eldrex/dashboard)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
## Overview
`@eldrex/dashboard` is the web client interface that allows developers and team leads to view, search, curate, and publish generated changelogs. It acts as the frontend companion for the DevDiff local server gateway.
## Installation
```bash
npm install @eldrex/dashboard
```
## Running the Dashboard
To serve the dashboard locally, use the CLI:
```bash
npx devdiff report --port 4200
```
## 🔒 Security & Privacy
The dashboard operates entirely inside your local network.
- **Zero Cloud Tracking**: We do not use trackers, analytics tools, or third-party tracking scripts.
- **Local Storage**: All data, preferences, and custom views are saved in your local workspace or browser localStorage.
- **Access Control**: When connected to `@eldrex/gateway`, requests are authenticated via secure local tokens.
## License
MIT © DevDiff Contributors
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DevDiff Playground — Connected to Your Workspace</title>
<meta name="description" content="DevDiff local playground — analyze your code changes with AI, switch personas, and explore formats. All data stays on your machine." />
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<!-- marked.js for markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- mermaid.js for diagram rendering -->
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<style>
/* ── Design Tokens ─────────────────────────────────────────────────────── */
:root {
--bg: #0a0f1e;
--bg2: #0f172a;
--surface: #1e293b;
--surface2: #263248;
--border: #2d3f55;
--text: #f1f5f9;
--text-dim: #94a3b8;
--text-muted:#64748b;
--accent: #6366f1;
--accent-h: #818cf8;
--cyan: #22d3ee;
--green: #22c55e;
--red: #ef4444;
--yellow: #f59e0b;
--radius: 10px;
--shadow: 0 4px 24px rgba(0,0,0,0.4);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
font-size: 14px;
line-height: 1.6;
}
/* ── Layout ─────────────────────────────────────────────────────────────── */
.app { display: flex; height: 100vh; overflow: hidden; }
/* ── Sidebar ─────────────────────────────────────────────────────────────── */
.sidebar {
width: 264px;
flex-shrink: 0;
background: var(--bg2);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-header {
padding: 20px 20px 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-logo {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 4px;
}
.sidebar-logo .logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--accent), var(--cyan));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.sidebar-logo h1 {
font-size: 15px;
font-weight: 700;
letter-spacing: -0.3px;
}
.sidebar-subtitle {
font-size: 11px;
color: var(--text-muted);
margin-left: 42px;
}
.sidebar-body { padding: 16px; flex: 1; }
.info-block {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
margin-bottom: 10px;
}
.info-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 4px;
font-weight: 600;
}
.info-value {
font-size: 13px;
font-weight: 500;
color: var(--text);
word-break: break-all;
}
.info-value.mono {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
/* AI status badge */
.ai-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
margin-bottom: 10px;
font-size: 12px;
font-weight: 500;
border: 1px solid transparent;
transition: all 0.3s;
}
.ai-badge.connected { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.3); color: var(--green); }
.ai-badge.disconnected { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.25); color: var(--red); }
.ai-badge.checking { background: rgba(245,158,11,.1); border-color: rgba(245,158,11,.25); color: var(--yellow); }
.ai-badge .pulse {
width: 7px; height: 7px;
border-radius: 50%;
background: currentColor;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.85); }
}
/* Range selector */
.range-select {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
border-radius: 8px;
padding: 8px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
margin-bottom: 10px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 14px;
}
.range-select:focus { outline: none; border-color: var(--accent); }
/* Refresh button */
.btn-refresh {
width: 100%;
padding: 11px 16px;
background: linear-gradient(135deg, var(--accent), #4f46e5);
color: #fff;
border: none;
border-radius: 9px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(99,102,241,.35);
}
.btn-refresh:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(99,102,241,.45); }
.btn-refresh:active { transform: translateY(0); }
.btn-refresh.loading { opacity: 0.7; pointer-events: none; }
.privacy-notice {
font-size: 11px;
color: var(--text-muted);
line-height: 1.5;
padding: 8px 10px;
background: rgba(255,255,255,.03);
border-radius: 8px;
border: 1px solid var(--border);
}
.privacy-notice span { color: var(--green); }
/* Session timer */
.session-timer {
font-family: 'JetBrains Mono', monospace;
font-size: 18px;
font-weight: 500;
color: var(--cyan);
margin-bottom: 2px;
}
/* ── Main ───────────────────────────────────────────────────────────────── */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Top toolbar */
.toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.toolbar-section { display: flex; align-items: center; gap: 6px; }
.toolbar-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.7px; margin-right: 2px; }
.toolbar-divider { width: 1px; height: 24px; background: var(--border); margin: 0 4px; }
/* Persona/format pill buttons */
.pill-btn {
padding: 5px 13px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-dim);
cursor: pointer;
font-size: 12px;
font-family: inherit;
font-weight: 500;
transition: all 0.18s;
white-space: nowrap;
}
.pill-btn:hover { border-color: var(--accent); color: var(--text); }
.pill-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
box-shadow: 0 0 12px rgba(99,102,241,.3);
}
/* ── Stats bar ──────────────────────────────────────────────────────────── */
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--border);
border-bottom: 1px solid var(--border);
}
.stat-card {
background: var(--bg2);
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.stat-card .stat-icon {
font-size: 20px;
opacity: 0.8;
}
.stat-card .stat-body {}
.stat-card .stat-num {
font-size: 22px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: var(--cyan);
line-height: 1;
}
.stat-card .stat-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
/* ── Output area ────────────────────────────────────────────────────────── */
.output-area {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.output-panel {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 12px;
min-height: 400px;
padding: 28px;
}
/* Loading state */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 320px;
gap: 16px;
color: var(--text-muted);
}
.spinner {
width: 36px; height: 36px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-state p { font-size: 13px; animation: blink 1.5s ease-in-out infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
/* Markdown styles */
.md-output h1 { font-size: 22px; font-weight: 700; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.md-output h2 { font-size: 17px; font-weight: 600; color: var(--cyan); margin: 20px 0 10px; }
.md-output h3 { font-size: 14px; font-weight: 600; margin: 14px 0 6px; }
.md-output p { color: #cbd5e1; margin-bottom: 10px; }
.md-output ul, .md-output ol { margin: 8px 0 12px 20px; color: #cbd5e1; }
.md-output li { margin-bottom: 4px; }
.md-output strong { color: var(--text); font-weight: 600; }
.md-output em { color: var(--text-dim); font-style: italic; }
.md-output code {
background: var(--surface2);
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--cyan);
}
.md-output pre {
background: var(--surface2);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 12px 0;
border: 1px solid var(--border);
}
.md-output pre code { background: none; padding: 0; color: var(--text); }
.md-output blockquote {
border-left: 3px solid var(--accent);
padding-left: 16px;
color: var(--text-dim);
margin: 12px 0;
}
.md-output hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.md-output table { width: 100%; border-collapse: collapse; margin: 12px 0; }
.md-output th { background: var(--surface2); padding: 8px 12px; text-align: left; font-size: 12px; text-transform: uppercase; color: var(--text-dim); }
.md-output td { padding: 8px 12px; border-bottom: 1px solid var(--border); color: #cbd5e1; }
/* JSON output */
.json-output {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.7;
color: #94a3b8;
white-space: pre-wrap;
word-break: break-all;
}
.json-output .k { color: #818cf8; } /* keys */
.json-output .s { color: #6ee7b7; } /* strings */
.json-output .n { color: #fb923c; } /* numbers */
.json-output .b { color: #f472b6; } /* booleans */
/* Mermaid container */
.mermaid-wrap {
background: #fff;
border-radius: 10px;
padding: 24px;
margin: 12px 0;
text-align: center;
overflow: auto;
}
/* Error state */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 320px;
gap: 12px;
text-align: center;
}
.error-state .error-icon { font-size: 40px; }
.error-state h3 { font-size: 16px; color: var(--red); }
.error-state p { font-size: 13px; color: var(--text-muted); max-width: 380px; }
.error-state code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; font-size: 12px; }
/* Toast notification */
.toast {
position: fixed;
top: 16px;
right: 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 16px;
font-size: 12px;
color: var(--text);
z-index: 9999;
animation: slideIn 0.3s ease, fadeOut 0.4s 2.6s ease forwards;
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: 8px;
}
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } }
@keyframes fadeOut { to { opacity: 0; transform: translateY(-10px); } }
/* Responsive */
@media (max-width: 900px) {
.app { flex-direction: column; }
.sidebar { width: 100%; height: auto; }
.stats-bar { grid-template-columns: repeat(2, 1fr); }
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg2); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #3d5168; }
</style>
</head>
<body>
<div class="app">
<!-- ── Sidebar ──────────────────────────────────────────────────────── -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<div class="logo-icon">🎮</div>
<h1>DevDiff</h1>
</div>
<p class="sidebar-subtitle">Local Playground</p>
</div>
<div class="sidebar-body">
<!-- Workspace info -->
<div class="info-block">
<div class="info-label">Workspace</div>
<div class="info-value" id="ws-name">Loading…</div>
</div>
<div class="info-block">
<div class="info-label">Git</div>
<div class="info-value" id="ws-git">Loading…</div>
</div>
<!-- AI status -->
<div class="ai-badge checking" id="ai-badge">
<span class="pulse"></span>
<span id="ai-badge-text">Checking AI…</span>
</div>
<div class="info-block">
<div class="info-label">AI Provider</div>
<div class="info-value mono" id="ws-provider">—</div>
</div>
<!-- Range selector -->
<div class="info-label" style="margin-bottom:5px;padding-left:1px;">Commit Range</div>
<select class="range-select" id="range-select">
<option value="HEAD~1..HEAD">Last 1 commit</option>
<option value="HEAD~5..HEAD" selected>Last 5 commits</option>
<option value="HEAD~10..HEAD">Last 10 commits</option>
<option value="HEAD~20..HEAD">Last 20 commits</option>
<option value="HEAD~50..HEAD">Last 50 commits</option>
</select>
<!-- Refresh -->
<button class="btn-refresh" id="btn-refresh" onclick="refreshChangelog()">
<span id="refresh-icon">🔄</span>
<span id="refresh-label">Analyze Changes</span>
</button>
<!-- Session timer -->
<div class="info-block">
<div class="info-label">Session Time</div>
<div class="session-timer" id="session-timer">00:00</div>
</div>
<!-- Privacy notice -->
<div class="privacy-notice">
<span>🔒</span> All analysis runs locally.<br>
Nothing is sent to the cloud.
</div>
</div>
</aside>
<!-- ── Main ────────────────────────────────────────────────────────── -->
<div class="main">
<!-- Toolbar -->
<div class="toolbar">
<div class="toolbar-section">
<span class="toolbar-label">Persona</span>
<div id="persona-btns">
<button class="pill-btn active" data-persona="developer">👨💻 Developer</button>
<button class="pill-btn" data-persona="ceo">📊 CEO</button>
<button class="pill-btn" data-persona="educator">📚 Educator</button>
<button class="pill-btn" data-persona="robot">🤖 Robot</button>
<button class="pill-btn" data-persona="data-analyst">📈 Analyst</button>
<button class="pill-btn" data-persona="journalist">📰 Journalist</button>
<button class="pill-btn" data-persona="pm">📋 PM</button>
<button class="pill-btn" data-persona="compliance">🔒 Compliance</button>
</div>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-section">
<span class="toolbar-label">Format</span>
<div id="format-btns">
<button class="pill-btn active" data-format="markdown">📝 Markdown</button>
<button class="pill-btn" data-format="mermaid">📊 Mermaid</button>
<button class="pill-btn" data-format="json">{ } JSON</button>
</div>
</div>
</div>
<!-- Stats bar -->
<div class="stats-bar">
<div class="stat-card">
<span class="stat-icon">📁</span>
<div class="stat-body">
<div class="stat-num" id="stat-files">—</div>
<div class="stat-label">Files Changed</div>
</div>
</div>
<div class="stat-card">
<span class="stat-icon">✅</span>
<div class="stat-body">
<div class="stat-num" id="stat-add">—</div>
<div class="stat-label">Lines Added</div>
</div>
</div>
<div class="stat-card">
<span class="stat-icon">🗑️</span>
<div class="stat-body">
<div class="stat-num" id="stat-del">—</div>
<div class="stat-label">Lines Removed</div>
</div>
</div>
<div class="stat-card">
<span class="stat-icon">🤖</span>
<div class="stat-body">
<div class="stat-num" id="stat-tokens">—</div>
<div class="stat-label">Tokens Used</div>
</div>
</div>
</div>
<!-- Changelog output -->
<div class="output-area">
<div class="output-panel" id="output-panel">
<div class="loading-state">
<div class="spinner"></div>
<p>Connecting to workspace…</p>
</div>
</div>
</div>
</div>
</div>
<script>
// ── State ────────────────────────────────────────────────────────────────
let persona = 'developer';
let format = 'markdown';
let wsConn = null;
let sessionStart = Date.now();
// ── Init ─────────────────────────────────────────────────────────────────
async function init() {
mermaid.initialize({ startOnLoad: false, theme: 'default' });
await loadWorkspaceInfo();
await loadStats();
connectWebSocket();
startSessionTimer();
// Persona buttons
document.querySelectorAll('#persona-btns .pill-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#persona-btns .pill-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
persona = btn.dataset.persona;
refreshChangelog();
});
});
// Format buttons
document.querySelectorAll('#format-btns .pill-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#format-btns .pill-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
format = btn.dataset.format;
refreshChangelog();
});
});
// Range selector
document.getElementById('range-select').addEventListener('change', () => {
loadStats();
refreshChangelog();
});
// Initial changelog load
await refreshChangelog();
}
// ── Workspace info ───────────────────────────────────────────────────────
async function loadWorkspaceInfo() {
try {
const data = await fetchJSON('/api/workspace');
document.getElementById('ws-name').textContent = data.name || '(unknown)';
const git = data.git || {};
document.getElementById('ws-git').textContent =
`${git.commits ?? '?'} commits · ${git.currentBranch ?? '?'}`;
document.getElementById('ws-provider').textContent = data.ai?.provider || 'ollama';
const badge = document.getElementById('ai-badge');
const text = document.getElementById('ai-badge-text');
if (data.ai?.status === 'connected') {
badge.className = 'ai-badge connected';
text.textContent = `AI Connected`;
if (data.ai.modelsAvailable?.length) {
text.textContent += ` · ${data.ai.modelsAvailable[0]}`;
}
} else {
badge.className = 'ai-badge disconnected';
text.textContent = 'AI Disconnected';
}
} catch {
document.getElementById('ws-name').textContent = '(could not load)';
}
}
// ── Stats ────────────────────────────────────────────────────────────────
async function loadStats() {
const range = document.getElementById('range-select').value;
try {
const data = await fetchJSON(`/api/stats?since=${encodeURIComponent(range)}`);
document.getElementById('stat-files').textContent = data.filesChanged ?? '0';
document.getElementById('stat-add').textContent = data.linesAdded ?? '0';
document.getElementById('stat-del').textContent = data.linesRemoved ?? '0';
document.getElementById('stat-tokens').textContent = data.tokensUsed ?? '0';
} catch {}
}
// ── Changelog refresh ────────────────────────────────────────────────────
async function refreshChangelog() {
const panel = document.getElementById('output-panel');
const btn = document.getElementById('btn-refresh');
const icon = document.getElementById('refresh-icon');
const label = document.getElementById('refresh-label');
const range = document.getElementById('range-select').value;
// Loading state
panel.innerHTML = `<div class="loading-state"><div class="spinner"></div><p>Generating ${persona} changelog…</p></div>`;
btn.classList.add('loading');
icon.textContent = '⏳';
label.textContent = 'Analyzing…';
try {
const data = await fetchJSON(
`/api/changelog?persona=${persona}&format=${format}&since=${encodeURIComponent(range)}`
);
if (!data.success) {
panel.innerHTML = `
<div class="error-state">
<span class="error-icon">⚠️</span>
<h3>${escapeHtml(data.error || 'Analysis failed')}</h3>
<p>Try changing the commit range, or make sure your AI provider is running.</p>
<p>Start Ollama: <code>ollama serve</code></p>
</div>`;
} else if (format === 'json') {
panel.innerHTML = `<div class="json-output">${syntaxHighlightJSON(
JSON.stringify(data.changelog, null, 2)
)}</div>`;
} else if (format === 'mermaid') {
const id = 'mermaid-' + Date.now();
panel.innerHTML = `<div class="mermaid-wrap"><div class="mermaid" id="${id}">${escapeHtml(data.changelog)}</div></div>`;
try { await mermaid.run({ nodes: [document.getElementById(id)] }); } catch {}
} else {
panel.innerHTML = `<div class="md-output">${marked.parse(data.changelog || '')}</div>`;
}
await loadStats();
} catch (err) {
panel.innerHTML = `
<div class="error-state">
<span class="error-icon">🔌</span>
<h3>Cannot reach DevDiff server</h3>
<p>Make sure the playground is running: <code>devdiff playground</code></p>
</div>`;
} finally {
btn.classList.remove('loading');
icon.textContent = '🔄';
label.textContent = 'Analyze Changes';
}
}
// ── WebSocket ────────────────────────────────────────────────────────────
function connectWebSocket() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
wsConn = new WebSocket(`${proto}//${location.host}`);
let refreshTimeout = null;
wsConn.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'workspace-changed') {
toast('📝 Changes detected — refreshing…');
clearTimeout(refreshTimeout);
refreshTimeout = setTimeout(() => {
loadStats();
refreshChangelog();
}, 1500);
}
} catch {}
};
wsConn.onclose = () => setTimeout(connectWebSocket, 3000);
}
// ── Session timer ─────────────────────────────────────────────────────────
function startSessionTimer() {
setInterval(() => {
const elapsed = Math.floor((Date.now() - sessionStart) / 1000);
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
const s = String(elapsed % 60).padStart(2, '0');
document.getElementById('session-timer').textContent = `${m}:${s}`;
}, 1000);
}
// ── Helpers ───────────────────────────────────────────────────────────────
async function fetchJSON(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
function escapeHtml(s = '') {
return String(s).replace(/[&<>"']/g, c =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])
);
}
function syntaxHighlightJSON(json) {
return escapeHtml(json).replace(
/("[\w\s]+")\s*:/g, '<span class="k">$1</span>:'
).replace(
/:\s*(".*?")/g, ': <span class="s">$1</span>'
).replace(
/:\s*(\d+\.?\d*)/g, ': <span class="n">$1</span>'
).replace(
/:\s*(true|false|null)/g, ': <span class="b">$1</span>'
);
}
function toast(msg) {
const el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => el.remove(), 3200);
}
// ── Start ─────────────────────────────────────────────────────────────────
init();
</script>
</body>
</html>
+4
-4

@@ -5,5 +5,5 @@

vite v5.4.21 building for production...
vite v6.4.3 building for production...
transforming...
✓ 31 modules transformed.
✓ 27 modules transformed.
rendering chunks...

@@ -13,3 +13,3 @@ computing gzip size...

dist/assets/index-3hymSP_O.css  3.80 kB │ gzip: 1.41 kB
dist/assets/index-DAFVNwFY.js 152.12 kB │ gzip: 48.62 kB
✓ built in 1.11s
dist/assets/index-Dk4e-dWH.js 153.29 kB │ gzip: 49.00 kB
✓ built in 934ms

@@ -12,3 +12,3 @@ <!doctype html>

/>
<script type="module" crossorigin src="/demo/assets/index-DAFVNwFY.js"></script>
<script type="module" crossorigin src="/demo/assets/index-Dk4e-dWH.js"></script>
<link rel="stylesheet" crossorigin href="/demo/assets/index-3hymSP_O.css">

@@ -15,0 +15,0 @@ </head>

{
"name": "@eldrex/dashboard",
"version": "1.0.2",
"version": "1.0.3",
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"description": "Web dashboard for DevDiff — visualize changes and changelogs",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {

@@ -23,8 +31,3 @@ "react": "^18.3.1",

"author": "Eldrex Delos Reyes Bula <eldrexdelosreyesbula@gmail.com>",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
}
}
"license": "MIT"
}

Sorry, the diff of this file is too big to display

MIT License
Copyright (c) 2026 Eldrex Delos Reyes Bula
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.