Skip to content

Commit 58157fc

Browse files
authored
feat(dashboard): add GroupWidget container with panel/collapsible/tabbed modes (#33)
feat(dashboard): add GroupWidget container with panel/collapsible/tabbed modes
2 parents 2df32a9 + b4816da commit 58157fc

12 files changed

Lines changed: 2734 additions & 24 deletions

bridge/web/css/style.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,33 @@ html, body {
397397
to { opacity: 0; transform: translateY(12px); }
398398
}
399399

400+
/* --- Group Widget ------------------------------------------ */
401+
.widget-group-header {
402+
padding: 6px 12px;
403+
font-weight: bold;
404+
font-size: 13px;
405+
border-radius: 4px 4px 0 0;
406+
}
407+
.widget-group-toggle {
408+
margin-right: 8px;
409+
}
410+
.widget-group-tabbar {
411+
display: inline-flex;
412+
gap: 2px;
413+
margin-left: 16px;
414+
}
415+
.widget-group-tab {
416+
padding: 3px 12px;
417+
border: none;
418+
cursor: pointer;
419+
font-size: 11px;
420+
border-radius: 3px 3px 0 0;
421+
opacity: 0.6;
422+
}
423+
.widget-group-tab.active {
424+
opacity: 1.0;
425+
}
426+
400427
/* --- Responsive -------------------------------------------- */
401428
@media (max-width: 900px) {
402429
:root { --grid-cols: 6; }

bridge/web/js/widgets.js

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var Widgets = (function () {
1717
case "text": return renderText(config, bodyEl);
1818
case "timeline": return renderTimeline(config, bodyEl);
1919
case "rawaxes": return renderRawAxes(config, bodyEl);
20+
case "group": return renderGroup(config, bodyEl);
2021
default:
2122
bodyEl.textContent = "Unknown widget type: " + type;
2223
}
@@ -229,6 +230,100 @@ var Widgets = (function () {
229230
'</div>';
230231
}
231232

233+
/* --- Group --------------------------------------------- */
234+
function renderGroup(config, container) {
235+
var mode = config.mode || 'panel';
236+
var label = config.label || '';
237+
238+
// Header
239+
if (label) {
240+
var header = document.createElement('div');
241+
header.className = 'widget-group-header';
242+
header.textContent = label;
243+
244+
if (mode === 'collapsible') {
245+
var toggle = document.createElement('span');
246+
toggle.className = 'widget-group-toggle';
247+
toggle.textContent = config.collapsed ? '>' : 'v';
248+
header.insertBefore(toggle, header.firstChild);
249+
header.style.cursor = 'pointer';
250+
header.addEventListener('click', function() {
251+
var content = container.querySelector('.widget-group-content');
252+
var isCollapsed = content.style.display === 'none';
253+
content.style.display = isCollapsed ? 'grid' : 'none';
254+
toggle.textContent = isCollapsed ? 'v' : '>';
255+
});
256+
}
257+
258+
if (mode === 'tabbed' && config.tabs && config.tabs.length > 0) {
259+
var tabBar = document.createElement('div');
260+
tabBar.className = 'widget-group-tabbar';
261+
config.tabs.forEach(function(tab, idx) {
262+
var tabBtn = document.createElement('button');
263+
tabBtn.className = 'widget-group-tab';
264+
if (tab.name === config.activeTab) {
265+
tabBtn.classList.add('active');
266+
}
267+
tabBtn.textContent = tab.name;
268+
tabBtn.addEventListener('click', function() {
269+
var panels = container.querySelectorAll('.widget-group-tabpanel');
270+
panels.forEach(function(p) { p.style.display = 'none'; });
271+
panels[idx].style.display = 'grid';
272+
tabBar.querySelectorAll('.widget-group-tab').forEach(function(b) {
273+
b.classList.remove('active');
274+
});
275+
tabBtn.classList.add('active');
276+
});
277+
tabBar.appendChild(tabBtn);
278+
});
279+
header.appendChild(tabBar);
280+
}
281+
282+
container.appendChild(header);
283+
}
284+
285+
// Content
286+
if (mode === 'tabbed' && config.tabs) {
287+
config.tabs.forEach(function(tab, idx) {
288+
var tabPanel = document.createElement('div');
289+
tabPanel.className = 'widget-group-tabpanel widget-group-content';
290+
tabPanel.style.display = (tab.name === config.activeTab) ? 'grid' : 'none';
291+
tabPanel.style.gridTemplateColumns = 'repeat(auto-fit, minmax(200px, 1fr))';
292+
tabPanel.style.gap = '8px';
293+
tabPanel.style.padding = '8px';
294+
295+
(tab.widgets || []).forEach(function(wCfg) {
296+
var wEl = document.createElement('div');
297+
wEl.className = 'widget';
298+
var wBody = document.createElement('div');
299+
wBody.className = 'widget-body';
300+
wEl.appendChild(wBody);
301+
render(wCfg, wBody);
302+
tabPanel.appendChild(wEl);
303+
});
304+
container.appendChild(tabPanel);
305+
});
306+
} else {
307+
var content = document.createElement('div');
308+
content.className = 'widget-group-content';
309+
content.style.display = config.collapsed ? 'none' : 'grid';
310+
content.style.gridTemplateColumns = 'repeat(auto-fit, minmax(200px, 1fr))';
311+
content.style.gap = '8px';
312+
content.style.padding = '8px';
313+
314+
(config.children || []).forEach(function(childCfg) {
315+
var wEl = document.createElement('div');
316+
wEl.className = 'widget';
317+
var wBody = document.createElement('div');
318+
wBody.className = 'widget-body';
319+
wEl.appendChild(wBody);
320+
render(childCfg, wBody);
321+
content.appendChild(wEl);
322+
});
323+
container.appendChild(content);
324+
}
325+
}
326+
232327
/* --- helpers ------------------------------------------- */
233328
function formatNumber(v) {
234329
if (v == null) return "--";
@@ -239,5 +334,5 @@ var Widgets = (function () {
239334
return v.toFixed(2);
240335
}
241336

242-
return { render: render };
337+
return { render: render, renderGroup: renderGroup };
243338
})();

0 commit comments

Comments
 (0)