mviz 1.6.4 → 1.6.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.
Files changed (47) hide show
  1. package/README.md +8 -8
  2. package/dist/charts/area.js +8 -36
  3. package/dist/charts/bar.js +8 -26
  4. package/dist/charts/bubble.js +9 -7
  5. package/dist/charts/combo.js +17 -39
  6. package/dist/charts/dumbbell.js +15 -14
  7. package/dist/charts/funnel.js +12 -7
  8. package/dist/charts/heatmap.js +8 -6
  9. package/dist/charts/line.js +6 -27
  10. package/dist/charts/scatter.js +7 -5
  11. package/dist/cli.js +4 -3
  12. package/dist/components/big_value.d.ts +23 -1
  13. package/dist/components/big_value.js +84 -25
  14. package/dist/components/delta.d.ts +24 -1
  15. package/dist/components/delta.js +63 -17
  16. package/dist/components/table-interactivity.d.ts +69 -0
  17. package/dist/components/table-interactivity.js +216 -0
  18. package/dist/components/table.d.ts +6 -1
  19. package/dist/components/table.js +53 -12
  20. package/dist/core/chart-helpers.d.ts +59 -5
  21. package/dist/core/chart-helpers.js +84 -5
  22. package/dist/core/formatting.d.ts +61 -4
  23. package/dist/core/formatting.js +216 -17
  24. package/dist/core/lint-rules/registry.d.ts +4 -2
  25. package/dist/core/lint-rules/registry.js +6 -1
  26. package/dist/core/lint-rules/rules/index.d.ts +1 -0
  27. package/dist/core/lint-rules/rules/index.js +1 -0
  28. package/dist/core/lint-rules/rules/pct-scalar-gt-one.d.ts +13 -0
  29. package/dist/core/lint-rules/rules/pct-scalar-gt-one.js +46 -0
  30. package/dist/core/lint-rules/types.d.ts +12 -0
  31. package/dist/core/linter.d.ts +10 -2
  32. package/dist/core/linter.js +60 -12
  33. package/dist/layout/block-loader.d.ts +31 -0
  34. package/dist/layout/block-loader.js +143 -0
  35. package/dist/layout/layout-resolver.d.ts +33 -0
  36. package/dist/layout/layout-resolver.js +73 -0
  37. package/dist/layout/markdown-parser.d.ts +34 -0
  38. package/dist/layout/markdown-parser.js +395 -0
  39. package/dist/layout/parser-types.d.ts +116 -0
  40. package/dist/layout/parser-types.js +11 -0
  41. package/dist/layout/parser.d.ts +31 -22
  42. package/dist/layout/parser.js +118 -1006
  43. package/dist/layout/renderer.d.ts +33 -0
  44. package/dist/layout/renderer.js +450 -0
  45. package/dist/types.d.ts +1 -1
  46. package/package.json +6 -6
  47. package/schema/mviz.v1.schema.json +402 -33
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Phase 4: Render.
3
+ *
4
+ * Turns a resolved `Section[]` IR into the inner HTML of a dashboard
5
+ * (componentsHtml + chartScripts + mermaidBlocks + testItems). The final
6
+ * page assembly (HTML <head>, frame, etc.) lives in `templates.ts` and is
7
+ * driven by the orchestrator in `parser.ts`.
8
+ *
9
+ * Component rendering (big_value, delta, alert, note, text, textarea,
10
+ * empty_space, table, sparkline, mermaid, inline_header) and ECharts chart
11
+ * rendering all live here.
12
+ */
13
+ import type { CustomTheme } from '../types.js';
14
+ import type { MermaidBlock, Section, TestItem } from './parser-types.js';
15
+ export interface RenderOptions {
16
+ theme: string;
17
+ testMode: boolean;
18
+ customTheme?: CustomTheme;
19
+ currency?: string;
20
+ }
21
+ export interface RenderResult {
22
+ componentsHtml: string;
23
+ chartScripts: string;
24
+ mermaidBlocks: MermaidBlock[];
25
+ testItems: TestItem[];
26
+ }
27
+ /**
28
+ * Render a list of sections into the dashboard body HTML and the supporting
29
+ * chart scripts (concatenated into single strings ready to embed).
30
+ */
31
+ export declare function renderSections(sections: Section[], options: RenderOptions): RenderResult;
32
+ export declare function escapeHtml(text: string): string;
33
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Phase 4: Render.
3
+ *
4
+ * Turns a resolved `Section[]` IR into the inner HTML of a dashboard
5
+ * (componentsHtml + chartScripts + mermaidBlocks + testItems). The final
6
+ * page assembly (HTML <head>, frame, etc.) lives in `templates.ts` and is
7
+ * driven by the orchestrator in `parser.ts`.
8
+ *
9
+ * Component rendering (big_value, delta, alert, note, text, textarea,
10
+ * empty_space, table, sparkline, mermaid, inline_header) and ECharts chart
11
+ * rendering all live here.
12
+ */
13
+ import { getOptionsBuilder } from '../charts/index.js';
14
+ import { serializeOption } from '../core/serializer.js';
15
+ import { resolveCurrency } from '../core/formatting.js';
16
+ import { ALERT_ICONS, COLORS, DEFAULT_SIZES, GRID_TOTAL_COLUMNS, getFontsWithCustom, getHeatmapColors, getThemeColorsWithCustom, } from '../core/themes.js';
17
+ import { computeDumbbellRanges, computeHeatmapRanges, computePctMultiplyRanges, extractCellValue, formatCell, } from '../components/table.js';
18
+ import { buildTableChrome, cellSortAttr, parseTableInteractivity, sharedTableCss, sharedTableScript, sortableHeaderAttrs, tableInitCall, } from '../components/table-interactivity.js';
19
+ import { renderBigValue as renderBigValueFragment } from '../components/big_value.js';
20
+ import { renderDelta as renderDeltaFragment } from '../components/delta.js';
21
+ /** Height per row unit in pixels (compact for print). Mirrors Python ROW_HEIGHT_PX. */
22
+ const ROW_HEIGHT_PX = 32;
23
+ /**
24
+ * Render a list of sections into the dashboard body HTML and the supporting
25
+ * chart scripts (concatenated into single strings ready to embed).
26
+ */
27
+ export function renderSections(sections, options) {
28
+ const ctx = createRenderContext(options);
29
+ for (const section of sections) {
30
+ renderSection(section, ctx);
31
+ }
32
+ return {
33
+ componentsHtml: ctx.componentsHtml.join(''),
34
+ chartScripts: ctx.chartScripts.join(''),
35
+ mermaidBlocks: ctx.mermaidBlocks,
36
+ testItems: ctx.testItems,
37
+ };
38
+ }
39
+ function createRenderContext(options) {
40
+ return {
41
+ options,
42
+ componentsHtml: [],
43
+ chartScripts: [],
44
+ mermaidBlocks: [],
45
+ testItems: [],
46
+ emittedTableShared: false,
47
+ chartCounter: 0,
48
+ seenTypes: new Set(),
49
+ };
50
+ }
51
+ function renderSection(section, ctx) {
52
+ const sectionHtml = [];
53
+ if (section.title) {
54
+ sectionHtml.push(`<h2 class="section-title">${escapeHtml(section.title.toUpperCase())}</h2>`);
55
+ }
56
+ for (const row of section.rows) {
57
+ const rowHtml = renderRow(row, section, ctx);
58
+ if (rowHtml)
59
+ sectionHtml.push(rowHtml);
60
+ }
61
+ if (sectionHtml.length === 0)
62
+ return;
63
+ const classes = ['dashboard-section'];
64
+ if (section.pageBreak)
65
+ classes.push('page-break');
66
+ if (section.hasDivider)
67
+ classes.push('has-divider');
68
+ ctx.componentsHtml.push(`
69
+ <section class="${classes.join(' ')}">
70
+ ${sectionHtml.join('')}
71
+ </section>`);
72
+ }
73
+ function renderRow(row, section, ctx) {
74
+ const rowItems = [];
75
+ for (const item of row) {
76
+ const html = renderItem(item, section, ctx);
77
+ if (html)
78
+ rowItems.push(html);
79
+ }
80
+ if (rowItems.length === 0)
81
+ return null;
82
+ return `
83
+ <div class="row">
84
+ ${rowItems.join('')}
85
+ </div>`;
86
+ }
87
+ function renderItem(item, section, ctx) {
88
+ const compType = item.type;
89
+ const spec = item.spec;
90
+ const size = item.size ?? DEFAULT_SIZES[compType] ?? [8, 4];
91
+ const colSpan = size[0];
92
+ const rowSpan = size[1];
93
+ ctx.chartCounter++;
94
+ const chartId = `chart_${ctx.chartCounter}`;
95
+ const itemHeight = rowSpan * ROW_HEIGHT_PX;
96
+ const anchorId = ctx.options.testMode && !ctx.seenTypes.has(compType) ? compType : '';
97
+ if (anchorId)
98
+ ctx.seenTypes.add(compType);
99
+ if (ctx.options.testMode)
100
+ trackTestItem(ctx, chartId, compType, spec, section);
101
+ const sparkType = spec.sparkType;
102
+ if (compType === 'sparkline' && (sparkType === 'pct' || sparkType === 'pct_bar')) {
103
+ return renderPctBarSparkline(spec, colSpan, anchorId);
104
+ }
105
+ return renderByType(compType, spec, { chartId, colSpan, itemHeight, anchorId }, ctx);
106
+ }
107
+ function renderByType(compType, spec, args, ctx) {
108
+ const builder = getOptionsBuilder(compType);
109
+ if (builder)
110
+ return renderEchartsAndCollect(compType, spec, args, ctx);
111
+ return renderSimpleComponent(compType, spec, args, ctx);
112
+ }
113
+ function renderEchartsAndCollect(compType, spec, args, ctx) {
114
+ const { html, script } = renderEchartsComponent(compType, spec, args.chartId, args.colSpan, args.itemHeight, args.anchorId, ctx.options.customTheme);
115
+ if (!html)
116
+ return `<div class="grid-item"><p>Error rendering ${compType}</p></div>`;
117
+ if (script)
118
+ ctx.chartScripts.push(script);
119
+ return html;
120
+ }
121
+ /** Dispatch table for non-ECharts component types. */
122
+ function renderSimpleComponent(compType, spec, args, ctx) {
123
+ const { colSpan, anchorId } = args;
124
+ const currency = ctx.options.currency;
125
+ switch (compType) {
126
+ case 'big_value': return renderBigValue(spec, colSpan, anchorId, currency);
127
+ case 'delta': return renderDelta(spec, colSpan, anchorId, currency);
128
+ case 'alert': return renderAlert(spec, colSpan, anchorId);
129
+ case 'note': return renderNote(spec, colSpan, anchorId);
130
+ case 'text': return renderText(spec, colSpan, anchorId);
131
+ case 'textarea': return renderTextarea(spec, colSpan, anchorId);
132
+ case 'table': return renderTableAndCollect(spec, colSpan, anchorId, ctx);
133
+ case 'empty_space': return renderEmptySpace(spec, colSpan, anchorId);
134
+ case 'mermaid': return renderMermaidAndCollect(spec, colSpan, anchorId, ctx);
135
+ case 'inline_header': return renderInlineHeader(spec, anchorId);
136
+ default: return `<div class="grid-item"><p>Unknown type: ${compType}</p></div>`;
137
+ }
138
+ }
139
+ function renderTableAndCollect(spec, colSpan, anchorId, ctx) {
140
+ const out = renderTable(spec, colSpan, anchorId, ctx.options.theme, ctx.options.customTheme, ctx.options.currency);
141
+ if (!out.script)
142
+ return out.html;
143
+ if (!ctx.emittedTableShared) {
144
+ ctx.emittedTableShared = true;
145
+ ctx.chartScripts.push(`(function(){var s=document.createElement('style');s.textContent=${JSON.stringify(sharedTableCss())};document.head.appendChild(s);})();`);
146
+ ctx.chartScripts.push(sharedTableScript());
147
+ }
148
+ ctx.chartScripts.push(out.script);
149
+ return out.html;
150
+ }
151
+ function renderMermaidAndCollect(spec, colSpan, anchorId, ctx) {
152
+ const placeholderId = `mermaid-${ctx.mermaidBlocks.length}`;
153
+ ctx.mermaidBlocks.push({ id: placeholderId, spec, theme: ctx.options.theme });
154
+ return renderMermaidPlaceholder(spec, colSpan, anchorId, placeholderId);
155
+ }
156
+ function trackTestItem(ctx, chartId, compType, spec, section) {
157
+ const testName = spec.title || spec.label ||
158
+ `${compType.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())} ${ctx.chartCounter}`;
159
+ ctx.testItems.push({
160
+ id: chartId,
161
+ name: testName,
162
+ type: compType,
163
+ spec,
164
+ section: section.title || compType,
165
+ });
166
+ }
167
+ // =============================================================================
168
+ // ECharts component rendering
169
+ // =============================================================================
170
+ function renderEchartsComponent(compType, spec, chartId, colSpan, itemHeight, anchorId, customTheme) {
171
+ const title = spec.title ?? '';
172
+ const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
173
+ const builder = getOptionsBuilder(compType);
174
+ if (!builder)
175
+ return { html: null, script: null };
176
+ const chartHeight = itemHeight - (title ? 24 : 0);
177
+ const fontFamily = getFontsWithCustom(customTheme).family;
178
+ const optionJsonLight = buildEchartsOption(spec, 'light', chartHeight, customTheme, fontFamily, builder);
179
+ if (!optionJsonLight)
180
+ return { html: null, script: null };
181
+ const optionJsonDark = buildEchartsOption(spec, 'dark', chartHeight, customTheme, fontFamily, builder);
182
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
183
+ const html = `
184
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
185
+ ${titleHtml}
186
+ <div id="${chartId}" style="width: 100%; height: ${chartHeight}px;"></div>
187
+ </div>`;
188
+ return { html, script: generateChartScript(chartId, optionJsonLight, optionJsonDark) };
189
+ }
190
+ function buildEchartsOption(spec, theme, chartHeight, customTheme, fontFamily, builder) {
191
+ const themedSpec = { ...spec, theme, height: chartHeight, customTheme };
192
+ const option = builder(themedSpec);
193
+ if (!option)
194
+ return null;
195
+ option.textStyle = { ...(option.textStyle ?? {}), fontFamily };
196
+ return serializeOption(option);
197
+ }
198
+ function generateChartScript(chartId, optionJsonLight, optionJsonDark) {
199
+ const darkOptions = optionJsonDark ?? optionJsonLight;
200
+ return `
201
+ (function() {
202
+ var chart = echarts.init(document.getElementById('${chartId}'), null, {renderer: 'svg'});
203
+ window.chartInstances = window.chartInstances || {};
204
+ window.chartOptions = window.chartOptions || {};
205
+ window.chartInstances['${chartId}'] = chart;
206
+ window.chartOptions['${chartId}'] = {
207
+ light: ${optionJsonLight},
208
+ dark: ${darkOptions}
209
+ };
210
+ var currentTheme = document.body.classList.contains('theme-dark') ? 'dark' : 'light';
211
+ chart.setOption(window.chartOptions['${chartId}'][currentTheme]);
212
+ window.addEventListener('resize', function() { chart.resize(); });
213
+ })();`;
214
+ }
215
+ // =============================================================================
216
+ // Simple component renderers
217
+ // =============================================================================
218
+ function renderBigValue(spec, colSpan, anchorId, currencyCode) {
219
+ // Delegates to the unified component renderer (#256).
220
+ return renderBigValueFragment(spec, { colSpan, anchorId, currencyCode });
221
+ }
222
+ function renderDelta(spec, colSpan, anchorId, currencyCode) {
223
+ // Delegates to the unified component renderer (#256).
224
+ return renderDeltaFragment(spec, { colSpan, anchorId, currencyCode });
225
+ }
226
+ const ALERT_COLORS = {
227
+ info: COLORS.INFO_BLUE,
228
+ success: COLORS.POSITIVE_GREEN,
229
+ warning: COLORS.WARNING_AMBER,
230
+ error: COLORS.ERROR_RED,
231
+ };
232
+ function renderAlert(spec, colSpan, anchorId) {
233
+ const message = spec.message ?? spec.content ?? '';
234
+ const alertType = spec.alertType ?? 'info';
235
+ const alertColor = ALERT_COLORS[alertType] ?? COLORS.INFO_BLUE;
236
+ const icon = ALERT_ICONS[alertType] ?? 'ℹ';
237
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
238
+ const parsedMessage = parseInlineMarkdown(message);
239
+ return `
240
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
241
+ <div class="alert" style="border-left-color: ${alertColor}; background-color: ${alertColor}1a;">
242
+ <span class="icon" style="color: ${alertColor};">${icon}</span>
243
+ <span class="message">${parsedMessage}</span>
244
+ </div>
245
+ </div>`;
246
+ }
247
+ function renderNote(spec, colSpan, anchorId) {
248
+ const content = spec.content ?? '';
249
+ const label = spec.label ?? '';
250
+ const noteType = spec.noteType ?? spec.variant ?? 'default';
251
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
252
+ const parsedContent = parseInlineMarkdown(content);
253
+ const contentHtml = label ? `<strong>${escapeHtml(label)}:</strong> ${parsedContent}` : parsedContent;
254
+ return `
255
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
256
+ <div class="note note-${noteType}">
257
+ <div class="note-content">${contentHtml}</div>
258
+ </div>
259
+ </div>`;
260
+ }
261
+ function renderText(spec, colSpan, anchorId) {
262
+ const content = spec.content ?? '';
263
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
264
+ return `
265
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
266
+ <div class="text-content">${escapeHtml(content)}</div>
267
+ </div>`;
268
+ }
269
+ function renderTextarea(spec, colSpan, anchorId) {
270
+ const content = spec.content ?? '';
271
+ const title = spec.title ?? '';
272
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
273
+ const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
274
+ const escapedContent = content.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
275
+ const contentId = `md-${Math.abs(hashString(content)) % 10000000}`;
276
+ return `
277
+ <div class="grid-item textarea-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
278
+ ${titleHtml}
279
+ <div class="markdown-content" id="${contentId}"></div>
280
+ <script>
281
+ document.getElementById('${contentId}').innerHTML = marked.parse(\`${escapedContent}\`);
282
+ </script>
283
+ </div>`;
284
+ }
285
+ function renderEmptySpace(_spec, colSpan, anchorId) {
286
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
287
+ return `
288
+ <div class="grid-item empty-space"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
289
+ </div>`;
290
+ }
291
+ function renderInlineHeader(spec, anchorId) {
292
+ const text = spec.text ?? '';
293
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
294
+ return `
295
+ <div class="inline-header"${anchorAttr} style="--col-span: ${GRID_TOTAL_COLUMNS}; grid-column: span ${GRID_TOTAL_COLUMNS};">
296
+ <p class="inline-header-text">${escapeHtml(text)}</p>
297
+ </div>`;
298
+ }
299
+ function renderMermaidPlaceholder(spec, colSpan, anchorId, placeholderId) {
300
+ const title = spec.title;
301
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
302
+ const titleHtml = title
303
+ ? `<h3 class="chart-title">${escapeHtml(title.toUpperCase())}</h3>`
304
+ : '';
305
+ return `
306
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
307
+ ${titleHtml}
308
+ <div class="mermaid-container" style="text-align: center; padding: 16px 0;">
309
+ <!-- MERMAID_PLACEHOLDER:${placeholderId} -->
310
+ </div>
311
+ </div>`;
312
+ }
313
+ function renderPctBarSparkline(spec, colSpan, anchorId) {
314
+ const title = spec.title ?? '';
315
+ const pctValue = typeof spec.value === 'number' ? spec.value : 0;
316
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
317
+ const pct = pctValue <= 1 ? pctValue : pctValue / 100;
318
+ const clampedPct = Math.max(0, Math.min(1, pct));
319
+ const widthPct = clampedPct * 100;
320
+ const displayValue = `${Math.round(clampedPct * 100)}%`;
321
+ const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
322
+ return `
323
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
324
+ ${titleHtml}
325
+ <div class="pct-bar-wrapper">
326
+ <div class="pct-bar-container">
327
+ <div class="pct-bar-fill" style="width: ${widthPct}%;"></div>
328
+ </div>
329
+ <span class="pct-bar-value">${displayValue}</span>
330
+ </div>
331
+ </div>`;
332
+ }
333
+ // =============================================================================
334
+ // Table rendering
335
+ // =============================================================================
336
+ let dashboardTableCounter = 0;
337
+ function renderTable(spec, colSpan, anchorId, baseTheme, customTheme, currencyCode) {
338
+ const data = spec.data ?? [];
339
+ const title = spec.title ?? '';
340
+ const striped = spec.striped === true;
341
+ const compact = spec.compact === true;
342
+ const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
343
+ const columns = resolveTableColumns(spec, data);
344
+ const styling = computeTableStyling(striped, compact);
345
+ const ranges = computeTableRanges(columns, data, baseTheme, customTheme);
346
+ const interactivity = parseTableInteractivity(spec);
347
+ const tableId = spec.id ?? `mviz-dash-table-${++dashboardTableCounter}`;
348
+ const chrome = buildTableChrome(tableId, interactivity, ranges.themeColors);
349
+ const tableCurrency = resolveCurrency(currencyCode ?? spec.currency);
350
+ const headerCells = buildTableHeader(columns, styling, interactivity);
351
+ const bodyRows = buildTableBody(columns, data, styling, ranges, customTheme, tableCurrency, interactivity);
352
+ const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
353
+ const hasInteractivity = interactivity.sortable || interactivity.filter !== false;
354
+ const script = hasInteractivity ? tableInitCall(tableId) : null;
355
+ const html = `
356
+ <div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
357
+ ${titleHtml}
358
+ ${chrome.toolbarHtml}
359
+ <table id="${tableId}" class="data-table${styling.compactClass}${styling.stripedClass}" style="width: 100%; border-collapse: collapse; font-size: ${styling.fontSize};">
360
+ <thead>
361
+ <tr>${headerCells}</tr>
362
+ </thead>
363
+ <tbody>
364
+ ${bodyRows}
365
+ </tbody>
366
+ </table>
367
+ </div>`;
368
+ return { html, script };
369
+ }
370
+ function resolveTableColumns(spec, data) {
371
+ let columns = spec.columns;
372
+ if (!columns && data.length > 0) {
373
+ const firstRow = data[0];
374
+ if (firstRow) {
375
+ columns = Object.keys(firstRow).map((key) => ({ id: key, title: key }));
376
+ }
377
+ }
378
+ return columns ?? [];
379
+ }
380
+ function computeTableStyling(striped, compact) {
381
+ return {
382
+ cellPadding: compact ? '3px 6px' : '5px 8px',
383
+ headerPadding: compact ? '3px 6px' : '6px 8px',
384
+ fontSize: compact ? '10px' : '11px',
385
+ compactClass: compact ? ' table-compact' : '',
386
+ stripedClass: striped ? ' table-striped' : '',
387
+ };
388
+ }
389
+ function computeTableRanges(columns, data, baseTheme, customTheme) {
390
+ return {
391
+ defaultHeatmapColors: getHeatmapColors(baseTheme),
392
+ heatmapRanges: computeHeatmapRanges(columns, data),
393
+ dumbbellRanges: computeDumbbellRanges(columns, data),
394
+ pctMultiplyByColumn: computePctMultiplyRanges(columns, data),
395
+ themeColors: getThemeColorsWithCustom(baseTheme, customTheme),
396
+ };
397
+ }
398
+ function buildTableHeader(columns, styling, interactivity) {
399
+ return columns.map((col, i) => {
400
+ const align = col.align ?? 'left';
401
+ const sortAttrs = sortableHeaderAttrs(i, interactivity.sortable);
402
+ const sortInd = interactivity.sortable ? '<span class="mviz-sort-ind"></span>' : '';
403
+ return `<th${sortAttrs} style="text-align: ${align}; padding: ${styling.headerPadding}; font-size: ${styling.fontSize};">${escapeHtml(col.title ?? col.id)}${sortInd}</th>`;
404
+ }).join('');
405
+ }
406
+ function buildTableBody(columns, data, styling, ranges, customTheme, tableCurrency, interactivity) {
407
+ const customPalette = customTheme?.palette;
408
+ return data.map((row) => {
409
+ const cells = columns.map((col) => {
410
+ const cellValue = row[col.id];
411
+ const align = col.align ?? 'left';
412
+ const result = formatCell(cellValue, col, ranges.themeColors, ranges.heatmapRanges, ranges.dumbbellRanges, ranges.defaultHeatmapColors, customPalette, tableCurrency, ranges.pctMultiplyByColumn);
413
+ const bgStyle = result.bgColor ? `background-color: ${result.bgColor};` : '';
414
+ const textStyle = result.textColor ? `color: ${result.textColor};` : '';
415
+ const sortRaw = extractCellValue(cellValue, col).value;
416
+ const sortAttr = interactivity.sortable ? cellSortAttr(sortRaw, col.fmt) : '';
417
+ return `<td${sortAttr} style="text-align: ${align}; padding: ${styling.cellPadding}; font-size: ${styling.fontSize}; ${bgStyle} ${textStyle}">${result.content}</td>`;
418
+ }).join('');
419
+ return `<tr>${cells}</tr>`;
420
+ }).join('');
421
+ }
422
+ // =============================================================================
423
+ // Helpers
424
+ // =============================================================================
425
+ export function escapeHtml(text) {
426
+ return text
427
+ .replace(/&/g, '&amp;')
428
+ .replace(/</g, '&lt;')
429
+ .replace(/>/g, '&gt;')
430
+ .replace(/"/g, '&quot;')
431
+ .replace(/'/g, '&#039;');
432
+ }
433
+ function parseInlineMarkdown(text) {
434
+ let result = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
435
+ result = result.replace(/__([^_]+)__/g, '<strong>$1</strong>');
436
+ result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
437
+ result = result.replace(/_([^_]+)_/g, '<em>$1</em>');
438
+ result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
439
+ return result;
440
+ }
441
+ function hashString(str) {
442
+ let hash = 0;
443
+ for (let i = 0; i < str.length; i++) {
444
+ const char = str.charCodeAt(i);
445
+ hash = ((hash << 5) - hash) + char;
446
+ hash = hash & hash;
447
+ }
448
+ return hash;
449
+ }
450
+ //# sourceMappingURL=renderer.js.map
package/dist/types.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  /**
5
5
  * Supported number format types
6
6
  */
7
- export type FormatType = 'auto' | 'currency' | 'currency_auto' | 'currency0k' | 'currency0m' | 'currency0b' | 'pct' | 'pct0' | 'pct1' | 'num0' | 'num1' | 'num0k' | 'num0m' | 'num0b';
7
+ export type FormatType = 'auto' | 'currency' | 'currency_auto' | 'currency0k' | 'currency0m' | 'currency0b' | 'pct' | 'pct0' | 'pct1' | 'num0' | 'num1' | 'num0k' | 'num0m' | 'num0b' | 'date' | 'duration';
8
8
  /**
9
9
  * Currency configuration
10
10
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mviz",
3
- "version": "1.6.4",
3
+ "version": "1.6.7",
4
4
  "description": "A chart & report builder designed for use by AI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -68,17 +68,17 @@
68
68
  "url": "https://github.com/matsonj/mviz/issues"
69
69
  },
70
70
  "engines": {
71
- "node": ">=20.0.0"
71
+ "node": ">=24.12.0"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/node": "^20.10.0",
75
- "@typescript-eslint/eslint-plugin": "^6.13.0",
76
- "@typescript-eslint/parser": "^6.13.0",
77
- "esbuild": "^0.21.0",
75
+ "@typescript-eslint/eslint-plugin": "^8.59.0",
76
+ "@typescript-eslint/parser": "^8.59.0",
77
+ "esbuild": "^0.28.0",
78
78
  "eslint": "^8.55.0",
79
79
  "prettier": "^3.1.0",
80
80
  "typescript": "^5.3.0",
81
- "vitest": "^1.0.0"
81
+ "vitest": "^3.2.4"
82
82
  },
83
83
  "dependencies": {
84
84
  "ajv": "^8.17.1",