@beeblock/svelar-datatable 0.1.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 (44) hide show
  1. package/README.md +149 -0
  2. package/dist/SvelarDatatablePlugin.d.ts +13 -0
  3. package/dist/export/ExportManager.d.ts +4 -0
  4. package/dist/export/clipboard.d.ts +2 -0
  5. package/dist/export/csv.d.ts +4 -0
  6. package/dist/export/excel.d.ts +2 -0
  7. package/dist/export/index.d.ts +6 -0
  8. package/dist/export/pdf.d.ts +2 -0
  9. package/dist/export/print.d.ts +2 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.js +19 -0
  12. package/dist/server/DataTableController.d.ts +4 -0
  13. package/dist/server/DataTableRequest.d.ts +3 -0
  14. package/dist/server/DataTableService.d.ts +25 -0
  15. package/dist/server/index.d.ts +3 -0
  16. package/dist/server/index.js +1 -0
  17. package/dist/state/DataTableStore.d.ts +64 -0
  18. package/dist/state/ServerDataTableStore.d.ts +23 -0
  19. package/dist/state/index.d.ts +2 -0
  20. package/dist/types.d.ts +208 -0
  21. package/dist/types.js +0 -0
  22. package/dist/ui/index.d.ts +20 -0
  23. package/package.json +45 -0
  24. package/src/ui/DataTable.svelte +385 -0
  25. package/src/ui/DataTableBody.svelte +180 -0
  26. package/src/ui/DataTableBubbleEditor.svelte +93 -0
  27. package/src/ui/DataTableButtons.svelte +139 -0
  28. package/src/ui/DataTableCell.svelte +381 -0
  29. package/src/ui/DataTableColumnToggle.svelte +111 -0
  30. package/src/ui/DataTableEditor.svelte +27 -0
  31. package/src/ui/DataTableEditorField.svelte +190 -0
  32. package/src/ui/DataTableEditorForm.svelte +94 -0
  33. package/src/ui/DataTableEmpty.svelte +40 -0
  34. package/src/ui/DataTableExpandedRow.svelte +37 -0
  35. package/src/ui/DataTableFooter.svelte +65 -0
  36. package/src/ui/DataTableHead.svelte +169 -0
  37. package/src/ui/DataTableLoading.svelte +44 -0
  38. package/src/ui/DataTableModalEditor.svelte +126 -0
  39. package/src/ui/DataTablePagination.svelte +205 -0
  40. package/src/ui/DataTableRow.svelte +192 -0
  41. package/src/ui/DataTableSearch.svelte +95 -0
  42. package/src/ui/DataTableToolbar.svelte +164 -0
  43. package/src/ui/index.ts +20 -0
  44. package/src/ui/virtual/VirtualScroller.svelte +62 -0
