@shotleybuilder/svelte-gridlite-kit 0.1.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 +260 -0
- package/dist/GridLite.svelte +1361 -0
- package/dist/GridLite.svelte.d.ts +42 -0
- package/dist/components/CellContextMenu.svelte +209 -0
- package/dist/components/CellContextMenu.svelte.d.ts +28 -0
- package/dist/components/ColumnMenu.svelte +234 -0
- package/dist/components/ColumnMenu.svelte.d.ts +29 -0
- package/dist/components/ColumnPicker.svelte +403 -0
- package/dist/components/ColumnPicker.svelte.d.ts +29 -0
- package/dist/components/FilterBar.svelte +390 -0
- package/dist/components/FilterBar.svelte.d.ts +38 -0
- package/dist/components/FilterCondition.svelte +643 -0
- package/dist/components/FilterCondition.svelte.d.ts +35 -0
- package/dist/components/GroupBar.svelte +463 -0
- package/dist/components/GroupBar.svelte.d.ts +33 -0
- package/dist/components/RowDetailModal.svelte +213 -0
- package/dist/components/RowDetailModal.svelte.d.ts +25 -0
- package/dist/components/SortBar.svelte +232 -0
- package/dist/components/SortBar.svelte.d.ts +30 -0
- package/dist/components/SortCondition.svelte +129 -0
- package/dist/components/SortCondition.svelte.d.ts +30 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +29 -0
- package/dist/query/builder.d.ts +160 -0
- package/dist/query/builder.js +432 -0
- package/dist/query/live.d.ts +50 -0
- package/dist/query/live.js +118 -0
- package/dist/query/schema.d.ts +30 -0
- package/dist/query/schema.js +75 -0
- package/dist/state/migrations.d.ts +29 -0
- package/dist/state/migrations.js +113 -0
- package/dist/state/views.d.ts +54 -0
- package/dist/state/views.js +130 -0
- package/dist/styles/gridlite.css +966 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +2 -0
- package/dist/utils/filters.d.ts +14 -0
- package/dist/utils/filters.js +49 -0
- package/dist/utils/formatters.d.ts +16 -0
- package/dist/utils/formatters.js +39 -0
- package/dist/utils/fuzzy.d.ts +47 -0
- package/dist/utils/fuzzy.js +142 -0
- package/package.json +76 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
<script>export let columns;
|
|
2
|
+
export let columnConfigs = [];
|
|
3
|
+
export let grouping = [];
|
|
4
|
+
export let onGroupingChange;
|
|
5
|
+
export let isExpanded = false;
|
|
6
|
+
export let onExpandedChange = void 0;
|
|
7
|
+
const MAX_LEVELS = 3;
|
|
8
|
+
const aggregateFunctions = [
|
|
9
|
+
{ value: "count", label: "Count" },
|
|
10
|
+
{ value: "sum", label: "Sum" },
|
|
11
|
+
{ value: "avg", label: "Average" },
|
|
12
|
+
{ value: "min", label: "Min" },
|
|
13
|
+
{ value: "max", label: "Max" }
|
|
14
|
+
];
|
|
15
|
+
function getColumnLabel(col) {
|
|
16
|
+
const cfg = columnConfigs.find((c) => c.name === col.name);
|
|
17
|
+
return cfg?.label ?? col.name;
|
|
18
|
+
}
|
|
19
|
+
function setExpanded(value) {
|
|
20
|
+
isExpanded = value;
|
|
21
|
+
if (onExpandedChange) {
|
|
22
|
+
onExpandedChange(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function addGroup() {
|
|
26
|
+
if (grouping.length >= MAX_LEVELS) return;
|
|
27
|
+
onGroupingChange([...grouping, { column: "" }]);
|
|
28
|
+
setExpanded(true);
|
|
29
|
+
}
|
|
30
|
+
function updateGroupColumn(index, column) {
|
|
31
|
+
const newGrouping = [...grouping];
|
|
32
|
+
newGrouping[index] = { ...newGrouping[index], column };
|
|
33
|
+
onGroupingChange(newGrouping);
|
|
34
|
+
}
|
|
35
|
+
function removeGroup(index) {
|
|
36
|
+
const newGrouping = grouping.filter((_, i) => i !== index);
|
|
37
|
+
onGroupingChange(newGrouping);
|
|
38
|
+
if (newGrouping.length === 0) {
|
|
39
|
+
setExpanded(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function clearAllGroups() {
|
|
43
|
+
onGroupingChange([]);
|
|
44
|
+
setExpanded(false);
|
|
45
|
+
}
|
|
46
|
+
function addAggregation(groupIndex) {
|
|
47
|
+
const newGrouping = [...grouping];
|
|
48
|
+
const group = { ...newGrouping[groupIndex] };
|
|
49
|
+
const agg = { column: "", function: "count" };
|
|
50
|
+
group.aggregations = [...group.aggregations ?? [], agg];
|
|
51
|
+
newGrouping[groupIndex] = group;
|
|
52
|
+
onGroupingChange(newGrouping);
|
|
53
|
+
}
|
|
54
|
+
function updateAggregation(groupIndex, aggIndex, updates) {
|
|
55
|
+
const newGrouping = [...grouping];
|
|
56
|
+
const group = { ...newGrouping[groupIndex] };
|
|
57
|
+
const aggs = [...group.aggregations ?? []];
|
|
58
|
+
aggs[aggIndex] = { ...aggs[aggIndex], ...updates };
|
|
59
|
+
group.aggregations = aggs;
|
|
60
|
+
newGrouping[groupIndex] = group;
|
|
61
|
+
onGroupingChange(newGrouping);
|
|
62
|
+
}
|
|
63
|
+
function removeAggregation(groupIndex, aggIndex) {
|
|
64
|
+
const newGrouping = [...grouping];
|
|
65
|
+
const group = { ...newGrouping[groupIndex] };
|
|
66
|
+
group.aggregations = (group.aggregations ?? []).filter((_, i) => i !== aggIndex);
|
|
67
|
+
newGrouping[groupIndex] = group;
|
|
68
|
+
onGroupingChange(newGrouping);
|
|
69
|
+
}
|
|
70
|
+
function handleAggFunctionChange(groupIndex, aggIndex, value) {
|
|
71
|
+
const updates = {};
|
|
72
|
+
updates.function = value;
|
|
73
|
+
updateAggregation(groupIndex, aggIndex, updates);
|
|
74
|
+
}
|
|
75
|
+
$: numericColumns = columns.filter((col) => {
|
|
76
|
+
const cfg = columnConfigs.find((c) => c.name === col.name);
|
|
77
|
+
const dt = cfg?.dataType ?? col.dataType;
|
|
78
|
+
return dt === "number";
|
|
79
|
+
});
|
|
80
|
+
$: hasGroups = grouping.length > 0;
|
|
81
|
+
$: validGroupCount = grouping.filter((g) => g.column !== "").length;
|
|
82
|
+
$: canAddMore = grouping.length < MAX_LEVELS;
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<div class="group-bar">
|
|
86
|
+
<button class="group-toggle-btn" on:click={() => setExpanded(!isExpanded)}>
|
|
87
|
+
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
88
|
+
<path
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
stroke-linejoin="round"
|
|
91
|
+
stroke-width="2"
|
|
92
|
+
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
Group
|
|
96
|
+
{#if validGroupCount > 0}
|
|
97
|
+
<span class="group-badge">{validGroupCount}</span>
|
|
98
|
+
{/if}
|
|
99
|
+
<svg
|
|
100
|
+
class="chevron"
|
|
101
|
+
class:expanded={isExpanded}
|
|
102
|
+
fill="none"
|
|
103
|
+
stroke="currentColor"
|
|
104
|
+
viewBox="0 0 24 24"
|
|
105
|
+
>
|
|
106
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
107
|
+
</svg>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
{#if isExpanded}
|
|
111
|
+
<div class="group-panel">
|
|
112
|
+
{#if hasGroups}
|
|
113
|
+
<div class="group-header">
|
|
114
|
+
<span class="group-label">Group by</span>
|
|
115
|
+
<button class="clear-all-btn" on:click={clearAllGroups}> Clear all </button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="group-levels">
|
|
119
|
+
{#each grouping as group, index (index)}
|
|
120
|
+
<div class="group-level">
|
|
121
|
+
<div class="group-level-row">
|
|
122
|
+
<select
|
|
123
|
+
class="field-select"
|
|
124
|
+
value={group.column}
|
|
125
|
+
on:change={(e) => updateGroupColumn(index, e.currentTarget.value)}
|
|
126
|
+
>
|
|
127
|
+
<option value="">Select field...</option>
|
|
128
|
+
{#each columns as column}
|
|
129
|
+
<option value={column.name}>
|
|
130
|
+
{getColumnLabel(column)}
|
|
131
|
+
</option>
|
|
132
|
+
{/each}
|
|
133
|
+
</select>
|
|
134
|
+
|
|
135
|
+
<button class="remove-btn" on:click={() => removeGroup(index)} title="Remove group" type="button">
|
|
136
|
+
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
137
|
+
<path
|
|
138
|
+
stroke-linecap="round"
|
|
139
|
+
stroke-linejoin="round"
|
|
140
|
+
stroke-width="2"
|
|
141
|
+
d="M6 18L18 6M6 6l12 12"
|
|
142
|
+
/>
|
|
143
|
+
</svg>
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{#if group.column}
|
|
148
|
+
<!-- Aggregations for this group level -->
|
|
149
|
+
{#if group.aggregations && group.aggregations.length > 0}
|
|
150
|
+
<div class="aggregations">
|
|
151
|
+
{#each group.aggregations as agg, aggIndex}
|
|
152
|
+
<div class="aggregation-row">
|
|
153
|
+
<select
|
|
154
|
+
class="agg-function-select"
|
|
155
|
+
value={agg.function}
|
|
156
|
+
on:change={(e) => handleAggFunctionChange(index, aggIndex, e.currentTarget.value)}
|
|
157
|
+
>
|
|
158
|
+
{#each aggregateFunctions as fn}
|
|
159
|
+
<option value={fn.value}>{fn.label}</option>
|
|
160
|
+
{/each}
|
|
161
|
+
</select>
|
|
162
|
+
|
|
163
|
+
<select
|
|
164
|
+
class="agg-column-select"
|
|
165
|
+
value={agg.column}
|
|
166
|
+
on:change={(e) =>
|
|
167
|
+
updateAggregation(index, aggIndex, {
|
|
168
|
+
column: e.currentTarget.value
|
|
169
|
+
})}
|
|
170
|
+
>
|
|
171
|
+
<option value="">Select column...</option>
|
|
172
|
+
{#if agg.function === 'count'}
|
|
173
|
+
<option value="*">All rows (*)</option>
|
|
174
|
+
{/if}
|
|
175
|
+
{#each agg.function === 'sum' || agg.function === 'avg' ? numericColumns : columns as col}
|
|
176
|
+
<option value={col.name}>{getColumnLabel(col)}</option>
|
|
177
|
+
{/each}
|
|
178
|
+
</select>
|
|
179
|
+
|
|
180
|
+
<button
|
|
181
|
+
class="remove-btn small"
|
|
182
|
+
on:click={() => removeAggregation(index, aggIndex)}
|
|
183
|
+
title="Remove aggregation"
|
|
184
|
+
type="button"
|
|
185
|
+
>
|
|
186
|
+
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
187
|
+
<path
|
|
188
|
+
stroke-linecap="round"
|
|
189
|
+
stroke-linejoin="round"
|
|
190
|
+
stroke-width="2"
|
|
191
|
+
d="M6 18L18 6M6 6l12 12"
|
|
192
|
+
/>
|
|
193
|
+
</svg>
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
{/each}
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
|
|
200
|
+
<button
|
|
201
|
+
class="add-agg-btn"
|
|
202
|
+
on:click={() => addAggregation(index)}
|
|
203
|
+
type="button"
|
|
204
|
+
>
|
|
205
|
+
+ Add aggregation
|
|
206
|
+
</button>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|
|
209
|
+
{/each}
|
|
210
|
+
</div>
|
|
211
|
+
{/if}
|
|
212
|
+
|
|
213
|
+
{#if canAddMore}
|
|
214
|
+
<button class="add-group-btn" on:click={addGroup}>
|
|
215
|
+
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
216
|
+
<path
|
|
217
|
+
stroke-linecap="round"
|
|
218
|
+
stroke-linejoin="round"
|
|
219
|
+
stroke-width="2"
|
|
220
|
+
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
|
221
|
+
/>
|
|
222
|
+
</svg>
|
|
223
|
+
{hasGroups ? 'Add subgroup' : 'Add group'}
|
|
224
|
+
</button>
|
|
225
|
+
{/if}
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<style>
|
|
231
|
+
.group-bar {
|
|
232
|
+
position: relative;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.group-toggle-btn {
|
|
236
|
+
display: inline-flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: 0.5rem;
|
|
239
|
+
padding: 0.5rem 1rem;
|
|
240
|
+
font-size: 0.875rem;
|
|
241
|
+
font-weight: 500;
|
|
242
|
+
color: #374151;
|
|
243
|
+
background: white;
|
|
244
|
+
border: 1px solid #d1d5db;
|
|
245
|
+
border-radius: 0.375rem;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
transition: all 0.2s;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.group-toggle-btn:hover {
|
|
251
|
+
background: #f9fafb;
|
|
252
|
+
border-color: #9ca3af;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.group-badge {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
min-width: 1.25rem;
|
|
260
|
+
height: 1.25rem;
|
|
261
|
+
padding: 0 0.375rem;
|
|
262
|
+
font-size: 0.75rem;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
color: white;
|
|
265
|
+
background: #059669;
|
|
266
|
+
border-radius: 0.75rem;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.chevron {
|
|
270
|
+
width: 1rem;
|
|
271
|
+
height: 1rem;
|
|
272
|
+
transition: transform 0.2s;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.chevron.expanded {
|
|
276
|
+
transform: rotate(180deg);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.group-panel {
|
|
280
|
+
position: absolute;
|
|
281
|
+
top: calc(100% + 0.5rem);
|
|
282
|
+
left: 0;
|
|
283
|
+
z-index: 20;
|
|
284
|
+
min-width: 450px;
|
|
285
|
+
padding: 1rem;
|
|
286
|
+
background: white;
|
|
287
|
+
border: 1px solid #e5e7eb;
|
|
288
|
+
border-radius: 0.5rem;
|
|
289
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.group-header {
|
|
293
|
+
display: flex;
|
|
294
|
+
justify-content: space-between;
|
|
295
|
+
align-items: center;
|
|
296
|
+
margin-bottom: 0.75rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.group-label {
|
|
300
|
+
font-size: 0.875rem;
|
|
301
|
+
font-weight: 600;
|
|
302
|
+
color: #374151;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.clear-all-btn {
|
|
306
|
+
font-size: 0.75rem;
|
|
307
|
+
color: #6b7280;
|
|
308
|
+
background: none;
|
|
309
|
+
border: none;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
padding: 0.25rem 0.5rem;
|
|
312
|
+
border-radius: 0.25rem;
|
|
313
|
+
transition: all 0.2s;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.clear-all-btn:hover {
|
|
317
|
+
color: #dc2626;
|
|
318
|
+
background: #fee2e2;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.group-levels {
|
|
322
|
+
display: flex;
|
|
323
|
+
flex-direction: column;
|
|
324
|
+
gap: 0.75rem;
|
|
325
|
+
margin-bottom: 0.75rem;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.group-level {
|
|
329
|
+
padding: 0.5rem;
|
|
330
|
+
background: #f9fafb;
|
|
331
|
+
border-radius: 0.375rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.group-level-row {
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 0.5rem;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.field-select {
|
|
341
|
+
flex: 1;
|
|
342
|
+
padding: 0.375rem 0.75rem;
|
|
343
|
+
font-size: 0.875rem;
|
|
344
|
+
border: 1px solid #d1d5db;
|
|
345
|
+
border-radius: 0.375rem;
|
|
346
|
+
background: white;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.field-select:focus {
|
|
350
|
+
outline: none;
|
|
351
|
+
border-color: #059669;
|
|
352
|
+
box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* Aggregation rows */
|
|
356
|
+
.aggregations {
|
|
357
|
+
display: flex;
|
|
358
|
+
flex-direction: column;
|
|
359
|
+
gap: 0.375rem;
|
|
360
|
+
margin-top: 0.5rem;
|
|
361
|
+
padding-left: 1rem;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.aggregation-row {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 0.375rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.agg-function-select,
|
|
371
|
+
.agg-column-select {
|
|
372
|
+
padding: 0.25rem 0.5rem;
|
|
373
|
+
font-size: 0.8125rem;
|
|
374
|
+
border: 1px solid #d1d5db;
|
|
375
|
+
border-radius: 0.25rem;
|
|
376
|
+
background: white;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.agg-function-select {
|
|
380
|
+
min-width: 90px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.agg-column-select {
|
|
384
|
+
flex: 1;
|
|
385
|
+
min-width: 120px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.agg-function-select:focus,
|
|
389
|
+
.agg-column-select:focus {
|
|
390
|
+
outline: none;
|
|
391
|
+
border-color: #059669;
|
|
392
|
+
box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.add-agg-btn {
|
|
396
|
+
display: inline-block;
|
|
397
|
+
margin-top: 0.375rem;
|
|
398
|
+
margin-left: 1rem;
|
|
399
|
+
padding: 0.25rem 0.5rem;
|
|
400
|
+
font-size: 0.75rem;
|
|
401
|
+
color: #6b7280;
|
|
402
|
+
background: none;
|
|
403
|
+
border: none;
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
border-radius: 0.25rem;
|
|
406
|
+
transition: all 0.15s;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.add-agg-btn:hover {
|
|
410
|
+
color: #059669;
|
|
411
|
+
background: #d1fae5;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.remove-btn {
|
|
415
|
+
flex-shrink: 0;
|
|
416
|
+
padding: 0.375rem;
|
|
417
|
+
background: none;
|
|
418
|
+
border: none;
|
|
419
|
+
color: #6b7280;
|
|
420
|
+
cursor: pointer;
|
|
421
|
+
border-radius: 0.25rem;
|
|
422
|
+
transition: all 0.2s;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.remove-btn:hover {
|
|
426
|
+
background: #fee2e2;
|
|
427
|
+
color: #dc2626;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.remove-btn.small {
|
|
431
|
+
padding: 0.25rem;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.add-group-btn {
|
|
435
|
+
display: inline-flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
gap: 0.5rem;
|
|
438
|
+
padding: 0.5rem 0.75rem;
|
|
439
|
+
font-size: 0.875rem;
|
|
440
|
+
font-weight: 500;
|
|
441
|
+
color: #059669;
|
|
442
|
+
background: white;
|
|
443
|
+
border: 1px dashed #059669;
|
|
444
|
+
border-radius: 0.375rem;
|
|
445
|
+
cursor: pointer;
|
|
446
|
+
transition: all 0.2s;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.add-group-btn:hover {
|
|
450
|
+
background: #d1fae5;
|
|
451
|
+
border-style: solid;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.icon {
|
|
455
|
+
width: 1rem;
|
|
456
|
+
height: 1rem;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.icon-sm {
|
|
460
|
+
width: 0.875rem;
|
|
461
|
+
height: 0.875rem;
|
|
462
|
+
}
|
|
463
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
/**
|
|
3
|
+
* GroupBar — Multi-level grouping controls with aggregation support.
|
|
4
|
+
*
|
|
5
|
+
* Emits GroupConfig[] which maps to GROUP BY with SUM/AVG/COUNT/MIN/MAX
|
|
6
|
+
* in the query builder. Up to 3 nested group levels.
|
|
7
|
+
*
|
|
8
|
+
* PGLite-native: uses GroupConfig with AggregationConfig for SQL aggregates,
|
|
9
|
+
* unlike svelte-table-kit which used simple string[] grouping.
|
|
10
|
+
*/
|
|
11
|
+
import type { GroupConfig, ColumnMetadata, ColumnConfig } from '../types.js';
|
|
12
|
+
declare const __propDef: {
|
|
13
|
+
props: {
|
|
14
|
+
columns: ColumnMetadata[];
|
|
15
|
+
columnConfigs?: ColumnConfig[];
|
|
16
|
+
grouping?: GroupConfig[];
|
|
17
|
+
onGroupingChange: (grouping: GroupConfig[]) => void;
|
|
18
|
+
isExpanded?: boolean;
|
|
19
|
+
onExpandedChange?: ((expanded: boolean) => void) | undefined;
|
|
20
|
+
};
|
|
21
|
+
events: {
|
|
22
|
+
[evt: string]: CustomEvent<any>;
|
|
23
|
+
};
|
|
24
|
+
slots: {};
|
|
25
|
+
exports?: {} | undefined;
|
|
26
|
+
bindings?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
export type GroupBarProps = typeof __propDef.props;
|
|
29
|
+
export type GroupBarEvents = typeof __propDef.events;
|
|
30
|
+
export type GroupBarSlots = typeof __propDef.slots;
|
|
31
|
+
export default class GroupBar extends SvelteComponent<GroupBarProps, GroupBarEvents, GroupBarSlots> {
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script>import { onMount, onDestroy } from "svelte";
|
|
2
|
+
export let isOpen = false;
|
|
3
|
+
export let hasPrev = false;
|
|
4
|
+
export let hasNext = false;
|
|
5
|
+
export let onClose;
|
|
6
|
+
export let onPrev = void 0;
|
|
7
|
+
export let onNext = void 0;
|
|
8
|
+
let modalElement = null;
|
|
9
|
+
let previouslyFocused = null;
|
|
10
|
+
let mounted = false;
|
|
11
|
+
function handleClose() {
|
|
12
|
+
onClose();
|
|
13
|
+
}
|
|
14
|
+
function handlePrev() {
|
|
15
|
+
if (hasPrev && onPrev) onPrev();
|
|
16
|
+
}
|
|
17
|
+
function handleNext() {
|
|
18
|
+
if (hasNext && onNext) onNext();
|
|
19
|
+
}
|
|
20
|
+
function handleKeydown(event) {
|
|
21
|
+
if (event.key === "Escape") {
|
|
22
|
+
onClose();
|
|
23
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
if (hasPrev && onPrev) onPrev();
|
|
26
|
+
} else if (event.key === "ArrowRight" || event.key === "ArrowDown") {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
if (hasNext && onNext) onNext();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function handleBackdropClick(event) {
|
|
32
|
+
if (event.target === event.currentTarget) {
|
|
33
|
+
onClose();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
$: if (mounted) {
|
|
37
|
+
if (isOpen) {
|
|
38
|
+
previouslyFocused = document.activeElement;
|
|
39
|
+
document.addEventListener("keydown", handleKeydown);
|
|
40
|
+
document.body.style.overflow = "hidden";
|
|
41
|
+
requestAnimationFrame(() => {
|
|
42
|
+
modalElement?.focus();
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
document.removeEventListener("keydown", handleKeydown);
|
|
46
|
+
document.body.style.overflow = "";
|
|
47
|
+
if (previouslyFocused) {
|
|
48
|
+
previouslyFocused.focus();
|
|
49
|
+
previouslyFocused = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
onMount(() => {
|
|
54
|
+
mounted = true;
|
|
55
|
+
});
|
|
56
|
+
onDestroy(() => {
|
|
57
|
+
if (mounted) {
|
|
58
|
+
document.removeEventListener("keydown", handleKeydown);
|
|
59
|
+
document.body.style.overflow = "";
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
{#if isOpen}
|
|
65
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
66
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
67
|
+
<div class="row-detail-backdrop" on:click={handleBackdropClick}>
|
|
68
|
+
<div
|
|
69
|
+
class="row-detail-modal"
|
|
70
|
+
bind:this={modalElement}
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-modal="true"
|
|
73
|
+
tabindex="-1"
|
|
74
|
+
>
|
|
75
|
+
<div class="row-detail-header">
|
|
76
|
+
<div class="row-detail-nav">
|
|
77
|
+
<button
|
|
78
|
+
class="nav-btn"
|
|
79
|
+
disabled={!hasPrev}
|
|
80
|
+
on:click={handlePrev}
|
|
81
|
+
aria-label="Previous row"
|
|
82
|
+
title="Previous row"
|
|
83
|
+
>
|
|
84
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
85
|
+
<path
|
|
86
|
+
stroke-linecap="round"
|
|
87
|
+
stroke-linejoin="round"
|
|
88
|
+
stroke-width="2"
|
|
89
|
+
d="M15 19l-7-7 7-7"
|
|
90
|
+
/>
|
|
91
|
+
</svg>
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
class="nav-btn"
|
|
95
|
+
disabled={!hasNext}
|
|
96
|
+
on:click={handleNext}
|
|
97
|
+
aria-label="Next row"
|
|
98
|
+
title="Next row"
|
|
99
|
+
>
|
|
100
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
101
|
+
<path
|
|
102
|
+
stroke-linecap="round"
|
|
103
|
+
stroke-linejoin="round"
|
|
104
|
+
stroke-width="2"
|
|
105
|
+
d="M9 5l7 7-7 7"
|
|
106
|
+
/>
|
|
107
|
+
</svg>
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<button class="close-btn" on:click={handleClose} aria-label="Close" title="Close">
|
|
111
|
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
112
|
+
<path
|
|
113
|
+
stroke-linecap="round"
|
|
114
|
+
stroke-linejoin="round"
|
|
115
|
+
stroke-width="2"
|
|
116
|
+
d="M6 18L18 6M6 6l12 12"
|
|
117
|
+
/>
|
|
118
|
+
</svg>
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="row-detail-body">
|
|
123
|
+
<slot />
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{/if}
|
|
128
|
+
|
|
129
|
+
<style>
|
|
130
|
+
.row-detail-backdrop {
|
|
131
|
+
position: fixed;
|
|
132
|
+
inset: 0;
|
|
133
|
+
z-index: 1000;
|
|
134
|
+
background: rgba(0, 0, 0, 0.5);
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.row-detail-modal {
|
|
141
|
+
position: relative;
|
|
142
|
+
background: white;
|
|
143
|
+
border-radius: 0.75rem;
|
|
144
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
145
|
+
max-width: 48rem;
|
|
146
|
+
width: calc(100% - 2rem);
|
|
147
|
+
max-height: 80vh;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
outline: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.row-detail-header {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: space-between;
|
|
157
|
+
padding: 0.75rem 1rem;
|
|
158
|
+
border-bottom: 1px solid #e5e7eb;
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.row-detail-nav {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: 0.25rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.nav-btn {
|
|
169
|
+
display: inline-flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
padding: 0.375rem;
|
|
173
|
+
background: white;
|
|
174
|
+
border: 1px solid #d1d5db;
|
|
175
|
+
border-radius: 0.375rem;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
color: #374151;
|
|
178
|
+
transition: background 0.15s;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.nav-btn:hover:not(:disabled) {
|
|
182
|
+
background: #f9fafb;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.nav-btn:disabled {
|
|
186
|
+
opacity: 0.4;
|
|
187
|
+
cursor: not-allowed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.close-btn {
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
padding: 0.375rem;
|
|
195
|
+
background: none;
|
|
196
|
+
border: none;
|
|
197
|
+
border-radius: 0.375rem;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
color: #6b7280;
|
|
200
|
+
transition: all 0.15s;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.close-btn:hover {
|
|
204
|
+
background: #f3f4f6;
|
|
205
|
+
color: #374151;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.row-detail-body {
|
|
209
|
+
padding: 1.5rem;
|
|
210
|
+
overflow-y: auto;
|
|
211
|
+
flex: 1;
|
|
212
|
+
}
|
|
213
|
+
</style>
|