@juspay/svelte-ui-components 2.80.2 → 2.80.4

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.
@@ -4,6 +4,7 @@
4
4
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
5
5
  import { getColor } from '../_chart/colors';
6
6
  import { formatNumber, formatPercent } from '../_chart/format';
7
+ import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
7
8
 
8
9
  // ── Props ──────────────────────────────────────────────────────
9
10
 
@@ -16,6 +17,7 @@
16
17
  showValueLabels = true,
17
18
  valueFormat,
18
19
  aspectRatio = 16 / 9,
20
+ radius = DEFAULT_CHART_CORNER_RADIUS,
19
21
  testId,
20
22
  classes,
21
23
  empty,
@@ -249,7 +251,7 @@
249
251
  width={stageColumnWidth}
250
252
  height={bh}
251
253
  fill={color}
252
- rx={2}
254
+ rx={radius}
253
255
  aria-label="{stage.category}: {formatLabel(stage)}"
254
256
  onmouseenter={(event) => handleEnter(event, index)}
255
257
  onmousemove={trackMouse}
@@ -21,6 +21,13 @@ export type OptionalFunnelChartProperties = {
21
21
  * Defaults to a light-teal shared palette neutral.
22
22
  */
23
23
  connectorColor?: string;
24
+ /**
25
+ * Corner radius on each stage bar in pixels. Defaults to
26
+ * `DEFAULT_CHART_CORNER_RADIUS` (4), mirroring the design system's base
27
+ * `--radius` token. SVG `rx`/`ry` cannot read CSS `var()`, so pass this
28
+ * prop explicitly to track a changed `--radius` at runtime.
29
+ */
30
+ radius?: number;
24
31
  /**
25
32
  * Horizontal width (in SVG user units relative to total inner width) of each
26
33
  * trapezoidal slope connector. Larger values produce steeper visual drops between stages.
@@ -47,7 +47,13 @@
47
47
  leftIconLabel = 'Leading action',
48
48
  rightIconLabel = 'Trailing action',
49
49
  mandatory = false,
50
- forceError = false
50
+ forceError = false,
51
+ rows,
52
+ autoResize = false,
53
+ minRows,
54
+ maxRows,
55
+ resize = 'none',
56
+ showCount = false
51
57
  }: InputProperties = $props();
52
58
 
53
59
  export function focus() {
@@ -104,6 +110,38 @@
104
110
  const hasLeftIcon = $derived(typeof leftIcon === 'function');
105
111
  const hasRightIcon = $derived(typeof rightIcon === 'function');
106
112
 
113
+ const charCount = $derived(value?.length ?? 0);
114
+ const effectiveResize = $derived(autoResize ? 'none' : resize);
115
+
116
+ // Grow the textarea to fit its content between minRows and maxRows.
117
+ function adjustTextAreaHeight(): void {
118
+ const el = inputElement;
119
+ if (!el || !useTextArea || !autoResize) {
120
+ return;
121
+ }
122
+ el.style.height = 'auto';
123
+ const styles = window.getComputedStyle(el);
124
+ const lineHeight = parseFloat(styles.lineHeight) || 20;
125
+ const verticalPadding = parseFloat(styles.paddingTop) + parseFloat(styles.paddingBottom);
126
+ const border = parseFloat(styles.borderTopWidth) + parseFloat(styles.borderBottomWidth);
127
+ const lower = minRows ?? rows ?? 2;
128
+ const minHeight = lower * lineHeight + verticalPadding + border;
129
+ const maxHeight =
130
+ maxRows != null ? maxRows * lineHeight + verticalPadding + border : Number.POSITIVE_INFINITY;
131
+ const nextHeight = Math.min(Math.max(el.scrollHeight, minHeight), maxHeight);
132
+ el.style.height = `${nextHeight}px`;
133
+ el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
134
+ }
135
+
136
+ // eslint-disable-next-line no-restricted-syntax
137
+ $effect(() => {
138
+ // Re-run on every value change (and on mount) while auto-resize is enabled.
139
+ void value;
140
+ if (useTextArea && autoResize) {
141
+ adjustTextAreaHeight();
142
+ }
143
+ });
144
+
107
145
  function handleOnInput(event: Event) {
108
146
  if (inputElement === null) {
109
147
  return;
@@ -232,8 +270,11 @@
232
270
  onpaste={handleOnPaste}
233
271
  onclick={onClick}
234
272
  onkeydown={onKeyDown}
273
+ data-pw={testId}
235
274
  class:action-input={actionInput}
236
275
  style="--focus-border: {addFocusColor ? 1 : 0}px;"
276
+ style:resize={effectiveResize}
277
+ rows={rows ?? null}
237
278
  disabled={disable}
238
279
  bind:this={inputElement}
239
280
  maxlength={dataType === 'tel' ? null : maxLength}
@@ -321,6 +362,11 @@
321
362
  {infoMessage}
322
363
  </div>
323
364
  {/if}
365
+ {#if useTextArea && showCount && !actionInput}
366
+ <div class="input-char-count" class:at-limit={charCount >= maxLength}>
367
+ {charCount}/{maxLength}
368
+ </div>
369
+ {/if}
324
370
  </div>
325
371
 
326
372
  <style>
@@ -466,6 +512,18 @@
466
512
  padding: var(--input-info-msg-padding);
467
513
  }
468
514
 
515
+ .input-char-count {
516
+ align-self: flex-end;
517
+ font-size: var(--input-char-count-size, 12px);
518
+ color: var(--input-char-count-color, #98a2b3);
519
+ margin: var(--input-char-count-margin, 4px 0 0);
520
+ font-variant-numeric: tabular-nums;
521
+ }
522
+
523
+ .input-char-count.at-limit {
524
+ color: var(--input-char-count-limit-color, var(--input-error-msg-text-color, #fa1405));
525
+ }
526
+
469
527
  ::placeholder {
470
528
  color: var(--input-placeholder-color);
471
529
  }
@@ -22,6 +22,24 @@ export type OptionalInputProperties = {
22
22
  max?: number;
23
23
  actionInput?: boolean;
24
24
  useTextArea?: boolean;
25
+ /** Initial visible rows for the textarea (only applies when `useTextArea`). */
26
+ rows?: number;
27
+ /**
28
+ * Grow/shrink the textarea to fit its content between `minRows` and `maxRows`
29
+ * (only when `useTextArea`). Disables manual resizing while active.
30
+ */
31
+ autoResize?: boolean;
32
+ /** Lower bound (in rows) when `autoResize` is on. Defaults to `rows`. */
33
+ minRows?: number;
34
+ /** Upper bound (in rows) when `autoResize` is on; beyond this the textarea scrolls. */
35
+ maxRows?: number;
36
+ /**
37
+ * Manual resize-handle behaviour for the textarea. Defaults to `'none'` (unchanged from
38
+ * before); forced to `'none'` when `autoResize` is on.
39
+ */
40
+ resize?: 'none' | 'vertical' | 'horizontal' | 'both';
41
+ /** Show a live `current / maxLength` character counter beneath the field. */
42
+ showCount?: boolean;
25
43
  autoComplete?: HTMLInputAttributes['autocomplete'];
26
44
  name?: string;
27
45
  textTransformers?: TextTransformer[];
@@ -44,6 +44,8 @@
44
44
  xTickFormat,
45
45
  yTickFormat,
46
46
  aspectRatio = 16 / 9,
47
+ minHeight = 0,
48
+ maxHeight = Infinity,
47
49
  tooltipSnippet,
48
50
  empty,
49
51
  highlightedIndex = null,
@@ -352,7 +354,13 @@
352
354
  <Legend items={legendItems} position="top" />
353
355
  {/if}
354
356
 
355
- <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
357
+ <ChartContainer
358
+ bind:width={chartWidth}
359
+ bind:height={chartHeight}
360
+ {aspectRatio}
361
+ {minHeight}
362
+ {maxHeight}
363
+ >
356
364
  {#if gradientFill || showArea}
357
365
  <defs>
358
366
  {#each lines as line, si (si)}
@@ -95,6 +95,10 @@ export type OptionalLineChartProperties = {
95
95
  yTickFormat?: (value: number | string) => string;
96
96
  /** Width-to-height ratio for the chart. */
97
97
  aspectRatio?: number;
98
+ /** Minimum chart height in pixels, regardless of computed aspect-ratio height. */
99
+ minHeight?: number;
100
+ /** Maximum chart height in pixels, regardless of computed aspect-ratio height. */
101
+ maxHeight?: number;
98
102
  /** Custom tooltip. Receives `{x, points: [{name, y, color, label?}]}`. */
99
103
  tooltipSnippet?: Snippet<[LineChartTooltipContext]>;
100
104
  /** Content rendered when all series are empty. */
@@ -107,7 +107,7 @@
107
107
  .pill-text {
108
108
  overflow: hidden;
109
109
  text-overflow: var(--pill-text-overflow, ellipsis);
110
- white-space: nowrap;
110
+ white-space: var(--pill-text-white-space, nowrap);
111
111
  }
112
112
 
113
113
  .pill-leading-icon {
@@ -5,6 +5,7 @@
5
5
  import { computeSankeyLayout } from '../_chart/geometry';
6
6
  import { getColor } from '../_chart/colors';
7
7
  import { formatNumber } from '../_chart/format';
8
+ import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
8
9
  import { SvelteMap, SvelteSet } from 'svelte/reactivity';
9
10
 
10
11
  // ── Props ──────────────────────────────────────────────────────
@@ -18,6 +19,7 @@
18
19
  showValues = false,
19
20
  showLabels = true,
20
21
  aspectRatio = 16 / 9,
22
+ radius = DEFAULT_CHART_CORNER_RADIUS,
21
23
  maxHeight = Infinity,
22
24
  valueFormat,
23
25
  tooltipSnippet,
@@ -343,8 +345,8 @@
343
345
  y={node.y}
344
346
  width={node.width}
345
347
  height={node.height}
346
- rx={2}
347
- ry={2}
348
+ rx={radius}
349
+ ry={radius}
348
350
  fill={color}
349
351
  onmouseenter={(e) => handleNodeEnter(e, node.id)}
350
352
  onmousemove={trackMouse}
@@ -41,6 +41,13 @@ export type OptionalSankeyChartProperties = {
41
41
  nodeWidth?: number;
42
42
  nodePadding?: number;
43
43
  iterations?: number;
44
+ /**
45
+ * Corner radius on each node rect in pixels. Defaults to
46
+ * `DEFAULT_CHART_CORNER_RADIUS` (4), mirroring the design system's base
47
+ * `--radius` token. SVG `rx`/`ry` cannot read CSS `var()`, so pass this
48
+ * prop explicitly to track a changed `--radius` at runtime.
49
+ */
50
+ radius?: number;
44
51
  showValues?: boolean;
45
52
  showLabels?: boolean;
46
53
  aspectRatio?: number;
@@ -17,6 +17,7 @@
17
17
  optionIndicator,
18
18
  showSelectAll = false,
19
19
  selectAllLabel = 'Select all',
20
+ showSelectedTick = false,
20
21
  triggerSummary,
21
22
  testId,
22
23
  itemTestId,
@@ -443,6 +444,7 @@
443
444
  <div
444
445
  class="select-option"
445
446
  class:multi={multiple}
447
+ class:tickable={showSelectedTick && !multiple}
446
448
  class:selected={value.includes(row.item.id)}
447
449
  class:highlighted={index === highlightedIndex}
448
450
  role="option"
@@ -478,7 +480,11 @@
478
480
  </span>
479
481
  {/if}
480
482
  {/if}
481
- {row.item.label}
483
+ <span class="select-option-label">{row.item.label}</span>
484
+ {#if showSelectedTick && !multiple && value.includes(row.item.id)}
485
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
486
+ <span class="select-option-tick" aria-hidden="true">{@html checkmarkSvg}</span>
487
+ {/if}
482
488
  </div>
483
489
  {/if}
484
490
  {/each}
@@ -740,4 +746,31 @@
740
746
  background: var(--select-ghost-trigger-open-background, rgba(0, 0, 0, 0.06));
741
747
  box-shadow: none;
742
748
  }
749
+
750
+ /* Single-select right-edge tick (showSelectedTick) */
751
+ .select-option.tickable {
752
+ display: flex;
753
+ align-items: center;
754
+ gap: 8px;
755
+ }
756
+
757
+ .select-option.tickable .select-option-label {
758
+ flex: 1 1 auto;
759
+ min-width: 0;
760
+ }
761
+
762
+ .select-option-tick {
763
+ display: inline-flex;
764
+ align-items: center;
765
+ justify-content: center;
766
+ flex-shrink: 0;
767
+ width: var(--select-option-tick-size, 16px);
768
+ height: var(--select-option-tick-size, 16px);
769
+ color: var(--select-option-tick-color, #2563eb);
770
+ }
771
+
772
+ .select-option-tick :global(svg) {
773
+ width: 100%;
774
+ height: 100%;
775
+ }
743
776
  </style>
@@ -40,6 +40,13 @@ export type OptionalSelectProperties = {
40
40
  showSelectAll?: boolean;
41
41
  /** Label for the `showSelectAll` row. Defaults to `'Select all'`. */
42
42
  selectAllLabel?: string;
43
+ /**
44
+ * Single-select only: when `true`, the currently selected option shows a
45
+ * checkmark at its right edge. No effect in `multiple` mode (which already
46
+ * renders a checkbox indicator). Themeable via `--select-option-tick-size`
47
+ * and `--select-option-tick-color`. Defaults to `false`.
48
+ */
49
+ showSelectedTick?: boolean;
43
50
  testId?: string;
44
51
  /** Fallback per-option test id prefix. Each option emits `data-pw="{itemTestId}-{id}"` when its own `item.testId` is not set. */
45
52
  itemTestId?: string;
@@ -60,7 +60,7 @@
60
60
  display: inline-block;
61
61
  width: 8px;
62
62
  height: 8px;
63
- border-radius: 2px;
63
+ border-radius: var(--chart-swatch-radius, 2px);
64
64
  flex-shrink: 0;
65
65
  }
66
66
 
@@ -48,7 +48,7 @@
48
48
  display: inline-block;
49
49
  width: var(--chart-legend-swatch-size, 12px);
50
50
  height: var(--chart-legend-swatch-size, 12px);
51
- border-radius: 2px;
51
+ border-radius: var(--chart-swatch-radius, 2px);
52
52
  flex-shrink: 0;
53
53
  }
54
54
 
@@ -128,7 +128,7 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
128
128
  nodeY.set(id, Math.max(0, weightedY - (nodeH.get(id) ?? 0) / 2));
129
129
  }
130
130
  }
131
- // Resolve overlaps
131
+ // Resolve overlaps: push down from the top.
132
132
  ids.sort((a, b) => (nodeY.get(a) ?? 0) - (nodeY.get(b) ?? 0));
133
133
  let y = 0;
134
134
  for (const id of ids) {
@@ -138,6 +138,37 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
138
138
  }
139
139
  y = (nodeY.get(id) ?? 0) + (nodeH.get(id) ?? 0) + nodePadding;
140
140
  }
141
+ // Resolve overlaps: pull back up from the bottom. The push-down pass
142
+ // above only ever grows a column's block downward, so a fan-out whose
143
+ // members share a weighted target centre drifts past the column's
144
+ // height budget column-to-column instead of staying level. Sweep from
145
+ // the last node up, clamping each node's bottom edge to the running
146
+ // boundary, mirroring d3-sankey's bidirectional resolveCollisions.
147
+ let bottomBoundary = height;
148
+ for (let index = ids.length - 1; index >= 0; index--) {
149
+ const id = ids[index];
150
+ const nodeBottom = (nodeY.get(id) ?? 0) + (nodeH.get(id) ?? 0);
151
+ if (nodeBottom > bottomBoundary) {
152
+ nodeY.set(id, bottomBoundary - (nodeH.get(id) ?? 0));
153
+ }
154
+ bottomBoundary = (nodeY.get(id) ?? 0) - nodePadding;
155
+ }
156
+ // Re-centre the column's node group within [0, height]: once overlaps
157
+ // are resolved, anchor the block at the midpoint of its remaining
158
+ // slack rather than leaving it wherever the top-down/bottom-up sweeps
159
+ // happened to land it, so a cluster sharing a weighted centre reads as
160
+ // centred on that target instead of stacked toward one edge.
161
+ const firstId = ids[0];
162
+ const lastId = ids[ids.length - 1];
163
+ const groupTop = nodeY.get(firstId) ?? 0;
164
+ const groupBottom = (nodeY.get(lastId) ?? 0) + (nodeH.get(lastId) ?? 0);
165
+ const idealGroupTop = Math.max(0, (height - (groupBottom - groupTop)) / 2);
166
+ const recentreShift = idealGroupTop - groupTop;
167
+ if (recentreShift !== 0) {
168
+ for (const id of ids) {
169
+ nodeY.set(id, (nodeY.get(id) ?? 0) + recentreShift);
170
+ }
171
+ }
141
172
  }
142
173
  }
143
174
  // Build computed nodes
@@ -1,5 +1,15 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { BarChartDataPoint } from '../BarChart/properties';
3
+ /**
4
+ * Corner radius (px) shared by every chart shape (bar/column rects, funnel
5
+ * stage bars, sankey nodes). Mirrors Lighthouse's `--radius` design token
6
+ * (0.25rem = 4px at the 16px root). SVG `rx`/`ry` attributes and the
7
+ * `roundedRectPath()` curve builder in `_chart/paths.ts` consume plain JS
8
+ * numbers, so this constant is the chart-layer equivalent of `var(--radius)`
9
+ * for surfaces CSS cannot reach. Consumers who need runtime sync to a
10
+ * *changed* `--radius` should pass the corresponding radius prop explicitly.
11
+ */
12
+ export declare const DEFAULT_CHART_CORNER_RADIUS = 4;
3
13
  export type Margin = {
4
14
  top: number;
5
15
  right: number;
@@ -1 +1,11 @@
1
- export {};
1
+ // ── Shared constants ──────────────────────────────────────────
2
+ /**
3
+ * Corner radius (px) shared by every chart shape (bar/column rects, funnel
4
+ * stage bars, sankey nodes). Mirrors Lighthouse's `--radius` design token
5
+ * (0.25rem = 4px at the 16px root). SVG `rx`/`ry` attributes and the
6
+ * `roundedRectPath()` curve builder in `_chart/paths.ts` consume plain JS
7
+ * numbers, so this constant is the chart-layer equivalent of `var(--radius)`
8
+ * for surfaces CSS cannot reach. Consumers who need runtime sync to a
9
+ * *changed* `--radius` should pass the corresponding radius prop explicitly.
10
+ */
11
+ export const DEFAULT_CHART_CORNER_RADIUS = 4;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.80.2",
3
+ "version": "2.80.4",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",