@juspay/svelte-ui-components 2.118.0 → 2.119.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.
@@ -8,6 +8,9 @@
8
8
  classes,
9
9
  testId,
10
10
  onclick,
11
+ href,
12
+ target,
13
+ rel,
11
14
  headerRight,
12
15
  footer,
13
16
  stretch = false,
@@ -15,7 +18,14 @@
15
18
  cssVars
16
19
  }: CardProperties = $props();
17
20
 
18
- const isInteractive = $derived(typeof onclick === 'function');
21
+ const isAnchor = $derived(typeof href === 'string' && href.length > 0);
22
+ const isInteractive = $derived(isAnchor || typeof onclick === 'function');
23
+ // HTML target values are ASCII case-insensitive (e.g. "_BLANK" opens a new
24
+ // context identically to "_blank"), so the comparison must normalize case
25
+ // to keep the noopener/noreferrer default from being silently skipped.
26
+ const resolvedRel = $derived(
27
+ rel ?? (target?.toLowerCase() === '_blank' ? 'noopener noreferrer' : null)
28
+ );
19
29
 
20
30
  const styleAttr = $derived(
21
31
  cssVars
@@ -37,8 +47,8 @@
37
47
  };
38
48
  </script>
39
49
 
40
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
41
- <div
50
+ <svelte:element
51
+ this={isAnchor ? 'a' : 'div'}
42
52
  class="card {classes ?? ''}"
43
53
  class:card-interactive={isInteractive}
44
54
  class:card-stretch={stretch}
@@ -46,10 +56,13 @@
46
56
  style={styleAttr}
47
57
  data-pw={typeof testId === 'string' ? testId : null}
48
58
  testID={typeof testId === 'string' ? testId : null}
49
- role={isInteractive ? 'button' : null}
50
- tabindex={isInteractive ? 0 : null}
51
- onclick={isInteractive ? onclick : null}
52
- onkeydown={isInteractive ? handleKeydown : null}
59
+ role={!isAnchor && isInteractive ? 'button' : null}
60
+ tabindex={!isAnchor && isInteractive ? 0 : null}
61
+ href={isAnchor ? href : null}
62
+ target={isAnchor ? target : null}
63
+ rel={isAnchor ? resolvedRel : null}
64
+ onclick={onclick ?? null}
65
+ onkeydown={!isAnchor && isInteractive ? handleKeydown : null}
53
66
  >
