mirror of
https://github.com/ruvnet/RuView
synced 2026-08-04 19:31:42 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
export interface SidebarItem {
|
||||
id: string;
|
||||
name: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
type SelectCallback = (id: string) => void;
|
||||
|
||||
export class Sidebar {
|
||||
private root: HTMLElement;
|
||||
private listEl: HTMLElement;
|
||||
private filterInput: HTMLInputElement;
|
||||
private items: SidebarItem[] = [];
|
||||
private activeId: string | null = null;
|
||||
private selectCallback: SelectCallback | null = null;
|
||||
private customFilter: ((item: SidebarItem) => boolean) | null = null;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'sidebar';
|
||||
|
||||
// Filter
|
||||
this.filterInput = document.createElement('input');
|
||||
this.filterInput.type = 'text';
|
||||
this.filterInput.className = 'sidebar-search';
|
||||
this.filterInput.placeholder = 'Filter...';
|
||||
this.filterInput.addEventListener('input', () => this.applyFilter());
|
||||
this.root.appendChild(this.filterInput);
|
||||
|
||||
// List
|
||||
this.listEl = document.createElement('div');
|
||||
this.listEl.className = 'sidebar-list';
|
||||
this.root.appendChild(this.listEl);
|
||||
|
||||
container.appendChild(this.root);
|
||||
}
|
||||
|
||||
setItems(items: SidebarItem[]): void {
|
||||
this.items = items;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
onSelect(callback: SelectCallback): void {
|
||||
this.selectCallback = callback;
|
||||
}
|
||||
|
||||
setFilter(filterFn: (item: SidebarItem) => boolean): void {
|
||||
this.customFilter = filterFn;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
private applyFilter(): void {
|
||||
const query = this.filterInput.value.toLowerCase().trim();
|
||||
const filtered = this.items.filter((item) => {
|
||||
if (this.customFilter && !this.customFilter(item)) return false;
|
||||
if (query && !item.name.toLowerCase().includes(query)) return false;
|
||||
return true;
|
||||
});
|
||||
this.render(filtered);
|
||||
}
|
||||
|
||||
private render(filtered: SidebarItem[]): void {
|
||||
this.listEl.innerHTML = '';
|
||||
for (const item of filtered) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'sidebar-item';
|
||||
if (item.id === this.activeId) el.classList.add('selected');
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'sidebar-item-label';
|
||||
nameSpan.textContent = item.name;
|
||||
el.appendChild(nameSpan);
|
||||
|
||||
if (item.score !== undefined) {
|
||||
const scoreSpan = document.createElement('span');
|
||||
scoreSpan.className = 'sidebar-item-secondary';
|
||||
scoreSpan.textContent = `Score: ${item.score.toFixed(2)}`;
|
||||
el.appendChild(scoreSpan);
|
||||
}
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
this.activeId = item.id;
|
||||
this.applyFilter();
|
||||
this.selectCallback?.(item.id);
|
||||
});
|
||||
|
||||
this.listEl.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
type ChangeCallback = (epoch: number) => void;
|
||||
|
||||
export class TimeScrubber {
|
||||
private root: HTMLElement;
|
||||
private slider: HTMLInputElement;
|
||||
private display: HTMLElement;
|
||||
private changeCallback: ChangeCallback | null = null;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'time-scrubber';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'time-scrubber-title';
|
||||
label.textContent = 'Epoch';
|
||||
this.root.appendChild(label);
|
||||
|
||||
this.slider = document.createElement('input');
|
||||
this.slider.type = 'range';
|
||||
this.slider.className = 'time-scrubber-range';
|
||||
this.slider.min = '0';
|
||||
this.slider.max = '100';
|
||||
this.slider.value = '0';
|
||||
this.slider.addEventListener('input', () => this.handleChange());
|
||||
this.root.appendChild(this.slider);
|
||||
|
||||
this.display = document.createElement('span');
|
||||
this.display.className = 'time-scrubber-label';
|
||||
this.display.textContent = 'E0';
|
||||
this.root.appendChild(this.display);
|
||||
|
||||
container.appendChild(this.root);
|
||||
}
|
||||
|
||||
setRange(min: number, max: number): void {
|
||||
this.slider.min = String(min);
|
||||
this.slider.max = String(max);
|
||||
const val = Number(this.slider.value);
|
||||
if (val < min) this.slider.value = String(min);
|
||||
if (val > max) this.slider.value = String(max);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
setValue(epoch: number): void {
|
||||
this.slider.value = String(epoch);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
onChange(callback: ChangeCallback): void {
|
||||
this.changeCallback = callback;
|
||||
}
|
||||
|
||||
private handleChange(): void {
|
||||
this.updateDisplay();
|
||||
this.changeCallback?.(Number(this.slider.value));
|
||||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
this.display.textContent = `E${this.slider.value}`;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export interface WitnessLogEntry {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
action: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
const BADGE_CLASS: Record<string, string> = {
|
||||
commit: 'witness-badge-commit',
|
||||
verify: 'witness-badge-verify',
|
||||
seal: 'witness-badge-seal',
|
||||
merge: 'witness-badge-merge',
|
||||
};
|
||||
|
||||
export class WitnessLog {
|
||||
private root: HTMLElement;
|
||||
private listEl: HTMLElement;
|
||||
private autoScroll = true;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'witness-log';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'witness-log-header';
|
||||
header.textContent = 'Witness Log';
|
||||
this.root.appendChild(header);
|
||||
|
||||
this.listEl = document.createElement('div');
|
||||
this.listEl.className = 'witness-log-list';
|
||||
|
||||
this.listEl.addEventListener('scroll', () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.listEl;
|
||||
this.autoScroll = scrollTop + clientHeight >= scrollHeight - 20;
|
||||
});
|
||||
|
||||
this.root.appendChild(this.listEl);
|
||||
container.appendChild(this.root);
|
||||
}
|
||||
|
||||
addEntry(entry: WitnessLogEntry): void {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'witness-log-entry';
|
||||
|
||||
const ts = document.createElement('span');
|
||||
ts.className = 'witness-ts';
|
||||
ts.textContent = entry.timestamp;
|
||||
el.appendChild(ts);
|
||||
|
||||
const typeBadge = document.createElement('span');
|
||||
const badgeCls = BADGE_CLASS[entry.type.toLowerCase()] ?? 'witness-badge-commit';
|
||||
typeBadge.className = `witness-badge ${badgeCls}`;
|
||||
typeBadge.textContent = entry.type;
|
||||
el.appendChild(typeBadge);
|
||||
|
||||
const action = document.createElement('span');
|
||||
action.className = 'witness-step';
|
||||
action.textContent = entry.action;
|
||||
el.appendChild(action);
|
||||
|
||||
const hash = document.createElement('span');
|
||||
hash.className = 'witness-hash';
|
||||
hash.textContent = entry.hash.substring(0, 12);
|
||||
hash.title = entry.hash;
|
||||
el.appendChild(hash);
|
||||
|
||||
this.listEl.appendChild(el);
|
||||
|
||||
if (this.autoScroll) {
|
||||
this.listEl.scrollTop = this.listEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.listEl.innerHTML = '';
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user