@miao-vision/cli 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -39673,7 +39673,7 @@ var require_xlsx = __commonJS({
39673
39673
  if (DBF_SUPPORTED_VERSIONS.indexOf(n[0]) > -1 && n[2] <= 12 && n[3] <= 31) return DBF.to_workbook(d, o);
39674
39674
  return read_prn(data, d, o, str);
39675
39675
  }
39676
- function readFileSync3(filename, opts) {
39676
+ function readFileSync4(filename, opts) {
39677
39677
  var o = opts || {};
39678
39678
  o.type = "file";
39679
39679
  return readSync(filename, o);
@@ -40509,8 +40509,8 @@ var require_xlsx = __commonJS({
40509
40509
  if (typeof parse_xlscfb !== "undefined") XLSX3.parse_xlscfb = parse_xlscfb;
40510
40510
  XLSX3.parse_zip = parse_zip;
40511
40511
  XLSX3.read = readSync;
40512
- XLSX3.readFile = readFileSync3;
40513
- XLSX3.readFileSync = readFileSync3;
40512
+ XLSX3.readFile = readFileSync4;
40513
+ XLSX3.readFileSync = readFileSync4;
40514
40514
  XLSX3.write = writeSync;
40515
40515
  XLSX3.writeFile = writeFileSync2;
40516
40516
  XLSX3.writeFileSync = writeFileSync2;
@@ -40541,8 +40541,8 @@ var require_xlsx = __commonJS({
40541
40541
  });
40542
40542
 
40543
40543
  // src/cli.ts
40544
- var import_node_fs2 = require("node:fs");
40545
- var import_node_path2 = require("node:path");
40544
+ var import_node_fs3 = require("node:fs");
40545
+ var import_node_path3 = require("node:path");
40546
40546
  var YAML = __toESM(require_dist(), 1);
40547
40547
 
40548
40548
  // src/data-loader.ts
@@ -41267,16 +41267,17 @@ var DEFAULT_SVG_THEME = {
41267
41267
  axisColor: "#94a3b8",
41268
41268
  labelColor: "#475569"
41269
41269
  };
41270
- function renderChartSvg(chart, rows, svgTheme) {
41270
+ function renderChartSvg(chart, rows, svgTheme, options = {}) {
41271
41271
  const theme = svgTheme ?? DEFAULT_SVG_THEME;
41272
41272
  const data = prepareChartData(rows, chart);
41273
- if (chart.type === "line" || chart.type === "area") return renderLineChart(chart, data, theme);
41274
- if (chart.type === "bar") return renderBarChart(chart, data, theme);
41275
- if (chart.type === "table") return renderTable(chart, data);
41273
+ if (chart.type === "line" || chart.type === "area") return renderLineChart(chart, data, theme, options);
41274
+ if (chart.type === "bar") return renderBarChart(chart, data, theme, options);
41275
+ if (chart.type === "pie") return renderPieChart(chart, data, theme, options);
41276
+ if (chart.type === "table") return renderTable(chart, data, options);
41276
41277
  if (chart.type === "bigvalue") return renderBigValue(chart, data);
41277
41278
  return renderUnsupported(chart);
41278
41279
  }
41279
- function renderBarChart(chart, rows, theme) {
41280
+ function renderBarChart(chart, rows, theme, options) {
41280
41281
  const xField = chart.encoding.x?.field ?? "";
41281
41282
  const yField = chart.encoding.y?.field ?? "";
41282
41283
  const width = numberStyle(chart, "width", 720);
@@ -41295,9 +41296,11 @@ function renderBarChart(chart, rows, theme) {
41295
41296
  const x = margin.left + index * (barWidth + barGap);
41296
41297
  const y = margin.top + chartHeight - barHeight;
41297
41298
  const color = theme.palette[index % theme.palette.length];
41299
+ const label = String(row[xField] ?? "");
41300
+ const tooltip = `${label}: ${value}`;
41298
41301
  return `<g>
41299
- <rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barWidth.toFixed(1)}" height="${barHeight.toFixed(1)}" rx="3" fill="${color}" />
41300
- <text x="${(x + barWidth / 2).toFixed(1)}" y="${(margin.top + chartHeight + 18).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="11">${escapeHtml(String(row[xField] ?? ""))}</text>
41302
+ <rect ${markAttrs(options.chartId, xField, row[xField], index, tooltip)} x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barWidth.toFixed(1)}" height="${barHeight.toFixed(1)}" rx="3" fill="${color}" />
41303
+ <text x="${(x + barWidth / 2).toFixed(1)}" y="${(margin.top + chartHeight + 18).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="11">${escapeHtml(label)}</text>
41301
41304
  </g>`;
41302
41305
  }).join("");
41303
41306
  return svgFrame(width, height, theme.background, `
@@ -41305,7 +41308,7 @@ function renderBarChart(chart, rows, theme) {
41305
41308
  ${bars}
41306
41309
  `);
41307
41310
  }
41308
- function renderLineChart(chart, rows, theme) {
41311
+ function renderLineChart(chart, rows, theme, options) {
41309
41312
  const xField = chart.encoding.x?.field ?? "";
41310
41313
  const yField = chart.encoding.y?.field ?? "";
41311
41314
  const width = numberStyle(chart, "width", 720);
@@ -41325,7 +41328,7 @@ function renderLineChart(chart, rows, theme) {
41325
41328
  });
41326
41329
  const path = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
41327
41330
  const dots = points.map(
41328
- (p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="4" fill="${lineColor}"><title>${escapeHtml(p.label)}: ${p.value}</title></circle>`
41331
+ (p) => `<circle ${markAttrs(options.chartId, xField, p.label, 0, `${p.label}: ${p.value}`)} cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="4" fill="${lineColor}"><title>${escapeHtml(p.label)}: ${p.value}</title></circle>`
41329
41332
  ).join("");
41330
41333
  const labels = points.map((p, i) => {
41331
41334
  if (i % Math.ceil(points.length / 8) !== 0) return "";
@@ -41338,11 +41341,42 @@ function renderLineChart(chart, rows, theme) {
41338
41341
  ${labels}
41339
41342
  `);
41340
41343
  }
41341
- function renderTable(chart, rows) {
41344
+ function renderPieChart(chart, rows, theme, options) {
41345
+ const labelField = chart.encoding.label?.field ?? "";
41346
+ const valueField = chart.encoding.value?.field ?? "";
41347
+ const width = numberStyle(chart, "width", 720);
41348
+ const height = numberStyle(chart, "height", 420);
41349
+ const cx = width / 2 - 80;
41350
+ const cy = height / 2;
41351
+ const radius = Math.min(width, height) * 0.34;
41352
+ const values = rows.map((row) => Math.max(0, Number(row[valueField]) || 0));
41353
+ const total = values.reduce((sum, value) => sum + value, 0) || 1;
41354
+ let angle = -Math.PI / 2;
41355
+ const slices = rows.map((row, index) => {
41356
+ const value = values[index];
41357
+ const nextAngle = angle + value / total * Math.PI * 2;
41358
+ const path = describeArc(cx, cy, radius, angle, nextAngle);
41359
+ const color = theme.palette[index % theme.palette.length];
41360
+ const label = String(row[labelField] ?? "");
41361
+ const tooltip = `${label}: ${value}`;
41362
+ angle = nextAngle;
41363
+ return `<path ${markAttrs(options.chartId, labelField, row[labelField], index, tooltip)} d="${path}" fill="${color}" stroke="${theme.background}" stroke-width="2" />`;
41364
+ }).join("");
41365
+ const legend = rows.map((row, index) => {
41366
+ const y = 72 + index * 24;
41367
+ return `<g>
41368
+ <rect x="${width - 210}" y="${y - 10}" width="10" height="10" fill="${theme.palette[index % theme.palette.length]}" />
41369
+ <text x="${width - 192}" y="${y}" fill="${theme.labelColor}" font-size="12">${escapeHtml(String(row[labelField] ?? ""))}</text>
41370
+ </g>`;
41371
+ }).join("");
41372
+ return svgFrame(width, height, theme.background, `${slices}${legend}`);
41373
+ }
41374
+ function renderTable(chart, rows, options) {
41342
41375
  const columns = Object.keys(rows[0] ?? {}).slice(0, 8);
41343
41376
  const header = columns.map((c) => `<th>${escapeHtml(c)}</th>`).join("");
41377
+ const markField = chart.encoding.label?.field ?? chart.encoding.x?.field ?? columns[0] ?? "";
41344
41378
  const body = rows.slice(0, 20).map(
41345
- (row) => `<tr>${columns.map((c) => `<td>${escapeHtml(String(row[c] ?? ""))}</td>`).join("")}</tr>`
41379
+ (row) => `<tr ${markAttrs(options.chartId, markField, row[markField], 0, String(row[markField] ?? "Row"))}>${columns.map((c) => `<td>${escapeHtml(String(row[c] ?? ""))}</td>`).join("")}</tr>`
41346
41380
  ).join("");
41347
41381
  return `<div class="miao-table-wrap"><table class="miao-table"><caption>${escapeHtml(chart.title ?? "Table")}</caption><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`;
41348
41382
  }
@@ -41360,6 +41394,33 @@ function svgFrame(width, height, bgColor, body) {
41360
41394
  ${body}
41361
41395
  </svg>`;
41362
41396
  }
41397
+ function markAttrs(chartId, field, value, rowKey, tooltip) {
41398
+ return [
41399
+ 'data-miao-mark="true"',
41400
+ chartId ? `data-chart-id="${escapeHtml(chartId)}"` : "",
41401
+ `data-field="${escapeHtml(field)}"`,
41402
+ `data-value="${escapeHtml(String(value ?? ""))}"`,
41403
+ `data-row-key="${escapeHtml(String(rowKey))}"`,
41404
+ `data-tooltip="${escapeHtml(tooltip)}"`
41405
+ ].filter(Boolean).join(" ");
41406
+ }
41407
+ function describeArc(cx, cy, radius, startAngle, endAngle) {
41408
+ const start = polarToCartesian(cx, cy, radius, endAngle);
41409
+ const end = polarToCartesian(cx, cy, radius, startAngle);
41410
+ const largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";
41411
+ return [
41412
+ `M ${cx.toFixed(1)} ${cy.toFixed(1)}`,
41413
+ `L ${start.x.toFixed(1)} ${start.y.toFixed(1)}`,
41414
+ `A ${radius.toFixed(1)} ${radius.toFixed(1)} 0 ${largeArcFlag} 0 ${end.x.toFixed(1)} ${end.y.toFixed(1)}`,
41415
+ "Z"
41416
+ ].join(" ");
41417
+ }
41418
+ function polarToCartesian(cx, cy, radius, angle) {
41419
+ return {
41420
+ x: cx + radius * Math.cos(angle),
41421
+ y: cy + radius * Math.sin(angle)
41422
+ };
41423
+ }
41363
41424
  function buildAxis(margin, chartWidth, chartHeight, xLabel, yLabel, yMin, yMax, theme) {
41364
41425
  const x0 = margin.left;
41365
41426
  const y0 = margin.top + chartHeight;
@@ -41604,6 +41665,441 @@ function getTheme(name) {
41604
41665
  return THEMES.default;
41605
41666
  }
41606
41667
 
41668
+ // src/interactive-runtime-assets.ts
41669
+ var INTERACTIVE_CSS = `
41670
+ .miao-interactive-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: end; margin: 0 0 24px; padding: 12px 0; border-top: 1px solid rgba(128,128,128,0.18); border-bottom: 1px solid rgba(128,128,128,0.18); }
41671
+ .miao-filter { display: grid; gap: 5px; font-size: 12px; }
41672
+ .miao-filter label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.56; }
41673
+ .miao-filter select, .miao-filter input { min-width: 140px; border: 1px solid rgba(128,128,128,0.28); border-radius: 4px; padding: 6px 8px; background: transparent; color: inherit; font: inherit; }
41674
+ .miao-filter-range { display: flex; gap: 6px; }
41675
+ .miao-reset { border: 1px solid rgba(128,128,128,0.28); border-radius: 4px; padding: 7px 10px; background: transparent; color: inherit; cursor: pointer; font: inherit; }
41676
+ .miao-reset:hover { background: rgba(128,128,128,0.08); }
41677
+ .miao-chart-svg [data-miao-mark] { cursor: pointer; transition: opacity 0.15s ease, stroke-width 0.15s ease; }
41678
+ .miao-chart-svg [data-miao-mark]:hover { opacity: 0.78; }
41679
+ .miao-chart-svg [data-miao-selected="true"] { stroke: currentColor; stroke-width: 2; }
41680
+ .miao-mark-hidden { opacity: 0.18; }
41681
+ .miao-detail { margin-top: 12px; overflow: auto; max-height: 320px; border: 1px solid rgba(128,128,128,0.18); border-radius: 4px; }
41682
+ .miao-detail-title { padding: 9px 10px; font-size: 12px; font-weight: 700; border-bottom: 1px solid rgba(128,128,128,0.18); }
41683
+ .miao-detail table { width: 100%; border-collapse: collapse; font-size: 12px; }
41684
+ .miao-detail th, .miao-detail td { padding: 7px 9px; border-bottom: 1px solid rgba(128,128,128,0.12); text-align: left; white-space: nowrap; }
41685
+ .miao-detail th { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.64; }
41686
+ .miao-tooltip { position: fixed; z-index: 9999; pointer-events: none; padding: 6px 8px; border-radius: 4px; background: rgba(20,20,19,0.92); color: #fff; font: 12px/1.35 system-ui, sans-serif; box-shadow: 0 8px 24px rgba(0,0,0,0.18); transform: translate(10px, 10px); }
41687
+ `;
41688
+ var INTERACTIVE_JS = `
41689
+ (function() {
41690
+ var specEl = document.getElementById('miao-viz-spec');
41691
+ var dataEl = document.getElementById('miao-viz-data');
41692
+ if (!specEl || !dataEl) return;
41693
+
41694
+ var spec = JSON.parse(specEl.textContent || '{}');
41695
+ var rows = JSON.parse(dataEl.textContent || '[]');
41696
+ var filters = (spec.interactions && spec.interactions.globalFilters) || [];
41697
+ var state = { filters: {}, selection: null };
41698
+ var tooltip = createTooltip();
41699
+
41700
+ function init() {
41701
+ renderControls();
41702
+ update();
41703
+ }
41704
+
41705
+ function renderControls() {
41706
+ if (!filters.length) return;
41707
+ var main = document.querySelector('.miao-viz-report');
41708
+ if (!main) return;
41709
+ var controls = document.createElement('section');
41710
+ controls.className = 'miao-interactive-controls';
41711
+ controls.setAttribute('aria-label', 'Interactive filters');
41712
+ filters.forEach(function(filter) {
41713
+ controls.appendChild(filter.type === 'range' ? renderRangeFilter(filter) : renderSelectFilter(filter));
41714
+ });
41715
+ var reset = document.createElement('button');
41716
+ reset.type = 'button';
41717
+ reset.className = 'miao-reset';
41718
+ reset.textContent = 'Reset';
41719
+ reset.addEventListener('click', function() {
41720
+ state.filters = {};
41721
+ state.selection = null;
41722
+ controls.querySelectorAll('select,input').forEach(function(input) { input.value = ''; });
41723
+ update();
41724
+ });
41725
+ controls.appendChild(reset);
41726
+ var header = main.querySelector('header');
41727
+ main.insertBefore(controls, header ? header.nextSibling : main.firstChild);
41728
+ }
41729
+
41730
+ function renderSelectFilter(filter) {
41731
+ var wrap = document.createElement('div');
41732
+ wrap.className = 'miao-filter';
41733
+ var label = document.createElement('label');
41734
+ label.textContent = filter.field;
41735
+ var select = document.createElement('select');
41736
+ select.innerHTML = '<option value="">All</option>' + uniqueValues(filter.field).map(function(value) {
41737
+ return '<option value="' + escapeAttr(value) + '">' + escapeHtml(value) + '</option>';
41738
+ }).join('');
41739
+ select.addEventListener('change', function() {
41740
+ state.filters[filter.field] = select.value;
41741
+ update();
41742
+ });
41743
+ wrap.appendChild(label);
41744
+ wrap.appendChild(select);
41745
+ return wrap;
41746
+ }
41747
+
41748
+ function renderRangeFilter(filter) {
41749
+ var wrap = document.createElement('div');
41750
+ wrap.className = 'miao-filter';
41751
+ var label = document.createElement('label');
41752
+ label.textContent = filter.field;
41753
+ var pair = document.createElement('div');
41754
+ pair.className = 'miao-filter-range';
41755
+ var min = document.createElement('input');
41756
+ var max = document.createElement('input');
41757
+ min.placeholder = 'Min';
41758
+ max.placeholder = 'Max';
41759
+ [min, max].forEach(function(input) {
41760
+ input.addEventListener('input', function() {
41761
+ state.filters[filter.field] = [min.value, max.value];
41762
+ update();
41763
+ });
41764
+ });
41765
+ pair.appendChild(min);
41766
+ pair.appendChild(max);
41767
+ wrap.appendChild(label);
41768
+ wrap.appendChild(pair);
41769
+ return wrap;
41770
+ }
41771
+
41772
+ function bindMarks() {
41773
+ document.querySelectorAll('[data-miao-mark]').forEach(function(mark) {
41774
+ var chart = chartSpec(mark.getAttribute('data-chart-id'));
41775
+ mark.addEventListener('mouseenter', function(event) {
41776
+ var text = mark.getAttribute('data-tooltip');
41777
+ if (!text) return;
41778
+ tooltip.textContent = text;
41779
+ tooltip.hidden = false;
41780
+ moveTooltip(event);
41781
+ });
41782
+ mark.addEventListener('mousemove', moveTooltip);
41783
+ mark.addEventListener('mouseleave', function() { tooltip.hidden = true; });
41784
+ mark.addEventListener('click', function() {
41785
+ if (!canSelect(chart)) return;
41786
+ var field = mark.getAttribute('data-field');
41787
+ var value = mark.getAttribute('data-value');
41788
+ if (!field) return;
41789
+ state.selection = state.selection && state.selection.field === field && String(state.selection.value) === String(value)
41790
+ ? null
41791
+ : { field: field, value: value };
41792
+ update();
41793
+ });
41794
+ });
41795
+ }
41796
+
41797
+ function update() {
41798
+ var filtered = applyFilters(rows);
41799
+ document.querySelectorAll('[data-miao-chart]').forEach(function(container) {
41800
+ var chart = chartSpec(container.getAttribute('data-miao-chart'));
41801
+ renderChart(container, chart, filtered);
41802
+ });
41803
+ bindMarks();
41804
+ document.querySelectorAll('[data-miao-mark]').forEach(function(mark) {
41805
+ var field = mark.getAttribute('data-field');
41806
+ var value = mark.getAttribute('data-value');
41807
+ var selected = state.selection && state.selection.field === field && String(state.selection.value) === String(value);
41808
+ mark.setAttribute('data-miao-selected', selected ? 'true' : 'false');
41809
+ mark.classList.toggle('miao-mark-hidden', Boolean((state.selection && !selected) || !markMatchesFilters(field, value)));
41810
+ });
41811
+ document.querySelectorAll('[data-miao-chart]').forEach(function(chart) {
41812
+ renderDetail(chart, filtered);
41813
+ });
41814
+ }
41815
+
41816
+ function renderChart(container, chart, sourceRows) {
41817
+ if (!chart || ['bar', 'pie', 'table'].indexOf(chart.type) === -1) return;
41818
+ var slot = container.querySelector('.miao-render-slot');
41819
+ if (!slot) return;
41820
+ var chartRows = prepareRows(sourceRows, chart);
41821
+ if (chart.type === 'bar') slot.innerHTML = renderBar(chart, chartRows, container.getAttribute('data-miao-chart'));
41822
+ else if (chart.type === 'pie') slot.innerHTML = renderPie(chart, chartRows, container.getAttribute('data-miao-chart'));
41823
+ else if (chart.type === 'table') slot.innerHTML = renderTable(chart, chartRows, container.getAttribute('data-miao-chart'));
41824
+ }
41825
+
41826
+ function prepareRows(sourceRows, chart) {
41827
+ return ((chart.data && chart.data.transform) || []).reduce(function(current, transform) {
41828
+ if (transform.type === 'derive-month' && transform.field && transform.as) {
41829
+ return current.map(function(row) {
41830
+ var copy = Object.assign({}, row);
41831
+ copy[transform.as] = toMonth(row[transform.field]);
41832
+ return copy;
41833
+ });
41834
+ }
41835
+ if (transform.type === 'aggregate') return aggregateRows(current, transform);
41836
+ if (transform.type === 'sort' && transform.field) {
41837
+ var order = transform.order === 'asc' ? 1 : -1;
41838
+ return current.slice().sort(function(a, b) {
41839
+ var an = Number(a[transform.field]);
41840
+ var bn = Number(b[transform.field]);
41841
+ if (Number.isFinite(an) && Number.isFinite(bn)) return (an - bn) * order;
41842
+ return String(a[transform.field] || '').localeCompare(String(b[transform.field] || '')) * order;
41843
+ });
41844
+ }
41845
+ if (transform.type === 'limit' && typeof transform.value === 'number') return current.slice(0, transform.value);
41846
+ return current;
41847
+ }, sourceRows.slice());
41848
+ }
41849
+
41850
+ function aggregateRows(sourceRows, transform) {
41851
+ var groupBy = transform.groupBy || [];
41852
+ var measures = transform.measures || [];
41853
+ var groups = new Map();
41854
+ sourceRows.forEach(function(row) {
41855
+ var key = JSON.stringify(groupBy.map(function(field) { return row[field]; }));
41856
+ var existing = groups.get(key) || [];
41857
+ existing.push(row);
41858
+ groups.set(key, existing);
41859
+ });
41860
+ return Array.from(groups.values()).map(function(groupRows) {
41861
+ var first = groupRows[0] || {};
41862
+ var out = {};
41863
+ groupBy.forEach(function(field) { out[field] = first[field]; });
41864
+ measures.forEach(function(measure) { out[measure.as] = aggregateMeasure(groupRows, measure); });
41865
+ return out;
41866
+ });
41867
+ }
41868
+
41869
+ function aggregateMeasure(sourceRows, measure) {
41870
+ if (measure.op === 'count') return sourceRows.length;
41871
+ var values = sourceRows.map(function(row) { return Number(row[measure.field]); }).filter(Number.isFinite);
41872
+ if (!values.length) return 0;
41873
+ if (measure.op === 'avg') return values.reduce(sum, 0) / values.length;
41874
+ if (measure.op === 'min') return Math.min.apply(null, values);
41875
+ if (measure.op === 'max') return Math.max.apply(null, values);
41876
+ return values.reduce(sum, 0);
41877
+ }
41878
+
41879
+ function renderBar(chart, chartRows, chartId) {
41880
+ var xField = field(chart, 'x');
41881
+ var yField = field(chart, 'y');
41882
+ var width = numberStyle(chart, 'width', 720);
41883
+ var height = numberStyle(chart, 'height', 420);
41884
+ var margin = { top: 24, right: 24, bottom: 48, left: 72 };
41885
+ var chartWidth = width - margin.left - margin.right;
41886
+ var chartHeight = height - margin.top - margin.bottom;
41887
+ var values = chartRows.map(function(row) { return Number(row[yField]); }).filter(Number.isFinite);
41888
+ var yMax = Math.max.apply(null, values.concat([1]));
41889
+ var gap = 8;
41890
+ var barWidth = Math.max(8, (chartWidth - gap * Math.max(chartRows.length - 1, 0)) / Math.max(chartRows.length, 1));
41891
+ var body = chartRows.map(function(row, index) {
41892
+ var value = Number(row[yField]) || 0;
41893
+ var barHeight = value / yMax * chartHeight;
41894
+ var x = margin.left + index * (barWidth + gap);
41895
+ var y = margin.top + chartHeight - barHeight;
41896
+ var label = String(row[xField] == null ? '' : row[xField]);
41897
+ return '<g><rect ' + markAttrs(chartId, xField, row[xField], index, label + ': ' + value) +
41898
+ ' x="' + fixed(x) + '" y="' + fixed(y) + '" width="' + fixed(barWidth) + '" height="' + fixed(barHeight) +
41899
+ '" rx="3" fill="' + color(index) + '" />' +
41900
+ '<text x="' + fixed(x + barWidth / 2) + '" y="' + fixed(margin.top + chartHeight + 18) +
41901
+ '" text-anchor="middle" fill="#475569" font-size="11">' + escapeHtml(label) + '</text></g>';
41902
+ }).join('');
41903
+ return svgFrame(width, height, body);
41904
+ }
41905
+
41906
+ function renderPie(chart, chartRows, chartId) {
41907
+ var labelField = field(chart, 'label');
41908
+ var valueField = field(chart, 'value');
41909
+ var width = numberStyle(chart, 'width', 720);
41910
+ var height = numberStyle(chart, 'height', 420);
41911
+ var cx = width / 2 - 80;
41912
+ var cy = height / 2;
41913
+ var radius = Math.min(width, height) * 0.34;
41914
+ var values = chartRows.map(function(row) { return Math.max(0, Number(row[valueField]) || 0); });
41915
+ var total = values.reduce(sum, 0) || 1;
41916
+ var angle = -Math.PI / 2;
41917
+ var slices = chartRows.map(function(row, index) {
41918
+ var value = values[index];
41919
+ var nextAngle = angle + value / total * Math.PI * 2;
41920
+ var path = describeArc(cx, cy, radius, angle, nextAngle);
41921
+ var label = String(row[labelField] == null ? '' : row[labelField]);
41922
+ angle = nextAngle;
41923
+ return '<path ' + markAttrs(chartId, labelField, row[labelField], index, label + ': ' + value) +
41924
+ ' d="' + path + '" fill="' + color(index) + '" stroke="#fff" stroke-width="2" />';
41925
+ }).join('');
41926
+ return svgFrame(width, height, slices);
41927
+ }
41928
+
41929
+ function renderTable(chart, chartRows, chartId) {
41930
+ var columns = Object.keys(chartRows[0] || rows[0] || {}).slice(0, 8);
41931
+ var markField = field(chart, 'label') || field(chart, 'x') || columns[0] || '';
41932
+ return '<div class="miao-table-wrap"><table class="miao-table"><thead><tr>' +
41933
+ columns.map(function(col) { return '<th>' + escapeHtml(col) + '</th>'; }).join('') +
41934
+ '</tr></thead><tbody>' + chartRows.slice(0, 20).map(function(row, index) {
41935
+ return '<tr ' + markAttrs(chartId, markField, row[markField], index, String(row[markField] || 'Row')) + '>' +
41936
+ columns.map(function(col) { return '<td>' + escapeHtml(row[col] == null ? '' : row[col]) + '</td>'; }).join('') +
41937
+ '</tr>';
41938
+ }).join('') + '</tbody></table></div>';
41939
+ }
41940
+
41941
+ function renderDetail(chart, filtered) {
41942
+ var chartId = chart.getAttribute('data-miao-chart');
41943
+ var detail = chart.querySelector('.miao-detail');
41944
+ if (!detail) {
41945
+ detail = document.createElement('div');
41946
+ detail.className = 'miao-detail';
41947
+ chart.appendChild(detail);
41948
+ }
41949
+ if (!state.selection) {
41950
+ detail.innerHTML = '';
41951
+ detail.hidden = true;
41952
+ return;
41953
+ }
41954
+ var selectedRows = filtered.filter(function(row) {
41955
+ return String(row[state.selection.field] == null ? '' : row[state.selection.field]) === String(state.selection.value);
41956
+ });
41957
+ detail.hidden = false;
41958
+ detail.innerHTML = detailTable(selectedRows, chartId);
41959
+ }
41960
+
41961
+ function applyFilters(source) {
41962
+ return source.filter(function(row) {
41963
+ return filters.every(function(filter) {
41964
+ var active = state.filters[filter.field];
41965
+ if (active == null || active === '' || (Array.isArray(active) && !active[0] && !active[1])) return true;
41966
+ if (filter.type === 'select') return String(row[filter.field] == null ? '' : row[filter.field]) === String(active);
41967
+ var current = comparable(row[filter.field]);
41968
+ if (current == null) return false;
41969
+ var min = comparable(active[0]);
41970
+ var max = comparable(active[1]);
41971
+ if (min != null && current < min) return false;
41972
+ if (max != null && current > max) return false;
41973
+ return true;
41974
+ });
41975
+ });
41976
+ }
41977
+
41978
+ function markMatchesFilters(field, value) {
41979
+ var activeFilters = filters.filter(function(filter) { return filter.field === field; });
41980
+ if (!activeFilters.length) return true;
41981
+ return activeFilters.every(function(filter) {
41982
+ var active = state.filters[filter.field];
41983
+ if (active == null || active === '' || (Array.isArray(active) && !active[0] && !active[1])) return true;
41984
+ if (filter.type === 'select') return String(value == null ? '' : value) === String(active);
41985
+ var current = comparable(value);
41986
+ if (current == null) return false;
41987
+ var min = comparable(active[0]);
41988
+ var max = comparable(active[1]);
41989
+ if (min != null && current < min) return false;
41990
+ if (max != null && current > max) return false;
41991
+ return true;
41992
+ });
41993
+ }
41994
+
41995
+ function chartSpec(chartId) {
41996
+ return (spec.charts || []).find(function(chart, index) { return (chart.id || ('chart-' + (index + 1))) === chartId; }) || null;
41997
+ }
41998
+ function canSelect(chart) { return Boolean(chart && ((chart.interaction && chart.interaction.select) || chart.drilldownPreset)); }
41999
+ function field(chart, channel) { return chart && chart.encoding && chart.encoding[channel] ? chart.encoding[channel].field : ''; }
42000
+ function numberStyle(chart, key, fallback) { return chart && chart.style && typeof chart.style[key] === 'number' ? chart.style[key] : fallback; }
42001
+
42002
+ function svgFrame(width, height, body) {
42003
+ return '<svg class="miao-chart-svg" viewBox="0 0 ' + width + ' ' + height + '" width="100%" height="' + height +
42004
+ '" role="img" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="' + width + '" height="' + height +
42005
+ '" fill="#fff" />' + body + '</svg>';
42006
+ }
42007
+
42008
+ function markAttrs(chartId, markField, value, rowKey, tooltipText) {
42009
+ return 'data-miao-mark="true" data-chart-id="' + escapeAttr(chartId || '') + '" data-field="' + escapeAttr(markField || '') +
42010
+ '" data-value="' + escapeAttr(value == null ? '' : value) + '" data-row-key="' + escapeAttr(rowKey) +
42011
+ '" data-tooltip="' + escapeAttr(tooltipText || '') + '"';
42012
+ }
42013
+
42014
+ function describeArc(cx, cy, radius, startAngle, endAngle) {
42015
+ var start = polarToCartesian(cx, cy, radius, endAngle);
42016
+ var end = polarToCartesian(cx, cy, radius, startAngle);
42017
+ var largeArc = endAngle - startAngle <= Math.PI ? '0' : '1';
42018
+ return 'M ' + fixed(cx) + ' ' + fixed(cy) + ' L ' + fixed(start.x) + ' ' + fixed(start.y) +
42019
+ ' A ' + fixed(radius) + ' ' + fixed(radius) + ' 0 ' + largeArc + ' 0 ' + fixed(end.x) + ' ' + fixed(end.y) + ' Z';
42020
+ }
42021
+
42022
+ function polarToCartesian(cx, cy, radius, angle) { return { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) }; }
42023
+ function toMonth(value) {
42024
+ var date = new Date(String(value));
42025
+ if (!Number.isFinite(date.getTime())) return String(value == null ? '' : value);
42026
+ return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
42027
+ }
42028
+ function color(index) { var palette = ['#2563eb', '#16a34a', '#f97316', '#dc2626', '#7c3aed', '#0891b2']; return palette[index % palette.length]; }
42029
+ function fixed(value) { return Number(value).toFixed(1); }
42030
+ function sum(a, b) { return a + b; }
42031
+
42032
+ function detailTable(selectedRows, chartId) {
42033
+ var visible = selectedRows.slice(0, 100);
42034
+ var columns = Object.keys(visible[0] || rows[0] || {}).slice(0, 8);
42035
+ if (!columns.length) return '<div class="miao-detail-title">No rows</div>';
42036
+ return '<div class="miao-detail-title">' + escapeHtml(chartId || 'Detail') + ': ' + selectedRows.length + ' rows</div>' +
42037
+ '<table><thead><tr>' + columns.map(function(col) { return '<th>' + escapeHtml(col) + '</th>'; }).join('') +
42038
+ '</tr></thead><tbody>' + visible.map(function(row) {
42039
+ return '<tr>' + columns.map(function(col) { return '<td>' + escapeHtml(row[col] == null ? '' : row[col]) + '</td>'; }).join('') + '</tr>';
42040
+ }).join('') + '</tbody></table>';
42041
+ }
42042
+
42043
+ function uniqueValues(field) {
42044
+ var seen = new Set();
42045
+ rows.forEach(function(row) {
42046
+ if (row[field] != null) seen.add(String(row[field]));
42047
+ });
42048
+ return Array.from(seen).slice(0, 200).sort();
42049
+ }
42050
+
42051
+ function comparable(value) {
42052
+ if (value == null || value === '') return null;
42053
+ var number = Number(value);
42054
+ if (Number.isFinite(number)) return number;
42055
+ var date = new Date(String(value)).getTime();
42056
+ return Number.isFinite(date) ? date : null;
42057
+ }
42058
+
42059
+ function createTooltip() {
42060
+ var el = document.createElement('div');
42061
+ el.className = 'miao-tooltip';
42062
+ el.hidden = true;
42063
+ document.body.appendChild(el);
42064
+ return el;
42065
+ }
42066
+
42067
+ function moveTooltip(event) {
42068
+ tooltip.style.left = event.clientX + 'px';
42069
+ tooltip.style.top = event.clientY + 'px';
42070
+ }
42071
+
42072
+ function escapeHtml(value) {
42073
+ return String(value).replace(/[&<>"']/g, function(char) {
42074
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char];
42075
+ });
42076
+ }
42077
+
42078
+ function escapeAttr(value) {
42079
+ return escapeHtml(value).replace(/\\n/g, ' ');
42080
+ }
42081
+
42082
+ init();
42083
+ })();
42084
+ `;
42085
+
42086
+ // src/interactive-runtime.ts
42087
+ function shouldEnableInteractiveRuntime(spec, options = {}) {
42088
+ if (options.enabled === true) return true;
42089
+ if (options.enabled === false) return false;
42090
+ if ((spec.interactions?.globalFilters?.length ?? 0) > 0) return true;
42091
+ return spec.charts.some((chart) => Boolean(chart.interaction || chart.drilldownPreset));
42092
+ }
42093
+ function renderInteractiveAssets(rows) {
42094
+ return `
42095
+ <script type="application/json" id="miao-viz-data">${escapeScriptJson(rows)}</script>
42096
+ <style>${INTERACTIVE_CSS}</style>
42097
+ <script>${INTERACTIVE_JS}</script>`;
42098
+ }
42099
+ function escapeScriptJson(value) {
42100
+ return JSON.stringify(value).replace(/</g, "\\u003c");
42101
+ }
42102
+
41607
42103
  // src/html-export.ts
41608
42104
  var INSIGHTS_CSS = `
41609
42105
  .report-insights { margin: 0 0 32px; padding: 16px 20px 14px; border-radius: 4px; border: 1px solid rgba(128,128,128,0.18); background: rgba(128,128,128,0.04); }
@@ -41611,9 +42107,10 @@ var INSIGHTS_CSS = `
41611
42107
  .insights-list { margin: 0; padding: 0 0 0 18px; }
41612
42108
  .insights-list li { margin: 5px 0; font-size: 13px; line-height: 1.55; opacity: 0.75; }
41613
42109
  `;
41614
- function renderStaticHtml(spec, profile, rows, themeOverride) {
42110
+ function renderStaticHtml(spec, profile, rows, themeOverride, interactiveOptions = {}) {
41615
42111
  const theme = getTheme(themeOverride ?? spec.theme);
41616
42112
  const title = spec.title ?? "Miao Vision Report";
42113
+ const interactive = shouldEnableInteractiveRuntime(spec, interactiveOptions);
41617
42114
  const header = theme.layout === "editorial" ? renderEditorialHeader(title, spec.description, profile) : renderDefaultHeader(title, spec.description, profile);
41618
42115
  const insights = spec.insights && spec.insights.length > 0 ? renderInsights(spec.insights) : "";
41619
42116
  let charts;
@@ -41629,16 +42126,18 @@ function renderStaticHtml(spec, profile, rows, themeOverride) {
41629
42126
  sections.push(renderKpiGroup(group, rows, theme));
41630
42127
  } else {
41631
42128
  const chart = spec.charts[i];
41632
- const svg = renderChartSvg(chart, rows, theme.svg);
41633
- sections.push(renderEditorialCard(chart, i, svg));
42129
+ const chartId = chartIdFor(chart, i);
42130
+ const svg = renderChartSvg(chart, rows, theme.svg, { chartId });
42131
+ sections.push(renderEditorialCard(chart, i, svg, chartId));
41634
42132
  i++;
41635
42133
  }
41636
42134
  }
41637
42135
  charts = sections.join("\n");
41638
42136
  } else {
41639
42137
  charts = spec.charts.map((chart, index) => {
41640
- const svg = renderChartSvg(chart, rows, theme.svg);
41641
- return renderDefaultCard(chart, index, svg);
42138
+ const chartId = chartIdFor(chart, index);
42139
+ const svg = renderChartSvg(chart, rows, theme.svg, { chartId });
42140
+ return renderDefaultCard(chart, index, svg, chartId);
41642
42141
  }).join("\n");
41643
42142
  }
41644
42143
  return `<!doctype html>
@@ -41657,6 +42156,7 @@ function renderStaticHtml(spec, profile, rows, themeOverride) {
41657
42156
  </main>
41658
42157
  <script type="application/json" id="miao-viz-spec">${escapeHtml(JSON.stringify(spec, null, 2))}</script>
41659
42158
  <script type="application/json" id="miao-viz-profile">${escapeHtml(JSON.stringify(profile, null, 2))}</script>
42159
+ ${interactive ? renderInteractiveAssets(rows) : ""}
41660
42160
  </body>
41661
42161
  </html>`;
41662
42162
  }
@@ -41684,11 +42184,11 @@ function renderEditorialHeader(title, description, profile) {
41684
42184
  </div>
41685
42185
  </header>`;
41686
42186
  }
41687
- function renderDefaultCard(chart, index, svg) {
42187
+ function renderDefaultCard(chart, index, svg, chartId) {
41688
42188
  const chartTitle = chart.title ?? `${chart.type} chart ${index + 1}`;
41689
- return `<section class="chart-block">
42189
+ return `<section class="chart-block" data-miao-chart="${escapeHtml(chartId)}">
41690
42190
  <h2>${escapeHtml(chartTitle)}</h2>
41691
- ${svg}
42191
+ <div class="miao-render-slot">${svg}</div>
41692
42192
  </section>`;
41693
42193
  }
41694
42194
  function renderKpiGroup(charts, rows, theme) {
@@ -41698,16 +42198,19 @@ function renderKpiGroup(charts, rows, theme) {
41698
42198
  <div class="kpi-grid">${items}</div>
41699
42199
  </section>`;
41700
42200
  }
41701
- function renderEditorialCard(chart, index, svg) {
42201
+ function renderEditorialCard(chart, index, svg, chartId) {
41702
42202
  const chartTitle = chart.title ?? `${chart.type} chart ${index + 1}`;
41703
42203
  const caption = buildCaption(chart);
41704
- return `<section class="chart-card">
42204
+ return `<section class="chart-card" data-miao-chart="${escapeHtml(chartId)}">
41705
42205
  <div class="chart-label">${escapeHtml(chart.type.toUpperCase())} CHART</div>
41706
42206
  <h2>${escapeHtml(chartTitle)}</h2>
41707
- ${svg}
42207
+ <div class="miao-render-slot">${svg}</div>
41708
42208
  ${caption ? `<p class="chart-caption">${escapeHtml(caption)}</p>` : ""}
41709
42209
  </section>`;
41710
42210
  }
42211
+ function chartIdFor(chart, index) {
42212
+ return chart.id ?? `chart-${index + 1}`;
42213
+ }
41711
42214
  function renderInsights(insights) {
41712
42215
  const items = insights.map((s) => `<li>${escapeHtml(s)}</li>`).join("\n ");
41713
42216
  return `<section class="report-insights">
@@ -56278,9 +56781,20 @@ var transformSchema = external_exports.object({
56278
56781
  order: external_exports.enum(["asc", "desc"]).optional(),
56279
56782
  value: external_exports.unknown().optional()
56280
56783
  });
56784
+ var globalFilterSchema = external_exports.object({
56785
+ field: external_exports.string().min(1),
56786
+ type: external_exports.enum(["select", "range"])
56787
+ });
56788
+ var chartInteractionSchema = external_exports.object({
56789
+ tooltip: external_exports.boolean().optional(),
56790
+ select: external_exports.enum(["filter", "detail"]).optional()
56791
+ });
56281
56792
  var chartSpecSchema = external_exports.object({
56793
+ id: external_exports.string().min(1).optional(),
56282
56794
  type: external_exports.enum(MVP_CHART_TYPES),
56283
56795
  title: external_exports.string().optional(),
56796
+ interaction: chartInteractionSchema.optional(),
56797
+ drilldownPreset: external_exports.enum(["category-detail"]).optional(),
56284
56798
  data: external_exports.object({
56285
56799
  source: external_exports.string().optional(),
56286
56800
  transform: external_exports.array(transformSchema).optional()
@@ -56299,6 +56813,9 @@ var reportSpecSchema = external_exports.object({
56299
56813
  title: external_exports.string().optional(),
56300
56814
  description: external_exports.string().optional(),
56301
56815
  theme: external_exports.enum(["default", "editorial", "dark", "minimal"]).optional(),
56816
+ interactions: external_exports.object({
56817
+ globalFilters: external_exports.array(globalFilterSchema).optional()
56818
+ }).optional(),
56302
56819
  insights: external_exports.array(external_exports.string()).optional(),
56303
56820
  charts: external_exports.array(chartSpecSchema).min(1)
56304
56821
  });
@@ -56332,6 +56849,7 @@ var REQUIRED_ENCODINGS = {
56332
56849
  table: [],
56333
56850
  bigvalue: ["value"]
56334
56851
  };
56852
+ var DRILLDOWN_CHART_TYPES = ["bar", "pie", "table"];
56335
56853
  function validateReportSpec(spec, profile, formats = ["html"]) {
56336
56854
  const parsed = reportSpecSchema.safeParse(spec);
56337
56855
  if (!parsed.success) {
@@ -56345,11 +56863,24 @@ function validateReportSpec(spec, profile, formats = ["html"]) {
56345
56863
  }
56346
56864
  }
56347
56865
  const availableFields = profile.columns.map((column) => column.name);
56866
+ const chartIds = /* @__PURE__ */ new Set();
56867
+ const interactionResult = validateReportInteractions(parsed.data, profile, availableFields);
56868
+ if (isAgentError(interactionResult)) return interactionResult;
56348
56869
  for (const chart of parsed.data.charts) {
56870
+ if (chart.id) {
56871
+ if (chartIds.has(chart.id)) {
56872
+ return agentError("DUPLICATE_CHART_ID", `Chart id '${chart.id}' is used more than once.`, {
56873
+ chartId: chart.id
56874
+ });
56875
+ }
56876
+ chartIds.add(chart.id);
56877
+ }
56349
56878
  const chartTypeResult = validateChartType(chart);
56350
56879
  if (isAgentError(chartTypeResult)) return chartTypeResult;
56351
56880
  const encodingResult = validateRequiredEncodings(chart);
56352
56881
  if (isAgentError(encodingResult)) return encodingResult;
56882
+ const chartInteractionResult = validateChartInteraction(chart);
56883
+ if (isAgentError(chartInteractionResult)) return chartInteractionResult;
56353
56884
  const derivedFields = collectDerivedFields(chart);
56354
56885
  const sourceFields = collectSourceFields(chart, derivedFields);
56355
56886
  for (const field of sourceFields) {
@@ -56392,6 +56923,46 @@ function validateRequiredEncodings(chart) {
56392
56923
  }
56393
56924
  return ok(chart);
56394
56925
  }
56926
+ function validateReportInteractions(spec, profile, availableFields) {
56927
+ for (const filter of spec.interactions?.globalFilters ?? []) {
56928
+ const column = profile.columns.find((candidate) => candidate.name === filter.field);
56929
+ if (!column) {
56930
+ return agentError("INTERACTION_FIELD_NOT_FOUND", `Interactive filter field '${filter.field}' was not found in the input data.`, {
56931
+ field: filter.field,
56932
+ availableFields
56933
+ });
56934
+ }
56935
+ if (filter.type === "range" && column.type !== "number" && column.type !== "date") {
56936
+ return agentError("INTERACTION_FILTER_TYPE_MISMATCH", `Range filter '${filter.field}' requires a number or date field.`, {
56937
+ field: filter.field,
56938
+ filterType: filter.type,
56939
+ columnType: column.type,
56940
+ supportedColumnTypes: ["number", "date"]
56941
+ });
56942
+ }
56943
+ }
56944
+ return ok(spec);
56945
+ }
56946
+ function validateChartInteraction(chart) {
56947
+ if (chart.drilldownPreset && chart.drilldownPreset !== "category-detail") {
56948
+ return agentError("UNSUPPORTED_DRILLDOWN_PRESET", `Drilldown preset '${chart.drilldownPreset}' is not supported.`, {
56949
+ supportedPresets: ["category-detail"]
56950
+ });
56951
+ }
56952
+ if (chart.drilldownPreset && !DRILLDOWN_CHART_TYPES.includes(chart.type)) {
56953
+ return agentError("UNSUPPORTED_DRILLDOWN_CHART_TYPE", `Drilldown preset '${chart.drilldownPreset}' is not supported for chart type '${chart.type}'.`, {
56954
+ chartType: chart.type,
56955
+ supportedChartTypes: DRILLDOWN_CHART_TYPES
56956
+ });
56957
+ }
56958
+ if (chart.interaction?.select && !DRILLDOWN_CHART_TYPES.includes(chart.type)) {
56959
+ return agentError("UNSUPPORTED_INTERACTION_CHART_TYPE", `Chart interaction select '${chart.interaction.select}' is not supported for chart type '${chart.type}'.`, {
56960
+ chartType: chart.type,
56961
+ supportedChartTypes: DRILLDOWN_CHART_TYPES
56962
+ });
56963
+ }
56964
+ return ok(chart);
56965
+ }
56395
56966
  function collectSourceFields(chart, derivedFields) {
56396
56967
  const fields = /* @__PURE__ */ new Set();
56397
56968
  for (const encoding of Object.values(chart.encoding)) {
@@ -56601,8 +57172,8 @@ var SLIDE_CSS = `
56601
57172
 
56602
57173
  /* \u2500\u2500 Shared slide elements \u2500\u2500 */
56603
57174
  .slide-eyebrow { font-family: var(--mv-mono); font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 3px; color: var(--mv-muted); margin-bottom: 14px; }
56604
- .slide-title { font-family: var(--mv-serif); font-size: 50px; font-weight: 500; line-height: 1.1; letter-spacing: -0.6px; color: var(--mv-ink); margin-bottom: 24px; }
56605
- .slide-claim { font-family: var(--mv-serif); font-size: 21px; line-height: 1.45; color: var(--mv-soft); margin-bottom: 18px; max-width: 52ch; }
57175
+ .slide-title { font-family: var(--mv-serif); font-size: 50px; font-weight: 500; line-height: 1.1; letter-spacing: -0.6px; color: var(--mv-ink); margin-bottom: 24px; max-width: 19ch; overflow-wrap: anywhere; }
57176
+ .slide-claim { font-family: var(--mv-serif); font-size: 21px; line-height: 1.45; color: var(--mv-soft); margin-bottom: 18px; max-width: 52ch; overflow-wrap: anywhere; }
56606
57177
  .slide-pts { list-style: none; counter-reset: pts; }
56607
57178
  .slide-pts li { counter-increment: pts; font-size: 16px; line-height: 1.55; color: var(--mv-soft); padding-left: 22px; position: relative; margin-bottom: 10px; }
56608
57179
  .slide-pts li::before { content: counter(pts) "."; position: absolute; left: 0; color: var(--mv-brand); font-weight: 500; font-family: var(--mv-mono); font-size: 13px; }
@@ -56613,7 +57184,7 @@ var SLIDE_CSS = `
56613
57184
  .slide-chart-full svg, .slide-chart-full .miao-table-wrap { width: 100%; }
56614
57185
 
56615
57186
  /* \u2500\u2500 Metrics \u2500\u2500 */
56616
- .slide-metrics { display: flex; gap: 0; padding-top: 14px; border-top: 0.5px dotted var(--mv-border); margin-bottom: 20px; }
57187
+ .slide-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; padding-top: 14px; border-top: 0.5px dotted var(--mv-border); margin-bottom: 20px; }
56617
57188
  .slide-metric { flex: 1; display: flex; flex-direction: column; gap: 5px; padding-right: 24px; }
56618
57189
  .slide-metric .v { font-family: var(--mv-serif); font-size: 42px; font-weight: 500; color: var(--mv-brand); line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -0.5px; }
56619
57190
  .slide-metric .l { font-family: var(--mv-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--mv-muted); }
@@ -56621,8 +57192,8 @@ var SLIDE_CSS = `
56621
57192
  /* \u2500\u2500 Cover \u2500\u2500 */
56622
57193
  .slide-cover { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; height: 100%; }
56623
57194
  .slide-cover-left { display: flex; flex-direction: column; }
56624
- .slide-cover h1 { font-family: var(--mv-serif); font-size: 56px; font-weight: 500; line-height: 1.05; letter-spacing: -1px; color: var(--mv-ink); margin-bottom: 14px; }
56625
- .slide-cover .sub { font-size: 18px; color: var(--mv-soft); line-height: 1.5; margin-bottom: 22px; max-width: 34ch; }
57195
+ .slide-cover h1 { font-family: var(--mv-serif); font-size: 56px; font-weight: 500; line-height: 1.05; letter-spacing: -1px; color: var(--mv-ink); margin-bottom: 14px; max-width: 12ch; overflow-wrap: anywhere; }
57196
+ .slide-cover .sub { font-size: 18px; color: var(--mv-soft); line-height: 1.5; margin-bottom: 22px; max-width: 34ch; overflow-wrap: anywhere; }
56626
57197
  .slide-cover .line { width: 48px; height: 2px; background: var(--mv-brand); margin-bottom: 14px; }
56627
57198
  .slide-cover .meta { font-family: var(--mv-mono); font-size: 11px; color: var(--mv-muted); letter-spacing: 0.5px; }
56628
57199
  .slide-cover-right { display: flex; align-items: center; justify-content: center; }
@@ -56643,6 +57214,8 @@ var SLIDE_CSS = `
56643
57214
 
56644
57215
  /* \u2500\u2500 Table styles inside slide \u2500\u2500 */
56645
57216
  .miao-table { width: 100%; border-collapse: collapse; font-size: 13px; }
57217
+ .miao-table-wrap { max-height: 430px; overflow: hidden; }
57218
+ .miao-table caption { caption-side: top; text-align: left; padding: 0 0 10px; color: var(--mv-muted); font-family: var(--mv-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
56646
57219
  .miao-table th { border-bottom: 1px solid var(--mv-border); padding: 7px 12px; text-align: left; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--mv-muted); font-family: var(--mv-mono); }
56647
57220
  .miao-table td { border-bottom: 1px solid var(--mv-border); padding: 8px 12px; color: var(--mv-soft); font-variant-numeric: tabular-nums; }
56648
57221
  .miao-table tbody tr:last-child td { border-bottom: none; }
@@ -56682,8 +57255,8 @@ var SLIDE_JS = `
56682
57255
  if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goTo(current + 1); }
56683
57256
  else if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(current - 1); }
56684
57257
  else if (e.key === 'f' || e.key === 'F') {
56685
- if (!document.fullscreenElement) document.documentElement.requestFullscreen();
56686
- else document.exitFullscreen();
57258
+ if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(function() {});
57259
+ else document.exitFullscreen().catch(function() {});
56687
57260
  }
56688
57261
  });
56689
57262
 
@@ -56694,8 +57267,8 @@ var SLIDE_JS = `
56694
57267
  if (btnPrev) btnPrev.addEventListener('click', function() { goTo(current - 1); });
56695
57268
  if (btnNext) btnNext.addEventListener('click', function() { goTo(current + 1); });
56696
57269
  if (btnFs) btnFs.addEventListener('click', function() {
56697
- if (!document.fullscreenElement) document.documentElement.requestFullscreen();
56698
- else document.exitFullscreen();
57270
+ if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(function() {});
57271
+ else document.exitFullscreen().catch(function() {});
56699
57272
  });
56700
57273
  if (btnPrint) btnPrint.addEventListener('click', function() {
56701
57274
  document.body.classList.remove('present-mode');
@@ -56977,8 +57550,310 @@ function hintForIssue(path, message) {
56977
57550
  return `Check ${path} in the DeckSpec.`;
56978
57551
  }
56979
57552
 
57553
+ // src/article-infographic.ts
57554
+ var import_node_fs2 = require("node:fs");
57555
+ var import_node_path2 = require("node:path");
57556
+ var ARTICLE_STYLES = ["editorial", "executive", "minimal"];
57557
+ var ARTICLE_FORMATS = ["html", "json", "markdown"];
57558
+ var DATE_PATTERN = /\b(?:\d{4}(?:[-/]\d{1,2}(?:[-/]\d{1,2})?)?|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4})\b/i;
57559
+ var NUMBER_PATTERN = /(?:[$¥€]\s?\d[\d,.]*|\b\d+(?:\.\d+)?%?\b)/;
57560
+ function parseArticleStyle(value) {
57561
+ if (!value) return "editorial";
57562
+ return ARTICLE_STYLES.includes(value) ? value : void 0;
57563
+ }
57564
+ function parseArticleFormat(value) {
57565
+ if (!value) return "html";
57566
+ return ARTICLE_FORMATS.includes(value) ? value : void 0;
57567
+ }
57568
+ function generateInfographicFromFile(file2, style) {
57569
+ const extension = (0, import_node_path2.extname)(file2).toLowerCase();
57570
+ if (extension && ![".md", ".markdown", ".txt"].includes(extension)) {
57571
+ return agentError("UNSUPPORTED_ARTICLE_INPUT", "Article input must be a Markdown or plain-text file.", {
57572
+ supportedExtensions: [".md", ".markdown", ".txt"]
57573
+ });
57574
+ }
57575
+ let raw;
57576
+ try {
57577
+ raw = (0, import_node_fs2.readFileSync)(file2, "utf8");
57578
+ } catch (error51) {
57579
+ return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Article input could not be read.", {
57580
+ file: file2
57581
+ });
57582
+ }
57583
+ const normalized = normalizeArticleText(raw);
57584
+ if (!normalized) {
57585
+ return agentError("EMPTY_ARTICLE_INPUT", "Article input is empty after normalization.", { file: file2 });
57586
+ }
57587
+ const parsed = parseArticle(normalized, file2);
57588
+ const spec = buildInfographicSpec(parsed, style, file2);
57589
+ return ok({ spec, markdown: renderInfographicMarkdown(spec) });
57590
+ }
57591
+ function normalizeArticleText(raw) {
57592
+ return raw.replace(/\r\n/g, "\n").replace(/\t/g, " ").split("\n").map((line) => line.replace(/[ \u00a0]+$/g, "")).join("\n").trim();
57593
+ }
57594
+ function parseArticle(text, file2) {
57595
+ const lines = text.split("\n");
57596
+ const metadata = extractMetadata(lines);
57597
+ const title = findTitle(lines) ?? titleFromFilename(file2);
57598
+ const contentLines = lines.filter((line) => {
57599
+ const trimmed = line.trim();
57600
+ return trimmed !== title && !trimmed.match(/^#\s+/) && !trimmed.match(/^(source|url|author|date):\s*/i);
57601
+ });
57602
+ const quotes = contentLines.filter((line) => line.trim().startsWith(">")).map((line) => cleanMarkdown(line.replace(/^>\s?/, ""))).filter(Boolean);
57603
+ const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) => cleanMarkdown(line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""))).filter(Boolean);
57604
+ const tableRows = extractTableRows(contentLines);
57605
+ const paragraphs = contentLines.join("\n").split(/\n{2,}/).map((block) => cleanMarkdown(block.replace(/\n/g, " "))).filter((block) => block.length > 0 && !block.startsWith("|") && !block.match(/^[-*+]\s+/));
57606
+ return {
57607
+ title,
57608
+ subtitle: metadata.subtitle ?? firstUsefulParagraph(paragraphs),
57609
+ source: metadata.source,
57610
+ paragraphs,
57611
+ listItems,
57612
+ quotes,
57613
+ tableRows
57614
+ };
57615
+ }
57616
+ function buildInfographicSpec(parsed, style, file2) {
57617
+ const evidence = [...parsed.listItems, ...sentences(parsed.paragraphs.join(" "))];
57618
+ const facts = collectFacts(evidence);
57619
+ const timeline = collectTimeline(evidence);
57620
+ const comparison = collectComparison(evidence, parsed.tableRows);
57621
+ const takeaways = collectTakeaways(evidence, facts);
57622
+ const summary = parsed.subtitle ?? takeaways[0]?.text ?? facts[0]?.text ?? "A concise visual summary of the source article.";
57623
+ const sections = [
57624
+ {
57625
+ type: "hero",
57626
+ title: parsed.title,
57627
+ emphasis: summary,
57628
+ items: [{ text: summary }]
57629
+ }
57630
+ ];
57631
+ if (facts.length > 0) sections.push({ type: "facts", title: "Key Facts", items: facts.slice(0, 6) });
57632
+ if (timeline.length > 1) sections.push({ type: "timeline", title: "Timeline", items: timeline.slice(0, 6) });
57633
+ if (comparison.length > 1) sections.push({ type: "comparison", title: "Comparison", items: comparison.slice(0, 6) });
57634
+ if (parsed.quotes.length > 0) {
57635
+ sections.push({
57636
+ type: "quote",
57637
+ title: "Notable Quote",
57638
+ emphasis: parsed.quotes[0],
57639
+ items: parsed.quotes.slice(0, 3).map((text) => ({ text }))
57640
+ });
57641
+ }
57642
+ if (takeaways.length > 0) sections.push({ type: "takeaways", title: "Takeaways", items: takeaways.slice(0, 5) });
57643
+ return {
57644
+ title: parsed.title,
57645
+ subtitle: parsed.subtitle,
57646
+ source: parsed.source,
57647
+ style,
57648
+ summary,
57649
+ sections,
57650
+ metadata: {
57651
+ inputFile: file2,
57652
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
57653
+ wordCount: parsed.paragraphs.join(" ").split(/\s+/).filter(Boolean).length
57654
+ }
57655
+ };
57656
+ }
57657
+ function renderInfographicMarkdown(spec) {
57658
+ const lines = [`# ${spec.title}`, ""];
57659
+ if (spec.subtitle) lines.push(spec.subtitle, "");
57660
+ if (spec.source) lines.push(`Source: ${spec.source}`, "");
57661
+ lines.push(`Style: ${spec.style}`, "", `## Summary`, "", spec.summary, "");
57662
+ for (const section of spec.sections.filter((section2) => section2.type !== "hero")) {
57663
+ lines.push(`## ${section.title}`, "");
57664
+ for (const item of section.items) {
57665
+ const prefix = item.label ? `**${item.label}:** ` : "";
57666
+ lines.push(`- ${prefix}${item.value ? `${item.value} \u2014 ` : ""}${item.text}`);
57667
+ }
57668
+ lines.push("");
57669
+ }
57670
+ return lines.join("\n").trimEnd() + "\n";
57671
+ }
57672
+ function extractMetadata(lines) {
57673
+ const sourceLine = lines.find((line) => line.match(/^(source|url):\s*/i));
57674
+ const subtitleLine = lines.find((line) => line.match(/^subtitle:\s*/i));
57675
+ return {
57676
+ source: sourceLine?.replace(/^(source|url):\s*/i, "").trim(),
57677
+ subtitle: subtitleLine?.replace(/^subtitle:\s*/i, "").trim()
57678
+ };
57679
+ }
57680
+ function findTitle(lines) {
57681
+ const heading = lines.find((line) => line.trim().match(/^#\s+\S/));
57682
+ if (heading) return cleanMarkdown(heading.replace(/^#\s+/, ""));
57683
+ const first = lines.find((line) => line.trim() && !line.match(/^(source|url|author|date):\s*/i));
57684
+ return first ? cleanMarkdown(first).slice(0, 120) : void 0;
57685
+ }
57686
+ function titleFromFilename(file2) {
57687
+ return (0, import_node_path2.basename)(file2, (0, import_node_path2.extname)(file2)).replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
57688
+ }
57689
+ function extractTableRows(lines) {
57690
+ return lines.filter((line) => line.includes("|") && !line.match(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/)).map((line) => line.split("|").map((cell) => cleanMarkdown(cell)).filter(Boolean)).filter((row) => row.length > 1);
57691
+ }
57692
+ function collectFacts(candidates) {
57693
+ return uniqueItems(candidates.filter((text) => NUMBER_PATTERN.test(text)).map((text) => ({
57694
+ value: text.match(NUMBER_PATTERN)?.[0],
57695
+ text: compactText(text, 150)
57696
+ })));
57697
+ }
57698
+ function collectTimeline(candidates) {
57699
+ return uniqueItems(candidates.filter((text) => DATE_PATTERN.test(text)).map((text) => ({
57700
+ label: text.match(DATE_PATTERN)?.[0],
57701
+ text: compactText(text, 150)
57702
+ })));
57703
+ }
57704
+ function collectComparison(candidates, tableRows) {
57705
+ const tableItems = tableRows.slice(1).map((row) => ({
57706
+ label: row[0],
57707
+ text: row.slice(1).join(" \u2014 ")
57708
+ }));
57709
+ const textItems = candidates.filter((text) => /\b(vs\.?|versus|compared with|compared to|whereas|while)\b/i.test(text)).map((text) => ({ text: compactText(text, 160) }));
57710
+ return uniqueItems([...tableItems, ...textItems]);
57711
+ }
57712
+ function collectTakeaways(candidates, facts) {
57713
+ const explicit = candidates.filter((text) => /\b(key|takeaway|therefore|recommend|should|must|need to|in summary|conclusion|next)\b/i.test(text)).map((text) => ({ text: compactText(text, 160) }));
57714
+ if (explicit.length > 0) return uniqueItems(explicit);
57715
+ return facts.slice(0, 3).map((item) => ({ text: item.text }));
57716
+ }
57717
+ function sentences(text) {
57718
+ return text.split(/(?<=[.!?。!?])\s+/).map(cleanMarkdown).filter((sentence) => sentence.length > 20);
57719
+ }
57720
+ function firstUsefulParagraph(paragraphs) {
57721
+ return paragraphs.find((paragraph) => paragraph.length > 40)?.slice(0, 220);
57722
+ }
57723
+ function uniqueItems(items) {
57724
+ const seen = /* @__PURE__ */ new Set();
57725
+ return items.filter((item) => {
57726
+ const key = item.text.toLowerCase();
57727
+ if (seen.has(key)) return false;
57728
+ seen.add(key);
57729
+ return true;
57730
+ });
57731
+ }
57732
+ function compactText(text, max) {
57733
+ const clean = cleanMarkdown(text);
57734
+ return clean.length > max ? `${clean.slice(0, max - 1).trim()}...` : clean;
57735
+ }
57736
+ function cleanMarkdown(value) {
57737
+ return value.replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
57738
+ }
57739
+
57740
+ // src/article-html.ts
57741
+ function renderInfographicHtml(spec) {
57742
+ return `<!doctype html>
57743
+ <html lang="en">
57744
+ <head>
57745
+ <meta charset="utf-8" />
57746
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
57747
+ <title>${escapeHtml(spec.title)}</title>
57748
+ <meta name="generator" content="Miao Vision article infographic" />
57749
+ ${spec.source ? `<meta name="source" content="${escapeHtml(spec.source)}" />` : ""}
57750
+ <style>${buildCss(spec.style)}</style>
57751
+ </head>
57752
+ <body>
57753
+ <main class="mv-infographic mv-infographic-${spec.style}">
57754
+ ${spec.sections.map(renderSection).join("\n")}
57755
+ </main>
57756
+ <script type="application/json" id="miao-infographic-spec">${escapeHtml(JSON.stringify(spec, null, 2))}</script>
57757
+ </body>
57758
+ </html>`;
57759
+ }
57760
+ function renderSection(section) {
57761
+ if (section.type === "hero") return renderHero(section);
57762
+ if (section.type === "facts") return renderFacts(section);
57763
+ if (section.type === "timeline") return renderTimeline(section);
57764
+ if (section.type === "comparison") return renderComparison(section);
57765
+ if (section.type === "quote") return renderQuote(section);
57766
+ return renderTakeaways(section);
57767
+ }
57768
+ function renderHero(section) {
57769
+ const lead = section.emphasis ?? section.items[0]?.text ?? "";
57770
+ return `<section class="mv-hero">
57771
+ <p class="mv-eyebrow">Miao Vision Infographic</p>
57772
+ <h1>${escapeHtml(section.title)}</h1>
57773
+ <p class="mv-lead">${escapeHtml(lead)}</p>
57774
+ </section>`;
57775
+ }
57776
+ function renderFacts(section) {
57777
+ return `<section class="mv-section mv-facts">
57778
+ <div class="mv-section-head"><span>01</span><h2>${escapeHtml(section.title)}</h2></div>
57779
+ <div class="mv-fact-grid">
57780
+ ${section.items.map((item) => `<article class="mv-fact">
57781
+ ${item.value ? `<strong>${escapeHtml(item.value)}</strong>` : ""}
57782
+ <p>${escapeHtml(item.text)}</p>
57783
+ </article>`).join("\n")}
57784
+ </div>
57785
+ </section>`;
57786
+ }
57787
+ function renderTimeline(section) {
57788
+ return `<section class="mv-section mv-timeline">
57789
+ <div class="mv-section-head"><span>02</span><h2>${escapeHtml(section.title)}</h2></div>
57790
+ <ol>
57791
+ ${section.items.map((item) => `<li><time>${escapeHtml(item.label ?? "")}</time><p>${escapeHtml(item.text)}</p></li>`).join("\n")}
57792
+ </ol>
57793
+ </section>`;
57794
+ }
57795
+ function renderComparison(section) {
57796
+ return `<section class="mv-section mv-comparison">
57797
+ <div class="mv-section-head"><span>03</span><h2>${escapeHtml(section.title)}</h2></div>
57798
+ <div class="mv-comparison-grid">
57799
+ ${section.items.map((item) => `<article>
57800
+ ${item.label ? `<h3>${escapeHtml(item.label)}</h3>` : ""}
57801
+ <p>${escapeHtml(item.text)}</p>
57802
+ </article>`).join("\n")}
57803
+ </div>
57804
+ </section>`;
57805
+ }
57806
+ function renderQuote(section) {
57807
+ const quote = section.emphasis ?? section.items[0]?.text ?? "";
57808
+ return `<section class="mv-section mv-quote">
57809
+ <blockquote>${escapeHtml(quote)}</blockquote>
57810
+ </section>`;
57811
+ }
57812
+ function renderTakeaways(section) {
57813
+ return `<section class="mv-section mv-takeaways">
57814
+ <div class="mv-section-head"><span>04</span><h2>${escapeHtml(section.title)}</h2></div>
57815
+ <ul>
57816
+ ${section.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}
57817
+ </ul>
57818
+ </section>`;
57819
+ }
57820
+ function buildCss(style) {
57821
+ const palette = style === "minimal" ? { bg: "#ffffff", ink: "#161616", muted: "#666666", card: "#ffffff", accent: "#111111", line: "#d8d8d8" } : style === "executive" ? { bg: "#f4f0e8", ink: "#18212f", muted: "#667085", card: "#ffffff", accent: "#1f5d8c", line: "#d7c9b8" } : { bg: "#f7efe2", ink: "#241b16", muted: "#75695d", card: "#fffaf2", accent: "#b64f2a", line: "#dfcdb7" };
57822
+ return `
57823
+ :root { color-scheme: light; --bg:${palette.bg}; --ink:${palette.ink}; --muted:${palette.muted}; --card:${palette.card}; --accent:${palette.accent}; --line:${palette.line}; }
57824
+ * { box-sizing: border-box; }
57825
+ body { margin: 0; background: var(--bg); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
57826
+ .mv-infographic { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 64px; }
57827
+ .mv-hero { min-height: 42vh; display: grid; align-content: center; border-bottom: 1px solid var(--line); padding: 28px 0 44px; }
57828
+ .mv-eyebrow { margin: 0 0 18px; color: var(--accent); font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.14em; }
57829
+ h1 { max-width: 860px; margin: 0; font-size: 58px; line-height: 1.02; letter-spacing: 0; }
57830
+ .mv-lead { max-width: 760px; margin: 22px 0 0; color: var(--muted); font-size: 21px; line-height: 1.55; }
57831
+ .mv-section { padding: 34px 0; border-bottom: 1px solid var(--line); }
57832
+ .mv-section-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 20px; }
57833
+ .mv-section-head span { color: var(--accent); font-weight: 800; font-size: 12px; }
57834
+ h2 { margin: 0; font-size: 28px; line-height: 1.15; letter-spacing: 0; }
57835
+ .mv-fact-grid, .mv-comparison-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
57836
+ .mv-fact, .mv-comparison article { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 18px; }
57837
+ .mv-fact strong { display: block; color: var(--accent); font-size: 30px; line-height: 1; margin-bottom: 12px; }
57838
+ .mv-fact p, .mv-comparison p, .mv-timeline p { margin: 0; color: var(--muted); line-height: 1.5; }
57839
+ .mv-comparison h3 { margin: 0 0 8px; font-size: 16px; }
57840
+ .mv-timeline ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
57841
+ .mv-timeline li { display: grid; grid-template-columns: 150px 1fr; gap: 18px; align-items: start; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 16px; }
57842
+ .mv-timeline time { color: var(--accent); font-weight: 800; }
57843
+ .mv-quote blockquote { margin: 0; max-width: 900px; color: var(--ink); font-size: 34px; line-height: 1.25; font-weight: 750; }
57844
+ .mv-takeaways ul { margin: 0; padding-left: 22px; display: grid; gap: 10px; color: var(--muted); line-height: 1.55; }
57845
+ @media (max-width: 720px) {
57846
+ .mv-infographic { width: min(100% - 24px, 1120px); padding-top: 28px; }
57847
+ h1 { font-size: 38px; }
57848
+ .mv-lead { font-size: 18px; }
57849
+ .mv-timeline li { grid-template-columns: 1fr; gap: 6px; }
57850
+ .mv-quote blockquote { font-size: 25px; }
57851
+ }
57852
+ `;
57853
+ }
57854
+
56980
57855
  // src/cli.ts
56981
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["h", "help", "summary", "reliable-only"]);
57856
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["h", "help", "summary", "reliable-only", "interactive", "no-interactive"]);
56982
57857
  async function main() {
56983
57858
  const args = parseArgs(process.argv.slice(2));
56984
57859
  if (args.command === "--help" || args.command === "-h" || args.command === "help" || !args.command) {
@@ -57010,12 +57885,16 @@ async function main() {
57010
57885
  printJson(runDeck(args));
57011
57886
  return;
57012
57887
  }
57888
+ if (args.command === "article") {
57889
+ printJson(runArticle(args));
57890
+ return;
57891
+ }
57013
57892
  if (args.command === "query") {
57014
57893
  printJson(runQuery(args));
57015
57894
  return;
57016
57895
  }
57017
57896
  printJson(agentError("UNKNOWN_COMMAND", `Unknown command: ${args.command ?? "(none)"}`, {
57018
- commands: ["profile", "validate", "catalog", "render", "deck", "query"]
57897
+ commands: ["profile", "validate", "catalog", "render", "deck", "article", "query"]
57019
57898
  }));
57020
57899
  process.exitCode = 1;
57021
57900
  } catch (error51) {
@@ -57073,11 +57952,12 @@ function runRender(args) {
57073
57952
  const validation = validateReportSpec(normalized, profile, formats);
57074
57953
  if (isAgentError(validation)) return fail(validation);
57075
57954
  const themeFlag = stringFlag(args, "theme");
57955
+ const interactive = args.flags["interactive"] === true ? true : args.flags["no-interactive"] === true ? false : void 0;
57076
57956
  const written = [];
57077
57957
  for (const format of formats) {
57078
57958
  if (format === "html") {
57079
57959
  const htmlPath = formatOutputPath(output, "html", formats.length > 1);
57080
- writeOutput(htmlPath, renderStaticHtml(validation.value, profile, dataset.value.rows, themeFlag));
57960
+ writeOutput(htmlPath, renderStaticHtml(validation.value, profile, dataset.value.rows, themeFlag, { enabled: interactive }));
57081
57961
  written.push(htmlPath);
57082
57962
  } else if (format === "svg") {
57083
57963
  const svgPath = formatOutputPath(output, "svg", formats.length > 1);
@@ -57132,6 +58012,47 @@ function runQuery(args) {
57132
58012
  if (isAgentError(result)) return fail(result);
57133
58013
  return { ok: true, value: result };
57134
58014
  }
58015
+ function runArticle(args) {
58016
+ const file2 = args.positional[0];
58017
+ if (!file2) {
58018
+ return fail(agentError("MISSING_INPUT", "Usage: miao-viz article <file> --output <file> [--style editorial|executive|minimal] [--format html|json|markdown]"));
58019
+ }
58020
+ const output = requiredFlag(args, "output");
58021
+ if (isAgentError(output)) return fail(output);
58022
+ const styleFlag = stringFlag(args, "style");
58023
+ const style = parseArticleStyle(styleFlag);
58024
+ if (!style) {
58025
+ return fail(agentError("UNSUPPORTED_ARTICLE_STYLE", `Unsupported article style: ${styleFlag}`, {
58026
+ supportedStyles: ["editorial", "executive", "minimal"]
58027
+ }));
58028
+ }
58029
+ const formatFlag = stringFlag(args, "format");
58030
+ const format = parseArticleFormat(formatFlag);
58031
+ if (!format) {
58032
+ return fail(agentError("UNSUPPORTED_ARTICLE_FORMAT", `Unsupported article output format: ${formatFlag}`, {
58033
+ supportedFormats: ["html", "json", "markdown"]
58034
+ }));
58035
+ }
58036
+ const generated = generateInfographicFromFile(file2, style);
58037
+ if (isAgentError(generated)) return fail(generated);
58038
+ if (format === "json") {
58039
+ writeOutput(output, `${JSON.stringify(generated.value.spec, null, 2)}
58040
+ `);
58041
+ } else if (format === "markdown") {
58042
+ writeOutput(output, generated.value.markdown);
58043
+ } else {
58044
+ writeOutput(output, renderInfographicHtml(generated.value.spec));
58045
+ }
58046
+ return {
58047
+ ok: true,
58048
+ value: {
58049
+ output,
58050
+ format,
58051
+ style,
58052
+ sections: generated.value.spec.sections.map((section) => section.type)
58053
+ }
58054
+ };
58055
+ }
57135
58056
  function normalizeSpec(spec) {
57136
58057
  const parsed = singleOrReportSpecSchema.safeParse(spec);
57137
58058
  if (!parsed.success) {
@@ -57147,12 +58068,12 @@ function parseFormats(value) {
57147
58068
  }
57148
58069
  }
57149
58070
  function readSpec(file2) {
57150
- const text = (0, import_node_fs2.readFileSync)(file2, "utf8");
58071
+ const text = (0, import_node_fs3.readFileSync)(file2, "utf8");
57151
58072
  if (file2.endsWith(".json")) return JSON.parse(text);
57152
58073
  return YAML.parse(text);
57153
58074
  }
57154
58075
  function readJson(file2) {
57155
- return JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8"));
58076
+ return JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
57156
58077
  }
57157
58078
  function readProfile(file2) {
57158
58079
  const parsed = readJson(file2);
@@ -57207,8 +58128,8 @@ function formatOutputPath(output, ext, multiple) {
57207
58128
  return output;
57208
58129
  }
57209
58130
  function writeOutput(file2, content) {
57210
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(file2), { recursive: true });
57211
- (0, import_node_fs2.writeFileSync)(file2, content, "utf8");
58131
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
58132
+ (0, import_node_fs3.writeFileSync)(file2, content, "utf8");
57212
58133
  }
57213
58134
  function fail(error51) {
57214
58135
  process.exitCode = 1;
@@ -57261,6 +58182,8 @@ Options:
57261
58182
  --output <file> Output file path
57262
58183
  --format <fmt> Output format: html, svg (default: html)
57263
58184
  --theme <name> Theme: default, editorial, dark, minimal
58185
+ --interactive Force lightweight interactive runtime for HTML output
58186
+ --no-interactive Force static HTML output even when interaction spec exists
57264
58187
  --sheet <name> Sheet name (Excel only)
57265
58188
  --limit <n> Max rows to read
57266
58189
  `,
@@ -57275,6 +58198,19 @@ Options:
57275
58198
  --theme <name> Theme: default, editorial, dark, minimal
57276
58199
  --sheet <name> Sheet name (Excel only)
57277
58200
  --limit <n> Max rows to read
58201
+ `,
58202
+ article: `Usage: miao-viz article <file> --output <file> [options]
58203
+
58204
+ Convert a local Markdown or plain-text article into a static infographic artifact.
58205
+ URL fetching is intentionally handled by the agent/skill layer; this command only reads local files.
58206
+
58207
+ Arguments:
58208
+ file Path to a .md, .markdown, or .txt article file
58209
+
58210
+ Options:
58211
+ --output <file> Output file path
58212
+ --format <fmt> Output format: html, json, markdown (default: html)
58213
+ --style <name> Style: editorial, executive, minimal (default: editorial)
57278
58214
  `,
57279
58215
  query: `Usage: miao-viz query <file> [options]
57280
58216
 
@@ -57309,6 +58245,7 @@ Commands:
57309
58245
  catalog List all available chart types
57310
58246
  render Render a vizspec to HTML or SVG
57311
58247
  deck Render a deck spec to HTML slides
58248
+ article Convert a local article to an infographic artifact
57312
58249
 
57313
58250
  Run "miao-viz <command> --help" for command-specific options.
57314
58251
  `);