Spaces:
Running
Running
Download renderers/_shared/cell.js from smapoles/dynamicsystemsmodeling: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/smapoles/dynamicsystemsmodeling/resolve/main/renderers/_shared/cell.js
- Command line
-
hf download hf://spaces/smapoles/dynamicsystemsmodeling/renderers/_shared/cell.js
-
curl -L -o cell.js https://huggingface.co/spaces/smapoles/dynamicsystemsmodeling/resolve/main/renderers/_shared/cell.js
11.3 kB
| // renderers/_shared/cell.js β the ONE reusable card/cell component. | |
| // Single owner of card chrome (spec Β§7.3): renderers build the standardized | |
| // shell here and own only what goes INSIDE the chart area. Everything is | |
| // DOM-built (no innerHTML with payload data), so payload strings need no | |
| // escaping. Styles live in the sibling cell.css / tooltip.css, auto-injected | |
| // once per document by ensureSharedCss() (createCard calls it for you). | |
| // | |
| // Typical renderer usage: | |
| // import { createCard, statCell, setCell, applyTips, | |
| // renderTable, renderMethodology } from '../_shared/cell.js'; | |
| // export function mount(el, payload) { | |
| // const { card, body, setStamp } = createCard(el, payload.meta); | |
| // const grid = document.createElement('div'); | |
| // grid.className = 'cf-cell-grid'; | |
| // grid.append(statCell('phi', 'persistence Β· Ο')); | |
| // body.append(grid); | |
| // applyTips(card, payload.tips); | |
| // setCell(card, 'phi', 'persistence Β· Ο', payload.stats.phi.toFixed(2)); | |
| // renderTable(body, payload.table); | |
| // renderMethodology(body, payload.method); | |
| // ... | |
| // } | |
| const SHARED_CSS = ['cell.css', 'tooltip.css']; | |
| /** | |
| * Idempotently inject <link> tags for the shared stylesheets into the | |
| * document head. Resolved relative to this module, so it works from the | |
| * gallery, a dashboard shell, or a test page alike. The gallery also links | |
| * these directly; the data-cf-shared guard prevents doubles. | |
| */ | |
| export function ensureSharedCss(doc = document) { | |
| const head = doc.head || doc.getElementsByTagName('head')[0]; | |
| if (!head) return; | |
| for (const file of SHARED_CSS) { | |
| if (head.querySelector(`link[data-cf-shared="${file}"]`)) continue; | |
| const link = doc.createElement('link'); | |
| link.rel = 'stylesheet'; | |
| link.href = new URL(`./${file}`, import.meta.url).href; | |
| link.setAttribute('data-cf-shared', file); | |
| head.appendChild(link); | |
| } | |
| } | |
| /** | |
| * Build the standardized card shell under `el`, namespaced by chart id: | |
| * the card root gets class `cf-card` PLUS `meta.id` (the per-chart CSS | |
| * namespace β scope all renderer-specific rules under it; no global ids). | |
| * | |
| * @param {HTMLElement} el mount host | |
| * @param {object} meta payload.meta ({ id, title, ... }) | |
| * @returns {{ card: HTMLElement, body: HTMLElement, setStamp: (t:string)=>void }} | |
| * card β the .cf-card root (query slots against this) | |
| * body β .cf-card-body, append chart/grid/table/method content here | |
| * setStamp β sets the quiet header stamp (e.g. "30 days" or a date) | |
| */ | |
| export function createCard(el, meta = {}) { | |
| ensureSharedCss(el.ownerDocument); | |
| const doc = el.ownerDocument; | |
| const card = doc.createElement('section'); | |
| card.className = meta.id ? `cf-card ${meta.id}` : 'cf-card'; | |
| const title = doc.createElement('div'); | |
| title.className = 'cf-card-title'; | |
| const titleText = doc.createElement('span'); | |
| titleText.setAttribute('data-field', 'title'); | |
| titleText.textContent = meta.title ?? meta.id ?? ''; | |
| const stamp = doc.createElement('span'); | |
| stamp.className = 'cf-stamp'; | |
| stamp.setAttribute('data-field', 'stamp'); | |
| stamp.textContent = 'β'; | |
| title.append(titleText, stamp); | |
| const body = doc.createElement('div'); | |
| body.className = 'cf-card-body'; | |
| card.append(title, body); | |
| el.appendChild(card); | |
| return { | |
| card, | |
| body, | |
| setStamp(text) { stamp.textContent = text ?? 'β'; }, | |
| }; | |
| } | |
| /** | |
| * Stat-cell factory. Produces the standard quiet-label-over-big-value cell: | |
| * <div class="cf-cell" data-slot="<slot>" [data-tip="<tip>"]> | |
| * <span class="cf-cell-label" data-field="label"> | |
| * <b class="cf-cell-value" data-field="val">β</b> | |
| * The slot string is the coupling key: markup (data-slot), CSS grid | |
| * placement (per-chart CSS targets [data-slot="x"]), and tip copy | |
| * (payload.tips[slot], applied here or later via applyTips). | |
| */ | |
| export function statCell(slot, label, tip = '') { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cf-cell'; | |
| cell.setAttribute('data-slot', slot); | |
| if (tip) cell.setAttribute('data-tip', tip); | |
| const lab = document.createElement('span'); | |
| lab.className = 'cf-cell-label'; | |
| lab.setAttribute('data-field', 'label'); | |
| lab.textContent = label ?? ''; | |
| const val = document.createElement('b'); | |
| val.className = 'cf-cell-value'; | |
| val.setAttribute('data-field', 'val'); | |
| val.textContent = 'β'; | |
| cell.append(lab, val); | |
| return cell; | |
| } | |
| /** | |
| * Set a stat cell's label + formatted value. `host` is any ancestor of the | |
| * cell (usually the card root). Missing slots are ignored (renderer decides | |
| * its slot set; payloads with fewer stats simply leave cells at "β"). | |
| */ | |
| export function setCell(host, slot, label, val) { | |
| const cell = host.querySelector(`[data-slot="${slot}"]`); | |
| if (!cell) return; | |
| const lab = cell.querySelector('[data-field="label"]'); | |
| const v = cell.querySelector('[data-field="val"]'); | |
| if (lab && label != null) lab.textContent = label; | |
| if (v) v.textContent = val == null || val === '' ? 'β' : String(val); | |
| } | |
| /** | |
| * Apply payload.tips ({ slot: copy }) onto every matching [data-slot] under | |
| * `host`. Empty/absent copy REMOVES the attribute, so the CSS empty-tip | |
| * guard never shows a blank box. | |
| */ | |
| export function applyTips(host, tips = {}) { | |
| for (const el of host.querySelectorAll('[data-slot]')) { | |
| const copy = tips ? tips[el.getAttribute('data-slot')] : null; | |
| if (copy) el.setAttribute('data-tip', copy); | |
| else el.removeAttribute('data-tip'); | |
| } | |
| } | |
| /** | |
| * Format one value per a CONTRACT-2 column spec ({ precision }). | |
| * null/undefined β em-dash; numbers honor precision; rest pass through. | |
| */ | |
| export function formatValue(v, precision) { | |
| if (v == null || v === '') return 'β'; | |
| if (typeof v === 'number' && Number.isFinite(v)) { | |
| return precision == null ? String(v) : v.toFixed(precision); | |
| } | |
| return String(v); | |
| } | |
| /** | |
| * Generic renderer for the CONTRACT-2 table block: | |
| * { columns: [{ key, label, unit, precision }], rows: [{ <key>: value }] } | |
| * Column headers show "label Β· unit" when a unit is given; numeric columns | |
| * (any column with a precision, or whose first non-null value is a number) | |
| * right-align with tabular numerals. Replaces `host`'s previous table | |
| * (data-cf-table) so update() can just call it again. | |
| * Returns the wrapper element (or null when tableSpec is empty). | |
| */ | |
| export function renderTable(host, tableSpec) { | |
| host.querySelector(':scope > [data-cf-table]')?.remove(); | |
| if (!tableSpec || !Array.isArray(tableSpec.columns) || tableSpec.columns.length === 0) { | |
| return null; | |
| } | |
| const doc = host.ownerDocument; | |
| const rows = Array.isArray(tableSpec.rows) ? tableSpec.rows : []; | |
| const numeric = tableSpec.columns.map((c) => { | |
| if (c.precision != null) return true; | |
| const probe = rows.find((r) => r[c.key] != null); | |
| return probe ? typeof probe[c.key] === 'number' : false; | |
| }); | |
| const wrap = doc.createElement('div'); | |
| wrap.className = 'cf-table-wrap'; | |
| wrap.setAttribute('data-cf-table', ''); | |
| const table = doc.createElement('table'); | |
| table.className = 'cf-table'; | |
| const thead = doc.createElement('thead'); | |
| const hr = doc.createElement('tr'); | |
| tableSpec.columns.forEach((c, i) => { | |
| const th = doc.createElement('th'); | |
| th.textContent = c.unit ? `${c.label} Β· ${c.unit}` : (c.label ?? c.key); | |
| if (numeric[i]) th.classList.add('cf-num'); | |
| hr.appendChild(th); | |
| }); | |
| thead.appendChild(hr); | |
| const tbody = doc.createElement('tbody'); | |
| for (const row of rows) { | |
| const tr = doc.createElement('tr'); | |
| tableSpec.columns.forEach((c, i) => { | |
| const td = doc.createElement('td'); | |
| td.textContent = formatValue(row[c.key], c.precision); | |
| if (numeric[i]) td.classList.add('cf-num'); | |
| tr.appendChild(td); | |
| }); | |
| tbody.appendChild(tr); | |
| } | |
| table.append(thead, tbody); | |
| wrap.appendChild(table); | |
| host.appendChild(wrap); | |
| return wrap; | |
| } | |
| /** | |
| * Render the engine-emitted methodology block (CONTRACT-2 method): | |
| * { prose: string | string[], equations: string[], citations: [{key, apa}] } | |
| * Prose splits on blank lines into paragraphs (or takes an array as-is); | |
| * equations render as display lines; citations as an APA hanging-indent | |
| * list. Replaces `host`'s previous methodology block (data-cf-method). | |
| * Returns the block element (or null when methodBlock is empty). | |
| */ | |
| export function renderMethodology(host, methodBlock) { | |
| host.querySelector(':scope > [data-cf-method]')?.remove(); | |
| if (!methodBlock) return null; | |
| const doc = host.ownerDocument; | |
| const block = doc.createElement('div'); | |
| block.className = 'cf-method'; | |
| block.setAttribute('data-cf-method', ''); | |
| block.setAttribute('data-slot', 'method'); | |
| const heading = doc.createElement('h4'); | |
| heading.className = 'cf-method-heading'; | |
| heading.textContent = 'Methodology'; | |
| block.appendChild(heading); | |
| const paragraphs = Array.isArray(methodBlock.prose) | |
| ? methodBlock.prose | |
| : String(methodBlock.prose ?? '').split(/\n\s*\n/); | |
| for (const para of paragraphs) { | |
| const text = String(para).trim(); | |
| if (!text) continue; | |
| const p = doc.createElement('p'); | |
| p.textContent = text; | |
| block.appendChild(p); | |
| } | |
| for (const eq of methodBlock.equations ?? []) { | |
| const span = doc.createElement('span'); | |
| span.className = 'cf-eq'; | |
| span.textContent = eq; | |
| block.appendChild(span); | |
| } | |
| const citations = methodBlock.citations ?? []; | |
| if (citations.length) { | |
| const list = doc.createElement('ul'); | |
| list.className = 'cf-citations'; | |
| for (const c of citations) { | |
| const li = doc.createElement('li'); | |
| li.className = 'cf-citation'; | |
| if (c.key) li.setAttribute('data-litref', c.key); | |
| li.textContent = c.apa ?? c.key ?? ''; | |
| list.appendChild(li); | |
| } | |
| block.appendChild(list); | |
| } | |
| host.appendChild(block); | |
| return block; | |
| } | |
| /** | |
| * Render the engine-emitted results scaffold (payload.results) as a "Results" | |
| * block beneath the methodology, mirroring renderMethodology. resultsBlock is | |
| * { summary, findings[] } from engine/results.py β the stated numbers, so the | |
| * card shows both a Method and a Results section per chart. | |
| */ | |
| export function renderResults(host, resultsBlock) { | |
| host.querySelector(':scope > [data-cf-results]')?.remove(); | |
| if (!resultsBlock || !(resultsBlock.summary | |
| || (Array.isArray(resultsBlock.findings) && resultsBlock.findings.length))) { | |
| return null; | |
| } | |
| const doc = host.ownerDocument; | |
| const block = doc.createElement('div'); | |
| block.className = 'cf-results'; | |
| block.setAttribute('data-cf-results', ''); | |
| block.setAttribute('data-slot', 'results'); | |
| const heading = doc.createElement('h4'); | |
| heading.className = 'cf-results-heading'; | |
| heading.textContent = 'Results'; | |
| block.appendChild(heading); | |
| if (resultsBlock.summary) { | |
| const p = doc.createElement('p'); | |
| p.textContent = String(resultsBlock.summary); | |
| block.appendChild(p); | |
| } | |
| const findings = resultsBlock.findings ?? []; | |
| if (findings.length) { | |
| const list = doc.createElement('ul'); | |
| list.className = 'cf-findings'; | |
| for (const f of findings) { | |
| const li = doc.createElement('li'); | |
| li.className = 'cf-finding'; | |
| li.textContent = String(f); | |
| list.appendChild(li); | |
| } | |
| block.appendChild(list); | |
| } | |
| host.appendChild(block); | |
| return block; | |
| } | |