ModsCodeMERGEBLE LLC Search 3,678 mods

ModsCode / Gallery / Look

Look: 12 pane designs

Each design is one file of look, a mod by the maker of ModsCode (not one of the listed mods): a pane the desktop app draws. Sample data only: no feed, no keys, nothing sent. How a pane is built.

Candles 15m

Candles 15m
candle.mjs
// candle: a candlestick chart, the look the other designs follow. No frame and no axis
// lines; the body is 55% fill at 62% of a slot, the wick 1px at 80%, volume in the bottom 18% at 10%
// ink, and the last value is a dashed line at 35% ink that runs past the plot. Sample data only.
const W = 380
const H = 250
const PRICE = { top: 12, bottom: 178 }
const VOL = { top: 196, bottom: 236 }
const PAD = { left: 8, right: 52 } // the right space holds the value labels; the last line runs into it
const UP = '#7FD1A6'
const DOWN = '#F2837F'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"

// A seeded walk: the same picture every time, so it can be compared by eye
function sample(n) {
  let seed = 20261009
  const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  const bars = []
  let close = 100
  for (let i = 0; i < n; i++) {
    const open = close
    close = Math.max(60, open + (rnd() - 0.48) * 2.4)
    const high = Math.max(open, close) + rnd() * 0.9
    const low = Math.min(open, close) - rnd() * 0.9
    bars.push({ open, high, low, close, vol: 0.3 + rnd() })
  }
  return bars
}

