mertani-web-toolkit 0.1.36 → 0.1.38

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.
@@ -3,7 +3,7 @@
3
3
  import TableBadge from './TableBadge.svelte';
4
4
  import TablePagination from './TablePagination.svelte';
5
5
  import Icon from '../Icon/Icon.svelte';
6
- import type { Column, DeviceStatus, BadgeStatusMapping } from './types.js';
6
+ import type { Column, BadgeStatusMapping } from './types.js';
7
7
 
8
8
  export type { Column } from './types';
9
9
 
@@ -39,11 +39,41 @@
39
39
  defaultBadgeStatusMappings?: BadgeStatusMapping[];
40
40
  } = $props();
41
41
 
42
- const emptyMessage = 'Tidak ada data';
42
+ const EMPTY_MESSAGE = 'Tidak ada data';
43
+ const HEADER_BASE_CLASS =
44
+ 'bg-bg-surface-subtle px-4 py-3 text-left text-xs font-bold whitespace-nowrap text-text-primary';
45
+ const CELL_BASE_CLASS =
46
+ 'px-4 py-3 text-sm whitespace-nowrap text-text-primary bg-bg-surface group-hover:bg-bg-surface-subtle';
47
+ const ROW_BASE_CLASS =
48
+ 'group border-b border-border-outline bg-bg-surface hover:bg-bg-surface-subtle';
43
49
 
44
50
  let scrollContainer: HTMLElement | null = $state(null);
45
51
  let internalHasHorizontalScroll = $state(false);
52
+ let isMobile = $state(false);
46
53
 
54
+ $effect(() => {
55
+ if (typeof window === 'undefined') return;
56
+
57
+ const mediaQuery = window.matchMedia('(max-width: 768px)');
58
+ isMobile = mediaQuery.matches;
59
+
60
+ const handleChange = (e: MediaQueryListEvent | MediaQueryList) => {
61
+ isMobile = e.matches;
62
+ };
63
+
64
+ // Modern browsers
65
+ if (mediaQuery.addEventListener) {
66
+ mediaQuery.addEventListener('change', handleChange);
67
+ return () => mediaQuery.removeEventListener('change', handleChange);
68
+ }
69
+ // Fallback for older browsers
70
+ else if (mediaQuery.addListener) {
71
+ mediaQuery.addListener(handleChange);
72
+ return () => mediaQuery.removeListener(handleChange);
73
+ }
74
+ });
75
+
76
+ // ========== SCROLL MANAGEMENT ==========
47
77
  function updateScrollState() {
48
78
  internalHasHorizontalScroll = scrollContainer
49
79
  ? scrollContainer.scrollWidth > scrollContainer.clientWidth
@@ -54,10 +84,8 @@
54
84
  if (scrollContainer) {
55
85
  const updateWithDelay = () => setTimeout(updateScrollState, 100);
56
86
  updateWithDelay();
57
-
58
87
  const handleResize = () => updateWithDelay();
59
88
  window.addEventListener('resize', handleResize);
60
-
61
89
  return () => window.removeEventListener('resize', handleResize);
62
90
  }
63
91
  });
@@ -70,55 +98,13 @@
70
98
 
71
99
  const hasHorizontalScroll = $derived(internalHasHorizontalScroll);
72
100
 
