@sondor-technology/sondor-components 1.0.0-1165-initial-setup.11

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 +116 -0
  54. package/dist/components/icon/icon-paths.d.ts.map +1 -0
  55. package/dist/components/icon/icon-paths.js +121 -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 +9 -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 +111 -0
  116. package/dist/components/vertical-nav/vertical-nav-item.d.ts +8 -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 +65 -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 +192 -0
  124. package/src/components/alert/alert.ts +101 -0
  125. package/src/components/badge/badge.ts +97 -0
  126. package/src/components/breadcrumb/breadcrumb.ts +99 -0
  127. package/src/components/button/button.ts +281 -0
  128. package/src/components/button-group/button-group.ts +67 -0
  129. package/src/components/card/card.ts +53 -0
  130. package/src/components/checkbox/checkbox.ts +238 -0
  131. package/src/components/code-block/code-block.ts +257 -0
  132. package/src/components/content-card/content-card.ts +134 -0
  133. package/src/components/divider/divider.ts +69 -0
  134. package/src/components/drawer/drawer.ts +218 -0
  135. package/src/components/field/field.ts +31 -0
  136. package/src/components/footer/footer.ts +58 -0
  137. package/src/components/form/form-step.ts +32 -0
  138. package/src/components/form/form.ts +376 -0
  139. package/src/components/grid/grid-row.ts +76 -0
  140. package/src/components/grid/grid.ts +37 -0
  141. package/src/components/icon/icon-paths.ts +124 -0
  142. package/src/components/icon/icon.ts +83 -0
  143. package/src/components/input/input.ts +248 -0
  144. package/src/components/link/link.ts +98 -0
  145. package/src/components/metadata/metadata.ts +67 -0
  146. package/src/components/modal/modal.ts +159 -0
  147. package/src/components/page-layout/page-layout.ts +705 -0
  148. package/src/components/radio/radio.ts +142 -0
  149. package/src/components/radio-group/radio-group.ts +157 -0
  150. package/src/components/router/router.ts +47 -0
  151. package/src/components/select/select.ts +162 -0
  152. package/src/components/skeleton/skeleton.ts +68 -0
  153. package/src/components/switch/switch.ts +181 -0
  154. package/src/components/table/data-table.ts +557 -0
  155. package/src/components/table/table.ts +154 -0
  156. package/src/components/tabs/tab-panel.ts +31 -0
  157. package/src/components/tabs/tab.ts +77 -0
  158. package/src/components/tabs/tabs.ts +88 -0
  159. package/src/components/textarea/textarea.ts +215 -0
  160. package/src/components/theme-toggle/theme-toggle.ts +131 -0
  161. package/src/components/vertical-nav/vertical-nav-group.ts +97 -0
  162. package/src/components/vertical-nav/vertical-nav-item.ts +50 -0
  163. package/src/styles/globals.css +58 -0
  164. package/src/styles/index.css +80 -0
  165. package/src/styles/reset.ts +51 -0
  166. package/src/styles/theme.css +59 -0
  167. package/src/styles/typography.css +26 -0
  168. package/tsconfig.json +16 -0