54
67
  {#if (typeof title === 'string' && title.length > 0) || typeof headerRight === 'function'}
55
68
  <div class="card-header" class:card-header-split={typeof headerRight === 'function'}>
@@ -85,10 +98,16 @@
85
98
  {@render footer()}
86
99
  </footer>
87
100
  {/if}
88
- </div>
101
+ </svelte:element>
89
102
 
90
103
  <style>
91
104
  .card {
105
+ /* Explicit so the root's layout doesn't depend on the browser default for
106
+ whichever tag svelte:element renders -- a <div> defaults to block and an
107
+ <a> defaults to inline, and an inline box ignores width/height entirely.
108
+ Pinning both here keeps anchor-mode cards byte-identical to div cards. */
109
+ display: block;
110
+ text-decoration: none;
92
111
  background: var(--card-background, inherit);
93
112
  border: var(--card-border, 1px solid currentColor);
94
113
  border-radius: var(--card-border-radius, var(--radius, 4px));
@@ -12,12 +12,28 @@ export type OptionalCardProperties = {
12
12
  /** Renders as `data-pw` on the root element for Playwright test selection. */
13
13
  testId?: string;
14
14
  /**
15
- * When provided the root element becomes interactive:
15
+ * When provided (and `href` is not) the root element becomes an interactive `<div>`:
16
16
  * `role="button"`, `tabindex=0`, and keydown (Enter/Space) triggers the handler.
17
- * When omitted the root is a plain `<div>` with no interactive attributes,
18
- * so existing consumers see zero behaviour change.
17
+ * When `href` is also provided, the root renders as a native `<a>` instead (see
18
+ * `href`) and this synthetic role/tabindex/keydown shim is skipped, since anchor
19
+ * semantics already cover focus and Enter-activation; `onclick` still fires either
20
+ * way. When neither `onclick` nor `href` is provided the root is a plain `<div>`
21
+ * with no interactive attributes, so existing consumers see zero behaviour change.
19
22
  */
20
23
  onclick?: (event: MouseEvent) => void;
24
+ /**
25
+ * Render the card root as an `<a>` styled identically, instead of a `<div>`. Use
26
+ * for card-styled links/navigation. When set, the root is natively focusable and
27
+ * Enter-activates, so the `role="button"`/`tabindex`/keydown shim used for a
28
+ * clickable `<div>` (see `onclick`) is not applied. `onclick` (if also provided)
29
+ * still fires alongside navigation. Omitted by default, so existing consumers are
30
+ * unaffected.
31
+ */
32
+ href?: string;
33
+ /** Anchor target (only applied when `href` is set), e.g. `_blank`. */
34
+ target?: string;
35
+ /** Anchor rel (only applied when `href` is set). Defaults to `noopener noreferrer` when `target="_blank"`. */
36
+ rel?: string;
21
37
  /**
22
38
  * Snippet rendered at the top-right of the header row, alongside title/description.
23
39
  * When omitted the header row is unchanged (title block takes full width).
@@ -94,13 +94,30 @@
94
94
  --pill-* and --input-* have not yet been re-declared: reading them in the same declaration
95
95
  that sets them would be a custom-property cycle and would compute to invalid. Consumers that
96
96
  theme Pill/Input app-wide get chips that match the rest of their UI, instead of the library's
97
- hardcoded light-theme hexes overriding their theme. */
97
+ own values overriding their theme.
98
+
99
+ Every token that carries the chip's APPEARANCE is captured here — colour, type, spacing and
100
+ shape alike. An app whose Pill is a 14px, 16px-radius chip was still getting 13px and a
101
+ 999px pill, because re-declaring a token on the element beats inheriting it no matter which
102
+ property it is; restoring only the colours left the same bug in a less obvious place.
103
+ Tokens the component owns STRUCTURALLY — the draft field's inline padding, margin and
104
+ shadow, and its width/height — deliberately stay fixed, since those position the field among
105
+ the chips rather than describe how it looks. */
98
106
  --chip-input-pill-background-default: var(--pill-background, #e0e0e0);
99
107
  --chip-input-pill-color-default: var(--pill-color, #333333);
100
108
  --chip-input-pill-dismiss-color-default: var(--pill-dismiss-color, currentColor);
109
+ --chip-input-pill-font-size-default: var(--pill-font-size, 13px);
110
+ --chip-input-pill-font-weight-default: var(--pill-font-weight, 500);
111
+ --chip-input-pill-padding-default: var(--pill-padding, 6px 10px);
112
+ --chip-input-pill-border-radius-default: var(--pill-border-radius, 999px);
113
+ --chip-input-pill-border-default: var(--pill-border, none);
114
+ --chip-input-pill-gap-default: var(--pill-gap, 4px);
115
+ --chip-input-pill-dismiss-size-default: var(--pill-dismiss-size, 14px);
116
+ --chip-input-pill-max-width-default: var(--pill-max-width);
101
117
  --chip-input-draft-border-default: var(--input-border, 1px solid transparent);
102
118
  --chip-input-draft-focus-border-default: var(--input-focus-border, 1px solid transparent);
103
119
  --chip-input-draft-radius-default: var(--input-radius, 4px);
120
+ --chip-input-draft-font-size-default: var(--input-font-size, 13px);
104
121
  }
105
122
 
106
123
  .chip-input-draft-wrap {
@@ -108,16 +125,25 @@
108
125
  }
109
126
 
110
127
  .chip-input :global(.chip-input-pill) {
111
- --pill-gap: var(--chip-input-pill-gap, 4px);
128
+ --pill-gap: var(--chip-input-pill-gap, var(--chip-input-pill-gap-default));
112
129
  --pill-background: var(--chip-input-pill-background, var(--chip-input-pill-background-default));
113
130
  --pill-color: var(--chip-input-pill-color, var(--chip-input-pill-color-default));
114
- --pill-font-size: var(--chip-input-pill-font-size, 13px);
115
- --pill-font-weight: var(--chip-input-pill-font-weight, 500);
116
- --pill-padding: var(--chip-input-pill-padding, 6px 10px);
117
- --pill-border-radius: var(--chip-input-pill-border-radius, 999px);
118
- --pill-border: var(--chip-input-pill-border, none);
119
- --pill-max-width: var(--chip-input-pill-max-width);
120
- --pill-dismiss-size: var(--chip-input-pill-dismiss-size, 14px);
131
+ --pill-font-size: var(--chip-input-pill-font-size, var(--chip-input-pill-font-size-default));
132
+ --pill-font-weight: var(
133
+ --chip-input-pill-font-weight,
134
+ var(--chip-input-pill-font-weight-default)
135
+ );
136
+ --pill-padding: var(--chip-input-pill-padding, var(--chip-input-pill-padding-default));
137
+ --pill-border-radius: var(
138
+ --chip-input-pill-border-radius,
139
+ var(--chip-input-pill-border-radius-default)
140
+ );
141
+ --pill-border: var(--chip-input-pill-border, var(--chip-input-pill-border-default));
142
+ --pill-max-width: var(--chip-input-pill-max-width, var(--chip-input-pill-max-width-default));
143
+ --pill-dismiss-size: var(
144
+ --chip-input-pill-dismiss-size,
145
+ var(--chip-input-pill-dismiss-size-default)
146
+ );
121
147
  --pill-dismiss-color: var(
122
148
  --chip-input-pill-dismiss-color,
123
149
  var(--chip-input-pill-dismiss-color-default)
@@ -133,9 +159,13 @@
133
159
  --chip-input-draft-focus-border,
134
160
  var(--chip-input-draft-focus-border-default)
135
161
  );
162
+ --input-font-size: var(--chip-input-draft-font-size, var(--chip-input-draft-font-size-default));
163
+
164
+ /* Structural, not thematic: the draft field sits inline among the chips, so it keeps its own
165
+ tight padding and no margin or shadow regardless of how the app themes Input elsewhere.
166
+ Override them per-instance via --chip-input-draft-* if a layout genuinely needs it. */
136
167
  --input-padding: var(--chip-input-draft-padding, 0 2px);
137
168
  --input-margin: 0;
138
- --input-font-size: var(--chip-input-draft-font-size, 13px);
139
169
  --input-box-shadow: none;
140
170
  }
141
171
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.118.0",
3
+ "version": "2.119.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",