@juspay/svelte-ui-components 2.90.1 → 2.91.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.
@@ -42,6 +42,20 @@
42
42
  originalIndex: number;
43
43
  } = $props();
44
44
 
45
+ // The td applies column.align as text-align, which flex containers ignore:
46
+ // column-flex builtins stretch their children and row-flex builtins pack to
47
+ // flex-start, so an aligned column's builtin cells stayed left. Mirror the
48
+ // column alignment as a modifier class the flex builtins translate into
49
+ // align-items / justify-content (same idiom as the header's
50
+ // justify-content mapping).
51
+ const alignmentClass = $derived(
52
+ column.align === 'right'
53
+ ? 'builtin-align-end'
54
+ : column.align === 'center'
55
+ ? 'builtin-align-center'
56
+ : ''
57
+ );
58
+
45
59
  let copied = $state(false);
46
60
  let copyResetTimer: ReturnType<typeof setTimeout> | null = null;
47
61
 
@@ -99,7 +113,7 @@
99
113
  {:else if column.type === 'text-tag'}
100
114
  {@const data = asJsonObject(value) ?? {}}
101
115
  {@const tag = asTagCellData(data.tag ?? null)}
102
- <div class="builtin-text-tag">
116
+ <div class="builtin-text-tag {alignmentClass}">
103
117
  <span class="builtin-primary-text"
104
118
  >{typeof data.text === 'string' ? data.text : cellValueToText(value)}</span
105
119
  >
@@ -114,7 +128,7 @@
114
128
  </div>
115
129
  {:else if column.type === 'two-line-text'}
116
130
  {@const data = asJsonObject(value) ?? {}}
117
- <div class="builtin-two-line">
131
+ <div class="builtin-two-line {alignmentClass}">
118
132
  <span class="builtin-primary-text">{typeof data.text1 === 'string' ? data.text1 : '-'}</span>
119
133
  <span class="builtin-secondary-text">{typeof data.text2 === 'string' ? data.text2 : '-'}</span>
120
134
  </div>
@@ -123,7 +137,7 @@
123
137
  {@const icons = Array.isArray(data.icons)
124
138
  ? data.icons.filter((iconSrc) => typeof iconSrc === 'string')
125
139
  : []}
