@sondor-technology/sondor-components 1.0.0-1165-initial-setup.11 → 1.0.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 (168) hide show
  1. package/package.json +2 -3
  2. package/src/components/alert/alert.ts +100 -100
  3. package/src/components/badge/badge.ts +96 -96
  4. package/src/components/breadcrumb/breadcrumb.ts +98 -98
  5. package/src/components/button/button.ts +281 -281
  6. package/src/components/button-group/button-group.ts +67 -67
  7. package/src/components/card/card.ts +52 -52
  8. package/src/components/checkbox/checkbox.ts +238 -238
  9. package/src/components/code-block/code-block.ts +256 -256
  10. package/src/components/content-card/content-card.ts +133 -133
  11. package/src/components/divider/divider.ts +68 -68
  12. package/src/components/drawer/drawer.ts +217 -217
  13. package/src/components/field/field.ts +30 -30
  14. package/src/components/footer/footer.ts +58 -58
  15. package/src/components/form/form-step.ts +32 -32
  16. package/src/components/form/form.ts +376 -376
  17. package/src/components/grid/grid-row.ts +75 -75
  18. package/src/components/grid/grid.ts +36 -36
  19. package/src/components/icon/icon-paths.ts +124 -124
  20. package/src/components/icon/icon.ts +82 -82
  21. package/src/components/input/input.ts +248 -248
  22. package/src/components/link/link.ts +97 -97
  23. package/src/components/metadata/metadata.ts +66 -66
  24. package/src/components/modal/modal.ts +158 -158
  25. package/src/components/page-layout/page-layout.ts +704 -704
  26. package/src/components/radio/radio.ts +142 -142
  27. package/src/components/radio-group/radio-group.ts +157 -157
  28. package/src/components/router/router.ts +46 -46
  29. package/src/components/select/select.ts +161 -161
  30. package/src/components/skeleton/skeleton.ts +67 -67
  31. package/src/components/switch/switch.ts +181 -181
  32. package/src/components/table/data-table.ts +556 -556
  33. package/src/components/table/table.ts +153 -153
  34. package/src/components/tabs/tab-panel.ts +30 -30
  35. package/src/components/tabs/tab.ts +76 -76
  36. package/src/components/tabs/tabs.ts +88 -88
  37. package/src/components/textarea/textarea.ts +215 -215
  38. package/src/components/theme-toggle/theme-toggle.ts +131 -131
  39. package/src/components/vertical-nav/vertical-nav-group.ts +96 -96
  40. package/src/components/vertical-nav/vertical-nav-item.ts +49 -49
  41. package/src/styles/globals.css +58 -58
  42. package/src/styles/index.css +80 -80
  43. package/src/styles/reset.ts +50 -50
  44. package/src/styles/theme.css +58 -58
  45. package/src/styles/typography.css +25 -25
  46. package/tsconfig.json +15 -15
  47. package/README.md +0 -8
  48. package/dist/components/alert/alert.d.ts +0 -13
  49. package/dist/components/alert/alert.d.ts.map +0 -1
  50. package/dist/components/alert/alert.js +0 -105
  51. package/dist/components/badge/badge.d.ts +0 -13
  52. package/dist/components/badge/badge.d.ts.map +0 -1
  53. package/dist/components/badge/badge.js +0 -101
  54. package/dist/components/breadcrumb/breadcrumb.d.ts +0 -19
  55. package/dist/components/breadcrumb/breadcrumb.d.ts.map +0 -1
  56. package/dist/components/breadcrumb/breadcrumb.js +0 -99
  57. package/dist/components/button/button.d.ts +0 -23
  58. package/dist/components/button/button.d.ts.map +0 -1
  59. package/dist/components/button/button.js +0 -304
  60. package/dist/components/button-group/button-group.d.ts +0 -6
  61. package/dist/components/button-group/button-group.d.ts.map +0 -1
  62. package/dist/components/button-group/button-group.js +0 -74
  63. package/dist/components/card/card.d.ts +0 -11
  64. package/dist/components/card/card.d.ts.map +0 -1
  65. package/dist/components/card/card.js +0 -54
  66. package/dist/components/checkbox/checkbox.d.ts +0 -28
  67. package/dist/components/checkbox/checkbox.d.ts.map +0 -1
  68. package/dist/components/checkbox/checkbox.js +0 -247
  69. package/dist/components/code-block/code-block.d.ts +0 -20
  70. package/dist/components/code-block/code-block.d.ts.map +0 -1
  71. package/dist/components/code-block/code-block.js +0 -260
  72. package/dist/components/content-card/content-card.d.ts +0 -13
  73. package/dist/components/content-card/content-card.d.ts.map +0 -1
  74. package/dist/components/content-card/content-card.js +0 -140
  75. package/dist/components/divider/divider.d.ts +0 -16
  76. package/dist/components/divider/divider.d.ts.map +0 -1
  77. package/dist/components/divider/divider.js +0 -82
  78. package/dist/components/drawer/drawer.d.ts +0 -22
  79. package/dist/components/drawer/drawer.d.ts.map +0 -1
  80. package/dist/components/drawer/drawer.js +0 -226
  81. package/dist/components/field/field.d.ts +0 -11
  82. package/dist/components/field/field.d.ts.map +0 -1
  83. package/dist/components/field/field.js +0 -32
  84. package/dist/components/footer/footer.d.ts +0 -6
  85. package/dist/components/footer/footer.d.ts.map +0 -1
  86. package/dist/components/footer/footer.js +0 -65
  87. package/dist/components/form/form-step.d.ts +0 -13
  88. package/dist/components/form/form-step.d.ts.map +0 -1
  89. package/dist/components/form/form-step.js +0 -41
  90. package/dist/components/form/form.d.ts +0 -25
  91. package/dist/components/form/form.d.ts.map +0 -1
  92. package/dist/components/form/form.js +0 -358
  93. package/dist/components/grid/grid-row.d.ts +0 -17
  94. package/dist/components/grid/grid-row.d.ts.map +0 -1
  95. package/dist/components/grid/grid-row.js +0 -92
  96. package/dist/components/grid/grid.d.ts +0 -13
  97. package/dist/components/grid/grid.d.ts.map +0 -1
  98. package/dist/components/grid/grid.js +0 -42
  99. package/dist/components/icon/icon-paths.d.ts +0 -116
  100. package/dist/components/icon/icon-paths.d.ts.map +0 -1
  101. package/dist/components/icon/icon-paths.js +0 -121
  102. package/dist/components/icon/icon.d.ts +0 -16
  103. package/dist/components/icon/icon.d.ts.map +0 -1
  104. package/dist/components/icon/icon.js +0 -95
  105. package/dist/components/input/input.d.ts +0 -31
  106. package/dist/components/input/input.d.ts.map +0 -1
  107. package/dist/components/input/input.js +0 -268
  108. package/dist/components/link/link.d.ts +0 -16
  109. package/dist/components/link/link.d.ts.map +0 -1
  110. package/dist/components/link/link.js +0 -116
  111. package/dist/components/metadata/metadata.d.ts +0 -18
  112. package/dist/components/metadata/metadata.d.ts.map +0 -1
  113. package/dist/components/metadata/metadata.js +0 -71
  114. package/dist/components/modal/modal.d.ts +0 -24
  115. package/dist/components/modal/modal.d.ts.map +0 -1
  116. package/dist/components/modal/modal.js +0 -163
  117. package/dist/components/page-layout/page-layout.d.ts +0 -46
  118. package/dist/components/page-layout/page-layout.d.ts.map +0 -1
  119. package/dist/components/page-layout/page-layout.js +0 -697
  120. package/dist/components/radio/radio.d.ts +0 -16
  121. package/dist/components/radio/radio.d.ts.map +0 -1
  122. package/dist/components/radio/radio.js +0 -152
  123. package/dist/components/radio-group/radio-group.d.ts +0 -27
  124. package/dist/components/radio-group/radio-group.d.ts.map +0 -1
  125. package/dist/components/radio-group/radio-group.js +0 -161
  126. package/dist/components/router/router.d.ts +0 -7
  127. package/dist/components/router/router.d.ts.map +0 -1
  128. package/dist/components/router/router.js +0 -37
  129. package/dist/components/select/select.d.ts +0 -16
  130. package/dist/components/select/select.d.ts.map +0 -1
  131. package/dist/components/select/select.js +0 -172
  132. package/dist/components/skeleton/skeleton.d.ts +0 -17
  133. package/dist/components/skeleton/skeleton.d.ts.map +0 -1
  134. package/dist/components/skeleton/skeleton.js +0 -78
  135. package/dist/components/switch/switch.d.ts +0 -25
  136. package/dist/components/switch/switch.d.ts.map +0 -1
  137. package/dist/components/switch/switch.js +0 -185
  138. package/dist/components/table/data-table.d.ts +0 -72
  139. package/dist/components/table/data-table.d.ts.map +0 -1
  140. package/dist/components/table/data-table.js +0 -551
  141. package/dist/components/table/table.d.ts +0 -22
  142. package/dist/components/table/table.d.ts.map +0 -1
  143. package/dist/components/table/table.js +0 -167
  144. package/dist/components/tabs/tab-panel.d.ts +0 -8
  145. package/dist/components/tabs/tab-panel.d.ts.map +0 -1
  146. package/dist/components/tabs/tab-panel.js +0 -46
  147. package/dist/components/tabs/tab.d.ts +0 -9
  148. package/dist/components/tabs/tab.d.ts.map +0 -1
  149. package/dist/components/tabs/tab.js +0 -91
  150. package/dist/components/tabs/tabs.d.ts +0 -14
  151. package/dist/components/tabs/tabs.d.ts.map +0 -1
  152. package/dist/components/tabs/tabs.js +0 -85
  153. package/dist/components/textarea/textarea.d.ts +0 -36
  154. package/dist/components/textarea/textarea.d.ts.map +0 -1
  155. package/dist/components/textarea/textarea.js +0 -231
  156. package/dist/components/theme-toggle/theme-toggle.d.ts +0 -15
  157. package/dist/components/theme-toggle/theme-toggle.d.ts.map +0 -1
  158. package/dist/components/theme-toggle/theme-toggle.js +0 -130
  159. package/dist/components/vertical-nav/vertical-nav-group.d.ts +0 -9
  160. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +0 -1
  161. package/dist/components/vertical-nav/vertical-nav-group.js +0 -111
  162. package/dist/components/vertical-nav/vertical-nav-item.d.ts +0 -8
  163. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +0 -1
  164. package/dist/components/vertical-nav/vertical-nav-item.js +0 -65
  165. package/dist/styles/index.css +0 -171
  166. package/dist/styles/reset.d.ts +0 -2
  167. package/dist/styles/reset.d.ts.map +0 -1
  168. package/dist/styles/reset.js +0 -50