@@ -0,0 +1,126 @@
1
+ <script lang="ts">
2
+ import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
3
+ import DataTableEditorForm from './DataTableEditorForm.svelte';
4
+
5
+ interface Props {
6
+ fields: EditorFieldDef[];
7
+ store: DataTableStore;
8
+ title?: string;
9
+ classNames?: DataTableClassNames;
10
+ onsubmit: (formData: Record<string, any>) => void | Promise<void>;
11
+ }
12
+ let { fields, store, title = 'Edit Record', classNames = {}, onsubmit }: Props = $props();
13
+
14
+ let state = $state(store.getState());
15
+ $effect(() => {
16
+ return store.subscribe(() => {
17
+ state = store.getState();
18
+ });
19
+ });
20
+
21
+ let isOpen = $derived(state.editorMode === 'modal');
22
+ let isCreate = $derived(state.editingRowId === null);
23
+
24
+ function close() {
25
+ store.closeEditor();
26
+ }
27
+
28
+ function handleBackdrop(e: MouseEvent) {
29
+ if ((e.target as HTMLElement).classList.contains('sdt-modal-backdrop')) {
30
+ close();
31
+ }
32
+ }
33
+
34
+ function handleKeydown(e: KeyboardEvent) {
35
+ if (e.key === 'Escape') close();
36
+ }
37
+ </script>
38
+
39
+ <svelte:window onkeydown={handleKeydown} />
40
+
41
+ {#if isOpen}
42
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
43
+ <div class="sdt-modal-backdrop {classNames.editorBackdrop ?? ''}" onclick={handleBackdrop}>
44
+ <div class="sdt-modal {classNames.editorModal ?? ''}" role="dialog" aria-modal="true">
45
+ <div class="sdt-modal-header">
46
+ <h3 class="sdt-modal-title">{isCreate ? 'Create Record' : title}</h3>
47
+ <button type="button" class="sdt-modal-close" onclick={close} aria-label="Close">
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
49
+ <path d="M18 6 6 18M6 6l12 12" />
50
+ </svg>
51
+ </button>
52
+ </div>
53
+ <div class="sdt-modal-body">
54
+ <DataTableEditorForm
55
+ {fields}
56
+ {store}
57
+ {onsubmit}
58
+ oncancel={close}
59
+ submitLabel={isCreate ? 'Create' : 'Save'}
60
+ />
61
+ </div>
62
+ </div>
63
+ </div>
64
+ {/if}
65
+
66
+ <style>
67
+ .sdt-modal-backdrop {
68
+ position: fixed;
69
+ inset: 0;
70
+ background: rgba(0, 0, 0, 0.5);
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ z-index: 50;
75
+ animation: sdt-fade-in 0.15s ease;
76
+ }
77
+ .sdt-modal {
78
+ background: var(--sdt-bg, #fff);
79
+ border-radius: 0.5rem;
80
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
81
+ width: 90%;
82
+ max-width: 480px;
83
+ max-height: 85vh;
84
+ display: flex;
85
+ flex-direction: column;
86
+ animation: sdt-slide-up 0.2s ease;
87
+ }
88
+ .sdt-modal-header {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: space-between;
92
+ padding: 1rem 1.25rem;
93
+ border-bottom: 1px solid var(--sdt-border, #e5e7eb);
94
+ }
95
+ .sdt-modal-title {
96
+ margin: 0;
97
+ font-size: 1rem;
98
+ font-weight: 600;
99
+ color: var(--sdt-text, #111827);
100
+ }
101
+ .sdt-modal-close {
102
+ background: none;
103
+ border: none;
104
+ cursor: pointer;
105
+ color: var(--sdt-text-muted, #6b7280);
106
+ padding: 0.25rem;
107
+ border-radius: 0.25rem;
108
+ display: flex;
109
+ }
110
+ .sdt-modal-close:hover {
111
+ background: var(--sdt-hover, #f3f4f6);
112
+ color: var(--sdt-text, #111827);
113
+ }
114
+ .sdt-modal-body {
115
+ padding: 1.25rem;
116
+ overflow-y: auto;
117
+ }
118
+ @keyframes sdt-fade-in {
119
+ from { opacity: 0; }
120
+ to { opacity: 1; }
121
+ }
122
+ @keyframes sdt-slide-up {
123
+ from { transform: translateY(10px); opacity: 0; }
124
+ to { transform: translateY(0); opacity: 1; }
125
+ }
126
+ </style>
@@ -0,0 +1,205 @@
1
+ <script lang="ts">
2
+ import type { DataTableStore, DataTableClassNames } from '../index.js';
3
+
4
+ interface Props {
5
+ store: DataTableStore;
6
+ perPageOptions?: number[];
7
+ classNames?: DataTableClassNames;
8
+ }
9
+ let { store, perPageOptions = [10, 15, 25, 50, 100], classNames = {} }: Props = $props();
10
+
11
+ let state = $state(store.getState());
12
+ $effect(() => {
13
+ return store.subscribe(() => {
14
+ state = store.getState();
15
+ });
16
+ });
17
+
18
+ let pagination = $derived(state.pagination);
19
+ let from = $derived(pagination.total === 0 ? 0 : (pagination.page - 1) * pagination.perPage + 1);
20
+ let to = $derived(Math.min(pagination.page * pagination.perPage, pagination.total));
21
+
22
+ // Generate page numbers with ellipsis
23
+ let pages = $derived(() => {
24
+ const total = pagination.lastPage;
25
+ const current = pagination.page;
26
+ const result: (number | '...')[] = [];
27
+ if (total <= 7) {
28
+ for (let i = 1; i <= total; i++) result.push(i);
29
+ } else {
30
+ result.push(1);
31
+ if (current > 3) result.push('...');
32
+ const start = Math.max(2, current - 1);
33
+ const end = Math.min(total - 1, current + 1);
34
+ for (let i = start; i <= end; i++) result.push(i);
35
+ if (current < total - 2) result.push('...');
36
+ result.push(total);
37
+ }
38
+ return result;
39
+ });
40
+
41
+ function setPage(p: number) {
42
+ store.setPage(p);
43
+ }
44
+
45
+ function handlePerPage(e: Event) {
46
+ store.setPerPage(Number((e.target as HTMLSelectElement).value));
47
+ }
48
+ </script>
49
+
50
+ <div class="sdt-pagination {classNames.pagination ?? ''}">
51
+ <div class="sdt-pagination-info {classNames.paginationInfo ?? ''}">
52
+ <span class="sdt-pagination-showing">
53
+ Showing {from} to {to} of {pagination.total}
54
+ </span>
55
+ <label class="sdt-per-page">
56
+ <select value={pagination.perPage} onchange={handlePerPage} class="sdt-per-page-select {classNames.perPageSelect ?? ''}">
57
+ {#each perPageOptions as opt}
58
+ <option value={opt}>{opt}</option>
59
+ {/each}
60
+ </select>
61
+ per page
62
+ </label>
63
+ </div>
64
+
65
+ <div class="sdt-pagination-controls {classNames.paginationControls ?? ''}">
66
+ <button
67
+ type="button"
68
+ class="sdt-page-btn {classNames.pageButton ?? ''}"
69
+ disabled={pagination.page <= 1}
70
+ onclick={() => setPage(1)}
71
+ aria-label="First page"
72
+ >
73
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
74
+ <path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
75
+ </svg>
76
+ </button>
77
+ <button
78
+ type="button"
79
+ class="sdt-page-btn {classNames.pageButton ?? ''}"
80
+ disabled={pagination.page <= 1}
81
+ onclick={() => setPage(pagination.page - 1)}
82
+ aria-label="Previous page"
83
+ >
84
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
85
+ <path d="m15 18-6-6 6-6" />
86
+ </svg>
87
+ </button>
88
+
89
+ {#each pages() as p}
90
+ {#if p === '...'}
91
+ <span class="sdt-page-ellipsis">...</span>
92
+ {:else}
93
+ <button
94
+ type="button"
95
+ class="sdt-page-btn {classNames.pageButton ?? ''}"
96
+ class:sdt-page-active={p === pagination.page}
97
+ onclick={() => setPage(p)}
98
+ >
99
+ {p}
100
+ </button>
101
+ {/if}
102
+ {/each}
103
+
104
+ <button
105
+ type="button"
106
+ class="sdt-page-btn {classNames.pageButton ?? ''}"
107
+ disabled={pagination.page >= pagination.lastPage}
108
+ onclick={() => setPage(pagination.page + 1)}
109
+ aria-label="Next page"
110
+ >
111
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
112
+ <path d="m9 18 6-6-6-6" />
113
+ </svg>
114
+ </button>
115
+ <button
116
+ type="button"
117
+ class="sdt-page-btn {classNames.pageButton ?? ''}"
118
+ disabled={pagination.page >= pagination.lastPage}
119
+ onclick={() => setPage(pagination.lastPage)}
120
+ aria-label="Last page"
121
+ >
122
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
123
+ <path d="m6 17 5-5-5-5M13 17l5-5-5-5" />
124
+ </svg>
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ <style>
130
+ .sdt-pagination {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: space-between;
134
+ padding: 0.75rem 1rem;
135
+ border-top: 1px solid var(--sdt-border, #e5e7eb);
136
+ font-size: 0.8125rem;
137
+ flex-wrap: wrap;
138
+ gap: 0.5rem;
139
+ }
140
+ .sdt-pagination-info {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 1rem;
144
+ color: var(--sdt-text-muted, #6b7280);
145
+ }
146
+ .sdt-per-page {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.375rem;
150
+ color: var(--sdt-text-muted, #6b7280);
151
+ cursor: pointer;
152
+ }
153
+ .sdt-per-page-select {
154
+ padding: 0.25rem 0.375rem;
155
+ border: 1px solid var(--sdt-border, #e5e7eb);
156
+ border-radius: 0.25rem;
157
+ background: var(--sdt-input-bg, #fff);
158
+ color: var(--sdt-text, #111827);
159
+ font-size: 0.8125rem;
160
+ cursor: pointer;
161
+ }
162
+ .sdt-pagination-controls {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 0.125rem;
166
+ }
167
+ .sdt-page-btn {
168
+ display: inline-flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ min-width: 2rem;
172
+ height: 2rem;
173
+ padding: 0 0.375rem;
174
+ border: 1px solid var(--sdt-border, #e5e7eb);
175
+ border-radius: 0.25rem;
176
+ background: var(--sdt-input-bg, #fff);
177
+ color: var(--sdt-text, #111827);
178
+ cursor: pointer;
179
+ font-size: 0.8125rem;
180
+ transition: background-color 0.15s, border-color 0.15s;
181
+ }
182
+ .sdt-page-btn:hover:not(:disabled) {
183
+ background-color: var(--sdt-hover, #f3f4f6);
184
+ }
185
+ .sdt-page-btn:disabled {
186
+ opacity: 0.4;
187
+ cursor: not-allowed;
188
+ }
189
+ .sdt-page-active {
190
+ background-color: var(--sdt-primary, #3b82f6);
191
+ border-color: var(--sdt-primary, #3b82f6);
192
+ color: #fff;
193
+ }
194
+ .sdt-page-active:hover:not(:disabled) {
195
+ background-color: var(--sdt-primary, #3b82f6);
196
+ }
197
+ .sdt-page-ellipsis {
198
+ display: inline-flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ min-width: 2rem;
202
+ height: 2rem;
203
+ color: var(--sdt-text-muted, #6b7280);
204
+ }
205
+ </style>
@@ -0,0 +1,192 @@
1
+ <script lang="ts">
2
+ import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.js';
3
+ import type { Snippet } from 'svelte';
4
+ import DataTableCell from './DataTableCell.svelte';
5
+ import DataTableExpandedRow from './DataTableExpandedRow.svelte';
6
+
7
+ interface Props {
8
+ row: any;
9
+ index: number;
10
+ columns: ColumnDef[];
11
+ store: DataTableStore;
12
+ selectable?: SelectionMode;
13
+ expandable?: boolean;
14
+ editorMode?: EditorMode;
15
+ rowClass?: string | ((row: any, index: number) => string);
16
+ onRowClick?: (row: any, event: MouseEvent) => void;
17
+ customCell?: Snippet<[{ row: any; column: ColumnDef; value: any }]>;
18
+ expandContent?: Snippet<[{ row: any }]>;
19
+ onExcelNavigate?: (direction: 'up' | 'down' | 'left' | 'right') => void;
20
+ onExcelCommit?: () => void;
21
+ onInlineCommit?: (row: any, columnKey: string, newValue: any, oldValue: any) => void;
22
+ onBubbleAnchor?: (el: HTMLElement) => void;
23
+ classNames?: DataTableClassNames;
24
+ }
25
+ let {
26
+ row,
27
+ index,
28
+ columns,
29
+ store,
30
+ selectable = 'none',
31
+ expandable = false,
32
+ editorMode,
33
+ rowClass,
34
+ onRowClick,
35
+ customCell,
36
+ expandContent,
37
+ onExcelNavigate,
38
+ onExcelCommit,
39
+ onInlineCommit,
40
+ onBubbleAnchor,
41
+ classNames = {},
42
+ }: Props = $props();
43
+
44
+ let state = $state(store.getState());
45
+ $effect(() => {
46
+ return store.subscribe(() => {
47
+ state = store.getState();
48
+ });
49
+ });
50
+
51
+ let rowId = $derived(store.getRowId(row));
52
+ let isSelected = $derived(state.selectedIds.has(rowId));
53
+ let expanded = $state(false);
54
+
55
+ let visibleColumns = $derived(
56
+ state.columnOrder
57
+ .filter((key) => state.columnVisibility[key] !== false)
58
+ .map((key) => columns.find((c) => c.key === key))
59
+ .filter(Boolean) as ColumnDef[]
60
+ );
61
+
62
+ let computedClass = $derived(() => {
63
+ if (!rowClass) return '';
64
+ if (typeof rowClass === 'function') return rowClass(row, index);
65
+ return rowClass;
66
+ });
67
+
68
+ let totalCols = $derived(
69
+ visibleColumns.length + (selectable !== 'none' ? 1 : 0) + (expandable ? 1 : 0)
70
+ );
71
+
72
+ function handleClick(e: MouseEvent) {
73
+ if (selectable === 'multi') {
74
+ const lastId = store.getLastSelectedId();
75
+ if (e.shiftKey && lastId !== null) {
76
+ store.selectRange(lastId, rowId);
77
+ } else {
78
+ store.toggleSelect(rowId);
79
+ }
80
+ } else if (selectable === 'single') {
81
+ if (isSelected) {
82
+ store.deselectAll();
83
+ } else {
84
+ store.selectSingle(rowId);
85
+ }
86
+ }
87
+ onRowClick?.(row, e);
88
+ }
89
+
90
+ function toggleExpand(e: Event) {
91
+ e.stopPropagation();
92
+ expanded = !expanded;
93
+ }
94
+
95
+ let trEl: HTMLTableRowElement | undefined = $state();
96
+
97
+ // Report this row element as the bubble anchor when it becomes the editing row
98
+ $effect(() => {
99
+ if (trEl && state.editingRowId === rowId && state.editorMode === 'bubble') {
100
+ onBubbleAnchor?.(trEl);
101
+ }
102
+ });
103
+ </script>
104
+
105
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
106
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
107
+ <tr
108
+ bind:this={trEl}
109
+ class="sdt-row {computedClass()} {classNames.tr ?? ''} {isSelected ? classNames.trSelected ?? '' : ''} {index % 2 === 1 ? classNames.trEven ?? '' : ''}"
110
+ class:sdt-row-selected={isSelected}
111
+ class:sdt-row-even={index % 2 === 1}
112
+ onclick={handleClick}
113
+ >
114
+ {#if selectable === 'multi'}
115
+ <td class="sdt-cell sdt-cell-checkbox {classNames.td ?? ''}">
116
+ <input type="checkbox" checked={isSelected} onchange={() => store.toggleSelect(rowId)} />
117
+ </td>
118
+ {:else if selectable === 'single'}
119
+ <td class="sdt-cell sdt-cell-checkbox {classNames.td ?? ''}">
120
+ <input type="radio" checked={isSelected} name="sdt-select" />
121
+ </td>
122
+ {/if}
123
+
124
+ {#if expandable}
125
+ <td class="sdt-cell sdt-cell-expand {classNames.td ?? ''}">
126
+ <button type="button" class="sdt-expand-btn" onclick={toggleExpand} aria-label="Expand row">
127
+ <svg
128
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"
129
+ class="sdt-expand-icon"
130
+ class:sdt-expanded={expanded}
131
+ >
132
+ <path d="m9 18 6-6-6-6" />
133
+ </svg>
134
+ </button>
135
+ </td>
136
+ {/if}
137
+
138
+ {#each visibleColumns as col}
139
+ <DataTableCell {row} rowIndex={index} column={col} {store} editorMode={editorMode ?? null} {customCell} {onExcelNavigate} {onExcelCommit} {onInlineCommit} tdClass={classNames.td} />
140
+ {/each}
141
+ </tr>
142
+
143
+ {#if expandable && expanded}
144
+ <DataTableExpandedRow {row} colSpan={totalCols} {expandContent} />
145
+ {/if}
146
+
147
+ <style>
148
+ .sdt-row {
149
+ transition: background-color 0.1s;
150
+ }
151
+ .sdt-row:hover {
152
+ background-color: var(--sdt-row-hover, #f9fafb);
153
+ }
154
+ .sdt-row-selected {
155
+ background-color: var(--sdt-row-selected, rgba(59, 130, 246, 0.08)) !important;
156
+ }
157
+ .sdt-row-even {
158
+ background-color: var(--sdt-row-stripe, transparent);
159
+ }
160
+ .sdt-cell-checkbox {
161
+ text-align: center;
162
+ width: 40px;
163
+ padding: var(--sdt-cell-padding, 0.625rem 0.75rem);
164
+ border-bottom: 1px solid var(--sdt-border, #e5e7eb);
165
+ }
166
+ .sdt-cell-checkbox input {
167
+ accent-color: var(--sdt-primary, #3b82f6);
168
+ }
169
+ .sdt-cell-expand {
170
+ width: 32px;
171
+ padding: var(--sdt-cell-padding, 0.625rem 0.5rem);
172
+ border-bottom: 1px solid var(--sdt-border, #e5e7eb);
173
+ }
174
+ .sdt-expand-btn {
175
+ background: none;
176
+ border: none;
177
+ cursor: pointer;
178
+ padding: 0.125rem;
179
+ display: flex;
180
+ color: var(--sdt-text-muted, #6b7280);
181
+ border-radius: 0.25rem;
182
+ }
183
+ .sdt-expand-btn:hover {
184
+ background: var(--sdt-hover, #f3f4f6);
185
+ }
186
+ .sdt-expand-icon {
187
+ transition: transform 0.15s;
188
+ }
189
+ .sdt-expanded {
190
+ transform: rotate(90deg);
191
+ }
192
+ </style>
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ import type { DataTableStore } from '../index.js';
3
+
4
+ interface Props {
5
+ store: DataTableStore;
6
+ debounceMs?: number;
7
+ placeholder?: string;
8
+ searchInputClass?: string;
9
+ }
10
+ let { store, debounceMs = 300, placeholder = 'Search...', searchInputClass = '' }: Props = $props();
11
+
12
+ let inputValue = $state(store.getState().globalSearch);
13
+ let timer: ReturnType<typeof setTimeout> | null = null;
14
+
15
+ function handleInput(e: Event) {
16
+ const val = (e.target as HTMLInputElement).value;
17
+ inputValue = val;
18
+ if (timer) clearTimeout(timer);
19
+ timer = setTimeout(() => {
20
+ store.setGlobalSearch(val);
21
+ }, debounceMs);
22
+ }
23
+
24
+ function handleClear() {
25
+ inputValue = '';
26
+ store.setGlobalSearch('');
27
+ }
28
+ </script>
29
+
30
+ <div class="sdt-search">
31
+ <svg class="sdt-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
32
+ <circle cx="11" cy="11" r="8" />
33
+ <path d="m21 21-4.3-4.3" />
34
+ </svg>
35
+ <input
36
+ type="text"
37
+ class="sdt-search-input {searchInputClass}"
38
+ value={inputValue}
39
+ oninput={handleInput}
40
+ {placeholder}
41
+ />
42
+ {#if inputValue}
43
+ <button type="button" class="sdt-search-clear" onclick={handleClear} aria-label="Clear search">
44
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
45
+ <path d="M18 6 6 18M6 6l12 12" />
46
+ </svg>
47
+ </button>
48
+ {/if}
49
+ </div>
50
+
51
+ <style>
52
+ .sdt-search {
53
+ position: relative;
54
+ display: flex;
55
+ align-items: center;
56
+ }
57
+ .sdt-search-icon {
58
+ position: absolute;
59
+ left: 0.625rem;
60
+ width: 1rem;
61
+ height: 1rem;
62
+ color: var(--sdt-text-muted, #6b7280);
63
+ pointer-events: none;
64
+ }
65
+ .sdt-search-input {
66
+ padding: 0.5rem 2rem 0.5rem 2.25rem;
67
+ border: 1px solid var(--sdt-border, #e5e7eb);
68
+ border-radius: 0.375rem;
69
+ font-size: 0.875rem;
70
+ background: var(--sdt-input-bg, #fff);
71
+ color: var(--sdt-text, #111827);
72
+ outline: none;
73
+ width: 100%;
74
+ max-width: 280px;
75
+ transition: border-color 0.15s;
76
+ }
77
+ .sdt-search-input:focus {
78
+ border-color: var(--sdt-primary, #3b82f6);
79
+ box-shadow: 0 0 0 2px var(--sdt-primary-ring, rgba(59, 130, 246, 0.2));
80
+ }
81
+ .sdt-search-clear {
82
+ position: absolute;
83
+ right: 0.5rem;
84
+ background: none;
85
+ border: none;
86
+ cursor: pointer;
87
+ color: var(--sdt-text-muted, #6b7280);
88
+ padding: 2px;
89
+ display: flex;
90
+ align-items: center;
91
+ }
92
+ .sdt-search-clear:hover {
93
+ color: var(--sdt-text, #111827);
94
+ }
95
+ </style>