@juspay/svelte-ui-components 2.93.0 → 2.95.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.
@@ -305,19 +305,41 @@
305
305
  {:else if column.type === 'button'}
306
306
  {@const buttonData = asButtonCellData(value)}
307
307
  {#if buttonData}
308
+ <!-- The icon <img> is always decorative (alt=""): an icon-only button is
309
+ named by the required ariaLabel on the Button, and a text-bearing
310
+ button is named by its visible text — the image never carries the
311
+ accessible name itself. -->
312
+ {#snippet buttonCellIcon()}
313
+ <img class="builtin-button-icon" src={buttonData.iconUrl} alt="" />
314
+ {/snippet}
308
315
  <!-- svelte-ignore a11y_no_static_element_interactions -->
309
316
  <span
310
317
  class="builtin-interactive"
318
+ class:builtin-icon-button={buttonData.iconUrl && !buttonData.text}
311
319
  onclick={stopClickPropagation}
312
320
  onkeydown={stopKeydownPropagation}
313
321
  >
314
- <Button
315
- text={buttonData.text}
316
- disabled={buttonData.disabled ?? false}
317
- classes={buttonData.classes ?? ''}
318
- testId={buttonData.testId}
319
- onclick={() => column.onButtonClick?.(rowIndex, originalIndex)}
320
- />
322
+ {#if buttonData.iconUrl}
323
+ <Button
324
+ text={buttonData.text}
325
+ icon={buttonCellIcon}
326
+ iconOnly={!buttonData.text}
327
+ ariaLabel={buttonData.ariaLabel}
328
+ disabled={buttonData.disabled ?? false}
329
+ classes={buttonData.classes ?? ''}
330
+ testId={buttonData.testId}
331
+ onclick={() => column.onButtonClick?.(rowIndex, originalIndex)}
332
+ />
333
+ {:else}
334
+ <Button
335
+ text={buttonData.text}
336
+ ariaLabel={buttonData.ariaLabel}
337
+ disabled={buttonData.disabled ?? false}
338
+ classes={buttonData.classes ?? ''}
339
+ testId={buttonData.testId}
340
+ onclick={() => column.onButtonClick?.(rowIndex, originalIndex)}
341
+ />
342
+ {/if}
321
343
  </span>
322
344
  {:else}
323
345
  {cellValueToText(value)}
@@ -333,13 +355,30 @@
333
355
  >
334
356
  {#if actionData.primaryButton}
335
357
  {@const primary = actionData.primaryButton}
336
- <Button
337
- text={primary.text}
338
- disabled={primary.disabled ?? false}
339
- classes={primary.classes ?? ''}
340
- testId={primary.testId}
341
- onclick={() => column.onPrimaryAction?.(rowIndex, originalIndex)}
342
- />
358
+ {#snippet primaryButtonIcon()}
359
+ <img class="builtin-button-icon" src={primary.iconUrl} alt="" />
360
+ {/snippet}
361
+ {#if primary.iconUrl}
362
+ <Button
363
+ text={primary.text}
364
+ icon={primaryButtonIcon}
365
+ iconOnly={!primary.text}
366
+ ariaLabel={primary.ariaLabel}
367
+ disabled={primary.disabled ?? false}
368
+ classes={primary.classes ?? ''}
369
+ testId={primary.testId}
370
+ onclick={() => column.onPrimaryAction?.(rowIndex, originalIndex)}
371
+ />
372
+ {:else}
373
+ <Button
374
+ text={primary.text}
375
+ ariaLabel={primary.ariaLabel}
376
+ disabled={primary.disabled ?? false}
377
+ classes={primary.classes ?? ''}
378
+ testId={primary.testId}
379
+ onclick={() => column.onPrimaryAction?.(rowIndex, originalIndex)}
380
+ />
381
+ {/if}
343
382
  {/if}
344
383
  {#if actionData.menuItems && actionData.menuItems.length > 0}
345
384
  <Menu
@@ -600,6 +639,12 @@
600
639
  height: var(--table-cell-input-icon-size, 16px);
601
640
  }
602
641
 
642
+ .builtin-button-icon {
643
+ display: block;
644
+ width: var(--table-cell-icon-size, 16px);
645
+ height: var(--table-cell-icon-size, 16px);
646
+ }
647
+
603
648
  .builtin-action-group {
604
649
  gap: var(--table-cell-inline-gap, 8px);
605
650
  }
@@ -10,8 +10,8 @@
10
10
  import Pagination from '../Pagination/Pagination.svelte';
11
11
  import Select from '../Select/Select.svelte';
12
12
  import chevronDownSmSvg from '../assets/chevron-down-sm.svg?raw';
13
- import chevronUpSvg from '../assets/chevron-up.svg?raw';
14
- import chevronDownSvg from '../assets/chevron-down.svg?raw';
13
+ import sortAscendingSvg from '../assets/sort-ascending.svg?raw';
14
+ import sortDescendingSvg from '../assets/sort-descending.svg?raw';
15
15
  import sortDefaultSvg from '../assets/sort-default.svg?raw';
16
16
  import searchSvg from '../assets/search.svg?raw';
17
17
  import closeSvg from '../assets/close.svg?raw';
@@ -745,7 +745,7 @@
745
745
  {:else}
746
746
  <span class="sort-icon">
747
747
  <!-- eslint-disable svelte/no-at-html-tags -->
748
- {@html chevronUpSvg}
748
+ {@html sortAscendingSvg}
749
749
  </span>
750
750
  {/if}
751
751
  {:else if sortColumn === colIndex && sortDirection === 'desc'}
@@ -754,7 +754,7 @@
754
754
  {:else}
755
755
  <span class="sort-icon">
756
756
  <!-- eslint-disable svelte/no-at-html-tags -->
757
- {@html chevronDownSvg}
757
+ {@html sortDescendingSvg}
758
758
  </span>
759
759
  {/if}
760
760
  {:else if typeof sortDefaultIcon === 'function'}
@@ -1324,18 +1324,32 @@
1324
1324
  align-items: center;
1325
1325
  }
1326
1326
 
1327
+ /* The sorted-direction half paints via currentColor; without an explicit
1328
+ color it inherits the Button's text color (white on the default button
1329
+ theme), which disappears on light headers. Default to the design
1330
+ system's active blue. */
1331
+ .sort-button :global(.sort-icon:not(.sort-icon-idle)) {
1332
+ color: var(--table-sort-active-color, #1b85ff);
1333
+ }
1334
+
1327
1335
  .sort-button :global(.sort-icon svg) {
1328
1336
  width: var(--table-sort-icon-size, 14px);
1329
1337
  height: var(--table-sort-icon-size, 14px);
1330
1338
  display: block;
1331
1339
  }
1332
1340
 
1341
+ /* The design system draws every sort state fully opaque — faintness comes
1342
+ from the glyph fills (inactive halves #C7C7C7, active direction
1343
+ currentColor), never from transparency. The opacity vars remain for
1344
+ consumers that prefer a fade but now default to solid; hover steps the
1345
+ inactive halves to the design system's hover gray. */
1333
1346
  .sort-button :global(.sort-icon-idle) {
1334
- opacity: var(--table-sort-idle-opacity, 0.5);
1347
+ opacity: var(--table-sort-idle-opacity, 1);
1335
1348
  }
1336
1349
 
1337
1350
  .sort-button:hover :global(.sort-icon-idle) {
1338
- opacity: var(--table-sort-idle-hover-opacity, 0.85);
1351
+ opacity: var(--table-sort-idle-hover-opacity, 1);
1352
+ --table-sort-inactive-color: var(--table-sort-hover-color, #797979);
1339
1353
  }
1340
1354
 
1341
1355
  /* ── Empty ──────────────────────────────────────────────────────────────── */
@@ -12,6 +12,13 @@ import type { TableActionGroupCellData, TableAvatarStackCellData, TableButtonCel
12
12
  export declare const asJsonObject: (value: TableCellValue) => {
13
13
  [key: string]: JSONValue;
14
14
  } | null;
15
+ /**
16
+ * Accepts an icon URL only when its scheme is safe to place in an `<img src>`
17
+ * — http(s), a `data:image/*` payload, or a scheme-less (relative) path. Cell
18
+ * data is consumer-supplied JSON, so `javascript:`/`vbscript:`/non-image
19
+ * `data:` URIs must never reach the DOM.
20
+ */
21
+ export declare const asSafeIconUrl: (value: TableCellValue) => string | null;
15
22
  export declare const asTagCellData: (value: TableCellValue) => TableTagCellData | null;
16
23
  export declare const asTagArrayItems: (value: TableCellValue) => TableTagArrayCellItem[] | null;
17
24
  export declare const asAvatarStackData: (value: TableCellValue) => TableAvatarStackCellData | null;
@@ -11,6 +11,32 @@ export const asJsonObject = (value) => {
11
11
  }
12
12
  return null;
13
13
  };
14
+ /**
15
+ * Accepts an icon URL only when its scheme is safe to place in an `<img src>`
16
+ * — http(s), a `data:image/*` payload, or a scheme-less (relative) path. Cell
17
+ * data is consumer-supplied JSON, so `javascript:`/`vbscript:`/non-image
18
+ * `data:` URIs must never reach the DOM.
19
+ */
20
+ export const asSafeIconUrl = (value) => {
21
+ if (typeof value !== 'string' || value.trim().length === 0) {
22
+ return null;
23
+ }
24
+ const candidate = value.trim();
25
+ const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(candidate);
26
+ if (schemeMatch === null) {
27
+ return candidate;
28
+ }
29
+ const scheme = schemeMatch[1].toLowerCase();
30
+ if (scheme === 'http' || scheme === 'https') {
31
+ return candidate;
32
+ }
33
+ // Image data URIs only, from an explicit subtype allowlist — a data: URI
34
+ // continues with ';' (parameters like base64) or ',' (payload).
35
+ if (scheme === 'data' && /^data:image\/(svg\+xml|png|jpeg|jpg|gif|webp)[;,]/i.test(candidate)) {
36
+ return candidate;
37
+ }
38
+ return null;
39
+ };
14
40
  export const asTagCellData = (value) => {
15
41
  const record = asJsonObject(value);
16
42
  if (record === null || typeof record.text !== 'string') {
@@ -160,8 +186,9 @@ export const asInputCellData = (value) => {
160
186
  if (typeof record.ariaLabel === 'string') {
161
187
  inputData.ariaLabel = record.ariaLabel;
162
188
  }
163
- if (typeof record.iconUrl === 'string') {
164
- inputData.iconUrl = record.iconUrl;
189
+ const inputIconUrl = asSafeIconUrl(record.iconUrl ?? null);
190
+ if (inputIconUrl !== null) {
191
+ inputData.iconUrl = inputIconUrl;
165
192
  }
166
193
  if (typeof record.dataType === 'string') {
167
194
  inputData.dataType = record.dataType;
@@ -192,20 +219,38 @@ export const asInputDataType = (value) => {
192
219
  };
193
220
  export const asButtonCellData = (value) => {
194
221
  const record = asJsonObject(value);
195
- if (record === null || typeof record.text !== 'string') {
222
+ if (record === null) {
196
223
  return null;
197
224
  }
198
- const buttonData = { text: record.text };
225
+ const buttonIconUrl = asSafeIconUrl(record.iconUrl ?? null);
226
+ const buttonAriaLabel = typeof record.ariaLabel === 'string' ? record.ariaLabel : null;
227
+ const common = {};
199
228
  if (typeof record.disabled === 'boolean') {
200
- buttonData.disabled = record.disabled;
229
+ common.disabled = record.disabled;
201
230
  }
202
231
  if (typeof record.classes === 'string') {
203
- buttonData.classes = record.classes;
232
+ common.classes = record.classes;
204
233
  }
205
234
  if (typeof record.testId === 'string') {
206
- buttonData.testId = record.testId;
235
+ common.testId = record.testId;
236
+ }
237
+ if (typeof record.text === 'string') {
238
+ const textButton = { text: record.text, ...common };
239
+ if (buttonIconUrl !== null) {
240
+ textButton.iconUrl = buttonIconUrl;
241
+ }
242
+ if (buttonAriaLabel !== null) {
243
+ textButton.ariaLabel = buttonAriaLabel;
244
+ }
245
+ return textButton;
207
246
  }
208
- return buttonData;
247
+ // Icon-only buttons require BOTH a safe icon and an accessible name — a
248
+ // button with no visible text and no aria-label is a WCAG 4.1.2 failure,
249
+ // so such data fails narrowing and the cell falls back to plain text.
250
+ if (buttonIconUrl !== null && buttonAriaLabel !== null) {
251
+ return { iconUrl: buttonIconUrl, ariaLabel: buttonAriaLabel, ...common };
252
+ }
253
+ return null;
209
254
  };
210
255
  const asMenuItemsData = (rawItems) => {
211
256
  if (!Array.isArray(rawItems)) {
@@ -116,6 +116,8 @@ export type TableInputCellData = {
116
116
  /**
117
117
  * URL of a passive leading icon rendered inside the input field (e.g. a
118
118
  * currency glyph). Sized via `--table-cell-input-icon-size` (default 16px).
119
+ * Scheme-validated at narrowing time: only http(s), `data:image/*`, and
120
+ * relative URLs are accepted; anything else is dropped.
119
121
  */
120
122
  iconUrl?: string;
121
123
  /** Input dataType forwarded to the rendered Input ('text' | 'tel' | 'number' | …). */
@@ -128,14 +130,52 @@ export type TableInputCellData = {
128
130
  /** Inline error message shown when the validation pattern rejects the value. */
129
131
  onErrorMessage?: string;
130
132
  };
131
- /** Cell shape for `type: 'button'` the handler lives on the column. */
132
- export type TableButtonCellData = {
133
- text: string;
133
+ /** Options shared by both button cell variants. */
134
+ export type TableButtonCellCommonData = {
134
135
  disabled?: boolean;
135
136
  /** CSS classes forwarded to the Button (the consumer owns variant mapping). */
136
137
  classes?: string;
137
138
  testId?: string;
138
139
  };
140
+ /**
141
+ * Text-bearing button cell — `text` stays required exactly as before this
142
+ * type became a union, so existing consumers are unaffected. An optional
143
+ * `iconUrl` renders a leading icon; `ariaLabel` may override the accessible
144
+ * name (the visible text already names the button).
145
+ */
146
+ export type TableTextButtonCellData = TableButtonCellCommonData & {
147
+ text: string;
148
+ /**
149
+ * URL of a leading icon rendered next to the text. Sized via
150
+ * `--table-cell-icon-size` (default 16px). Scheme-validated at narrowing
151
+ * time: only http(s), `data:image/*`, and relative URLs are accepted;
152
+ * anything else is dropped.
153
+ */
154
+ iconUrl?: string;
155
+ ariaLabel?: string;
156
+ };
157
+ /**
158
+ * Icon-only button cell — renders as a bare ghost control. `ariaLabel` is
159
+ * required (the button has no visible text, so it MUST carry an accessible
160
+ * name); the decoder enforces the same rule at runtime for untyped data.
161
+ */
162
+ export type TableIconOnlyButtonCellData = TableButtonCellCommonData & {
163
+ text?: never;
164
+ /**
165
+ * URL of the button's icon. Sized via `--table-cell-icon-size` (default
166
+ * 16px). Scheme-validated at narrowing time: only http(s), `data:image/*`,
167
+ * and relative URLs are accepted; anything else is dropped (the cell then
168
+ * fails narrowing and falls back to plain text of the raw value).
169
+ */
170
+ iconUrl: string;
171
+ ariaLabel: string;
172
+ };
173
+ /**
174
+ * Cell shape for `type: 'button'` — the handler lives on the column. A
175
+ * discriminated union: text-bearing buttons keep `text` required (unchanged
176
+ * public contract), icon-only buttons require `iconUrl` + `ariaLabel`.
177
+ */
178
+ export type TableButtonCellData = TableTextButtonCellData | TableIconOnlyButtonCellData;
139
179
  /** One overflow-menu entry for action-group / popup-menu cells (JSON-safe). */
140
180
  export type TableMenuItemData = {
141
181
  id: string;
@@ -0,0 +1,4 @@
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.64645 1.64645C5.84171 1.45118 6.15829 1.45118 6.35355 1.64645L8.85355 4.14645C9.04882 4.34171 9.04882 4.65829 8.85355 4.85355C8.65829 5.04882 8.34171 5.04882 8.14645 4.85355L6 2.70711L3.85355 4.85355C3.65829 5.04882 3.34171 5.04882 3.14645 4.85355C2.95118 4.65829 2.95118 4.34171 3.14645 4.14645L5.64645 1.64645Z" fill="currentColor"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.14645 7.14645C3.34171 6.95118 3.65829 6.95118 3.85355 7.14645L6 9.29289L8.14645 7.14645C8.34171 6.95118 8.65829 6.95118 8.85355 7.14645C9.04882 7.34171 9.04882 7.65829 8.85355 7.85355L6.35355 10.3536C6.15829 10.5488 5.84171 10.5488 5.64645 10.3536L3.14645 7.85355C2.95118 7.65829 2.95118 7.34171 3.14645 7.14645Z" fill="var(--table-sort-inactive-color, #C7C7C7)"/>
4
+ </svg>
@@ -1,4 +1,4 @@
1
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <polyline points="18 15 12 9 6 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
3
- <polyline points="6 17 12 23 18 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.64645 1.64645C5.84171 1.45118 6.15829 1.45118 6.35355 1.64645L8.85355 4.14645C9.04882 4.34171 9.04882 4.65829 8.85355 4.85355C8.65829 5.04882 8.34171 5.04882 8.14645 4.85355L6 2.70711L3.85355 4.85355C3.65829 5.04882 3.34171 5.04882 3.14645 4.85355C2.95118 4.65829 2.95118 4.34171 3.14645 4.14645L5.64645 1.64645Z" fill="var(--table-sort-inactive-color, #C7C7C7)"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.14645 7.14645C3.34171 6.95118 3.65829 6.95118 3.85355 7.14645L6 9.29289L8.14645 7.14645C8.34171 6.95118 8.65829 6.95118 8.85355 7.14645C9.04882 7.34171 9.04882 7.65829 8.85355 7.85355L6.35355 10.3536C6.15829 10.5488 5.84171 10.5488 5.64645 10.3536L3.14645 7.85355C2.95118 7.65829 2.95118 7.34171 3.14645 7.14645Z" fill="var(--table-sort-inactive-color, #C7C7C7)"/>
4
4
  </svg>
@@ -0,0 +1,4 @@
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.64645 1.64645C5.84171 1.45118 6.15829 1.45118 6.35355 1.64645L8.85355 4.14645C9.04882 4.34171 9.04882 4.65829 8.85355 4.85355C8.65829 5.04882 8.34171 5.04882 8.14645 4.85355L6 2.70711L3.85355 4.85355C3.65829 5.04882 3.34171 5.04882 3.14645 4.85355C2.95118 4.65829 2.95118 4.34171 3.14645 4.14645L5.64645 1.64645Z" fill="var(--table-sort-inactive-color, #C7C7C7)"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.14645 7.14645C3.34171 6.95118 3.65829 6.95118 3.85355 7.14645L6 9.29289L8.14645 7.14645C8.34171 6.95118 8.65829 6.95118 8.85355 7.14645C9.04882 7.34171 9.04882 7.65829 8.85355 7.85355L6.35355 10.3536C6.15829 10.5488 5.84171 10.5488 5.64645 10.3536L3.14645 7.85355C2.95118 7.65829 2.95118 7.34171 3.14645 7.14645Z" fill="currentColor"/>
4
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.93.0",
3
+ "version": "2.95.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",