@juspay/svelte-ui-components 2.80.2 → 2.80.3

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,10 +4,20 @@
4
4
 
5
5
  let {
6
6
  text,
7
+ variant = 'primary',
8
+ size = 'md',
9
+ iconOnly = false,
10
+ fullWidth = false,
11
+ href,
12
+ target,
13
+ rel,
14
+ loading = false,
15
+ allowHtml = false,
7
16
  enable = true,
8
17
  disabled = false,
9
18
  showLoader = false,
10
19
  loaderType,
20
+ showProgressBar = $bindable(false),
11
21
  type = 'button',
12
22
  testId,
13
23
  ariaLabel,
@@ -22,30 +32,46 @@
22
32
  onmouseleave,
23
33
  ontouchstart,
24
34
  ontouchend,
25
- showProgressBar = $bindable(false),
26
35
  icon,
27
36
  children,
28
37
  classes
29
38
  }: ButtonProperties = $props();
30
39
 
31
- let isDisabled = $derived(!enable || disabled || showLoader);
40
+ // `loading` and the legacy Circular loaderType both render the spinner and disable the button.
41
+ let showCircularLoader = $derived(loading || (showLoader && loaderType === 'Circular'));
42
+ // The legacy ProgressBar loader stays clickable until the first click starts the bar, then
43
+ // it disables (the documented flow). So it must NOT count toward "disabled" before it starts.
44
+ let isProgressBarLoader = $derived(showLoader && loaderType === 'ProgressBar');
45
+ let isBusy = $derived(showCircularLoader || showProgressBar);
46
+ let isDisabled = $derived(disabled || !enable || showCircularLoader || showProgressBar);
47
+ let resolvedRel = $derived(rel ?? (target === '_blank' ? 'noopener noreferrer' : null));
32
48
 