@@ -1,557 +1,557 @@
1
- import { LitElement, html, css, PropertyValues } from 'lit';
2
- import { customElement, property, state } from 'lit/decorators.js';
3
- import { classMap } from 'lit/directives/class-map.js';
4
- import { resetStyles } from '../../styles/reset.js';
5
-
6
- export interface DataTableColumn {
7
- header: string;
8
- key: string;
9
- sortable?: boolean;
10
- hidden?: boolean;
11
- }
12
-
13
- export interface SortConfig {
14
- key: string;
15
- direction: 'asc' | 'desc';
16
- }
17
-
18
- @customElement('st-data-table')
19
- export class StDataTable extends LitElement {
20
- /**
21
- * Array defining table headers, keys, and sortability.
22
- */
23
- @property({ type: Array }) columns: DataTableColumn[] = [];
24
-
25
- /**
26
- * Array of row data objects rendered in the table.
27
- */
28
- @property({ type: Array }) data: Record<string, any>[] = [];
29
-
30
- /**
31
- * Total number of rows available on the server (used for pagination).
32
- */
33
- @property({ type: Number, attribute: 'total-rows' }) totalRows = 0;
34
-
35
- /**
36
- * Current page number (1-indexed).
37
- */
38
- @property({ type: Number }) page = 1;
39
-
40
- /**
41
- * Number of rows per page.
42
- */
43
- @property({ type: Number, attribute: 'page-size' }) pageSize = 10;
44
-
45
- /**
46
- * Enables holding 'Shift' to sort by multiple columns sequentially.
47
- */
48
- @property({ type: Boolean, attribute: 'multi-sort' }) multiSort = false;
49
-
50
- /**
51
- * Displays loading skeleton rows and locks controls.
52
- */
53
- @property({ type: Boolean, reflect: true }) loading = false;
54
-
55
- // --- Internal State ---
56
- @state() private internalColumns: DataTableColumn[] = [];
57
- @state() private sortState: SortConfig[] = [];
58
- @state() private searchQuery = '';
59
- @state() private showColumnMenu = false;
60
-
61
- private searchTimeout?: number;
62
- private draggedColumnKey: string | null = null;
63
-
64
- static styles = [
65
- resetStyles,
66
- css`
67
- :host {
68
- display: block;
69
- width: 100%;
70
- font-family: var(--st-font-family, inherit);
71
- }
72
-
73
- .toolbar {
74
- display: flex;
75
- justify-content: space-between;
76
- align-items: center;
77
- gap: var(--st-spacing-md);
78
- margin-bottom: var(--st-spacing-md);
79
- flex-wrap: wrap;
80
- }
81
-
82
- .search-wrapper {
83
- flex: 1;
84
- min-width: 250px;
85
- position: relative;
86
- }
87
-
88
- .search-input {
89
- width: 100%;
90
- padding: 0.5rem 0.75rem 0.5rem 2.25rem;
91
- border: 1px solid var(--st-color-border);
92
- border-radius: var(--st-radius-md);
93
- background-color: var(--st-color-surface);
94
- color: var(--st-color-text);
95
- font-size: var(--st-font-size-sm);
96
- }
97
-
98
- .search-input:disabled {
99
- opacity: 0.6;
100
- cursor: not-allowed;
101
- }
102
-
103
- .search-icon {
104
- position: absolute;
105
- left: 0.75rem;
106
- top: 50%;
107
- transform: translateY(-50%);
108
- width: 1rem;
109
- height: 1rem;
110
- color: var(--st-color-text-muted);
111
- }
112
-
113
- .columns-toggle-wrapper {
114
- position: relative;
115
- }
116
-
117
- .columns-menu {
118
- position: absolute;
119
- top: calc(100% + 0.5rem);
120
- right: 0;
121
- background: var(--st-color-surface);
122
- border: 1px solid var(--st-color-border);
123
- border-radius: var(--st-radius-md);
124
- padding: var(--st-spacing-sm);
125
- min-width: 200px;
126
- box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
127
- z-index: 10;
128
- display: flex;
129
- flex-direction: column;
130
- gap: 0.25rem;
131
- }
132
-
133
- .column-option {
134
- display: flex;
135
- align-items: center;
136
- gap: 0.5rem;
137
- font-size: var(--st-font-size-sm);
138
- cursor: pointer;
139
- padding: 0.25rem;
140
- }
141
-
142
- .table-wrapper {
143
- width: 100%;
144
- overflow-x: auto;
145
- border: 1px solid var(--st-color-border);
146
- border-radius: var(--st-radius-md);
147
- }
148
-
149
- table {
150
- width: 100%;
151
- border-collapse: collapse;
152
- text-align: left;
153
- font-size: var(--st-font-size-sm);
154
- background-color: var(--st-color-surface);
155
- }
156
-
157
- th {
158
- font-weight: 600;
159
- padding: var(--st-spacing-sm) var(--st-spacing-md);
160
- border-bottom: 2px solid var(--st-color-border);
161
- color: var(--st-color-text-muted);
162
- white-space: nowrap;
163
- background-color: var(--st-color-surface-muted, #f8fafc);
164
- user-select: none;
165
- }
166
-
167
- th[draggable="true"] {
168
- cursor: grab;
169
- }
170
-
171
- th[draggable="true"]:active {
172
- cursor: grabbing;
173
- }
174
-
175
- th.drag-over {
176
- border-left: 2px solid var(--st-color-primary);
177
- }
178
-
179
- .th-content {
180
- display: flex;
181
- align-items: center;
182
- gap: 0.5rem;
183
- }
184
-
185
- .sortable-header {
186
- cursor: pointer;
187
- display: flex;
188
- align-items: center;
189
- gap: 0.25rem;
190
- }
191
-
192
- .sortable-header:hover {
193
- color: var(--st-color-text);
194
- }
195
-
196
- .sort-icon {
197
- width: 1rem;
198
- height: 1rem;
199
- color: var(--st-color-text-muted);
200
- }
201
-
202
- .sort-icon.active {
203
- color: var(--st-color-primary);
204
- }
205
-
206
- .sort-priority {
207
- font-size: 0.7rem;
208
- background: var(--st-color-border);
209
- color: var(--st-color-text);
210
- border-radius: 50%;
211
- width: 1.1rem;
212
- height: 1.1rem;
213
- display: inline-flex;
214
- align-items: center;
215
- justify-content: center;
216
- }
217
-
218
- td {
219
- padding: var(--st-spacing-sm) var(--st-spacing-md);
220
- border-bottom: 1px solid var(--st-color-border);
221
- color: var(--st-color-text);
222
- }
223
-
224
- tbody tr:last-child td {
225
- border-bottom: none;
226
- }
227
-
228
- tbody tr:hover {
229
- background-color: var(--st-color-surface-hover, #f1f5f9);
230
- }
231
-
232
- .pagination {
233
- display: flex;
234
- justify-content: space-between;
235
- align-items: center;
236
- padding: var(--st-spacing-md) 0;
237
- font-size: var(--st-font-size-sm);
238
- color: var(--st-color-text-muted);
239
- }
240
-
241
- .pagination-controls {
242
- display: flex;
243
- gap: var(--st-spacing-sm);
244
- }
245
-
246
- .drag-handle {
247
- opacity: 0.3;
248
- cursor: grab;
249
- }
250
-
251
- .drag-handle:hover {
252
- opacity: 0.7;
253
- }
254
- `
255
- ];
256
-
257
- willUpdate(changedProperties: PropertyValues) {
258
- if (changedProperties.has('columns') && this.columns.length > 0) {
259
- if (this.internalColumns.length === 0) {
260
- this.internalColumns = [...this.columns];
261
- }
262
- }
263
- }
264
-
265
- firstUpdated() {
266
- if (this.internalColumns.length === 0 && this.columns.length > 0) {
267
- this.internalColumns = [...this.columns];
268
- }
269
- this.emitChange();
270
- }
271
-
272
- connectedCallback() {
273
- super.connectedCallback();
274
- document.addEventListener('click', this.handleOutsideClick);
275
- }
276
-
277
- disconnectedCallback() {
278
- document.removeEventListener('click', this.handleOutsideClick);
279
- super.disconnectedCallback();
280
- }
281
-
282
- private handleOutsideClick = (e: MouseEvent) => {
283
- const target = e.target as HTMLElement;
284
- if (!target.closest('.columns-toggle-wrapper')) {
285
- this.showColumnMenu = false;
286
- }
287
- };
288
-
289
- private emitChange() {
290
- this.dispatchEvent(new CustomEvent('st-data-change', {
291
- detail: {
292
- page: this.page,
293
- pageSize: this.pageSize,
294
- search: this.searchQuery,
295
- sort: this.sortState
296
- },
297
- bubbles: true,
298
- composed: true
299
- }));
300
- }
301
-
302
- private handleSearchInput(e: Event) {
303
- if (this.loading) return;
304
- const val = (e.target as HTMLInputElement).value;
305
- window.clearTimeout(this.searchTimeout);
306
-
307
- this.searchTimeout = window.setTimeout(() => {
308
- this.searchQuery = val;
309
- this.page = 1;
310
- this.emitChange();
311
- }, 350);
312
- }
313
-
314
- private handleSort(key: string, e: MouseEvent) {
315
- if (this.loading) return;
316
- const isShift = e.shiftKey && this.multiSort;
317
- const existingIndex = this.sortState.findIndex(s => s.key === key);
318
- let newDirection: 'asc' | 'desc' | null = 'asc';
319
-
320
- if (existingIndex !== -1) {
321
- newDirection = this.sortState[existingIndex].direction === 'asc' ? 'desc' : null;
322
- }
323
-
324
- let newSortState = [...this.sortState];
325
-
326
- if (isShift) {
327
- if (newDirection) {
328
- if (existingIndex !== -1) {
329
- newSortState[existingIndex].direction = newDirection;
330
- } else {
331
- newSortState.push({ key, direction: newDirection });
332
- }
333
- } else {
334
- newSortState.splice(existingIndex, 1);
335
- }
336
- } else {
337
- if (newDirection) {
338
- newSortState = [{ key, direction: newDirection }];
339
- } else {
340
- newSortState = [];
341
- }
342
- }
343
-
344
- this.sortState = newSortState;
345
- this.page = 1;
346
- this.emitChange();
347
- }
348
-
349
- private get totalPages() {
350
- return Math.max(1, Math.ceil(this.totalRows / this.pageSize));
351
- }
352
-
353
- private prevPage() {
354
- if (!this.loading && this.page > 1) {
355
- this.page--;
356
- this.emitChange();
357
- }
358
- }
359
-
360
- private nextPage() {
361
- if (!this.loading && this.page < this.totalPages) {
362
- this.page++;
363
- this.emitChange();
364
- }
365
- }
366
-
367
- private toggleColumnVisibility(key: string) {
368
- this.internalColumns = this.internalColumns.map(col =>
369
- col.key === key ? { ...col, hidden: !col.hidden } : col
370
- );
371
- }
372
-
373
- private handleDragStart(e: DragEvent, key: string) {
374
- this.draggedColumnKey = key;
375
- if (e.dataTransfer) {
376
- e.dataTransfer.effectAllowed = 'move';
377
- e.dataTransfer.setData('text/plain', key);
378
- }
379
- }
380
-
381
- private handleDragOver(e: DragEvent) {
382
- e.preventDefault();
383
- if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
384
- const target = (e.target as HTMLElement).closest('th');
385
- if (target) target.classList.add('drag-over');
386
- }
387
-
388
- private handleDragLeave(e: DragEvent) {
389
- const target = (e.target as HTMLElement).closest('th');
390
- if (target) target.classList.remove('drag-over');
391
- }
392
-
393
- private handleDrop(e: DragEvent, targetKey: string) {
394
- e.preventDefault();
395
- const targetEl = (e.target as HTMLElement).closest('th');
396
- if (targetEl) targetEl.classList.remove('drag-over');
397
-
398
- if (!this.draggedColumnKey || this.draggedColumnKey === targetKey) return;
399
-
400
- const newCols = [...this.internalColumns];
401
- const fromIndex = newCols.findIndex(c => c.key === this.draggedColumnKey);
402
- const toIndex = newCols.findIndex(c => c.key === targetKey);
403
-
404
- const [movedCol] = newCols.splice(fromIndex, 1);
405
- newCols.splice(toIndex, 0, movedCol);
406
-
407
- this.internalColumns = newCols;
408
- this.draggedColumnKey = null;
409
- }
410
-
411
- private renderSortIcon(key: string) {
412
- const sortIndex = this.sortState.findIndex(s => s.key === key);
413
- if (sortIndex === -1) {
414
- return html`<svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>`;
415
- }
416
-
417
- const sortConfig = this.sortState[sortIndex];
418
- const icon = sortConfig.direction === 'asc'
419
- ? html`<svg class="sort-icon active" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>`
420
- : html`<svg class="sort-icon active" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`;
421
-
422
- const priority = this.sortState.length > 1 && this.multiSort
423
- ? html`<span class="sort-priority">${sortIndex + 1}</span>`
424
- : '';
425
-
426
- return html`${icon}${priority}`;
427
- }
428
-
429
- render() {
430
- const visibleColumns = this.internalColumns.filter(c => !c.hidden);
431
- const skeletonRowsCount = Math.min(this.pageSize, 5);
432
-
433
- return html`
434
- <!-- Toolbar -->
435
- <div class="toolbar">
436
- <div class="search-wrapper">
437
- <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
438
- <input
439
- type="text"
440
- class="search-input"
441
- placeholder="Search..."
442
- ?disabled=${this.loading}
443
- @input=${this.handleSearchInput}
444
- >
445
- </div>
446
-
447
- <div class="columns-toggle-wrapper">
448
- <st-button
449
- variant="secondary"
450
- size="sm"
451
- @click=${() => this.showColumnMenu = !this.showColumnMenu}
452
- >
453
- Columns
454
- </st-button>
455
-
456
- ${this.showColumnMenu ? html`
457
- <div class="columns-menu">
458
- ${this.internalColumns.map(col => html`
459
- <label class="column-option">
460
- <input
461
- type="checkbox"
462
- .checked=${!col.hidden}
463
- @change=${() => this.toggleColumnVisibility(col.key)}
464
- >
465
- ${col.header}
466
- </label>
467
- `)}
468
- </div>
469
- ` : ''}
470
- </div>
471
- </div>
472
-
473
- <!-- Table -->
474
- <div class="table-wrapper">
475
- <table>
476
- <thead>
477
- <tr>
478
- ${visibleColumns.map(col => html`
479
- <th
480
- draggable="true"
481
- @dragstart=${(e: DragEvent) => this.handleDragStart(e, col.key)}
482
- @dragover=${this.handleDragOver}
483
- @dragleave=${this.handleDragLeave}
484
- @drop=${(e: DragEvent) => this.handleDrop(e, col.key)}
485
- >
486
- <div class="th-content">
487
- <svg class="drag-handle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>
488
-
489
- <div
490
- class=${classMap({ 'sortable-header': !!col.sortable })}
491
- @click=${(e: MouseEvent) => col.sortable && this.handleSort(col.key, e)}
492
- >
493
- ${col.header}
494
- ${col.sortable ? this.renderSortIcon(col.key) : ''}
495
- </div>
496
- </div>
497
- </th>
498
- `)}
499
- </tr>
500
- </thead>
501
- <tbody>
502
- ${this.loading ? html`
503
- ${Array.from({ length: skeletonRowsCount }).map(() => html`
504
- <tr>
505
- ${visibleColumns.map(() => html`
506
- <td><st-skeleton variant="text" width="75%"></st-skeleton></td>
507
- `)}
508
- </tr>
509
- `)}
510
- ` : this.data.length === 0 ? html`
511
- <tr>
512
- <td colspan=${visibleColumns.length} style="text-align: center; padding: 2rem;">No results found.</td>
513
- </tr>
514
- ` : this.data.map(row => html`
515
- <tr>
516
- ${visibleColumns.map(col => html`
517
- <td>${row[col.key]}</td>
518
- `)}
519
- </tr>
520
- `)}
521
- </tbody>
522
- </table>
523
- </div>
524
-
525
- <!-- Pagination -->
526
- <div class="pagination">
527
- <div class="pagination-info">
528
- Page ${this.page} of ${this.totalPages} (${this.totalRows} total items)
529
- </div>
530
- <div class="pagination-controls">
531
- <st-button
532
- variant="secondary"
533
- size="sm"
534
- ?disabled=${this.loading || this.page === 1}
535
- @click=${this.prevPage}
536
- >
537
- Prev
538
- </st-button>
539
- <st-button
540
- variant="secondary"
541
- size="sm"
542
- ?disabled=${this.loading || this.page === this.totalPages}
543
- @click=${this.nextPage}
544
- >
545
- Next
546
- </st-button>
547
- </div>
548
- </div>
549
- `;
550
- }
551
- }
552
-
553
- declare global {
554
- interface HTMLElementTagNameMap {
555
- 'st-data-table': StDataTable;
556
- }
1
+ import { LitElement, html, css, PropertyValues } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import { resetStyles } from '../../styles/reset.js';
5
+
6
+ export interface DataTableColumn {
7
+ header: string;
8
+ key: string;
9
+ sortable?: boolean;
10
+ hidden?: boolean;
11
+ }
12
+
13
+ export interface SortConfig {
14
+ key: string;
15
+ direction: 'asc' | 'desc';
16
+ }
17
+
18
+ @customElement('st-data-table')
19
+ export class StDataTable extends LitElement {
20
+ /**
21
+ * Array defining table headers, keys, and sortability.
22
+ */
23
+ @property({ type: Array }) columns: DataTableColumn[] = [];
24
+
25
+ /**
26
+ * Array of row data objects rendered in the table.
27
+ */
28
+ @property({ type: Array }) data: Record<string, any>[] = [];
29
+
30
+ /**
31
+ * Total number of rows available on the server (used for pagination).
32
+ */
33
+ @property({ type: Number, attribute: 'total-rows' }) totalRows = 0;
34
+
35
+ /**
36
+ * Current page number (1-indexed).
37
+ */
38
+ @property({ type: Number }) page = 1;
39
+
40
+ /**
41
+ * Number of rows per page.
42
+ */
43
+ @property({ type: Number, attribute: 'page-size' }) pageSize = 10;
44
+
45
+ /**
46
+ * Enables holding 'Shift' to sort by multiple columns sequentially.
47
+ */
48
+ @property({ type: Boolean, attribute: 'multi-sort' }) multiSort = false;
49
+
50
+ /**
51
+ * Displays loading skeleton rows and locks controls.
52
+ */
53
+ @property({ type: Boolean, reflect: true }) loading = false;
54
+
55
+ // --- Internal State ---
56
+ @state() private internalColumns: DataTableColumn[] = [];
57
+ @state() private sortState: SortConfig[] = [];
58
+ @state() private searchQuery = '';
59
+ @state() private showColumnMenu = false;
60
+
61
+ private searchTimeout?: number;
62
+ private draggedColumnKey: string | null = null;
63
+
64
+ static styles = [
65
+ resetStyles,
66
+ css`
67
+ :host {
68
+ display: block;
69
+ width: 100%;
70
+ font-family: var(--st-font-family, inherit);
71
+ }
72
+
73
+ .toolbar {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ align-items: center;
77
+ gap: var(--st-spacing-md);
78
+ margin-bottom: var(--st-spacing-md);
79
+ flex-wrap: wrap;
80
+ }
81
+
82
+ .search-wrapper {
83
+ flex: 1;
84
+ min-width: 250px;
85
+ position: relative;
86
+ }
87
+
88
+ .search-input {
89
+ width: 100%;
90
+ padding: 0.5rem 0.75rem 0.5rem 2.25rem;
91
+ border: 1px solid var(--st-color-border);
92
+ border-radius: var(--st-radius-md);
93
+ background-color: var(--st-color-surface);
94
+ color: var(--st-color-text);
95
+ font-size: var(--st-font-size-sm);
96
+ }
97
+
98
+ .search-input:disabled {
99
+ opacity: 0.6;
100
+ cursor: not-allowed;
101
+ }
102
+
103
+ .search-icon {
104
+ position: absolute;
105
+ left: 0.75rem;
106
+ top: 50%;
107
+ transform: translateY(-50%);
108
+ width: 1rem;
109
+ height: 1rem;
110
+ color: var(--st-color-text-muted);
111
+ }
112
+
113
+ .columns-toggle-wrapper {
114
+ position: relative;
115
+ }
116
+
117
+ .columns-menu {
118
+ position: absolute;
119
+ top: calc(100% + 0.5rem);
120
+ right: 0;
121
+ background: var(--st-color-surface);
122
+ border: 1px solid var(--st-color-border);
123
+ border-radius: var(--st-radius-md);
124
+ padding: var(--st-spacing-sm);
125
+ min-width: 200px;
126
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
127
+ z-index: 10;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 0.25rem;
131
+ }
132
+
133
+ .column-option {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 0.5rem;
137
+ font-size: var(--st-font-size-sm);
138
+ cursor: pointer;
139
+ padding: 0.25rem;
140
+ }
141
+
142
+ .table-wrapper {
143
+ width: 100%;
144
+ overflow-x: auto;
145
+ border: 1px solid var(--st-color-border);
146
+ border-radius: var(--st-radius-md);
147
+ }
148
+
149
+ table {
150
+ width: 100%;
151
+ border-collapse: collapse;
152
+ text-align: left;
153
+ font-size: var(--st-font-size-sm);
154
+ background-color: var(--st-color-surface);
155
+ }
156
+
157
+ th {
158
+ font-weight: 600;
159
+ padding: var(--st-spacing-sm) var(--st-spacing-md);
160
+ border-bottom: 2px solid var(--st-color-border);
161
+ color: var(--st-color-text-muted);
162
+ white-space: nowrap;
163
+ background-color: var(--st-color-surface-muted, #f8fafc);
164
+ user-select: none;
165
+ }
166
+
167
+ th[draggable="true"] {
168
+ cursor: grab;
169
+ }
170
+
171
+ th[draggable="true"]:active {
172
+ cursor: grabbing;
173
+ }
174
+
175
+ th.drag-over {
176
+ border-left: 2px solid var(--st-color-primary);
177
+ }
178
+
179
+ .th-content {
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 0.5rem;
183
+ }
184
+
185
+ .sortable-header {
186
+ cursor: pointer;
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 0.25rem;
190
+ }
191
+
192
+ .sortable-header:hover {
193
+ color: var(--st-color-text);
194
+ }
195
+
196
+ .sort-icon {
197
+ width: 1rem;
198
+ height: 1rem;
199
+ color: var(--st-color-text-muted);
200
+ }
201
+
202
+ .sort-icon.active {
203
+ color: var(--st-color-primary);
204
+ }
205
+
206
+ .sort-priority {
207
+ font-size: 0.7rem;
208
+ background: var(--st-color-border);
209
+ color: var(--st-color-text);
210
+ border-radius: 50%;
211
+ width: 1.1rem;
212
+ height: 1.1rem;
213
+ display: inline-flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ }
217
+
218
+ td {
219
+ padding: var(--st-spacing-sm) var(--st-spacing-md);
220
+ border-bottom: 1px solid var(--st-color-border);
221
+ color: var(--st-color-text);
222
+ }
223
+
224
+ tbody tr:last-child td {
225
+ border-bottom: none;
226
+ }
227
+
228
+ tbody tr:hover {
229
+ background-color: var(--st-color-surface-hover, #f1f5f9);
230
+ }
231
+
232
+ .pagination {
233
+ display: flex;
234
+ justify-content: space-between;
235
+ align-items: center;
236
+ padding: var(--st-spacing-md) 0;
237
+ font-size: var(--st-font-size-sm);
238
+ color: var(--st-color-text-muted);
239
+ }
240
+
241
+ .pagination-controls {
242
+ display: flex;
243
+ gap: var(--st-spacing-sm);
244
+ }
245
+
246
+ .drag-handle {
247
+ opacity: 0.3;
248
+ cursor: grab;
249
+ }
250
+
251
+ .drag-handle:hover {
252
+ opacity: 0.7;
253
+ }
254
+ `
255
+ ];
256
+
257
+ willUpdate(changedProperties: PropertyValues) {
258
+ if (changedProperties.has('columns') && this.columns.length > 0) {
259
+ if (this.internalColumns.length === 0) {
260
+ this.internalColumns = [...this.columns];
261
+ }
262
+ }
263
+ }
264
+
265
+ firstUpdated() {
266
+ if (this.internalColumns.length === 0 && this.columns.length > 0) {
267
+ this.internalColumns = [...this.columns];
268
+ }
269
+ this.emitChange();
270
+ }
271
+
272
+ connectedCallback() {
273
+ super.connectedCallback();
274
+ document.addEventListener('click', this.handleOutsideClick);
275
+ }
276
+
277
+ disconnectedCallback() {
278
+ document.removeEventListener('click', this.handleOutsideClick);
279
+ super.disconnectedCallback();
280
+ }
281
+
282
+ private handleOutsideClick = (e: MouseEvent) => {
283
+ const target = e.target as HTMLElement;
284
+ if (!target.closest('.columns-toggle-wrapper')) {
285
+ this.showColumnMenu = false;
286
+ }
287
+ };
288
+
289
+ private emitChange() {
290
+ this.dispatchEvent(new CustomEvent('st-data-change', {
291
+ detail: {
292
+ page: this.page,
293
+ pageSize: this.pageSize,
294
+ search: this.searchQuery,
295
+ sort: this.sortState
296
+ },
297
+ bubbles: true,
298
+ composed: true
299
+ }));
300
+ }
301
+
302
+ private handleSearchInput(e: Event) {
303
+ if (this.loading) return;
304
+ const val = (e.target as HTMLInputElement).value;
305
+ window.clearTimeout(this.searchTimeout);
306
+
307
+ this.searchTimeout = window.setTimeout(() => {
308
+ this.searchQuery = val;
309
+ this.page = 1;
310
+ this.emitChange();
311
+ }, 350);
312
+ }
313
+
314
+ private handleSort(key: string, e: MouseEvent) {
315
+ if (this.loading) return;
316
+ const isShift = e.shiftKey && this.multiSort;
317
+ const existingIndex = this.sortState.findIndex(s => s.key === key);
318
+ let newDirection: 'asc' | 'desc' | null = 'asc';
319
+
320
+ if (existingIndex !== -1) {
321
+ newDirection = this.sortState[existingIndex].direction === 'asc' ? 'desc' : null;
322
+ }
323
+
324
+ let newSortState = [...this.sortState];
325
+
326
+ if (isShift) {
327
+ if (newDirection) {
328
+ if (existingIndex !== -1) {
329
+ newSortState[existingIndex].direction = newDirection;
330
+ } else {
331
+ newSortState.push({ key, direction: newDirection });
332
+ }
333
+ } else {
334
+ newSortState.splice(existingIndex, 1);
335
+ }
336
+ } else {
337
+ if (newDirection) {
338
+ newSortState = [{ key, direction: newDirection }];
339
+ } else {
340
+ newSortState = [];
341
+ }
342
+ }
343
+
344
+ this.sortState = newSortState;
345
+ this.page = 1;
346
+ this.emitChange();
347
+ }
348
+
349
+ private get totalPages() {
350
+ return Math.max(1, Math.ceil(this.totalRows / this.pageSize));
351
+ }
352
+
353
+ private prevPage() {
354
+ if (!this.loading && this.page > 1) {
355
+ this.page--;
356
+ this.emitChange();
357
+ }
358
+ }
359
+
360
+ private nextPage() {
361
+ if (!this.loading && this.page < this.totalPages) {
362
+ this.page++;
363
+ this.emitChange();
364
+ }
365
+ }
366
+
367
+ private toggleColumnVisibility(key: string) {
368
+ this.internalColumns = this.internalColumns.map(col =>
369
+ col.key === key ? { ...col, hidden: !col.hidden } : col
370
+ );
371
+ }
372
+
373
+ private handleDragStart(e: DragEvent, key: string) {
374
+ this.draggedColumnKey = key;
375
+ if (e.dataTransfer) {
376
+ e.dataTransfer.effectAllowed = 'move';
377
+ e.dataTransfer.setData('text/plain', key);
378
+ }
379
+ }
380
+
381
+ private handleDragOver(e: DragEvent) {
382
+ e.preventDefault();
383
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
384
+ const target = (e.target as HTMLElement).closest('th');
385
+ if (target) target.classList.add('drag-over');
386
+ }
387
+
388
+ private handleDragLeave(e: DragEvent) {
389
+ const target = (e.target as HTMLElement).closest('th');
390
+ if (target) target.classList.remove('drag-over');
391
+ }
392
+
393
+ private handleDrop(e: DragEvent, targetKey: string) {
394
+ e.preventDefault();
395
+ const targetEl = (e.target as HTMLElement).closest('th');
396
+ if (targetEl) targetEl.classList.remove('drag-over');
397
+
398
+ if (!this.draggedColumnKey || this.draggedColumnKey === targetKey) return;
399
+
400
+ const newCols = [...this.internalColumns];
401
+ const fromIndex = newCols.findIndex(c => c.key === this.draggedColumnKey);
402
+ const toIndex = newCols.findIndex(c => c.key === targetKey);
403
+
404
+ const [movedCol] = newCols.splice(fromIndex, 1);
405
+ newCols.splice(toIndex, 0, movedCol);
406
+
407
+ this.internalColumns = newCols;
408
+ this.draggedColumnKey = null;
409
+ }
410
+
411
+ private renderSortIcon(key: string) {
412
+ const sortIndex = this.sortState.findIndex(s => s.key === key);
413
+ if (sortIndex === -1) {
414
+ return html`<svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>`;
415
+ }
416
+
417
+ const sortConfig = this.sortState[sortIndex];
418
+ const icon = sortConfig.direction === 'asc'
419
+ ? html`<svg class="sort-icon active" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>`
420
+ : html`<svg class="sort-icon active" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`;
421
+
422
+ const priority = this.sortState.length > 1 && this.multiSort
423
+ ? html`<span class="sort-priority">${sortIndex + 1}</span>`
424
+ : '';
425
+
426
+ return html`${icon}${priority}`;
427
+ }
428
+
429
+ render() {
430
+ const visibleColumns = this.internalColumns.filter(c => !c.hidden);
431
+ const skeletonRowsCount = Math.min(this.pageSize, 5);
432
+
433
+ return html`
434
+ <!-- Toolbar -->
435
+ <div class="toolbar">
436
+ <div class="search-wrapper">
437
+ <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
438
+ <input
439
+ type="text"
440
+ class="search-input"
441
+ placeholder="Search..."
442
+ ?disabled=${this.loading}
443
+ @input=${this.handleSearchInput}
444
+ >
445
+ </div>
446
+
447
+ <div class="columns-toggle-wrapper">
448
+ <st-button
449
+ variant="secondary"
450
+ size="sm"
451
+ @click=${() => this.showColumnMenu = !this.showColumnMenu}
452
+ >
453
+ Columns
454
+ </st-button>
455
+
456
+ ${this.showColumnMenu ? html`
457
+ <div class="columns-menu">
458
+ ${this.internalColumns.map(col => html`
459
+ <label class="column-option">
460
+ <input
461
+ type="checkbox"
462
+ .checked=${!col.hidden}
463
+ @change=${() => this.toggleColumnVisibility(col.key)}
464
+ >
465
+ ${col.header}
466
+ </label>
467
+ `)}
468
+ </div>
469
+ ` : ''}
470
+ </div>
471
+ </div>
472
+
473
+ <!-- Table -->
474
+ <div class="table-wrapper">
475
+ <table>
476
+ <thead>
477
+ <tr>
478
+ ${visibleColumns.map(col => html`
479
+ <th
480
+ draggable="true"
481
+ @dragstart=${(e: DragEvent) => this.handleDragStart(e, col.key)}
482
+ @dragover=${this.handleDragOver}
483
+ @dragleave=${this.handleDragLeave}
484
+ @drop=${(e: DragEvent) => this.handleDrop(e, col.key)}
485
+ >
486
+ <div class="th-content">
487
+ <svg class="drag-handle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>
488
+
489
+ <div
490
+ class=${classMap({ 'sortable-header': !!col.sortable })}
491
+ @click=${(e: MouseEvent) => col.sortable && this.handleSort(col.key, e)}
492
+ >
493
+ ${col.header}
494
+ ${col.sortable ? this.renderSortIcon(col.key) : ''}
495
+ </div>
496
+ </div>
497
+ </th>
498
+ `)}
499
+ </tr>
500
+ </thead>
501
+ <tbody>
502
+ ${this.loading ? html`
503
+ ${Array.from({ length: skeletonRowsCount }).map(() => html`
504
+ <tr>
505
+ ${visibleColumns.map(() => html`
506
+ <td><st-skeleton variant="text" width="75%"></st-skeleton></td>
507
+ `)}
508
+ </tr>
509
+ `)}
510
+ ` : this.data.length === 0 ? html`
511
+ <tr>
512
+ <td colspan=${visibleColumns.length} style="text-align: center; padding: 2rem;">No results found.</td>
513
+ </tr>
514
+ ` : this.data.map(row => html`
515
+ <tr>
516
+ ${visibleColumns.map(col => html`
517
+ <td>${row[col.key]}</td>
518
+ `)}
519
+ </tr>
520
+ `)}
521
+ </tbody>
522
+ </table>
523
+ </div>
524
+
525
+ <!-- Pagination -->
526
+ <div class="pagination">
527
+ <div class="pagination-info">
528
+ Page ${this.page} of ${this.totalPages} (${this.totalRows} total items)
529
+ </div>
530
+ <div class="pagination-controls">
531
+ <st-button
532
+ variant="secondary"
533
+ size="sm"
534
+ ?disabled=${this.loading || this.page === 1}
535
+ @click=${this.prevPage}
536
+ >
537
+ Prev
538
+ </st-button>
539
+ <st-button
540
+ variant="secondary"
541
+ size="sm"
542
+ ?disabled=${this.loading || this.page === this.totalPages}
543
+ @click=${this.nextPage}
544
+ >
545
+ Next
546
+ </st-button>
547
+ </div>
548
+ </div>
549
+ `;
550
+ }
551
+ }
552
+
553
+ declare global {
554
+ interface HTMLElementTagNameMap {
555
+ 'st-data-table': StDataTable;
556
+ }
557
557
  }