@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.
Files changed (43) hide show
  1. package/README.md +260 -0
  2. package/dist/GridLite.svelte +1361 -0
  3. package/dist/GridLite.svelte.d.ts +42 -0
  4. package/dist/components/CellContextMenu.svelte +209 -0
  5. package/dist/components/CellContextMenu.svelte.d.ts +28 -0
  6. package/dist/components/ColumnMenu.svelte +234 -0
  7. package/dist/components/ColumnMenu.svelte.d.ts +29 -0
  8. package/dist/components/ColumnPicker.svelte +403 -0
  9. package/dist/components/ColumnPicker.svelte.d.ts +29 -0
  10. package/dist/components/FilterBar.svelte +390 -0
  11. package/dist/components/FilterBar.svelte.d.ts +38 -0
  12. package/dist/components/FilterCondition.svelte +643 -0
  13. package/dist/components/FilterCondition.svelte.d.ts +35 -0
  14. package/dist/components/GroupBar.svelte +463 -0
  15. package/dist/components/GroupBar.svelte.d.ts +33 -0
  16. package/dist/components/RowDetailModal.svelte +213 -0
  17. package/dist/components/RowDetailModal.svelte.d.ts +25 -0
  18. package/dist/components/SortBar.svelte +232 -0
  19. package/dist/components/SortBar.svelte.d.ts +30 -0
  20. package/dist/components/SortCondition.svelte +129 -0
  21. package/dist/components/SortCondition.svelte.d.ts +30 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +29 -0
  24. package/dist/query/builder.d.ts +160 -0
  25. package/dist/query/builder.js +432 -0
  26. package/dist/query/live.d.ts +50 -0
  27. package/dist/query/live.js +118 -0
  28. package/dist/query/schema.d.ts +30 -0
  29. package/dist/query/schema.js +75 -0
  30. package/dist/state/migrations.d.ts +29 -0
  31. package/dist/state/migrations.js +113 -0
  32. package/dist/state/views.d.ts +54 -0
  33. package/dist/state/views.js +130 -0
  34. package/dist/styles/gridlite.css +966 -0
  35. package/dist/types.d.ts +164 -0
  36. package/dist/types.js +2 -0
  37. package/dist/utils/filters.d.ts +14 -0
  38. package/dist/utils/filters.js +49 -0
  39. package/dist/utils/formatters.d.ts +16 -0
  40. package/dist/utils/formatters.js +39 -0
  41. package/dist/utils/fuzzy.d.ts +47 -0
  42. package/dist/utils/fuzzy.js +142 -0
  43. package/package.json +76 -0
