@juspay/svelte-ui-components 2.28.2 → 2.29.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.
Files changed (34) hide show
  1. package/dist/Banner/Banner.svelte +34 -7
  2. package/dist/Banner/properties.d.ts +2 -0
  3. package/dist/Breadcrumb/Breadcrumb.svelte +58 -0
  4. package/dist/Breadcrumb/Breadcrumb.svelte.d.ts +4 -0
  5. package/dist/Breadcrumb/properties.d.ts +19 -0
  6. package/dist/Breadcrumb/properties.js +1 -0
  7. package/dist/Calendar/Calendar.svelte +10 -4
  8. package/dist/Calendar/properties.d.ts +2 -0
  9. package/dist/DateRangePicker/DateRangePicker.svelte +627 -0
  10. package/dist/DateRangePicker/DateRangePicker.svelte.d.ts +4 -0
  11. package/dist/DateRangePicker/properties.d.ts +74 -0
  12. package/dist/DateRangePicker/properties.js +1 -0
  13. package/dist/EmptyState/EmptyState.svelte +3 -1
  14. package/dist/EmptyState/properties.d.ts +1 -1
  15. package/dist/FileInput/FileInput.svelte +176 -0
  16. package/dist/FileInput/FileInput.svelte.d.ts +4 -0
  17. package/dist/FileInput/properties.d.ts +18 -0
  18. package/dist/FileInput/properties.js +1 -0
  19. package/dist/Modal/Modal.svelte +5 -4
  20. package/dist/Pagination/Pagination.svelte +56 -11
  21. package/dist/Pagination/properties.d.ts +15 -0
  22. package/dist/Pill/Pill.svelte +10 -0
  23. package/dist/Pill/properties.d.ts +7 -0
  24. package/dist/Select/Select.svelte +77 -30
  25. package/dist/Select/Select.svelte.d.ts +1 -1
  26. package/dist/Select/properties.d.ts +12 -1
  27. package/dist/Sheet/Sheet.svelte +16 -0
  28. package/dist/Sheet/properties.d.ts +4 -0
  29. package/dist/Table/Table.svelte +28 -6
  30. package/dist/Table/properties.d.ts +6 -0
  31. package/dist/Tabs/Tabs.svelte +52 -8
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +3 -0
  34. package/package.json +1 -1
@@ -5,18 +5,26 @@
5
5
  import chevronDownSvg from '../assets/chevron-down.svg?raw';
6
6
 
7
7
  let {
8
- items,
8
+ items: rawItems,
9
9
  value = $bindable([]),
10
10
  multiple = false,
11
11
  searchable = false,
12
12
  placeholder = '',
13
13
  disabled = false,
14
+ bottomContent,
15
+ optionIndicator,
14
16
  testId,
17
+ itemTestId,
15
18
  onchange,
16
- classes
19
+ classes,
20
+ open = $bindable(false)
17
21
  }: SelectProperties = $props();
18
22
 
19
- let isOpen = $state(false);
23
+ function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
24
+ return source.map((entry) => (typeof entry === 'string' ? { id: entry, label: entry } : entry));
25
+ }
26
+
27
+ let items: SelectItem[] = $derived(normalizeItems(rawItems));
20
28
  let query = $state('');
21
29
  let highlightedIndex = $state(-1);
22
30
  let containerEl: HTMLDivElement | null = $state(null);
@@ -48,13 +56,13 @@
48
56
  highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : null
49
57
  );
50
58
 
51
- let searchPlaceholder = $derived(isOpen && displayText.length > 0 ? displayText : placeholder);
59
+ let searchPlaceholder = $derived(open && displayText.length > 0 ? displayText : placeholder);
52
60
 