33
49
  function handleButtonClick(event: MouseEvent): void {
34
- if (showProgressBar) {
50
+ if (isDisabled) {
51
+ // Anchors have no native disabled state — block navigation/handler explicitly.
52
+ if (href) {
53
+ event.preventDefault();
54
+ }
35
55
  return;
36
56
  }
37
57
  onclick?.(event);
38
- if (showLoader && loaderType === 'ProgressBar') {
58
+ if (isProgressBarLoader) {
39
59
  showProgressBar = true;
40
60
  }
41
61
  }
42
62
  </script>
43
63
 
44
- <div class="button-container {classes ?? ''}">
64
+ <div
65
+ class="button-container variant-{variant} size-{size} {classes ?? ''}"
66
+ class:icon-only={iconOnly}
67
+ class:full-width={fullWidth}
68
+ >
45
69
  {#if showProgressBar}
46
70
  <div class="button-progress-bar"></div>
47
71
  {/if}
48
- <button
72
+ <svelte:element
73
+ this={href ? 'a' : 'button'}
74
+ class="button-el"
49
75
  class:disabled={isDisabled}
50
76
  onclick={handleButtonClick}
51
77
  {onkeydown}
@@ -55,28 +81,38 @@
55
81
  {onmouseleave}
56
82
  {ontouchstart}
57
83
  {ontouchend}
58
- disabled={isDisabled}
59
- {type}
60
84
  data-pw={testId}
61
- aria-label={ariaLabel}
62
- aria-expanded={ariaExpanded}
63
- aria-selected={ariaSelected}
64
- {role}
85
+ role={role ?? null}
86
+ aria-label={ariaLabel ?? null}
87
+ aria-expanded={ariaExpanded ?? null}
88
+ aria-selected={ariaSelected ?? null}
89
+ aria-busy={isBusy || null}
90
+ type={href ? null : type}
91
+ disabled={href ? null : isDisabled}
92
+ href={href ? (isDisabled ? null : href) : null}
93
+ target={href ? target : null}
94
+ rel={href ? resolvedRel : null}
95
+ aria-disabled={href && isDisabled ? 'true' : null}
96
+ tabindex={href && isDisabled ? -1 : null}
65
97
  >
66
- {#if showLoader && loaderType === 'Circular'}
98
+ {#if showCircularLoader}
67
99
  <div class="button-loader"><Loader /></div>
68
100
  {/if}
69
101
  {#if typeof icon === 'function'}
70
102
  <div class="button-icon">{@render icon()}</div>
71
103
  {/if}
72
104
  {#if typeof text === 'string' && text.length > 0}
73
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
74
- <div class="button-text">{@html text}</div>
105
+ {#if allowHtml}
106
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
107
+ <div class="button-text">{@html text}</div>
108
+ {:else}
109
+ <div class="button-text">{text}</div>
110
+ {/if}
75
111
  {/if}
76
112
  {#if typeof children === 'function'}
77
113
  {@render children()}
78
114
  {/if}
79
- </button>
115
+ </svelte:element>
80
116
  </div>
81
117
 
82
118
  <style>
@@ -84,23 +120,92 @@
84
120
  position: relative;
85
121
  width: var(--button-width, fit-content);
86
122
  }
87
- button {
123
+
124
+ /* ---- Variant defaults (set the internal --_btn-* layer; explicit --button-* always wins) ---- */
125
+ .variant-primary {
126
+ --_btn-color: #3a4550;
127
+ --_btn-text-color: #ffffff;
128
+ --_btn-border: none;
129
+ }
130
+
131
+ .variant-secondary {
132
+ --_btn-color: transparent;
133
+ --_btn-text-color: #3a4550;
134
+ --_btn-border: 1px solid #cbd5e1;
135
+ --_btn-hover-color: #f1f5f9;
136
+ --_btn-hover-text-color: #3a4550;
137
+ --_btn-hover-border: 1px solid #cbd5e1;
138
+ }
139
+
140
+ .variant-ghost {
141
+ --_btn-color: transparent;
142
+ --_btn-text-color: #3a4550;
143
+ --_btn-border: none;
144
+ --_btn-hover-color: #f1f5f9;
145
+ --_btn-hover-text-color: #3a4550;
146
+ --_btn-hover-border: none;
147
+ }
148
+
149
+ .variant-destructive {
150
+ --_btn-color: #e7000b;
151
+ --_btn-text-color: #ffffff;
152
+ --_btn-border: none;
153
+ --_btn-hover-color: #c10007;
154
+ --_btn-hover-text-color: #ffffff;
155
+ --_btn-hover-border: none;
156
+ }
157
+
158
+ /* ---- Size defaults (md reproduces the legacy 16px padding / 14px font) ---- */
159
+ .size-sm {
160
+ --_btn-padding: 8px 12px;
161
+ --_btn-font-size: 13px;
162
+ }
163
+
164
+ .size-md {
165
+ --_btn-padding: 16px;
166
+ --_btn-font-size: 14px;
167
+ }
168
+
169
+ .size-lg {
170
+ --_btn-padding: 20px 28px;
171
+ --_btn-font-size: 16px;
172
+ }
173
+
174
+ .icon-only {
175
+ --_btn-padding: 8px;
176
+ }
177
+
178
+ .icon-only.size-sm {
179
+ --_btn-padding: 6px;
180
+ }
181
+
182
+ .icon-only.size-lg {
183
+ --_btn-padding: 12px;
184
+ }
185
+
186
+ .full-width {
187
+ --button-width: 100%;
188
+ }
189
+
190
+ .button-el {
88
191
  max-height: var(--button-max-height);
89
192
  max-width: var(--button-max-width);
90
193
  min-width: var(--button-min-width);
91
194
  font-family: var(--button-font-family);
92
195
  font-weight: var(--button-font-weight, 500);
93
- font-size: var(--button-font-size, 14px);
94
- background-color: var(--button-color, #3a4550);
95
- color: var(--button-text-color, white);
196
+ font-size: var(--button-font-size, var(--_btn-font-size, 14px));
197
+ background-color: var(--button-color, var(--_btn-color, #3a4550));
198
+ color: var(--button-text-color, var(--_btn-text-color, white));
96
199
  height: var(--button-height, fit-content);
97
- padding: var(--button-padding, 16px);
200
+ padding: var(--button-padding, var(--_btn-padding, 16px));
98
201
  margin: var(--button-margin);
99
- border-radius: var(--button-border-radius, var(--radius, 4px));
202
+ border-radius: var(--button-border-radius, var(--radius, 6px));
100
203
  width: var(--button-width, fit-content);
101
204
  cursor: var(--cursor, pointer);
102
205
  opacity: var(--opacity, 1);
103
- border: var(--button-border, none);
206
+ border: var(--button-border, var(--_btn-border, none));
207
+ box-sizing: border-box;
208
+ text-decoration: none;
104
209
  display: flex;
105
210
  justify-content: var(--button-justify-content, center);
106
211
  align-items: center;
@@ -110,14 +215,16 @@
110
215
  box-shadow: var(--button-box-shadow, none);
111
216
  }
112
217
 
113
- .disabled {
218
+ .button-el.disabled,
219
+ .button-el:disabled {
114
220
  cursor: var(--disabled-cursor, not-allowed);
115
221
  opacity: var(--disabled-opacity, 0.4);
116
- color: var(--disabled-text-color, var(--button-text-color, white));
222
+ color: var(--disabled-text-color, var(--button-text-color, var(--_btn-text-color, white)));
117
223
  font-size: var(--disabled-font-size);
118
224
  font-weight: var(--disabled-font-weight);
119
- border: var(--disabled-border);
120
- background: var(--disabled-background-color, var(--button-color, #3a4550));
225
+ /* Preserve the variant border when disabled so secondary (bordered) stays distinct from ghost. */
226
+ border: var(--disabled-border, var(--button-border, var(--_btn-border, none)));
227
+ background: var(--disabled-background-color, var(--button-color, var(--_btn-color, #3a4550)));
121
228
  box-shadow: var(
122
229
  --button-disabled-box-shadow,
123
230
  var(--disabled-box-shadow, var(--button-box-shadow, none))
@@ -138,21 +245,30 @@
138
245
  display: var(--button-text-display);
139
246
  }
140
247
 
141
- button:hover {
142
- background: var(--button-hover-color, var(--button-color, #3a4550));
143
- color: var(--button-hover-text-color, var(--button-text-color, white));
144
- border: var(--button-hover-border, var(--button-border, none));
248
+ .button-el:hover:not(.disabled) {
249
+ background: var(
250
+ --button-hover-color,
251
+ var(--_btn-hover-color, var(--button-color, var(--_btn-color, #3a4550)))
252
+ );
253
+ color: var(
254
+ --button-hover-text-color,
255
+ var(--_btn-hover-text-color, var(--button-text-color, var(--_btn-text-color, white)))
256
+ );
257
+ border: var(
258
+ --button-hover-border,
259
+ var(--_btn-hover-border, var(--button-border, var(--_btn-border, none)))
260
+ );
145
261
  transform: var(--button-hover-transform);
146
262
  box-shadow: var(--button-hover-box-shadow, var(--button-box-shadow, none));
147
263
  }
148
264
 
149
- button:active {
265
+ .button-el:active:not(.disabled) {
150
266
  transform: var(--button-active-transform);
151
- background: var(--button-active-background, var(--button-color, #3a4550));
267
+ background: var(--button-active-background, var(--button-color, var(--_btn-color, #3a4550)));
152
268
  box-shadow: var(--button-active-box-shadow, var(--button-box-shadow, none));
153
269
  }
154
270
 
155
- button:focus-visible {
271
+ .button-el:focus-visible {
156
272
  box-shadow: var(--button-focus-visible-box-shadow, var(--button-box-shadow, none));
157
273
  }
158
274
 
@@ -1,12 +1,51 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  export type LoaderType = 'Circular' | 'ProgressBar';
3
+ /** Visual style of the button. Maps to a built-in palette via `--button-*` variables. */
4
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
5
+ /** Size preset controlling padding/height/font-size. */
6
+ export type ButtonSize = 'sm' | 'md' | 'lg';
3
7
  export type ButtonProperties = OptionalButtonProperties & ButtonEventProperties;
4
8
  export type OptionalButtonProperties = {
5
9
  text?: string;
10
+ /**
11
+ * Visual style: `primary` (default, filled), `secondary` (outlined), `ghost`
12
+ * (transparent), `destructive` (danger). Each maps to the `--button-*` variables, so an
13
+ * explicit `--button-color` / `classes` override always wins over the variant default.
14
+ */
15
+ variant?: ButtonVariant;
16
+ /** Size preset: `sm` / `md` (default) / `lg`. Controls padding, height, and font size. */
17
+ size?: ButtonSize;
18
+ /** Square padding for an icon-only button. Pair with `ariaLabel` for accessibility. */
19
+ iconOnly?: boolean;
20
+ /** Stretch the button to the full width of its container. */
21
+ fullWidth?: boolean;
22
+ /**
23
+ * Render the button as an `<a>` styled identically. Use for button-styled links/navigation.
24
+ * When set, `type` is ignored; a disabled link is rendered inert via `aria-disabled`.
25
+ */
26
+ href?: string;
27
+ /** Anchor target (only with `href`), e.g. `_blank`. */
28
+ target?: string;
29
+ /** Anchor rel (only with `href`). Defaults to `noopener noreferrer` when `target="_blank"`. */
30
+ rel?: string;
31
+ /**
32
+ * Show a loading state: renders the circular loader, sets `aria-busy`, and disables the
33
+ * button. Preferred over the legacy `showLoader`/`loaderType` pair.
34
+ */
35
+ loading?: boolean;
36
+ /**
37
+ * Render the `text` as raw HTML. Defaults to `false` (safe plain-text rendering). Only enable
38
+ * for trusted, non-user-derived markup.
39
+ */
40
+ allowHtml?: boolean;
41
+ /** @deprecated Use `disabled` instead. Retained for backward compatibility. */
6
42
  enable?: boolean;
7
- showProgressBar?: boolean;
43
+ /** @deprecated Prefer `loading`. Shows a loader; pair with `loaderType`. */
8
44
  showLoader?: boolean;
45
+ /** @deprecated Used with `showLoader`. `'Circular'` or `'ProgressBar'`. */
9
46
  loaderType?: LoaderType;
47
+ /** @deprecated Drives the progress-bar animation when `loaderType='ProgressBar'`. */
48
+ showProgressBar?: boolean;
10
49
  type?: 'submit' | 'reset' | 'button';
11
50
  testId?: string;
12
51
  icon?: Snippet;
@@ -30,6 +30,9 @@
30
30
  barRadius = 3,
31
31
  barPadding = 0.25,
32
32
  aspectRatio = 16 / 9,
33
+ minBarHeight = 2,
34
+ margin,
35
+ tooltipPortal = false,
33
36
  tooltipSnippet,
34
37
  onbarclick,
35
38
  testId,
@@ -44,6 +47,8 @@
44
47
  let hoveredCategoryIndex = $state<number | null>(null);
45
48
  let mouseX = $state(0);
46
49
  let mouseY = $state(0);
50
+ let mouseClientX = $state(0);
51
+ let mouseClientY = $state(0);
47
52
 
48
53
  // ── Formatters ─────────────────────────────────────────────────
49
54
 
@@ -53,7 +58,13 @@
53
58
  // ── Layout — wider right margin to accommodate right-axis labels ─
54
59
 
55
60
  const dims = $derived(
56
- computeChartDimensions(chartWidth, chartHeight, { top: 24, right: 56, bottom: 40, left: 56 })
61
+ computeChartDimensions(chartWidth, chartHeight, {
62
+ top: 24,
63
+ right: 56,
64
+ bottom: 40,
65
+ left: 56,
66
+ ...margin
67
+ })
57
68
  );
58
69
 
59
70
  // ── Scales ─────────────────────────────────────────────────────
@@ -152,7 +163,7 @@
152
163
  const barX = bandStart + subIndex * subBandWidth + barGap;
153
164
  const valueY = scale(value);
154
165
  const barY = value >= 0 ? valueY : zeroY;
155
- const barHeight = Math.max(2, Math.abs(valueY - zeroY));
166
+ const barHeight = Math.max(minBarHeight, Math.abs(valueY - zeroY));
156
167
 
157
168
  const path = roundedRectPath(barX, barY, barW, barHeight, barRadius, barRadius, 0, 0);
158
169
 
@@ -278,6 +289,22 @@
278
289
  const rect = containerEl.getBoundingClientRect();
279
290
  mouseX = event.clientX - rect.left;
280
291
  mouseY = event.clientY - rect.top;
292
+ mouseClientX = event.clientX;
293
+ mouseClientY = event.clientY;
294
+ };
295
+
296
+ /**
297
+ * Svelte action: relocates the tooltip layer to `document.body` so a `position:fixed`
298
+ * tooltip is never clipped by an `overflow`/scroll ancestor. Used only when
299
+ * `tooltipPortal` is set; `use:` actions never run during SSR.
300
+ */
301
+ const portalToBody = (node: HTMLElement) => {
302
+ document.body.appendChild(node);
303
+ return {
304
+ destroy: () => {
305
+ node.remove();
306
+ }
307
+ };
281
308
  };
282
309
 
283
310
  const handleCategoryEnter = (event: MouseEvent, catIdx: number) => {
@@ -439,12 +466,31 @@
439
466
  </ChartContainer>
440
467
 
441
468
  <!-- Tooltip -->
442
- {#if typeof tooltipSnippet === 'function' && hoveredCategoryIndex !== null}
443
- <div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
444
- {@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
445
- </div>
446
- {:else}
447
- <ChartTooltip data={tooltipData} {mouseX} {mouseY} />
469
+ {#if hoveredCategoryIndex !== null}
470
+ {#if tooltipPortal}
471
+ <!-- Portaled to <body> with fixed viewport coords so the tooltip is never
472
+ clipped by an overflow/scroll ancestor (e.g. a scrollable report sheet).
473
+ The inner elements keep their own +12/-12 offset relative to this layer. -->
474
+ <div
475
+ class="chart-tooltip-portal"
476
+ style="left: {mouseClientX}px; top: {mouseClientY}px;"
477
+ use:portalToBody
478
+ >
479
+ {#if typeof tooltipSnippet === 'function'}
480
+ <div class="chart-tooltip-slot" style="left: 12px; top: -12px;">
481
+ {@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
482
+ </div>
483
+ {:else}
484
+ <ChartTooltip data={tooltipData} mouseX={0} mouseY={0} />
485
+ {/if}
486
+ </div>
487
+ {:else if typeof tooltipSnippet === 'function'}
488
+ <div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
489
+ {@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
490
+ </div>
491
+ {:else}
492
+ <ChartTooltip data={tooltipData} {mouseX} {mouseY} />
493
+ {/if}
448
494
  {/if}
449
495
  {:else}
450
496
  <div class="chart-empty">No data available.</div>
@@ -513,6 +559,12 @@
513
559
  pointer-events: none;
514
560
  }
515
561
 
562
+ .chart-tooltip-portal {
563
+ position: fixed;
564
+ z-index: var(--chart-tooltip-z-index, 10);
565
+ pointer-events: none;
566
+ }
567
+
516
568
  .chart-empty {
517
569
  padding: var(--chart-empty-padding, 32px 24px);
518
570
  color: var(--chart-empty-color, #9ca3af);
@@ -95,6 +95,32 @@ export type OptionalDualAxisBarChartProperties = {
95
95
  testId?: string;
96
96
  /** Extra CSS class string on the root `<div>`. */
97
97
  classes?: string;
98
+ /**
99
+ * Minimum rendered bar height in pixels. The default `2` keeps very small non-zero
100
+ * values visible, but it also paints a 2px stub for genuine zeros — set `0` so an
101
+ * all-zero/dormant series renders nothing (letting the consumer detect it and show
102
+ * an empty state instead of a row of indistinguishable baseline stubs). Default `2`.
103
+ */
104
+ minBarHeight?: number;
105
+ /**
106
+ * Override the plot margins (px) reserved for axis titles and tick labels. Merged
107
+ * over the defaults `{ top: 24, right: 56, bottom: 40, left: 56 }`; widen `left`/`right`
108
+ * when long currency tick labels (e.g. `₹1,00,000`) would otherwise overlap the
109
+ * gridlines in a narrow container.
110
+ */
111
+ margin?: {
112
+ top?: number;
113
+ right?: number;
114
+ bottom?: number;
115
+ left?: number;
116
+ };
117
+ /**
118
+ * Render the hover tooltip on `document.body` with `position: fixed` viewport
119
+ * coordinates instead of `position: absolute` inside the chart. Set `true` when the
120
+ * chart sits in an `overflow:auto`/scrollable container that would otherwise clip the
121
+ * tooltip. Default `false` (in-chart positioning, unchanged).
122
+ */
123
+ tooltipPortal?: boolean;
98
124
  };
99
125
  export type DualAxisBarChartEventProperties = {
100
126
  /**
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.3",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",