/**
* 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`
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 = `";
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;
}
}