@shotleybuilder/svelte-gridlite-kit 0.2.1 → 0.3.1

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 CHANGED
@@ -156,12 +156,13 @@ The component accepts a **PGLite instance + table name** (or a raw SQL query), n
156
156
  | Numeric range hints | Scan all rows for min/max | `SELECT MIN(), MAX()` |
157
157
  | Column type detection | Sample rows and guess | Schema introspection |
158
158
  | Config persistence | localStorage JSON | PGLite tables with IndexedDB backing |
159
+ | Column labels | Static config only | User-editable, persisted in PGLite |
159
160
  | Global search | String matching across columns | `ILIKE` or full-text search |
160
161
 
161
162
  ### Key Design Decisions
162
163
 
163
164
  - **No TanStack Table dependency.** The SQL engine IS the table engine.
164
- - **PGLite is the state store.** Table configs, view presets, column visibility, sort/filter state — all stored in PGLite tables, persisted automatically via IndexedDB.
165
+ - **PGLite is the state store.** Table configs, view presets, column visibility, custom labels, sort/filter state — all stored in PGLite tables, persisted automatically via IndexedDB.
165
166
  - **FilterBar emits SQL.** Postgres operators (regex, `ILIKE`, date math, JSON paths, FTS) are available natively.
166
167
  - **Live queries drive reactivity.** PGLite `live.query()` replaces Svelte writable stores for data. UI auto-updates when underlying data changes.
167
168
  - **Column types come from schema introspection**, not data sampling.
@@ -1,5 +1,5 @@
1
1
  <script>import { onMount, onDestroy } from "svelte";
