/** * folk-gov-quadratic — Weight Transformer * * Inline weight transform GovMod. Accepts raw weight on input port, * applies sqrt/log/linear transform, and emits effective weight on output. * Always passes (gate-out = satisfied). Visualizes raw vs effective in a bar chart. */ import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; import type { PortDescriptor } from "./data-types"; const HEADER_COLOR = "#14b8a6"; type TransformMode = "sqrt" | "log" | "linear"; interface WeightEntry { who: string; raw: number; effective: number; } const styles = css` :host { background: var(--rs-bg-surface, #1e293b); border-radius: 10px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25); min-width: 240px; min-height: 140px; overflow: hidden; } .header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: ${HEADER_COLOR}; color: white; font-size: 12px; font-weight: 600; cursor: move; border-radius: 10px 10px 0 0; } .header-title { display: flex; align-items: center; gap: 6px; } .header-actions button { background: transparent; border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 14px; } .header-actions button:hover { background: rgba(255, 255, 255, 0.2); } .body { display: flex; flex-direction: column; padding: 12px; gap: 8px; } .title-input { background: transparent; border: none; color: var(--rs-text-primary, #e2e8f0); font-size: 13px; font-weight: 600; width: 100%; outline: none; } .title-input::placeholder { color: var(--rs-text-muted, #64748b); } .mode-row { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--rs-text-muted, #94a3b8); } .mode-select { background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px; color: var(--rs-text-primary, #e2e8f0); font-size: 11px; padding: 2px 6px; outline: none; } .chart-area { min-height: 60px; } .chart-area svg { width: 100%; display: block; } .entries-list { max-height: 80px; overflow-y: auto; font-size: 10px; color: var(--rs-text-muted, #94a3b8); } .entry-item { display: flex; justify-content: space-between; padding: 2px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } .status-label { font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; text-align: center; color: #22c55e; } `; declare global { interface HTMLElementTagNameMap { "folk-gov-quadratic": FolkGovQuadratic; } } export class FolkGovQuadratic extends FolkShape { static override tagName = "folk-gov-quadratic"; static override portDescriptors: PortDescriptor[] = [ { name: "weight-in", type: "json", direction: "input" }, { name: "weight-out", type: "json", direction: "output" }, { name: "gate-out", type: "json", direction: "output" }, ]; static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules).map(r => r.cssText).join("\n"); const childRules = Array.from(styles.cssRules).map(r => r.cssText).join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #title = "Weight Transform"; #mode: TransformMode = "sqrt"; #entries: WeightEntry[] = []; // DOM refs #titleEl!: HTMLInputElement; #modeEl!: HTMLSelectElement; #chartEl!: HTMLElement; #listEl!: HTMLElement; get title() { return this.#title; } set title(v: string) { this.#title = v; if (this.#titleEl) this.#titleEl.value = v; } get mode() { return this.#mode; } set mode(v: TransformMode) { this.#mode = v; if (this.#modeEl) this.#modeEl.value = v; this.#recalc(); } get entries(): WeightEntry[] { return [...this.#entries]; } set entries(v: WeightEntry[]) { this.#entries = v; this.#updateVisuals(); this.#emitPorts(); } #transform(raw: number): number { if (raw <= 0) return 0; switch (this.#mode) { case "sqrt": return Math.sqrt(raw); case "log": return Math.log1p(raw); case "linear": return raw; } } #recalc() { for (const e of this.#entries) { e.effective = this.#transform(e.raw); } this.#updateVisuals(); this.#emitPorts(); this.dispatchEvent(new CustomEvent("content-change")); } override createRenderRoot() { const root = super.createRenderRoot(); this.initPorts(); const wrapper = document.createElement("div"); wrapper.style.cssText = "width:100%;height:100%;display:flex;flex-direction:column;"; wrapper.innerHTML = html`
√ Quadratic
Mode:
PASSTHROUGH
`; const slot = root.querySelector("slot"); const container = slot?.parentElement as HTMLElement; if (container) container.replaceWith(wrapper); // Cache refs this.#titleEl = wrapper.querySelector(".title-input") as HTMLInputElement; this.#modeEl = wrapper.querySelector(".mode-select") as HTMLSelectElement; this.#chartEl = wrapper.querySelector(".chart-area") as HTMLElement; this.#listEl = wrapper.querySelector(".entries-list") as HTMLElement; // Set initial values this.#titleEl.value = this.#title; this.#modeEl.value = this.#mode; this.#updateVisuals(); // Wire events this.#titleEl.addEventListener("input", (e) => { e.stopPropagation(); this.#title = this.#titleEl.value; this.dispatchEvent(new CustomEvent("content-change")); }); this.#modeEl.addEventListener("change", (e) => { e.stopPropagation(); this.#mode = this.#modeEl.value as TransformMode; this.#recalc(); }); wrapper.querySelector(".close-btn")!.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); // Prevent drag on inputs for (const el of wrapper.querySelectorAll("input, select, button")) { el.addEventListener("pointerdown", (e) => e.stopPropagation()); } // Handle input port this.addEventListener("port-value-changed", ((e: CustomEvent) => { const { name, value } = e.detail; if (name === "weight-in" && value && typeof value === "object") { const v = value as any; // Accept { who, weight } or { who, raw } const who = v.who || v.memberName || "anonymous"; const raw = v.weight || v.raw || v.amount || 0; // Update or add const existing = this.#entries.find(e => e.who === who); if (existing) { existing.raw = raw; existing.effective = this.#transform(raw); } else { this.#entries.push({ who, raw, effective: this.#transform(raw) }); } this.#updateVisuals(); this.#emitPorts(); this.dispatchEvent(new CustomEvent("content-change")); } }) as EventListener); return root; } #updateVisuals() { this.#renderChart(); this.#renderList(); } #renderChart() { if (!this.#chartEl) return; if (this.#entries.length === 0) { this.#chartEl.innerHTML = ""; return; } const W = 220; const H = 70; const PAD = { top: 6, right: 8, bottom: 16, left: 8 }; const plotW = W - PAD.left - PAD.right; const plotH = H - PAD.top - PAD.bottom; const maxRaw = Math.max(1, ...this.#entries.map(e => e.raw)); const maxEff = Math.max(1, ...this.#entries.map(e => e.effective)); const maxVal = Math.max(maxRaw, maxEff); const barW = Math.max(6, Math.min(20, plotW / (this.#entries.length * 2.5))); let svg = ``; // Grid line svg += ``; const entries = this.#entries.slice(0, 8); // max 8 bars const groupW = plotW / entries.length; for (let i = 0; i < entries.length; i++) { const e = entries[i]; const cx = PAD.left + groupW * i + groupW / 2; const rawH = (e.raw / maxVal) * plotH; const effH = (e.effective / maxVal) * plotH; // Raw bar (dimmed) svg += ``; // Effective bar (teal) svg += ``; // Label const label = e.who.length > 5 ? e.who.slice(0, 5) : e.who; svg += `${label}`; } // Legend svg += ``; svg += `raw`; svg += ``; svg += `eff`; svg += ""; this.#chartEl.innerHTML = svg; } #renderList() { if (!this.#listEl) return; this.#listEl.innerHTML = this.#entries.map(e => `
${e.who}${e.raw.toFixed(1)} → ${e.effective.toFixed(2)}
` ).join(""); } #emitPorts() { const totalRaw = this.#entries.reduce((s, e) => s + e.raw, 0); const totalEffective = this.#entries.reduce((s, e) => s + e.effective, 0); this.setPortValue("weight-out", { totalRaw, totalEffective, mode: this.#mode, entries: this.#entries.map(e => ({ who: e.who, raw: e.raw, effective: e.effective })), }); // Always satisfied — this is a passthrough transform this.setPortValue("gate-out", { satisfied: true, totalRaw, totalEffective, mode: this.#mode, }); } override toJSON() { return { ...super.toJSON(), type: "folk-gov-quadratic", title: this.#title, mode: this.#mode, entries: this.#entries, }; } static override fromData(data: Record): FolkGovQuadratic { const shape = FolkShape.fromData.call(this, data) as FolkGovQuadratic; if (data.title !== undefined) shape.title = data.title; if (data.mode !== undefined) shape.mode = data.mode; if (data.entries !== undefined) shape.entries = data.entries; return shape; } override applyData(data: Record): void { super.applyData(data); if (data.title !== undefined && data.title !== this.#title) this.title = data.title; if (data.mode !== undefined && data.mode !== this.#mode) this.mode = data.mode; if (data.entries !== undefined && JSON.stringify(data.entries) !== JSON.stringify(this.#entries)) this.entries = data.entries; } }