mertani-web-toolkit 0.1.23 → 0.1.24

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.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import './button.css';
3
3
  import { Icon } from '../../index.js';
4
- import type { TIconName } from '../../types/Icon.js';
4
+ import type { TIconName } from '../../icons/index.js';
5
5
 
6
6
  interface Props {
7
7
  size?: 'small' | 'medium' | 'large';
@@ -1,5 +1,5 @@
1
1
  import './button.css';
2
- import type { TIconName } from '../../types/Icon.js';
2
+ import type { TIconName } from '../../icons/index.js';
3
3
  interface Props {
4
4
  size?: 'small' | 'medium' | 'large';
5
5
  variant?: 'primary' | 'secondary' | 'outline';
@@ -0,0 +1,300 @@
1
+ <script lang="ts">
2
+ import TableShimmer from './TableShimmer.svelte';
3
+ import TableBadge from './TableBadge.svelte';
4
+ import TablePagination from './TablePagination.svelte';
5
+ import Icon from '../Icon/Icon.svelte';
6
+ import type { Column, DeviceStatus } from './types.js';
7
+
8
+ export type { Column } from './types';
9
+
10
+ let {
11
+ columns = [],
12
+ data = [],
13
+ loading = false,
14
+ height = 'calc(100vh - 350px)',
15
+ minHeight = '500px',
16
+ onRowClick,
17
+ showPagination = false,
18
+ currentPage = 1,
19
+ totalPages = 1,
20
+ itemsPerPage = 25,
21
+ totalItems = 0,
22
+ onPageChange,
23
+ onItemsPerPageChange
24
+ }: {
25
+ columns?: Column[];
26
+ data?: any[];
27
+ loading?: boolean;
28
+ height?: string;
29
+ minHeight?: string;
30
+ onRowClick?: (row: any, index: number) => void;
31
+ showPagination?: boolean;
32
+ currentPage?: number;
33
+ totalPages?: number;
34
+ itemsPerPage?: number;
35
+ totalItems?: number;
36
+ onPageChange?: (page: number) => void;
37
+ onItemsPerPageChange?: (itemsPerPage: number) => void;
38
+ } = $props();
39
+
40
+ const emptyMessage = 'Tidak ada data';
41
+
42
+ let scrollContainer: HTMLElement | null = $state(null);
43
+ let internalHasHorizontalScroll = $state(false);
44
+
45
+ function updateScrollState() {
46
+ internalHasHorizontalScroll = scrollContainer
47
+ ? scrollContainer.scrollWidth > scrollContainer.clientWidth
48
+ : false;
49
+ }
50
+
51
+ $effect(() => {
52
+ if (scrollContainer) {
53
+ const updateWithDelay = () => setTimeout(updateScrollState, 100);
54
+ updateWithDelay();
55
+
56
+ const handleResize = () => updateWithDelay();
57
+ window.addEventListener('resize', handleResize);
58
+
59
+ return () => window.removeEventListener('resize', handleResize);
60
+ }
61
+ });
62
+
63
+ $effect(() => {
64
+ if (!loading) {
65
+ setTimeout(updateScrollState, 200);
66
+ }
67
+ });
68
+
69
+ const hasHorizontalScroll = $derived(internalHasHorizontalScroll);
70
+
71
+ function calculateStickyLeft(column: Column, index: number): number {
72
+ let left = 0;
73
+ for (let i = 0; i < index; i++) {
74
+ const prevCol = columns[i];
75
+ if (prevCol.sticky === 'left') {
76
+ const width = prevCol.width || prevCol.minWidth || '160px';
77
+ const widthValue = parseFloat(width.toString().replace('px', '')) || 160;
78
+ left += widthValue;
79
+ }
80
+ }
81
+ if (column.stickyOffset) {
82
+ left += column.stickyOffset;
83
+ }
84
+ return left;
85
+ }
86
+
87
+ function getStickyStyle(column: Column, index: number): string {
88
+ if (!column.sticky) return '';
89
+ if (column.sticky === 'left') {
90
+ return `position: sticky; left: ${calculateStickyLeft(column, index)}px; z-index: 10;`;
91
+ }
92
+ if (column.sticky === 'right') {
93
+ return `position: sticky; right: 0; z-index: 10;`;
94
+ }
95
+ return '';
96
+ }
97
+
98
+ function getHeaderStickyStyle(column: Column, index: number): string {
99
+ if (!column.sticky) return '';
100
+ if (column.sticky === 'left') {
101
+ return `position: sticky; left: ${calculateStickyLeft(column, index)}px; z-index: 20;`;
102
+ }
103
+ if (column.sticky === 'right') {
104
+ return `position: sticky; right: 0; z-index: 20;`;
105
+ }
106
+ return '';
107
+ }
108
+ </script>
109
+
110
+ <div
111
+ class="overflow-x-auto overflow-y-auto mx-4 table-scroll-container"
112
+ style="height: {height}; min-height: {minHeight};"
113
+ bind:this={scrollContainer}
114
+ >
115
+ <table class="w-full border-collapse">
116
+ <thead class="bg-[#F8F9FA] border-b border-[#EAECF0] sticky top-0 z-30">
117
+ <tr>
118
+ {#each columns as column, index}
119
+ <th
120
+ class="px-4 py-3 text-left text-xs font-bold text-[#475467] whitespace-nowrap bg-[#F8F9FA] {column.sticky === 'left' && hasHorizontalScroll ? 'shadow-right' : ''} {column.sticky === 'right' && hasHorizontalScroll ? 'shadow-left' : ''} {column.headerClass || ''}"
121
+ style="
122
+ {column.width ? `width: ${column.width};` : ''}
123
+ {column.minWidth ? `min-width: ${column.minWidth};` : ''}
124
+ {column.maxWidth ? `max-width: ${column.maxWidth};` : ''}
125
+ {column.align === 'center' ? 'text-align: center;' : ''}
126
+ {column.align === 'right' ? 'text-align: right;' : ''}
127
+ {getHeaderStickyStyle(column, index)}
128
+ "
129
+ >
130
+ {column.label}
131
+ </th>
132
+ {/each}
133
+ </tr>
134
+ </thead>
135
+ <tbody>
136
+ {#if loading}
137
+ <TableShimmer rows={5} columns={columns.length} />
138
+ {:else if data.length === 0}
139
+ <tr>
140
+ <td colspan={columns.length} class="px-4 py-8 text-center text-sm text-[#98A2B3]">
141
+ {emptyMessage}
142
+ </td>
143
+ </tr>
144
+ {:else}
145
+ {#each data as row, rowIndex}
146
+ <tr
147
+ class="group border-b border-[#EAECF0] hover:bg-[#F9FAFB] {rowIndex % 2 === 0 ? 'bg-white' : 'bg-[#F8F9FA]'} {onRowClick !== undefined ? 'cursor-pointer' : ''}"
148
+ onclick={() => onRowClick?.(row, rowIndex)}
149
+ >
150
+ {#each columns as column, colIndex}
151
+ <td
152
+ class="px-4 py-3 text-sm text-[#2C2C30] whitespace-nowrap {column.sticky === 'left' && hasHorizontalScroll ? 'shadow-right' : ''} {column.sticky === 'right' && hasHorizontalScroll ? 'shadow-left' : ''} {rowIndex % 2 === 0 ? 'bg-white group-hover:bg-[#F9FAFB]' : 'bg-[#F8F9FA] group-hover:bg-[#F9FAFB]'} {column.cellClass || ''}"
153
+ style="
154
+ {column.width ? `width: ${column.width};` : ''}
155
+ {column.minWidth ? `min-width: ${column.minWidth};` : ''}
156
+ {column.maxWidth ? `max-width: ${column.maxWidth};` : ''}
157
+ {column.align === 'center' ? 'text-align: center;' : ''}
158
+ {column.align === 'right' ? 'text-align: right;' : ''}
159
+ {getStickyStyle(column, colIndex)}
160
+ {column.sticky ? `background-color: ${rowIndex % 2 === 0 ? 'white' : '#F8F9FA'};` : ''}
161
+ "
162
+ >
163
+ {#if column.actions && column.actions.length > 0}
164
+ <div class="flex items-center gap-2 {column.align === 'center' ? 'justify-center' : column.align === 'right' ? 'justify-end' : 'justify-start'}">
165
+ {#each column.actions as action}
166
+ {@const handleAction = () => {
167
+ const onClickHandler = column.onActions?.[action.actionKey];
168
+ const actionType = action.actionType || 'function';
169
+
170
+ if (actionType === 'function' && onClickHandler) {
171
+ onClickHandler(row, rowIndex);
172
+ } else if (actionType === 'alert' && action.actionValue) {
173
+ let message = action.actionValue;
174
+ // Replace placeholders like {id}, {name}, etc. with row data
175
+ message = message.replace(/\{(\w+)\}/g, (match, key) => {
176
+ return String(row[key] ?? rowIndex);
177
+ });
178
+ alert(message);
179
+ } else if ((actionType === 'dialog' || actionType === 'modal') && action.actionValue) {
180
+ // Dispatch custom event for dialog/modal
181
+ window.dispatchEvent(new CustomEvent('table-action-dialog', {
182
+ detail: {
183
+ dialogId: action.actionValue,
184
+ actionKey: action.actionKey,
185
+ row,
186
+ rowIndex
187
+ }
188
+ }));
189
+ } else if (actionType === 'navigate' && action.actionValue) {
190
+ let url = action.actionValue;
191
+ // Replace placeholders like {id}, {name}, etc. with row data
192
+ url = url.replace(/\{(\w+)\}/g, (match, key) => {
193
+ return String(row[key] ?? rowIndex);
194
+ });
195
+ window.location.href = url;
196
+ }
197
+ }}
198
+ <Icon
199
+ name={action.icon as any}
200
+ width={18}
201
+ height={18}
202
+ color={action.color || '#6B7280'}
203
+ action={handleAction}
204
+ style="cursor: pointer;"
205
+ />
206
+ {/each}
207
+ </div>
208
+ {:else if column.showBadge}
209
+ {@const badgeStatus = row[column.badgeKey || column.key] as DeviceStatus}
210
+ <div class="flex {column.align === 'center' ? 'justify-center' : column.align === 'right' ? 'justify-end' : 'justify-start'}">
211
+ <TableBadge status={badgeStatus} label={column.badgeLabel} />
212
+ </div>
213
+ {:else if column.render}
214
+ {@html String(column.render(row, rowIndex) ?? '-')}
215
+ {:else}
216
+ {row[column.key] ?? '-'}
217
+ {/if}
218
+ </td>
219
+ {/each}
220
+ </tr>
221
+ {/each}
222
+ {/if}
223
+ </tbody>
224
+ </table>
225
+ </div>
226
+
227
+ {#if showPagination}
228
+ <TablePagination
229
+ {currentPage}
230
+ {totalPages}
231
+ {itemsPerPage}
232
+ {totalItems}
233
+ {onPageChange}
234
+ {onItemsPerPageChange}
235
+ />
236
+ {/if}
237
+
238
+ <style>
239
+ .table-scroll-container {
240
+ scrollbar-width: thin;
241
+ scrollbar-color: rgba(152, 162, 179, 0.3) transparent;
242
+ }
243
+
244
+ .table-scroll-container::-webkit-scrollbar {
245
+ height: 6px;
246
+ width: 6px;
247
+ }
248
+
249
+ .table-scroll-container::-webkit-scrollbar-track {
250
+ background: transparent;
251
+ }
252
+
253
+ .table-scroll-container::-webkit-scrollbar-thumb {
254
+ background-color: rgba(152, 162, 179, 0.3);
255
+ border-radius: 3px;
256
+ }
257
+
258
+ .table-scroll-container::-webkit-scrollbar-thumb:hover {
259
+ background-color: rgba(80, 103, 142, 0.5);
260
+ }
261
+
262
+ .table-scroll-container::-webkit-scrollbar-corner {
263
+ background: transparent;
264
+ }
265
+
266
+ :global(.shadow-right) {
267
+ position: relative;
268
+ box-shadow: 2px 0 16px -2px rgba(0, 0, 0, 0.12) !important;
269
+ }
270
+
271
+ :global(.shadow-right::after) {
272
+ content: '';
273
+ position: absolute;
274
+ top: 0;
275
+ right: -12px;
276
+ width: 12px;
277
+ height: 100%;
278
+ background: linear-gradient(to right, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04), transparent);
279
+ pointer-events: none;
280
+ z-index: -1;
281
+ }
282
+
283
+ :global(.shadow-left) {
284
+ position: relative;
285
+ box-shadow: -2px 0 16px -2px rgba(0, 0, 0, 0.12) !important;
286
+ }
287
+
288
+ :global(.shadow-left::before) {
289
+ content: '';
290
+ position: absolute;
291
+ top: 0;
292
+ left: -12px;
293
+ width: 12px;
294
+ height: 100%;
295
+ background: linear-gradient(to left, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04), transparent);
296
+ pointer-events: none;
297
+ z-index: -1;
298
+ }
299
+ </style>
300
+
@@ -0,0 +1,21 @@
1
+ import type { Column } from './types.js';
2
+ type $$ComponentProps = {
3
+ columns?: Column[];
4
+ data?: any[];
5
+ loading?: boolean;
6
+ height?: string;
7
+ minHeight?: string;
8
+ onRowClick?: (row: any, index: number) => void;
9
+ showPagination?: boolean;
10
+ currentPage?: number;
11
+ totalPages?: number;
12
+ itemsPerPage?: number;
13
+ totalItems?: number;
14
+ onPageChange?: (page: number) => void;
15
+ onItemsPerPageChange?: (itemsPerPage: number) => void;
16
+ };
17
+ declare const Table: import("svelte").Component<$$ComponentProps, {
18
+ Column: typeof Column;
19
+ }, "">;
20
+ type Table = ReturnType<typeof Table>;
21
+ export default Table;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { DeviceStatus } from './types.js';
3
+
4
+ function getStatusBadgeClass(status: DeviceStatus): string {
5
+ switch (status) {
6
+ case 'Online':
7
+ return 'bg-[#C9FFE6] text-[#12B76A] border border-[#12B76A]';
8
+ case 'Offline':
9
+ return 'bg-[#FFE1E4] text-[#F04438] border border-[#F04438]';
10
+ case 'Delay':
11
+ return 'bg-[#FFF9E6] text-[#FFA000] border border-[#FFA000]';
12
+ case 'Maintenance':
13
+ return 'bg-[#E8ECF1] text-[#212529] border border-[#7F8286]';
14
+ default:
15
+ return 'bg-[#E8ECF1] text-[#212529] border border-[#7F8286]';
16
+ }
17
+ }
18
+
19
+ const { status, label }: { status: DeviceStatus; label?: string } = $props();
20
+ </script>
21
+
22
+ <span
23
+ class="inline-block rounded border px-2 text-sm font-normal leading-[23px] {getStatusBadgeClass(
24
+ status
25
+ )}"
26
+ >
27
+ {label || status}
28
+ </span>
29
+
@@ -0,0 +1,8 @@
1
+ import type { DeviceStatus } from './types.js';
2
+ type $$ComponentProps = {
3
+ status: DeviceStatus;
4
+ label?: string;
5
+ };
6
+ declare const TableBadge: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type TableBadge = ReturnType<typeof TableBadge>;
8
+ export default TableBadge;