sveltacular 1.0.26 → 1.0.27

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.
@@ -8,7 +8,8 @@
8
8
  shape = 'rounded' as 'circular' | 'square' | 'rounded' | 'badge' | 'circle',
9
9
  fill = 'solid' as 'solid' | 'outline',
10
10
  compact = false,
11
- label
11
+ label,
12
+ children
12
13
  }: {
13
14
  size?: FormFieldSizeOptions;
14
15
  variant?: 'standard' | 'positive' | 'negative';
@@ -16,11 +17,15 @@
16
17
  fill?: 'solid' | 'outline';
17
18
  compact?: boolean;
18
19
  label?: string;
20
+ children?: Snippet;
19
21
  } = $props();
20
22
  </script>
21
23
 
22
24
  <div class="pill {size} {variant} {shape} {fill}" class:compact>
23
- <span>{label}</span>
25
+ <span>
26
+ {label}
27
+ {@render children?.()}
28
+ </span>
24
29
  </div>
25
30
 
26
31
  <style>.pill {
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { FormFieldSizeOptions } from '../../types/form.js';
2
3
  type $$ComponentProps = {
3
4
  size?: FormFieldSizeOptions;
@@ -6,6 +7,7 @@ type $$ComponentProps = {
6
7
  fill?: 'solid' | 'outline';
7
8
  compact?: boolean;
8
9
  label?: string;
10
+ children?: Snippet;
9
11
  };
10
12
  declare const Pill: import("svelte").Component<$$ComponentProps, {}, "">;
11
13
  type Pill = ReturnType<typeof Pill>;
@@ -1,4 +1,4 @@
1
- import type { JsonObject, ColumnDef, TextColumn, NumberColumn, CurrencyColumn, DateColumn, DateTimeColumn, BooleanColumn, EmailColumn, CustomColumn } from '../types/data.js';
1
+ import type { JsonObject, ColumnDef, TextColumn, NumberColumn, CurrencyColumn, DateColumn, DateTimeColumn, BooleanColumn, EmailColumn, ArrayColumn, CustomColumn } from '../types/data.js';
2
2
  export interface CellRenderContext<T extends JsonObject = JsonObject> {
3
3
  row: T;
4
4
  column: ColumnDef<T>;
@@ -15,6 +15,14 @@ export declare function formatDateCell<T extends JsonObject>(row: T, column: Dat
15
15
  export declare function formatDateTimeCell<T extends JsonObject>(row: T, column: DateTimeColumn<T>): string;
16
16
  export declare function formatBooleanCell<T extends JsonObject>(row: T, column: BooleanColumn<T>): string;
17
17
  export declare function formatEmailCell<T extends JsonObject>(row: T, column: EmailColumn<T>): string;
18
+ export interface ArrayCellResult {
19
+ items: Array<{
20
+ text: string;
21
+ link: string | null;
22
+ }>;
23
+ separator: 'comma' | 'semicolon' | 'line' | 'pill';
24
+ }
25
+ export declare function formatArrayCell<T extends JsonObject>(row: T, column: ArrayColumn<T>): ArrayCellResult;
18
26
  export declare function formatCustomCell<T extends JsonObject>(row: T, column: CustomColumn<T>): string;
19
27
  export declare function formatCell<T extends JsonObject>(row: T, column: ColumnDef<T>): string;
20
28
  export declare function getCellLink<T extends JsonObject>(row: T, column: ColumnDef<T>): string | null;
@@ -10,92 +10,101 @@ export function isNullish(value) {
10
10
  export function isEmpty(value) {
11
11
  return typeof value === 'string' && value.trim() === '';
12
12
  }
13
+ function createFormatter(config) {
14
+ return (row, column) => {
15
+ const value = config.getValue(row, column);
16
+ // Check for null/undefined
17
+ if (isNullish(value) && 'nullText' in column && column.nullText) {
18
+ return column.nullText;
19
+ }
20
+ // Check for empty (if applicable)
21
+ if (config.checkEmpty && config.checkEmpty(value)) {
22
+ if ('emptyText' in column && column.emptyText) {
23
+ return column.emptyText;
24
+ }
25
+ }
26
+ // Use custom format if provided
27
+ if (config.customFormat && 'format' in column && column.format) {
28
+ return column.format(value, row);
29
+ }
30
+ // Use default format
31
+ return config.defaultFormat(value);
32
+ };
33
+ }
13
34
  // Format text column
14
35
  export function formatTextCell(row, column) {
15
- const value = getCellValue(row, column.key);
16
- if (isNullish(value) && column.nullText) {
17
- return column.nullText;
18
- }
19
- if (isEmpty(value) && column.emptyText) {
20
- return column.emptyText;
21
- }
22
- if (column.format) {
23
- return column.format(String(value), row);
24
- }
25
- return String(value ?? '');
36
+ const formatter = createFormatter({
37
+ getValue: (row, column) => getCellValue(row, column.key),
38
+ checkEmpty: isEmpty,
39
+ defaultFormat: (value) => String(value ?? ''),
40
+ customFormat: (value, row) => String(value)
41
+ });
42
+ return formatter(row, column);
26
43
  }
27
44
  // Format number column
28
45
  export function formatNumberCell(row, column) {
29
- const value = getCellValue(row, column.key);
30
- if (isNullish(value) && column.nullText) {
31
- return column.nullText;
32
- }
33
- const numValue = Number(value);
34
- if (isNaN(numValue)) {
35
- return column.emptyText ?? '';
36
- }
37
- if (column.format) {
38
- return column.format(numValue, row);
39
- }
40
- return numValue.toLocaleString();
46
+ const formatter = createFormatter({
47
+ getValue: (row, column) => {
48
+ const value = getCellValue(row, column.key);
49
+ return Number(value);
50
+ },
51
+ checkEmpty: (value) => isNaN(value),
52
+ defaultFormat: (value) => value.toLocaleString(),
53
+ customFormat: (value, row) => value.toString()
54
+ });
55
+ return formatter(row, column);
41
56
  }
42
57
  // Format currency column
43
58
  export function formatCurrencyCell(row, column) {
44
- const value = getCellValue(row, column.key);
45
- if (isNullish(value) && column.nullText) {
46
- return column.nullText;
47
- }
48
- const numValue = Number(value);
49
- if (isNaN(numValue)) {
50
- return column.emptyText ?? '';
51
- }
52
- if (column.format) {
53
- return column.format(numValue, row);
54
- }
55
- return new Intl.NumberFormat('en-US', {
56
- style: 'currency',
57
- currency: column.currency ?? 'USD'
58
- }).format(numValue);
59
+ const formatter = createFormatter({
60
+ getValue: (row, column) => {
61
+ const value = getCellValue(row, column.key);
62
+ return Number(value);
63
+ },
64
+ checkEmpty: (value) => isNaN(value),
65
+ defaultFormat: (value) => new Intl.NumberFormat('en-US', {
66
+ style: 'currency',
67
+ currency: column.currency ?? 'USD'
68
+ }).format(value),
69
+ customFormat: (value, row) => value.toString()
70
+ });
71
+ return formatter(row, column);
59
72
  }
60
73
  // Format date column
61
74
  export function formatDateCell(row, column) {
62
- const value = getCellValue(row, column.key);
63
- if (isNullish(value) && column.nullText) {
64
- return column.nullText;
65
- }
66
- if (isEmpty(value) && column.emptyText) {
67
- return column.emptyText;
68
- }
69
- if (column.format) {
70
- return column.format(value, row);
71
- }
72
- try {
73
- const date = typeof value === 'string' ? new Date(value) : value;
74
- return date.toISOString().substring(0, 10);
75
- }
76
- catch {
77
- return String(value ?? '');
78
- }
75
+ const formatter = createFormatter({
76
+ getValue: (row, column) => getCellValue(row, column.key),
77
+ checkEmpty: isEmpty,
78
+ defaultFormat: (value) => {
79
+ try {
80
+ const date = typeof value === 'string' ? new Date(value) : value;
81
+ return date.toISOString().substring(0, 10);
82
+ }
83
+ catch {
84
+ return String(value ?? '');
85
+ }
86
+ },
87
+ customFormat: (value, row) => String(value)
88
+ });
89
+ return formatter(row, column);
79
90
  }
80
91
  // Format datetime column
81
92
  export function formatDateTimeCell(row, column) {
82
- const value = getCellValue(row, column.key);
83
- if (isNullish(value) && column.nullText) {
84
- return column.nullText;
85
- }
86
- if (isEmpty(value) && column.emptyText) {
87
- return column.emptyText;
88
- }
89
- if (column.format) {
90
- return column.format(value, row);
91
- }
92
- try {
93
- const date = typeof value === 'string' ? new Date(value) : value;
94
- return date.toLocaleString();
95
- }
96
- catch {
97
- return String(value ?? '');
98
- }
93
+ const formatter = createFormatter({
94
+ getValue: (row, column) => getCellValue(row, column.key),
95
+ checkEmpty: isEmpty,
96
+ defaultFormat: (value) => {
97
+ try {
98
+ const date = typeof value === 'string' ? new Date(value) : value;
99
+ return date.toLocaleString();
100
+ }
101
+ catch {
102
+ return String(value ?? '');
103
+ }
104
+ },
105
+ customFormat: (value, row) => String(value)
106
+ });
107
+ return formatter(row, column);
99
108
  }
100
109
  // Format boolean column
101
110
  export function formatBooleanCell(row, column) {
@@ -114,23 +123,43 @@ export function formatBooleanCell(row, column) {
114
123
  }
115
124
  // Format email column
116
125
  export function formatEmailCell(row, column) {
126
+ const formatter = createFormatter({
127
+ getValue: (row, column) => getCellValue(row, column.key),
128
+ checkEmpty: isEmpty,
129
+ defaultFormat: (value) => String(value ?? ''),
130
+ customFormat: (value, row) => String(value)
131
+ });
132
+ return formatter(row, column);
133
+ }
134
+ // Format array column
135
+ export function formatArrayCell(row, column) {
117
136
  const value = getCellValue(row, column.key);
118
- if (isNullish(value) && column.nullText) {
119
- return column.nullText;
120
- }
121
- if (isEmpty(value) && column.emptyText) {
122
- return column.emptyText;
123
- }
124
- if (column.format) {
125
- return column.format(String(value), row);
126
- }
127
- return String(value ?? '');
137
+ const arr = Array.isArray(value) ? value : [];
138
+ const separator = column.separator ?? 'comma';
139
+ const items = arr.map((element, index) => {
140
+ let text;
141
+ if (column.format) {
142
+ text = column.format(element, row, index);
143
+ }
144
+ else if (column.displayKey && typeof element === 'object' && element !== null) {
145
+ text = String(element[column.displayKey] ?? '');
146
+ }
147
+ else {
148
+ text = String(element);
149
+ }
150
+ const link = column.link ? column.link(element, row, index) : null;
151
+ return { text, link };
152
+ });
153
+ return { items, separator };
128
154
  }
129
155
  // Format custom column
130
156
  export function formatCustomCell(row, column) {
131
157
  if (isNullish(row) && column.nullText) {
132
158
  return column.nullText;
133
159
  }
160
+ if (!column.render) {
161
+ return '';
162
+ }
134
163
  const value = column.render(row);
135
164
  return String(value ?? '');
136
165
  }
@@ -152,14 +181,25 @@ export function formatCell(row, column) {
152
181
  return formatBooleanCell(row, column);
153
182
  case 'email':
154
183
  return formatEmailCell(row, column);
184
+ case 'array':
185
+ // Array type returns structured data, not a string
186
+ // Use formatArrayCell directly instead
187
+ return '';
155
188
  case 'custom':
156
189
  return formatCustomCell(row, column);
157
190
  }
158
191
  }
159
192
  // Get link for a cell if applicable
160
193
  export function getCellLink(row, column) {
194
+ // Array columns handle their own links via formatArrayCell
195
+ if (column.type === 'array') {
196
+ return null;
197
+ }
161
198
  if ('link' in column && column.link) {
162
- return column.link(row);
199
+ // Type guard to ensure we're not dealing with array column
200
+ if (typeof column.link === 'function') {
201
+ return column.link(row);
202
+ }
163
203
  }
164
204
  // Auto-link emails
165
205
  if (column.type === 'email') {
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import type { JsonObject } from '../types/data.js';
3
+ import type { ButtonVariant, FormFieldSizeOptions } from '../types/form.js';
4
+ import TableCell from './table-cell.svelte';
5
+ import Button from '../forms/button/button.svelte';
6
+ import DropdownButton from '../navigation/dropdown-button/dropdown-button.svelte';
7
+ import DropdownItem from '../generic/dropdown-item/dropdown-item.svelte';
8
+
9
+ interface Action {
10
+ text: string;
11
+ variant?: ButtonVariant;
12
+ href?: (row: JsonObject) => string;
13
+ onClick?: (row: JsonObject) => unknown;
14
+ }
15
+
16
+ interface Actions {
17
+ text?: string;
18
+ type?: 'buttons' | 'dropdown';
19
+ variant?: ButtonVariant | 'default';
20
+ size?: FormFieldSizeOptions;
21
+ align?: 'left' | 'center' | 'right';
22
+ items: Action[];
23
+ }
24
+
25
+ let {
26
+ actions,
27
+ row,
28
+ actionButtonVariant = 'outline',
29
+ actionButtonSize = 'sm',
30
+ actionAlign = 'center'
31
+ }: {
32
+ actions: Actions;
33
+ row: JsonObject;
34
+ actionButtonVariant?: ButtonVariant;
35
+ actionButtonSize?: FormFieldSizeOptions;
36
+ actionAlign?: 'left' | 'center' | 'right';
37
+ } = $props();
38
+ </script>
39
+
40
+ <TableCell type="actions" align={actionAlign}>
41
+ {#if actions.type === 'dropdown'}
42
+ <DropdownButton text={actions.text ?? ''} variant="ghost">
43
+ {#each actions.items as action}
44
+ <DropdownItem
45
+ href={action.href ? action.href(row) : undefined}
46
+ onClick={action.onClick ? () => action.onClick?.(row) : undefined}
47
+ >{action.text}</DropdownItem
48
+ >
49
+ {/each}
50
+ </DropdownButton>
51
+ {:else}
52
+ <div class="actions">
53
+ {#each actions.items as action}
54
+ {@const buttonVariant = action.variant ?? actionButtonVariant}
55
+ <Button
56
+ type="button"
57
+ variant={buttonVariant}
58
+ size={actionButtonSize}
59
+ href={action.href ? action.href(row) : undefined}
60
+ onClick={action.onClick ? () => action.onClick?.(row) : undefined}
61
+ >
62
+ {action.text}
63
+ </Button>
64
+ {/each}
65
+ </div>
66
+ {/if}
67
+ </TableCell>
68
+
69
+ <style>.actions {
70
+ display: flex;
71
+ gap: 0.5rem;
72
+ }</style>
@@ -0,0 +1,26 @@
1
+ import type { JsonObject } from '../types/data.js';
2
+ import type { ButtonVariant, FormFieldSizeOptions } from '../types/form.js';
3
+ interface Action {
4
+ text: string;
5
+ variant?: ButtonVariant;
6
+ href?: (row: JsonObject) => string;
7
+ onClick?: (row: JsonObject) => unknown;
8
+ }
9
+ interface Actions {
10
+ text?: string;
11
+ type?: 'buttons' | 'dropdown';
12
+ variant?: ButtonVariant | 'default';
13
+ size?: FormFieldSizeOptions;
14
+ align?: 'left' | 'center' | 'right';
15
+ items: Action[];
16
+ }
17
+ type $$ComponentProps = {
18
+ actions: Actions;
19
+ row: JsonObject;
20
+ actionButtonVariant?: ButtonVariant;
21
+ actionButtonSize?: FormFieldSizeOptions;
22
+ actionAlign?: 'left' | 'center' | 'right';
23
+ };
24
+ declare const DataGridActionsCell: import("svelte").Component<$$ComponentProps, {}, "">;
25
+ type DataGridActionsCell = ReturnType<typeof DataGridActionsCell>;
26
+ export default DataGridActionsCell;
@@ -0,0 +1,104 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { ColumnDef, JsonObject } from '../types/data.js';
4
+ import TableCell from './table-cell.svelte';
5
+ import Pill from '../generic/pill/pill.svelte';
6
+ import {
7
+ formatCell,
8
+ getCellLink,
9
+ getCellAlignment,
10
+ getCellTypeClass,
11
+ getCellValue,
12
+ formatArrayCell
13
+ } from './cell-renderers.js';
14
+
15
+ interface CellContext<T extends JsonObject = JsonObject> {
16
+ row: T;
17
+ value: unknown;
18
+ column: ColumnDef<T>;
19
+ rowIndex: number;
20
+ }
21
+
22
+ let {
23
+ row,
24
+ column,
25
+ rowIndex,
26
+ cellSnippet = undefined,
27
+ width = undefined
28
+ }: {
29
+ row: JsonObject;
30
+ column: ColumnDef;
31
+ rowIndex: number;
32
+ cellSnippet?: Snippet<[CellContext]>;
33
+ width?: number | string;
34
+ } = $props();
35
+
36
+ let cellValue = $derived(getCellValue(row, column.key));
37
+ let cellAlign = $derived(getCellAlignment(column));
38
+ </script>
39
+
40
+ <TableCell type={getCellTypeClass(column)} {width} align={cellAlign}>
41
+ {#if cellSnippet}
42
+ {@render cellSnippet({ row, value: cellValue, column, rowIndex })}
43
+ {:else if column.type === 'custom' && column.component}
44
+ {@const CellComponent = column.component}
45
+ <CellComponent {row} value={cellValue} column={column} {rowIndex} />
46
+ {:else if column.type === 'array'}
47
+ {@const arrayResult = formatArrayCell(row, column)}
48
+ <span class="array-cell array-{arrayResult.separator}">
49
+ {#each arrayResult.items as item, i}
50
+ {#if arrayResult.separator === 'pill'}
51
+ <Pill compact>
52
+ {#if item.link}<a href={item.link}>{item.text}</a>{:else}{item.text}{/if}
53
+ </Pill>
54
+ {:else if arrayResult.separator === 'line'}
55
+ <div>
56
+ {#if item.link}<a href={item.link}>{item.text}</a>{:else}{item.text}{/if}
57
+ </div>
58
+ {:else}
59
+ {#if i > 0}
60
+ {#if arrayResult.separator === 'comma'},
61
+ {:else if arrayResult.separator === 'semicolon'};
62
+ {/if}
63
+ {/if}
64
+ {#if item.link}<a href={item.link}>{item.text}</a>{:else}{item.text}{/if}
65
+ {/if}
66
+ {/each}
67
+ </span>
68
+ {:else if column.type === 'check' || column.type === 'boolean'}
69
+ {#if row[column.key]}
70
+ <Pill shape="circle" variant="positive" compact label="✔" />
71
+ {/if}
72
+ {:else}
73
+ {@const cellLink = getCellLink(row, column)}
74
+ {#if cellLink}
75
+ <a href={cellLink}>{formatCell(row, column)}</a>
76
+ {:else}
77
+ {formatCell(row, column)}
78
+ {/if}
79
+ {/if}
80
+ </TableCell>
81
+
82
+ <style>a {
83
+ color: var(--table-link-fg, rgb(0, 0, 200));
84
+ text-decoration: none;
85
+ }
86
+ a:hover {
87
+ text-decoration: underline;
88
+ }
89
+
90
+ .array-cell {
91
+ display: inline-flex;
92
+ align-items: flex-start;
93
+ flex-wrap: wrap;
94
+ }
95
+ .array-cell.array-comma, .array-cell.array-semicolon {
96
+ gap: 0;
97
+ }
98
+ .array-cell.array-line {
99
+ flex-direction: column;
100
+ gap: 0.25rem;
101
+ }
102
+ .array-cell.array-pill {
103
+ gap: 0.25rem;
104
+ }</style>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ColumnDef, JsonObject } from '../types/data.js';
3
+ interface CellContext<T extends JsonObject = JsonObject> {
4
+ row: T;
5
+ value: unknown;
6
+ column: ColumnDef<T>;
7
+ rowIndex: number;
8
+ }
9
+ type $$ComponentProps = {
10
+ row: JsonObject;
11
+ column: ColumnDef;
12
+ rowIndex: number;
13
+ cellSnippet?: Snippet<[CellContext]>;
14
+ width?: number | string;
15
+ };
16
+ declare const DataGridCell: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type DataGridCell = ReturnType<typeof DataGridCell>;
18
+ export default DataGridCell;
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { ColumnDef, JsonObject } from '../types/data.js';
4
+ import type { ButtonVariant, FormFieldSizeOptions } from '../types/form.js';
5
+ import TableRow from './table-row.svelte';
6
+ import TableSelectionCell from './table-selection-cell.svelte';
7
+ import DataGridCell from './data-grid-cell.svelte';
8
+ import DataGridActionsCell from './data-grid-actions-cell.svelte';
9
+
10
+ interface CellContext<T extends JsonObject = JsonObject> {
11
+ row: T;
12
+ value: unknown;
13
+ column: ColumnDef<T>;
14
+ rowIndex: number;
15
+ }
16
+
17
+ interface Action {
18
+ text: string;
19
+ variant?: ButtonVariant;
20
+ href?: (row: JsonObject) => string;
21
+ onClick?: (row: JsonObject) => unknown;
22
+ }
23
+
24
+ interface Actions {
25
+ text?: string;
26
+ type?: 'buttons' | 'dropdown';
27
+ variant?: ButtonVariant | 'default';
28
+ size?: FormFieldSizeOptions;
29
+ align?: 'left' | 'center' | 'right';
30
+ items: Action[];
31
+ }
32
+
33
+ let {
34
+ row,
35
+ rowIndex,
36
+ visibleCols,
37
+ hasSelectionCol = false,
38
+ hasActionCol = false,
39
+ actions = undefined,
40
+ cells = undefined,
41
+ actionButtonVariant = 'outline',
42
+ actionButtonSize = 'sm',
43
+ actionAlign = 'center'
44
+ }: {
45
+ row: JsonObject;
46
+ rowIndex: number;
47
+ visibleCols: ColumnDef[];
48
+ hasSelectionCol?: boolean;
49
+ hasActionCol?: boolean;
50
+ actions?: Actions;
51
+ cells?: Record<string, Snippet<[CellContext]>>;
52
+ actionButtonVariant?: ButtonVariant;
53
+ actionButtonSize?: FormFieldSizeOptions;
54
+ actionAlign?: 'left' | 'center' | 'right';
55
+ } = $props();
56
+ </script>
57
+
58
+ <TableRow {row} {rowIndex} selectable={hasSelectionCol}>
59
+ {#if hasSelectionCol}
60
+ <TableSelectionCell {row} {rowIndex} />
61
+ {/if}
62
+ {#each visibleCols as col}
63
+ <DataGridCell
64
+ {row}
65
+ column={col}
66
+ {rowIndex}
67
+ cellSnippet={cells?.[col.key]}
68
+ width={col.width}
69
+ />
70
+ {/each}
71
+ {#if hasActionCol && actions}
72
+ <DataGridActionsCell {actions} {row} {actionButtonVariant} {actionButtonSize} {actionAlign} />
73
+ {/if}
74
+ </TableRow>
@@ -0,0 +1,38 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ColumnDef, JsonObject } from '../types/data.js';
3
+ import type { ButtonVariant, FormFieldSizeOptions } from '../types/form.js';
4
+ interface CellContext<T extends JsonObject = JsonObject> {
5
+ row: T;
6
+ value: unknown;
7
+ column: ColumnDef<T>;
8
+ rowIndex: number;
9
+ }
10
+ interface Action {
11
+ text: string;
12
+ variant?: ButtonVariant;
13
+ href?: (row: JsonObject) => string;
14
+ onClick?: (row: JsonObject) => unknown;
15
+ }
16
+ interface Actions {
17
+ text?: string;
18
+ type?: 'buttons' | 'dropdown';
19
+ variant?: ButtonVariant | 'default';
20
+ size?: FormFieldSizeOptions;
21
+ align?: 'left' | 'center' | 'right';
22
+ items: Action[];
23
+ }
24
+ type $$ComponentProps = {
25
+ row: JsonObject;
26
+ rowIndex: number;
27
+ visibleCols: ColumnDef[];
28
+ hasSelectionCol?: boolean;
29
+ hasActionCol?: boolean;
30
+ actions?: Actions;
31
+ cells?: Record<string, Snippet<[CellContext]>>;
32
+ actionButtonVariant?: ButtonVariant;
33
+ actionButtonSize?: FormFieldSizeOptions;
34
+ actionAlign?: 'left' | 'center' | 'right';
35
+ };
36
+ declare const DataGridRow: import("svelte").Component<$$ComponentProps, {}, "">;
37
+ type DataGridRow = ReturnType<typeof DataGridRow>;
38
+ export default DataGridRow;
@@ -2,33 +2,29 @@
2
2
  import TableCell from './table-cell.svelte';
3
3
  import TableHeaderCell from './table-header-cell.svelte';
4
4
  import TableHeader from './table-header.svelte';
5
- import TableRow from './table-row.svelte';
6
5
  import Table from './table.svelte';
7
- import TableSelectionCell from './table-selection-cell.svelte';
8
6
  import TableSelectionHeaderCell from './table-selection-header-cell.svelte';
7
+ import DataGridRow from './data-grid-row.svelte';
9
8
  import type { ColumnDef, JsonObject, PaginationProperties } from '../types/data.js';
10
- import Button from '../forms/button/button.svelte';
11
- import DropdownItem from '../generic/dropdown-item/dropdown-item.svelte';
12
9
  import Empty from '../generic/empty/empty.svelte';
13
- import Pill from '../generic/pill/pill.svelte';
14
10
  import Icon from '../icons/icon.svelte';
15
- import DropdownButton from '../navigation/dropdown-button/dropdown-button.svelte';
16
11
  import Pagination from '../navigation/pagination/pagination.svelte';
17
12
  import Loading from '../placeholders/loading.svelte';
18
13
  import TableCaption from './table-caption.svelte';
19
- import {
20
- formatCell,
21
- getCellLink,
22
- getCellAlignment,
23
- getCellTypeClass,
24
- sortRows
25
- } from './cell-renderers.js';
14
+ import { getCellAlignment, sortRows } from './cell-renderers.js';
26
15
  import type { Snippet } from 'svelte';
27
16
  import { useVirtualList } from '../helpers/use-virtual-list.svelte.js';
28
17
  import type { ButtonVariant, FormFieldSizeOptions } from '../types/form.js';
29
18
 
30
19
  type PaginationEvent = (pagination: PaginationProperties) => void;
31
20
 
21
+ interface CellContext<T extends JsonObject = JsonObject> {
22
+ row: T;
23
+ value: unknown;
24
+ column: ColumnDef<T>;
25
+ rowIndex: number;
26
+ }
27
+
32
28
  interface Action {
33
29
  text: string;
34
30
  variant?: ButtonVariant;
@@ -61,6 +57,7 @@
61
57
  onSelectionChange = undefined,
62
58
  selectedCount = $bindable(0),
63
59
  children = undefined,
60
+ cells = undefined,
64
61
  virtualScroll = false,
65
62
  rowHeight = 48,
66
63
  maxHeight = '600px'
@@ -80,6 +77,7 @@
80
77
  onSelectionChange?: (selectedRows: JsonObject[]) => void;
81
78
  selectedCount?: number;
82
79
  children?: Snippet;
80
+ cells?: Record<string, Snippet<[CellContext]>>;
83
81
  virtualScroll?: boolean;
84
82
  rowHeight?: number;
85
83
  maxHeight?: string;
@@ -257,7 +255,7 @@
257
255
  : ''}
258
256
  >
259
257
  {#if !filteredRows?.length}
260
- <TableRow>
258
+ <tr>
261
259
  <TableCell colspan={colCount}>
262
260
  <div class="empty" role="status" aria-live="polite">
263
261
  {#if rows === undefined}
@@ -269,7 +267,7 @@
269
267
  {/if}
270
268
  </div>
271
269
  </TableCell>
272
- </TableRow>
270
+ </tr>
273
271
  {:else if virtualScroll && !pagination && virtual}
274
272
  <!-- Virtual scrolling mode -->
275
273
  <tr style="height: {virtual.totalHeight}px; position: relative;">
@@ -280,57 +278,18 @@
280
278
  <div
281
279
  style="position: absolute; top: {vItem.offsetTop}px; height: {vItem.height}px; width: 100%; display: table; table-layout: fixed;"
282
280
  >
283
- <TableRow {row} rowIndex={index} selectable={hasSelectionCol}>
284
- {#if hasSelectionCol}
285
- <TableSelectionCell {row} rowIndex={index} />
286
- {/if}
287
- {#each visibleCols as col}
288
- {@const cellValue = formatCell(row, col)}
289
- {@const cellLink = getCellLink(row, col)}
290
- {@const cellAlign = getCellAlignment(col)}
291
- <TableCell type={getCellTypeClass(col)} width={col.width} align={cellAlign}>
292
- {#if cellLink}
293
- <a href={cellLink}>{cellValue}</a>
294
- {:else if col.type === 'check' || col.type === 'boolean'}
295
- {#if row[col.key]}
296
- <Pill shape="circle" variant="positive" compact label="✔" />
297
- {/if}
298
- {:else}
299
- {cellValue}
300
- {/if}
301
- </TableCell>
302
- {/each}
303
- {#if hasActionCol && actions}
304
- <TableCell type="actions" align={actionAlign}>
305
- {#if actions.type === 'dropdown'}
306
- <DropdownButton text={actions.text ?? ''} variant="ghost">
307
- {#each actions.items as action}
308
- <DropdownItem
309
- href={action.href ? action.href(row) : undefined}
310
- onClick={action.onClick ? () => action.onClick?.(row) : undefined}
311
- >{action.text}</DropdownItem
312
- >
313
- {/each}
314
- </DropdownButton>
315
- {:else}
316
- <div class="actions">
317
- {#each actions.items as action}
318
- {@const buttonVariant = action.variant ?? actionButtonVariant}
319
- <Button
320
- collapse={true}
321
- type="button"
322
- variant={buttonVariant}
323
- size={actionButtonSize}
324
- href={action.href ? action.href(row) : undefined}
325
- onClick={action.onClick ? () => action.onClick?.(row) : undefined}
326
- label={action.text}
327
- />
328
- {/each}
329
- </div>
330
- {/if}
331
- </TableCell>
332
- {/if}
333
- </TableRow>
281
+ <DataGridRow
282
+ {row}
283
+ rowIndex={index}
284
+ {visibleCols}
285
+ {hasSelectionCol}
286
+ {hasActionCol}
287
+ {actions}
288
+ {cells}
289
+ {actionButtonVariant}
290
+ {actionButtonSize}
291
+ {actionAlign}
292
+ />
334
293
  </div>
335
294
  {/each}
336
295
  </td>
@@ -338,56 +297,18 @@
338
297
  {:else}
339
298
  <!-- Regular rendering mode -->
340
299
  {#each filteredRows as row, index}
341
- <TableRow {row} rowIndex={index} selectable={hasSelectionCol}>
342
- {#if hasSelectionCol}
343
- <TableSelectionCell {row} rowIndex={index} />
344
- {/if}
345
- {#each visibleCols as col}
346
- {@const cellValue = formatCell(row, col)}
347
- {@const cellLink = getCellLink(row, col)}
348
- {@const cellAlign = getCellAlignment(col)}
349
- <TableCell type={getCellTypeClass(col)} width={col.width} align={cellAlign}>
350
- {#if cellLink}
351
- <a href={cellLink}>{cellValue}</a>
352
- {:else if col.type === 'check' || col.type === 'boolean'}
353
- {#if row[col.key]}
354
- <Pill shape="circle" variant="positive" compact label="✔" />
355
- {/if}
356
- {:else}
357
- {cellValue}
358
- {/if}
359
- </TableCell>
360
- {/each}
361
- {#if hasActionCol && actions}
362
- <TableCell type="actions" align={actionAlign}>
363
- {#if actions.type === 'dropdown'}
364
- <DropdownButton text={actions.text ?? ''} variant="ghost">
365
- {#each actions.items as action}
366
- <DropdownItem
367
- href={action.href ? action.href(row) : undefined}
368
- onClick={action.onClick ? () => action.onClick?.(row) : undefined}
369
- >{action.text}</DropdownItem
370
- >
371
- {/each}
372
- </DropdownButton>
373
- {:else}
374
- <div class="actions">
375
- {#each actions.items as action}
376
- {@const buttonVariant = action.variant ?? actionButtonVariant}
377
- <Button
378
- type="button"
379
- variant={buttonVariant}
380
- size={actionButtonSize}
381
- href={action.href ? action.href(row) : undefined}
382
- onClick={action.onClick ? () => action.onClick?.(row) : undefined}
383
- label={action.text}
384
- />
385
- {/each}
386
- </div>
387
- {/if}
388
- </TableCell>
389
- {/if}
390
- </TableRow>
300
+ <DataGridRow
301
+ {row}
302
+ rowIndex={index}
303
+ {visibleCols}
304
+ {hasSelectionCol}
305
+ {hasActionCol}
306
+ {actions}
307
+ {cells}
308
+ {actionButtonVariant}
309
+ {actionButtonSize}
310
+ {actionAlign}
311
+ />
391
312
  {/each}
392
313
  {/if}
393
314
  </tbody>
@@ -418,14 +339,6 @@
418
339
  letter-spacing: 0.2rem;
419
340
  }
420
341
 
421
- a {
422
- color: var(--table-link-fg, rgb(0, 0, 200));
423
- text-decoration: none;
424
- }
425
- a:hover {
426
- text-decoration: underline;
427
- }
428
-
429
342
  tfoot {
430
343
  background: var(--table-footer-bg);
431
344
  color: var(--table-footer-fg);
@@ -442,9 +355,4 @@ td.footer-cell :global(.pagination) {
442
355
  display: flex;
443
356
  justify-content: center;
444
357
  align-items: center;
445
- }
446
-
447
- .actions {
448
- display: flex;
449
- gap: 0.5rem;
450
358
  }</style>
@@ -2,6 +2,12 @@ import type { ColumnDef, JsonObject, PaginationProperties } from '../types/data.
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { ButtonVariant, FormFieldSizeOptions } from '../types/form.js';
4
4
  type PaginationEvent = (pagination: PaginationProperties) => void;
5
+ interface CellContext<T extends JsonObject = JsonObject> {
6
+ row: T;
7
+ value: unknown;
8
+ column: ColumnDef<T>;
9
+ rowIndex: number;
10
+ }
5
11
  interface Action {
6
12
  text: string;
7
13
  variant?: ButtonVariant;
@@ -32,6 +38,7 @@ type $$ComponentProps = {
32
38
  onSelectionChange?: (selectedRows: JsonObject[]) => void;
33
39
  selectedCount?: number;
34
40
  children?: Snippet;
41
+ cells?: Record<string, Snippet<[CellContext]>>;
35
42
  virtualScroll?: boolean;
36
43
  rowHeight?: number;
37
44
  maxHeight?: string;
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import type { CellRendererProps } from '../types/data.js';
3
+ import Pill from '../generic/pill/pill.svelte';
4
+
5
+ type StatusValue = 'active' | 'completed' | 'on-hold';
6
+
7
+ let { value }: CellRendererProps = $props();
8
+ </script>
9
+
10
+ {#if value === 'active'}
11
+ <Pill variant="positive" compact label="✓ Active" />
12
+ {:else if value === 'completed'}
13
+ <Pill variant="standard" compact label="✓ Completed" />
14
+ {:else}
15
+ <Pill variant="negative" compact label="⚠ On Hold" />
16
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { CellRendererProps } from '../types/data.js';
2
+ declare const ExampleStatusCell: import("svelte").Component<CellRendererProps<import("../types/data.js").JsonObject>, {}, "">;
3
+ type ExampleStatusCell = ReturnType<typeof ExampleStatusCell>;
4
+ export default ExampleStatusCell;
@@ -7,4 +7,5 @@ export { default as TableRow } from './table-row.svelte';
7
7
  export { default as TableCaption } from './table-caption.svelte';
8
8
  export { createTableContext, getTableContext, TableContext } from './table-context.svelte.js';
9
9
  export type { TableContextConfig } from './table-context.svelte.js';
10
- export { formatCell, formatTextCell, formatNumberCell, formatCurrencyCell, formatDateCell, formatDateTimeCell, formatBooleanCell, formatEmailCell, formatCustomCell, getCellValue, getCellLink, getCellAlignment, getCellTypeClass, sortRows, compareValues } from './cell-renderers.js';
10
+ export { formatCell, formatTextCell, formatNumberCell, formatCurrencyCell, formatDateCell, formatDateTimeCell, formatBooleanCell, formatEmailCell, formatArrayCell, formatCustomCell, getCellValue, getCellLink, getCellAlignment, getCellTypeClass, sortRows, compareValues } from './cell-renderers.js';
11
+ export type { ArrayCellResult, CellRenderContext } from './cell-renderers.js';
@@ -9,4 +9,4 @@ export { default as TableCaption } from './table-caption.svelte';
9
9
  // Context and utilities
10
10
  export { createTableContext, getTableContext, TableContext } from './table-context.svelte.js';
11
11
  // Cell renderers and utilities
12
- export { formatCell, formatTextCell, formatNumberCell, formatCurrencyCell, formatDateCell, formatDateTimeCell, formatBooleanCell, formatEmailCell, formatCustomCell, getCellValue, getCellLink, getCellAlignment, getCellTypeClass, sortRows, compareValues } from './cell-renderers.js';
12
+ export { formatCell, formatTextCell, formatNumberCell, formatCurrencyCell, formatDateCell, formatDateTimeCell, formatBooleanCell, formatEmailCell, formatArrayCell, formatCustomCell, getCellValue, getCellLink, getCellAlignment, getCellTypeClass, sortRows, compareValues } from './cell-renderers.js';
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  export type JsonValue = string | number | boolean | null | {
2
3
  [key: string]: JsonValue;
3
4
  } | JsonValue[];
@@ -50,11 +51,25 @@ export interface EmailColumn<T extends JsonObject = JsonObject> extends BaseColu
50
51
  type: 'email';
51
52
  format?: (value: string, row: T) => string;
52
53
  }
54
+ export interface ArrayColumn<T extends JsonObject = JsonObject> extends BaseColumn<T> {
55
+ type: 'array';
56
+ displayKey?: string;
57
+ format?: (element: unknown, row: T, index: number) => string;
58
+ link?: (element: unknown, row: T, index: number) => string | null;
59
+ separator?: 'comma' | 'semicolon' | 'line' | 'pill';
60
+ }
61
+ export interface CellRendererProps<T extends JsonObject = JsonObject> {
62
+ row: T;
63
+ value: unknown;
64
+ column: ColumnDef<T>;
65
+ rowIndex: number;
66
+ }
53
67
  export interface CustomColumn<T extends JsonObject = JsonObject> extends BaseColumn<T> {
54
68
  type: 'custom';
55
- render: (row: T) => string | number | boolean;
69
+ render?: (row: T) => string | number | boolean;
70
+ component?: Component<CellRendererProps<T>>;
56
71
  }
57
- export type ColumnDef<T extends JsonObject = JsonObject> = TextColumn<T> | NumberColumn<T> | CurrencyColumn<T> | DateColumn<T> | DateTimeColumn<T> | BooleanColumn<T> | EmailColumn<T> | CustomColumn<T>;
72
+ export type ColumnDef<T extends JsonObject = JsonObject> = TextColumn<T> | NumberColumn<T> | CurrencyColumn<T> | DateColumn<T> | DateTimeColumn<T> | BooleanColumn<T> | EmailColumn<T> | ArrayColumn<T> | CustomColumn<T>;
58
73
  export type DataCol = {
59
74
  key: string;
60
75
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltacular",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
4
4
  "description": "A Svelte component library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",