@juspay/svelte-ui-components 2.102.0 → 2.104.0

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.
@@ -36,7 +36,8 @@
36
36
  nodeColorResolver,
37
37
  minLinkWidth = 1,
38
38
  dataLabelOffsetX = 0,
39
- disableDimOnHover = false
39
+ disableDimOnHover = false,
40
+ firstColumnLabelSide = 'left'
40
41
  }: SankeyChartProperties = $props();
41
42
 
42
43
  // ── State ──────────────────────────────────────────────────────
@@ -54,20 +55,44 @@
54
55
  let format = $derived(valueFormat ?? formatNumber);
55
56
  let isEmpty = $derived(nodes.length === 0);
56
57
  const MARGIN = 40;
57
- // A 12px label's rendered line box measures ~16px (≈1.33em) across common
58
- // font stacks; two label centres closer than this overlap visibly.
59
- const LABEL_LINE_PX = 16;
60
58
 
61
59
  // Real text measurement via the shared canvas-backed helper (exact on the
62
60
  // client, 0.6em/char heuristic under SSR/tests). Character estimates used
63
61
  // to both over-reserve the right label gutter (dead canvas) and under-budget
64
62
  // uppercase-heavy labels (text sliding under the next column's bars).
63
+ // Weight and family are part of the spec: a 700-weight label is measurably
64
+ // wider than a 400 one, and the app's rendered font rarely matches the
65
+ // measurement default — budgets must be computed at the rendered style or
66
+ // borderline labels truncate (or overflow) for no visible reason.
67
+ // Family only enters the spec once the document's fonts have loaded: measuring
68
+ // a not-yet-loaded webfont silently measures its fallback (usually wider) and
69
+ // the width cache would pin that stale value under the loaded font's key.
70
+ // Until then the measurement default applies — same behaviour as before.
71
+ // One-shot promise subscription at init (browser only) — nothing to tear
72
+ // down, and the $state write re-derives every measurement consumer.
73
+ let fontsLoaded = $state(typeof document !== 'undefined' && document.fonts?.status === 'loaded');
74
+ if (typeof document !== 'undefined' && document.fonts?.status !== 'loaded') {
75
+ document.fonts?.ready.then(() => {
76
+ fontsLoaded = true;
77
+ });
78
+ }
79
+ let chartFontFamily = $derived(
80
+ fontsLoaded && containerEl ? getComputedStyle(containerEl).fontFamily : null
81
+ );
65
82
  let labelFont = $derived({
66
- size: containerEl ? readCssVarPx(containerEl, '--sankey-label-font-size', 12) : 12
83
+ size: containerEl ? readCssVarPx(containerEl, '--sankey-label-font-size', 12) : 12,
84
+ weight: containerEl ? readCssVarPx(containerEl, '--sankey-label-font-weight', 400) : 400,
85
+ family: chartFontFamily
67
86
  });
68
87
  let colLabelFont = $derived({
69
- size: containerEl ? readCssVarPx(containerEl, '--sankey-col-label-font-size', 11) : 11
88
+ size: containerEl ? readCssVarPx(containerEl, '--sankey-col-label-font-size', 11) : 11,
89
+ weight: containerEl ? readCssVarPx(containerEl, '--sankey-col-label-font-weight', 400) : 400,
90
+ family: chartFontFamily
70
91
  });
92
+ // A label's rendered line box measures ≈1.33em across common font stacks;
93
+ // two label centres closer than this overlap visibly. Derived from the
94
+ // tokened size so consumers that scale labels keep honest de-collision.
95
+ let labelLinePx = $derived(Math.ceil(labelFont.size * 1.33));
71
96
 
72
97
  // Final-column labels render to the RIGHT of their node; the bare 40px margin is
73
98
  // nowhere near enough for real funnel labels ("PARTIALLY_FAILED (1,234)"), so they
@@ -101,8 +126,11 @@
101
126
  // the room falls below an ellipsis, vanish entirely. Mirror the sink gutter:
102
127
  // reserve a capped left gutter sized from the source-node labels (sources are what
103
128
  // land in the first column) and shift the diagram right by it.
