@juspay/svelte-ui-components 2.124.0 → 2.126.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 (56) hide show
  1. package/dist/Chat/Chat.svelte +158 -0
  2. package/dist/Chat/Chat.svelte.d.ts +4 -0
  3. package/dist/Chat/controller.svelte.d.ts +24 -0
  4. package/dist/Chat/controller.svelte.js +190 -0
  5. package/dist/Chat/properties.d.ts +51 -0
  6. package/dist/Chat/properties.js +1 -0
  7. package/dist/Chat/roles.d.ts +2 -0
  8. package/dist/Chat/roles.js +3 -0
  9. package/dist/Chat/types.d.ts +45 -0
  10. package/dist/Chat/types.js +1 -0
  11. package/dist/ChatBubble/ChatBubble.svelte +419 -0
  12. package/dist/ChatBubble/ChatBubble.svelte.d.ts +4 -0
  13. package/dist/ChatBubble/properties.d.ts +43 -0
  14. package/dist/ChatBubble/properties.js +1 -0
  15. package/dist/ChatComposer/ChatComposer.svelte +289 -0
  16. package/dist/ChatComposer/ChatComposer.svelte.d.ts +4 -0
  17. package/dist/ChatComposer/properties.d.ts +33 -0
  18. package/dist/ChatComposer/properties.js +1 -0
  19. package/dist/ChatHeader/ChatHeader.svelte +169 -0
  20. package/dist/ChatHeader/ChatHeader.svelte.d.ts +4 -0
  21. package/dist/ChatHeader/properties.d.ts +19 -0
  22. package/dist/ChatHeader/properties.js +1 -0
  23. package/dist/ChatMessage/ChatMessage.svelte +330 -0
  24. package/dist/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  25. package/dist/ChatMessage/properties.d.ts +29 -0
  26. package/dist/ChatMessage/properties.js +1 -0
  27. package/dist/ChatMessageList/ChatMessageList.svelte +181 -0
  28. package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +4 -0
  29. package/dist/ChatMessageList/properties.d.ts +22 -0
  30. package/dist/ChatMessageList/properties.js +1 -0
  31. package/dist/ChatSuggestions/ChatSuggestions.svelte +46 -0
  32. package/dist/ChatSuggestions/ChatSuggestions.svelte.d.ts +4 -0
  33. package/dist/ChatSuggestions/properties.d.ts +16 -0
  34. package/dist/ChatSuggestions/properties.js +1 -0
  35. package/dist/ChatToolStatus/ChatToolStatus.svelte +63 -0
  36. package/dist/ChatToolStatus/ChatToolStatus.svelte.d.ts +4 -0
  37. package/dist/ChatToolStatus/properties.d.ts +10 -0
  38. package/dist/ChatToolStatus/properties.js +1 -0
  39. package/dist/LineChart/LineChart.svelte +36 -15
  40. package/dist/Resizable/Resizable.svelte +304 -0
  41. package/dist/Resizable/Resizable.svelte.d.ts +4 -0
  42. package/dist/Resizable/properties.d.ts +27 -0
  43. package/dist/Resizable/properties.js +1 -0
  44. package/dist/Table/Table.svelte +109 -10
  45. package/dist/Table/properties.d.ts +1 -0
  46. package/dist/assets/attach.svg +3 -0
  47. package/dist/assets/chat.svg +3 -0
  48. package/dist/assets/mic.svg +5 -0
  49. package/dist/assets/retry.svg +4 -0
  50. package/dist/assets/send.svg +4 -0
  51. package/dist/assets/stop.svg +3 -0
  52. package/dist/assets/thumb-down.svg +4 -0
  53. package/dist/assets/thumb-up.svg +4 -0
  54. package/dist/index.d.ts +21 -0
  55. package/dist/index.js +11 -0
  56. package/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import Loader from '../Loader/Loader.svelte';