126
- <div class="builtin-icon-label">
140
+ <div class="builtin-icon-label {alignmentClass}">
127
141
  {#each icons as iconSrc, iconIndex (`${iconIndex}-${iconSrc}`)}
128
142
  <span class="builtin-icon-label-icon">
129
143
  <Img src={String(iconSrc)} alt="" fallback="" />
@@ -133,7 +147,7 @@
133
147
  </div>
134
148
  {:else if column.type === 'image-two-line-text'}
135
149
  {@const data = asJsonObject(value) ?? {}}
136
- <div class="builtin-image-two-line">
150
+ <div class="builtin-image-two-line {alignmentClass}">
137
151
  {#if typeof data.imageUrl === 'string' && data.imageUrl}
138
152
  <span class="builtin-thumb">
139
153
  <Img
@@ -145,7 +159,7 @@
145
159
  {:else}
146
160
  <span class="builtin-thumb builtin-thumb-placeholder"></span>
147
161
  {/if}
148
- <div class="builtin-two-line">
162
+ <div class="builtin-two-line {alignmentClass}">
149
163
  <span class="builtin-primary-text">{typeof data.text1 === 'string' ? data.text1 : '-'}</span>
150
164
  <span class="builtin-secondary-text">{typeof data.text2 === 'string' ? data.text2 : '-'}</span
151
165
  >
@@ -154,7 +168,7 @@
154
168
  {:else if column.type === 'tag-array'}
155
169
  {@const tags = asTagArrayItems(value)}
156
170
  {#if tags}
157
- <div class="builtin-tag-array">
171
+ <div class="builtin-tag-array {alignmentClass}">
158
172
  {#each tags as tag (tag.text)}
159
173
  <Pill text={tag.text} classes={tag.classes ?? ''} />
160
174
  {/each}
@@ -166,7 +180,7 @@
166
180
  {@const stackData = asAvatarStackData(value)}
167
181
  {#if stackData}
168
182
  {@const stack = buildAvatarStack(stackData)}
169
- <div class="builtin-avatar-stack">
183
+ <div class="builtin-avatar-stack {alignmentClass}">
170
184
  <IconStack icons={stack.icons} />
171
185
  {#if stack.rest > 0}
172
186
  <span class="builtin-avatar-rest">+{stack.rest}</span>
@@ -181,7 +195,7 @@
181
195
  {:else}
182
196
  {@const compare = asCompareCellData(value)}
183
197
  {#if compare}
184
- <div class="builtin-compare">
198
+ <div class="builtin-compare {alignmentClass}">
185
199
  {#if typeof compare.primary === 'string'}
186
200
  <span class="builtin-primary-text">{compare.primary}</span>
187
201
  {/if}
@@ -448,6 +462,35 @@
448
462
  gap: var(--table-cell-line-gap, 2px);
449
463
  }
450
464
 
465
+ /* column.align lands on the td as text-align, which flex layouts ignore.
466
+ Column-flex builtins follow it via align-items… */
467
+ .builtin-two-line.builtin-align-end,
468
+ .builtin-compare.builtin-align-end {
469
+ align-items: flex-end;
470
+ }
471
+
472
+ .builtin-two-line.builtin-align-center,
473
+ .builtin-compare.builtin-align-center {
474
+ align-items: center;
475
+ }
476
+
477
+ /* …and row-flex builtins follow it via justify-content. */
478
+ .builtin-text-tag.builtin-align-end,
479
+ .builtin-icon-label.builtin-align-end,
480
+ .builtin-image-two-line.builtin-align-end,
481
+ .builtin-tag-array.builtin-align-end,
482
+ .builtin-avatar-stack.builtin-align-end {
483
+ justify-content: flex-end;
484
+ }
485
+
486
+ .builtin-text-tag.builtin-align-center,
487
+ .builtin-icon-label.builtin-align-center,
488
+ .builtin-image-two-line.builtin-align-center,
489
+ .builtin-tag-array.builtin-align-center,
490
+ .builtin-avatar-stack.builtin-align-center {
491
+ justify-content: center;
492
+ }
493
+
451
494
  .builtin-text-tag {
452
495
  display: flex;
453
496
  align-items: center;
@@ -133,6 +133,37 @@
133
133
  let isRowClickable = $derived(typeof onRowClick === 'function');
134
134
  let isStickyHeader = $derived(stickyHeader || isTableScrollable);
135
135
 
136
+ // ─── Horizontal-scroll affordance ────────────────────────────────────────
137
+ // The table clips columns behind an internal horizontal scroll on narrow
138
+ // viewports, but a bare scroll container gives no visual hint that more
139
+ // columns exist. Track whether either edge has hidden content and surface
140
+ // it as edge scrims (see .table-scroll-shell styles).
141
+ let canScrollLeft = $state(false);
142
+ let canScrollRight = $state(false);
143
+
144
+ const trackHorizontalScroll = (scrollNode: HTMLElement) => {
145
+ const updateScrollHints = () => {
146
+ canScrollLeft = scrollNode.scrollLeft > 2;
147
+ canScrollRight = scrollNode.scrollLeft + scrollNode.clientWidth < scrollNode.scrollWidth - 2;
148
+ };
149
+ updateScrollHints();
150
+ scrollNode.addEventListener('scroll', updateScrollHints, { passive: true });
151
+ // Both the container resizing (viewport changes) and the table resizing
152
+ // (async rows/columns arriving) change scrollWidth, so observe both.
153
+ const hintResizeObserver = new ResizeObserver(updateScrollHints);
154
+ hintResizeObserver.observe(scrollNode);
155
+ const tableElement = scrollNode.querySelector('table');
156
+ if (tableElement) {
157
+ hintResizeObserver.observe(tableElement);
158
+ }
159
+ return {
160
+ destroy: () => {
161
+ scrollNode.removeEventListener('scroll', updateScrollHints);
162
+ hintResizeObserver.disconnect();
163
+ }
164
+ };
165
+ };
166
+
136
167
  // ─── C2-3: Search ─────────────────────────────────────────────────────────
137
168
  let searchTerm = $state('');
138
169
  let hasSearchConfig = $derived(!!searchConfig);
@@ -572,263 +603,286 @@
572
603
  class="table-container {isTableScrollable ? 'scrollable-table' : ''} {classes ?? ''}"
573
604
  data-pw={testId}
574
605
  >
575
- <table>
576
- {#if caption}
577
- <caption class="sr-only">{caption}</caption>
578
- {/if}
579
- <thead>
580
- <tr>
581
- {#if isCheckboxMode && !isSingleSelect}
582
- <th class="table-header table-checkbox-col" class:table-header-sticky={isStickyHeader}>
583
- <!-- Header tri-state checkbox -->
584
- <span
585
- class="table-checkbox-box"
586
- class:checked={headerCheckboxState === 'all'}
587
- class:indeterminate={headerCheckboxState === 'some'}
588
- role="checkbox"
589
- tabindex={0}
590
- aria-checked={headerCheckboxState === 'some'
591
- ? 'mixed'
592
- : headerCheckboxState === 'all'}
593
- aria-label="Select all rows"
594
- {...checkboxSelection?.getRowAttributes
595
- ? checkboxSelection.getRowAttributes('__header__', -1)
596
- : {}}
597
- aria-controls={selectableRowIds.map((rowId) => `row-checkbox-${rowId}`).join(' ')}
598
- onclick={toggleAllSelection}
599
- onkeydown={(keyboardEvent) =>
600
- handleCheckboxKeydown(keyboardEvent, toggleAllSelection)}
601
- >
602
- {#if headerCheckboxState === 'all'}
603
- <!-- eslint-disable svelte/no-at-html-tags -->
604
- <span class="table-checkbox-icon">{@html checkmarkSvg}</span>
605
- {:else if headerCheckboxState === 'some'}
606
- <!-- eslint-disable svelte/no-at-html-tags -->
607
- <span class="table-checkbox-icon">{@html minusSvg}</span>
608
- {/if}
609
- </span>
610
- </th>
611
- {:else if isCheckboxMode && isSingleSelect}
612
- <!-- In single-select mode the header cell is an empty spacer -->
613
- <th class="table-header table-checkbox-col" class:table-header-sticky={isStickyHeader}>
614
- </th>
615
- {/if}
616
- {#if rowNumberColumn}
617
- <th class="table-header table-row-number-col" class:table-header-sticky={isStickyHeader}
618
- >{rowNumberLabel}</th
619
- >
606
+ <div
607
+ class="table-scroll-shell"
608
+ class:scrollable-left={canScrollLeft}
609
+ class:scrollable-right={canScrollRight}
610
+ >
611
+ <div class="table-scroll" use:trackHorizontalScroll>
612
+ <table>
613
+ {#if caption}
614
+ <caption class="sr-only">{caption}</caption>
620
615
  {/if}
621
- {#each effectiveHeaders as header, colIndex (colIndex)}
622
- {@const headerColumn = columns?.[colIndex]}
623
- <th
624
- class="table-header"
625
- class:table-header-sticky={isStickyHeader}
626
- data-pw={headerColumn?.testId ?? null}
627
- style:text-align={headerColumn?.align ?? null}
628
- style:max-width={headerColumn?.maxWidth ?? null}
629
- >
630
- <span
631
- class="table-header-content"
632
- style:justify-content={headerColumn?.align === 'right'
633
- ? 'flex-end'
634
- : headerColumn?.align === 'center'
635
- ? 'center'
636
- : null}
637
- >
638
- {#if headerColumn?.tooltip}
639
- <Tooltip
640
- text={headerColumn.tooltip}
641
- position={headerTooltipPosition}
642
- icon={headerTooltipIcon}
643
- iconPosition="trailing"
644
- >
645
- <span
646
- class="table-header-label"
647
- class:table-header-label-plain={headerTooltipIcon}>{header}</span
648
- >
649
- </Tooltip>
650
- {:else}
651
- {header}
652
- {/if}
653
- {#if headerColumn?.filter}
654
- {@const filter = headerColumn.filter}
655
- <span class="table-header-filter">
656
- <Menu
657
- items={filter.options.map((option) => ({
658
- value: option.value,
659
- label: option.label
660
- }))}
661
- selectedValue={filter.selectedValue ?? null}
662
- role="listbox"
663
- testId={headerColumn.testId && `${headerColumn.testId}-filter`}
664
- onselect={(menuItem) =>
665
- filter.onFilterChange?.(
666
- menuItem.value === filter.selectedValue ? null : menuItem.value
667
- )}
668
- >
669
- {#snippet trigger()}
670
- <span
671
- class="table-header-filter-trigger"
672
- class:table-header-filter-active={typeof filter.selectedValue ===
673
- 'string'}
674
- >
675
- <Button
676
- ariaLabel="Filter by {header}"
677
- testId={headerColumn.testId && `${headerColumn.testId}-filter-trigger`}
678
- >
679
- {#snippet icon()}
680
- <!-- eslint-disable svelte/no-at-html-tags -->
681
- <span class="table-header-filter-icon">{@html chevronDownSmSvg}</span>
682
- {/snippet}
683
- </Button>
684
- </span>
685
- {/snippet}
686
- </Menu>
687
- </span>
688
- {/if}
689
- {#if isColumnSortable(colIndex)}
690
- <div class="sort-button">
691
- <Button onclick={() => handleSort(colIndex)} ariaLabel="Sort by {header}">
692
- {#if sortColumn === colIndex && sortDirection === 'asc'}
693
- {#if typeof sortAscIcon === 'function'}
694
- {@render sortAscIcon()}
695
- {:else}
696
- <span class="sort-icon">
697
- <!-- eslint-disable svelte/no-at-html-tags -->
698
- {@html chevronUpSvg}
699
- </span>
700
- {/if}
701
- {:else if sortColumn === colIndex && sortDirection === 'desc'}
702
- {#if typeof sortDescIcon === 'function'}
703
- {@render sortDescIcon()}
704
- {:else}
705
- <span class="sort-icon">
706
- <!-- eslint-disable svelte/no-at-html-tags -->
707
- {@html chevronDownSvg}
708
- </span>
709
- {/if}
710
- {:else if typeof sortDefaultIcon === 'function'}
711
- {@render sortDefaultIcon()}
712
- {:else}
713
- <span class="sort-icon sort-icon-idle">
714
- <!-- eslint-disable svelte/no-at-html-tags -->
715
- {@html sortDefaultSvg}
716
- </span>
717
- {/if}
718
- </Button>
719
- </div>
720
- {/if}
721
- </span>
722
- </th>
723
- {/each}
724
- </tr>
725
- </thead>
726
- <tbody>
727
- {#if filteredTableData.length === 0 && typeof empty === 'function'}
728
- <tr>
729
- <td
730
- class="table-empty"
731
- colspan={effectiveHeaders.length +
732
- (isCheckboxMode ? 1 : 0) +
733
- (rowNumberColumn ? 1 : 0)}
734
- >
735
- {@render empty()}
736
- </td>
737
- </tr>
738
- {:else}
739
- {#each paginatedTableData as row, pageRowIndex (rowIdByRow.get(row) ?? pageRowIndex)}
740
- {@const rowIndex = pageRowIndex + rowIndexOffset}
741
- {@const originalIndex = originalIndexByRow.get(row) ?? rowIndex}
742
- {@const rowId = rowIdByRow.get(row) ?? String(rowIndex)}
743
- {@const rowDisabled = isCheckboxMode && isRowDisabled(rowId)}
744
- {@const rowSelected = isCheckboxMode && isRowSelected(rowId)}
745
- <tr
746
- class="table-row"
747
- class:table-row-clickable={isRowClickable}
748
- class:table-row-selected={rowSelected}
749
- data-pw={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
750
- onclick={isRowClickable ? () => handleRowClick(rowIndex, row, originalIndex) : null}
751
- onkeydown={isRowClickable
752
- ? (keyboardEvent) => handleRowKeydown(keyboardEvent, rowIndex, row, originalIndex)
753
- : null}
754
- tabindex={isRowClickable ? 0 : null}
755
- >
756
- {#if isCheckboxMode}
757
- <td class="table-content table-checkbox-col">
616
+ <thead>
617
+ <tr>
618
+ {#if isCheckboxMode && !isSingleSelect}
619
+ <th
620
+ class="table-header table-checkbox-col"
621
+ class:table-header-sticky={isStickyHeader}
622
+ >
623
+ <!-- Header tri-state checkbox -->
758
624
  <span
759
625
  class="table-checkbox-box"
760
- class:checked={rowSelected}
761
- class:disabled={rowDisabled}
626
+ class:checked={headerCheckboxState === 'all'}
627
+ class:indeterminate={headerCheckboxState === 'some'}
762
628
  role="checkbox"
763
- id={`row-checkbox-${rowId}`}
764
- tabindex={rowDisabled ? -1 : 0}
765
- aria-checked={rowSelected}
766
- aria-disabled={rowDisabled}
767
- aria-label={`Select row ${rowId || 'non-selectable'}`}
629
+ tabindex={0}
630
+ aria-checked={headerCheckboxState === 'some'
631
+ ? 'mixed'
632
+ : headerCheckboxState === 'all'}
633
+ aria-label="Select all rows"
768
634
  {...checkboxSelection?.getRowAttributes
769
- ? checkboxSelection.getRowAttributes(rowId, rowIndex)
635
+ ? checkboxSelection.getRowAttributes('__header__', -1)
770
636
  : {}}
771
- onclick={(mouseEvent) => {
772
- mouseEvent.stopPropagation();
773
- toggleRowSelection(rowId);
774
- }}
775
- onkeydown={(keyboardEvent) => {
776
- keyboardEvent.stopPropagation();
777
- handleCheckboxKeydown(keyboardEvent, () => toggleRowSelection(rowId));
778
- }}
637
+ aria-controls={selectableRowIds
638
+ .map((rowId) => `row-checkbox-${rowId}`)
639
+ .join(' ')}
640
+ onclick={toggleAllSelection}
641
+ onkeydown={(keyboardEvent) =>
642
+ handleCheckboxKeydown(keyboardEvent, toggleAllSelection)}
779
643
  >
780
- {#if rowSelected}
644
+ {#if headerCheckboxState === 'all'}
781
645
  <!-- eslint-disable svelte/no-at-html-tags -->
782
646
  <span class="table-checkbox-icon">{@html checkmarkSvg}</span>
647
+ {:else if headerCheckboxState === 'some'}
648
+ <!-- eslint-disable svelte/no-at-html-tags -->
649
+ <span class="table-checkbox-icon">{@html minusSvg}</span>
783
650
  {/if}
784
651
  </span>
785
- </td>
652
+ </th>
653
+ {:else if isCheckboxMode && isSingleSelect}
654
+ <!-- In single-select mode the header cell is an empty spacer -->
655
+ <th
656
+ class="table-header table-checkbox-col"
657
+ class:table-header-sticky={isStickyHeader}
658
+ >
659
+ </th>
786
660
  {/if}
787
661
  {#if rowNumberColumn}
788
- <td class="table-content table-row-number-col">{rowNumberFor(pageRowIndex)}</td>
662
+ <th
663
+ class="table-header table-row-number-col"
664
+ class:table-header-sticky={isStickyHeader}>{rowNumberLabel}</th
665
+ >
789
666
  {/if}
790
- {#each row as cellValue, colIndex (colIndex)}
791
- {@const keyedColumn = columns?.[colIndex]}
792
- {@const keyedRow = keyedRowByProjected?.get(row)}
793
- {@const isScalarCell =
794
- typeof cellValue === 'string' ||
795
- typeof cellValue === 'number' ||
796
- typeof cellValue === 'boolean'}
797
- <td
798
- class="table-content"
799
- data-pw={typeof getCellTestId === 'function'
800
- ? getCellTestId(row, cellValue, rowIndex)
801
- : null}
802
- style:text-align={keyedColumn?.align ?? null}
803
- style:max-width={keyedColumn?.maxWidth ?? null}
804
- title={keyedColumn?.maxWidth && isScalarCell ? String(cellValue) : null}
667
+ {#each effectiveHeaders as header, colIndex (colIndex)}
668
+ {@const headerColumn = columns?.[colIndex]}
669
+ <th
670
+ class="table-header"
671
+ class:table-header-sticky={isStickyHeader}
672
+ data-pw={headerColumn?.testId ?? null}
673
+ style:text-align={headerColumn?.align ?? null}
674
+ style:max-width={headerColumn?.maxWidth ?? null}
805
675
  >
806
- <div
807
- class={isContentScrollable ? 'scrollable-content' : ''}
808
- class:table-cell-clamp={keyedColumn?.maxWidth && isScalarCell}
676
+ <span
677
+ class="table-header-content"
678
+ style:justify-content={headerColumn?.align === 'right'
679
+ ? 'flex-end'
680
+ : headerColumn?.align === 'center'
681
+ ? 'center'
682
+ : null}
809
683
  >
810
- {#if keyedColumn && typeof keyedColumn.cell === 'function' && keyedRow}
811
- {@render keyedColumn.cell(keyedRow, rowIndex, originalIndex)}
812
- {:else if keyedColumn?.type && keyedColumn.type !== 'text' && keyedColumn.type !== 'custom'}
813
- <BuiltinCell
814
- column={keyedColumn}
815
- value={cellValue}
816
- {rowIndex}
817
- {originalIndex}
818
- />
819
- {:else if typeof cell === 'function'}
820
- {@render cell(cellValue, rowIndex, colIndex)}
684
+ {#if headerColumn?.tooltip}
685
+ <Tooltip
686
+ text={headerColumn.tooltip}
687
+ position={headerTooltipPosition}
688
+ icon={headerTooltipIcon}
689
+ iconPosition="trailing"
690
+ >
691
+ <span
692
+ class="table-header-label"
693
+ class:table-header-label-plain={headerTooltipIcon}>{header}</span
694
+ >
695
+ </Tooltip>
821
696
  {:else}
822
- {cellValue}
697
+ {header}
823
698
  {/if}
824
- </div>
825
- </td>
699
+ {#if headerColumn?.filter}
700
+ {@const filter = headerColumn.filter}
701
+ <span class="table-header-filter">
702
+ <Menu
703
+ items={filter.options.map((option) => ({
704
+ value: option.value,
705
+ label: option.label
706
+ }))}
707
+ selectedValue={filter.selectedValue ?? null}
708
+ role="listbox"
709
+ testId={headerColumn.testId && `${headerColumn.testId}-filter`}
710
+ onselect={(menuItem) =>
711
+ filter.onFilterChange?.(
712
+ menuItem.value === filter.selectedValue ? null : menuItem.value
713
+ )}
714
+ >
715
+ {#snippet trigger()}
716
+ <span
717
+ class="table-header-filter-trigger"
718
+ class:table-header-filter-active={typeof filter.selectedValue ===
719
+ 'string'}
720
+ >
721
+ <Button
722
+ ariaLabel="Filter by {header}"
723
+ testId={headerColumn.testId &&
724
+ `${headerColumn.testId}-filter-trigger`}
725
+ >
726
+ {#snippet icon()}
727
+ <!-- eslint-disable svelte/no-at-html-tags -->
728
+ <span class="table-header-filter-icon"
729
+ >{@html chevronDownSmSvg}</span
730
+ >
731
+ {/snippet}
732
+ </Button>
733
+ </span>
734
+ {/snippet}
735
+ </Menu>
736
+ </span>
737
+ {/if}
738
+ {#if isColumnSortable(colIndex)}
739
+ <div class="sort-button">
740
+ <Button onclick={() => handleSort(colIndex)} ariaLabel="Sort by {header}">
741
+ {#if sortColumn === colIndex && sortDirection === 'asc'}
742
+ {#if typeof sortAscIcon === 'function'}
743
+ {@render sortAscIcon()}
744
+ {:else}
745
+ <span class="sort-icon">
746
+ <!-- eslint-disable svelte/no-at-html-tags -->
747
+ {@html chevronUpSvg}
748
+ </span>
749
+ {/if}
750
+ {:else if sortColumn === colIndex && sortDirection === 'desc'}
751
+ {#if typeof sortDescIcon === 'function'}
752
+ {@render sortDescIcon()}
753
+ {:else}
754
+ <span class="sort-icon">
755
+ <!-- eslint-disable svelte/no-at-html-tags -->
756
+ {@html chevronDownSvg}
757
+ </span>
758
+ {/if}
759
+ {:else if typeof sortDefaultIcon === 'function'}
760
+ {@render sortDefaultIcon()}
761
+ {:else}
762
+ <span class="sort-icon sort-icon-idle">
763
+ <!-- eslint-disable svelte/no-at-html-tags -->
764
+ {@html sortDefaultSvg}
765
+ </span>
766
+ {/if}
767
+ </Button>
768
+ </div>
769
+ {/if}
770
+ </span>
771
+ </th>
826
772
  {/each}
827
773
  </tr>
828
- {/each}
829
- {/if}
830
- </tbody>
831
- </table>
774
+ </thead>
775
+ <tbody>
776
+ {#if filteredTableData.length === 0 && typeof empty === 'function'}
777
+ <tr>
778
+ <td
779
+ class="table-empty"
780
+ colspan={effectiveHeaders.length +
781
+ (isCheckboxMode ? 1 : 0) +
782
+ (rowNumberColumn ? 1 : 0)}
783
+ >
784
+ {@render empty()}
785
+ </td>
786
+ </tr>
787
+ {:else}
788
+ {#each paginatedTableData as row, pageRowIndex (rowIdByRow.get(row) ?? pageRowIndex)}
789
+ {@const rowIndex = pageRowIndex + rowIndexOffset}
790
+ {@const originalIndex = originalIndexByRow.get(row) ?? rowIndex}
791
+ {@const rowId = rowIdByRow.get(row) ?? String(rowIndex)}
792
+ {@const rowDisabled = isCheckboxMode && isRowDisabled(rowId)}
793
+ {@const rowSelected = isCheckboxMode && isRowSelected(rowId)}
794
+ <tr
795
+ class="table-row"
796
+ class:table-row-clickable={isRowClickable}
797
+ class:table-row-selected={rowSelected}
798
+ data-pw={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
799
+ onclick={isRowClickable
800
+ ? () => handleRowClick(rowIndex, row, originalIndex)
801
+ : null}
802
+ onkeydown={isRowClickable
803
+ ? (keyboardEvent) =>
804
+ handleRowKeydown(keyboardEvent, rowIndex, row, originalIndex)
805
+ : null}
806
+ tabindex={isRowClickable ? 0 : null}
807
+ >
808
+ {#if isCheckboxMode}
809
+ <td class="table-content table-checkbox-col">
810
+ <span
811
+ class="table-checkbox-box"
812
+ class:checked={rowSelected}
813
+ class:disabled={rowDisabled}
814
+ role="checkbox"
815
+ id={`row-checkbox-${rowId}`}
816
+ tabindex={rowDisabled ? -1 : 0}
817
+ aria-checked={rowSelected}
818
+ aria-disabled={rowDisabled}
819
+ aria-label={`Select row ${rowId || 'non-selectable'}`}
820
+ {...checkboxSelection?.getRowAttributes
821
+ ? checkboxSelection.getRowAttributes(rowId, rowIndex)
822
+ : {}}
823
+ onclick={(mouseEvent) => {
824
+ mouseEvent.stopPropagation();
825
+ toggleRowSelection(rowId);
826
+ }}
827
+ onkeydown={(keyboardEvent) => {
828
+ keyboardEvent.stopPropagation();
829
+ handleCheckboxKeydown(keyboardEvent, () => toggleRowSelection(rowId));
830
+ }}
831
+ >
832
+ {#if rowSelected}
833
+ <!-- eslint-disable svelte/no-at-html-tags -->
834
+ <span class="table-checkbox-icon">{@html checkmarkSvg}</span>
835
+ {/if}
836
+ </span>
837
+ </td>
838
+ {/if}
839
+ {#if rowNumberColumn}
840
+ <td class="table-content table-row-number-col">{rowNumberFor(pageRowIndex)}</td>
841
+ {/if}
842
+ {#each row as cellValue, colIndex (colIndex)}
843
+ {@const keyedColumn = columns?.[colIndex]}
844
+ {@const keyedRow = keyedRowByProjected?.get(row)}
845
+ {@const isScalarCell =
846
+ typeof cellValue === 'string' ||
847
+ typeof cellValue === 'number' ||
848
+ typeof cellValue === 'boolean'}
849
+ <td
850
+ class="table-content"
851
+ data-pw={typeof getCellTestId === 'function'
852
+ ? getCellTestId(row, cellValue, rowIndex)
853
+ : null}
854
+ style:text-align={keyedColumn?.align ?? null}
855
+ style:max-width={keyedColumn?.maxWidth ?? null}
856
+ title={keyedColumn?.maxWidth && isScalarCell ? String(cellValue) : null}
857
+ >
858
+ <div
859
+ class={isContentScrollable ? 'scrollable-content' : ''}
860
+ class:table-cell-clamp={keyedColumn?.maxWidth && isScalarCell}
861
+ >
862
+ {#if keyedColumn && typeof keyedColumn.cell === 'function' && keyedRow}
863
+ {@render keyedColumn.cell(keyedRow, rowIndex, originalIndex)}
864
+ {:else if keyedColumn?.type && keyedColumn.type !== 'text' && keyedColumn.type !== 'custom'}
865
+ <BuiltinCell
866
+ column={keyedColumn}
867
+ value={cellValue}
868
+ {rowIndex}
869
+ {originalIndex}
870
+ />
871
+ {:else if typeof cell === 'function'}
872
+ {@render cell(cellValue, rowIndex, colIndex)}
873
+ {:else}
874
+ {cellValue}
875
+ {/if}
876
+ </div>
877
+ </td>
878
+ {/each}
879
+ </tr>
880
+ {/each}
881
+ {/if}
882
+ </tbody>
883
+ </table>
884
+ </div>
885
+ </div>
832
886
  {#if typeof paginatorSlot === 'function'}
833
887
  <div class="table-footer">
834
888
  {@render paginatorSlot()}
@@ -968,6 +1022,57 @@
968
1022
  overflow-y: auto;
969
1023
  }
970
1024
 
1025
+ .table-scroll-shell {
1026
+ position: relative;
1027
+ min-width: 0;
1028
+ }
1029
+
1030
+ /* Edge scrims: fade the clipped side of the table into its background so
1031
+ hidden columns read as "more content this way". Class-driven from live
1032
+ scroll state — never shown when the table fits. pointer-events: none
1033
+ keeps cells under the fade clickable; z-index 2 paints above sticky
1034
+ headers (z-index 1). */
1035
+ .table-scroll-shell::before,
1036
+ .table-scroll-shell::after {
1037
+ content: '';
1038
+ position: absolute;
1039
+ top: 0;
1040
+ bottom: 0;
1041
+ width: var(--table-scroll-scrim-width, 32px);
1042
+ opacity: 0;
1043
+ pointer-events: none;
1044
+ transition: opacity 0.2s ease;
1045
+ z-index: 2;
1046
+ }
1047
+
1048
+ .table-scroll-shell::before {
1049
+ left: 0;
1050
+ border-radius: var(--table-border-radius, var(--radius, 4px)) 0 0
1051
+ var(--table-border-radius, var(--radius, 4px));
1052
+ background: linear-gradient(to right, var(--table-scroll-scrim-color, #ffffff), transparent);
1053
+ }
1054
+
1055
+ .table-scroll-shell::after {
1056
+ right: 0;
1057
+ border-radius: 0 var(--table-border-radius, var(--radius, 4px))
1058
+ var(--table-border-radius, var(--radius, 4px)) 0;
1059
+ background: linear-gradient(to left, var(--table-scroll-scrim-color, #ffffff), transparent);
1060
+ }
1061
+
1062
+ .table-scroll-shell.scrollable-left::before {
1063
+ opacity: 1;
1064
+ }
1065
+
1066
+ .table-scroll-shell.scrollable-right::after {
1067
+ opacity: 1;
1068
+ }
1069
+
1070
+ .table-scroll {
1071
+ overflow-x: auto;
1072
+ min-width: 0;
1073
+ scrollbar-width: thin;
1074
+ }
1075
+
971
1076
  table {
972
1077
  width: var(--table-width, 100%);
973
1078
  border-collapse: var(--table-border-collapse, collapse);
@@ -192,6 +192,9 @@ export type TableColumn = {
192
192
  /**
193
193
  * Horizontal alignment for this column's header and body cells. When unset,
194
194
  * cells follow the table-wide `--table-text-align` (left by default).
195
+ * Built-in flex cells (compare, two-line-text, text-tag, icon-label,
196
+ * image-two-line-text, tag-array, avatar-stack) mirror it into their flex
197
+ * alignment so stacked/inline content follows the aligned edge too.
195
198
  */
196
199
  align?: 'left' | 'center' | 'right';
197
200
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.90.1",
3
+ "version": "2.91.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",