const ReflowUI = (() => {
const theme = {
bg: '#0a0a0a',
bgSurface: '#111',
bgElevated:'#1a1a1a',
border: '#222',
borderLight:'#333',
textPrimary: '#e5e5e5',
textSecondary: '#999',
textMuted: '#666',
textDim: '#444',
green: '#22c55e',
blue: '#3b82f6',
amber: '#f59e0b',
red: '#ef4444',
purple: '#a855f7',
orange: '#f97316',
cyan: '#06b6d4',
pink: '#ec4899',
gain: '#f59e0b',
reduction: '#ef4444',
signal: '#22c55e',
spectrum: '#22c55e',
inactive: '#666',
fontMono: '10px ui-monospace, "SF Mono", "Cascadia Code", monospace',
fontSmall: '9px ui-monospace, "SF Mono", "Cascadia Code", monospace',
fontTiny: '8px ui-monospace, "SF Mono", "Cascadia Code", monospace',
fontLabel: '11px ui-monospace, "SF Mono", "Cascadia Code", monospace',
pad: '4px',
padSmall: '2px',
radius: '4px',
radiusSmall: '3px',
};
const hostCSS = `
:host {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
width: 100%;
color: ${theme.textPrimary};
font: ${theme.fontMono};
--rf-bg: ${theme.bg};
--rf-surface: ${theme.bgSurface};
--rf-border: ${theme.border};
--rf-green: ${theme.green};
--rf-blue: ${theme.blue};
--rf-amber: ${theme.amber};
--rf-red: ${theme.red};
--rf-text: ${theme.textPrimary};
--rf-text-muted: ${theme.textMuted};
--rf-radius: ${theme.radius};
}
`;
const canvasCSS = `
canvas {
width: 100%;
border-radius: var(--rf-radius);
background: var(--rf-bg);
}
`;
const infoCSS = `
.rf-info {
font: ${theme.fontSmall};
color: ${theme.textMuted};
padding: ${theme.padSmall} ${theme.pad};
}
`;
const gridCSS = `
.rf-grid {
display: grid;
gap: 2px 8px;
padding: ${theme.pad};
font: ${theme.fontMono};
}
.rf-grid .label { color: ${theme.textMuted}; }
.rf-grid .value { color: ${theme.green}; text-align: right; }
`;
const icons = {
activity: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12h4l3-9 4 18 3-9h4"/>',
'volume-2': '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 8a5 5 0 0 1 0 8M17.7 5a9 9 0 0 1 0 14M6 15H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2l3.5-4.5A.8.8 0 0 1 11 5v14a.8.8 0 0 1-1.5.5L6 15"/>',
'volume-x': '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 15H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2l3.5-4.5A.8.8 0 0 1 11 5v14a.8.8 0 0 1-1.5.5L6 15M16 9l4 4m0-4-4 4"/>',
sliders: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 3v3m0 4v10M12 3v10m0 4v3M17 3v5m0 4v8M4 10h6M9 17h6M14 12h6"/>',
'bar-chart': '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12v8h4v-8H3Zm7-8v16h4V4h-4Zm7 4v12h4V8h-4Z"/>',
'bar-chart-2':'<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 3v18h18M7 16v-3m4 3V8m4 8v-5m4 5V5"/>',
zap: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 3L4 14h7l-1 7 9-11h-7l1-7z"/>',
sun: '<circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32 1.41-1.41"/>',
image: '<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8.5" cy="8.5" r="1.5" stroke="currentColor" stroke-width="1.5" fill="none"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 15-3.09-3.09a2 2 0 0 0-2.82 0L6 21"/>',
scissors: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0 6 3 3 0 0 0 0-6Zm12-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM8.12 8.12 12 12m0 0 3.88 3.88M8.12 15.88 12 12"/>',
'git-merge': '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 18a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM6 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM6 21V6m0 6c0-2 2-4 6-4h3"/>',
shield: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
'minimize-2': '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7"/>',
music: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 18V5l12-2v13M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm12-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>',
radio: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.9 4.9C1.68 8.13 1.68 13.37 4.9 16.6m12.73.53c3.22-3.23 3.22-8.47 0-11.7M7.76 7.76a6 6 0 0 0 0 8.49m6.48-.01a6 6 0 0 0 0-8.49"/><circle cx="12" cy="12" r="2" fill="currentColor"/>',
wind: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2m12-6a2.5 2.5 0 1 1 1.8 4.3m-9 3.2a2.5 2.5 0 1 0 1.8 4.3H2"/>',
layers: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m12 2 10 6.5v7L12 22 2 15.5v-7L12 2Zm0 20v-6.5M22 8.5l-10 7-10-7"/>',
clock: '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 6v6l4 2"/>',
upload: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m14-7-5-5-5 5m5-5v12"/>',
download: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m5-3 5 5 5-5m-5 5V3"/>',
};
function icon(name, size = 16, color = 'currentColor') {
const svg = icons[name];
if (!svg) return '';
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" style="color:${color};flex-shrink:0">${svg}</svg>`;
}
const canvas = {
freqToX(freq, width) {
return (Math.log10(freq / 20) / Math.log10(1000)) * width;
},
xToFreq(x, width) {
return 20 * Math.pow(1000, x / width);
},
dbToY(db, height, range = 24) {
return height / 2 - (db / range) * (height / 2);
},
yToDb(y, height, range = 24) {
return -((y - height / 2) / (height / 2)) * range;
},
drawFreqGrid(ctx, width, height) {
ctx.strokeStyle = theme.bgElevated;
ctx.lineWidth = 0.5;
for (const f of [100, 1000, 10000]) {
const x = canvas.freqToX(f, width);
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke();
}
},
drawDbGrid(ctx, width, height, interval = 12) {
ctx.strokeStyle = theme.bgElevated;
ctx.lineWidth = 0.5;
const mid = height / 2;
ctx.beginPath(); ctx.moveTo(0, mid); ctx.lineTo(width, mid); ctx.stroke();
for (let db = interval; db <= 24; db += interval) {
for (const sign of [1, -1]) {
const y = canvas.dbToY(db * sign, height);
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke();
}
}
},
setupHiDPI(canvasEl, cssHeight) {
const dpr = window.devicePixelRatio || 1;
const w = canvasEl.offsetWidth;
const h = cssHeight;
canvasEl.width = w * dpr;
canvasEl.height = h * dpr;
const ctx = canvasEl.getContext('2d');
ctx.scale(dpr, dpr);
return { ctx, width: w, height: h };
},
};
function drawMeter(ctx, x, y, width, height, value, color = theme.green) {
ctx.fillStyle = theme.bgSurface;
ctx.beginPath();
ctx.roundRect(x, y, width, height, 2);
ctx.fill();
const fillH = Math.min(1, Math.max(0, value)) * height;
if (fillH > 0) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(x, y + height - fillH, width, fillH, 2);
ctx.fill();
}
}
function drawHMeter(ctx, x, y, width, height, value, color = theme.green) {
ctx.fillStyle = theme.bgSurface;
ctx.beginPath();
ctx.roundRect(x, y, width, height, 2);
ctx.fill();
const fillW = Math.min(1, Math.max(0, value)) * width;
if (fillW > 0) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(x, y, fillW, height, 2);
ctx.fill();
}
}
function formatBytes(b) {
if (b < 1024) return b + ' B';
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB';
return (b / 1073741824).toFixed(2) + ' GB';
}
function formatDb(db) {
const sign = db >= 0 ? '+' : '';
return `${sign}${db.toFixed(1)} dB`;
}
function formatFreq(hz) {
if (hz >= 1000) return (hz / 1000).toFixed(1) + ' kHz';
return Math.round(hz) + ' Hz';
}
class ReflowComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._unsubs = [];
}
get template() { return ''; }
get styles() { return ''; }
onConnect() {}
onDisconnect() {}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>${hostCSS}${canvasCSS}${infoCSS}${gridCSS}${this.styles}</style>
${this.template}
`;
this.onConnect();
}
disconnectedCallback() {
this.onDisconnect();
for (const unsub of this._unsubs) unsub?.();
this._unsubs = [];
}
sub(fn) {
const unsub = fn?.();
if (unsub) this._unsubs.push(unsub);
}
$(id) { return this.shadowRoot.getElementById(id); }
$q(sel) { return this.shadowRoot.querySelector(sel); }
getProps() { return this.zeal?.getProperties() || {}; }
}
return {
theme,
icons,
icon,
canvas,
drawMeter,
drawHMeter,
formatBytes,
formatDb,
formatFreq,
hostCSS,
canvasCSS,
infoCSS,
gridCSS,
ReflowComponent,
};
})();
if (typeof globalThis !== 'undefined') globalThis.ReflowUI = ReflowUI;