@@ -0,0 +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
+ }
557
+ }
@@ -0,0 +1,154 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { resetStyles } from '../../styles/reset.js';
4
+
5
+ export type TableMode = 'horizontal' | 'vertical';
6
+
7
+ export interface TableColumn {
8
+ header: string;
9
+ key: string;
10
+ }
11
+
12
+ @customElement('st-table')
13
+ export class StTable extends LitElement {
14
+ @property({ type: Array }) columns: TableColumn[] = [];
15
+ @property({ type: Array }) data: Record<string, any>[] = [];
16
+
17
+ @property({ type: String, reflect: true }) mode: TableMode = 'horizontal';
18
+ @property({ type: Boolean, reflect: true }) striped = false;
19
+ @property({ type: Boolean, reflect: true }) hoverable = false;
20
+ @property({ type: Boolean, reflect: true }) dense = false;
21
+
22
+ static styles = [
23
+ resetStyles,
24
+ css`
25
+ :host {
26
+ display: block;
27
+ width: 100%;
28
+ }
29
+
30
+ .table-wrapper {
31
+ width: 100%;
32
+ overflow-x: auto;
33
+ -webkit-overflow-scrolling: touch;
34
+ }
35
+
36
+ table {
37
+ width: 100%;
38
+ border-collapse: collapse;
39
+ text-align: left;
40
+ font-size: var(--st-font-size-base, 1rem);
41
+ color: var(--st-color-text);
42
+ }
43
+
44
+ th {
45
+ font-weight: 600;
46
+ padding: var(--st-spacing-md) var(--st-spacing-lg);
47
+ border-bottom: 2px solid var(--st-color-border);
48
+ color: var(--st-color-text-muted);
49
+ white-space: nowrap;
50
+ }
51
+
52
+ td {
53
+ padding: var(--st-spacing-md) var(--st-spacing-lg);
54
+ border-bottom: 1px solid var(--st-color-border);
55
+ }
56
+
57
+ /* Modifiers */
58
+ :host([dense]) th,
59
+ :host([dense]) td {
60
+ padding: var(--st-spacing-sm) var(--st-spacing-md);
61
+ }
62
+
63
+ :host([striped]) tbody tr:nth-of-type(odd) {
64
+ background-color: var(--st-color-surface-muted);
65
+ }
66
+
67
+ :host([hoverable]) tbody tr:hover {
68
+ background-color: var(--st-color-surface-hover);
69
+ }
70
+
71
+ /* ==========================================================
72
+ Vertical Mode (Responsive Stacked Cards)
73
+ ========================================================== */
74
+ :host([mode="vertical"]) table,
75
+ :host([mode="vertical"]) tbody,
76
+ :host([mode="vertical"]) tr,
77
+ :host([mode="vertical"]) td {
78
+ display: block;
79
+ width: 100%;
80
+ }
81
+
82
+ /* Visually hide header but keep for screen readers */
83
+ :host([mode="vertical"]) thead {
84
+ position: absolute;
85
+ width: 1px;
86
+ height: 1px;
87
+ padding: 0;
88
+ margin: -1px;
89
+ overflow: hidden;
90
+ clip: rect(0, 0, 0, 0);
91
+ white-space: nowrap;
92
+ border: 0;
93
+ }
94
+
95
+ :host([mode="vertical"]) tr {
96
+ margin-bottom: var(--st-spacing-md);
97
+ border: 1px solid var(--st-color-border);
98
+ border-radius: var(--st-radius-md);
99
+ background-color: var(--st-color-surface);
100
+ overflow: hidden;
101
+ }
102
+
103
+ :host([mode="vertical"]) td {
104
+ display: flex;
105
+ justify-content: space-between;
106
+ align-items: center;
107
+ border-bottom: 1px solid var(--st-color-border);
108
+ text-align: right;
109
+ }
110
+
111
+ :host([mode="vertical"]) td:last-child {
112
+ border-bottom: none;
113
+ }
114
+
115
+ /* Inject the header name on the left */
116
+ :host([mode="vertical"]) td::before {
117
+ content: attr(data-label);
118
+ font-weight: 600;
119
+ text-align: left;
120
+ padding-right: var(--st-spacing-md);
121
+ color: var(--st-color-text-muted);
122
+ }
123
+ `
124
+ ];
125
+
126
+ render() {
127
+ return html`
128
+ <div class="table-wrapper">
129
+ <table>
130
+ <thead>
131
+ <tr>
132
+ ${this.columns.map(col => html`<th scope="col">${col.header}</th>`)}
133
+ </tr>
134
+ </thead>
135
+ <tbody>
136
+ ${this.data.map(row => html`
137
+ <tr>
138
+ ${this.columns.map(col => html`
139
+ <td data-label=${col.header}>${row[col.key]}</td>
140
+ `)}
141
+ </tr>
142
+ `)}
143
+ </tbody>
144
+ </table>
145
+ </div>
146
+ `;
147
+ }
148
+ }
149
+
150
+ declare global {
151
+ interface HTMLElementTagNameMap {
152
+ 'st-table': StTable;
153
+ }
154
+ }