3
+ import type { ChatToolStatusProperties } from './properties';
4
+
5
+ let { label, icon, testId, classes }: ChatToolStatusProperties = $props();
6
+ </script>
7
+
8
+ <div
9
+ class="chat-tool-status {classes ?? ''}"
10
+ aria-live="polite"
11
+ data-pw={typeof testId === 'string' ? testId : null}
12
+ testID={typeof testId === 'string' ? testId : null}
13
+ >
14
+ <span class="indicator">
15
+ {#if typeof icon === 'function'}
16
+ {@render icon()}
17
+ {:else}
18
+ <Loader />
19
+ {/if}
20
+ </span>
21
+ <span class="label">{label}</span>
22
+ </div>
23
+
24
+ <style>
25
+ .chat-tool-status {
26
+ box-sizing: border-box;
27
+ display: inline-flex;
28
+ align-items: center;
29
+ gap: var(--chat-tool-status-gap, 8px);
30
+ width: fit-content;
31
+ padding: var(--chat-tool-status-padding, 8px 14px);
32
+ background: var(--chat-tool-status-background, #ffffff);
33
+ border: var(--chat-tool-status-border, 1px solid #e4e4e7);
34
+ border-radius: var(--chat-tool-status-border-radius, 999px);
35
+ box-shadow: var(--chat-tool-status-box-shadow, 0 6px 20px rgba(0, 0, 0, 0.08));
36
+ color: var(--chat-tool-status-color, #52525b);
37
+ font-size: var(--chat-tool-status-font-size, 0.85rem);
38
+ font-weight: var(--chat-tool-status-font-weight, 500);
39
+ max-width: var(--chat-tool-status-max-width, 100%);
40
+ }
41
+
42
+ .indicator {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ flex-shrink: 0;
46
+ color: var(--chat-tool-status-indicator-color, currentColor);
47
+ --loader-foreground: var(--chat-tool-status-spinner-color, currentColor);
48
+ --loader-foreground-end: var(--chat-tool-status-spinner-color-end, transparent);
49
+ --loader-width: var(--chat-tool-status-spinner-size, 14px);
50
+ --loader-height: var(--chat-tool-status-spinner-size, 14px);
51
+ --loader-before-width: 7px;
52
+ --loader-before-height: 7px;
53
+ --loader-after-width: 11px;
54
+ --loader-after-height: 11px;
55
+ --loader-background: var(--chat-tool-status-background, #ffffff);
56
+ }
57
+
58
+ .label {
59
+ white-space: nowrap;
60
+ overflow: hidden;
61
+ text-overflow: ellipsis;
62
+ }
63
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { ChatToolStatusProperties } from './properties';
2
+ declare const ChatToolStatus: import("svelte").Component<ChatToolStatusProperties, {}, "">;
3
+ type ChatToolStatus = ReturnType<typeof ChatToolStatus>;
4
+ export default ChatToolStatus;
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type ChatToolStatusProperties = OptionalChatToolStatusProperties & MandatoryChatToolStatusProperties;
3
+ export type MandatoryChatToolStatusProperties = {
4
+ label: string;
5
+ };
6
+ export type OptionalChatToolStatusProperties = {
7
+ icon?: Snippet;
8
+ testId?: string;
9
+ classes?: string;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -228,6 +228,10 @@
228
228
  })
229
229
  );
230
230
 
231
+ // Markers paint back-to-front so the first series ends up on top — see the
232
+ // comment on the marker loop in the markup.
233
+ let markerPaintOrder = $derived(lines.map((_line, index) => index).reverse());
234
+
231
235
  let legendItems = $derived<LegendItem[]>(
232
236
  series.map((s, i) => ({
233
237
  label: s.name,
@@ -658,6 +662,38 @@
658
662
  stroke-dasharray={line.dash}
659
663
  fill="none"
660
664
  />
665
+ {#if showValues && pointLabelPlacements[si]}
666
+ {#each line.points as _point, pi (pi)}
667
+ {@const pl = pointLabelPlacements[si][pi]}
668
+ {#if pl?.visible && Number.isFinite(pl.x) && Number.isFinite(pl.y)}
669
+ <text
670
+ class="point-value"
671
+ x={pl.x}
672
+ y={pl.y}
673
+ text-anchor="middle"
674
+ dominant-baseline={pl.dominantBaseline}
675
+ >{formatNumber(series[si].data[pi].y)}</text
676
+ >
677
+ {/if}
678
+ {/each}
679
+ {/if}
680
+ {/if}
681
+ {/each}
682
+
683
+ <!--
684
+ Point markers are painted after every line, and in REVERSE series
685
+ order. Two series that share a value put their markers on identical
686
+ coordinates, and SVG has no z-index — whichever is written last wins.
687
+ Painting forwards meant the last series in the array hid the first,
688
+ so a chart passed [primary, comparison] lost the primary series'
689
+ marker wherever the two periods happened to agree, most visibly at
690
+ the first bucket. Reversing here makes the FIRST series win, which is
691
+ the one a reader is looking at; the legend keeps its original order
692
+ because it is derived from `series`, not from this loop.
693
+ -->
694
+ {#each markerPaintOrder as si (si)}
695
+ {@const line = lines[si]}
696
+ {#if !line.hidden}
661
697
  {#if line.points.length === 1 && !showDots && Number.isFinite(line.points[0].x) && Number.isFinite(line.points[0].y)}
662
698
  <circle
663
699
  class="single-point"
@@ -684,21 +720,6 @@
684
720
  {/if}
685
721
  {/each}
686
722
  {/if}
687
- {#if showValues && pointLabelPlacements[si]}
688
- {#each line.points as _point, pi (pi)}
689
- {@const pl = pointLabelPlacements[si][pi]}
690
- {#if pl?.visible && Number.isFinite(pl.x) && Number.isFinite(pl.y)}
691
- <text
692
- class="point-value"
693
- x={pl.x}
694
- y={pl.y}
695
- text-anchor="middle"
696
- dominant-baseline={pl.dominantBaseline}
697
- >{formatNumber(series[si].data[pi].y)}</text
698
- >
699
- {/if}
700
- {/each}
701
- {/if}
702
723
  {/if}
703
724
  {/each}
704
725
 
@@ -0,0 +1,304 @@
1
+ <script lang="ts">
2
+ import type { ResizableProperties, ResizeEdge } from './properties';
3
+
4
+ let {
5
+ width = $bindable(null),
6
+ height = $bindable(null),
7
+ minWidth = 0,
8
+ maxWidth = Number.POSITIVE_INFINITY,
9
+ minHeight = 0,
10
+ maxHeight = Number.POSITIVE_INFINITY,
11
+ handles = ['bottom-right'],
12
+ step = 16,
13
+ disabled = false,
14
+ handleLabel = 'Resize',
15
+ children,
16
+ onresize,
17
+ onresizestart,
18
+ onresizeend,
19
+ testId,
20
+ classes
21
+ }: ResizableProperties = $props();
22
+
23
+ const DIRS: Record<ResizeEdge, { x: -1 | 0 | 1; y: -1 | 0 | 1 }> = {
24
+ top: { x: 0, y: -1 },
25
+ right: { x: 1, y: 0 },
26
+ bottom: { x: 0, y: 1 },
27
+ left: { x: -1, y: 0 },
28
+ 'top-left': { x: -1, y: -1 },
29
+ 'top-right': { x: 1, y: -1 },
30
+ 'bottom-left': { x: -1, y: 1 },
31
+ 'bottom-right': { x: 1, y: 1 }
32
+ };
33
+
34
+ let container: HTMLElement | null = $state(null);
35
+ let active: {
36
+ edge: ResizeEdge;
37
+ pointerId: number;
38
+ startX: number;
39
+ startY: number;
40
+ startW: number;
41
+ startH: number;
42
+ } | null = $state(null);
43
+
44
+ let activeHandles = $derived(disabled ? [] : handles);
45
+
46
+ function clampWidth(value: number): number {
47
+ return Math.min(maxWidth, Math.max(minWidth, value));
48
+ }
49
+
50
+ function clampHeight(value: number): number {
51
+ return Math.min(maxHeight, Math.max(minHeight, value));
52
+ }
53
+
54
+ function measuredWidth(): number {
55
+ return typeof width === 'number' ? width : (container?.offsetWidth ?? 0);
56
+ }
57
+
58
+ function measuredHeight(): number {
59
+ return typeof height === 'number' ? height : (container?.offsetHeight ?? 0);
60
+ }
61
+
62
+ function axisOf(edge: ResizeEdge): 'x' | 'y' | 'both' {
63
+ const dir = DIRS[edge];
64
+ if (dir.x !== 0 && dir.y !== 0) {
65
+ return 'both';
66
+ }
67
+ return dir.x !== 0 ? 'x' : 'y';
68
+ }
69
+
70
+ function orientationOf(edge: ResizeEdge): 'horizontal' | 'vertical' | null {
71
+ const axis = axisOf(edge);
72
+ if (axis === 'x') {
73
+ return 'vertical';
74
+ }
75
+ if (axis === 'y') {
76
+ return 'horizontal';
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function startResize(
82
+ event: PointerEvent & { currentTarget: HTMLElement },
83
+ edge: ResizeEdge
84
+ ): void {
85
+ if (disabled) {
86
+ return;
87
+ }
88
+ event.preventDefault();
89
+ const startW = clampWidth(measuredWidth());
90
+ const startH = clampHeight(measuredHeight());
91
+ width = startW;
92
+ height = startH;
93
+ active = {
94
+ edge,
95
+ pointerId: event.pointerId,
96
+ startX: event.clientX,
97
+ startY: event.clientY,
98
+ startW,
99
+ startH
100
+ };
101
+ event.currentTarget.setPointerCapture(event.pointerId);
102
+ onresizestart?.({ width: startW, height: startH });
103
+ }
104
+
105
+ function moveResize(event: PointerEvent): void {
106
+ if (active === null) {
107
+ return;
108
+ }
109
+ const dir = DIRS[active.edge];
110
+ if (dir.x !== 0) {
111
+ width = clampWidth(active.startW + dir.x * (event.clientX - active.startX));
112
+ }
113
+ if (dir.y !== 0) {
114
+ height = clampHeight(active.startH + dir.y * (event.clientY - active.startY));
115
+ }
116
+ onresize?.({ width: measuredWidth(), height: measuredHeight() });
117
+ }
118
+
119
+ function endResize(event: PointerEvent & { currentTarget: HTMLElement }): void {
120
+ if (active === null) {
121
+ return;
122
+ }
123
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
124
+ event.currentTarget.releasePointerCapture(event.pointerId);
125
+ }
126
+ active = null;
127
+ onresizeend?.({ width: measuredWidth(), height: measuredHeight() });
128
+ }
129
+
130
+ function keyResize(event: KeyboardEvent, edge: ResizeEdge): void {
131
+ if (disabled) {
132
+ return;
133
+ }
134
+ const dir = DIRS[edge];
135
+ let nextWidth = measuredWidth();
136
+ let nextHeight = measuredHeight();
137
+ let handled = false;
138
+
139
+ if (dir.x !== 0 && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
140
+ nextWidth = clampWidth(nextWidth + (event.key === 'ArrowRight' ? step : -step));
141
+ handled = true;
142
+ }
143
+ if (dir.y !== 0 && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
144
+ nextHeight = clampHeight(nextHeight + (event.key === 'ArrowDown' ? step : -step));
145
+ handled = true;
146
+ }
147
+
148
+ if (handled) {
149
+ event.preventDefault();
150
+ width = nextWidth;
151
+ height = nextHeight;
152
+ onresize?.({ width: nextWidth, height: nextHeight });
153
+ }
154
+ }
155
+
156
+ function valueNow(edge: ResizeEdge): number | null {
157
+ const axis = axisOf(edge);
158
+ if (axis === 'y') {
159
+ return typeof height === 'number' ? height : null;
160
+ }
161
+ return typeof width === 'number' ? width : null;
162
+ }
163
+
164
+ function valueMin(edge: ResizeEdge): number {
165
+ return axisOf(edge) === 'y' ? minHeight : minWidth;
166
+ }
167
+
168
+ function valueMax(edge: ResizeEdge): number | null {
169
+ const max = axisOf(edge) === 'y' ? maxHeight : maxWidth;
170
+ return Number.isFinite(max) ? max : null;
171
+ }
172
+ </script>
173
+
174
+ <div
175
+ class="resizable {classes ?? ''}"
176
+ class:resizing={active !== null}
177
+ data-pw={typeof testId === 'string' ? testId : null}
178
+ testID={typeof testId === 'string' ? testId : null}
179
+ bind:this={container}
180
+ style:width={typeof width === 'number' ? `${width}px` : null}
181
+ style:height={typeof height === 'number' ? `${height}px` : null}
182
+ >
183
+ {#if typeof children === 'function'}
184
+ {@render children()}
185
+ {/if}
186
+
187
+ {#each activeHandles as edge (edge)}
188
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
189
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
190
+ <div
191
+ class="handle"
192
+ data-edge={edge}
193
+ role="separator"
194
+ tabindex="0"
195
+ aria-label={handleLabel}
196
+ aria-orientation={orientationOf(edge)}
197
+ aria-valuenow={valueNow(edge)}
198
+ aria-valuemin={valueMin(edge)}
199
+ aria-valuemax={valueMax(edge)}
200
+ onpointerdown={(event) => startResize(event, edge)}
201
+ onpointermove={moveResize}
202
+ onpointerup={endResize}
203
+ onkeydown={(event) => keyResize(event, edge)}
204
+ ></div>
205
+ {/each}
206
+ </div>
207
+
208
+ <style>
209
+ .resizable {
210
+ position: relative;
211
+ box-sizing: border-box;
212
+ max-width: var(--resizable-max-width, none);
213
+ max-height: var(--resizable-max-height, none);
214
+ transition: var(--resizable-transition, none);
215
+ }
216
+
217
+ .resizable.resizing {
218
+ user-select: none;
219
+ transition: none;
220
+ }
221
+
222
+ @media (prefers-reduced-motion: reduce) {
223
+ .resizable {
224
+ transition: none;
225
+ }
226
+ }
227
+
228
+ .handle {
229
+ position: absolute;
230
+ touch-action: none;
231
+ z-index: var(--resizable-handle-z-index, 2);
232
+ background: var(--resizable-handle-color, transparent);
233
+ }
234
+
235
+ .handle:focus-visible {
236
+ outline: var(--resizable-handle-focus-outline, 2px solid #3b5bdb);
237
+ outline-offset: var(--resizable-handle-focus-outline-offset, -2px);
238
+ }
239
+
240
+ .handle[data-edge='top'],
241
+ .handle[data-edge='bottom'] {
242
+ left: 0;
243
+ right: 0;
244
+ height: var(--resizable-edge-size, 8px);
245
+ cursor: ns-resize;
246
+ }
247
+
248
+ .handle[data-edge='left'],
249
+ .handle[data-edge='right'] {
250
+ top: 0;
251
+ bottom: 0;
252
+ width: var(--resizable-edge-size, 8px);
253
+ cursor: ew-resize;
254
+ }
255
+
256
+ .handle[data-edge='top'] {
257
+ top: 0;
258
+ }
259
+
260
+ .handle[data-edge='bottom'] {
261
+ bottom: 0;
262
+ }
263
+
264
+ .handle[data-edge='left'] {
265
+ left: 0;
266
+ }
267
+
268
+ .handle[data-edge='right'] {
269
+ right: 0;
270
+ }
271
+
272
+ .handle[data-edge='top-left'],
273
+ .handle[data-edge='top-right'],
274
+ .handle[data-edge='bottom-left'],
275
+ .handle[data-edge='bottom-right'] {
276
+ width: var(--resizable-corner-size, 14px);
277
+ height: var(--resizable-corner-size, 14px);
278
+ z-index: var(--resizable-corner-z-index, 3);
279
+ }
280
+
281
+ .handle[data-edge='top-left'] {
282
+ top: 0;
283
+ left: 0;
284
+ cursor: nwse-resize;
285
+ }
286
+
287
+ .handle[data-edge='top-right'] {
288
+ top: 0;
289
+ right: 0;
290
+ cursor: nesw-resize;
291
+ }
292
+
293
+ .handle[data-edge='bottom-left'] {
294
+ bottom: 0;
295
+ left: 0;
296
+ cursor: nesw-resize;
297
+ }
298
+
299
+ .handle[data-edge='bottom-right'] {
300
+ bottom: 0;
301
+ right: 0;
302
+ cursor: nwse-resize;
303
+ }
304
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { ResizableProperties } from './properties';
2
+ declare const Resizable: import("svelte").Component<ResizableProperties, {}, "width" | "height">;
3
+ type Resizable = ReturnType<typeof Resizable>;
4
+ export default Resizable;
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type ResizeEdge = 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
3
+ export type ResizeSize = {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ export type ResizableProperties = OptionalResizableProperties & ResizableEventProperties;
8
+ export type OptionalResizableProperties = {
9
+ width?: number | null;
10
+ height?: number | null;
11
+ minWidth?: number;
12
+ maxWidth?: number;
13
+ minHeight?: number;
14
+ maxHeight?: number;
15
+ handles?: ResizeEdge[];
16
+ step?: number;
17
+ disabled?: boolean;
18
+ handleLabel?: string;
19
+ children?: Snippet;
20
+ testId?: string;
21
+ classes?: string;
22
+ };
23
+ export type ResizableEventProperties = {
24
+ onresize?: (size: ResizeSize) => void;
25
+ onresizestart?: (size: ResizeSize) => void;
26
+ onresizeend?: (size: ResizeSize) => void;
27
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -5,6 +5,7 @@
5
5
  import type { JSONValue } from 'type-decoder';
6
6
  import { SvelteSet } from 'svelte/reactivity';
7
7
  import Button from '../Button/Button.svelte';
8
+ import Input from '../Input/Input.svelte';
8
9
  import Menu from '../Menu/Menu.svelte';
9
10
  import Tooltip from '../Tooltip/Tooltip.svelte';
10
11
  import Pagination from '../Pagination/Pagination.svelte';
@@ -169,23 +170,39 @@
169
170
  let hasSearchConfig = $derived(!!searchConfig);
170
171
  let isServerSearch = $derived(typeof onSearchChange === 'function');
171
172
  let searchInputRef = $state<HTMLInputElement | null>(null);
173
+ let inlineSearchInputRef = $state<{ focus: () => void } | null>(null);
174
+ let isInlineSearch = $derived(searchConfig?.displayMode === 'inline');
175
+ let isInlineSearchExpanded = $state(false);
176
+
177
+ const updateSearch = (term: string): void => {
178
+ searchTerm = term;
179
+ pageOverride = 1;
180
+ pagination?.onPageChange?.(1);
181
+ if (isServerSearch) {
182
+ onSearchChange?.(term);
183
+ }
184
+ };
172
185
 
173
186
  const handleSearchInput = (): void => {
174
187
  if (!searchInputRef) {
175
188
  return;
176
189
  }
177
- searchTerm = searchInputRef.value;
178
- pageOverride = 1;
179
- if (isServerSearch) {
180
- onSearchChange?.(searchTerm);
181
- }
190
+ updateSearch(searchInputRef.value);
182
191
  };
183
192
 
184
193
  const clearSearch = (): void => {
185
- searchTerm = '';
186
- pageOverride = 1;
187
- if (isServerSearch) {
188
- onSearchChange?.('');
194
+ updateSearch('');
195
+ isInlineSearchExpanded = false;
196
+ };
197
+
198
+ const expandInlineSearch = (): void => {
199
+ isInlineSearchExpanded = true;
200
+ queueMicrotask(() => inlineSearchInputRef?.focus());
201
+ };
202
+
203
+ const collapseInlineSearchIfEmpty = (): void => {
204
+ if (searchTerm.trim() === '') {
205
+ clearSearch();
189
206
  }
190
207
  };
191
208
 
@@ -561,7 +578,7 @@
561
578
  </div>
562
579
  {/if}
563
580
 
564
- {#if hasSearchConfig}
581
+ {#if hasSearchConfig && !isInlineSearch}
565
582
  <div class="table-search">
566
583
  <span class="table-search-icon">
567
584
  <!-- eslint-disable svelte/no-at-html-tags -->
@@ -773,6 +790,51 @@
773
790
  </Button>
774
791
  </div>
775
792
  {/if}
793
+ {#if hasSearchConfig && isInlineSearch && colIndex === effectiveHeaders.length - 1}
794
+ <span class="table-header-inline-search">
795
+ {#if isInlineSearchExpanded}
796
+ <Input
797
+ bind:this={inlineSearchInputRef}
798
+ bind:value={searchTerm}
799
+ placeholder={searchConfig?.placeholder ?? 'Search…'}
800
+ testId={searchConfig?.testId ?? ''}
801
+ ariaLabel={searchConfig?.placeholder ?? 'Search'}
802
+ autoComplete="off"
803
+ onInput={(value: string) => updateSearch(value)}
804
+ onBlur={collapseInlineSearchIfEmpty}
805
+ classes="table-inline-search-input"
806
+ />
807
+ <span class="table-inline-search-clear">
808
+ <Button
809
+ variant="ghost"
810
+ iconOnly={true}
811
+ ariaLabel="Close search"
812
+ onclick={clearSearch}
813
+ >
814
+ {#snippet icon()}
815
+ <!-- eslint-disable svelte/no-at-html-tags -->
816
+ {@html closeSvg}
817
+ {/snippet}
818
+ </Button>
819
+ </span>
820
+ {:else}
821
+ <span class="table-inline-search-trigger">
822
+ <Button
823
+ variant="ghost"
824
+ iconOnly={true}
825
+ ariaLabel={searchConfig?.placeholder ?? 'Search'}
826
+ testId={searchConfig?.testId && `${searchConfig.testId}-trigger`}
827
+ onclick={expandInlineSearch}
828
+ >
829
+ {#snippet icon()}
830
+ <!-- eslint-disable svelte/no-at-html-tags -->
831
+ {@html searchSvg}
832
+ {/snippet}
833
+ </Button>
834
+ </span>
835
+ {/if}
836
+ </span>
837
+ {/if}
776
838
  </span>
777
839
  </th>
778
840
  {/each}
@@ -1052,6 +1114,43 @@
1052
1114
  height: var(--table-search-clear-icon-size, 14px);
1053
1115
  }
1054
1116
 
1117
+ .table-header-inline-search {
1118
+ display: inline-flex;
1119
+ align-items: center;
1120
+ gap: var(--table-inline-search-gap, 6px);
1121
+ margin-left: auto;
1122
+ }
1123
+
1124
+ .table-inline-search-trigger {
1125
+ --button-color: transparent;
1126
+ --button-border: none;
1127
+ --button-padding: var(--table-inline-search-trigger-padding, 4px);
1128
+ --button-text-color: var(--table-inline-search-icon-color, #9ca3af);
1129
+ }
1130
+
1131
+ .table-inline-search-trigger :global(svg) {
1132
+ width: var(--table-inline-search-icon-size, 16px);
1133
+ height: var(--table-inline-search-icon-size, 16px);
1134
+ display: block;
1135
+ }
1136
+
1137
+ .table-inline-search-input {
1138
+ --input-margin: 0;
1139
+ width: var(--table-inline-search-input-width, 160px);
1140
+ }
1141
+
1142
+ .table-inline-search-clear {
1143
+ --button-color: transparent;
1144
+ --button-border: none;
1145
+ --button-padding: var(--table-inline-search-clear-padding, 2px);
1146
+ --button-text-color: var(--table-inline-search-clear-color, #6b7280);
1147
+ }
1148
+
1149
+ .table-inline-search-clear :global(svg) {
1150
+ width: var(--table-inline-search-clear-icon-size, 14px);
1151
+ height: var(--table-inline-search-clear-icon-size, 14px);
1152
+ }
1153
+
1055
1154
  /* ── Container ──────────────────────────────────────────────────────────── */
1056
1155
  .table-container {
1057
1156
  border: var(--table-border, 1px solid #e5e7eb);
@@ -361,6 +361,7 @@ export type TableSearchConfig = {
361
361
  placeholder?: string;
362
362
  searchableColumnIndices?: number[];
363
363
  testId?: string;
364
+ displayMode?: 'toolbar' | 'inline';
364
365
  };
365
366
  export type TableProperties = OptionalTableProperties & TableEventProperties;
366
367
  export type OptionalTableProperties = {
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M16 7v9a4 4 0 0 1-8 0V6.5a2.5 2.5 0 0 1 5 0V15.5" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
3
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="9" y="2" width="6" height="12" rx="3" />
3
+ <path d="M5 11a7 7 0 0 0 14 0" />
4
+ <line x1="12" y1="18" x2="12" y2="22" />
5
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
2
+ <polyline points="23 4 23 10 17 10" />
3
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
4
+ </svg>