function chartSource(bars) {
  const n = bars.length
  const lo = Math.min(...bars.map(b => b.low))
  const hi = Math.max(...bars.map(b => b.high))
  const vmax = Math.max(...bars.map(b => b.vol))
  const plotW = W - PAD.left - PAD.right
  const slot = plotW / n
  const body = Math.max(1, slot * 0.62)
  const y = v => PRICE.bottom - ((v - lo) / (hi - lo)) * (PRICE.bottom - PRICE.top)
  const vy = v => VOL.bottom - (v / vmax) * (VOL.bottom - VOL.top)

  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  bars.forEach((b, i) => {
    const cx = PAD.left + slot * (i + 0.5)
    const color = b.close >= b.open ? UP : DOWN
    const top = y(Math.max(b.open, b.close))
    const bottom = y(Math.min(b.open, b.close))
    parts.push(`<rect x="${cx - body / 2}" y="${vy(b.vol)}" width="${body}" height="${VOL.bottom - vy(b.vol)}" fill="${INK}" fill-opacity="0.1"/>`)
    parts.push(`<line x1="${cx}" x2="${cx}" y1="${y(b.high)}" y2="${y(b.low)}" stroke="${color}" stroke-opacity="0.8" stroke-width="1"/>`)
    parts.push(`<rect x="${cx - body / 2}" y="${top}" width="${body}" height="${Math.max(1, bottom - top)}" fill="${color}" fill-opacity="0.55"/>`)
  })

  const last = bars[n - 1]
  const ly = y(last.close)
  const lx = PAD.left + slot * (n - 0.5)
  parts.push(`<line x1="${lx}" x2="${W - 4}" y1="${ly}" y2="${ly}" stroke="${INK}" stroke-opacity="0.35" stroke-dasharray="3 3"/>`)
  parts.push(`<text x="${W - 4}" y="${ly - 4}" text-anchor="end" font-size="10" font-family="${FONT}" fill="${INK}">${last.close.toFixed(2)}</text>`)
  // The range labels step aside when the last value sits on them (a close near the high or low)
  const clear = yy => Math.abs(yy - ly) > 12
  if (clear(PRICE.top + 8)) parts.push(`<text x="${W - 4}" y="${PRICE.top + 8}" text-anchor="end" font-size="9" font-family="${FONT}" fill="${MUTED}">${hi.toFixed(2)}</text>`)
  if (clear(PRICE.bottom)) parts.push(`<text x="${W - 4}" y="${PRICE.bottom}" text-anchor="end" font-size="9" font-family="${FONT}" fill="${MUTED}">${lo.toFixed(2)}</text>`)
  parts.push(`<text x="${PAD.left}" y="${H - 4}" font-size="9" font-family="${FONT}" fill="${MUTED}">SAMPLE 15m</text>`)
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = chartSource(sample(48))

export default {
  id: 'candle',
  title: 'Candles 15m',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Candlestick chart with sample data', width: W, height: H }),
}

Sparklines, six rows

Sparklines, six rows
sparks.mjs
// sparks: six sparkline rows in candle's look. Each row is a 1px line over an area at 8% ink,
// a dot on the last value and the value right-aligned, so the level and its recent move read without
// axes. Each row has its own range, so a flat metric still shows its wiggle. Sample data only.
const W = 380
const H = 300
const STAGE = '#0F1216'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const TOP = 16
const ROW = 46
const PLOT = { left: 100, right: 296, top: 8, bottom: 32 } // offsets inside one row
const N = 60

// A seeded walk per metric: the same picture every time, so the rows can be compared by eye
function walk(seed, base, step, cap) {
  let s = seed
  const rnd = () => (s = (Math.imul(s, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  const out = []
  let v = base
  for (let i = 0; i < N; i++) {
    // Clamped so each metric stays plausible (a percentage cannot pass 100)
    v = Math.min(cap, Math.max(base * 0.5, v + (rnd() - 0.5) * step))
    out.push(v)
  }
  return out
}

const grouped = v => String(Math.round(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')

const METRICS = [
  { name: 'requests', seed: 11, base: 1280, step: 260, fmt: v => grouped(v) + '/m' },
  { name: 'errors', seed: 23, base: 0.42, step: 0.2, fmt: v => v.toFixed(2) + '%' },
  { name: 'latency', seed: 37, base: 212, step: 48, fmt: v => Math.round(v) + ' ms' },
  { name: 'queue', seed: 41, base: 17, step: 9, fmt: v => String(Math.round(v)) },
  { name: 'cache', seed: 53, base: 91.3, step: 3.4, cap: 99.4, fmt: v => v.toFixed(1) + '%' },
  { name: 'cost', seed: 67, base: 3.18, step: 0.7, fmt: v => '$' + v.toFixed(2) },
]

function row(m, i) {
  const vals = walk(m.seed, m.base, m.step, m.cap ?? Infinity)
  const lo = Math.min(...vals)
  const hi = Math.max(...vals)
  const top = TOP + i * ROW
  const y0 = top + PLOT.top
  const y1 = top + PLOT.bottom
  const x = k => PLOT.left + ((PLOT.right - PLOT.left) * k) / (N - 1)
  const y = v => y1 - ((v - lo) / (hi - lo || 1)) * (y1 - y0)
  const line = vals.map((v, k) => `${x(k).toFixed(1)},${y(v).toFixed(1)}`).join(' ')
  const last = vals[N - 1]
  const parts = [
    `<text x="12" y="${top + 20}" font-size="10" font-family="${FONT}" fill="${MUTED}">${m.name}</text>`,
    `<polygon points="${PLOT.left},${y1} ${line} ${PLOT.right},${y1}" fill="${INK}" fill-opacity="0.08"/>`,
    `<polyline points="${line}" fill="none" stroke="${INK}" stroke-opacity="0.85" stroke-width="1"/>`,
    `<circle cx="${x(N - 1).toFixed(1)}" cy="${y(last).toFixed(1)}" r="2" fill="${INK}"/>`,
    `<text x="${W - 12}" y="${top + 20}" text-anchor="end" font-size="11" font-family="${FONT}" fill="${INK}">${m.fmt(last)}</text>`,
  ]
  // A hairline between rows, not after the last one
  if (i < METRICS.length - 1) {
    parts.push(`<line x1="12" x2="${W - 12}" y1="${top + ROW - 6}" y2="${top + ROW - 6}" stroke="${INK}" stroke-opacity="0.08"/>`)
  }
  return parts.join('')
}

const SOURCE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>${METRICS.map(row).join('')}<text x="12" y="${H - 6}" font-size="9" font-family="${FONT}" fill="${MUTED}">SAMPLE 1h</text></svg>`

export default {
  id: 'sparks',
  title: 'Sparklines, six rows',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Six sparkline rows with their last values, sample data', width: W, height: H }),
}

Hourly bars around zero

Hourly bars around zero
bars.mjs
// bars: 24 hourly bars around a zero line in candle's look. Bars take candle's 62% slot and 55%
// fill, so the two designs read as one system. Up and down share one scale; the scale end is rounded
// up to a whole hundred so the three ticks are plain numbers. The largest bar carries its value.
// Sample data only.
const W = 380
const H = 260
const STAGE = '#0F1216'
const UP = '#7FD1A6'
const DOWN = '#F2837F'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const PLOT = { left: 44, right: 372, top: 16, bottom: 232 }
const N = 24

// A seeded walk of hourly values, biased a little to the plus side: the same picture every time
function sample(n) {
  let seed = 20261010
  const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  const out = []
  for (let i = 0; i < n; i++) out.push(Math.round((rnd() - 0.45) * 520))
  return out
}

function barsSource(vals) {
  const maxAbs = Math.max(...vals.map(v => Math.abs(v)))
  const T = Math.ceil((maxAbs * 1.12) / 100) * 100 // the scale end; the ticks sit at +T, 0 and -T
  const zeroY = (PLOT.top + PLOT.bottom) / 2
  const half = (PLOT.bottom - PLOT.top) / 2
  const slot = (PLOT.right - PLOT.left) / N
  const bw = slot * 0.62
  const y = v => zeroY - (v / T) * half
  const cx = i => PLOT.left + slot * (i + 0.5)
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]

  for (const [v, label] of [[T, `+${T}`], [0, '0'], [-T, `-${T}`]]) {
    parts.push(`<line x1="40" x2="${PLOT.left}" y1="${y(v)}" y2="${y(v)}" stroke="${MUTED}" stroke-width="1"/>`)
    parts.push(`<text x="36" y="${y(v) + 3}" text-anchor="end" font-size="9" font-family="${FONT}" fill="${MUTED}">${label}</text>`)
  }
  parts.push(`<line x1="${PLOT.left}" x2="${PLOT.right}" y1="${zeroY}" y2="${zeroY}" stroke="${INK}" stroke-opacity="0.35" stroke-width="1"/>`)

  let big = 0
  vals.forEach((v, i) => {
    if (Math.abs(v) > Math.abs(vals[big])) big = i
    const top = Math.min(y(v), zeroY)
    const h = Math.max(1, Math.abs(y(v) - zeroY))
    parts.push(`<rect x="${(cx(i) - bw / 2).toFixed(1)}" y="${top.toFixed(1)}" width="${bw.toFixed(1)}" height="${h.toFixed(1)}" fill="${v >= 0 ? UP : DOWN}" fill-opacity="0.55"/>`)
  })

  // The largest bar is labelled outside its tip: above a plus bar, below a minus bar
  const bv = vals[big]
  const ly = bv >= 0 ? y(bv) - 4 : y(bv) + 11
  parts.push(`<text x="${cx(big).toFixed(1)}" y="${ly.toFixed(1)}" text-anchor="middle" font-size="9" font-family="${FONT}" fill="${INK}">${bv > 0 ? '+' : ''}${bv}</text>`)

  for (const h of [0, 6, 12, 18]) {
    parts.push(`<text x="${cx(h).toFixed(1)}" y="${H - 8}" text-anchor="middle" font-size="9" font-family="${FONT}" fill="${MUTED}">${String(h).padStart(2, '0')}</text>`)
  }
  parts.push(`<text x="${W - 12}" y="${H - 8}" text-anchor="end" font-size="9" font-family="${FONT}" fill="${MUTED}">SAMPLE 1h</text>`)
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = barsSource(sample(N))

export default {
  id: 'bars',
  title: 'Hourly bars around zero',
  element: ui => ui.Svg({ source: SOURCE, alt: '24 hourly bars around a zero line, up and down, sample data', width: W, height: H }),
}

Allocation donut, five segments

Allocation donut, five segments
donut.mjs
// donut: an allocation ring in candle's look. Five segments are arc paths with a 2px gap between
// neighbours, measured along the mid-radius, so the gap stays even as the ring curves. The total sits
// in the centre and a legend on the right carries each share. Fills are ink steps with one amber
// accent, which keeps the palette to the candle set. Sample data only.
const W = 380
const H = 220
const STAGE = '#0F1216'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const AMBER = '#E3B66B'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const CX = 110
const CY = 110
const R_OUT = 84
const R_IN = 62
const R_MID = (R_OUT + R_IN) / 2
const GAP = 2 // px between two segments, along the mid-radius
const LX = 236 // legend left edge
const ROW = 22

// Shares in whole percent (they sum to 100), in ring order from the top
const SEGMENTS = [
  { name: 'Large cap', pct: 34, color: INK, opacity: 0.9 },
  { name: 'Mid cap', pct: 21, color: INK, opacity: 0.65 },
  { name: 'Bonds', pct: 19, color: INK, opacity: 0.42 },
  { name: 'Cash', pct: 16, color: INK, opacity: 0.22 },
  { name: 'Alt', pct: 10, color: AMBER, opacity: 0.9 },
]

const at = (r, a) => [CX + r * Math.cos(a), CY + r * Math.sin(a)]

// Half the gap comes off each end, so two neighbours are GAP apart on the mid-radius
function segPath(a0, a1) {
  const d = GAP / R_MID / 2
  const s = a0 + d
  const e = a1 - d
  const large = e - s > Math.PI ? 1 : 0
  const [xs, ys] = at(R_OUT, s)
  const [xe, ye] = at(R_OUT, e)
  const [xie, yie] = at(R_IN, e)
  const [xis, yis] = at(R_IN, s)
  return `M${xs.toFixed(2)} ${ys.toFixed(2)} A${R_OUT} ${R_OUT} 0 ${large} 1 ${xe.toFixed(2)} ${ye.toFixed(2)} L${xie.toFixed(2)} ${yie.toFixed(2)} A${R_IN} ${R_IN} 0 ${large} 0 ${xis.toFixed(2)} ${yis.toFixed(2)}Z`
}

function donutSource() {
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  let a = -Math.PI / 2 // start at 12 o'clock and go clockwise
  for (const seg of SEGMENTS) {
    const a1 = a + (seg.pct / 100) * 2 * Math.PI
    parts.push(`<path d="${segPath(a, a1)}" fill="${seg.color}" fill-opacity="${seg.opacity}"/>`)
    a = a1
  }

  parts.push(`<text x="${CX}" y="${CY + 2}" text-anchor="middle" font-size="14" font-family="${FONT}" fill="${INK}">2.48M</text>`)
  parts.push(`<text x="${CX}" y="${CY + 16}" text-anchor="middle" font-size="9" font-family="${FONT}" fill="${MUTED}">TOTAL</text>`)

  const top = CY - ((SEGMENTS.length - 1) * ROW) / 2
  SEGMENTS.forEach((seg, i) => {
    const y = top + i * ROW
    parts.push(`<rect x="${LX}" y="${y - 3}" width="6" height="6" fill="${seg.color}" fill-opacity="${seg.opacity}"/>`)
    parts.push(`<text x="${LX + 14}" y="${y + 4}" font-size="11" font-family="${FONT}" fill="${INK}">${seg.name}</text>`)
    parts.push(`<text x="${W - 12}" y="${y + 4}" text-anchor="end" font-size="11" font-family="${FONT}" fill="${INK}">${seg.pct}%</text>`)
  })
  parts.push(`<text x="12" y="${H - 8}" font-size="9" font-family="${FONT}" fill="${MUTED}">SAMPLE ALLOCATION</text>`)
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = donutSource()

export default {
  id: 'donut',
  title: 'Allocation donut, five segments',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Allocation donut with five segments, total in the centre, legend with percentages, sample data', width: W, height: H }),
}

Three gauges by threshold

Three gauges by threshold
gauge.mjs
// gauge: three semicircular dials in a row in candle's look. Ticks every 10%, a needle, a label
// above and the value under each. The colour follows a threshold (up below 60%, amber to 85%, down
// above), so the state reads before the number does. The value arc is 2px so the colour carries; every
// other stroke is 1px. Sample data only.
const W = 380
const H = 150
const STAGE = '#0F1216'
const UP = '#7FD1A6'
const DOWN = '#F2837F'
const AMBER = '#E3B66B'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const R = 46
const CY = 102
const CELL = W / 3

const DIALS = [
  { label: 'CPU', value: 0.42 },
  { label: 'MEMORY', value: 0.71 },
  { label: 'DISK', value: 0.91 },
]

// Fraction 0 is the left end, 1 the right end; the arc runs over the top, clockwise on screen
const at = (cx, r, f) => {
  const t = Math.PI * (1 - f)
  return [cx + r * Math.cos(t), CY - r * Math.sin(t)]
}
const pt = (cx, r, f) => at(cx, r, f).map(n => n.toFixed(2)).join(' ')

const colorFor = v => (v < 0.6 ? UP : v <= 0.85 ? AMBER : DOWN)

function dial(d, i) {
  const cx = CELL / 2 + i * CELL
  const v = d.value
  const color = colorFor(v)
  const parts = [
    `<text x="${cx}" y="${CY - R - 12}" text-anchor="middle" font-size="10" font-family="${FONT}" fill="${MUTED}">${d.label}</text>`,
    `<path d="M${pt(cx, R, 0)} A${R} ${R} 0 0 1 ${pt(cx, R, 1)}" fill="none" stroke="${INK}" stroke-opacity="0.2" stroke-width="1"/>`,
  ]
  for (let k = 0; k <= 10; k++) {
    const [x1, y1] = at(cx, R - 8, k / 10)
    const [x2, y2] = at(cx, R - 3, k / 10)
    parts.push(`<line x1="${x1.toFixed(2)}" y1="${y1.toFixed(2)}" x2="${x2.toFixed(2)}" y2="${y2.toFixed(2)}" stroke="${MUTED}" stroke-width="1"/>`)
  }
  parts.push(`<path d="M${pt(cx, R, 0)} A${R} ${R} 0 0 1 ${pt(cx, R, v)}" fill="none" stroke="${color}" stroke-width="2"/>`)
  const [nx, ny] = at(cx, R - 10, v)
  parts.push(`<line x1="${cx}" y1="${CY}" x2="${nx.toFixed(2)}" y2="${ny.toFixed(2)}" stroke="${INK}" stroke-width="1"/>`)
  parts.push(`<circle cx="${cx}" cy="${CY}" r="2" fill="${INK}"/>`)
  parts.push(`<text x="${cx - R}" y="${CY + 12}" text-anchor="middle" font-size="9" font-family="${FONT}" fill="${MUTED}">0</text>`)
  parts.push(`<text x="${cx + R}" y="${CY + 12}" text-anchor="middle" font-size="9" font-family="${FONT}" fill="${MUTED}">100</text>`)
  parts.push(`<text x="${cx}" y="${CY + 22}" text-anchor="middle" font-size="13" font-family="${FONT}" fill="${color}">${Math.round(v * 100)}%</text>`)
  return parts.join('')
}

const SOURCE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>${DIALS.map(dial).join('')}<text x="12" y="${H - 6}" font-size="9" font-family="${FONT}" fill="${MUTED}">SAMPLE usage</text></svg>`

export default {
  id: 'gauge',
  title: 'Three gauges by threshold',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Three semicircular gauges with ticks every 10 percent, coloured by threshold, sample data', width: W, height: H }),
}

Calendar heatmap, 26 weeks

Calendar heatmap, 26 weeks
heatmap.mjs
// heatmap: a calendar heatmap in candle's look, 26 weeks by 7 days with weeks as columns. Each cell
// is ink at an opacity from 0.06 to 0.85, so an idle day still keeps its place in the grid. The last
// cell is today (the only amber on the sheet) and the days after it are hollow outlines. Sample data only.
const W = 380
const H = 136
const CELL = 9
const PITCH = 11 // a 9px cell and a 2px gap
const X0 = 24
const Y0 = 24
const WEEKS = 26
const TODAY = { week: 25, day: 4 } // the sample runs Mon 2026-04-13 to Sun 2026-10-11; day 4 is Fri 10-09
// The week that holds the 1st of each month (the sample opens in April)
const MONTHS = [[0, 'APR'], [2, 'MAY'], [7, 'JUN'], [11, 'JUL'], [15, 'AUG'], [20, 'SEP'], [24, 'OCT']]
const INK = '#ECEAE6'
const MUTED = '#85827C'
const AMBER = '#E3B66B'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"

// The same ramp for the cells and the legend: 0.06 when idle, 0.85 when full
const ink = v => (0.06 + 0.79 * v).toFixed(2)

// A seeded calendar: weekdays busier than weekends, activity rising toward today, and some idle days
function sample() {
  let seed = 20261009
  const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  const weeks = []
  for (let w = 0; w < WEEKS; w++) {
    const days = []
    for (let d = 0; d < 7; d++) {
      const idle = rnd() < 0.12
      const busy = d < 5 ? 0.85 : 0.4
      days.push(idle ? 0 : Math.min(1, rnd() * busy + (0.15 * w) / (WEEKS - 1)))
    }
    weeks.push(days)
  }
  return weeks
}

function heatmapSource(weeks) {
  const label = (x, y, s, anchor = 'start') =>
    `<text x="${x}" y="${y}" text-anchor="${anchor}" font-size="9" font-family="${FONT}" fill="${MUTED}">${s}</text>`
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  for (const [w, name] of MONTHS) parts.push(label(X0 + w * PITCH, Y0 - 8, name))
  for (const [d, name] of [[0, 'M'], [2, 'W'], [4, 'F']]) parts.push(label(8, Y0 + d * PITCH + 8, name))
  weeks.forEach((days, w) => {
    days.forEach((v, d) => {
      const x = X0 + w * PITCH
      const y = Y0 + d * PITCH
      if (w === TODAY.week && d > TODAY.day) {
        // No data yet: an outline keeps the slot without reading as a zero
        parts.push(`<rect x="${x + 0.5}" y="${y + 0.5}" width="8" height="8" fill="none" stroke="${INK}" stroke-opacity="0.12"/>`)
      } else {
        parts.push(`<rect x="${x}" y="${y}" width="${CELL}" height="${CELL}" fill="${INK}" fill-opacity="${ink(v)}"/>`)
      }
    })
  })
  parts.push(`<rect x="${X0 + TODAY.week * PITCH + 0.5}" y="${Y0 + TODAY.day * PITCH + 0.5}" width="8" height="8" fill="none" stroke="${AMBER}" stroke-width="1"/>`)
  // The legend uses the same ramp, read from idle to full
  const STEPS = [0, 0.25, 0.5, 0.75, 1]
  parts.push(label(285, 123, 'LESS', 'end'))
  STEPS.forEach((v, i) => parts.push(`<rect x="${291 + i * PITCH}" y="116" width="${CELL}" height="${CELL}" fill="${INK}" fill-opacity="${ink(v)}"/>`))
  parts.push(label(372, 123, 'MORE', 'end'))
  parts.push(label(8, 123, `SAMPLE ${WEEKS} WEEKS`))
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = heatmapSource(sample())

export default {
  id: 'heatmap',
  title: 'Calendar heatmap, 26 weeks',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Calendar heatmap of 26 weeks of sample activity', width: W, height: H }),
}

Treemap, ten sectors

Treemap, ten sectors
treemap.mjs
// treemap: ten sample sectors in candle's look, laid out across the whole 380x300 pane by the squarified
// method so the cells stay close to square. Size is the value; fill is up or down at 25% by the sign of the
// change. Each cell is inset 1px, which leaves 2px gutters. A label appears only when its cell is big
// enough for it, since a clipped name reads worse than none. Sample data only.
const W = 380
const H = 300
const UP = '#7FD1A6'
const DOWN = '#F2837F'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const NAMES = ['Software', 'Semis', 'Banks', 'Energy', 'Pharma', 'Retail', 'Autos', 'Utilities', 'Telecom', 'Materials']

// A seeded set of sizes and signs: the same picture every time. A product of two draws gives many
// small sectors and a few large ones, which is closer to a real sector spread than a flat range.
function sample() {
  let seed = 20261009
  const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  return NAMES.map(name => ({ name, value: 20 + 180 * rnd() * rnd(), up: rnd() < 0.55 }))
}

// Worst aspect ratio of a strip laid along a side of length `side`
function worst(row, side) {
  const sum = row.reduce((s, r) => s + r.area, 0)
  const max = Math.max(...row.map(r => r.area))
  const min = Math.min(...row.map(r => r.area))
  return Math.max((side * side * max) / (sum * sum), (sum * sum) / (side * side * min))
}

// Squarified layout: keep adding to a strip while its worst ratio improves, then cut the strip along
// the shorter side. Items must already be sorted largest first.
function squarify(items, x, y, w, h) {
  const out = []
  let rest = items
  while (rest.length) {
    const side = Math.min(w, h)
    let n = 1
    while (n < rest.length && worst(rest.slice(0, n + 1), side) <= worst(rest.slice(0, n), side)) n++
    const row = rest.slice(0, n)
    rest = rest.slice(n)
    const sum = row.reduce((s, r) => s + r.area, 0)
    if (w >= h) {
      // The strip is a column on the left
      const cw = sum / h
      let cy = y
      for (const r of row) {
        const rh = r.area / cw
        out.push({ ...r, x, y: cy, w: cw, h: rh })
        cy += rh
      }
      x += cw
      w -= cw
    } else {
      // The strip is a row along the top
      const rh = sum / w
      let cx = x
      for (const r of row) {
        const cw = r.area / rh
        out.push({ ...r, x: cx, y, w: cw, h: rh })
        cx += cw
      }
      y += rh
      h -= rh
    }
  }
  return out
}

const f = n => n.toFixed(1)
const text = (x, y, s, size, fill) =>
  `<text x="${f(x)}" y="${f(y)}" font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`

function treemapSource(items) {
  const total = items.reduce((s, it) => s + it.value, 0)
  const scale = (W * H) / total
  const sorted = items.slice().sort((a, b) => b.value - a.value).map(it => ({ ...it, area: it.value * scale }))
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  for (const c of squarify(sorted, 0, 0, W, H)) {
    const left = c.x + 1
    const top = c.y + 1
    const cw = c.w - 2
    const ch = c.h - 2
    parts.push(`<rect x="${f(left)}" y="${f(top)}" width="${f(cw)}" height="${f(ch)}" fill="${c.up ? UP : DOWN}" fill-opacity="0.25" stroke="${INK}" stroke-opacity="0.12" stroke-width="1"/>`)
    // About 5.6px a character at 10px; the value line needs the taller cells
    if (cw >= c.name.length * 5.6 + 8 && ch >= 16) {
      parts.push(text(left + 6, top + 14, c.name, 10, INK))
      if (ch >= 32) parts.push(text(left + 6, top + 26, String(Math.round(c.value)), 9, MUTED))
    }
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = treemapSource(sample())

export default {
  id: 'treemap',
  title: 'Treemap, ten sectors',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Treemap of ten sample sectors: size is value, fill is up or down', width: W, height: H }),
}

Radar, six axes

Radar, six axes
radar.mjs
// radar: six axes in candle's look, with four 1px rings and six spokes at 12% ink. The newer series is an
// up-green fill at 25% with an up stroke; the older one is an ink line at 60%. Each vertex has a dot so a
// single value can be read off the chart. Axis names sit just outside the outer ring. Sample data only.
const W = 380
const H = 320
const CX = 190
const CY = 164
const R = 116
const AXES = ['Trend', 'Momentum', 'Volume', 'Volatility', 'Breadth', 'Value']
const UP = '#7FD1A6'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"

// A seeded pair of readings per axis, as fractions of the outer ring
function sample() {
  let seed = 20261009
  const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  const now = AXES.map(() => 0.35 + rnd() * 0.6)
  const last = AXES.map(() => 0.25 + rnd() * 0.6)
  return { now, last }
}

// Axis 0 points up and the rest run clockwise, so the first name sits at twelve o'clock
const angle = k => -Math.PI / 2 + (k * Math.PI) / 3
const f = n => n.toFixed(1)
const at = (k, r) => [CX + r * Math.cos(angle(k)), CY + r * Math.sin(angle(k))]
const shape = radii => radii.map((r, k) => at(k, r).map(f).join(',')).join(' ')
const text = (x, y, s, anchor, size, fill) =>
  `<text x="${f(x)}" y="${f(y)}" text-anchor="${anchor}" font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`
const dot = (x, y, fill, opacity) => `<circle cx="${f(x)}" cy="${f(y)}" r="2" fill="${fill}" fill-opacity="${opacity}"/>`

function radarSource({ now, last }) {
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  for (const share of [0.25, 0.5, 0.75, 1]) {
    parts.push(`<polygon points="${shape(AXES.map(() => R * share))}" fill="none" stroke="${INK}" stroke-opacity="0.12" stroke-width="1"/>`)
  }
  AXES.forEach((_, k) => {
    const [x, y] = at(k, R)
    parts.push(`<line x1="${CX}" y1="${CY}" x2="${f(x)}" y2="${f(y)}" stroke="${INK}" stroke-opacity="0.12" stroke-width="1"/>`)
  })
  const nowR = now.map(v => v * R)
  const lastR = last.map(v => v * R)
  parts.push(`<polygon points="${shape(nowR)}" fill="${UP}" fill-opacity="0.25" stroke="${UP}" stroke-width="1"/>`)
  parts.push(`<polygon points="${shape(lastR)}" fill="none" stroke="${INK}" stroke-opacity="0.6" stroke-width="1"/>`)
  nowR.forEach((r, k) => {
    const [x, y] = at(k, r)
    parts.push(dot(x, y, UP, 1))
  })
  lastR.forEach((r, k) => {
    const [x, y] = at(k, r)
    parts.push(dot(x, y, INK, 0.6))
  })
  AXES.forEach((name, k) => {
    const c = Math.cos(angle(k))
    const s = Math.sin(angle(k))
    const [x, y] = at(k, R + 12)
    // Names at twelve and six o'clock sit above or below their tip; the side names are centred on it
    const anchor = c > 0.2 ? 'start' : c < -0.2 ? 'end' : 'middle'
    const dy = s < -0.9 ? 0 : s > 0.9 ? 9 : 3.5
    parts.push(text(x, y + dy, name, anchor, 10, MUTED))
  })
  parts.push(text(8, 314, 'SAMPLE 6 AXES', 'start', 9, MUTED))
  parts.push(`<line x1="262" y1="310.5" x2="274" y2="310.5" stroke="${UP}" stroke-width="1"/>`)
  parts.push(text(278, 314, 'NOW', 'start', 9, UP))
  parts.push(`<line x1="304" y1="310.5" x2="316" y2="310.5" stroke="${INK}" stroke-opacity="0.6" stroke-width="1"/>`)
  parts.push(text(320, 314, 'LAST QTR', 'start', 9, MUTED))
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = radarSource(sample())

export default {
  id: 'radar',
  title: 'Radar, six axes',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Radar chart of six axes with two sample series', width: W, height: H }),
}

Gantt, 14 days

Gantt, 14 days
gantt.mjs
// gantt: a seven-task plan over 14 days in candle's look. Bars are 8px with a 2px corner; the done part
// is ink at 55% over a faint track. Each finish-to-start link is a 1px elbow at 35% ink that bends 6px
// before its target, and a successor always starts at least one free day after its predecessor ends, so
// no vertical leg runs through another bar. Sample plan only.
const W = 380
const H = 260
const X0 = 128 // the left edge of day 1; the task names sit in the column before it
const DAY = 17
const ROW0 = 40
const ROW = 28
const TODAY = 6
const INK = '#ECEAE6'
const MUTED = '#85827C'
const AMBER = '#E3B66B'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"

// Days are 1-based and inclusive. "after" is the row (index) of the task that must finish first.
const TASKS = [
  { name: 'Brief', start: 1, end: 2, done: 1 },
  { name: 'Research', start: 4, end: 6, done: 1, after: 0 },
  { name: 'Prototype', start: 4, end: 7, done: 0.6, after: 0 },
  { name: 'Design', start: 10, end: 12, done: 0.4, after: 1 },
  { name: 'Build', start: 9, end: 12, done: 0.2, after: 2 },
  { name: 'Test', start: 14, end: 14, done: 0, after: 4 },
  { name: 'Docs', start: 5, end: 8, done: 0 },
]

const dayX = d => X0 + (d - 1) * DAY // the left edge of day d
const midY = i => ROW0 + i * ROW + ROW / 2 // the centre line of row i
const f = n => n.toFixed(1)
const text = (x, y, s, anchor, size, fill) =>
  `<text x="${f(x)}" y="${f(y)}" text-anchor="${anchor}" font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`
const bar = (x, y, w, opacity) =>
  `<rect x="${f(x)}" y="${f(y)}" width="${f(w)}" height="8" rx="2" fill="${INK}" fill-opacity="${opacity}"/>`

function ganttSource() {
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  // Header: day numbers, with a brighter tick at each day edge and a faint column line below it
  parts.push(text(8, 30, 'TASK', 'start', 9, MUTED))
  for (let d = 1; d <= 14; d++) parts.push(text(dayX(d) + DAY / 2, 30, String(d), 'middle', 9, MUTED))
  for (let d = 1; d <= 15; d++) {
    const x = dayX(d)
    parts.push(`<line x1="${x}" y1="34" x2="${x}" y2="38" stroke="${INK}" stroke-opacity="0.3" stroke-width="1"/>`)
    parts.push(`<line x1="${x}" y1="38" x2="${x}" y2="236" stroke="${INK}" stroke-opacity="0.08" stroke-width="1"/>`)
  }
  parts.push(`<line x1="8" y1="38" x2="366" y2="38" stroke="${INK}" stroke-opacity="0.12" stroke-width="1"/>`)
  // Links first, so the bars sit on top of them
  TASKS.forEach((t, i) => {
    if (t.after === undefined) return
    const x1 = dayX(TASKS[t.after].end + 1) - 1 // the right edge of the earlier task
    const x2 = dayX(t.start) + 1 // the left edge of this one
    const bend = x2 - 6
    parts.push(`<path d="M${x1} ${midY(t.after)} H${bend} V${midY(i)} H${x2}" fill="none" stroke="${INK}" stroke-opacity="0.35" stroke-width="1"/>`)
  })
  TASKS.forEach((t, i) => {
    const x = dayX(t.start) + 1
    const w = (t.end - t.start + 1) * DAY - 2
    const y = midY(i) - 4
    parts.push(bar(x, y, w, 0.14))
    if (t.done > 0) parts.push(bar(x, y, w * t.done, 0.55))
    parts.push(text(8, midY(i) + 3.5, t.name, 'start', 10, INK))
  })
  // The today line runs over the bars so it stays visible where it crosses them
  const tx = dayX(TODAY) + DAY / 2 // the middle of day 6
  parts.push(`<line x1="${f(tx)}" y1="36" x2="${f(tx)}" y2="236" stroke="${AMBER}" stroke-width="1" stroke-dasharray="3 3"/>`)
  parts.push(text(tx, 252, 'TODAY', 'middle', 9, AMBER))
  parts.push(text(8, 252, 'SAMPLE 7 TASKS, 14 DAYS', 'start', 9, MUTED))
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

const SOURCE = ganttSource()

export default {
  id: 'gantt',
  title: 'Gantt, 14 days',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Gantt timeline of seven sample tasks over 14 days with a today line', width: W, height: H }),
}

Kanban board (sample)

Kanban board (sample)
kanban.mjs
// kanban: a three-column board in candle's look. Cards are 1px outlines at 18% ink with no fill,
// so the columns carry the structure; a done card gets a thin up-coloured stripe at its left edge.
// Static sample task titles, so every render is the same.
const W = 380
const H = 400
const COL = { w: 116, gap: 8, x0: 8 }
const HEAD = { text: 20, rule: 30, cards: 44 }
const CARD = { h: 54, gap: 8 }
const UP = '#7FD1A6'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const AMBER = '#E3B66B'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"

const COLUMNS = [
  { name: 'todo', cards: [['Price alerts', 'alerts'], ['CSV export', 'data'], ['Chart dark mode', 'ui'], ['Fill notices', 'feed'], ['Rate limit docs', 'docs']] },
  { name: 'doing', accent: true, cards: [['Order book depth', 'feed'], ['Retry on 429', 'api'], ['Sparkline cache', 'perf']] },
  { name: 'done', done: true, cards: [['Login screen', 'auth'], ['Candle store', 'data'], ['Settings page', 'ui'], ['Symbol search', 'ui']] },
]

// One column: a muted name and count over a hairline, then the cards down from HEAD.cards
function column(col, i) {
  const x = COL.x0 + i * (COL.w + COL.gap)
  const parts = [
    `<text x="${x}" y="${HEAD.text}" font-size="10" font-family="${FONT}" fill="${MUTED}">${col.name}</text>`,
    `<text x="${x + COL.w}" y="${HEAD.text}" text-anchor="end" font-size="10" font-family="${FONT}" fill="${col.accent ? AMBER : MUTED}">${col.cards.length}</text>`,
    `<line x1="${x}" x2="${x + COL.w}" y1="${HEAD.rule + 0.5}" y2="${HEAD.rule + 0.5}" stroke="${INK}" stroke-opacity="0.18"/>`,
  ]
  col.cards.forEach(([title, tag], k) => {
    const y = HEAD.cards + k * (CARD.h + CARD.gap)
    parts.push(`<rect x="${x + 0.5}" y="${y + 0.5}" width="${COL.w - 1}" height="${CARD.h - 1}" rx="3" fill="none" stroke="${INK}" stroke-opacity="0.18"/>`)
    // The stripe sits inside the border and stops short of the corners, so the rounded outline stays clean
    if (col.done) parts.push(`<rect x="${x + 1}" y="${y + 6}" width="2" height="${CARD.h - 12}" fill="${UP}"/>`)
    parts.push(`<text x="${x + 9}" y="${y + 21}" font-size="10" font-family="${FONT}" fill="${INK}">${title}</text>`)
    // The tag pill is sized from its character count; the font is not measured, so it is an estimate
    const pw = tag.length * 5 + 8
    parts.push(`<rect x="${x + 9.5}" y="${y + 28.5}" width="${pw}" height="13" rx="2" fill="none" stroke="${MUTED}" stroke-opacity="0.5"/>`)
    parts.push(`<text x="${x + 13}" y="${y + 38}" font-size="9" font-family="${FONT}" fill="${MUTED}">${tag}</text>`)
  })
  return parts.join('')
}

const SOURCE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>${COLUMNS.map(column).join('')}</svg>`

export default {
  id: 'kanban',
  title: 'Kanban board (sample)',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Kanban board with todo, doing and done columns and sample task cards', width: W, height: H }),
}

Stat tiles (sample)

Stat tiles (sample)
tiles.mjs
// tiles: six stat tiles in two rows of three, in candle's look. Each tile is a 1px outline at 14% ink
// with a muted label, a 22px value, a delta with a triangle drawn as a path (not a text glyph), and a
// 1px sparkline along the bottom. The sparklines come from a seeded walk, so the picture repeats.
const W = 380
const H = 204
const TILE = { w: 116, h: 90, gap: 8, margin: 8 }
const UP = '#7FD1A6'
const DOWN = '#F2837F'
const INK = '#ECEAE6'
const MUTED = '#85827C'
const AMBER = '#E3B66B'
const STAGE = '#0F1216'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"

const TILES = [
  { label: 'Equity', value: '12.48M', up: true, delta: '+2.4%', seed: 20261001, drift: 0.02 },
  { label: 'Day P/L', value: '+182K', up: true, delta: '+1.5%', seed: 20261002, drift: 0.02 },
  { label: 'Win rate', value: '58.3%', up: false, delta: '-0.8pt', seed: 20261003, drift: -0.02 },
  { label: 'Orders', value: '142', up: true, delta: '+12', seed: 20261004, drift: 0.01 },
  { label: 'Fills', value: '1,087', up: false, delta: '-4.1%', seed: 20261005, drift: -0.01 },
  { label: 'Drawdown', value: '-2.9%', up: true, delta: '+0.4pt', seed: 20261006, drift: 0.01, accent: true },
]

// A seeded walk with a drift, scaled to 0..1: the same line every time, and it leans with the delta
function walk(seed, n, drift) {
  let s = seed
  const rnd = () => (s = (Math.imul(s, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
  const out = []
  let v = 0.5
  for (let i = 0; i < n; i++) {
    v += (rnd() - 0.5) * 0.3 + drift
    out.push(v)
  }
  const lo = Math.min(...out)
  const hi = Math.max(...out)
  return out.map(x => (x - lo) / (hi - lo || 1))
}

function tile(t, i) {
  const x = TILE.margin + (i % 3) * (TILE.w + TILE.gap)
  const y = TILE.margin + Math.floor(i / 3) * (TILE.h + TILE.gap)
  const trend = t.up ? UP : DOWN
  const pts = walk(t.seed, 24, t.drift)
  // The sparkline band is y+66 to y+82, inset 8px from each side of the tile
  const d = pts.map((v, k) => {
    const px = x + 8 + ((TILE.w - 16) * k) / (pts.length - 1)
    const py = y + 82 - v * 16
    return `${k ? 'L' : 'M'}${px.toFixed(1)} ${py.toFixed(1)}`
  }).join('')
  const tri = t.up
    ? `M${x + 8} ${y + 58}L${x + 14} ${y + 58}L${x + 11} ${y + 51.5}Z`
    : `M${x + 8} ${y + 51.5}L${x + 14} ${y + 51.5}L${x + 11} ${y + 58}Z`
  return [
    `<rect x="${x + 0.5}" y="${y + 0.5}" width="${TILE.w - 1}" height="${TILE.h - 1}" rx="3" fill="none" stroke="${INK}" stroke-opacity="0.14"/>`,
    `<text x="${x + 8}" y="${y + 14}" font-size="9" font-family="${FONT}" fill="${MUTED}">${t.label}</text>`,
    `<text x="${x + 8}" y="${y + 42}" font-size="22" font-family="${FONT}" fill="${INK}">${t.value}</text>`,
    `<path d="${tri}" fill="${trend}"/>`,
    `<text x="${x + 18}" y="${y + 58}" font-size="9" font-family="${FONT}" fill="${trend}">${t.delta}</text>`,
    // Amber marks the drawdown line only: the one tile where a rise is a warning
    `<path d="${d}" fill="none" stroke="${t.accent ? AMBER : trend}" stroke-width="1" stroke-linejoin="round"/>`,
  ].join('')
}

const SOURCE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>${TILES.map(tile).join('')}</svg>`

export default {
  id: 'tiles',
  title: 'Stat tiles (sample)',
  element: ui => ui.Svg({ source: SOURCE, alt: 'Six stat tiles in two rows of three, each with a value, a delta and a sparkline', width: W, height: H }),
}

Table (text only)

Table (text only)
table.mjs
// table: Box and Text only, no Svg, so what gets judged is the pane's own text layout. Each row is a row
// of Text cells, and each cell is a string padded to its column width so the columns line up in a
// monospace face. Numbers are padded on the left (right-aligned). The owner column is dimmed as secondary.
const COLS = [
  { w: 14, right: false }, // name
  { w: 8, right: false }, // owner
  { w: 5, right: true }, // count
  { w: 8, right: true }, // change
  { w: 8, right: false }, // status
]
const GAP = 1 // the row Box gap; the rule below is sized to include it
const RULE = '-'.repeat(COLS.reduce((n, c) => n + c.w, 0) + GAP * (COLS.length - 1))

const HEAD = ['name', 'owner', 'count', 'change', 'status']
const ROWS = [
  ['mean-revert', 'mika', 14, '+1.24%', 'open'],
  ['breakout-2', 'ren', 6, '-0.37%', 'hold'],
  ['pairs-eth', 'mika', 22, '+0.08%', 'open'],
  ['vol-carry', 'sho', 3, '-2.11%', 'review'],
  ['gap-fill', 'kai', 9, '+0.66%', 'filled'],
  ['sector-rot', 'ren', 17, '+3.02%', 'open'],
  ['range-scalp', 'yui', 40, '-0.19%', 'paused'],
  ['news-drift', 'sho', 5, '+0.44%', 'review'],
  ['basis-arb', 'kai', 11, '-0.92%', 'hold'],
  ['trend-4h', 'mika', 8, '+1.71%', 'open'],
  ['opening-rng', 'yui', 4, '-1.08%', 'closed'],
  ['earnings-pm', 'ren', 2, '+0.27%', 'new'],
]

const pad = (s, col) => (col.right ? String(s).padStart(col.w) : String(s).padEnd(col.w))

// One table row: a Box of Text cells. dimAll dims the header; the owner column is always dimmed.
function row(ui, key, values, dimAll) {
  const { Box, Text } = ui
  return Box({
    key,
    flexDirection: 'row',
    gap: GAP,
    children: values.map((v, i) => Text({
      key: i,
      dimColor: dimAll || i === 1,
      children: [pad(v, COLS[i])],
    })),
  })
}

export default {
  id: 'table',
  title: 'Table (text only)',
  element: ui => {
    const { Box, Text } = ui
    return Box({
      flexDirection: 'column',
      children: [
        Text({ bold: true, children: ['Open positions (sample)'] }),
        row(ui, 'head', HEAD, true),
        Text({ dimColor: true, children: [RULE] }),
        ...ROWS.map((r, i) => row(ui, `r${i}`, r, false)),
      ],
    })
  },
}

Code: PolyForm Noncommercial License 1.0.0. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).