@praxisui/core 10.0.0-rc.31 → 10.0.0-rc.33

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.
@@ -111,6 +111,10 @@ const VISUALIZATION_TONES = new Set([
111
111
  'danger',
112
112
  'critical',
113
113
  ]);
114
+ const VISUALIZATION_SCALE_SCOPES = new Set([
115
+ 'row',
116
+ 'page',
117
+ ]);
114
118
  const TABLE_SAFE_KINDS = new Set([
115
119
  'line',
116
120
  'area',
@@ -218,6 +222,7 @@ function normalizePraxisPresentationVisualization(value) {
218
222
  }
219
223
  const surface = normalizeEnum$1(value.surface, VISUALIZATION_SURFACES);
220
224
  const size = normalizeEnum$1(value.size, VISUALIZATION_SIZES);
225
+ const scaleScope = normalizeEnum$1(value.scaleScope, VISUALIZATION_SCALE_SCOPES);
221
226
  const tone = normalizeEnum$1(value.tone, VISUALIZATION_TONES);
222
227
  const ariaLabel = normalizeText$1(value.ariaLabel);
223
228
  const valueSuffix = normalizeText$1(value.valueSuffix);
@@ -228,7 +233,10 @@ function normalizePraxisPresentationVisualization(value) {
228
233
  let evalVal = value.value;
229
234
  if (evalVal === undefined && Array.isArray(value.points) && value.points.length > 0) {
230
235
  const last = value.points[value.points.length - 1];
231
- if (last && typeof last === 'object' && Number.isFinite(last.value)) {
236
+ if (typeof last === 'number' && Number.isFinite(last)) {
237
+ evalVal = last;
238
+ }
239
+ else if (last && typeof last === 'object' && Number.isFinite(last.value)) {
232
240
  evalVal = last.value;
233
241
  }
234
242
  }
@@ -251,6 +259,7 @@ function normalizePraxisPresentationVisualization(value) {
251
259
  fallbackText,
252
260
  ...(surface ? { surface } : {}),
253
261
  ...(size ? { size } : {}),
262
+ ...(scaleScope ? { scaleScope } : {}),
254
263
  ...(resolvedTone ? { tone: resolvedTone } : {}),
255
264
  ...(resolvedAriaLabel ? { ariaLabel: resolvedAriaLabel } : {}),
256
265
  ...(valueSuffix ? { valueSuffix } : {}),
@@ -258,6 +267,10 @@ function normalizePraxisPresentationVisualization(value) {
258
267
  ...(Object.prototype.hasOwnProperty.call(value, 'value') ? { value: value.value } : {}),
259
268
  ...(normalizeExpression(value.valueExpr, 'valueExpr') ?? {}),
260
269
  ...(normalizeExpression(value.valueSuffixExpr, 'valueSuffixExpr') ?? {}),
270
+ ...(normalizeNumber(value.min, 'min') ?? {}),
271
+ ...(normalizeExpression(value.minExpr, 'minExpr') ?? {}),
272
+ ...(normalizeNumber(value.max, 'max') ?? {}),
273
+ ...(normalizeExpression(value.maxExpr, 'maxExpr') ?? {}),
261
274
  ...(normalizeNumber(value.total, 'total') ?? {}),
262
275
  ...(normalizeExpression(value.totalExpr, 'totalExpr') ?? {}),
263
276
  ...(normalizeNumber(value.target, 'target') ?? {}),
@@ -318,49 +331,75 @@ function renderPraxisPresentationVisualizationHtml(value, options = {}) {
318
331
  }
319
332
  }
320
333
  function renderComparisonVisualizationHtml(visualization, options) {
321
- const points = visualization.points ?? [];
334
+ let points = visualization.points ?? [];
335
+ if (points.length === 0) {
336
+ const primaryValue = toFiniteNumber(visualization.value);
337
+ const referenceValue = toFiniteNumber(visualization.baseline) ?? toFiniteNumber(visualization.target);
338
+ if (primaryValue !== undefined) {
339
+ points = [
340
+ { value: primaryValue, tone: visualization.tone ?? 'info' },
341
+ ...(referenceValue !== undefined
342
+ ? [{ value: referenceValue, tone: 'neutral' }]
343
+ : []),
344
+ ];
345
+ }
346
+ }
322
347
  if (points.length === 0) {
323
348
  return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
324
349
  }
325
- const max = Math.max(1, ...points.map((point) => Math.max(0, point.value)));
350
+ const explicitCeiling = Math.max(toFiniteNumber(visualization.max) ?? 0, toFiniteNumber(visualization.total) ?? 0);
351
+ const max = Math.max(1, explicitCeiling, ...points.map((point) => Math.max(0, point.value)));
352
+ const suffix = visualization.valueSuffix ?? '';
353
+ const useCompact = visualization.compactValue === true;
326
354
  const rows = points
327
355
  .map((point) => {
328
356
  const width = percent(point.value, max);
357
+ const formattedVal = `${formatMicroNumber(point.value, options.locale, useCompact)}${suffix}`;
358
+ const readout = `${point.label ? `${point.label}: ` : ''}${formattedVal} (${Math.round(Number(width))}%)`;
329
359
  return `
330
- <span class="pfx-micro-viz__row" data-tone="${escapeHtml(point.tone ?? visualization.tone ?? 'info')}">
360
+ <span class="pfx-micro-viz__row" data-tone="${escapeHtml(point.tone ?? visualization.tone ?? 'info')}" data-micro-readout="${escapeHtml(readout)}">
331
361
  <span class="pfx-micro-viz__label">${escapeHtml(point.label ?? '')}</span>
332
362
  <span class="pfx-micro-viz__track">
333
363
  <span class="pfx-micro-viz__bar" style="width:${width}%"></span>
334
364
  </span>
335
- <span class="pfx-micro-viz__value">${escapeHtml(formatMicroNumber(point.value, options.locale))}</span>
365
+ <span class="pfx-micro-viz__value">${escapeHtml(formattedVal)}</span>
336
366
  </span>`;
337
367
  })
338
368
  .join('');
339
- return `<span class="pfx-micro-viz" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">${rows}</span>`;
369
+ return `<span class="pfx-micro-viz pfx-micro-viz__comparison" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">${rows}</span>`;
340
370
  }
341
371
  function renderStackedBarVisualizationHtml(visualization, options) {
342
- const segments = visualization.segments ?? [];
372
+ const segments = visualization.segments?.length
373
+ ? visualization.segments
374
+ : (visualization.items?.filter((item) => typeof item?.value === 'number' && Number.isFinite(item.value)) ?? []);
343
375
  if (segments.length === 0) {
344
376
  return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
345
377
  }
346
378
  const total = visualization.total && visualization.total > 0
347
379
  ? visualization.total
348
380
  : Math.max(1, segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0));
381
+ const suffix = visualization.valueSuffix ?? '';
349
382
  const bars = segments
350
- .map((segment) => `
383
+ .map((segment) => {
384
+ const pctStr = percent(segment.value, total);
385
+ const formattedVal = `${formatMicroNumber(segment.value, options.locale)}${suffix}`;
386
+ const readout = `${segment.label ? `${segment.label}: ` : ''}${formattedVal} (${Math.round(Number(pctStr))}%)`;
387
+ return `
351
388
  <span
352
389
  class="pfx-micro-viz__segment"
353
390
  data-tone="${escapeHtml(segment.tone ?? visualization.tone ?? 'info')}"
354
- style="width:${percent(segment.value, total)}%"
355
- title="${escapeHtml(`${segment.label ?? ''} ${formatMicroNumber(segment.value, options.locale)}`.trim())}"
356
- ></span>`)
391
+ data-micro-readout="${escapeHtml(readout)}"
392
+ style="width:${pctStr}%"
393
+ title="${escapeHtml(`${segment.label ?? ''} ${formattedVal}`.trim())}"
394
+ ></span>`;
395
+ })
357
396
  .join('');
358
397
  const meta = segments
359
398
  .slice(0, 3)
360
399
  .map((segment) => `<span>${escapeHtml(segment.label ?? '')}: ${escapeHtml(formatMicroNumber(segment.value, options.locale))}</span>`)
361
400
  .join('');
362
401
  return `
363
- <span class="pfx-micro-viz" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
402
+ <span class="pfx-micro-viz pfx-micro-viz__stacked" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
364
403
  <span class="pfx-micro-viz__segments">${bars}</span>
365
404
  <span class="pfx-micro-viz__meta">${meta}</span>
366
405
  </span>`;
@@ -374,9 +413,15 @@ function resolveBulletMax(visualization) {
374
413
  toFiniteNumber(visualization.value) ?? 0,
375
414
  visualization.target ?? 0,
376
415
  visualization.total ?? 0,
416
+ visualization.max ?? 0,
377
417
  ...thresholdValues
378
418
  ].filter((v) => Number.isFinite(v) && v > 0);
379
- return Math.max(...values, 100);
419
+ const fallbackCeiling = visualization.total && visualization.total > 0
420
+ ? visualization.total
421
+ : visualization.max && visualization.max > 0
422
+ ? visualization.max
423
+ : 100;
424
+ return Math.max(...values, fallbackCeiling);
380
425
  }
381
426
  function renderBulletVisualizationHtml(visualization, options) {
382
427
  const currentValue = toFiniteNumber(visualization.value);
@@ -387,6 +432,7 @@ function renderBulletVisualizationHtml(visualization, options) {
387
432
  const target = toFiniteNumber(visualization.target);
388
433
  const targetLeft = target === undefined ? null : percent(target, max);
389
434
  const isTable = visualization.surface === 'table-cell';
435
+ const suffix = visualization.valueSuffix ?? '';
390
436
  // thresholds: sort ascending by boundary value so bands draw continuously regardless of declaration order
391
437
  const thresholds = visualization.thresholds ?? [];
392
438
  const sortedThresholds = [...thresholds].sort((a, b) => {
@@ -418,11 +464,19 @@ function renderBulletVisualizationHtml(visualization, options) {
418
464
  <span>${escapeHtml(formatMicroNumber(max, options.locale))}</span>
419
465
  </div>`;
420
466
  const showCompactValue = isTable && visualization.compactValue !== false;
467
+ const useCompactNotation = visualization.compactValue === true;
421
468
  const compactValueHtml = showCompactValue
422
- ? `<span class="pfx-micro-bullet__compact-value">${escapeHtml(formatMicroNumber(currentValue, options.locale))}${escapeHtml(visualization.valueSuffix ?? '')}</span>`
469
+ ? `<span class="pfx-micro-bullet__compact-value">${escapeHtml(formatMicroNumber(currentValue, options.locale, useCompactNotation))}${escapeHtml(suffix)}</span>`
423
470
  : '';
471
+ const currentStr = `${formatMicroNumber(currentValue, options.locale)}${suffix}`;
472
+ const diff = target !== undefined ? currentValue - target : undefined;
473
+ const diffSign = diff !== undefined && diff > 0 ? '+' : '';
474
+ const bulletReadout = target !== undefined && diff !== undefined
475
+ ? `${currentStr} / Target ${formatMicroNumber(target, options.locale)}${suffix} (${diffSign}${formatMicroNumber(diff, options.locale)}${suffix})`
476
+ : `${currentStr} / ${formatMicroNumber(max, options.locale)}${suffix}`;
477
+ const tabIndexAttr = isTable ? '' : ' tabindex="0"';
424
478
  return `
425
- <span class="pfx-micro-viz pfx-micro-viz__bullet" data-tone="${escapeHtml(visualization.tone ?? 'info')}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
479
+ <span class="pfx-micro-viz pfx-micro-viz__bullet"${tabIndexAttr} data-tone="${escapeHtml(visualization.tone ?? 'info')}" data-micro-readout="${escapeHtml(bulletReadout)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
426
480
  ${topHtml}
427
481
  <span class="pfx-micro-bullet__track">
428
482
  ${bandsHtml}
@@ -455,8 +509,19 @@ function renderDeltaVisualizationHtml(visualization, options) {
455
509
  ? 'down'
456
510
  : 'stable';
457
511
  const aria = visualization.ariaLabel ?? `${visualization.fallbackText}: ${directionLabel} ${text}`;
512
+ const target = toFiniteNumber(visualization.target);
513
+ const baseline = toFiniteNumber(visualization.baseline);
514
+ const isTable = visualization.surface === 'table-cell';
515
+ const deltaReadout = target !== undefined
516
+ ? `Target ${formatMicroNumber(target, options.locale)}${valueSuffix}`
517
+ : baseline !== undefined
518
+ ? `Base ${formatMicroNumber(baseline, options.locale)}${valueSuffix}`
519
+ : null;
520
+ const readoutAttr = deltaReadout
521
+ ? `${isTable ? '' : ' tabindex="0"'} data-micro-readout="${escapeHtml(deltaReadout)}"`
522
+ : '';
458
523
  return `
459
- <span class="pfx-micro-viz pfx-micro-viz__delta" data-tone="${escapeHtml(tone)}" data-direction="${escapeHtml(direction)}" role="img" aria-label="${escapeHtml(aria)}">
524
+ <span class="pfx-micro-viz pfx-micro-viz__delta" data-tone="${escapeHtml(tone)}" data-direction="${escapeHtml(direction)}"${readoutAttr} role="img" aria-label="${escapeHtml(aria)}">
460
525
  <span class="pfx-micro-viz__delta-symbol" aria-hidden="true">${symbol}</span>
461
526
  <span>${escapeHtml(text)}</span>
462
527
  </span>`;
@@ -470,7 +535,14 @@ function percent(value, total) {
470
535
  function toFiniteNumber(value) {
471
536
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
472
537
  }
473
- function formatMicroNumber(value, locale) {
538
+ function formatMicroNumber(value, locale, compactValue = false) {
539
+ if (compactValue && Math.abs(value) >= 1000) {
540
+ return new Intl.NumberFormat(locale || undefined, {
541
+ notation: 'compact',
542
+ compactDisplay: 'short',
543
+ maximumFractionDigits: 1,
544
+ }).format(value);
545
+ }
474
546
  return new Intl.NumberFormat(locale || undefined, {
475
547
  maximumFractionDigits: Math.abs(value) < 10 ? 2 : 0,
476
548
  }).format(value);
@@ -545,6 +617,9 @@ function normalizeNumericEntries(value) {
545
617
  }
546
618
  return value
547
619
  .map((item) => {
620
+ if (typeof item === 'number') {
621
+ return Number.isFinite(item) ? { value: item } : null;
622
+ }
548
623
  if (!item || typeof item !== 'object' || !Number.isFinite(item.value)) {
549
624
  return null;
550
625
  }
@@ -605,6 +680,7 @@ function renderRadialVisualizationHtml(visualization, options) {
605
680
  const total = visualization.total && visualization.total > 0 ? visualization.total : 100;
606
681
  const pct = Math.max(0, Math.min(100, (value / total) * 100));
607
682
  const tone = visualization.tone ?? 'info';
683
+ const suffix = visualization.valueSuffix ?? '';
608
684
  const dashArray = `${pct.toFixed(1)}, 100`;
609
685
  const pctText = `${Math.round(pct)}%`;
610
686
  const aria = visualization.ariaLabel ?? `${visualization.fallbackText} (${pctText})`;
@@ -615,8 +691,10 @@ function renderRadialVisualizationHtml(visualization, options) {
615
691
  const externalText = isTable
616
692
  ? `<span class="pfx-micro-radial-value">${escapeHtml(pctText)}</span>`
617
693
  : '';
694
+ const radialReadout = `${formatMicroNumber(value, options.locale)}${suffix} / ${formatMicroNumber(total, options.locale)}${suffix} (${pctText})`;
695
+ const tabIndexAttr = isTable ? '' : ' tabindex="0"';
618
696
  return `
619
- <span class="pfx-micro-viz pfx-micro-viz__radial" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(aria)}">
697
+ <span class="pfx-micro-viz pfx-micro-viz__radial"${tabIndexAttr} data-tone="${escapeHtml(tone)}" data-micro-readout="${escapeHtml(radialReadout)}" role="img" aria-label="${escapeHtml(aria)}">
620
698
  <svg class="pfx-micro-radial-svg" viewBox="0 0 36 36">
621
699
  <circle class="pfx-micro-radial-svg__track" cx="18" cy="18" r="15.9155" />
622
700
  <circle class="pfx-micro-radial-svg__bar" cx="18" cy="18" r="15.9155" stroke-dasharray="${dashArray}" transform="rotate(-90 18 18)" />
@@ -633,11 +711,14 @@ function renderHarveyBallVisualizationHtml(visualization, options) {
633
711
  const total = visualization.total && visualization.total > 0 ? visualization.total : 100;
634
712
  const pct = Math.max(0, Math.min(100, (value / total) * 100));
635
713
  const tone = visualization.tone ?? 'info';
714
+ const isTable = visualization.surface === 'table-cell';
715
+ const tabIndexAttr = isTable ? '' : ' tabindex="0"';
636
716
  const strokeLength = pct * 0.50265;
637
717
  const dashArray = `${strokeLength.toFixed(3)} 50.265`;
638
718
  const aria = visualization.ariaLabel ?? `${visualization.fallbackText} (${Math.round(pct)}%)`;
719
+ const harveyReadout = `${formatMicroNumber(value, options.locale)}/${formatMicroNumber(total, options.locale)} (${Math.round(pct)}%)`;
639
720
  return `
640
- <span class="pfx-micro-viz pfx-micro-viz__harvey" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(aria)}">
721
+ <span class="pfx-micro-viz pfx-micro-viz__harvey"${tabIndexAttr} data-tone="${escapeHtml(tone)}" data-micro-readout="${escapeHtml(harveyReadout)}" role="img" aria-label="${escapeHtml(aria)}">
641
722
  <svg class="pfx-micro-harvey-svg" viewBox="0 0 36 36">
642
723
  <circle class="pfx-micro-harvey-svg__track" cx="18" cy="18" r="16" />
643
724
  <circle class="pfx-micro-harvey-svg__bar" cx="18" cy="18" r="8" stroke-dasharray="${dashArray}" transform="rotate(-90 18 18)" />
@@ -649,19 +730,57 @@ function renderHarveyBallVisualizationHtml(visualization, options) {
649
730
  </span>
650
731
  </span>`;
651
732
  }
652
- function getPointsCoordinates(points, width, height, padding) {
733
+ function resolveTrendBounds(points, visualization, anchorZero = false) {
734
+ const values = points.map((p) => p.value);
735
+ const refValue = toFiniteNumber(visualization?.target) ?? toFiniteNumber(visualization?.baseline);
736
+ const explicitMin = toFiniteNumber(visualization?.min);
737
+ const explicitMax = toFiniteNumber(visualization?.max);
738
+ const baseCandidates = anchorZero ? [...values, 0] : [...values];
739
+ if (refValue !== undefined) {
740
+ baseCandidates.push(refValue);
741
+ }
742
+ const minVal = explicitMin !== undefined ? Math.min(explicitMin, ...baseCandidates) : Math.min(...baseCandidates);
743
+ const maxVal = explicitMax !== undefined ? Math.max(explicitMax, ...baseCandidates) : Math.max(...baseCandidates);
744
+ const valRange = maxVal - minVal === 0 ? 1 : maxVal - minVal;
745
+ return { minVal, maxVal, valRange, refValue };
746
+ }
747
+ function getPointsCoordinates(points, width, height, padding, visualization) {
653
748
  if (points.length < 2)
654
749
  return [];
655
- const values = points.map(p => p.value);
656
- const minVal = Math.min(...values);
657
- const maxVal = Math.max(...values);
658
- const valRange = maxVal - minVal === 0 ? 1 : maxVal - minVal;
750
+ const { minVal, valRange } = resolveTrendBounds(points, visualization, false);
659
751
  return points.map((p, index) => {
660
752
  const x = padding + (index / (points.length - 1)) * (width - 2 * padding);
661
753
  const y = height - padding - ((p.value - minVal) / valRange) * (height - 2 * padding);
662
754
  return { x, y };
663
755
  });
664
756
  }
757
+ function renderTrendReferenceLineHtml(points, width, height, padding, visualization, anchorZero = false) {
758
+ const { minVal, valRange, refValue } = resolveTrendBounds(points, visualization, anchorZero);
759
+ if (refValue === undefined) {
760
+ return '';
761
+ }
762
+ const refY = height - padding - ((refValue - minVal) / valRange) * (height - 2 * padding);
763
+ return `<line class="pfx-micro-trend__ref-line" x1="${padding}" y1="${refY.toFixed(1)}" x2="${width - padding}" y2="${refY.toFixed(1)}" stroke="currentColor" stroke-opacity="0.38" stroke-width="1" stroke-dasharray="2 2" />`;
764
+ }
765
+ function renderTrendScrubLayerHtml(points, coords, width, height, visualization, options) {
766
+ const defaultTone = visualization.tone ?? 'info';
767
+ const suffix = visualization.valueSuffix ?? '';
768
+ const slicesHtml = points
769
+ .map((point, index) => {
770
+ const coord = coords[index] ?? { x: 50, y: 15 };
771
+ const xPct = ((coord.x / width) * 100).toFixed(1);
772
+ const yPct = ((coord.y / height) * 100).toFixed(1);
773
+ const pointTone = point.tone ??
774
+ (visualization.thresholds?.length
775
+ ? resolvePraxisMicroVisualizationTone(point.value, visualization.thresholds)?.tone ?? defaultTone
776
+ : defaultTone);
777
+ const labelPrefix = point.label ? `${point.label}: ` : `#${index + 1}: `;
778
+ const readout = `${labelPrefix}${formatMicroNumber(point.value, options.locale)}${suffix}`;
779
+ return `<span class="pfx-micro-trend__scrub-slice" data-tone="${escapeHtml(pointTone)}" data-micro-readout="${escapeHtml(readout)}" style="--pfx-scrub-x:${xPct}%;--pfx-scrub-y:${yPct}%;"></span>`;
780
+ })
781
+ .join('');
782
+ return `<span class="pfx-micro-trend__scrub-layer" aria-hidden="true">${slicesHtml}</span>`;
783
+ }
665
784
  function renderLineVisualizationHtml(visualization, options) {
666
785
  const points = visualization.points ?? [];
667
786
  const tone = visualization.tone ?? 'info';
@@ -671,8 +790,9 @@ function renderLineVisualizationHtml(visualization, options) {
671
790
  const width = 100;
672
791
  const height = 30;
673
792
  const padding = 4;
674
- const coords = getPointsCoordinates(points, width, height, padding);
793
+ const coords = getPointsCoordinates(points, width, height, padding, visualization);
675
794
  const pathD = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
795
+ const refLineHtml = renderTrendReferenceLineHtml(points, width, height, padding, visualization, false);
676
796
  // Highlight extreme/boundary values
677
797
  let minIdx = 0;
678
798
  let maxIdx = 0;
@@ -693,6 +813,7 @@ function renderLineVisualizationHtml(visualization, options) {
693
813
  markers.push({ x: coords[maxIdx].x, y: coords[maxIdx].y, tone: points[maxIdx].tone ?? 'success' });
694
814
  }
695
815
  const circlesHtml = markers.map(c => `<circle cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="2.5" class="pfx-micro-trend__marker" data-tone="${escapeHtml(c.tone)}" />`).join('');
816
+ const scrubLayerHtml = renderTrendScrubLayerHtml(points, coords, width, height, visualization, options);
696
817
  const isTable = visualization.surface === 'table-cell';
697
818
  const firstPoint = points[0];
698
819
  const lastPoint = points[points.length - 1];
@@ -711,9 +832,11 @@ function renderLineVisualizationHtml(visualization, options) {
711
832
  ${leftHtml}
712
833
  <span class="pfx-micro-viz pfx-micro-viz__line" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
713
834
  <svg class="pfx-micro-line__svg" viewBox="0 0 100 30">
835
+ ${refLineHtml}
714
836
  <path class="pfx-micro-line__path" d="${pathD}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
715
837
  ${circlesHtml}
716
838
  </svg>
839
+ ${scrubLayerHtml}
717
840
  </span>
718
841
  ${rightHtml}
719
842
  </span>`;
@@ -727,8 +850,9 @@ function renderAreaVisualizationHtml(visualization, options) {
727
850
  const width = 100;
728
851
  const height = 30;
729
852
  const padding = 4;
730
- const coords = getPointsCoordinates(points, width, height, padding);
853
+ const coords = getPointsCoordinates(points, width, height, padding, visualization);
731
854
  const lineD = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
855
+ const refLineHtml = renderTrendReferenceLineHtml(points, width, height, padding, visualization, false);
732
856
  const firstX = coords[0].x.toFixed(1);
733
857
  const lastX = coords[coords.length - 1].x.toFixed(1);
734
858
  const areaD = `${lineD} L ${lastX} ${height} L ${firstX} ${height} Z`;
@@ -752,6 +876,7 @@ function renderAreaVisualizationHtml(visualization, options) {
752
876
  markers.push({ x: coords[maxIdx].x, y: coords[maxIdx].y, tone: points[maxIdx].tone ?? 'success' });
753
877
  }
754
878
  const circlesHtml = markers.map(c => `<circle cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="2.5" class="pfx-micro-trend__marker" data-tone="${escapeHtml(c.tone)}" />`).join('');
879
+ const scrubLayerHtml = renderTrendScrubLayerHtml(points, coords, width, height, visualization, options);
755
880
  const isTable = visualization.surface === 'table-cell';
756
881
  const firstPoint = points[0];
757
882
  const lastPoint = points[points.length - 1];
@@ -770,10 +895,12 @@ function renderAreaVisualizationHtml(visualization, options) {
770
895
  ${leftHtml}
771
896
  <span class="pfx-micro-viz pfx-micro-viz__area" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
772
897
  <svg class="pfx-micro-area__svg" viewBox="0 0 100 30">
898
+ ${refLineHtml}
773
899
  <path class="pfx-micro-area__fill" d="${areaD}" fill="currentColor" fill-opacity="0.15" />
774
900
  <path class="pfx-micro-area__path" d="${lineD}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
775
901
  ${circlesHtml}
776
902
  </svg>
903
+ ${scrubLayerHtml}
777
904
  </span>
778
905
  ${rightHtml}
779
906
  </span>`;
@@ -787,21 +914,22 @@ function renderColumnVisualizationHtml(visualization, options) {
787
914
  const width = 100;
788
915
  const height = 30;
789
916
  const padding = 4;
790
- const values = points.map(p => p.value);
791
- const maxVal = Math.max(...values, 0);
792
- const minVal = Math.min(...values, 0);
793
- const valRange = maxVal - minVal === 0 ? 1 : maxVal - minVal;
917
+ const { minVal, valRange } = resolveTrendBounds(points, visualization, true);
918
+ const refLineHtml = renderTrendReferenceLineHtml(points, width, height, padding, visualization, true);
794
919
  const colWidth = Math.max(2, (width - 2 * padding) / points.length - 2);
920
+ const colCoords = [];
795
921
  const columnsHtml = points.map((p, i) => {
796
922
  const x = padding + i * ((width - 2 * padding) / points.length) + 1;
797
923
  const rectHeight = Math.max(2, ((p.value - minVal) / valRange) * (height - 2 * padding));
798
924
  const rectY = height - padding - rectHeight;
925
+ colCoords.push({ x: x + colWidth / 2, y: rectY });
799
926
  const itemTone = p.tone ??
800
927
  (visualization.thresholds?.length
801
928
  ? resolvePraxisMicroVisualizationTone(p.value, visualization.thresholds)?.tone ?? tone
802
929
  : tone);
803
- return `<rect class="pfx-micro-column__bar" x="${x.toFixed(1)}" y="${rectY.toFixed(1)}" width="${colWidth.toFixed(1)}" height="${rectHeight.toFixed(1)}" fill="currentColor" data-tone="${escapeHtml(itemTone)}" />`;
930
+ return `<rect class="pfx-micro-column__bar" x="${x.toFixed(1)}" y="${rectY.toFixed(1)}" width="${colWidth.toFixed(1)}" height="${rectHeight.toFixed(1)}" rx="1" fill="currentColor" data-tone="${escapeHtml(itemTone)}" />`;
804
931
  }).join('');
932
+ const scrubLayerHtml = renderTrendScrubLayerHtml(points, colCoords, width, height, visualization, options);
805
933
  const isTable = visualization.surface === 'table-cell';
806
934
  const firstPoint = points[0];
807
935
  const lastPoint = points[points.length - 1];
@@ -821,7 +949,9 @@ function renderColumnVisualizationHtml(visualization, options) {
821
949
  <span class="pfx-micro-viz pfx-micro-viz__column" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
822
950
  <svg class="pfx-micro-column__svg" viewBox="0 0 100 30">
823
951
  ${columnsHtml}
952
+ ${refLineHtml}
824
953
  </svg>
954
+ ${scrubLayerHtml}
825
955
  </span>
826
956
  ${rightHtml}
827
957
  </span>`;
@@ -851,14 +981,23 @@ function renderProcessFlowVisualizationHtml(visualization, options) {
851
981
  const isLast = index === items.length - 1;
852
982
  const itemLabel = stepLabels[index];
853
983
  const label = !isTable && item.label ? `<span class="pfx-micro-step__label"${labelInlineStyle}>${escapeHtml(item.label)}</span>` : '';
984
+ const isActiveStep = itemState === 'active' ||
985
+ itemState === 'current' ||
986
+ itemState === 'in_progress' ||
987
+ item.icon === 'sync' ||
988
+ item.icon === 'pending' ||
989
+ item.icon === 'pending_actions' ||
990
+ item.icon === 'schedule' ||
991
+ (!itemState && itemTone === 'warning');
992
+ const stepReadout = `${index + 1}/${items.length} · ${itemLabel}`;
854
993
  const connector = !isLast ? `<span class="pfx-micro-step__connector" data-tone="${escapeHtml(itemTone)}"></span>` : '';
855
994
  const stepCircle = `
856
- <span class="pfx-micro-step__node" data-tone="${escapeHtml(itemTone)}" aria-hidden="true">
995
+ <span class="pfx-micro-step__node" data-tone="${escapeHtml(itemTone)}"${isActiveStep ? ' data-active="true"' : ''} aria-hidden="true">
857
996
  ${item.icon ? `<span class="material-icons pfx-micro-step__icon" aria-hidden="true">${escapeHtml(item.icon)}</span>` : `${index + 1}`}
858
997
  </span>`;
859
998
  return `
860
999
  <div class="pfx-micro-step" data-state="${escapeHtml(itemState)}" data-tone="${escapeHtml(itemTone)}">
861
- <div class="pfx-micro-step__node-wrapper" title="${escapeHtml(itemLabel)}">
1000
+ <div class="pfx-micro-step__node-wrapper" data-micro-readout="${escapeHtml(stepReadout)}" title="${escapeHtml(itemLabel)}">
862
1001
  ${stepCircle}
863
1002
  ${label}
864
1003
  </div>
@@ -2015,10 +2154,14 @@ class SchemaNormalizationKernel {
2015
2154
  out.maxLength = Number(ui.maxLength);
2016
2155
  if (ui.maxLengthMessage !== undefined)
2017
2156
  out.maxLengthMessage = ui.maxLengthMessage;
2018
- if (ui.min !== undefined)
2019
- out.min = Number(ui.min);
2020
- if (ui.max !== undefined)
2021
- out.max = Number(ui.max);
2157
+ if (ui.min !== undefined) {
2158
+ const parsedMin = Number(ui.min);
2159
+ out.min = Number.isFinite(parsedMin) ? parsedMin : String(ui.min).trim();
2160
+ }
2161
+ if (ui.max !== undefined) {
2162
+ const parsedMax = Number(ui.max);
2163
+ out.max = Number.isFinite(parsedMax) ? parsedMax : String(ui.max).trim();
2164
+ }
2022
2165
  if (ui.minMessage !== undefined)
2023
2166
  out.minMessage = ui.minMessage;
2024
2167
  if (ui.maxMessage !== undefined)
@@ -2155,14 +2298,18 @@ class SchemaNormalizationKernel {
2155
2298
  return ['number', 'integer', 'int', 'float', 'double', 'decimal'].includes(token);
2156
2299
  }
2157
2300
  resolveNumericFormatToken(prop, ui, resolvedType) {
2301
+ if (ui?.numberFormat?.percentage === true || ui?.['numberFormat.percentage'] === true) {
2302
+ return 'percent';
2303
+ }
2304
+ const explicitUiFormat = this.resolveNonEmptyStringToken(ui?.numericFormat, ui?.numberFormat?.style, ui?.['numberFormat.style']);
2305
+ if (explicitUiFormat) {
2306
+ return explicitUiFormat;
2307
+ }
2158
2308
  const typeToken = resolvedType ?? prop?.type;
2159
2309
  if (!this.isNumericSchemaType(typeToken)) {
2160
2310
  return undefined;
2161
2311
  }
2162
- if (ui?.numberFormat?.percentage === true || ui?.['numberFormat.percentage'] === true) {
2163
- return 'percent';
2164
- }
2165
- return this.resolveNonEmptyStringToken(ui?.numericFormat, ui?.format, ui?.numberFormat?.style, ui?.['numberFormat.style'], prop?.format);
2312
+ return this.resolveNonEmptyStringToken(ui?.format, prop?.format);
2166
2313
  }
2167
2314
  /**
2168
2315
  * Converte o schema bruto do backend (OpenAPI + x-ui) em FieldDefinition[] tipado.
@@ -2530,10 +2677,22 @@ class SchemaNormalizationKernel {
2530
2677
  field.numericStep = Number(ui.numericStep);
2531
2678
  }
2532
2679
  if (ui.numericMin !== undefined) {
2533
- field.numericMin = Number(ui.numericMin);
2680
+ const parsedMin = Number(ui.numericMin);
2681
+ if (Number.isFinite(parsedMin)) {
2682
+ field.numericMin = parsedMin;
2683
+ }
2684
+ else if (field.min === undefined && String(ui.numericMin).trim().length > 0) {
2685
+ field.min = String(ui.numericMin).trim();
2686
+ }
2534
2687
  }
2535
2688
  if (ui.numericMax !== undefined) {
2536
- field.numericMax = Number(ui.numericMax);
2689
+ const parsedMax = Number(ui.numericMax);
2690
+ if (Number.isFinite(parsedMax)) {
2691
+ field.numericMax = parsedMax;
2692
+ }
2693
+ else if (field.max === undefined && String(ui.numericMax).trim().length > 0) {
2694
+ field.max = String(ui.numericMax).trim();
2695
+ }
2537
2696
  }
2538
2697
  if (ui.numericMaxLength !== undefined) {
2539
2698
  field.numericMaxLength = Number(ui.numericMaxLength);
@@ -2541,6 +2700,145 @@ class SchemaNormalizationKernel {
2541
2700
  if (ui.buttons !== undefined) {
2542
2701
  field.buttons = this.parseButtons(ui.buttons);
2543
2702
  }
2703
+ // -------------------------------------------------------------------
2704
+ // Inline filter and specialized control metadata (x-ui)
2705
+ // -------------------------------------------------------------------
2706
+ const inlinePassThroughKeys = [
2707
+ 'prefixIcon',
2708
+ 'suffixIcon',
2709
+ 'prefixIconColor',
2710
+ 'suffixIconColor',
2711
+ 'chipLeadingIcon',
2712
+ 'chipTrailingIcon',
2713
+ 'chipLeadingIconColor',
2714
+ 'chipTrailingIconColor',
2715
+ 'clearIcon',
2716
+ 'clearIconColor',
2717
+ 'showQuickClear',
2718
+ 'clearable',
2719
+ 'inlineAutoSize',
2720
+ 'inlineAutoSizeMaxPx',
2721
+ 'inlineAutoSizeMaxVw',
2722
+ 'inlineOverlay',
2723
+ 'inlineChipDisplay',
2724
+ 'inlineChipSummaryTemplate',
2725
+ 'inlineChipUnitLabel',
2726
+ 'inlinePanelTitle',
2727
+ 'inlineQuickPresets',
2728
+ 'inlineQuickPresetsApplyMode',
2729
+ 'inlineTexts',
2730
+ 'shortcuts',
2731
+ 'showShortcuts',
2732
+ 'quickPresets',
2733
+ 'quickPresetsAuto',
2734
+ 'quickPresetsLabels',
2735
+ 'granularity',
2736
+ 'defaultGranularity',
2737
+ 'selectionGrid',
2738
+ 'showSlider',
2739
+ 'showInputs',
2740
+ 'minPeriod',
2741
+ 'maxPeriod',
2742
+ 'minYear',
2743
+ 'maxYear',
2744
+ 'minTime',
2745
+ 'maxTime',
2746
+ 'stepMinutes',
2747
+ 'startField',
2748
+ 'endField',
2749
+ 'startLabel',
2750
+ 'endLabel',
2751
+ 'startPlaceholder',
2752
+ 'endPlaceholder',
2753
+ 'inputMinLabel',
2754
+ 'inputMaxLabel',
2755
+ 'maxVisibleChips',
2756
+ 'maxVisibleBadges',
2757
+ 'showSelectAll',
2758
+ 'selectAllLabel',
2759
+ 'clearSelectionLabel',
2760
+ 'searchPlaceholder',
2761
+ 'noResultsText',
2762
+ 'allowSelectAll',
2763
+ 'governedPaletteRef',
2764
+ 'palette',
2765
+ 'paletteColors',
2766
+ 'paletteSettings',
2767
+ 'documentType',
2768
+ 'version',
2769
+ 'nodes',
2770
+ 'nodeIdField',
2771
+ 'parentIdField',
2772
+ 'childrenField',
2773
+ 'lazyLoad',
2774
+ 'marks',
2775
+ 'semanticBands',
2776
+ 'distribution',
2777
+ 'discrete',
2778
+ 'showTicks',
2779
+ 'trueLabel',
2780
+ 'falseLabel',
2781
+ 'currencyCode',
2782
+ 'locale',
2783
+ 'precision',
2784
+ 'rangeMode',
2785
+ 'maxRating',
2786
+ 'allowHalf',
2787
+ 'ratingIcon',
2788
+ 'ratingEmptyIcon',
2789
+ 'ratingHalfIcon',
2790
+ 'ratingTone',
2791
+ 'ratingToneColors',
2792
+ 'ratingLabels',
2793
+ 'distanceUnit',
2794
+ 'distanceBaseUnit',
2795
+ 'distanceUnits',
2796
+ 'distanceAllowUnitToggle',
2797
+ 'distanceUnitLabels',
2798
+ 'distanceDisplayDecimals',
2799
+ 'distanceDecimalsKm',
2800
+ 'distanceDecimalsMi',
2801
+ 'distancePresets',
2802
+ 'distanceSubtitle',
2803
+ 'distanceRadialRings',
2804
+ 'distanceAccentColor',
2805
+ 'distanceRingColor',
2806
+ 'distanceTrackColor',
2807
+ 'distanceGlowColor',
2808
+ 'distanceGradientLowColor',
2809
+ 'distanceGradientMidColor',
2810
+ 'distanceGradientHighColor',
2811
+ 'distanceRangeBandOpacity',
2812
+ 'pipelineOptions',
2813
+ 'pipelineColors',
2814
+ 'pipelineTexts',
2815
+ 'scoreBands',
2816
+ 'priorityBands',
2817
+ 'severityBands',
2818
+ 'scorePalette',
2819
+ 'scoreTexts',
2820
+ 'relativePeriodOptions',
2821
+ 'relativePeriodPresets',
2822
+ 'relativePeriodTexts',
2823
+ 'sentimentOptions',
2824
+ 'sentimentPalette',
2825
+ 'sentimentTexts',
2826
+ 'colorLabelOptions',
2827
+ 'colorLabelPalette',
2828
+ 'colorLabelTexts',
2829
+ 'timeOptions',
2830
+ 'inlineTimeQuickPresets',
2831
+ 'inlineVisualStyle',
2832
+ 'percentPresets',
2833
+ 'numberFormat',
2834
+ 'valueLabelFormat',
2835
+ 'scale',
2836
+ ];
2837
+ for (const key of inlinePassThroughKeys) {
2838
+ if (ui[key] !== undefined && field[key] === undefined) {
2839
+ field[key] = ui[key];
2840
+ }
2841
+ }
2544
2842
  fields.push(field);
2545
2843
  }
2546
2844
  if (fields.length === 0 &&
@@ -2587,7 +2885,21 @@ class SchemaNormalizationKernel {
2587
2885
  }
2588
2886
  // Compatibilidade para schemas de range publicados como oneOf(array|object).
2589
2887
  const controlToken = String(ui?.controlType || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '');
2590
- const isRangeControl = ['daterange', 'datetimerange', 'rangeslider', 'pricerange'].includes(controlToken);
2888
+ const isRangeControl = [
2889
+ 'daterange',
2890
+ 'datetimerange',
2891
+ 'rangeslider',
2892
+ 'pricerange',
2893
+ 'inlinerange',
2894
+ 'inlinecurrencyrange',
2895
+ 'inlinedaterange',
2896
+ 'inlineperiodrange',
2897
+ 'inlineyearrange',
2898
+ 'inlinemonthrange',
2899
+ 'inlinetimerange',
2900
+ 'inlinedistanceradius',
2901
+ 'inlinescorepriority',
2902
+ ].includes(controlToken);
2591
2903
  if (isRangeControl && nonNullTypes.includes('array')) {
2592
2904
  return 'array';
2593
2905
  }