@@ -0,0 +1,390 @@
1
+ <script>import { quoteIdentifier } from "../query/builder.js";
2
+ import FilterConditionComponent from "./FilterCondition.svelte";
3
+ export let db;
4
+ export let table;
5
+ export let columns;
6
+ export let columnConfigs = [];
7
+ export let allowedColumns = [];
8
+ export let conditions = [];
9
+ export let onConditionsChange;
10
+ export let logic = "and";
11
+ export let onLogicChange;
12
+ export let isExpanded = false;
13
+ export let onExpandedChange = void 0;
14
+ let columnValuesCache = /* @__PURE__ */ new Map();
15
+ let numericRangeCache = /* @__PURE__ */ new Map();
16
+ async function getColumnValues(columnName) {
17
+ if (!columnName || !table) return [];
18
+ if (columnValuesCache.has(columnName)) {
19
+ return columnValuesCache.get(columnName);
20
+ }
21
+ try {
22
+ const quotedCol = quoteIdentifier(columnName, allowedColumns);
23
+ const sql = `SELECT DISTINCT ${quotedCol}::TEXT AS val FROM ${quoteIdentifier(table)} WHERE ${quotedCol} IS NOT NULL ORDER BY val LIMIT 200`;
24
+ const result = await db.query(sql);
25
+ const values = result.rows.map((r) => r.val);
26
+ columnValuesCache.set(columnName, values);
27
+ return values;
28
+ } catch {
29
+ columnValuesCache.set(columnName, []);
30
+ return [];
31
+ }
32
+ }
33
+ async function getNumericRange(columnName) {
34
+ if (!columnName || !table) return null;
35
+ if (numericRangeCache.has(columnName)) {
36
+ return numericRangeCache.get(columnName);
37
+ }
38
+ const col = columns.find((c) => c.name === columnName);
39
+ const cfg = columnConfigs.find((c) => c.name === columnName);
40
+ const dataType = cfg?.dataType ?? col?.dataType;
41
+ if (dataType !== "number") {
42
+ numericRangeCache.set(columnName, null);
43
+ return null;
44
+ }
45
+ try {
46
+ const quotedCol = quoteIdentifier(columnName, allowedColumns);
47
+ const sql = `SELECT MIN(${quotedCol})::NUMERIC AS min_val, MAX(${quotedCol})::NUMERIC AS max_val FROM ${quoteIdentifier(table)}`;
48
+ const result = await db.query(sql);
49
+ const row = result.rows[0];
50
+ if (row && row.min_val != null && row.max_val != null) {
51
+ const range = { min: Number(row.min_val), max: Number(row.max_val) };
52
+ numericRangeCache.set(columnName, range);
53
+ return range;
54
+ }
55
+ numericRangeCache.set(columnName, null);
56
+ return null;
57
+ } catch {
58
+ numericRangeCache.set(columnName, null);
59
+ return null;
60
+ }
61
+ }
62
+ let conditionValues = /* @__PURE__ */ new Map();
63
+ let conditionRanges = /* @__PURE__ */ new Map();
64
+ async function loadSuggestionsForCondition(conditionId, field) {
65
+ if (!field) return;
66
+ const values = await getColumnValues(field);
67
+ conditionValues.set(conditionId, values);
68
+ conditionValues = conditionValues;
69
+ const range = await getNumericRange(field);
70
+ conditionRanges.set(conditionId, range);
71
+ conditionRanges = conditionRanges;
72
+ }
73
+ $: {
74
+ for (const cond of conditions) {
75
+ if (cond.field) {
76
+ const cached = conditionValues.get(cond.id);
77
+ if (cached === void 0) {
78
+ loadSuggestionsForCondition(cond.id, cond.field);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ function generateId() {
84
+ return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
85
+ }
86
+ function addCondition() {
87
+ const newCondition = {
88
+ id: generateId(),
89
+ field: "",
90
+ operator: "equals",
91
+ value: ""
92
+ };
93
+ onConditionsChange([...conditions, newCondition]);
94
+ setExpanded(true);
95
+ }
96
+ function setExpanded(value) {
97
+ isExpanded = value;
98
+ if (onExpandedChange) {
99
+ onExpandedChange(value);
100
+ }
101
+ }
102
+ function updateCondition(index, updated) {
103
+ const prev = conditions[index];
104
+ const newConditions = [...conditions];
105
+ newConditions[index] = updated;
106
+ onConditionsChange(newConditions);
107
+ if (prev.field !== updated.field && updated.field) {
108
+ conditionValues.delete(updated.id);
109
+ conditionRanges.delete(updated.id);
110
+ loadSuggestionsForCondition(updated.id, updated.field);
111
+ }
112
+ }
113
+ function removeCondition(index) {
114
+ const removed = conditions[index];
115
+ const newConditions = conditions.filter((_, i) => i !== index);
116
+ onConditionsChange(newConditions);
117
+ conditionValues.delete(removed.id);
118
+ conditionRanges.delete(removed.id);
119
+ if (newConditions.length === 0) {
120
+ setExpanded(false);
121
+ }
122
+ }
123
+ function clearAllConditions() {
124
+ onConditionsChange([]);
125
+ conditionValues.clear();
126
+ conditionRanges.clear();
127
+ conditionValues = conditionValues;
128
+ conditionRanges = conditionRanges;
129
+ setExpanded(false);
130
+ }
131
+ export function invalidateCaches() {
132
+ columnValuesCache.clear();
133
+ numericRangeCache.clear();
134
+ conditionValues.clear();
135
+ conditionRanges.clear();
136
+ conditionValues = conditionValues;
137
+ conditionRanges = conditionRanges;
138
+ }
139
+ $: hasConditions = conditions.length > 0;
140
+ $: filterCount = conditions.filter(
141
+ (c) => c.field && (c.operator === "is_empty" || c.operator === "is_not_empty" || c.value !== null && c.value !== void 0 && c.value !== "")
142
+ ).length;
143
+ </script>
144
+
145
+ <div class="filter-bar">
146
+ <button class="filter-toggle-btn" on:click={() => setExpanded(!isExpanded)}>
147
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
148
+ <path
149
+ stroke-linecap="round"
150
+ stroke-linejoin="round"
151
+ stroke-width="2"
152
+ d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
153
+ />
154
+ </svg>
155
+ Filter
156
+ {#if filterCount > 0}
157
+ <span class="filter-badge">{filterCount}</span>
158
+ {/if}
159
+ <svg class="chevron" class:expanded={isExpanded} fill="none" stroke="currentColor" viewBox="0 0 24 24">
160
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
161
+ </svg>
162
+ </button>
163
+
164
+ {#if isExpanded}
165
+ <div class="filter-panel">
166
+ {#if hasConditions}
167
+ <div class="filter-header">
168
+ <button class="clear-all-btn" on:click={clearAllConditions}> Clear all </button>
169
+ </div>
170
+
171
+ <div class="filter-conditions">
172
+ {#each conditions as condition, index (condition.id)}
173
+ <div class="condition-row">
174
+ {#if index === 0}
175
+ <span class="filter-label">Where</span>
176
+ {:else}
177
+ <select
178
+ class="logic-select"
179
+ value={logic}
180
+ on:change={(e) => {
181
+ const newLogic = e.currentTarget.value;
182
+ onLogicChange(newLogic === 'or' ? 'or' : 'and');
183
+ }}
184
+ >
185
+ <option value="and">and</option>
186
+ <option value="or">or</option>
187
+ </select>
188
+ {/if}
189
+ <div class="condition-wrapper">
190
+ <FilterConditionComponent
191
+ {condition}
192
+ {columns}
193
+ {columnConfigs}
194
+ columnValues={conditionValues.get(condition.id) ?? []}
195
+ numericRange={conditionRanges.get(condition.id) ?? null}
196
+ onUpdate={(updated) => updateCondition(index, updated)}
197
+ onRemove={() => removeCondition(index)}
198
+ />
199
+ </div>
200
+ </div>
201
+ {/each}
202
+ </div>
203
+ {/if}
204
+
205
+ <button class="add-condition-btn" on:click={addCondition}>
206
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
207
+ <path
208
+ stroke-linecap="round"
209
+ stroke-linejoin="round"
210
+ stroke-width="2"
211
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
212
+ />
213
+ </svg>
214
+ Add condition
215
+ </button>
216
+ </div>
217
+ {/if}
218
+ </div>
219
+
220
+ <style>
221
+ .filter-bar {
222
+ position: relative;
223
+ }
224
+
225
+ .filter-toggle-btn {
226
+ display: inline-flex;
227
+ align-items: center;
228
+ gap: 0.5rem;
229
+ padding: 0.5rem 1rem;
230
+ font-size: 0.875rem;
231
+ font-weight: 500;
232
+ color: #374151;
233
+ background: white;
234
+ border: 1px solid #d1d5db;
235
+ border-radius: 0.375rem;
236
+ cursor: pointer;
237
+ transition: all 0.2s;
238
+ }
239
+
240
+ .filter-toggle-btn:hover {
241
+ background: #f9fafb;
242
+ border-color: #9ca3af;
243
+ }
244
+
245
+ .filter-badge {
246
+ display: inline-flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ min-width: 1.25rem;
250
+ height: 1.25rem;
251
+ padding: 0 0.375rem;
252
+ font-size: 0.75rem;
253
+ font-weight: 600;
254
+ color: white;
255
+ background: #4f46e5;
256
+ border-radius: 0.75rem;
257
+ }
258
+
259
+ .chevron {
260
+ width: 1rem;
261
+ height: 1rem;
262
+ transition: transform 0.2s;
263
+ }
264
+
265
+ .chevron.expanded {
266
+ transform: rotate(180deg);
267
+ }
268
+
269
+ .filter-panel {
270
+ position: absolute;
271
+ top: calc(100% + 0.5rem);
272
+ left: 0;
273
+ z-index: 20;
274
+ min-width: 600px;
275
+ padding: 1rem;
276
+ background: white;
277
+ border: 1px solid #e5e7eb;
278
+ border-radius: 0.5rem;
279
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
280
+ }
281
+
282
+ .filter-header {
283
+ display: flex;
284
+ justify-content: flex-end;
285
+ align-items: center;
286
+ margin-bottom: 0.75rem;
287
+ }
288
+
289
+ .filter-label {
290
+ flex-shrink: 0;
291
+ display: inline-flex;
292
+ align-items: center;
293
+ min-width: 60px;
294
+ padding: 0.375rem 0.5rem;
295
+ font-size: 0.75rem;
296
+ font-weight: 600;
297
+ color: #374151;
298
+ text-align: left;
299
+ height: fit-content;
300
+ margin-top: 0.5rem;
301
+ box-sizing: border-box;
302
+ }
303
+
304
+ .clear-all-btn {
305
+ font-size: 0.75rem;
306
+ color: #6b7280;
307
+ background: none;
308
+ border: none;
309
+ cursor: pointer;
310
+ padding: 0.25rem 0.5rem;
311
+ border-radius: 0.25rem;
312
+ transition: all 0.2s;
313
+ }
314
+
315
+ .clear-all-btn:hover {
316
+ color: #dc2626;
317
+ background: #fee2e2;
318
+ }
319
+
320
+ .filter-conditions {
321
+ display: flex;
322
+ flex-direction: column;
323
+ gap: 0.5rem;
324
+ margin-bottom: 0.75rem;
325
+ }
326
+
327
+ .condition-row {
328
+ display: flex;
329
+ align-items: flex-start;
330
+ gap: 0.5rem;
331
+ }
332
+
333
+ .logic-select {
334
+ flex-shrink: 0;
335
+ padding: 0.375rem 0.5rem;
336
+ font-size: 0.75rem;
337
+ font-weight: 600;
338
+ color: #6b7280;
339
+ text-transform: lowercase;
340
+ background: white;
341
+ border: 1px solid #d1d5db;
342
+ border-radius: 0.375rem;
343
+ cursor: pointer;
344
+ min-width: 60px;
345
+ height: fit-content;
346
+ margin-top: 0.5rem;
347
+ }
348
+
349
+ .logic-select:hover {
350
+ background: #f9fafb;
351
+ border-color: #9ca3af;
352
+ }
353
+
354
+ .logic-select:focus {
355
+ outline: none;
356
+ border-color: #4f46e5;
357
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
358
+ }
359
+
360
+ .condition-wrapper {
361
+ flex: 1;
362
+ display: flex;
363
+ flex-direction: column;
364
+ }
365
+
366
+ .add-condition-btn {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ gap: 0.5rem;
370
+ padding: 0.5rem 0.75rem;
371
+ font-size: 0.875rem;
372
+ font-weight: 500;
373
+ color: #4f46e5;
374
+ background: white;
375
+ border: 1px dashed #4f46e5;
376
+ border-radius: 0.375rem;
377
+ cursor: pointer;
378
+ transition: all 0.2s;
379
+ }
380
+
381
+ .add-condition-btn:hover {
382
+ background: #eef2ff;
383
+ border-style: solid;
384
+ }
385
+
386
+ .icon {
387
+ width: 1rem;
388
+ height: 1rem;
389
+ }
390
+ </style>
@@ -0,0 +1,38 @@
1
+ import { SvelteComponent } from "svelte";
2
+ /**
3
+ * FilterBar — Advanced filtering UI panel.
4
+ *
5
+ * PGLite-native: fetches column value suggestions via SELECT DISTINCT
6
+ * and numeric ranges via SELECT MIN(), MAX() — no in-memory data scanning.
7
+ */
8
+ import type { FilterCondition, FilterLogic, ColumnMetadata, ColumnConfig } from '../types.js';
9
+ import type { PGliteWithLive } from '../query/live.js';
10
+ declare const __propDef: {
11
+ props: {
12
+ /** PGLite instance for running suggestion queries */ db: PGliteWithLive;
13
+ /** Table name for suggestion queries */ table: string;
14
+ /** Introspected column metadata */ columns: ColumnMetadata[];
15
+ /** Column config overrides (labels, dataType, selectOptions) */ columnConfigs?: ColumnConfig[];
16
+ /** Allowed column names for query safety */ allowedColumns?: string[];
17
+ /** Current filter conditions */ conditions?: FilterCondition[];
18
+ onConditionsChange: (conditions: FilterCondition[]) => void;
19
+ /** Filter logic (AND/OR) */ logic?: FilterLogic;
20
+ onLogicChange: (logic: FilterLogic) => void;
21
+ /** Expand/collapse state */ isExpanded?: boolean;
22
+ onExpandedChange?: ((expanded: boolean) => void) | undefined;
23
+ invalidateCaches?: () => void;
24
+ };
25
+ events: {
26
+ [evt: string]: CustomEvent<any>;
27
+ };
28
+ slots: {};
29
+ exports?: {} | undefined;
30
+ bindings?: string | undefined;
31
+ };
32
+ export type FilterBarProps = typeof __propDef.props;
33
+ export type FilterBarEvents = typeof __propDef.events;
34
+ export type FilterBarSlots = typeof __propDef.slots;
35
+ export default class FilterBar extends SvelteComponent<FilterBarProps, FilterBarEvents, FilterBarSlots> {
36
+ get invalidateCaches(): () => void;
37
+ }
38
+ export {};