129
+ // With `firstColumnLabelSide: 'right'` the labels render over the outgoing
130
+ // ribbons like every other column, so no gutter is reserved at all and the
131
+ // diagram gains that width.
104
132
  let firstColumnLabelGutter = $derived.by(() => {
105
- if (!showLabels || nodes.length === 0 || chartWidth <= 0) {
133
+ if (firstColumnLabelSide === 'right' || !showLabels || nodes.length === 0 || chartWidth <= 0) {
106
134
  return 0;
107
135
  }
108
136
  const targetIds = new Set(links.map((link) => link.target));
@@ -184,7 +212,7 @@
184
212
  // SVG's left edge is that gutter plus the base margin, minus the inset —
185
213
  // symmetric with the last column's sink-gutter budget below.
186
214
  const available =
187
- column === 0
215
+ column === 0 && firstColumnLabelSide === 'left'
188
216
  ? Math.max(0, firstColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
189
217
  : column === columnCount - 1
190
218
  ? Math.max(0, lastColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
@@ -222,7 +250,7 @@
222
250
  continue;
223
251
  }
224
252
  const centerGap = node.y + node.height / 2 - (lastKept.y + lastKept.height / 2);
225
- if (centerGap < LABEL_LINE_PX) {
253
+ if (centerGap < labelLinePx) {
226
254
  if (node.value > lastKept.value) {
227
255
  hidden.add(lastKept.id);
228
256
  lastKept = node;
@@ -490,11 +518,11 @@
490
518
  <text
491
519
  class="sankey-label"
492
520
  class:node-dimmed={dimmed}
493
- x={node.column === 0
521
+ x={node.column === 0 && firstColumnLabelSide === 'left'
494
522
  ? node.x - 6 - dataLabelOffsetX
495
523
  : node.x + node.width + 6 + dataLabelOffsetX}
496
524
  y={node.y + node.height / 2}
497
- text-anchor={node.column === 0 ? 'end' : 'start'}
525
+ text-anchor={node.column === 0 && firstColumnLabelSide === 'left' ? 'end' : 'start'}
498
526
  dominant-baseline="middle"
499
527
  >{truncateLabel(
500
528
  showValues ? `${node.label} (${format(node.value)})` : node.label,
@@ -537,13 +565,22 @@
537
565
  .sankey-label {
538
566
  fill: var(--sankey-label-color, #333);
539
567
  font-size: var(--sankey-label-font-size, 12px);
568
+ font-weight: var(--sankey-label-font-weight, 400);
540
569
  font-family: var(--chart-font-family, inherit);
570
+ /* Highcharts-style text outline: node labels render over link ribbons, so
571
+ a halo in the chart's background colour keeps them legible at any flow
572
+ density. Off (transparent / 0) by default. */
573
+ paint-order: stroke;
574
+ stroke: var(--sankey-label-halo-color, transparent);
575
+ stroke-width: var(--sankey-label-halo-width, 0);
576
+ stroke-linejoin: round;
541
577
  pointer-events: none;
542
578
  transition: opacity var(--chart-transition-duration, 0.2s) ease;
543
579
  }
544
580
  .sankey-col-label {
545
581
  fill: var(--sankey-col-label-color, #666);
546
582
  font-size: var(--sankey-col-label-font-size, 11px);
583
+ font-weight: var(--sankey-col-label-font-weight, 400);
547
584
  font-family: var(--chart-font-family, inherit);
548
585
  pointer-events: none;
549
586
  }
@@ -84,6 +84,14 @@ export type OptionalSankeyChartProperties = {
84
84
  * to read the overall structure. Defaults to `false` (standard dim-on-hover behaviour).
85
85
  */
86
86
  disableDimOnHover?: boolean;
87
+ /**
88
+ * Which side of a first-column node its label renders on. `'left'` (default) anchors the
89
+ * label into a reserved left gutter outside the diagram. `'right'` renders it like every
90
+ * other column — to the right of the bar, over the outgoing ribbons (pair with the
91
+ * `--sankey-label-halo-*` tokens for legibility); no left gutter is reserved, so the
92
+ * diagram gains that width.
93
+ */
94
+ firstColumnLabelSide?: 'left' | 'right';
87
95
  };
88
96
  export type SankeyChartEventProperties = {
89
97
  onnodeclick?: (event: {
@@ -288,6 +288,7 @@
288
288
  }
289
289
 
290
290
  .statcard-header {
291
+ order: var(--statcard-header-order, 0);
291
292
  display: flex;
292
293
  align-items: center;
293
294
  justify-content: space-between;
@@ -362,6 +363,7 @@
362
363
  }
363
364
 
364
365
  .statcard-value-row {
366
+ order: var(--statcard-value-row-order, 0);
365
367
  display: flex;
366
368
  align-items: var(--statcard-value-row-align, baseline);
367
369
  gap: var(--statcard-value-row-gap, 8px);
@@ -391,6 +393,7 @@
391
393
  }
392
394
 
393
395
  .statcard-subtitle {
396
+ order: var(--statcard-subtitle-order, 0);
394
397
  font-size: var(--statcard-subtitle-font-size, 12px);
395
398
  font-weight: var(--statcard-subtitle-font-weight, 400);
396
399
  color: var(--statcard-subtitle-color, #9ca3af);
@@ -399,6 +402,7 @@
399
402
 
400
403
  /* Multi-row layout */
401
404
  .statcard-rows {
405
+ order: var(--statcard-rows-order, 0);
402
406
  display: flex;
403
407
  flex-direction: column;
404
408
  gap: var(--statcard-rows-gap, 0px);
@@ -473,7 +477,7 @@
473
477
  font-weight: var(--statcard-breakdown-heading-font-weight, 600);
474
478
  color: var(--statcard-breakdown-heading-color, #6b7280);
475
479
  letter-spacing: 0.04em;
476
- text-transform: uppercase;
480
+ text-transform: var(--statcard-breakdown-heading-text-transform, uppercase);
477
481
  margin-top: var(--statcard-breakdown-heading-margin-top, 6px);
478
482
  }
479
483
 
@@ -486,7 +490,9 @@
486
490
 
487
491
  .statcard-breakdown-item {
488
492
  display: flex;
489
- flex-direction: column;
493
+ flex-direction: var(--statcard-breakdown-item-flex-direction, column);
494
+ align-items: var(--statcard-breakdown-item-align-items, stretch);
495
+ flex-wrap: var(--statcard-breakdown-item-flex-wrap, nowrap);
490
496
  gap: var(--statcard-breakdown-item-gap, 2px);
491
497
  }
492
498
 
@@ -494,6 +500,7 @@
494
500
  as the caption). Previously the delta was a third stacked child, so it
495
501
  rendered on its own line beneath the number. */
496
502
  .statcard-breakdown-value-line {
503
+ order: var(--statcard-breakdown-value-line-order, 0);
497
504
  display: flex;
498
505
  align-items: baseline;
499
506
  gap: var(--statcard-breakdown-value-gap, 6px);
@@ -1,7 +1,7 @@
1
1
  export type FontSpec = {
2
2
  /** Font size in px. */
3
3
  size: number;
4
- family?: string;
4
+ family?: string | null;
5
5
  weight?: number | string;
6
6
  };
7
7
  export declare function measureText(text: string, font: FontSpec): {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.102.0",
3
+ "version": "2.104.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",