53
- async function open(): Promise<void> {
54
- if (disabled || isOpen) {
61
+ async function openDropdown(): Promise<void> {
62
+ if (disabled || open) {
55
63
  return;
56
64
  }
57
- isOpen = true;
65
+ open = true;
58
66
  highlightedIndex = -1;
59
67
  query = '';
60
68
  if (searchable) {
@@ -66,7 +74,7 @@
66
74
  }
67
75
 
68
76
  function close(): void {
69
- isOpen = false;
77
+ open = false;
70
78
  query = '';
71
79
  highlightedIndex = -1;
72
80
  }
@@ -119,17 +127,17 @@
119
127
 
120
128
  function handleTriggerClick(event: MouseEvent): void {
121
129
  if (event.target instanceof HTMLInputElement) {
122
- if (!isOpen) {
123
- open();
130
+ if (!open) {
131
+ openDropdown();
124
132
  }
125
133
  return;
126
134
  }
127
135
  if (multiple && searchable) {
128
- open();
129
- } else if (isOpen) {
136
+ openDropdown();
137
+ } else if (open) {
130
138
  close();
131
139
  } else {
132
- open();
140
+ openDropdown();
133
141
  }
134
142
  }
135
143
 
@@ -140,28 +148,28 @@
140
148
  switch (event.key) {
141
149
  case 'Enter':
142
150
  event.preventDefault();
143
- if (isOpen) {
151
+ if (open) {
144
152
  selectHighlighted();
145
153
  } else {
146
- open();
154
+ openDropdown();
147
155
  }
148
156
  break;
149
157
  case ' ':
150
158
  if (!(event.target instanceof HTMLInputElement)) {
151
159
  event.preventDefault();
152
- if (isOpen) {
160
+ if (open) {
153
161
  selectHighlighted();
154
162
  } else {
155
- open();
163
+ openDropdown();
156
164
  }
157
165
  }
158
166
  break;
159
167
  case 'ArrowDown':
160
168
  event.preventDefault();
161
- if (isOpen) {
169
+ if (open) {
162
170
  moveHighlight(1);
163
171
  } else {
164
- open();
172
+ openDropdown();
165
173
  }
166
174
  break;
167
175
  case 'ArrowUp':
@@ -169,7 +177,7 @@
169
177
  moveHighlight(-1);
170
178
  break;
171
179
  case 'Escape':
172
- if (isOpen) {
180
+ if (open) {
173
181
  close();
174
182
  if (!searchable && triggerEl !== null) {
175
183
  triggerEl.focus();
@@ -185,7 +193,7 @@
185
193
  }
186
194
  break;
187
195
  case 'Tab':
188
- if (isOpen) {
196
+ if (open) {
189
197
  close();
190
198
  }
191
199
  break;
@@ -197,15 +205,15 @@
197
205
  return;
198
206
  }
199
207
  query = event.target.value;
200
- if (!isOpen) {
201
- isOpen = true;
208
+ if (!open) {
209
+ open = true;
202
210
  }
203
211
  highlightedIndex = -1;
204
212
  }
205
213
 
206
214
  function handleSearchFocus(): void {
207
- if (!isOpen) {
208
- open();
215
+ if (!open) {
216
+ openDropdown();
209
217
  }
210
218
  }
211
219
 
@@ -229,7 +237,7 @@
229
237
 
230
238
  <div
231
239
  class="select {classes ?? ''}"
232
- class:open={isOpen}
240
+ class:open
233
241
  class:disabled
234
242
  bind:this={containerEl}
235
243
  {...typeof testId === 'string' ? { 'data-pw': testId } : {}}
@@ -240,7 +248,7 @@
240
248
  onclick={handleTriggerClick}
241
249
  onkeydown={handleKeydown}
242
250
  role="combobox"
243
- aria-expanded={isOpen}
251
+ aria-expanded={open}
244
252
  aria-haspopup="listbox"
245
253
  aria-controls={listboxId}
246
254
  {...highlightedOptionId !== null ? { 'aria-activedescendant': highlightedOptionId } : {}}
@@ -276,7 +284,7 @@
276
284
  <input
277
285
  class="select-search"
278
286
  type="text"
279
- value={isOpen ? query : displayText}
287
+ value={open ? query : displayText}
280
288
  oninput={handleSearchInput}
281
289
  onfocus={handleSearchFocus}
282
290
  bind:this={searchInputEl}
@@ -294,28 +302,49 @@
294
302
  <span class="select-arrow">{@html chevronDownSvg}</span>
295
303
  </div>
296
304
 
297
- {#if isOpen && !disabled}
305
+ {#if open && !disabled}
298
306
  <div class="select-dropdown" role="listbox" id={listboxId} aria-multiselectable={multiple}>
299
307
  {#if filteredItems.length === 0}
300
308
  <div class="select-empty">No results</div>
301
309
  {:else}
302
310
  {#each filteredItems as item, index (item.id)}
303
- <!-- svelte-ignore a11y_click_events_have_key_events -->
304
311
  <div
305
312
  class="select-option"
313
+ class:multi={multiple}
306
314
  class:selected={value.includes(item.id)}
307
315
  class:highlighted={index === highlightedIndex}
308
316
  role="option"
309
317
  id={`${listboxId}-option-${index}`}
310
318
  aria-selected={value.includes(item.id)}
311
319
  tabindex="-1"
320
+ {...item.testId
321
+ ? { 'data-pw': item.testId }
322
+ : typeof itemTestId === 'string'
323
+ ? { 'data-pw': `${itemTestId}-${item.id}` }
324
+ : typeof testId === 'string'
325
+ ? { 'data-pw': `${testId}-${item.id}` }
326
+ : {}}
312
327
  onclick={() => selectItem(item.id)}
313
328
  onmouseenter={() => (highlightedIndex = index)}
314
329
  >
330
+ {#if multiple}
331
+ {#if typeof optionIndicator === 'function'}
332
+ {@render optionIndicator({ checked: value.includes(item.id) })}
333
+ {:else}
334
+ <span class="select-option-indicator" aria-hidden="true"
335
+ >{value.includes(item.id) ? '☑' : '☐'}</span
336
+ >
337
+ {/if}
338
+ {/if}
315
339
  {item.label}
316
340
  </div>
317
341
  {/each}
318
342
  {/if}
343
+ {#if typeof bottomContent === 'function'}
344
+ <div class="select-bottom-content">
345
+ {@render bottomContent()}
346
+ </div>
347
+ {/if}
319
348
  </div>
320
349
  {/if}
321
350
  </div>
@@ -436,6 +465,12 @@
436
465
  transition: background 0.1s;
437
466
  }
438
467
 
468
+ .select-option.multi {
469
+ display: flex;
470
+ align-items: center;
471
+ gap: var(--select-option-gap, 0);
472
+ }
473
+
439
474
  .select-option:hover,
440
475
  .select-option.highlighted {
441
476
  background: var(--select-option-hover-background, #f0f0f0);
@@ -454,6 +489,13 @@
454
489
  );
455
490
  }
456
491
 
492
+ .select-option-indicator {
493
+ display: inline-flex;
494
+ align-items: center;
495
+ color: var(--select-option-indicator-color, currentColor);
496
+ flex-shrink: 0;
497
+ }
498
+
457
499
  .select-empty {
458
500
  padding: var(--select-empty-padding, 8px 12px);
459
501
  color: var(--select-empty-color, #999999);
@@ -461,6 +503,11 @@
461
503
  font-size: var(--select-empty-font-size, inherit);
462
504
  }
463
505
 
506
+ .select-bottom-content {
507
+ border-top: var(--select-bottom-content-border, none);
508
+ padding: var(--select-bottom-content-padding, 8px 12px);
509
+ }
510
+
464
511
  .select-trigger :global(.pill) {
465
512
  --pill-background: var(--select-pill-background, #e0e0e0);
466
513
  --pill-color: var(--select-pill-color, #333333);
@@ -1,4 +1,4 @@
1
1
  import type { SelectProperties } from './properties';
2
- declare const Select: import("svelte").Component<SelectProperties, {}, "value">;
2
+ declare const Select: import("svelte").Component<SelectProperties, {}, "value" | "open">;
3
3
  type Select = ReturnType<typeof Select>;
4
4
  export default Select;
@@ -1,10 +1,13 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export type SelectProperties = MandatorySelectProperties & OptionalSelectProperties & SelectEventProperties;
2
3
  export type SelectItem = {
3
4
  id: string;
4
5
  label: string;
6
+ /** Optional per-option test id, emitted as `data-pw` on the option element. */
7
+ testId?: string;
5
8
  };
6
9
  export type MandatorySelectProperties = {
7
- items: SelectItem[];
10
+ items: SelectItem[] | string[];
8
11
  };
9
12
  export type OptionalSelectProperties = {
10
13
  value?: string[];
@@ -12,8 +15,16 @@ export type OptionalSelectProperties = {
12
15
  searchable?: boolean;
13
16
  placeholder?: string;
14
17
  disabled?: boolean;
18
+ bottomContent?: Snippet;
19
+ optionIndicator?: Snippet<[{
20
+ checked: boolean;
21
+ }]>;
15
22
  testId?: string;
23
+ /** Fallback per-option test id prefix. Each option emits `data-pw="{itemTestId}-{id}"` when its own `item.testId` is not set. */
24
+ itemTestId?: string;
16
25
  classes?: string;
26
+ /** Bindable. Controls whether the dropdown is open; the component writes back on open/close so parents can `bind:open` to observe or drive it. Unbound, the component manages its own state. */
27
+ open?: boolean;
17
28
  };
18
29
  export type SelectEventProperties = {
19
30
  onchange?: (value: string[]) => void;
@@ -14,6 +14,8 @@
14
14
  content,
15
15
  footer,
16
16
  onclose,
17
+ onafteropen,
18
+ onafterclose,
17
19
  classes
18
20
  }: SheetProperties = $props();
19
21
 
@@ -90,6 +92,18 @@
90
92
  }
91
93
  };
92
94
  }
95
+
96
+ function handleIntroEnd() {
97
+ if (typeof onafteropen === 'function') {
98
+ onafteropen();
99
+ }
100
+ }
101
+
102
+ function handleOutroEnd() {
103
+ if (typeof onafterclose === 'function') {
104
+ onafterclose();
105
+ }
106
+ }
93
107
  </script>
94
108
 
95
109
  {#if open}
@@ -112,6 +126,8 @@
112
126
  aria-label={title ?? 'Sheet'}
113
127
  tabindex="-1"
114
128
  transition:fly|global={flyParams}
129
+ onintroend={handleIntroEnd}
130
+ onoutroend={handleOutroEnd}
115
131
  >
116
132
  {#if typeof title === 'string' || showCloseButton}
117
133
  <div class="sheet-header">
@@ -16,4 +16,8 @@ export type OptionalSheetProperties = {
16
16
  };
17
17
  export type SheetEventProperties = {
18
18
  onclose?: () => void;
19
+ /** Called once after the open transition has fully completed. */
20
+ onafteropen?: () => void;
21
+ /** Called once after the close transition has fully completed. */
22
+ onafterclose?: () => void;
19
23
  };
@@ -24,7 +24,10 @@
24
24
  empty,
25
25
  onRowClick,
26
26
  onSort,
27
- classes
27
+ classes,
28
+ paginatorSlot,
29
+ getRowTestId,
30
+ getCellTestId
28
31
  }: TableProperties = $props();
29
32
 
30
33
  let sortColumn = $state<number | null>(null);
@@ -48,9 +51,9 @@
48
51
  const colIndex = sortColumn;
49
52
  const direction = sortDirection;
50
53
 
51
- return [...tableData].sort((a, b) => {
52
- const valueA = a[colIndex];
53
- const valueB = b[colIndex];
54
+ return [...tableData].sort((rowA, rowB) => {
55
+ const valueA = rowA[colIndex];
56
+ const valueB = rowB[colIndex];
54
57
 
55
58
  if (typeof valueA === 'number' && typeof valueB === 'number') {
56
59
  return direction === 'asc' ? valueA - valueB : valueB - valueA;
@@ -171,12 +174,20 @@
171
174
  <tr
172
175
  class="table-row"
173
176
  class:table-row-clickable={isRowClickable}
177
+ data-pw={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
174
178
  onclick={isRowClickable ? () => handleRowClick(rowIndex, row) : null}
175
- onkeydown={isRowClickable ? (e) => handleRowKeydown(e, rowIndex, row) : null}
179
+ onkeydown={isRowClickable
180
+ ? (keyboardEvent) => handleRowKeydown(keyboardEvent, rowIndex, row)
181
+ : null}
176
182
  tabindex={isRowClickable ? 0 : null}
177
183
  >
178
184
  {#each row as cellValue, colIndex (colIndex)}
179
- <td class="table-content">
185
+ <td
186
+ class="table-content"
187
+ data-pw={typeof getCellTestId === 'function'
188
+ ? getCellTestId(row, cellValue, rowIndex)
189
+ : null}
190
+ >
180
191
  <div class={isContentScrollable ? 'scrollable-content' : ''}>
181
192
  {#if typeof cell === 'function'}
182
193
  {@render cell(cellValue, rowIndex, colIndex)}
@@ -191,6 +202,11 @@
191
202
  {/if}
192
203
  </tbody>
193
204
  </table>
205
+ {#if typeof paginatorSlot === 'function'}
206
+ <div class="table-footer">
207
+ {@render paginatorSlot()}
208
+ </div>
209
+ {/if}
194
210
  </div>
195
211
  {/if}
196
212
 
@@ -341,6 +357,12 @@
341
357
  color: var(--table-empty-color, #9ca3af);
342
358
  }
343
359
 
360
+ .table-footer {
361
+ border-top: var(--table-footer-border, 1px solid #e5e7eb);
362
+ padding: var(--table-footer-padding, 8px 16px);
363
+ background-color: var(--table-footer-background, transparent);
364
+ }
365
+
344
366
  .sr-only {
345
367
  position: absolute;
346
368
  width: 1px;
@@ -19,6 +19,12 @@ export type OptionalTableProperties = {
19
19
  cell?: Snippet<[JSONValue, number, number]>;
20
20
  empty?: Snippet;
21
21
  classes?: string;
22
+ /** Snippet rendered in a footer region below the table (e.g. a paginator). */
23
+ paginatorSlot?: Snippet;
24
+ /** Return a data-pw value for the given row. */
25
+ getRowTestId?: (row: JSONValue[], rowIndex: number) => string;
26
+ /** Return a data-pw value for the given cell. */
27
+ getCellTestId?: (row: JSONValue[], column: JSONValue, rowIndex: number) => string;
22
28
  };
23
29
  export type TableEventProperties = {
24
30
  onRowClick?: (rowIndex: number, rowData: JSONValue[]) => void;
@@ -44,6 +44,11 @@
44
44
  let canScrollLeft = $state(false);
45
45
  let canScrollRight = $state(false);
46
46
 
47
+ // Sliding indicator state
48
+ let indicatorLeft = $state(0);
49
+ let indicatorWidth = $state(0);
50
+ let indicatorReady = $state(false);
51
+
47
52
  function updateOverflow(): void {
48
53
  if (scrollContainer === null) {
49
54
  return;
@@ -53,6 +58,22 @@
53
58
  canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
54
59
  }
55
60
 
61
+ function updateIndicator(): void {
62
+ if (scrollContainer === null) {
63
+ return;
64
+ }
65
+ const activeEl = scrollContainer.querySelector<HTMLElement>('.tabs-item.active');
66
+ if (activeEl === null) {
67
+ indicatorReady = false;
68
+ indicatorLeft = 0;
69
+ indicatorWidth = 0;
70
+ return;
71
+ }
72
+ indicatorLeft = activeEl.offsetLeft;
73
+ indicatorWidth = activeEl.offsetWidth;
74
+ indicatorReady = true;
75
+ }
76
+
56
77
  function scroll(direction: 'left' | 'right'): void {
57
78
  if (scrollContainer === null) {
58
79
  return;
@@ -73,7 +94,10 @@
73
94
  if (typeof rawItem !== 'object') {
74
95
  return;
75
96
  }
76
- const tabItem = toTabItem(rawItem)!;
97
+ const tabItem = toTabItem(rawItem);
98
+ if (tabItem === null) {
99
+ return;
100
+ }
77
101
  if (tabItem.key === activeKey) {
78
102
  return;
79
103
  }
@@ -101,8 +125,17 @@
101
125
  function initOverflow(node: HTMLDivElement): { destroy: () => void } {
102
126
  scrollContainer = node;
103
127
  updateOverflow();
104
- const observer = new MutationObserver(updateOverflow);
105
- observer.observe(node, { childList: true, subtree: true });
128
+ updateIndicator();
129
+ const observer = new MutationObserver(() => {
130
+ updateOverflow();
131
+ updateIndicator();
132
+ });
133
+ observer.observe(node, {
134
+ childList: true,
135
+ subtree: true,
136
+ attributes: true,
137
+ attributeFilter: ['class']
138
+ });
106
139
  return {
107
140
  destroy() {
108
141
  observer.disconnect();
@@ -153,11 +186,15 @@
153
186
  {:else}
154
187
  {label}
155
188
  {/if}
156
- {#if isActiveItem(index)}
157
- <span class="tabs-indicator"></span>
158
- {/if}
159
189
  </div>
160
190
  {/each}
191
+ {#if indicatorReady}
192
+ <span
193
+ class="tabs-indicator"
194
+ aria-hidden="true"
195
+ style="left: {indicatorLeft}px; width: {indicatorWidth}px;"
196
+ ></span>
197
+ {/if}
161
198
  </div>
162
199
  {#if canScrollRight}
163
200
  <button
@@ -199,6 +236,7 @@
199
236
  gap: var(--tabs-bar-gap, 0px);
200
237
  overflow-x: auto;
201
238
  scrollbar-width: none;
239
+ position: relative;
202
240
  }
203
241
 
204
242
  .tabs-bar::-webkit-scrollbar {
@@ -294,10 +332,16 @@
294
332
  .tabs-indicator {
295
333
  position: absolute;
296
334
  bottom: 0;
297
- left: 0;
298
- right: 0;
299
335
  height: var(--tabs-indicator-height, 2px);
300
336
  background-color: var(--tabs-indicator-color, #1a73e8);
301
337
  border-radius: var(--tabs-indicator-border-radius, 2px 2px 0 0);
338
+ transition: var(--tabs-indicator-transition, left 0.3s ease, width 0.3s ease);
339
+ pointer-events: none;
340
+ }
341
+
342
+ @media (prefers-reduced-motion: reduce) {
343
+ .tabs-indicator {
344
+ transition: none;
345
+ }
302
346
  }
303
347
  </style>
package/dist/index.d.ts CHANGED
@@ -56,6 +56,9 @@ export { default as EmptyState } from './EmptyState/EmptyState.svelte';
56
56
  export { default as Combobox } from './Combobox/Combobox.svelte';
57
57
  export { default as ColorPicker } from './ColorPicker/ColorPicker.svelte';
58
58
  export { default as SplitInput } from './SplitInput/SplitInput.svelte';
59
+ export { default as FileInput } from './FileInput/FileInput.svelte';
60
+ export { default as DateRangePicker } from './DateRangePicker/DateRangePicker.svelte';
61
+ export { default as Breadcrumb } from './Breadcrumb/Breadcrumb.svelte';
59
62
  export type * from './Button/properties';
60
63
  export type * from './Modal/properties';
61
64
  export type * from './Input/properties';
@@ -108,4 +111,7 @@ export type * from './EmptyState/properties';
108
111
  export type * from './Combobox/properties';
109
112
  export type * from './ColorPicker/properties';
110
113
  export type * from './SplitInput/properties';
114
+ export type * from './FileInput/properties';
115
+ export type * from './DateRangePicker/properties';
116
+ export type * from './Breadcrumb/properties';
111
117
  export { validateInput } from './utils';
package/dist/index.js CHANGED
@@ -56,4 +56,7 @@ export { default as EmptyState } from './EmptyState/EmptyState.svelte';
56
56
  export { default as Combobox } from './Combobox/Combobox.svelte';
57
57
  export { default as ColorPicker } from './ColorPicker/ColorPicker.svelte';
58
58
  export { default as SplitInput } from './SplitInput/SplitInput.svelte';
59
+ export { default as FileInput } from './FileInput/FileInput.svelte';
60
+ export { default as DateRangePicker } from './DateRangePicker/DateRangePicker.svelte';
61
+ export { default as Breadcrumb } from './Breadcrumb/Breadcrumb.svelte';
59
62
  export { validateInput } from './utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.28.2",
3
+ "version": "2.29.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",