@sondor-technology/sondor-components 1.0.0 → 1.0.1-develop.3

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/README.md +8 -0
  2. package/dist/components/alert/alert.d.ts +13 -0
  3. package/dist/components/alert/alert.d.ts.map +1 -0
  4. package/dist/components/alert/alert.js +105 -0
  5. package/dist/components/badge/badge.d.ts +13 -0
  6. package/dist/components/badge/badge.d.ts.map +1 -0
  7. package/dist/components/badge/badge.js +101 -0
  8. package/dist/components/breadcrumb/breadcrumb.d.ts +19 -0
  9. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
  10. package/dist/components/breadcrumb/breadcrumb.js +99 -0
  11. package/dist/components/button/button.d.ts +23 -0
  12. package/dist/components/button/button.d.ts.map +1 -0
  13. package/dist/components/button/button.js +304 -0
  14. package/dist/components/button-group/button-group.d.ts +6 -0
  15. package/dist/components/button-group/button-group.d.ts.map +1 -0
  16. package/dist/components/button-group/button-group.js +74 -0
  17. package/dist/components/card/card.d.ts +11 -0
  18. package/dist/components/card/card.d.ts.map +1 -0
  19. package/dist/components/card/card.js +54 -0
  20. package/dist/components/checkbox/checkbox.d.ts +28 -0
  21. package/dist/components/checkbox/checkbox.d.ts.map +1 -0
  22. package/dist/components/checkbox/checkbox.js +247 -0
  23. package/dist/components/code-block/code-block.d.ts +20 -0
  24. package/dist/components/code-block/code-block.d.ts.map +1 -0
  25. package/dist/components/code-block/code-block.js +260 -0
  26. package/dist/components/content-card/content-card.d.ts +13 -0
  27. package/dist/components/content-card/content-card.d.ts.map +1 -0
  28. package/dist/components/content-card/content-card.js +140 -0
  29. package/dist/components/divider/divider.d.ts +16 -0
  30. package/dist/components/divider/divider.d.ts.map +1 -0
  31. package/dist/components/divider/divider.js +82 -0
  32. package/dist/components/drawer/drawer.d.ts +22 -0
  33. package/dist/components/drawer/drawer.d.ts.map +1 -0
  34. package/dist/components/drawer/drawer.js +226 -0
  35. package/dist/components/field/field.d.ts +11 -0
  36. package/dist/components/field/field.d.ts.map +1 -0
  37. package/dist/components/field/field.js +32 -0
  38. package/dist/components/footer/footer.d.ts +6 -0
  39. package/dist/components/footer/footer.d.ts.map +1 -0
  40. package/dist/components/footer/footer.js +65 -0
  41. package/dist/components/form/form-step.d.ts +13 -0
  42. package/dist/components/form/form-step.d.ts.map +1 -0
  43. package/dist/components/form/form-step.js +41 -0
  44. package/dist/components/form/form.d.ts +25 -0
  45. package/dist/components/form/form.d.ts.map +1 -0
  46. package/dist/components/form/form.js +358 -0
  47. package/dist/components/grid/grid-row.d.ts +17 -0
  48. package/dist/components/grid/grid-row.d.ts.map +1 -0
  49. package/dist/components/grid/grid-row.js +92 -0
  50. package/dist/components/grid/grid.d.ts +13 -0
  51. package/dist/components/grid/grid.d.ts.map +1 -0
  52. package/dist/components/grid/grid.js +42 -0
  53. package/dist/components/icon/icon-paths.d.ts +132 -0
  54. package/dist/components/icon/icon-paths.d.ts.map +1 -0
  55. package/dist/components/icon/icon-paths.js +137 -0
  56. package/dist/components/icon/icon.d.ts +16 -0
  57. package/dist/components/icon/icon.d.ts.map +1 -0
  58. package/dist/components/icon/icon.js +95 -0
  59. package/dist/components/input/input.d.ts +31 -0
  60. package/dist/components/input/input.d.ts.map +1 -0
  61. package/dist/components/input/input.js +268 -0
  62. package/dist/components/link/link.d.ts +16 -0
  63. package/dist/components/link/link.d.ts.map +1 -0
  64. package/dist/components/link/link.js +116 -0
  65. package/dist/components/metadata/metadata.d.ts +18 -0
  66. package/dist/components/metadata/metadata.d.ts.map +1 -0
  67. package/dist/components/metadata/metadata.js +71 -0
  68. package/dist/components/modal/modal.d.ts +24 -0
  69. package/dist/components/modal/modal.d.ts.map +1 -0
  70. package/dist/components/modal/modal.js +163 -0
  71. package/dist/components/page-layout/page-layout.d.ts +46 -0
  72. package/dist/components/page-layout/page-layout.d.ts.map +1 -0
  73. package/dist/components/page-layout/page-layout.js +697 -0
  74. package/dist/components/radio/radio.d.ts +16 -0
  75. package/dist/components/radio/radio.d.ts.map +1 -0
  76. package/dist/components/radio/radio.js +152 -0
  77. package/dist/components/radio-group/radio-group.d.ts +27 -0
  78. package/dist/components/radio-group/radio-group.d.ts.map +1 -0
  79. package/dist/components/radio-group/radio-group.js +161 -0
  80. package/dist/components/router/router.d.ts +7 -0
  81. package/dist/components/router/router.d.ts.map +1 -0
  82. package/dist/components/router/router.js +37 -0
  83. package/dist/components/select/select.d.ts +16 -0
  84. package/dist/components/select/select.d.ts.map +1 -0
  85. package/dist/components/select/select.js +172 -0
  86. package/dist/components/skeleton/skeleton.d.ts +17 -0
  87. package/dist/components/skeleton/skeleton.d.ts.map +1 -0
  88. package/dist/components/skeleton/skeleton.js +78 -0
  89. package/dist/components/switch/switch.d.ts +25 -0
  90. package/dist/components/switch/switch.d.ts.map +1 -0
  91. package/dist/components/switch/switch.js +185 -0
  92. package/dist/components/table/data-table.d.ts +72 -0
  93. package/dist/components/table/data-table.d.ts.map +1 -0
  94. package/dist/components/table/data-table.js +551 -0
  95. package/dist/components/table/table.d.ts +22 -0
  96. package/dist/components/table/table.d.ts.map +1 -0
  97. package/dist/components/table/table.js +167 -0
  98. package/dist/components/tabs/tab-panel.d.ts +8 -0
  99. package/dist/components/tabs/tab-panel.d.ts.map +1 -0
  100. package/dist/components/tabs/tab-panel.js +46 -0
  101. package/dist/components/tabs/tab.d.ts +9 -0
  102. package/dist/components/tabs/tab.d.ts.map +1 -0
  103. package/dist/components/tabs/tab.js +91 -0
  104. package/dist/components/tabs/tabs.d.ts +14 -0
  105. package/dist/components/tabs/tabs.d.ts.map +1 -0
  106. package/dist/components/tabs/tabs.js +85 -0
  107. package/dist/components/textarea/textarea.d.ts +36 -0
  108. package/dist/components/textarea/textarea.d.ts.map +1 -0
  109. package/dist/components/textarea/textarea.js +231 -0
  110. package/dist/components/theme-toggle/theme-toggle.d.ts +15 -0
  111. package/dist/components/theme-toggle/theme-toggle.d.ts.map +1 -0
  112. package/dist/components/theme-toggle/theme-toggle.js +130 -0
  113. package/dist/components/vertical-nav/vertical-nav-group.d.ts +10 -0
  114. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +1 -0
  115. package/dist/components/vertical-nav/vertical-nav-group.js +127 -0
  116. package/dist/components/vertical-nav/vertical-nav-item.d.ts +9 -0
  117. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +1 -0
  118. package/dist/components/vertical-nav/vertical-nav-item.js +85 -0
  119. package/dist/styles/index.css +171 -0
  120. package/dist/styles/reset.d.ts +2 -0
  121. package/dist/styles/reset.d.ts.map +1 -0
  122. package/dist/styles/reset.js +50 -0
  123. package/package.json +7 -2
  124. package/src/components/alert/alert.ts +100 -100
  125. package/src/components/badge/badge.ts +96 -96
  126. package/src/components/breadcrumb/breadcrumb.ts +98 -98
  127. package/src/components/button/button.ts +281 -281
  128. package/src/components/button-group/button-group.ts +67 -67
  129. package/src/components/card/card.ts +52 -52
  130. package/src/components/checkbox/checkbox.ts +238 -238
  131. package/src/components/code-block/code-block.ts +256 -256
  132. package/src/components/content-card/content-card.ts +133 -133
  133. package/src/components/divider/divider.ts +68 -68
  134. package/src/components/drawer/drawer.ts +217 -217
  135. package/src/components/field/field.ts +30 -30
  136. package/src/components/footer/footer.ts +58 -58
  137. package/src/components/form/form-step.ts +32 -32
  138. package/src/components/form/form.ts +376 -376
  139. package/src/components/grid/grid-row.ts +75 -75
  140. package/src/components/grid/grid.ts +36 -36
  141. package/src/components/icon/icon-paths.ts +140 -124
  142. package/src/components/icon/icon.ts +82 -82
  143. package/src/components/input/input.ts +248 -248
  144. package/src/components/link/link.ts +97 -97
  145. package/src/components/metadata/metadata.ts +66 -66
  146. package/src/components/modal/modal.ts +158 -158
  147. package/src/components/page-layout/page-layout.ts +704 -704
  148. package/src/components/radio/radio.ts +142 -142
  149. package/src/components/radio-group/radio-group.ts +157 -157
  150. package/src/components/router/router.ts +46 -46
  151. package/src/components/select/select.ts +161 -161
  152. package/src/components/skeleton/skeleton.ts +67 -67
  153. package/src/components/switch/switch.ts +181 -181
  154. package/src/components/table/data-table.ts +556 -556
  155. package/src/components/table/table.ts +153 -153
  156. package/src/components/tabs/tab-panel.ts +30 -30
  157. package/src/components/tabs/tab.ts +76 -76
  158. package/src/components/tabs/tabs.ts +88 -88
  159. package/src/components/textarea/textarea.ts +215 -215
  160. package/src/components/theme-toggle/theme-toggle.ts +131 -131
  161. package/src/components/vertical-nav/vertical-nav-group.ts +109 -96
  162. package/src/components/vertical-nav/vertical-nav-item.ts +66 -49
  163. package/src/styles/globals.css +58 -58
  164. package/src/styles/index.css +80 -80
  165. package/src/styles/reset.ts +50 -50
  166. package/src/styles/theme.css +58 -58
  167. package/src/styles/typography.css +25 -25
  168. package/tsconfig.json +15 -15
@@ -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
  }