2
- import { introspectTable, getColumnNames } from "./query/schema.js";
2
+ import { introspectTable, getColumnNames, mapOidToDataType } from "./query/schema.js";
3
3
  import {
4
4
  buildQuery,
5
5
  buildCountQuery,
@@ -11,6 +11,7 @@ import {
11
11
  createLiveQueryStore
12
12
  } from "./query/live.js";
13
13
  import { runMigrations } from "./state/migrations.js";
14
+ import { loadColumnState, saveColumnState } from "./state/views.js";
14
15
  import FilterBar from "./components/FilterBar.svelte";
15
16
  import SortBar from "./components/SortBar.svelte";
16
17
  import GroupBar from "./components/GroupBar.svelte";
@@ -63,6 +64,9 @@ let resizeStartWidth = 0;
63
64
  const COL_MIN_WIDTH = 62;
64
65
  const COL_MAX_WIDTH = 1e3;
65
66
  const COL_DEFAULT_WIDTH = 180;
67
+ let customLabels = {};
68
+ let editingColumnLabel = null;
69
+ let editingLabelValue = "";
66
70
  let groupData = [];
67
71
  let expandedGroups = /* @__PURE__ */ new Set();
68
72
  let totalGroups = 0;
@@ -110,6 +114,20 @@ $: orderedColumns = (() => {
110
114
  $: validGrouping = grouping.filter((g) => g.column !== "");
111
115
  $: isGrouped = validGrouping.length > 0;
112
116
  $: nonGroupedColumns = isGrouped ? orderedColumns.filter((col) => !validGrouping.some((g) => g.column === col.name)) : orderedColumns;
117
+ $: mergedColumnConfigs = (() => {
118
+ const base = config?.columns ?? [];
119
+ if (Object.keys(customLabels).length === 0) return base;
120
+ const configMap = new Map(base.map((c) => [c.name, c]));
121
+ for (const [name, label] of Object.entries(customLabels)) {
122
+ const existing = configMap.get(name);
123
+ if (existing) {
124
+ configMap.set(name, { ...existing, label });
125
+ } else {
126
+ configMap.set(name, { name, label });
127
+ }
128
+ }
129
+ return [...configMap.values()];
130
+ })();
113
131
  function groupKey(group) {
114
132
  return Object.entries(group.values).map(([col, val]) => `${col}=${val === null || val === void 0 ? "__null__" : String(val)}`).join("::");
115
133
  }
@@ -124,6 +142,14 @@ async function init() {
124
142
  return;
125
143
  }
126
144
  }
145
+ if (config?.id) {
146
+ const savedState = await loadColumnState(db, config.id);
147
+ const labels = {};
148
+ for (const col of savedState) {
149
+ if (col.label) labels[col.name] = col.label;
150
+ }
151
+ if (Object.keys(labels).length > 0) customLabels = labels;
152
+ }
127
153
  initialized = true;
128
154
  await rebuildQuery();
129
155
  } catch (err) {
@@ -171,6 +197,16 @@ async function rebuildQuery() {
171
197
  store = createLiveQueryStore(db, sql, params);
172
198
  store.subscribe((state) => {
173
199
  storeState = state;
200
+ if (query && columns.length === 0 && state.fields.length > 0) {
201
+ columns = state.fields.map((f) => ({
202
+ name: f.name,
203
+ dataType: mapOidToDataType(f.dataTypeID),
204
+ postgresType: "unknown",
205
+ nullable: true,
206
+ hasDefault: false
207
+ }));
208
+ allowedColumns = columns.map((c) => c.name);
209
+ }
174
210
  });
175
211
  }
176
212
  function cleanAgg(g) {
@@ -478,13 +514,12 @@ function clearGlobalSearch() {
478
514
  }
479
515
  function handleCellContextMenu(event, row, col) {
480
516
  event.preventDefault();
481
- const colConfig = config?.columns?.find((c) => c.name === col.name);
482
517
  contextMenu = {
483
518
  x: event.clientX,
484
519
  y: event.clientY,
485
520
  value: row[col.name],
486
521
  columnName: col.name,
487
- columnLabel: colConfig?.label ?? col.name,
522
+ columnLabel: getColumnLabel(col),
488
523
  isNumeric: col.dataType === "number"
489
524
  };
490
525
  }
@@ -559,9 +594,60 @@ function handleColumnOrderChange(newOrder) {
559
594
  notifyStateChange();
560
595
  }
561
596
  function getColumnLabel(col) {
597
+ if (col.name in customLabels) return customLabels[col.name];
562
598
  const cfg = config?.columns?.find((c) => c.name === col.name);
563
599
  return cfg?.label ?? col.name;
564
600
  }
601
+ function startEditingLabel(columnName) {
602
+ const col = columns.find((c) => c.name === columnName);
603
+ if (!col) return;
604
+ editingColumnLabel = columnName;
605
+ editingLabelValue = getColumnLabel(col);
606
+ }
607
+ function commitLabelEdit() {
608
+ if (!editingColumnLabel) return;
609
+ const columnName = editingColumnLabel;
610
+ const newLabel = editingLabelValue.trim();
611
+ const cfg = config?.columns?.find((c) => c.name === columnName);
612
+ const defaultLabel = cfg?.label ?? columnName;
613
+ if (newLabel && newLabel !== defaultLabel) {
614
+ customLabels = { ...customLabels, [columnName]: newLabel };
615
+ } else {
616
+ const { [columnName]: _, ...rest } = customLabels;
617
+ customLabels = rest;
618
+ }
619
+ editingColumnLabel = null;
620
+ editingLabelValue = "";
621
+ persistColumnLabels();
622
+ notifyStateChange();
623
+ }
624
+ function cancelLabelEdit() {
625
+ editingColumnLabel = null;
626
+ editingLabelValue = "";
627
+ }
628
+ function handleLabelKeydown(event) {
629
+ if (event.key === "Enter") {
630
+ event.preventDefault();
631
+ commitLabelEdit();
632
+ } else if (event.key === "Escape") {
633
+ cancelLabelEdit();
634
+ }
635
+ }
636
+ async function persistColumnLabels() {
637
+ if (!config?.id) return;
638
+ try {
639
+ const colState = columns.map((col, i) => ({
640
+ name: col.name,
641
+ visible: isColumnVisible(col.name),
642
+ width: columnSizing[col.name] ?? void 0,
643
+ position: columnOrder.indexOf(col.name) >= 0 ? columnOrder.indexOf(col.name) : i,
644
+ label: customLabels[col.name] ?? null
645
+ }));
646
+ await saveColumnState(db, config.id, colState);
647
+ } catch (err) {
648
+ console.error("Failed to persist column labels:", err);
649
+ }
650
+ }
565
651
  function isColumnVisible(columnName) {
566
652
  if (columnName in columnVisibility) {
567
653
  return columnVisibility[columnName];
@@ -744,7 +830,7 @@ onDestroy(() => {
744
830
  </button>
745
831
  <ColumnPicker
746
832
  {columns}
747
- columnConfigs={config?.columns ?? []}
833
+ columnConfigs={mergedColumnConfigs}
748
834
  {columnVisibility}
749
835
  {columnOrder}
750
836
  isOpen={showColumnPicker}
@@ -763,7 +849,7 @@ onDestroy(() => {
763
849
  {db}
764
850
  {table}
765
851
  {columns}
766
- columnConfigs={config?.columns ?? []}
852
+ columnConfigs={mergedColumnConfigs}
767
853
  {allowedColumns}
768
854
  conditions={filters}
769
855
  onConditionsChange={handleFiltersChange}
@@ -780,7 +866,7 @@ onDestroy(() => {
780
866
  <div class="gridlite-toolbar-group">
781
867
  <GroupBar
782
868
  {columns}
783
- columnConfigs={config?.columns ?? []}
869
+ columnConfigs={mergedColumnConfigs}
784
870
  {grouping}
785
871
  onGroupingChange={handleGroupingChange}
786
872
  isExpanded={groupExpanded}
@@ -794,7 +880,7 @@ onDestroy(() => {
794
880
  <div class="gridlite-toolbar-sort">
795
881
  <SortBar
796
882
  {columns}
797
- columnConfigs={config?.columns ?? []}
883
+ columnConfigs={mergedColumnConfigs}
798
884
  {sorting}
799
885
  onSortingChange={handleSortingChange}
800
886
  isExpanded={sortExpanded}
@@ -941,7 +1027,7 @@ onDestroy(() => {
941
1027
  <div class="gridlite-toolbar-sort">
942
1028
  <SortBar
943
1029
  {columns}
944
- columnConfigs={config?.columns ?? []}
1030
+ columnConfigs={mergedColumnConfigs}
945
1031
  {sorting}
946
1032
  onSortingChange={handleSortingChange}
947
1033
  isExpanded={sortExpanded}
@@ -953,7 +1039,7 @@ onDestroy(() => {
953
1039
  <div class="gridlite-toolbar-group">
954
1040
  <GroupBar
955
1041
  {columns}
956
- columnConfigs={config?.columns ?? []}
1042
+ columnConfigs={mergedColumnConfigs}
957
1043
  {grouping}
958
1044
  onGroupingChange={handleGroupingChange}
959
1045
  isExpanded={groupExpanded}
@@ -1052,14 +1138,27 @@ onDestroy(() => {
1052
1138
  on:dragend={handleDragEnd}
1053
1139
  style={features.columnReordering ? 'cursor: grab;' : ''}
1054
1140
  >
1055
- <span class="gridlite-th-label">
1056
- {#if config?.columns}
1057
- {@const colConfig = config.columns.find((c) => c.name === col.name)}
1058
- {colConfig?.label ?? col.name}
1059
- {:else}
1060
- {col.name}
1061
- {/if}
1062
- </span>
1141
+ {#if editingColumnLabel === col.name}
1142
+ <!-- svelte-ignore a11y-autofocus -->
1143
+ <input
1144
+ class="gridlite-th-label-input"
1145
+ type="text"
1146
+ bind:value={editingLabelValue}
1147
+ on:blur={commitLabelEdit}
1148
+ on:keydown={handleLabelKeydown}
1149
+ on:click|stopPropagation
1150
+ autofocus
1151
+ />
1152
+ {:else}
1153
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
1154
+ <span
1155
+ class="gridlite-th-label"
1156
+ on:dblclick|stopPropagation={() => startEditingLabel(col.name)}
1157
+ title="Double-click to rename"
1158
+ >
1159
+ {getColumnLabel(col)}
1160
+ </span>
1161
+ {/if}
1063
1162
  {#if table}
1064
1163
  <button
1065
1164
  class="gridlite-th-menu-btn"
@@ -1285,7 +1384,7 @@ onDestroy(() => {
1285
1384
  <div class="gridlite-aggrid-sidebar-header">Columns</div>
1286
1385
  <ColumnPicker
1287
1386
  {columns}
1288
- columnConfigs={config?.columns ?? []}
1387
+ columnConfigs={mergedColumnConfigs}
1289
1388
  {columnVisibility}
1290
1389
  {columnOrder}
1291
1390
  isOpen={true}
@@ -1303,7 +1402,7 @@ onDestroy(() => {
1303
1402
  {db}
1304
1403
  table={table ?? ''}
1305
1404
  {columns}
1306
- columnConfigs={config?.columns ?? []}
1405
+ columnConfigs={mergedColumnConfigs}
1307
1406
  {allowedColumns}
1308
1407
  conditions={filters}
1309
1408
  onConditionsChange={handleFiltersChange}
@@ -1350,17 +1449,10 @@ onDestroy(() => {
1350
1449
  <dl class="gridlite-row-detail">
1351
1450
  {#each orderedColumns as col}
1352
1451
  <div class="gridlite-row-detail-field">
1353
- <dt>
1354
- {#if config?.columns}
1355
- {@const colConfig = config.columns.find((c) => c.name === col.name)}
1356
- {colConfig?.label ?? col.name}
1357
- {:else}
1358
- {col.name}
1359
- {/if}
1360
- </dt>
1452
+ <dt>{getColumnLabel(col)}</dt>
1361
1453
  <dd>
1362
- {#if config?.columns}
1363
- {@const colConfig = config.columns.find((c) => c.name === col.name)}
1454
+ {#if mergedColumnConfigs.length > 0}
1455
+ {@const colConfig = mergedColumnConfigs.find((c) => c.name === col.name)}
1364
1456
  {#if colConfig?.format}
1365
1457
  {colConfig.format(rowDetailRow[col.name])}
1366
1458
  {:else}
@@ -22,6 +22,8 @@ declare const __propDef: {
22
22
  setGlobalFilter?: (search: string) => void;
23
23
  };
24
24
  events: {
25
+ click: PointerEvent;
26
+ } & {
25
27
  [evt: string]: CustomEvent<any>;
26
28
  };
27
29
  slots: {
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
11
11
  export type { GridLiteProps, GridConfig, GridFeatures, GridState, ColumnConfig, ColumnDataType, ColumnMetadata, FilterCondition, FilterOperator, FilterLogic, SortConfig, GroupConfig, AggregationConfig, AggregateFunction, ViewPreset, ClassNameMap, RowHeight, ColumnSpacing, ParameterizedQuery, ToolbarLayout, } from "./types.js";
12
12
  export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
13
13
  export type { QueryOptions, GroupSummaryOptions, GroupDetailOptions, } from "./query/builder.js";
14
- export { mapPostgresType, introspectTable, getColumnNames, } from "./query/schema.js";
14
+ export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
15
15
  export { createLiveQueryStore, createLiveQueryStoreFromQuery, } from "./query/live.js";
16
16
  export type { LiveQueryState, LiveQueryStore, PGliteWithLive, } from "./query/live.js";
17
17
  export { runMigrations, getLatestVersion, isMigrated, } from "./state/migrations.js";
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
14
14
  // Query builder
15
15
  export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
16
16
  // Schema introspection
17
- export { mapPostgresType, introspectTable, getColumnNames, } from "./query/schema.js";
17
+ export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
18
18
  // Live query store
19
19
  export { createLiveQueryStore, createLiveQueryStoreFromQuery, } from "./query/live.js";
20
20
  // State persistence — migrations
@@ -5,13 +5,18 @@
5
5
  * and nullability for a given table. Maps Postgres data types to
6
6
  * GridLite's ColumnDataType for filter operator selection and UI rendering.
7
7
  */
8
- import type { PGlite } from '@electric-sql/pglite';
9
- import type { ColumnDataType, ColumnMetadata } from '../types.js';
8
+ import type { PGlite } from "@electric-sql/pglite";
9
+ import type { ColumnDataType, ColumnMetadata } from "../types.js";
10
10
  /**
11
11
  * Map a Postgres data_type string (from information_schema.columns)
12
12
  * to a GridLite ColumnDataType.
13
13
  */
14
14
  export declare function mapPostgresType(postgresType: string): ColumnDataType;
15
+ /**
16
+ * Map a Postgres type OID to a GridLite ColumnDataType.
17
+ * Returns 'text' for unrecognized OIDs.
18
+ */
19
+ export declare function mapOidToDataType(oid: number): ColumnDataType;
15
20
  /**
16
21
  * Introspect a table's schema using information_schema.columns.
17
22
  *
@@ -13,34 +13,69 @@
13
13
  export function mapPostgresType(postgresType) {
14
14
  const t = postgresType.toLowerCase();
15
15
  // Numeric types
16
- if (t === 'integer' ||
17
- t === 'bigint' ||
18
- t === 'smallint' ||
19
- t === 'numeric' ||
20
- t === 'decimal' ||
21
- t === 'real' ||
22
- t === 'double precision' ||
23
- t === 'serial' ||
24
- t === 'bigserial' ||
25
- t === 'smallserial' ||
26
- t === 'money') {
27
- return 'number';
16
+ if (t === "integer" ||
17
+ t === "bigint" ||
18
+ t === "smallint" ||
19
+ t === "numeric" ||
20
+ t === "decimal" ||
21
+ t === "real" ||
22
+ t === "double precision" ||
23
+ t === "serial" ||
24
+ t === "bigserial" ||
25
+ t === "smallserial" ||
26
+ t === "money") {
27
+ return "number";
28
28
  }
29
29
  // Date/time types
30
- if (t === 'date' ||
31
- t === 'timestamp without time zone' ||
32
- t === 'timestamp with time zone' ||
33
- t === 'time without time zone' ||
34
- t === 'time with time zone' ||
35
- t === 'interval') {
36
- return 'date';
30
+ if (t === "date" ||
31
+ t === "timestamp without time zone" ||
32
+ t === "timestamp with time zone" ||
33
+ t === "time without time zone" ||
34
+ t === "time with time zone" ||
35
+ t === "interval") {
36
+ return "date";
37
37
  }
38
38
  // Boolean
39
- if (t === 'boolean') {
40
- return 'boolean';
39
+ if (t === "boolean") {
40
+ return "boolean";
41
41
  }
42
42
  // Everything else is text (varchar, char, text, json, jsonb, uuid, etc.)
43
- return 'text';
43
+ return "text";
44
+ }
45
+ // ─── OID → ColumnDataType Mapping ───────────────────────────────────────────
46
+ /**
47
+ * Map a Postgres type OID (from query result fields) to a GridLite ColumnDataType.
48
+ * Used when introspecting columns from raw query results rather than information_schema.
49
+ *
50
+ * Common OIDs from the Postgres catalog (pg_type):
51
+ * https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
52
+ */
53
+ const OID_MAP = {
54
+ // Boolean
55
+ 16: "boolean",
56
+ // Numeric
57
+ 20: "number", // int8 / bigint
58
+ 21: "number", // int2 / smallint
59
+ 23: "number", // int4 / integer
60
+ 26: "number", // oid
61
+ 700: "number", // float4 / real
62
+ 701: "number", // float8 / double precision
63
+ 790: "number", // money
64
+ 1700: "number", // numeric / decimal
65
+ // Date/time
66
+ 1082: "date", // date
67
+ 1083: "date", // time
68
+ 1114: "date", // timestamp without time zone
69
+ 1184: "date", // timestamp with time zone
70
+ 1186: "date", // interval
71
+ 1266: "date", // time with time zone
72
+ };
73
+ /**
74
+ * Map a Postgres type OID to a GridLite ColumnDataType.
75
+ * Returns 'text' for unrecognized OIDs.
76
+ */
77
+ export function mapOidToDataType(oid) {
78
+ return OID_MAP[oid] ?? "text";
44
79
  }
45
80
  /**
46
81
  * Introspect a table's schema using information_schema.columns.
@@ -52,7 +87,7 @@ export function mapPostgresType(postgresType) {
52
87
  * @param tableName - The table to introspect
53
88
  * @param schema - The schema to search (defaults to 'public')
54
89
  */
55
- export async function introspectTable(db, tableName, schema = 'public') {
90
+ export async function introspectTable(db, tableName, schema = "public") {
56
91
  const result = await db.query(`SELECT column_name, data_type, is_nullable, column_default
57
92
  FROM information_schema.columns
58
93
  WHERE table_name = $1 AND table_schema = $2
@@ -61,15 +96,15 @@ export async function introspectTable(db, tableName, schema = 'public') {
61
96
  name: row.column_name,
62
97
  dataType: mapPostgresType(row.data_type),
63
98
  postgresType: row.data_type,
64
- nullable: row.is_nullable === 'YES',
65
- hasDefault: row.column_default !== null
99
+ nullable: row.is_nullable === "YES",
100
+ hasDefault: row.column_default !== null,
66
101
  }));
67
102
  }
68
103
  /**
69
104
  * Get the list of column names for a table.
70
105
  * Useful for the query builder's allowedColumns parameter.
71
106
  */
72
- export async function getColumnNames(db, tableName, schema = 'public') {
107
+ export async function getColumnNames(db, tableName, schema = "public") {
73
108
  const columns = await introspectTable(db, tableName, schema);
74
109
  return columns.map((c) => c.name);
75
110
  }
@@ -51,6 +51,13 @@ const MIGRATIONS = [
51
51
  ON _gridlite_column_state (grid_id);
52
52
  `,
53
53
  },
54
+ {
55
+ version: 2,
56
+ description: "Add label column to column_state for user-editable column names",
57
+ sql: `
58
+ ALTER TABLE _gridlite_column_state ADD COLUMN IF NOT EXISTS label TEXT;
59
+ `,
60
+ },
54
61
  ];
55
62
  // ─── Migration Runner ───────────────────────────────────────────────────────
56
63
  /**
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * Requires `runMigrations()` to have been called first.
9
9
  */
10
- import type { PGlite } from '@electric-sql/pglite';
11
- import type { ViewPreset } from '../types.js';
10
+ import type { PGlite } from "@electric-sql/pglite";
11
+ import type { ViewPreset } from "../types.js";
12
12
  /**
13
13
  * Save a view configuration. Creates or updates (upserts).
14
14
  */
@@ -42,6 +42,7 @@ export declare function saveColumnState(db: PGlite, gridId: string, columns: {
42
42
  visible?: boolean;
43
43
  width?: number;
44
44
  position?: number;
45
+ label?: string | null;
45
46
  }[], viewId?: string): Promise<void>;
46
47
  /**
47
48
  * Load column state for a grid (optionally scoped to a view).
@@ -51,4 +52,5 @@ export declare function loadColumnState(db: PGlite, gridId: string, viewId?: str
51
52
  visible: boolean;
52
53
  width: number | null;
53
54
  position: number | null;
55
+ label: string | null;
54
56
  }[]>;
@@ -30,11 +30,11 @@ export async function saveView(db, gridId, view) {
30
30
  view.name,
31
31
  view.description ?? null,
32
32
  JSON.stringify(view.filters ?? []),
33
- view.filterLogic ?? 'and',
33
+ view.filterLogic ?? "and",
34
34
  JSON.stringify(view.sorting ?? []),
35
35
  JSON.stringify(view.grouping ?? []),
36
36
  JSON.stringify(view.columnVisibility ?? {}),
37
- JSON.stringify(view.columnOrder ?? [])
37
+ JSON.stringify(view.columnOrder ?? []),
38
38
  ]);
39
39
  }
40
40
  /**
@@ -76,7 +76,9 @@ export async function setDefaultView(db, gridId, viewId) {
76
76
  */
77
77
  export async function deleteView(db, viewId) {
78
78
  // Also clean up associated column state
79
- await db.query(`DELETE FROM _gridlite_column_state WHERE view_id = $1`, [viewId]);
79
+ await db.query(`DELETE FROM _gridlite_column_state WHERE view_id = $1`, [
80
+ viewId,
81
+ ]);
80
82
  await db.query(`DELETE FROM _gridlite_views WHERE id = $1`, [viewId]);
81
83
  }
82
84
  // ─── Column State CRUD ──────────────────────────────────────────────────────
@@ -84,26 +86,27 @@ export async function deleteView(db, viewId) {
84
86
  * Save column state for a grid (optionally scoped to a view).
85
87
  * Replaces all existing column state for the given grid+view.
86
88
  */
87
- export async function saveColumnState(db, gridId, columns, viewId = '__default__') {
89
+ export async function saveColumnState(db, gridId, columns, viewId = "__default__") {
88
90
  // Clear existing state for this grid+view
89
91
  await db.query(`DELETE FROM _gridlite_column_state WHERE grid_id = $1 AND view_id = $2`, [gridId, viewId]);
90
92
  // Insert new state
91
93
  for (const col of columns) {
92
- await db.query(`INSERT INTO _gridlite_column_state (grid_id, view_id, column_name, visible, width, position)
93
- VALUES ($1, $2, $3, $4, $5, $6)`, [
94
+ await db.query(`INSERT INTO _gridlite_column_state (grid_id, view_id, column_name, visible, width, position, label)
95
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
94
96
  gridId,
95
97
  viewId,
96
98
  col.name,
97
99
  col.visible ?? true,
98
100
  col.width ?? null,
99
- col.position ?? null
101
+ col.position ?? null,
102
+ col.label ?? null,
100
103
  ]);
101
104
  }
102
105
  }
103
106
  /**
104
107
  * Load column state for a grid (optionally scoped to a view).
105
108
  */
106
- export async function loadColumnState(db, gridId, viewId = '__default__') {
109
+ export async function loadColumnState(db, gridId, viewId = "__default__") {
107
110
  const result = await db.query(`SELECT * FROM _gridlite_column_state
108
111
  WHERE grid_id = $1 AND view_id = $2
109
112
  ORDER BY position NULLS LAST, column_name`, [gridId, viewId]);
@@ -111,7 +114,8 @@ export async function loadColumnState(db, gridId, viewId = '__default__') {
111
114
  name: row.column_name,
112
115
  visible: row.visible,
113
116
  width: row.width,
114
- position: row.position
117
+ position: row.position,
118
+ label: row.label,
115
119
  }));
116
120
  }
117
121
  // ─── Helpers ────────────────────────────────────────────────────────────────
@@ -125,6 +129,6 @@ function rowToViewPreset(row) {
125
129
  sorting: row.sorting,
126
130
  grouping: row.grouping,
127
131
  columnVisibility: row.column_visibility,
128
- columnOrder: row.column_order
132
+ columnOrder: row.column_order,
129
133
  };
130
134
  }
@@ -752,6 +752,21 @@
752
752
 
753
753
  .gridlite-th-label {
754
754
  flex: 1;
755
+ cursor: default;
756
+ }
757
+
758
+ .gridlite-th-label-input {
759
+ flex: 1;
760
+ font: inherit;
761
+ font-weight: inherit;
762
+ font-size: inherit;
763
+ padding: 0 2px;
764
+ margin: -1px 0;
765
+ border: 1px solid #3b82f6;
766
+ border-radius: 3px;
767
+ outline: none;
768
+ background: #fff;
769
+ min-width: 40px;
755
770
  }
756
771
 
757
772
  .gridlite-th-menu-btn {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotleybuilder/svelte-gridlite-kit",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "A SQL-native data grid component for Svelte and SvelteKit, powered by PGLite",
5
5
  "author": "Sertantai",
6
6
  "license": "MIT",