73
- function calculateStickyLeft(column: Column, index: number, useFlatColumns: boolean = false): number {
74
- let left = 0;
75
- const colsToUse = useFlatColumns ? flatColumns : columns;
76
-
77
- for (let i = 0; i < index; i++) {
78
- const prevCol = colsToUse[i];
79
- if (prevCol.sticky === 'left') {
80
- const width = prevCol.width || prevCol.minWidth || '160px';
81
- const widthValue = parseFloat(width.toString().replace('px', '')) || 160;
82
- left += widthValue;
83
- }
84
- }
85
- if (column.stickyOffset) {
86
- left += column.stickyOffset;
87
- }
88
- return left;
89
- }
90
-
91
- function getStickyStyle(column: Column, index: number, useFlatColumns: boolean = false): string {
92
- if (!column.sticky) return '';
93
- if (column.sticky === 'left') {
94
- return `position: sticky; left: ${calculateStickyLeft(column, index, useFlatColumns)}px; z-index: 10;`;
95
- }
96
- if (column.sticky === 'right') {
97
- return `position: sticky; right: 0; z-index: 10;`;
98
- }
99
- return '';
100
- }
101
-
102
- function getHeaderStickyStyle(column: Column, index: number): string {
103
- if (!column.sticky) return '';
104
- if (column.sticky === 'left') {
105
- return `position: sticky; left: ${calculateStickyLeft(column, index)}px; z-index: 20;`;
106
- }
107
- if (column.sticky === 'right') {
108
- return `position: sticky; right: 0; z-index: 20;`;
109
- }
110
- return '';
111
- }
112
-
113
- // Flatten columns to get all leaf columns (for body rendering)
114
- // Also preserves parent borderRight info for child columns
101
+ // ========== COLUMN UTILITIES ==========
115
102
  function flattenColumns(cols: Column[], parentBorderRight?: boolean): Column[] {
116
103
  const result: Column[] = [];
117
104
  for (const col of cols) {
118
105
  if (col.children && col.children.length > 0) {
119
106
  const childBorderRight = col.borderRight || parentBorderRight;
120
107
  const children = flattenColumns(col.children, childBorderRight);
121
- // Mark last child if parent has borderRight
122
108
  if (childBorderRight && children.length > 0) {
123
109
  children[children.length - 1] = { ...children[children.length - 1], borderRight: true };
124
110
  }
@@ -130,7 +116,6 @@
130
116
  return result;
131
117
  }
132
118
 
133
- // Get total column count (including children)
134
119
  function getTotalColumnCount(cols: Column[]): number {
135
120
  let count = 0;
136
121
  for (const col of cols) {
@@ -143,23 +128,152 @@
143
128
  return count;
144
129
  }
145
130
 
146
- // Check if any column has children
147
- const hasNestedColumns = $derived(columns.some(col => col.children && col.children.length > 0));
148
-
149
- // Flattened columns for body rendering
131
+ const hasNestedColumns = $derived(columns.some((col) => col.children && col.children.length > 0));
150
132
  const flatColumns = $derived(flattenColumns(columns, false));
151
-
152
- // Total column count for colspan
153
133
  const totalColumnCount = $derived(getTotalColumnCount(columns));
134
+
135
+ // ========== STICKY COLUMN UTILITIES ==========
136
+ function parseWidth(width: string | undefined): number {
137
+ if (!width) return 160;
138
+ return parseFloat(width.toString().replace('px', '')) || 160;
139
+ }
140
+
141
+ function calculateStickyLeft(
142
+ column: Column,
143
+ index: number,
144
+ useFlatColumns: boolean = false
145
+ ): number {
146
+ let left = 0;
147
+ const colsToUse = useFlatColumns ? flatColumns : columns;
148
+
149
+ for (let i = 0; i < index; i++) {
150
+ const prevCol = colsToUse[i];
151
+ if (prevCol.sticky === 'left') {
152
+ left += parseWidth(prevCol.width || prevCol.minWidth);
153
+ }
154
+ }
155
+ if (column.stickyOffset) {
156
+ left += column.stickyOffset;
157
+ }
158
+ return left;
159
+ }
160
+
161
+ function getStickyStyle(
162
+ column: Column,
163
+ index: number,
164
+ useFlatColumns: boolean = false,
165
+ zIndex: number = 10
166
+ ): string {
167
+ // Disable sticky on mobile
168
+ if (isMobile || !column.sticky) return '';
169
+ if (column.sticky === 'left') {
170
+ return `position: sticky; left: ${calculateStickyLeft(column, index, useFlatColumns)}px; z-index: ${zIndex};`;
171
+ }
172
+ if (column.sticky === 'right') {
173
+ return `position: sticky; right: 0; z-index: ${zIndex};`;
174
+ }
175
+ return '';
176
+ }
177
+
178
+ function getHeaderStickyStyle(column: Column, index: number): string {
179
+ return getStickyStyle(column, index, false, 20);
180
+ }
181
+
182
+ // ========== STYLE UTILITIES ==========
183
+ function getColumnStyles(column: Column): string {
184
+ const styles: string[] = [];
185
+ if (column.width) styles.push(`width: ${column.width};`);
186
+ if (column.minWidth) styles.push(`min-width: ${column.minWidth};`);
187
+ if (column.maxWidth) styles.push(`max-width: ${column.maxWidth};`);
188
+ if (column.align === 'center') styles.push('text-align: center;');
189
+ if (column.align === 'right') styles.push('text-align: right;');
190
+ return styles.join(' ');
191
+ }
192
+
193
+ function getHeaderClasses(column: Column): string {
194
+ const classes: string[] = [HEADER_BASE_CLASS];
195
+ if (column.borderRight) classes.push('border-r border-border-outline');
196
+ if (column.headerClass) classes.push(column.headerClass);
197
+ return classes.join(' ');
198
+ }
199
+
200
+ function getHeaderShadow(column: Column): string | null {
201
+ if (isMobile) return null;
202
+ if (column.sticky === 'left' && hasHorizontalScroll) return 'right';
203
+ if (column.sticky === 'right' && hasHorizontalScroll) return 'left';
204
+ return null;
205
+ }
206
+
207
+ function getCellClasses(column: Column): string {
208
+ const classes: string[] = [CELL_BASE_CLASS];
209
+ if (column.borderRight) classes.push('border-r border-border-outline');
210
+ if (column.cellClass) classes.push(column.cellClass);
211
+ return classes.join(' ');
212
+ }
213
+
214
+ function getCellShadow(column: Column): string | null {
215
+ if (isMobile) return null;
216
+ if (column.sticky === 'left' && hasHorizontalScroll) return 'right';
217
+ if (column.sticky === 'right' && hasHorizontalScroll) return 'left';
218
+ return null;
219
+ }
220
+
221
+ function getRowClasses(): string {
222
+ const classes: string[] = [ROW_BASE_CLASS];
223
+ if (onRowClick !== undefined) classes.push('cursor-pointer');
224
+ return classes.join(' ');
225
+ }
226
+
227
+ function getAlignClasses(align?: 'left' | 'center' | 'right'): string {
228
+ if (align === 'center') return 'justify-center';
229
+ if (align === 'right') return 'justify-end';
230
+ return 'justify-start';
231
+ }
232
+
233
+ // ========== ACTION HANDLERS ==========
234
+ function replacePlaceholders(template: string, row: any, rowIndex: number): string {
235
+ return template.replace(/\{(\w+)\}/g, (match, key) => String(row[key] ?? rowIndex));
236
+ }
237
+
238
+ function handleAction(action: any, column: Column, row: any, rowIndex: number) {
239
+ const onClickHandler = column.onActions?.[action.actionKey];
240
+ const actionType = action.actionType || 'function';
241
+
242
+ switch (actionType) {
243
+ case 'function':
244
+ if (onClickHandler) onClickHandler(row, rowIndex);
245
+ break;
246
+ case 'alert':
247
+ if (action.actionValue) {
248
+ alert(replacePlaceholders(action.actionValue, row, rowIndex));
249
+ }
250
+ break;
251
+ case 'dialog':
252
+ case 'modal':
253
+ if (action.actionValue) {
254
+ window.dispatchEvent(
255
+ new CustomEvent('table-action-dialog', {
256
+ detail: { dialogId: action.actionValue, actionKey: action.actionKey, row, rowIndex }
257
+ })
258
+ );
259
+ }
260
+ break;
261
+ case 'navigate':
262
+ if (action.actionValue) {
263
+ window.location.href = replacePlaceholders(action.actionValue, row, rowIndex);
264
+ }
265
+ break;
266
+ }
267
+ }
154
268
  </script>
155
269
 
156
270
  <div
157
- class="overflow-x-auto overflow-y-auto mx-4 table-scroll-container"
271
+ class="table-scroll-container mx-4 overflow-x-auto overflow-y-auto"
158
272
  style="height: {height}; min-height: {minHeight};"
159
273
  bind:this={scrollContainer}
160
274
  >
161
275
  <table class="w-full border-collapse">
162
- <thead class="bg-[#F8F9FA] border-b border-[#EAECF0] sticky top-0 z-30">
276
+ <thead class="sticky top-0 z-30">
163
277
  {#if hasNestedColumns}
164
278
  <!-- Parent header row -->
165
279
  <tr>
@@ -168,15 +282,9 @@
168
282
  <th
169
283
  colspan={childCount}
170
284
  rowspan={column.children && column.children.length > 0 ? 1 : 2}
171
- 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.borderRight ? 'border-r border-[#EAECF0]' : ''} {column.headerClass || ''}"
172
- style="
173
- {column.width ? `width: ${column.width};` : ''}
174
- {column.minWidth ? `min-width: ${column.minWidth};` : ''}
175
- {column.maxWidth ? `max-width: ${column.maxWidth};` : ''}
176
- {column.align === 'center' ? 'text-align: center;' : ''}
177
- {column.align === 'right' ? 'text-align: right;' : ''}
178
- {getHeaderStickyStyle(column, index)}
179
- "
285
+ class={getHeaderClasses(column)}
286
+ data-shadow={getHeaderShadow(column)}
287
+ style="{getColumnStyles(column)} {getHeaderStickyStyle(column, index)}"
180
288
  >
181
289
  {#if loading}
182
290
  <div class="shimmer-skeleton h-4 w-24"></div>
@@ -192,16 +300,18 @@
192
300
  {#if column.children && column.children.length > 0}
193
301
  {#each column.children as childColumn, childIndex}
194
302
  {@const isLastChild = childIndex === column.children.length - 1}
195
- {@const shouldShowBorder = childColumn.borderRight || (column.borderRight && isLastChild)}
303
+ {@const shouldShowBorder =
304
+ childColumn.borderRight || (column.borderRight && isLastChild)}
196
305
  <th
197
- class="px-4 py-3 text-left text-xs font-bold text-[#475467] whitespace-nowrap bg-[#F8F9FA] {childColumn.sticky === 'left' && hasHorizontalScroll ? 'shadow-right' : ''} {childColumn.sticky === 'right' && hasHorizontalScroll ? 'shadow-left' : ''} {shouldShowBorder ? 'border-r border-[#EAECF0]' : ''} {childColumn.headerClass || ''}"
198
- style="
199
- {childColumn.width ? `width: ${childColumn.width};` : ''}
200
- {childColumn.minWidth ? `min-width: ${childColumn.minWidth};` : ''}
201
- {childColumn.maxWidth ? `max-width: ${childColumn.maxWidth};` : ''}
202
- {childColumn.align === 'center' ? 'text-align: center;' : ''}
203
- {childColumn.align === 'right' ? 'text-align: right;' : ''}
204
- "
306
+ class="{HEADER_BASE_CLASS} {shouldShowBorder
307
+ ? 'border-r border-border-outline'
308
+ : ''} {childColumn.headerClass || ''}"
309
+ data-shadow={!isMobile && childColumn.sticky === 'left' && hasHorizontalScroll
310
+ ? 'right'
311
+ : !isMobile && childColumn.sticky === 'right' && hasHorizontalScroll
312
+ ? 'left'
313
+ : null}
314
+ style={getColumnStyles(childColumn)}
205
315
  >
206
316
  {#if loading}
207
317
  <div class="shimmer-skeleton h-4 w-24"></div>
@@ -214,19 +324,13 @@
214
324
  {/each}
215
325
  </tr>
216
326
  {:else}
217
- <!-- Simple header row (no nested columns) -->
327
+ <!-- Simple header row -->
218
328
  <tr>
219
329
  {#each columns as column, index}
220
330
  <th
221
- 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.borderRight ? 'border-r border-[#EAECF0]' : ''} {column.headerClass || ''}"
222
- style="
223
- {column.width ? `width: ${column.width};` : ''}
224
- {column.minWidth ? `min-width: ${column.minWidth};` : ''}
225
- {column.maxWidth ? `max-width: ${column.maxWidth};` : ''}
226
- {column.align === 'center' ? 'text-align: center;' : ''}
227
- {column.align === 'right' ? 'text-align: right;' : ''}
228
- {getHeaderStickyStyle(column, index)}
229
- "
331
+ class={getHeaderClasses(column)}
332
+ data-shadow={getHeaderShadow(column)}
333
+ style="{getColumnStyles(column)} {getHeaderStickyStyle(column, index)}"
230
334
  >
231
335
  {#if loading}
232
336
  <div class="shimmer-skeleton h-4 w-24"></div>
@@ -243,70 +347,28 @@
243
347
  <TableShimmer rows={5} columns={totalColumnCount} />
244
348
  {:else if data.length === 0}
245
349
  <tr>
246
- <td colspan={totalColumnCount} class="px-4 py-8 text-center text-sm text-[#98A2B3]">
247
- {emptyMessage}
350
+ <td colspan={totalColumnCount} class="px-4 py-8 text-center text-sm text-text-tertiary">
351
+ {EMPTY_MESSAGE}
248
352
  </td>
249
353
  </tr>
250
354
  {:else}
251
355
  {#each data as row, rowIndex}
252
- <tr
253
- class="group border-b border-[#EAECF0] hover:bg-[#F9FAFB] {rowIndex % 2 === 0 ? 'bg-white' : 'bg-[#F8F9FA]'} {onRowClick !== undefined ? 'cursor-pointer' : ''}"
254
- onclick={() => onRowClick?.(row, rowIndex)}
255
- >
356
+ <tr class={getRowClasses()} onclick={() => onRowClick?.(row, rowIndex)}>
256
357
  {#each flatColumns as column, colIndex}
257
358
  <td
258
- class="px-4 py-3 text-sm text-[#2C2C30] whitespace-nowrap {column.sticky === 'left' && hasHorizontalScroll ? 'shadow-right' : ''} {column.sticky === 'right' && hasHorizontalScroll ? 'shadow-left' : ''} {column.borderRight ? 'border-r border-[#EAECF0]' : ''} {rowIndex % 2 === 0 ? 'bg-white group-hover:bg-[#F9FAFB]' : 'bg-[#F8F9FA] group-hover:bg-[#F9FAFB]'} {column.cellClass || ''}"
259
- style="
260
- {column.width ? `width: ${column.width};` : ''}
261
- {column.minWidth ? `min-width: ${column.minWidth};` : ''}
262
- {column.maxWidth ? `max-width: ${column.maxWidth};` : ''}
263
- {column.align === 'center' ? 'text-align: center;' : ''}
264
- {column.align === 'right' ? 'text-align: right;' : ''}
265
- {getStickyStyle(column, colIndex, true)}
266
- {column.sticky ? `background-color: ${rowIndex % 2 === 0 ? 'white' : '#F8F9FA'};` : ''}
267
- "
359
+ class={getCellClasses(column)}
360
+ data-shadow={getCellShadow(column)}
361
+ style="{getColumnStyles(column)} {getStickyStyle(column, colIndex, true)}"
268
362
  >
269
363
  {#if column.actions && column.actions.length > 0}
270
- <div class="flex items-center gap-2 {column.align === 'center' ? 'justify-center' : column.align === 'right' ? 'justify-end' : 'justify-start'}">
364
+ <div class="flex items-center gap-2 {getAlignClasses(column.align)}">
271
365
  {#each column.actions as action}
272
- {@const handleAction = () => {
273
- const onClickHandler = column.onActions?.[action.actionKey];
274
- const actionType = action.actionType || 'function';
275
-
276
- if (actionType === 'function' && onClickHandler) {
277
- onClickHandler(row, rowIndex);
278
- } else if (actionType === 'alert' && action.actionValue) {
279
- let message = action.actionValue;
280
- // Replace placeholders like {id}, {name}, etc. with row data
281
- message = message.replace(/\{(\w+)\}/g, (match, key) => {
282
- return String(row[key] ?? rowIndex);
283
- });
284
- alert(message);
285
- } else if ((actionType === 'dialog' || actionType === 'modal') && action.actionValue) {
286
- // Dispatch custom event for dialog/modal
287
- window.dispatchEvent(new CustomEvent('table-action-dialog', {
288
- detail: {
289
- dialogId: action.actionValue,
290
- actionKey: action.actionKey,
291
- row,
292
- rowIndex
293
- }
294
- }));
295
- } else if (actionType === 'navigate' && action.actionValue) {
296
- let url = action.actionValue;
297
- // Replace placeholders like {id}, {name}, etc. with row data
298
- url = url.replace(/\{(\w+)\}/g, (match, key) => {
299
- return String(row[key] ?? rowIndex);
300
- });
301
- window.location.href = url;
302
- }
303
- }}
304
366
  <Icon
305
367
  name={action.icon as any}
306
368
  width={18}
307
369
  height={18}
308
- color={action.color || '#6B7280'}
309
- onclick={handleAction}
370
+ color={action.color || 'text-text-primary'}
371
+ onclick={() => handleAction(action, column, row, rowIndex)}
310
372
  style="cursor: pointer;"
311
373
  />
312
374
  {/each}
@@ -315,7 +377,7 @@
315
377
  {@const badgeStatus = String(row[column.badgeKey || column.key || ''] || '')}
316
378
  {@const badgeMappings = column.badgeStatusMappings || defaultBadgeStatusMappings}
317
379
  {#if badgeMappings && badgeMappings.length > 0}
318
- <div class="flex {column.align === 'center' ? 'justify-center' : column.align === 'right' ? 'justify-end' : 'justify-start'}">
380
+ <div class="flex {getAlignClasses(column.align)}">
319
381
  <TableBadge
320
382
  status={badgeStatus}
321
383
  label={column.badgeLabel}
@@ -323,8 +385,10 @@
323
385
  />
324
386
  </div>
325
387
  {:else}
326
- <div class="flex {column.align === 'center' ? 'justify-center' : column.align === 'right' ? 'justify-end' : 'justify-start'}">
327
- <span class="inline-block rounded border px-2 text-sm font-normal leading-[23px] bg-[#E8ECF1] text-[#212529] border-[#7F8286]">
388
+ <div class="flex {getAlignClasses(column.align)}">
389
+ <span
390
+ class="inline-block rounded border border-border-divider-base bg-bg-surface-raised px-2 text-sm leading-[23px] font-normal text-text-primary"
391
+ >
328
392
  {column.badgeLabel || badgeStatus}
329
393
  </span>
330
394
  </div>
@@ -355,63 +419,46 @@
355
419
  {/if}
356
420
 
357
421
  <style>
358
- .table-scroll-container {
359
- scrollbar-width: thin;
360
- scrollbar-color: rgba(152, 162, 179, 0.3) transparent;
361
- }
362
-
363
- .table-scroll-container::-webkit-scrollbar {
364
- height: 6px;
365
- width: 6px;
366
- }
367
-
368
- .table-scroll-container::-webkit-scrollbar-track {
369
- background: transparent;
370
- }
371
-
372
- .table-scroll-container::-webkit-scrollbar-thumb {
373
- background-color: rgba(152, 162, 179, 0.3);
374
- border-radius: 3px;
375
- }
376
-
377
- .table-scroll-container::-webkit-scrollbar-thumb:hover {
378
- background-color: rgba(80, 103, 142, 0.5);
379
- }
380
-
381
- .table-scroll-container::-webkit-scrollbar-corner {
382
- background: transparent;
383
- }
384
-
385
- :global(.shadow-right) {
422
+ [data-shadow='right'] {
386
423
  position: relative;
387
- box-shadow: 2px 0 16px -2px rgba(0, 0, 0, 0.12) !important;
424
+ box-shadow: 2px 0 16px -2px color-mix(in srgb, var(--color-text-primary) 10%, transparent) !important;
388
425
  }
389
426
 
390
- :global(.shadow-right::after) {
427
+ [data-shadow='right']::after {
391
428
  content: '';
392
429
  position: absolute;
393
430
  top: 0;
394
431
  right: -12px;
395
432
  width: 12px;
396
433
  height: 100%;
397
- background: linear-gradient(to right, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04), transparent);
434
+ background: linear-gradient(
435
+ to right,
436
+ color-mix(in srgb, var(--color-text-primary) 8%, transparent),
437
+ color-mix(in srgb, var(--color-text-primary) 4%, transparent),
438
+ transparent
439
+ );
398
440
  pointer-events: none;
399
441
  z-index: -1;
400
442
  }
401
443
 
402
- :global(.shadow-left) {
444
+ [data-shadow='left'] {
403
445
  position: relative;
404
- box-shadow: -2px 0 16px -2px rgba(0, 0, 0, 0.12) !important;
446
+ box-shadow: -2px 0 16px -2px color-mix(in srgb, var(--color-text-primary) 10%, transparent) !important;
405
447
  }
406
448
 
407
- :global(.shadow-left::before) {
449
+ [data-shadow='left']::before {
408
450
  content: '';
409
451
  position: absolute;
410
452
  top: 0;
411
453
  left: -12px;
412
454
  width: 12px;
413
455
  height: 100%;
414
- background: linear-gradient(to left, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04), transparent);
456
+ background: linear-gradient(
457
+ to left,
458
+ color-mix(in srgb, var(--color-text-primary) 8%, transparent),
459
+ color-mix(in srgb, var(--color-text-primary) 4%, transparent),
460
+ transparent
461
+ );
415
462
  pointer-events: none;
416
463
  z-index: -1;
417
464
  }
@@ -420,7 +467,7 @@
420
467
  position: relative;
421
468
  overflow: hidden;
422
469
  border-radius: 4px;
423
- background-color: #e7e5e5;
470
+ background-color: var(--color-bg-surface-subtle);
424
471
  }
425
472
 
426
473
  .shimmer-skeleton::after {
@@ -432,10 +479,10 @@
432
479
  transform: translateX(-100%);
433
480
  background-image: linear-gradient(
434
481
  90deg,
435
- rgba(255, 255, 255, 0) 0,
436
- rgba(255, 255, 255, 0.2) 20%,
437
- rgba(255, 255, 255, 0.5) 80%,
438
- rgba(255, 255, 255, 0) 100%
482
+ color-mix(in srgb, var(--color-text-primary) 0%, transparent) 0,
483
+ color-mix(in srgb, var(--color-text-primary) 20%, transparent) 20%,
484
+ color-mix(in srgb, var(--color-text-primary) 50%, transparent) 80%,
485
+ color-mix(in srgb, var(--color-text-primary) 100%, transparent) 100%
439
486
  );
440
487
  animation: shimmer 3s infinite;
441
488
  content: '';
@@ -447,4 +494,3 @@
447
494
  }
448
495
  }
449
496
  </style>
450
-
@@ -5,21 +5,21 @@
5
5
  function getVariantClass(variant: BadgeVariant): string {
6
6
  switch (variant) {
7
7
  case 'success':
8
- return 'bg-[#C9FFE6] text-[#12B76A] border border-[#12B76A]';
8
+ return 'bg-bg-success-subtle text-text-success-ti border border-border-success-bor';
9
9
  case 'error':
10
- return 'bg-[#FFE1E4] text-[#F04438] border border-[#F04438]';
10
+ return 'bg-bg-error-subtle text-text-error-ti border border-border-error-bor';
11
11
  case 'warning':
12
- return 'bg-[#FFF9E6] text-bg-act-primary border border-bg-act-primary';
12
+ return 'bg-bg-warning-subtle text-text-warning-ti border border-border-warning-bor';
13
13
  case 'info':
14
- return 'bg-[#E0F2FE] text-[#0284C7] border border-[#0284C7]';
14
+ return 'bg-bg-info-subtle text-text-info-ti border border-border-info-bor';
15
15
  case 'neutral':
16
16
  default:
17
- return 'bg-[#E8ECF1] text-[#212529] border border-[#7F8286]';
17
+ return 'bg-bg-surface-subtle text-text-secondary border border-border-divider-base';
18
18
  }
19
19
  }
20
20
 
21
21
  function getStatusBadgeClass(status: string, mappings: BadgeStatusMapping[]): string {
22
- const mapping = mappings.find(m => m.status === status);
22
+ const mapping = mappings.find((m) => m.status === status);
23
23
  const variant = mapping?.variant || 'neutral';
24
24
  return getVariantClass(variant);
25
25
  }
@@ -36,11 +36,10 @@
36
36
  </script>
37
37
 
38
38
  <span
39
- class="inline-block rounded border px-2 text-sm font-normal leading-[23px] {getStatusBadgeClass(
39
+ class="inline-block rounded border px-2 text-sm leading-[23px] font-normal {getStatusBadgeClass(
40
40
  status,
41
41
  statusMappings
42
42
  )}"
43
43
  >
44
44
  {label || status}
45
45
  </span>
46
-
@@ -16,6 +16,30 @@
16
16
  } = $props();
17
17
 
18
18
  const limitValues = [10, 25, 50, 100, 200];
19
+ let isMobile = $state(false);
20
+
21
+ // ========== MOBILE DETECTION ==========
22
+ $effect(() => {
23
+ if (typeof window === 'undefined') return;
24
+
25
+ const mediaQuery = window.matchMedia('(max-width: 768px)');
26
+ isMobile = mediaQuery.matches;
27
+
28
+ const handleChange = (e: MediaQueryListEvent | MediaQueryList) => {
29
+ isMobile = e.matches;
30
+ };
31
+
32
+ // Modern browsers
33
+ if (mediaQuery.addEventListener) {
34
+ mediaQuery.addEventListener('change', handleChange);
35
+ return () => mediaQuery.removeEventListener('change', handleChange);
36
+ }
37
+ // Fallback for older browsers
38
+ else if (mediaQuery.addListener) {
39
+ mediaQuery.addListener(handleChange);
40
+ return () => mediaQuery.removeListener(handleChange);
41
+ }
42
+ });
19
43
 
20
44
  function handlePrevPage() {
21
45
  if (currentPage > 1 && onPageChange) {
@@ -42,19 +66,19 @@
42
66
  </script>
43
67
 
44
68
  {#if shouldShow}
45
- <div class="pagination">
69
+ <div class="pagination" class:mobile={isMobile}>
46
70
  <div class="limit">
47
71
  <p>Tampilkan</p>
48
72
  <select
49
73
  value={itemsPerPage.toString()}
50
74
  onchange={handleItemsPerPageChange}
51
- class="items-per-page-select"
75
+ class="items-per-page-select text-text-primary"
52
76
  >
53
77
  {#each limitValues as value}
54
- <option value={value.toString()}>{value}</option>
78
+ <option value={value.toString()} class="text-text-primary">{value}</option>
55
79
  {/each}
56
80
  </select>
57
- <p>{currentPage} dari {totalPages} Halaman</p>
81
+ <p class="page-info">{currentPage} dari {totalPages} Halaman</p>
58
82
  </div>
59
83
  <div class="navigation">
60
84
  <button
@@ -64,7 +88,13 @@
64
88
  disabled={currentPage <= 1}
65
89
  aria-label="Halaman Sebelumnya"
66
90
  >
67
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
91
+ <svg
92
+ width="16"
93
+ height="16"
94
+ viewBox="0 0 16 16"
95
+ fill="none"
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ >
68
98
  <path
69
99
  d="M10 12L6 8L10 4"
70
100
  stroke="currentColor"
@@ -81,7 +111,13 @@
81
111
  disabled={currentPage >= totalPages}
82
112
  aria-label="Halaman Selanjutnya"
83
113
  >
84
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
114
+ <svg
115
+ width="16"
116
+ height="16"
117
+ viewBox="0 0 16 16"
118
+ fill="none"
119
+ xmlns="http://www.w3.org/2000/svg"
120
+ >
85
121
  <path
86
122
  d="M6 4L10 8L6 12"
87
123
  stroke="currentColor"
@@ -99,14 +135,24 @@
99
135
  .pagination {
100
136
  position: sticky;
101
137
  bottom: 0;
102
- background-color: white;
138
+ background-color: var(--color-bg-surface);
103
139
  z-index: 30;
104
140
  padding: 8px 16px;
105
- border-top: 1px solid #ccc;
141
+ border-top: 1px solid var(--color-border-outline);
106
142
  display: flex;
107
143
  justify-content: space-between;
108
144
  align-items: center;
109
145
  margin: 0 16px;
146
+ width: 100%;
147
+ box-sizing: border-box;
148
+ max-width: 100%;
149
+ overflow: hidden;
150
+ }
151
+
152
+ .pagination.mobile {
153
+ flex-direction: column;
154
+ gap: 12px;
155
+ padding: 12px 16px;
110
156
  }
111
157
 
112
158
  .limit {
@@ -116,23 +162,47 @@
116
162
  gap: 12px;
117
163
  position: relative;
118
164
  z-index: 31;
165
+ flex-wrap: wrap;
166
+ min-width: 0;
167
+ flex: 1;
168
+ }
169
+
170
+ .pagination.mobile .limit {
171
+ width: 100%;
172
+ justify-content: center;
173
+ gap: 8px;
119
174
  }
120
175
 
121
176
  .limit p {
122
177
  font-size: 0.875rem;
123
- color: #2c2c30;
178
+ color: var(--color-text-primary);
124
179
  margin: 0;
180
+ white-space: nowrap;
181
+ }
182
+
183
+ .pagination.mobile .limit p {
184
+ font-size: 0.8125rem;
185
+ }
186
+
187
+ .page-info {
188
+ white-space: nowrap;
125
189
  }
126
190
 
127
191
  .items-per-page-select {
128
192
  padding: 4px 8px;
129
- border: 1px solid #e4e7ec;
193
+ border: 1px solid var(--color-border-divider-base);
130
194
  border-radius: 6px;
131
195
  font-size: 0.875rem;
132
- color: #2c2c30;
133
- background-color: white;
196
+ color: var(--color-text-primary);
197
+ background-color: var(--color-bg-surface);
134
198
  cursor: pointer;
135
199
  min-width: 64px;
200
+ flex-shrink: 0;
201
+ }
202
+
203
+ .pagination.mobile .items-per-page-select {
204
+ font-size: 0.8125rem;
205
+ padding: 6px 8px;
136
206
  }
137
207
 
138
208
  .items-per-page-select:focus {
@@ -144,6 +214,13 @@
144
214
  .navigation {
145
215
  display: flex;
146
216
  gap: 12px;
217
+ flex-shrink: 0;
218
+ }
219
+
220
+ .pagination.mobile .navigation {
221
+ width: 100%;
222
+ justify-content: center;
223
+ gap: 16px;
147
224
  }
148
225
 
149
226
  .nav-button {
@@ -152,12 +229,18 @@
152
229
  justify-content: center;
153
230
  height: 28px;
154
231
  width: 28px;
155
- color: #a3a6b7;
232
+ color: var(--color-text-secondary);
156
233
  cursor: default;
157
234
  border-radius: 6px;
158
235
  border: none;
159
236
  background: transparent;
160
237
  padding: 0;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ .pagination.mobile .nav-button {
242
+ height: 32px;
243
+ width: 32px;
161
244
  }
162
245
 
163
246
  .nav-button.active {
@@ -172,7 +255,28 @@
172
255
  }
173
256
 
174
257
  .nav-button:not(:disabled):hover.active {
175
- background-color: rgba(255, 160, 0, 0.1);
258
+ background-color: color-mix(in srgb, var(--color-bg-act-primary) 10%, transparent);
176
259
  }
177
- </style>
178
260
 
261
+ /* Mobile responsive adjustments */
262
+ @media (max-width: 768px) {
263
+ .pagination {
264
+ margin: 0;
265
+ padding: 12px 8px;
266
+ }
267
+
268
+ .limit {
269
+ gap: 6px;
270
+ }
271
+
272
+ .limit p {
273
+ font-size: 0.75rem;
274
+ }
275
+
276
+ .items-per-page-select {
277
+ font-size: 0.75rem;
278
+ padding: 6px 6px;
279
+ min-width: 56px;
280
+ }
281
+ }
282
+ </style>
@@ -3,7 +3,7 @@
3
3
  </script>
4
4
 
5
5
  {#each { length: rows } as _}
6
- <tr class="border-b border-[#EAECF0]">
6
+ <tr class="border-b border-border-divider-subtle">
7
7
  {#each { length: columns } as _}
8
8
  <td class="px-4 py-3">
9
9
  <div class="shimmer-skeleton h-4 w-24"></div>
@@ -17,7 +17,7 @@
17
17
  position: relative;
18
18
  overflow: hidden;
19
19
  border-radius: 4px;
20
- background-color: #e7e5e5;
20
+ background-color: var(--color-bg-surface-subtle);
21
21
  }
22
22
 
23
23
  .shimmer-skeleton::after {
@@ -29,10 +29,10 @@
29
29
  transform: translateX(-100%);
30
30
  background-image: linear-gradient(
31
31
  90deg,
32
- rgba(255, 255, 255, 0) 0,
33
- rgba(255, 255, 255, 0.2) 20%,
34
- rgba(255, 255, 255, 0.5) 80%,
35
- rgba(255, 255, 255, 0) 100%
32
+ color-mix(in srgb, var(--color-bg-surface-subtle) 0%, transparent) 0,
33
+ color-mix(in srgb, var(--color-bg-surface-subtle) 20%, transparent) 20%,
34
+ color-mix(in srgb, var(--color-bg-surface-subtle) 50%, transparent) 80%,
35
+ color-mix(in srgb, var(--color-bg-surface-subtle) 100%, transparent) 100%
36
36
  );
37
37
  animation: shimmer 3s infinite;
38
38
  content: '';
@@ -44,4 +44,3 @@
44
44
  }
45
45
  }
46
46
  </style>
47
-
@@ -12,7 +12,7 @@ export type TableAction = {
12
12
  actionType?: 'function' | 'alert' | 'dialog' | 'navigate' | 'modal';
13
13
  actionValue?: string;
14
14
  };
15
- export type Column<T = any> = {
15
+ export type Column<T = unknown> = {
16
16
  key?: string;
17
17
  label: string;
18
18
  width?: string;
@@ -21,7 +21,7 @@ export type Column<T = any> = {
21
21
  sticky?: 'left' | 'right';
22
22
  stickyOffset?: number;
23
23
  align?: 'left' | 'center' | 'right';
24
- render?: (row: T, index: number) => string | number | null | undefined | any;
24
+ render?: (row: T, index: number) => string | number | null | undefined | unknown;
25
25
  headerClass?: string;
26
26
  cellClass?: string;
27
27
  showBadge?: boolean;
@@ -0,0 +1,4 @@
1
+ export declare const isDarkMode: import("svelte/store").Writable<boolean>;
2
+ export declare function setDarkMode(value: boolean): void;
3
+ export declare function toggleDarkMode(): void;
4
+ export declare function initTheme(): void;
@@ -0,0 +1,55 @@
1
+ import { writable } from 'svelte/store';
2
+ export const isDarkMode = writable(false);
3
+ export function setDarkMode(value) {
4
+ if (typeof document !== 'undefined') {
5
+ if (value) {
6
+ document.documentElement.setAttribute('data-theme', 'dark');
7
+ localStorage.setItem('theme', 'dark');
8
+ }
9
+ else {
10
+ document.documentElement.removeAttribute('data-theme');
11
+ localStorage.setItem('theme', 'light');
12
+ }
13
+ }
14
+ isDarkMode.set(value);
15
+ }
16
+ export function toggleDarkMode() {
17
+ isDarkMode.update((current) => {
18
+ const newValue = !current;
19
+ if (typeof document !== 'undefined') {
20
+ if (newValue) {
21
+ document.documentElement.setAttribute('data-theme', 'dark');
22
+ localStorage.setItem('theme', 'dark');
23
+ }
24
+ else {
25
+ document.documentElement.removeAttribute('data-theme');
26
+ localStorage.setItem('theme', 'light');
27
+ }
28
+ }
29
+ return newValue;
30
+ });
31
+ }
32
+ export function initTheme() {
33
+ if (typeof window === 'undefined')
34
+ return;
35
+ const savedTheme = localStorage.getItem('theme');
36
+ // Jika tidak ada item theme maka default jadi light
37
+ if (!savedTheme) {
38
+ document.documentElement.removeAttribute('data-theme'); // light
39
+ localStorage.setItem('theme', 'light');
40
+ isDarkMode.set(false);
41
+ return;
42
+ }
43
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
44
+ const shouldBeDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
45
+ if (shouldBeDark) {
46
+ document.documentElement.setAttribute('data-theme', 'dark');
47
+ localStorage.setItem('theme', 'dark');
48
+ isDarkMode.set(true);
49
+ }
50
+ else {
51
+ document.documentElement.removeAttribute('data-theme');
52
+ localStorage.setItem('theme', 'light');
53
+ isDarkMode.set(false);
54
+ }
55
+ }
@@ -0,0 +1,104 @@
1
+ {
2
+ "light": {
3
+ "--color-bg-canvas": "#F4F4F5",
4
+ "--color-bg-surface": "#FFFFFF",
5
+ "--color-bg-surface-raised": "#FFFFFF",
6
+ "--color-bg-surface-subtle": "#F4F4F5",
7
+ "--color-bg-inverse": "#09090B",
8
+ "--color-bg-act-primary": "#FF6900",
9
+ "--color-bg-act-hover": "#F54900",
10
+ "--color-bg-act-pressed": "#CA3500",
11
+ "--color-bg-act-subtle": "#FFEDD4",
12
+ "--color-bg-disabled": "#E4E4E7",
13
+ "--color-bg-info-subtle": "#F6FAFE",
14
+ "--color-bg-success-subtle": "#F6FEF9",
15
+ "--color-bg-error-subtle": "#FFFBFA",
16
+ "--color-bg-warning-subtle": "#FFFCF5",
17
+
18
+ "--color-text-primary": "#18181B",
19
+ "--color-text-secondary": "#71717B",
20
+ "--color-text-tertiary": "#9F9FA9",
21
+ "--color-text-inverse": "#FFFFFF",
22
+ "--color-text-link": "#FF6900",
23
+ "--color-text-disabled": "#E4E4E7",
24
+ "--color-text-on-act": "#FFFFFF",
25
+ "--color-text-info-ti": "#3684F3",
26
+ "--color-text-success-ti": "#12B76A",
27
+ "--color-text-error-ti": "#F04438",
28
+ "--color-text-warning-ti": "#E9AF3D",
29
+
30
+ "--color-border-divider-subtle": "#F4F4F5",
31
+ "--color-border-divider-base": "#9F9FA9",
32
+ "--color-border-outline": "#E4E4E7",
33
+ "--color-border-form": "#D4D4D8",
34
+ "--color-border-act-primary": "#FF6900",
35
+ "--color-border-act-subtle": "#FFF7ED",
36
+ "--color-border-disabled": "#E4E4E7",
37
+ "--color-border-info-bor": "#3684F3",
38
+ "--color-border-success-bor": "#12B76A",
39
+ "--color-border-error-bor": "#D92D20",
40
+ "--color-border-warning-bor": "#E9AF3D",
41
+
42
+ "--color-chart-teal-subtle": "#F0FDFA",
43
+ "--color-chart-purple-subtle": "#FAF5FF",
44
+ "--color-chart-blue-subtle": "#EFF6FF",
45
+ "--color-chart-orange-subtle": "#FFF7ED",
46
+ "--color-chart-gray-subtle": "#FAFAFA",
47
+ "--color-chart-teal-solid": "#00BBA7",
48
+ "--color-chart-purple-solid": "#AD46FF",
49
+ "--color-chart-blue-solid": "#2B7FFF",
50
+ "--color-chart-orange-solid": "#FF6900",
51
+ "--color-chart-gray-solid": "#9F9FA9"
52
+ },
53
+ "dark": {
54
+ "--color-bg-canvas": "#09090B",
55
+ "--color-bg-surface": "#18181B",
56
+ "--color-bg-surface-raised": "#27272A",
57
+ "--color-bg-surface-subtle": "#09090B",
58
+ "--color-bg-inverse": "#FFFFFF",
59
+ "--color-bg-act-primary": "#FF6900",
60
+ "--color-bg-act-hover": "#FF8904",
61
+ "--color-bg-act-pressed": "#FFB86A",
62
+ "--color-bg-act-subtle": "#441306",
63
+ "--color-bg-disabled": "#27272A",
64
+ "--color-bg-info-subtle": "#003999",
65
+ "--color-bg-success-subtle": "#054F31",
66
+ "--color-bg-error-subtle": "#7A271A",
67
+ "--color-bg-warning-subtle": "#5F370E",
68
+
69
+ "--color-text-primary": "#FAFAFA",
70
+ "--color-text-secondary": "#9F9FA9",
71
+ "--color-text-tertiary": "#52525C",
72
+ "--color-text-inverse": "#09090B",
73
+ "--color-text-link": "#FF6900",
74
+ "--color-text-disabled": "#3F3F46",
75
+ "--color-text-on-act": "#FFFFFF",
76
+ "--color-text-info-ti": "#0169CD",
77
+ "--color-text-success-ti": "#12B76A",
78
+ "--color-text-error-ti": "#D92D20",
79
+ "--color-text-warning-ti": "#CA8828",
80
+
81
+ "--color-border-divider-subtle": "#27272A",
82
+ "--color-border-divider-base": "#71717B",
83
+ "--color-border-outline": "#27272A",
84
+ "--color-border-form": "#3F3F46",
85
+ "--color-border-act-primary": "#FF6900",
86
+ "--color-border-act-subtle": "#441306",
87
+ "--color-border-disabled": "#3F3F46",
88
+ "--color-border-info-bor": "#3684F3",
89
+ "--color-border-success-bor": "#12B76A",
90
+ "--color-border-error-bor": "#D92D20",
91
+ "--color-border-warning-bor": "#CA8828",
92
+
93
+ "--color-chart-teal-subtle": "#022F2E",
94
+ "--color-chart-purple-subtle": "#3C0366",
95
+ "--color-chart-blue-subtle": "#162456",
96
+ "--color-chart-orange-subtle": "#441306",
97
+ "--color-chart-gray-subtle": "#3F3F47",
98
+ "--color-chart-teal-solid": "#00BBA7",
99
+ "--color-chart-purple-solid": "#AD46FF",
100
+ "--color-chart-blue-solid": "#2B7FFF",
101
+ "--color-chart-orange-solid": "#FF6900",
102
+ "--color-chart-gray-solid": "#D4D4D8"
103
+ }
104
+ }
@@ -0,0 +1,9 @@
1
+ export type ThemeTokens = Record<string, string>;
2
+ /**
3
+ * Menerapkan theme tokens ke CSS variables pada :root.
4
+ * CSS variables ini akan otomatis tersedia untuk @theme block di app.css
5
+ * yang menggunakan var(--variable-name).
6
+ *
7
+ * @param tokens - Object berisi CSS variable names dan values (contoh: { "--bg-canvas": "#F4F4F5" })
8
+ */
9
+ export declare function applyTheme(tokens: ThemeTokens): void;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Menerapkan theme tokens ke CSS variables pada :root.
3
+ * CSS variables ini akan otomatis tersedia untuk @theme block di app.css
4
+ * yang menggunakan var(--variable-name).
5
+ *
6
+ * @param tokens - Object berisi CSS variable names dan values (contoh: { "--bg-canvas": "#F4F4F5" })
7
+ */
8
+ export function applyTheme(tokens) {
9
+ const root = document.documentElement;
10
+ Object.entries(tokens).forEach(([token, value]) => {
11
+ root.style.setProperty(token, value);
12
+ });
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mertani-web-toolkit",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "homepage": "https://storybook.mertani.com/",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -15,7 +15,8 @@
15
15
  "test:e2e": "playwright test",
16
16
  "test": "npm run test:e2e",
17
17
  "storybook": "storybook dev -p 6006",
18
- "build-storybook": "storybook build"
18
+ "build-storybook": "storybook build",
19
+ "publish": "npm run build && npm pack && npm publish"
19
20
  },
20
21
  "files": [
21
22
  "dist",