svelte-tably 1.0.0-next.0

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.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # svelte-tably
2
+
3
+ Work in progress. I needed a break from my primary project, so here's a little side-project exploring that amazing capabilities of Svelte 5 with a Dynamic table!
4
+
5
+ A high performant dynamic table
6
+
7
+ - [x] Sticky columns
8
+ - [x] Show/hide columns
9
+ - [x] Re-order columns
10
+ - [x] Resize columns
11
+ - [x] Statusbar
12
+ - [x] Virtual data (for sorting/filtering)
13
+ - [x] Panels
14
+ - [ ] sorting
15
+ - [ ] select
16
+ - [ ] filtering
17
+ - [ ] orderable table
18
+ - [ ] row context-menu
19
+ - [ ] dropout section
@@ -0,0 +1,98 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script module lang='ts'>
12
+
13
+ export interface Column<T = unknown, V = unknown> {
14
+ header: Snippet
15
+ row: Snippet<[item: T, value?: V]>
16
+ statusbar?: Snippet<[data: T[]]>
17
+
18
+ /** Default options for initial table */
19
+ defaults: {
20
+ sticky?: boolean
21
+ sort?: boolean
22
+ show?: boolean
23
+ }
24
+ /** More options */
25
+ options: {
26
+ value?: (item: T) => V
27
+ sorting?: unknown extends V ? (a: T, b: T) => number : (a: V, b: V) => number
28
+ }
29
+ }
30
+
31
+ </script>
32
+
33
+ <script lang='ts' generics='T extends Record<PropertyKey, any>, V'>
34
+
35
+ import { onDestroy, type Snippet } from 'svelte'
36
+ import { getTableState } from './Table.svelte'
37
+
38
+ interface Props {
39
+ header: Column<T, V>['header']
40
+ row: Column<T, V>['row']
41
+ statusbar?: Column<T, V>['statusbar']
42
+
43
+ // options
44
+ sticky?: boolean
45
+ sort?: boolean
46
+ show?: boolean
47
+ value?: Column<T, V>['options']['value']
48
+ sorting?: Column<T, V>['options']['sorting']
49
+ }
50
+
51
+ let {
52
+ header, row, statusbar,
53
+
54
+ sticky = false,
55
+ sort = false,
56
+ show = true,
57
+
58
+ value, sorting,
59
+
60
+ ...rest
61
+ }: Props = $props()
62
+ const key = (rest as unknown as { __key: string }).__key
63
+
64
+ const column: Column<T, V> = $state({
65
+ header,
66
+ row,
67
+ statusbar,
68
+ defaults: {
69
+ sticky,
70
+ sort,
71
+ show
72
+ },
73
+ options: {
74
+ value,
75
+ sorting
76
+ }
77
+ })
78
+
79
+ const table = getTableState()
80
+ table.addColumn(key, column as Column)
81
+
82
+ onDestroy(() => {
83
+ table.removeColumn(key)
84
+ })
85
+
86
+ </script>
87
+ <!---------------------------------------------------->
88
+
89
+
90
+
91
+
92
+
93
+ <!---------------------------------------------------->
94
+ <style>
95
+
96
+
97
+
98
+ </style>
@@ -0,0 +1,50 @@
1
+ export interface Column<T = unknown, V = unknown> {
2
+ header: Snippet;
3
+ row: Snippet<[item: T, value?: V]>;
4
+ statusbar?: Snippet<[data: T[]]>;
5
+ /** Default options for initial table */
6
+ defaults: {
7
+ sticky?: boolean;
8
+ sort?: boolean;
9
+ show?: boolean;
10
+ };
11
+ /** More options */
12
+ options: {
13
+ value?: (item: T) => V;
14
+ sorting?: unknown extends V ? (a: T, b: T) => number : (a: V, b: V) => number;
15
+ };
16
+ }
17
+ import { type Snippet } from 'svelte';
18
+ declare class __sveltets_Render<T extends Record<PropertyKey, any>, V> {
19
+ props(): {
20
+ header: Column<T_1, V_1>["header"];
21
+ row: Column<T_1, V_1>["row"];
22
+ statusbar?: Column<T_1, V_1>["statusbar"];
23
+ sticky?: boolean;
24
+ sort?: boolean;
25
+ show?: boolean;
26
+ value?: Column<T_1, V_1>["options"]["value"];
27
+ sorting?: Column<T_1, V_1>["options"]["sorting"];
28
+ };
29
+ events(): {};
30
+ slots(): {};
31
+ bindings(): "";
32
+ exports(): {};
33
+ }
34
+ interface $$IsomorphicComponent {
35
+ new <T extends Record<PropertyKey, any>, V>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, V>['props']>, ReturnType<__sveltets_Render<T, V>['events']>, ReturnType<__sveltets_Render<T, V>['slots']>> & {
36
+ $$bindings?: ReturnType<__sveltets_Render<T, V>['bindings']>;
37
+ } & ReturnType<__sveltets_Render<T, V>['exports']>;
38
+ <T extends Record<PropertyKey, any>, V>(internal: unknown, props: ReturnType<__sveltets_Render<T, V>['props']> & {}): ReturnType<__sveltets_Render<T, V>['exports']>;
39
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
40
+ }
41
+ /**
42
+ * This is a description, \
43
+ * on how to use this.
44
+ *
45
+ * @example
46
+ * <Component />
47
+ */
48
+ declare const Column: $$IsomorphicComponent;
49
+ type Column<T extends Record<PropertyKey, any>, V> = InstanceType<typeof Column<T, V>>;
50
+ export default Column;
@@ -0,0 +1,73 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script module lang='ts'>
12
+
13
+ export interface Panel {
14
+ /** A darkened backdrop? */
15
+ backdrop: boolean
16
+ content: Snippet<[table: TableState]>
17
+ }
18
+
19
+ export class PanelTween {
20
+ #tween = new Tween(0, { duration: 300, easing: sineInOut })
21
+ current = $derived(this.#tween.current)
22
+ transitioning = $state(false)
23
+
24
+ /** bind:clientWidth */
25
+ width = $state(0)
26
+
27
+ set target(value: number) {
28
+ this.transitioning = true
29
+ this.#tween.set(value).then(() => this.transitioning = false)
30
+ }
31
+
32
+ constructor(cb: () => string | undefined) {
33
+ $effect.pre(() => {
34
+ this.target = cb() ? this.width : 0
35
+ })
36
+ }
37
+ }
38
+
39
+ </script>
40
+
41
+ <script lang='ts'>
42
+
43
+ import { onDestroy, type Snippet } from 'svelte'
44
+ import { getTableState, type TableState } from './Table.svelte'
45
+ import { Tween } from 'svelte/motion'
46
+ import { sineInOut } from 'svelte/easing'
47
+
48
+ interface Props {
49
+ /** A darkened backdrop? */
50
+ backdrop?: boolean
51
+ children: Snippet<[table: TableState]>
52
+ }
53
+
54
+ let {
55
+ backdrop = true,
56
+ children,
57
+ ...rest
58
+ }: Props = $props()
59
+ const key = (rest as unknown as { __key: string }).__key
60
+
61
+ const panel: Panel = $state({
62
+ backdrop,
63
+ content: children
64
+ })
65
+
66
+ const table = getTableState()
67
+ table.panels[key] = panel
68
+
69
+ onDestroy(() => {
70
+ delete table.panels[key]
71
+ })
72
+
73
+ </script>
@@ -0,0 +1,31 @@
1
+ export interface Panel {
2
+ /** A darkened backdrop? */
3
+ backdrop: boolean;
4
+ content: Snippet<[table: TableState]>;
5
+ }
6
+ export declare class PanelTween {
7
+ #private;
8
+ current: number;
9
+ transitioning: boolean;
10
+ /** bind:clientWidth */
11
+ width: number;
12
+ set target(value: number);
13
+ constructor(cb: () => string | undefined);
14
+ }
15
+ import { type Snippet } from 'svelte';
16
+ import { type TableState } from './Table.svelte';
17
+ interface Props {
18
+ /** A darkened backdrop? */
19
+ backdrop?: boolean;
20
+ children: Snippet<[table: TableState]>;
21
+ }
22
+ /**
23
+ * This is a description, \
24
+ * on how to use this.
25
+ *
26
+ * @example
27
+ * <Component />
28
+ */
29
+ declare const Panel: import("svelte").Component<Props, {}, "">;
30
+ type Panel = ReturnType<typeof Panel>;
31
+ export default Panel;
@@ -0,0 +1,349 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script module lang='ts'>
12
+
13
+ export interface TableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>> {
14
+ columns: Record<string, Column<T, unknown>>
15
+ panels: Record<string, Panel>
16
+ sortby?: string
17
+ positions: {
18
+ sticky: string[]
19
+ scroll: string[]
20
+ hidden: string[]
21
+ toggle(key: string): void
22
+ }
23
+ readonly data: T[]
24
+ addColumn(key: string, options: Column<T, unknown>): void
25
+ removeColumn(key: string): void
26
+ }
27
+
28
+ export function getTableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>>() {
29
+ return getContext<TableState<T>>('svelte5-table')
30
+ }
31
+
32
+ </script>
33
+
34
+ <script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
35
+
36
+ import { getContext, setContext, type Snippet } from 'svelte'
37
+ import { type Column } from './Column.svelte'
38
+ import { PanelTween, type Panel } from './Panel.svelte'
39
+ import { fly } from 'svelte/transition'
40
+ import { sineInOut } from 'svelte/easing'
41
+
42
+ interface Props {
43
+ children?: Snippet
44
+ panel?: string
45
+ data?: T[]
46
+ id?: string
47
+ }
48
+
49
+ let {
50
+ children,
51
+ panel,
52
+ data = [],
53
+ id = Array.from({length: 12}, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join('')
54
+ }: Props = $props()
55
+
56
+ const table: TableState<T> = $state({
57
+ columns: {},
58
+ panels: {},
59
+ positions: {
60
+ sticky: [],
61
+ scroll: [],
62
+ hidden: [],
63
+ toggle(key) {
64
+ if(table.positions.hidden.includes(key))
65
+ table.positions.hidden = table.positions.hidden.filter(column => column !== key)
66
+ else
67
+ table.positions.hidden.push(key)
68
+ }
69
+ },
70
+ get data() {
71
+ return data
72
+ },
73
+ addColumn(key, column) {
74
+ table.columns[key] = column
75
+
76
+ if(column.defaults.sort)
77
+ table.sortby = key
78
+
79
+ if(!column.defaults.show)
80
+ table.positions.hidden.push(key)
81
+
82
+ if(column.defaults.sticky)
83
+ table.positions.sticky.push(key)
84
+ else
85
+ table.positions.scroll.push(key)
86
+ },
87
+ removeColumn(key) {
88
+ delete table.columns[key]
89
+ table.positions.sticky = table.positions.sticky.filter(column => column !== key)
90
+ table.positions.scroll = table.positions.scroll.filter(column => column !== key)
91
+ table.positions.hidden = table.positions.hidden.filter(column => column !== key)
92
+ }
93
+ })
94
+
95
+ setContext('svelte5-table', table)
96
+
97
+ // * --- *
98
+
99
+ const panelTween = new PanelTween(() => panel)
100
+ const elements = $state({}) as Record<'headers' | 'statusbar', HTMLElement>
101
+
102
+ /** Order of columns */
103
+ const columns = $derived([...table.positions.sticky, ...table.positions.scroll].filter(key => !table.positions.hidden.includes(key)))
104
+
105
+ /** Width of each column */
106
+ const widths = $state({}) as Record<string, number>
107
+
108
+ /** grid-template-columns for widths */
109
+ const style = $derived(`
110
+ #${id} > .headers, #${id} > .rows > .row, #${id} > .statusbar {
111
+ grid-template-columns: ${columns.map((key, i, arr) => i === arr.length - 1 ? `minmax(${widths[key] || 150}px, 1fr)` : `${widths[key] || 150}px`).join(' ')};
112
+ }
113
+ `)
114
+
115
+ const observer = typeof MutationObserver === 'undefined' ? undefined : new MutationObserver(mutations => {
116
+ const target = mutations[0].target as HTMLDivElement
117
+ widths[target.getAttribute('data-column')!] = parseFloat(target.style.width)
118
+ })
119
+
120
+ function observe(node: HTMLDivElement, column: string) {
121
+ observer?.observe(node, {attributes: true})
122
+ return { destroy: () => observer?.disconnect() }
123
+ }
124
+
125
+ function onscroll(event: Event) {
126
+ const target = event.target as HTMLDivElement
127
+ elements.headers.scrollLeft = target.scrollLeft
128
+ elements.statusbar.scrollLeft = target.scrollLeft
129
+ }
130
+
131
+ </script>
132
+ <!---------------------------------------------------->
133
+
134
+ <svelte:head>
135
+ {@html `<style>${style}</style>`}
136
+ </svelte:head>
137
+
138
+ <div id={id} class='table'>
139
+
140
+ <div class='headers' bind:this={elements.headers}>
141
+ {#each table.positions.sticky as column, i (column)}
142
+ {#if !table.positions.hidden.includes(column)}
143
+ <div class='column sticky' data-column="{column}" use:observe={column}>
144
+ {@render table.columns[column]?.header()}
145
+ </div>
146
+ {/if}
147
+ {/each}
148
+ {#each table.positions.scroll as column, i (column)}
149
+ {#if !table.positions.hidden.includes(column)}
150
+ <div class='column' use:observe={column}>
151
+ {@render table.columns[column]?.header()}
152
+ </div>
153
+ {/if}
154
+ {/each}
155
+ </div>
156
+
157
+ <div class="rows" {onscroll}>
158
+ {#each data as item}
159
+ <div class='row'>
160
+ {#each table.positions.sticky as column, i (column)}
161
+ {#if !table.positions.hidden.includes(column)}
162
+ {@const col = table.columns[column]}
163
+ <div class='column sticky' class:border={i == table.positions.sticky.length - 1}>
164
+ {@render col.row(item, col.options.value ? col.options.value(item) : undefined)}
165
+ </div>
166
+ {/if}
167
+ {/each}
168
+ {#each table.positions.scroll as column, i (column)}
169
+ {#if !table.positions.hidden.includes(column)}
170
+ {@const col = table.columns[column]}
171
+ <div class='column'>
172
+ {@render col.row(item, col.options.value ? col.options.value(item) : undefined)}
173
+ </div>
174
+ {/if}
175
+ {/each}
176
+ </div>
177
+ {/each}
178
+ </div>
179
+
180
+ <div class='statusbar' bind:this={elements.statusbar}>
181
+ {#each table.positions.sticky as column, i (column)}
182
+ {#if !table.positions.hidden.includes(column)}
183
+ <div class='column sticky' class:border={i == table.positions.sticky.length - 1}>
184
+ {@render table.columns[column]?.statusbar?.(data)}
185
+ </div>
186
+ {/if}
187
+ {/each}
188
+ {#each table.positions.scroll as column, i (column)}
189
+ {#if !table.positions.hidden.includes(column)}
190
+ <div class='column'>
191
+ {@render table.columns[column]?.statusbar?.(data)}
192
+ </div>
193
+ {/if}
194
+ {/each}
195
+ </div>
196
+
197
+ <div class='panel' style='width: {panelTween.current}px;' style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}>
198
+ {#if panel && panel in table.panels}
199
+ <div
200
+ class="panel-content"
201
+ bind:clientWidth={panelTween.width}
202
+ in:fly={{ x: 100, easing: sineInOut, duration:300 }}
203
+ out:fly={{ x:100, duration:200, easing: sineInOut }}
204
+ >
205
+ {@render table.panels[panel].content(table as TableState)}
206
+ </div>
207
+ {/if}
208
+ </div>
209
+ </div>
210
+
211
+
212
+ {@render children?.()}
213
+
214
+
215
+
216
+ <!---------------------------------------------------->
217
+ <style>
218
+
219
+ .table, .table * {
220
+ box-sizing: border-box;
221
+ }
222
+
223
+ .sticky {
224
+ position: sticky;
225
+ left: 0px;
226
+ /* right: 100px; */
227
+ background-color: white;
228
+ z-index: 1;
229
+ }
230
+
231
+ .sticky.border {
232
+ border-right: 1px solid hsla(0, 0%, 90%);
233
+ }
234
+
235
+ .headers > .column {
236
+ border-right: 1px solid hsla(0, 0%, 90%);
237
+ resize: horizontal;
238
+ overflow: hidden;
239
+ padding: var(--padding-y) 0;
240
+ }
241
+
242
+ .table {
243
+ --panel: 250px;
244
+ --padding-x: 1rem;
245
+ --padding-y: .5rem;
246
+ --gap: .25rem;
247
+ --header-height: 2.5rem;
248
+
249
+ display: grid;
250
+
251
+ grid-template-areas:
252
+ "headers panel"
253
+ "rows panel"
254
+ "statusbar panel"
255
+ ;
256
+
257
+ grid-template-columns: auto min-content;
258
+ grid-template-rows: auto 1fr auto;
259
+
260
+ border: 1px solid hsla(0, 0%, 90%);
261
+ border-radius: .25rem;
262
+
263
+ max-height: 100%;
264
+ }
265
+
266
+ .headers {
267
+ grid-area: headers;
268
+ z-index: 2;
269
+ overflow: hidden;
270
+ padding-right: 1rem;
271
+ }
272
+
273
+ .headers > .column {
274
+ width: auto !important;
275
+ background-color: hsla(0, 0%, 100%);
276
+ border-bottom: 1px solid hsla(0, 0%, 90%);
277
+ }
278
+
279
+ .rows {
280
+ grid-area: rows;
281
+ display: grid;
282
+ overflow: auto;
283
+ scrollbar-width: thin;
284
+ }
285
+
286
+ .statusbar {
287
+ grid-area: statusbar;
288
+ overflow: hidden;
289
+ padding-right: 1rem;
290
+ }
291
+
292
+ .statusbar > .column {
293
+ background-color: hsla(0, 0%, 99%);
294
+ border-top: 1px solid hsla(0, 0%, 90%);
295
+ padding: calc(var(--padding-y) / 2) 0;
296
+ }
297
+
298
+ .headers, .row, .statusbar {
299
+ display: grid;
300
+ width: 100%;
301
+ height: 100%;
302
+
303
+ & > .column {
304
+ display: flex;
305
+ padding-left: var(--padding-x);
306
+ overflow: hidden;
307
+ }
308
+
309
+ & > *:last-child {
310
+ width: 100%;
311
+ padding-right: var(--padding-x);
312
+ }
313
+ }
314
+
315
+ .row:nth-child(1) > * {
316
+ padding-top: calc(var(--padding-y) + var(--gap));
317
+ }
318
+ .row:nth-last-child(1) > * {
319
+ padding-bottom: calc(var(--padding-y) + var(--gap));
320
+ }
321
+
322
+ .row > * {
323
+ padding: var(--gap) 0;
324
+ }
325
+
326
+ .panel {
327
+ position: relative;
328
+ grid-area: panel;
329
+ width: var(--panel);
330
+ height: 100%;
331
+ background-color: white;
332
+
333
+ border-left: 1px solid hsla(0, 0%, 90%);
334
+
335
+ > .panel-content {
336
+ position: absolute;
337
+ top: 0;
338
+ right: 0;
339
+ width: min-content;
340
+ overflow: hidden;
341
+ padding: var(--padding-y) var(--padding-x);
342
+ }
343
+ }
344
+
345
+ .statusbar {
346
+ grid-area: statusbar;
347
+ }
348
+
349
+ </style>
@@ -0,0 +1,47 @@
1
+ export interface TableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>> {
2
+ columns: Record<string, Column<T, unknown>>;
3
+ panels: Record<string, Panel>;
4
+ sortby?: string;
5
+ positions: {
6
+ sticky: string[];
7
+ scroll: string[];
8
+ hidden: string[];
9
+ toggle(key: string): void;
10
+ };
11
+ readonly data: T[];
12
+ addColumn(key: string, options: Column<T, unknown>): void;
13
+ removeColumn(key: string): void;
14
+ }
15
+ export declare function getTableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>>(): TableState<T>;
16
+ import { type Snippet } from 'svelte';
17
+ import { type Column } from './Column.svelte';
18
+ import { type Panel } from './Panel.svelte';
19
+ declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
20
+ props(): {
21
+ children?: Snippet;
22
+ panel?: string;
23
+ data?: T[] | undefined;
24
+ id?: string;
25
+ };
26
+ events(): {};
27
+ slots(): {};
28
+ bindings(): "";
29
+ exports(): {};
30
+ }
31
+ interface $$IsomorphicComponent {
32
+ new <T extends Record<PropertyKey, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
33
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
34
+ } & ReturnType<__sveltets_Render<T>['exports']>;
35
+ <T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
36
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
37
+ }
38
+ /**
39
+ * This is a description, \
40
+ * on how to use this.
41
+ *
42
+ * @example
43
+ * <Component />
44
+ */
45
+ declare const Table: $$IsomorphicComponent;
46
+ type Table<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Table<T>>;
47
+ export default Table;
@@ -0,0 +1,12 @@
1
+ import { default as _Table } from './Table.svelte';
2
+ import Column from './Column.svelte';
3
+ import { default as _Panel } from './Panel.svelte';
4
+ declare const LATIN: readonly ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
5
+ type Capital = typeof LATIN[number];
6
+ declare const Table: typeof _Table & {
7
+ [key: `${Capital}${string}`]: typeof Column;
8
+ };
9
+ declare const Panel: {
10
+ [key: `${Capital}${string}`]: typeof _Panel;
11
+ };
12
+ export { Table, Panel };
@@ -0,0 +1,31 @@
1
+ import { default as _Table } from './Table.svelte';
2
+ import Column from './Column.svelte';
3
+ import { default as _Panel } from './Panel.svelte';
4
+ const LATIN = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
5
+ const Table = new Proxy(_Table, {
6
+ get(target, p, receiver) {
7
+ if (typeof p !== 'string' || !LATIN.includes(p[0])) {
8
+ return Reflect.get(target, p, receiver);
9
+ }
10
+ return new Proxy(Column, {
11
+ apply(_, __, [anchor, props]) {
12
+ Object.assign(props, { __key: p });
13
+ return Column(anchor, props);
14
+ },
15
+ });
16
+ }
17
+ });
18
+ const Panel = new Proxy(_Panel, {
19
+ get(target, p, receiver) {
20
+ if (typeof p !== 'string' || !LATIN.includes(p[0])) {
21
+ return Reflect.get(target, p, receiver);
22
+ }
23
+ return new Proxy(_Panel, {
24
+ apply(_, __, [anchor, props]) {
25
+ Object.assign(props, { __key: p });
26
+ return _Panel(anchor, props);
27
+ },
28
+ });
29
+ },
30
+ });
31
+ export { Table, Panel };
@@ -0,0 +1 @@
1
+ export * from './Table/index.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './Table/index.js';
@@ -0,0 +1,33 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script lang="ts">
12
+ import type { Snippet } from 'svelte'
13
+ import { getTableState, type ColumnOptions } from '../Table.svelte'
14
+
15
+ interface Props {
16
+ [key: string]: Snippet<[options: ColumnOptions]>
17
+ }
18
+
19
+ let headers: Props = $props()
20
+ const table = getTableState()
21
+
22
+ let keys = [] as string[]
23
+
24
+ $effect.pre(() => {
25
+ keys.forEach((key) => delete table.columns[key].header)
26
+ keys = []
27
+
28
+ for (const [key, value] of Object.entries(headers)) {
29
+ table.updateColumn(key, { header: value })
30
+ keys.push(key)
31
+ }
32
+ })
33
+ </script>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type ColumnOptions } from '../Table.svelte';
3
+ interface Props {
4
+ [key: string]: Snippet<[options: ColumnOptions]>;
5
+ }
6
+ /**
7
+ * This is a description, \
8
+ * on how to use this.
9
+ *
10
+ * @example
11
+ * <Component />
12
+ */
13
+ declare const Headers: import("svelte").Component<Props, {}, "">;
14
+ type Headers = ReturnType<typeof Headers>;
15
+ export default Headers;
@@ -0,0 +1,25 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script lang='ts'>
12
+
13
+ import type { Snippet } from 'svelte'
14
+ import { getTableState, type TableState } from './Table.svelte'
15
+
16
+ interface Props {
17
+ [key: string]: Snippet<[table: TableState]>
18
+ }
19
+
20
+ let panels: Props = $props()
21
+
22
+ getTableState().panels = panels
23
+
24
+ </script>
25
+ <!---------------------------------------------------->
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type TableState } from './Table.svelte';
3
+ interface Props {
4
+ [key: string]: Snippet<[table: TableState]>;
5
+ }
6
+ /**
7
+ * This is a description, \
8
+ * on how to use this.
9
+ *
10
+ * @example
11
+ * <Component />
12
+ */
13
+ declare const Panels: import("svelte").Component<Props, {}, "">;
14
+ type Panels = ReturnType<typeof Panels>;
15
+ export default Panels;
@@ -0,0 +1,35 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
12
+
13
+ import type { Snippet } from 'svelte'
14
+ import { getTableState } from './Table.svelte'
15
+
16
+ interface Props {
17
+ [key: string]: Snippet<[data: T]>
18
+ }
19
+
20
+ let rows: Props = $props()
21
+ const table = getTableState<T>()
22
+
23
+ let keys = [] as string[]
24
+
25
+ $effect.pre(() => {
26
+ keys.forEach(key => delete table.columns[key].row)
27
+ keys = []
28
+
29
+ for(const [key, value] of Object.entries(rows)) {
30
+ table.updateColumn(key, { row: value })
31
+ keys.push(key)
32
+ }
33
+ })
34
+
35
+ </script>
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
3
+ props(): {
4
+ [key: string]: Snippet<[data: T]>;
5
+ };
6
+ events(): {};
7
+ slots(): {};
8
+ bindings(): "";
9
+ exports(): {};
10
+ }
11
+ interface $$IsomorphicComponent {
12
+ new <T extends Record<PropertyKey, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
13
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
14
+ } & ReturnType<__sveltets_Render<T>['exports']>;
15
+ <T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
16
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
17
+ }
18
+ /**
19
+ * This is a description, \
20
+ * on how to use this.
21
+ *
22
+ * @example
23
+ * <Component />
24
+ */
25
+ declare const Rows: $$IsomorphicComponent;
26
+ type Rows<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Rows<T>>;
27
+ export default Rows;
@@ -0,0 +1,35 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script lang='ts'>
12
+
13
+ import type { Snippet } from 'svelte'
14
+ import { getTableState, type TableState } from './Table.svelte'
15
+
16
+ interface Props {
17
+ [key: string]: Snippet<[data: T[]]>
18
+ }
19
+
20
+ let statusbars: Props = $props()
21
+ const table = getTableState()
22
+
23
+ let keys = [] as string[]
24
+
25
+ $effect.pre(() => {
26
+ keys.forEach(key => delete table.columns[key].statusbar)
27
+ keys = []
28
+ for(const [key, value] of Object.entries(statusbars)) {
29
+ table.updateColumn(key, { statusbar: value })
30
+ keys.push(key)
31
+ }
32
+ })
33
+
34
+ </script>
35
+ <!---------------------------------------------------->
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * This is a description, \
4
+ * on how to use this.
5
+ *
6
+ * @example
7
+ * <Component />
8
+ */
9
+ declare const Statusbar: import("svelte").Component<{
10
+ [key: string]: Snippet<[data: T[]]>;
11
+ }, {}, "">;
12
+ type Statusbar = ReturnType<typeof Statusbar>;
13
+ export default Statusbar;
@@ -0,0 +1,336 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script module lang='ts'>
12
+
13
+ export interface ColumnOptions {
14
+ sticky(): ColumnOptions
15
+ }
16
+
17
+ interface Column<T = unknown> {
18
+ header?: Snippet<[options: ColumnOptions]>
19
+ row?: Snippet<[data: T]>
20
+ statusbar?: Snippet<[data: T[]]>
21
+ defaults?: {
22
+ sticky?: boolean
23
+ }
24
+ }
25
+
26
+ export interface TableState<T = unknown> {
27
+ columns: Record<string, Column<T>>
28
+ order: {
29
+ sticky: string[]
30
+ scroll: string[]
31
+ }
32
+ panels: Record<string, Snippet<[table: TableState]>>
33
+ readonly data: T[]
34
+ updateColumn(key: string, options: Column<T>): void
35
+ }
36
+
37
+ export function getTableState<T>() {
38
+ return getContext('svelte-dynamic-table') as TableState<T>
39
+ }
40
+
41
+ </script>
42
+
43
+ <script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
44
+
45
+ import { getContext, setContext, type Snippet } from 'svelte'
46
+ import { sineInOut } from 'svelte/easing'
47
+ import { Spring, Tween } from 'svelte/motion'
48
+ import { fly } from 'svelte/transition'
49
+
50
+ interface Props {
51
+ children?: Snippet
52
+ data: T[]
53
+ panel?: string
54
+ }
55
+
56
+ let {
57
+ children,
58
+ data,
59
+ panel = $bindable()
60
+ }: Props = $props()
61
+
62
+ const table: TableState<T> = $state({
63
+ columns: {},
64
+ order: {
65
+ sticky: [],
66
+ scroll: []
67
+ },
68
+ panels: {},
69
+ get data() {
70
+ return data
71
+ },
72
+ updateColumn(key, options) {
73
+ if(!(key in table.columns)) {
74
+ const column = $state({})
75
+ table.columns[key] = column
76
+ }
77
+ Object.assign(table.columns[key], options)
78
+ }
79
+ })
80
+
81
+ const columns = $derived([...table.order.sticky, ...table.order.scroll])
82
+
83
+ /** Passed to header */
84
+ function columnOptions(key: string) {
85
+ const options = $state({}) as NonNullable<Column['defaults']>
86
+ table.updateColumn(key, { defaults: options })
87
+ return {
88
+ sticky() {
89
+ options.sticky = true
90
+ return this
91
+ }
92
+ } as ColumnOptions
93
+ }
94
+
95
+ setContext('svelte-dynamic-table', table)
96
+
97
+ const widths = $state({}) as Record<string, number>
98
+ const headers = new WeakMap<HTMLDivElement, string>()
99
+
100
+ const tableId = Array.from({length: 12}, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join('')
101
+
102
+ const style = $derived(`
103
+ #${tableId} > .headers, #${tableId} > .rows > .row, #${tableId} > .statusbar {
104
+ grid-template-columns: ${columns.map((key, i, arr) => i === arr.length - 1 ? `minmax(${widths[key] || 150}px, 1fr)` : `${widths[key] || 150}px`).join(' ')};
105
+ }
106
+ `)
107
+
108
+ const observer = typeof MutationObserver === 'undefined' ? undefined : new MutationObserver(mutations => {
109
+ const target = mutations[0].target as HTMLDivElement
110
+ const column = headers.get(target)
111
+ if (!column) return
112
+ widths[column] = parseFloat(target.style.width)
113
+ })
114
+
115
+ function observe(node: HTMLDivElement, column: string) {
116
+ observer?.observe(node, {attributes: true})
117
+ headers.set(node, column)
118
+ return {
119
+ destroy: () => observer?.disconnect()
120
+ }
121
+ }
122
+
123
+ let elements = $state({}) as Record<string, HTMLElement>
124
+ function onscroll(event: Event) {
125
+ const target = event.target as HTMLDivElement
126
+ elements.headers.scrollLeft = target.scrollLeft
127
+ elements.statusbar.scrollLeft = target.scrollLeft
128
+ }
129
+
130
+ const panelTween = new Tween(0, { duration: 300, easing: sineInOut })
131
+ let panelWidth = $state(0)
132
+ let panelTransitioning = $state(false)
133
+ $effect(() => {
134
+ panelTransitioning = true
135
+ panelTween.set(panel ? panelWidth : 0).then(() => panelTransitioning = false)
136
+ })
137
+
138
+ </script>
139
+ <!---------------------------------------------------->
140
+
141
+ <svelte:head>
142
+ {@html `<style>${style}</style>`}
143
+ </svelte:head>
144
+
145
+ <div id={tableId} class='table'>
146
+
147
+ <div class='headers' bind:this={elements.headers}>
148
+ {#each table.order.sticky as column, i (column)}
149
+ <div class='column sticky' use:observe={column}>
150
+ {@render table.columns[column]?.header?.({ sticky() {} } as ColumnOptions)}
151
+ </div>
152
+ {/each}
153
+ {#each table.order.scroll as column, i (column)}
154
+ <div class='column' use:observe={column}>
155
+ {@render table.columns[column]?.header?.({ sticky() {} } as ColumnOptions)}
156
+ </div>
157
+ {/each}
158
+
159
+ </div>
160
+
161
+ <div class="rows" {onscroll}>
162
+ {#each data as item}
163
+ <div class='row'>
164
+ {#each table.order.sticky as column, i (column)}
165
+ <div class='column sticky' class:border={i == table.order.sticky.length - 1}>
166
+ {@render table.columns[column]?.row?.(item)}
167
+ </div>
168
+ {/each}
169
+ {#each table.order.scroll as column, i (column)}
170
+ <div class='column'>
171
+ {@render table.columns[column]?.row?.(item)}
172
+ </div>
173
+ {/each}
174
+ </div>
175
+ {/each}
176
+ </div>
177
+
178
+ <div class='statusbar' bind:this={elements.statusbar}>
179
+ {#each table.order.sticky as column, i (column)}
180
+ <div class='column sticky' class:border={i == table.order.sticky.length - 1}>
181
+ {@render table.columns[column]?.statusbar?.(data)}
182
+ </div>
183
+ {/each}
184
+ {#each table.order.scroll as column, i (column)}
185
+ <div class='column'>
186
+ {@render table.columns[column]?.statusbar?.(data)}
187
+ </div>
188
+ {/each}
189
+ </div>
190
+
191
+ <div class='panel' style='width: {panelTween.current}px;' style:overflow={panelTransitioning ? 'hidden' : 'auto'}>
192
+ {#if panel && table.panels[panel]}
193
+ <div class="panel-content" bind:clientWidth={panelWidth} in:fly={{ x: 100, easing: sineInOut, duration:300 }} out:fly={{ x:100, duration:200, easing: sineInOut }}>
194
+ {@render table.panels[panel](table as TableState)}
195
+ </div>
196
+ {/if}
197
+ </div>
198
+ </div>
199
+
200
+
201
+
202
+ {@render children?.()}
203
+
204
+
205
+
206
+ <!---------------------------------------------------->
207
+ <style>
208
+
209
+ .table, .table * {
210
+ box-sizing: border-box;
211
+ }
212
+
213
+ .sticky {
214
+ position: sticky;
215
+ left: 0px;
216
+ /* right: 100px; */
217
+ background-color: white;
218
+ z-index: 1;
219
+ &.border {
220
+ border-right: 1px solid hsla(0, 0%, 90%);
221
+ }
222
+ }
223
+
224
+ .headers > .column {
225
+ border-right: 1px solid hsla(0, 0%, 90%);
226
+ resize: horizontal;
227
+ overflow: hidden;
228
+ padding: var(--padding-y) 0;
229
+ }
230
+
231
+ .table {
232
+ --panel: 250px;
233
+ --padding-x: 1rem;
234
+ --padding-y: .5rem;
235
+ --gap: .25rem;
236
+ --header-height: 2.5rem;
237
+
238
+ display: grid;
239
+
240
+ grid-template-areas:
241
+ "headers panel"
242
+ "rows panel"
243
+ "statusbar panel"
244
+ ;
245
+
246
+ grid-template-columns: auto min-content;
247
+ grid-template-rows: auto 1fr auto;
248
+
249
+ border: 1px solid hsla(0, 0%, 90%);
250
+ border-radius: .25rem;
251
+
252
+ max-height: 100%;
253
+ }
254
+
255
+ .headers {
256
+ grid-area: headers;
257
+ z-index: 2;
258
+ overflow: hidden;
259
+ padding-right: 1rem;
260
+ > .column {
261
+ width: auto !important;
262
+ background-color: white;
263
+ border-bottom: 1px solid hsla(0, 0%, 90%);
264
+ }
265
+ }
266
+
267
+ .rows {
268
+ grid-area: rows;
269
+ display: grid;
270
+ overflow: auto;
271
+ scrollbar-width: thin;
272
+ }
273
+
274
+ .statusbar {
275
+ grid-area: statusbar;
276
+ overflow: hidden;
277
+ padding-right: 1rem;
278
+ > .column {
279
+ background-color: hsla(0, 0%, 99%);
280
+ border-top: 1px solid hsla(0, 0%, 90%);
281
+ padding: calc(var(--padding-y) / 2) 0;
282
+ }
283
+ }
284
+
285
+ .headers, .row, .statusbar {
286
+ display: grid;
287
+ width: 100%;
288
+ height: 100%;
289
+
290
+ & > .column {
291
+ display: flex;
292
+ padding-left: var(--padding-x);
293
+ overflow: hidden;
294
+ }
295
+
296
+ & > *:last-child {
297
+ width: 100%;
298
+ padding-right: var(--padding-x);
299
+ }
300
+ }
301
+
302
+ .row:nth-child(1) > * {
303
+ padding-top: calc(var(--padding-y) + var(--gap));
304
+ }
305
+ .row:nth-last-child(1) > * {
306
+ padding-bottom: calc(var(--padding-y) + var(--gap));
307
+ }
308
+
309
+ .row > * {
310
+ padding: var(--gap) 0;
311
+ }
312
+
313
+ .panel {
314
+ position: relative;
315
+ grid-area: panel;
316
+ width: var(--panel);
317
+ height: 100%;
318
+ background-color: white;
319
+
320
+ border-left: 1px solid hsla(0, 0%, 90%);
321
+
322
+ > .panel-content {
323
+ position: absolute;
324
+ top: 0;
325
+ right: 0;
326
+ width: min-content;
327
+ overflow: hidden;
328
+ padding: var(--padding-y) var(--padding-x);
329
+ }
330
+ }
331
+
332
+ .statusbar {
333
+ grid-area: statusbar;
334
+ }
335
+
336
+ </style>
@@ -0,0 +1,51 @@
1
+ export interface ColumnOptions {
2
+ sticky(): ColumnOptions;
3
+ }
4
+ interface Column<T = unknown> {
5
+ header?: Snippet<[options: ColumnOptions]>;
6
+ row?: Snippet<[data: T]>;
7
+ statusbar?: Snippet<[data: T[]]>;
8
+ defaults?: {
9
+ sticky?: boolean;
10
+ };
11
+ }
12
+ export interface TableState<T = unknown> {
13
+ columns: Record<string, Column<T>>;
14
+ order: {
15
+ sticky: string[];
16
+ scroll: string[];
17
+ };
18
+ panels: Record<string, Snippet<[table: TableState]>>;
19
+ readonly data: T[];
20
+ updateColumn(key: string, options: Column<T>): void;
21
+ }
22
+ export declare function getTableState<T>(): TableState<T>;
23
+ import { type Snippet } from 'svelte';
24
+ declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
25
+ props(): {
26
+ children?: Snippet;
27
+ data: T[];
28
+ panel?: string;
29
+ };
30
+ events(): {};
31
+ slots(): {};
32
+ bindings(): "panel";
33
+ exports(): {};
34
+ }
35
+ interface $$IsomorphicComponent {
36
+ new <T extends Record<PropertyKey, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
37
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
38
+ } & ReturnType<__sveltets_Render<T>['exports']>;
39
+ <T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
40
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
41
+ }
42
+ /**
43
+ * This is a description, \
44
+ * on how to use this.
45
+ *
46
+ * @example
47
+ * <Component />
48
+ */
49
+ declare const Table: $$IsomorphicComponent;
50
+ type Table<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Table<T>>;
51
+ export default Table;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "svelte-tably",
3
+ "version": "1.0.0-next.0",
4
+ "devDependencies": {
5
+ "@sveltejs/adapter-auto": "^3.0.0",
6
+ "@sveltejs/kit": "^2.9.0",
7
+ "@sveltejs/package": "^2.0.0",
8
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
9
+ "publint": "^0.2.0",
10
+ "svelte": "^5.0.0",
11
+ "svelte-check": "^4.0.0",
12
+ "typescript": "^5.0.0",
13
+ "vite": "^6.0.0"
14
+ },
15
+ "peerDependencies": {
16
+ "svelte": "^5.0.0"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "svelte": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "!dist/**/*.test.*",
27
+ "!dist/**/*.spec.*"
28
+ ],
29
+ "scripts": {
30
+ "dev": "vite dev",
31
+ "build": "vite build && npm run package",
32
+ "preview": "vite preview",
33
+ "package": "svelte-kit sync && svelte-package && publint",
34
+ "prepublishOnly": "npm run package",
35
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
36
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
37
+ },
38
+ "sideEffects": [
39
+ "**/*.css"
40
+ ],
41
+ "svelte": "./dist/index.js",
42
+ "type": "module",
43
+ "types": "./dist/index.d.ts",
44
+ "dependencies": {
45
+ "@faker-js/faker": "^9.3.0"
46
+ }
47
+ }