bolt-table 0.1.37 → 0.1.38

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/dist/index.d.mts CHANGED
@@ -251,8 +251,30 @@ interface AIColumnVisibilityOperation {
251
251
  type: "hideColumns" | "showColumns";
252
252
  columns: string[];
253
253
  }
254
+ /** Resize a column to a specific width. */
255
+ interface AIResizeColumnOperation {
256
+ type: "resizeColumn";
257
+ column: string;
258
+ width: number;
259
+ }
260
+ /** Reorder columns by moving a column to a new position. */
261
+ interface AIReorderColumnsOperation {
262
+ type: "reorderColumns";
263
+ order: string[];
264
+ }
265
+ /** Pin or unpin a column. */
266
+ interface AIPinColumnOperation {
267
+ type: "pinColumn";
268
+ column: string;
269
+ pinned: "left" | "right" | false;
270
+ }
271
+ /** Set the current page number. */
272
+ interface AISetPageOperation {
273
+ type: "setPage";
274
+ page: number;
275
+ }
254
276
  /** Union of all possible AI operations. */
255
- type AIOperation = AIFilterOperation | AIStyleOperation | AICellStyleOperation | AISortOperation | AIColumnVisibilityOperation;
277
+ type AIOperation = AIFilterOperation | AIStyleOperation | AICellStyleOperation | AISortOperation | AIColumnVisibilityOperation | AIResizeColumnOperation | AIReorderColumnsOperation | AIPinColumnOperation | AISetPageOperation;
256
278
  /** The structured response returned by the AI. */
257
279
  interface AIResponse {
258
280
  operations: AIOperation[];
@@ -595,4 +617,4 @@ interface TableBodyProps {
595
617
  }
596
618
  declare const TableBody: React$1.FC<TableBodyProps>;
597
619
 
598
- export { type AICellStyleOperation, type AIColumnVisibilityOperation, type AICondition, type AIFilterOperation, type AIOperation, type AIOperator, type AIResponse, type AISortOperation, type AIStyleOperation, BoltTable, type BoltTableAIConfig, type BoltTableConfig, type BoltTableIcons, type CellContextMenuItem, type ClassNamesTypes, type ColumnContextMenuItem, type ColumnPersistenceConfig, type ColumnType, type DataRecord, DraggableHeader, type ExpandableConfig, type PaginationType, ResizeOverlay, type RowPinningConfig, type RowSelectionConfig, type SortDirection, type StylesTypes, TableBody, defineConfig };
620
+ export { type AICellStyleOperation, type AIColumnVisibilityOperation, type AICondition, type AIFilterOperation, type AIOperation, type AIOperator, type AIPinColumnOperation, type AIReorderColumnsOperation, type AIResizeColumnOperation, type AIResponse, type AISetPageOperation, type AISortOperation, type AIStyleOperation, BoltTable, type BoltTableAIConfig, type BoltTableConfig, type BoltTableIcons, type CellContextMenuItem, type ClassNamesTypes, type ColumnContextMenuItem, type ColumnPersistenceConfig, type ColumnType, type DataRecord, DraggableHeader, type ExpandableConfig, type PaginationType, ResizeOverlay, type RowPinningConfig, type RowSelectionConfig, type SortDirection, type StylesTypes, TableBody, defineConfig };
package/dist/index.d.ts CHANGED
@@ -251,8 +251,30 @@ interface AIColumnVisibilityOperation {
251
251
  type: "hideColumns" | "showColumns";
252
252
  columns: string[];
253
253
  }
254
+ /** Resize a column to a specific width. */
255
+ interface AIResizeColumnOperation {
256
+ type: "resizeColumn";
257
+ column: string;
258
+ width: number;
259
+ }
260
+ /** Reorder columns by moving a column to a new position. */
261
+ interface AIReorderColumnsOperation {
262
+ type: "reorderColumns";
263
+ order: string[];
264
+ }
265
+ /** Pin or unpin a column. */
266
+ interface AIPinColumnOperation {
267
+ type: "pinColumn";
268
+ column: string;
269
+ pinned: "left" | "right" | false;
270
+ }
271
+ /** Set the current page number. */
272
+ interface AISetPageOperation {
273
+ type: "setPage";
274
+ page: number;
275
+ }
254
276
  /** Union of all possible AI operations. */
255
- type AIOperation = AIFilterOperation | AIStyleOperation | AICellStyleOperation | AISortOperation | AIColumnVisibilityOperation;
277
+ type AIOperation = AIFilterOperation | AIStyleOperation | AICellStyleOperation | AISortOperation | AIColumnVisibilityOperation | AIResizeColumnOperation | AIReorderColumnsOperation | AIPinColumnOperation | AISetPageOperation;
256
278
  /** The structured response returned by the AI. */
257
279
  interface AIResponse {
258
280
  operations: AIOperation[];
@@ -595,4 +617,4 @@ interface TableBodyProps {
595
617
  }
596
618
  declare const TableBody: React$1.FC<TableBodyProps>;
597
619
 
598
- export { type AICellStyleOperation, type AIColumnVisibilityOperation, type AICondition, type AIFilterOperation, type AIOperation, type AIOperator, type AIResponse, type AISortOperation, type AIStyleOperation, BoltTable, type BoltTableAIConfig, type BoltTableConfig, type BoltTableIcons, type CellContextMenuItem, type ClassNamesTypes, type ColumnContextMenuItem, type ColumnPersistenceConfig, type ColumnType, type DataRecord, DraggableHeader, type ExpandableConfig, type PaginationType, ResizeOverlay, type RowPinningConfig, type RowSelectionConfig, type SortDirection, type StylesTypes, TableBody, defineConfig };
620
+ export { type AICellStyleOperation, type AIColumnVisibilityOperation, type AICondition, type AIFilterOperation, type AIOperation, type AIOperator, type AIPinColumnOperation, type AIReorderColumnsOperation, type AIResizeColumnOperation, type AIResponse, type AISetPageOperation, type AISortOperation, type AIStyleOperation, BoltTable, type BoltTableAIConfig, type BoltTableConfig, type BoltTableIcons, type CellContextMenuItem, type ClassNamesTypes, type ColumnContextMenuItem, type ColumnPersistenceConfig, type ColumnType, type DataRecord, DraggableHeader, type ExpandableConfig, type PaginationType, ResizeOverlay, type RowPinningConfig, type RowSelectionConfig, type SortDirection, type StylesTypes, TableBody, defineConfig };
package/dist/index.js CHANGED
@@ -711,66 +711,68 @@ function detectColumnType(key, data) {
711
711
  }
712
712
  return { type: "string", sample: sampleStr };
713
713
  }
714
+ var cachedSchema = null;
715
+ function buildSchemaFingerprint(columns, dataLen) {
716
+ return columns.filter((c) => c.key !== "__select__" && c.key !== "__expand__").map((c) => c.key).join(",") + `:${dataLen}`;
717
+ }
714
718
  function buildSystemPrompt(columns, data) {
715
- const schemaLines = columns.filter((c) => c.key !== "__select__" && c.key !== "__expand__").map((c) => {
719
+ const fingerprint = buildSchemaFingerprint(columns, data.length);
720
+ if (cachedSchema?.fingerprint === fingerprint) {
721
+ return cachedSchema.prompt;
722
+ }
723
+ const cols = columns.filter((c) => c.key !== "__select__" && c.key !== "__expand__");
724
+ const schema = cols.map((c) => {
716
725
  const key = c.dataIndex ?? c.key;
717
726
  const title = typeof c.title === "string" ? c.title : c.key;
718
727
  const info = detectColumnType(key, data);
719
- return ` - key: "${c.key}", title: "${title}", dataIndex: "${key}", type: ${info.type}${info.sample ? ` (values: ${info.sample})` : ""}`;
728
+ const w = c.width ?? 150;
729
+ const pin = c.pinned ? `, pinned: "${c.pinned}"` : "";
730
+ const hidden = c.hidden ? ", hidden: true" : "";
731
+ const vals = info.sample ? "|vals: " + info.sample : "";
732
+ return ` ${c.key}|${key}|"${title}"|${info.type}|w:${w}${pin}${hidden}${vals}`;
720
733
  }).join("\n");
721
- const sample = data.slice(0, 5).map((row) => {
734
+ const sample = data.slice(0, 3).map((row) => {
722
735
  const obj = {};
723
- for (const col of columns) {
724
- if (col.key === "__select__" || col.key === "__expand__") continue;
736
+ for (const col of cols) {
725
737
  const di = col.dataIndex ?? col.key;
726
738
  obj[di] = row[di];
727
739
  }
728
740
  return obj;
729
741
  });
730
- return `You are a data table assistant. You help users query, filter, sort, and style tabular data.
731
- You MUST respond with ONLY a valid JSON object \u2014 no markdown fences, no explanation, no extra text.
732
-
733
- ## Table Schema
734
- ${schemaLines}
735
-
736
- ## Sample Data (first ${sample.length} of ${data.length} rows)
737
- ${JSON.stringify(sample, null, 2)}
738
-
739
- ## Available Operations
740
- Combine any of these in a single response:
742
+ const prompt = `Data table AI. Respond ONLY with valid JSON, no markdown/explanation.
741
743
 
742
- 1. **filter** \u2014 show only rows matching conditions
743
- { "type": "filter", "conditions": [{ "column": "<dataIndex>", "op": "<op>", "value": <val> }], "logic": "and" | "or" }
744
+ SCHEMA (key|dataIndex|title|type|width|flags|sample):
745
+ ${schema}
744
746
 
745
- 2. **rowStyle** \u2014 apply CSS styles to entire rows matching conditions
746
- { "type": "rowStyle", "conditions": [...], "logic": "and"|"or", "style": { "<cssProp>": "<value>" } }
747
+ SAMPLE (${sample.length}/${data.length} rows):
748
+ ${JSON.stringify(sample)}
747
749
 
748
- 3. **cellStyle** \u2014 apply CSS styles to a specific column's cells matching conditions
749
- { "type": "cellStyle", "column": "<dataIndex>", "conditions": [...], "logic": "and"|"or", "style": { "<cssProp>": "<value>" } }
750
+ COLUMN ORDER: [${cols.map((c) => `"${c.key}"`).join(",")}]
750
751
 
751
- 4. **sort** \u2014 sort data by a column
752
- { "type": "sort", "column": "<dataIndex>", "direction": "asc" | "desc" }
752
+ OPS (combine any):
753
+ filter: {type:"filter",conditions:[{column:"<dataIndex>",op:"<op>",value:<v>}],logic:"and"|"or"}
754
+ sort: {type:"sort",column:"<dataIndex>",direction:"asc"|"desc"}
755
+ rowStyle: {type:"rowStyle",conditions:[...],logic:"and"|"or",style:{cssProp:"val"}}
756
+ cellStyle: {type:"cellStyle",column:"<dataIndex>",conditions:[...],logic:"and"|"or",style:{cssProp:"val"}}
757
+ hideColumns: {type:"hideColumns",columns:["key",...]}
758
+ showColumns: {type:"showColumns",columns:["key",...]}
759
+ resizeColumn: {type:"resizeColumn",column:"<key>",width:<px>}
760
+ reorderColumns: {type:"reorderColumns",order:["key1","key2",...]} (full column order)
761
+ pinColumn: {type:"pinColumn",column:"<key>",pinned:"left"|"right"|false}
762
+ setPage: {type:"setPage",page:<number>}
753
763
 
754
- 5. **hideColumns** / **showColumns** \u2014 toggle column visibility
755
- { "type": "hideColumns" | "showColumns", "columns": ["<key>", ...] }
764
+ OPS: eq,neq,gt,gte,lt,lte,contains,notContains,startsWith,endsWith,in,notIn
756
765
 
757
- ## Operators
758
- eq, neq, gt, gte, lt, lte, contains, notContains, startsWith, endsWith, in, notIn
766
+ FORMAT: {"operations":[...],"message":"brief description"}
759
767
 
760
- ## Response format
761
- {
762
- "operations": [ ... ],
763
- "message": "Brief user-friendly description of what was applied"
764
- }
765
-
766
- ## Rules
767
- - Use the dataIndex values from the schema, not display titles.
768
- - For colors use semi-transparent values like "rgba(255,0,0,0.15)" so text stays readable.
769
- - CSS property names must be camelCase (e.g. "backgroundColor", "color", "fontWeight").
770
- - If the user asks to highlight / color / mark specific rows, use rowStyle or cellStyle.
771
- - If the user asks to show only certain rows, use filter.
772
- - You can combine filter + rowStyle + sort etc. in one response.
773
- - The "message" should be concise: what was done, in plain English.`;
768
+ RULES:
769
+ - Use dataIndex for data ops, key for column ops (hide/show/resize/reorder/pin).
770
+ - Colors: semi-transparent rgba. CSS props: camelCase.
771
+ - reorderColumns: provide FULL ordered array of ALL visible column keys.
772
+ - resizeColumn width: integer pixels (min 40, max 800).
773
+ - Combine multiple ops freely. Message: concise plain English.`;
774
+ cachedSchema = { fingerprint, prompt };
775
+ return prompt;
774
776
  }
775
777
  async function callAI(config, systemPrompt, userQuery) {
776
778
  const { provider, apiKey, model, baseUrl, maxTokens = 1024, temperature = 0.1 } = config;
@@ -936,6 +938,10 @@ function applyAIOperations(data, operations) {
936
938
  const cellStyleOps = [];
937
939
  const hideColumns = [];
938
940
  const showColumns = [];
941
+ const resizeOps = [];
942
+ let reorderOp = null;
943
+ const pinOps = [];
944
+ let setPageOp = null;
939
945
  for (const op of operations) {
940
946
  switch (op.type) {
941
947
  case "filter":
@@ -956,12 +962,24 @@ function applyAIOperations(data, operations) {
956
962
  case "showColumns":
957
963
  showColumns.push(...op.columns);
958
964
  break;
965
+ case "resizeColumn":
966
+ resizeOps.push(op);
967
+ break;
968
+ case "reorderColumns":
969
+ reorderOp = op;
970
+ break;
971
+ case "pinColumn":
972
+ pinOps.push(op);
973
+ break;
974
+ case "setPage":
975
+ setPageOp = op;
976
+ break;
959
977
  }
960
978
  }
961
979
  if (sortOp) {
962
980
  filteredData = applyAISort(filteredData, sortOp);
963
981
  }
964
- return { filteredData, sortOp, styleOps, cellStyleOps, hideColumns, showColumns };
982
+ return { filteredData, sortOp, styleOps, cellStyleOps, hideColumns, showColumns, resizeOps, reorderOp, pinOps, setPageOp };
965
983
  }
966
984
 
967
985
  // src/ResizeOverlay.tsx
@@ -2248,6 +2266,101 @@ function BoltTable({
2248
2266
  const [aiFilteredDataKeys, setAiFilteredDataKeys] = (0, import_react4.useState)(null);
2249
2267
  const [aiSortKey, setAiSortKey] = (0, import_react4.useState)(null);
2250
2268
  const [aiSortDir, setAiSortDir] = (0, import_react4.useState)(null);
2269
+ const aiFiltersStorageKey = columnPersistence && typeof columnPersistence === "object" ? `bt-ai-filters-${columnPersistence.storageKey}` : "bt-ai-filters";
2270
+ const [savedAIFilters, setSavedAIFilters] = (0, import_react4.useState)(() => {
2271
+ try {
2272
+ const raw = localStorage.getItem(aiFiltersStorageKey);
2273
+ return raw ? JSON.parse(raw) : [];
2274
+ } catch {
2275
+ return [];
2276
+ }
2277
+ });
2278
+ const [showSavedFilters, setShowSavedFilters] = (0, import_react4.useState)(false);
2279
+ const savedFiltersRef = (0, import_react4.useRef)(null);
2280
+ import_react4.default.useEffect(() => {
2281
+ if (!showSavedFilters) return;
2282
+ const close = (e) => {
2283
+ if (savedFiltersRef.current && !savedFiltersRef.current.contains(e.target)) {
2284
+ setShowSavedFilters(false);
2285
+ }
2286
+ };
2287
+ document.addEventListener("mousedown", close);
2288
+ return () => document.removeEventListener("mousedown", close);
2289
+ }, [showSavedFilters]);
2290
+ const saveCurrentAIFilter = (0, import_react4.useCallback)(() => {
2291
+ if (!aiResult) return;
2292
+ const label = aiQuery || aiResult.message;
2293
+ const entry = { label, operations: aiResult.operations, query: aiQuery };
2294
+ const next = [...savedAIFilters, entry];
2295
+ setSavedAIFilters(next);
2296
+ try {
2297
+ localStorage.setItem(aiFiltersStorageKey, JSON.stringify(next));
2298
+ } catch {
2299
+ }
2300
+ }, [aiResult, aiQuery, savedAIFilters, aiFiltersStorageKey]);
2301
+ const removeSavedFilter = (0, import_react4.useCallback)((index) => {
2302
+ const next = savedAIFilters.filter((_, i) => i !== index);
2303
+ setSavedAIFilters(next);
2304
+ try {
2305
+ localStorage.setItem(aiFiltersStorageKey, JSON.stringify(next));
2306
+ } catch {
2307
+ }
2308
+ }, [savedAIFilters, aiFiltersStorageKey]);
2309
+ const applySavedFilter = (0, import_react4.useCallback)((filter) => {
2310
+ const { filteredData, sortOp, styleOps: sOps, cellStyleOps: csOps, hideColumns: hideCols, showColumns: showCols, resizeOps, reorderOp, pinOps, setPageOp } = applyAIOperations(data, filter.operations);
2311
+ setAiStyleOps(sOps);
2312
+ setAiCellStyleOps(csOps);
2313
+ if (filter.operations.some((op) => op.type === "filter")) {
2314
+ const keySet = /* @__PURE__ */ new Set();
2315
+ filteredData.forEach((row, idx) => {
2316
+ const k = typeof rowKey === "function" ? rowKey(row) : String(row[typeof rowKey === "string" ? rowKey : "id"] ?? idx);
2317
+ keySet.add(k);
2318
+ });
2319
+ setAiFilteredDataKeys(keySet);
2320
+ } else {
2321
+ setAiFilteredDataKeys(null);
2322
+ }
2323
+ if (sortOp) {
2324
+ setAiSortKey(sortOp.column);
2325
+ setAiSortDir(sortOp.direction);
2326
+ } else {
2327
+ setAiSortKey(null);
2328
+ setAiSortDir(null);
2329
+ }
2330
+ if (hideCols.length > 0 || showCols.length > 0) {
2331
+ setColumns(
2332
+ (prev) => prev.map((col) => {
2333
+ if (hideCols.includes(col.key)) return { ...col, hidden: true };
2334
+ if (showCols.includes(col.key)) return { ...col, hidden: false };
2335
+ return col;
2336
+ })
2337
+ );
2338
+ }
2339
+ for (const rOp of resizeOps) {
2340
+ const w = Math.max(40, Math.min(800, rOp.width));
2341
+ setColumnWidths((prev) => {
2342
+ const n = new Map(prev);
2343
+ n.set(rOp.column, w);
2344
+ return n;
2345
+ });
2346
+ onColumnResize?.(rOp.column, w);
2347
+ }
2348
+ if (reorderOp) {
2349
+ setColumnOrder(reorderOp.order);
2350
+ onColumnOrderChange?.(reorderOp.order);
2351
+ }
2352
+ for (const pOp of pinOps) {
2353
+ setColumns(
2354
+ (prev) => prev.map((col) => col.key === pOp.column ? { ...col, pinned: pOp.pinned } : col)
2355
+ );
2356
+ onColumnPin?.(pOp.column, pOp.pinned);
2357
+ }
2358
+ if (setPageOp) {
2359
+ setInternalPage(setPageOp.page);
2360
+ }
2361
+ setAiResult({ operations: filter.operations, message: `Applied saved filter: ${filter.label}` });
2362
+ setShowSavedFilters(false);
2363
+ }, [data, rowKey, onColumnResize, onColumnOrderChange, onColumnPin]);
2251
2364
  const onAIResponseRef = (0, import_react4.useRef)(onAIResponse);
2252
2365
  onAIResponseRef.current = onAIResponse;
2253
2366
  const handleAISubmit = (0, import_react4.useCallback)(async () => {
@@ -2269,7 +2382,7 @@ function BoltTable({
2269
2382
  } else {
2270
2383
  throw new Error("AI mode requires either aiConfig or onAIQuery prop");
2271
2384
  }
2272
- const { filteredData, sortOp, styleOps: sOps, cellStyleOps: csOps, hideColumns, showColumns } = applyAIOperations(data, response.operations);
2385
+ const { filteredData, sortOp, styleOps: sOps, cellStyleOps: csOps, hideColumns: hideCols, showColumns: showCols, resizeOps, reorderOp, pinOps, setPageOp } = applyAIOperations(data, response.operations);
2273
2386
  setAiStyleOps(sOps);
2274
2387
  setAiCellStyleOps(csOps);
2275
2388
  if (response.operations.some((op) => op.type === "filter")) {
@@ -2289,15 +2402,37 @@ function BoltTable({
2289
2402
  setAiSortKey(null);
2290
2403
  setAiSortDir(null);
2291
2404
  }
2292
- if (hideColumns.length > 0 || showColumns.length > 0) {
2405
+ if (hideCols.length > 0 || showCols.length > 0) {
2293
2406
  setColumns(
2294
2407
  (prev) => prev.map((col) => {
2295
- if (hideColumns.includes(col.key)) return { ...col, hidden: true };
2296
- if (showColumns.includes(col.key)) return { ...col, hidden: false };
2408
+ if (hideCols.includes(col.key)) return { ...col, hidden: true };
2409
+ if (showCols.includes(col.key)) return { ...col, hidden: false };
2297
2410
  return col;
2298
2411
  })
2299
2412
  );
2300
2413
  }
2414
+ for (const rOp of resizeOps) {
2415
+ const w = Math.max(40, Math.min(800, rOp.width));
2416
+ setColumnWidths((prev) => {
2417
+ const n = new Map(prev);
2418
+ n.set(rOp.column, w);
2419
+ return n;
2420
+ });
2421
+ onColumnResize?.(rOp.column, w);
2422
+ }
2423
+ if (reorderOp) {
2424
+ setColumnOrder(reorderOp.order);
2425
+ onColumnOrderChange?.(reorderOp.order);
2426
+ }
2427
+ for (const pOp of pinOps) {
2428
+ setColumns(
2429
+ (prev) => prev.map((col) => col.key === pOp.column ? { ...col, pinned: pOp.pinned } : col)
2430
+ );
2431
+ onColumnPin?.(pOp.column, pOp.pinned);
2432
+ }
2433
+ if (setPageOp) {
2434
+ setInternalPage(setPageOp.page);
2435
+ }
2301
2436
  setAiResult(response);
2302
2437
  onAIResponseRef.current?.(response);
2303
2438
  } catch (err) {
@@ -3435,7 +3570,7 @@ function BoltTable({
3435
3570
  borderBottom: "1px solid rgba(128,128,128,0.2)",
3436
3571
  fontSize: 12,
3437
3572
  flexShrink: 0,
3438
- overflow: "hidden"
3573
+ zIndex: 20
3439
3574
  },
3440
3575
  children: [
3441
3576
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -3627,6 +3762,110 @@ function BoltTable({
3627
3762
  }
3628
3763
  )
3629
3764
  ] }),
3765
+ aiMode && savedAIFilters.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { ref: savedFiltersRef, style: { position: "relative", flexShrink: 0 }, children: [
3766
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3767
+ "button",
3768
+ {
3769
+ type: "button",
3770
+ onClick: () => setShowSavedFilters((p) => !p),
3771
+ style: {
3772
+ display: "flex",
3773
+ alignItems: "center",
3774
+ gap: 4,
3775
+ background: "none",
3776
+ border: "1px solid rgba(128,128,128,0.2)",
3777
+ borderRadius: 4,
3778
+ cursor: "pointer",
3779
+ padding: "4px 6px",
3780
+ color: "inherit",
3781
+ fontSize: 12
3782
+ },
3783
+ title: "Saved AI filters",
3784
+ children: [
3785
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }) }),
3786
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: savedAIFilters.length })
3787
+ ]
3788
+ }
3789
+ ),
3790
+ showSavedFilters && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3791
+ "div",
3792
+ {
3793
+ style: {
3794
+ position: "absolute",
3795
+ top: "100%",
3796
+ right: 0,
3797
+ zIndex: 99999,
3798
+ minWidth: 240,
3799
+ maxWidth: 360,
3800
+ maxHeight: 320,
3801
+ overflowY: "auto",
3802
+ borderRadius: 8,
3803
+ border: "1px solid rgba(128,128,128,0.2)",
3804
+ boxShadow: "0 4px 24px rgba(0,0,0,0.12)",
3805
+ backdropFilter: "blur(16px)",
3806
+ WebkitBackdropFilter: "blur(16px)",
3807
+ backgroundColor: "rgba(128,128,128,0.08)",
3808
+ padding: "4px 0",
3809
+ marginTop: 4
3810
+ },
3811
+ children: [
3812
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { padding: "6px 12px", fontSize: 11, opacity: 0.5, fontWeight: 600 }, children: "Saved Filters" }),
3813
+ savedAIFilters.map((f, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3814
+ "div",
3815
+ {
3816
+ style: {
3817
+ display: "flex",
3818
+ alignItems: "center",
3819
+ gap: 6,
3820
+ padding: "6px 12px",
3821
+ cursor: "pointer",
3822
+ fontSize: 12
3823
+ },
3824
+ onMouseEnter: (e) => {
3825
+ e.currentTarget.style.backgroundColor = "rgba(128,128,128,0.15)";
3826
+ },
3827
+ onMouseLeave: (e) => {
3828
+ e.currentTarget.style.backgroundColor = "transparent";
3829
+ },
3830
+ children: [
3831
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3832
+ "span",
3833
+ {
3834
+ style: { flex: "1 1 0%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
3835
+ onClick: () => applySavedFilter(f),
3836
+ children: f.label
3837
+ }
3838
+ ),
3839
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3840
+ "button",
3841
+ {
3842
+ type: "button",
3843
+ onClick: (e) => {
3844
+ e.stopPropagation();
3845
+ removeSavedFilter(i);
3846
+ },
3847
+ style: {
3848
+ display: "flex",
3849
+ alignItems: "center",
3850
+ background: "none",
3851
+ border: "none",
3852
+ cursor: "pointer",
3853
+ padding: 2,
3854
+ color: "GrayText",
3855
+ flexShrink: 0
3856
+ },
3857
+ title: "Remove",
3858
+ children: icons?.close ?? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(XIcon, { style: { width: 10, height: 10 } })
3859
+ }
3860
+ )
3861
+ ]
3862
+ },
3863
+ i
3864
+ ))
3865
+ ]
3866
+ }
3867
+ )
3868
+ ] }),
3630
3869
  aiMode && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3631
3870
  "button",
3632
3871
  {
@@ -3792,6 +4031,37 @@ function BoltTable({
3792
4031
  children: [
3793
4032
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: accentColor, display: "flex", flexShrink: 0 }, children: icons?.sparkles ?? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SparklesIcon, { style: { width: 14, height: 14 } }) }),
3794
4033
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { flex: "1 1 0%", opacity: 0.85 }, children: aiResult.message }),
4034
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
4035
+ "button",
4036
+ {
4037
+ type: "button",
4038
+ onClick: saveCurrentAIFilter,
4039
+ style: {
4040
+ display: "flex",
4041
+ alignItems: "center",
4042
+ gap: 4,
4043
+ background: `${accentColor}12`,
4044
+ border: `1px solid ${accentColor}30`,
4045
+ borderRadius: 4,
4046
+ cursor: "pointer",
4047
+ padding: "2px 8px",
4048
+ color: accentColor,
4049
+ fontSize: 11,
4050
+ flexShrink: 0,
4051
+ fontWeight: 500,
4052
+ transition: "all 0.2s ease"
4053
+ },
4054
+ title: "Save this filter for quick access later",
4055
+ children: [
4056
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4057
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }),
4058
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "17 21 17 13 7 13 7 21" }),
4059
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "7 3 7 8 15 8" })
4060
+ ] }),
4061
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Save Filter" })
4062
+ ]
4063
+ }
4064
+ ),
3795
4065
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3796
4066
  "button",
3797
4067
  {
package/dist/index.mjs CHANGED
@@ -676,66 +676,68 @@ function detectColumnType(key, data) {
676
676
  }
677
677
  return { type: "string", sample: sampleStr };
678
678
  }
679
+ var cachedSchema = null;
680
+ function buildSchemaFingerprint(columns, dataLen) {
681
+ return columns.filter((c) => c.key !== "__select__" && c.key !== "__expand__").map((c) => c.key).join(",") + `:${dataLen}`;
682
+ }
679
683
  function buildSystemPrompt(columns, data) {
680
- const schemaLines = columns.filter((c) => c.key !== "__select__" && c.key !== "__expand__").map((c) => {
684
+ const fingerprint = buildSchemaFingerprint(columns, data.length);
685
+ if (cachedSchema?.fingerprint === fingerprint) {
686
+ return cachedSchema.prompt;
687
+ }
688
+ const cols = columns.filter((c) => c.key !== "__select__" && c.key !== "__expand__");
689
+ const schema = cols.map((c) => {
681
690
  const key = c.dataIndex ?? c.key;
682
691
  const title = typeof c.title === "string" ? c.title : c.key;
683
692
  const info = detectColumnType(key, data);
684
- return ` - key: "${c.key}", title: "${title}", dataIndex: "${key}", type: ${info.type}${info.sample ? ` (values: ${info.sample})` : ""}`;
693
+ const w = c.width ?? 150;
694
+ const pin = c.pinned ? `, pinned: "${c.pinned}"` : "";
695
+ const hidden = c.hidden ? ", hidden: true" : "";
696
+ const vals = info.sample ? "|vals: " + info.sample : "";
697
+ return ` ${c.key}|${key}|"${title}"|${info.type}|w:${w}${pin}${hidden}${vals}`;
685
698
  }).join("\n");
686
- const sample = data.slice(0, 5).map((row) => {
699
+ const sample = data.slice(0, 3).map((row) => {
687
700
  const obj = {};
688
- for (const col of columns) {
689
- if (col.key === "__select__" || col.key === "__expand__") continue;
701
+ for (const col of cols) {
690
702
  const di = col.dataIndex ?? col.key;
691
703
  obj[di] = row[di];
692
704
  }
693
705
  return obj;
694
706
  });
695
- return `You are a data table assistant. You help users query, filter, sort, and style tabular data.
696
- You MUST respond with ONLY a valid JSON object \u2014 no markdown fences, no explanation, no extra text.
697
-
698
- ## Table Schema
699
- ${schemaLines}
700
-
701
- ## Sample Data (first ${sample.length} of ${data.length} rows)
702
- ${JSON.stringify(sample, null, 2)}
703
-
704
- ## Available Operations
705
- Combine any of these in a single response:
707
+ const prompt = `Data table AI. Respond ONLY with valid JSON, no markdown/explanation.
706
708
 
707
- 1. **filter** \u2014 show only rows matching conditions
708
- { "type": "filter", "conditions": [{ "column": "<dataIndex>", "op": "<op>", "value": <val> }], "logic": "and" | "or" }
709
+ SCHEMA (key|dataIndex|title|type|width|flags|sample):
710
+ ${schema}
709
711
 
710
- 2. **rowStyle** \u2014 apply CSS styles to entire rows matching conditions
711
- { "type": "rowStyle", "conditions": [...], "logic": "and"|"or", "style": { "<cssProp>": "<value>" } }
712
+ SAMPLE (${sample.length}/${data.length} rows):
713
+ ${JSON.stringify(sample)}
712
714
 
713
- 3. **cellStyle** \u2014 apply CSS styles to a specific column's cells matching conditions
714
- { "type": "cellStyle", "column": "<dataIndex>", "conditions": [...], "logic": "and"|"or", "style": { "<cssProp>": "<value>" } }
715
+ COLUMN ORDER: [${cols.map((c) => `"${c.key}"`).join(",")}]
715
716
 
716
- 4. **sort** \u2014 sort data by a column
717
- { "type": "sort", "column": "<dataIndex>", "direction": "asc" | "desc" }
717
+ OPS (combine any):
718
+ filter: {type:"filter",conditions:[{column:"<dataIndex>",op:"<op>",value:<v>}],logic:"and"|"or"}
719
+ sort: {type:"sort",column:"<dataIndex>",direction:"asc"|"desc"}
720
+ rowStyle: {type:"rowStyle",conditions:[...],logic:"and"|"or",style:{cssProp:"val"}}
721
+ cellStyle: {type:"cellStyle",column:"<dataIndex>",conditions:[...],logic:"and"|"or",style:{cssProp:"val"}}
722
+ hideColumns: {type:"hideColumns",columns:["key",...]}
723
+ showColumns: {type:"showColumns",columns:["key",...]}
724
+ resizeColumn: {type:"resizeColumn",column:"<key>",width:<px>}
725
+ reorderColumns: {type:"reorderColumns",order:["key1","key2",...]} (full column order)
726
+ pinColumn: {type:"pinColumn",column:"<key>",pinned:"left"|"right"|false}
727
+ setPage: {type:"setPage",page:<number>}
718
728
 
719
- 5. **hideColumns** / **showColumns** \u2014 toggle column visibility
720
- { "type": "hideColumns" | "showColumns", "columns": ["<key>", ...] }
729
+ OPS: eq,neq,gt,gte,lt,lte,contains,notContains,startsWith,endsWith,in,notIn
721
730
 
722
- ## Operators
723
- eq, neq, gt, gte, lt, lte, contains, notContains, startsWith, endsWith, in, notIn
731
+ FORMAT: {"operations":[...],"message":"brief description"}
724
732
 
725
- ## Response format
726
- {
727
- "operations": [ ... ],
728
- "message": "Brief user-friendly description of what was applied"
729
- }
730
-
731
- ## Rules
732
- - Use the dataIndex values from the schema, not display titles.
733
- - For colors use semi-transparent values like "rgba(255,0,0,0.15)" so text stays readable.
734
- - CSS property names must be camelCase (e.g. "backgroundColor", "color", "fontWeight").
735
- - If the user asks to highlight / color / mark specific rows, use rowStyle or cellStyle.
736
- - If the user asks to show only certain rows, use filter.
737
- - You can combine filter + rowStyle + sort etc. in one response.
738
- - The "message" should be concise: what was done, in plain English.`;
733
+ RULES:
734
+ - Use dataIndex for data ops, key for column ops (hide/show/resize/reorder/pin).
735
+ - Colors: semi-transparent rgba. CSS props: camelCase.
736
+ - reorderColumns: provide FULL ordered array of ALL visible column keys.
737
+ - resizeColumn width: integer pixels (min 40, max 800).
738
+ - Combine multiple ops freely. Message: concise plain English.`;
739
+ cachedSchema = { fingerprint, prompt };
740
+ return prompt;
739
741
  }
740
742
  async function callAI(config, systemPrompt, userQuery) {
741
743
  const { provider, apiKey, model, baseUrl, maxTokens = 1024, temperature = 0.1 } = config;
@@ -901,6 +903,10 @@ function applyAIOperations(data, operations) {
901
903
  const cellStyleOps = [];
902
904
  const hideColumns = [];
903
905
  const showColumns = [];
906
+ const resizeOps = [];
907
+ let reorderOp = null;
908
+ const pinOps = [];
909
+ let setPageOp = null;
904
910
  for (const op of operations) {
905
911
  switch (op.type) {
906
912
  case "filter":
@@ -921,12 +927,24 @@ function applyAIOperations(data, operations) {
921
927
  case "showColumns":
922
928
  showColumns.push(...op.columns);
923
929
  break;
930
+ case "resizeColumn":
931
+ resizeOps.push(op);
932
+ break;
933
+ case "reorderColumns":
934
+ reorderOp = op;
935
+ break;
936
+ case "pinColumn":
937
+ pinOps.push(op);
938
+ break;
939
+ case "setPage":
940
+ setPageOp = op;
941
+ break;
924
942
  }
925
943
  }
926
944
  if (sortOp) {
927
945
  filteredData = applyAISort(filteredData, sortOp);
928
946
  }
929
- return { filteredData, sortOp, styleOps, cellStyleOps, hideColumns, showColumns };
947
+ return { filteredData, sortOp, styleOps, cellStyleOps, hideColumns, showColumns, resizeOps, reorderOp, pinOps, setPageOp };
930
948
  }
931
949
 
932
950
  // src/ResizeOverlay.tsx
@@ -2219,6 +2237,101 @@ function BoltTable({
2219
2237
  const [aiFilteredDataKeys, setAiFilteredDataKeys] = useState3(null);
2220
2238
  const [aiSortKey, setAiSortKey] = useState3(null);
2221
2239
  const [aiSortDir, setAiSortDir] = useState3(null);
2240
+ const aiFiltersStorageKey = columnPersistence && typeof columnPersistence === "object" ? `bt-ai-filters-${columnPersistence.storageKey}` : "bt-ai-filters";
2241
+ const [savedAIFilters, setSavedAIFilters] = useState3(() => {
2242
+ try {
2243
+ const raw = localStorage.getItem(aiFiltersStorageKey);
2244
+ return raw ? JSON.parse(raw) : [];
2245
+ } catch {
2246
+ return [];
2247
+ }
2248
+ });
2249
+ const [showSavedFilters, setShowSavedFilters] = useState3(false);
2250
+ const savedFiltersRef = useRef4(null);
2251
+ React4.useEffect(() => {
2252
+ if (!showSavedFilters) return;
2253
+ const close = (e) => {
2254
+ if (savedFiltersRef.current && !savedFiltersRef.current.contains(e.target)) {
2255
+ setShowSavedFilters(false);
2256
+ }
2257
+ };
2258
+ document.addEventListener("mousedown", close);
2259
+ return () => document.removeEventListener("mousedown", close);
2260
+ }, [showSavedFilters]);
2261
+ const saveCurrentAIFilter = useCallback2(() => {
2262
+ if (!aiResult) return;
2263
+ const label = aiQuery || aiResult.message;
2264
+ const entry = { label, operations: aiResult.operations, query: aiQuery };
2265
+ const next = [...savedAIFilters, entry];
2266
+ setSavedAIFilters(next);
2267
+ try {
2268
+ localStorage.setItem(aiFiltersStorageKey, JSON.stringify(next));
2269
+ } catch {
2270
+ }
2271
+ }, [aiResult, aiQuery, savedAIFilters, aiFiltersStorageKey]);
2272
+ const removeSavedFilter = useCallback2((index) => {
2273
+ const next = savedAIFilters.filter((_, i) => i !== index);
2274
+ setSavedAIFilters(next);
2275
+ try {
2276
+ localStorage.setItem(aiFiltersStorageKey, JSON.stringify(next));
2277
+ } catch {
2278
+ }
2279
+ }, [savedAIFilters, aiFiltersStorageKey]);
2280
+ const applySavedFilter = useCallback2((filter) => {
2281
+ const { filteredData, sortOp, styleOps: sOps, cellStyleOps: csOps, hideColumns: hideCols, showColumns: showCols, resizeOps, reorderOp, pinOps, setPageOp } = applyAIOperations(data, filter.operations);
2282
+ setAiStyleOps(sOps);
2283
+ setAiCellStyleOps(csOps);
2284
+ if (filter.operations.some((op) => op.type === "filter")) {
2285
+ const keySet = /* @__PURE__ */ new Set();
2286
+ filteredData.forEach((row, idx) => {
2287
+ const k = typeof rowKey === "function" ? rowKey(row) : String(row[typeof rowKey === "string" ? rowKey : "id"] ?? idx);
2288
+ keySet.add(k);
2289
+ });
2290
+ setAiFilteredDataKeys(keySet);
2291
+ } else {
2292
+ setAiFilteredDataKeys(null);
2293
+ }
2294
+ if (sortOp) {
2295
+ setAiSortKey(sortOp.column);
2296
+ setAiSortDir(sortOp.direction);
2297
+ } else {
2298
+ setAiSortKey(null);
2299
+ setAiSortDir(null);
2300
+ }
2301
+ if (hideCols.length > 0 || showCols.length > 0) {
2302
+ setColumns(
2303
+ (prev) => prev.map((col) => {
2304
+ if (hideCols.includes(col.key)) return { ...col, hidden: true };
2305
+ if (showCols.includes(col.key)) return { ...col, hidden: false };
2306
+ return col;
2307
+ })
2308
+ );
2309
+ }
2310
+ for (const rOp of resizeOps) {
2311
+ const w = Math.max(40, Math.min(800, rOp.width));
2312
+ setColumnWidths((prev) => {
2313
+ const n = new Map(prev);
2314
+ n.set(rOp.column, w);
2315
+ return n;
2316
+ });
2317
+ onColumnResize?.(rOp.column, w);
2318
+ }
2319
+ if (reorderOp) {
2320
+ setColumnOrder(reorderOp.order);
2321
+ onColumnOrderChange?.(reorderOp.order);
2322
+ }
2323
+ for (const pOp of pinOps) {
2324
+ setColumns(
2325
+ (prev) => prev.map((col) => col.key === pOp.column ? { ...col, pinned: pOp.pinned } : col)
2326
+ );
2327
+ onColumnPin?.(pOp.column, pOp.pinned);
2328
+ }
2329
+ if (setPageOp) {
2330
+ setInternalPage(setPageOp.page);
2331
+ }
2332
+ setAiResult({ operations: filter.operations, message: `Applied saved filter: ${filter.label}` });
2333
+ setShowSavedFilters(false);
2334
+ }, [data, rowKey, onColumnResize, onColumnOrderChange, onColumnPin]);
2222
2335
  const onAIResponseRef = useRef4(onAIResponse);
2223
2336
  onAIResponseRef.current = onAIResponse;
2224
2337
  const handleAISubmit = useCallback2(async () => {
@@ -2240,7 +2353,7 @@ function BoltTable({
2240
2353
  } else {
2241
2354
  throw new Error("AI mode requires either aiConfig or onAIQuery prop");
2242
2355
  }
2243
- const { filteredData, sortOp, styleOps: sOps, cellStyleOps: csOps, hideColumns, showColumns } = applyAIOperations(data, response.operations);
2356
+ const { filteredData, sortOp, styleOps: sOps, cellStyleOps: csOps, hideColumns: hideCols, showColumns: showCols, resizeOps, reorderOp, pinOps, setPageOp } = applyAIOperations(data, response.operations);
2244
2357
  setAiStyleOps(sOps);
2245
2358
  setAiCellStyleOps(csOps);
2246
2359
  if (response.operations.some((op) => op.type === "filter")) {
@@ -2260,15 +2373,37 @@ function BoltTable({
2260
2373
  setAiSortKey(null);
2261
2374
  setAiSortDir(null);
2262
2375
  }
2263
- if (hideColumns.length > 0 || showColumns.length > 0) {
2376
+ if (hideCols.length > 0 || showCols.length > 0) {
2264
2377
  setColumns(
2265
2378
  (prev) => prev.map((col) => {
2266
- if (hideColumns.includes(col.key)) return { ...col, hidden: true };
2267
- if (showColumns.includes(col.key)) return { ...col, hidden: false };
2379
+ if (hideCols.includes(col.key)) return { ...col, hidden: true };
2380
+ if (showCols.includes(col.key)) return { ...col, hidden: false };
2268
2381
  return col;
2269
2382
  })
2270
2383
  );
2271
2384
  }
2385
+ for (const rOp of resizeOps) {
2386
+ const w = Math.max(40, Math.min(800, rOp.width));
2387
+ setColumnWidths((prev) => {
2388
+ const n = new Map(prev);
2389
+ n.set(rOp.column, w);
2390
+ return n;
2391
+ });
2392
+ onColumnResize?.(rOp.column, w);
2393
+ }
2394
+ if (reorderOp) {
2395
+ setColumnOrder(reorderOp.order);
2396
+ onColumnOrderChange?.(reorderOp.order);
2397
+ }
2398
+ for (const pOp of pinOps) {
2399
+ setColumns(
2400
+ (prev) => prev.map((col) => col.key === pOp.column ? { ...col, pinned: pOp.pinned } : col)
2401
+ );
2402
+ onColumnPin?.(pOp.column, pOp.pinned);
2403
+ }
2404
+ if (setPageOp) {
2405
+ setInternalPage(setPageOp.page);
2406
+ }
2272
2407
  setAiResult(response);
2273
2408
  onAIResponseRef.current?.(response);
2274
2409
  } catch (err) {
@@ -3406,7 +3541,7 @@ function BoltTable({
3406
3541
  borderBottom: "1px solid rgba(128,128,128,0.2)",
3407
3542
  fontSize: 12,
3408
3543
  flexShrink: 0,
3409
- overflow: "hidden"
3544
+ zIndex: 20
3410
3545
  },
3411
3546
  children: [
3412
3547
  /* @__PURE__ */ jsxs5(
@@ -3598,6 +3733,110 @@ function BoltTable({
3598
3733
  }
3599
3734
  )
3600
3735
  ] }),
3736
+ aiMode && savedAIFilters.length > 0 && /* @__PURE__ */ jsxs5("div", { ref: savedFiltersRef, style: { position: "relative", flexShrink: 0 }, children: [
3737
+ /* @__PURE__ */ jsxs5(
3738
+ "button",
3739
+ {
3740
+ type: "button",
3741
+ onClick: () => setShowSavedFilters((p) => !p),
3742
+ style: {
3743
+ display: "flex",
3744
+ alignItems: "center",
3745
+ gap: 4,
3746
+ background: "none",
3747
+ border: "1px solid rgba(128,128,128,0.2)",
3748
+ borderRadius: 4,
3749
+ cursor: "pointer",
3750
+ padding: "4px 6px",
3751
+ color: "inherit",
3752
+ fontSize: 12
3753
+ },
3754
+ title: "Saved AI filters",
3755
+ children: [
3756
+ /* @__PURE__ */ jsx5("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx5("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }) }),
3757
+ /* @__PURE__ */ jsx5("span", { children: savedAIFilters.length })
3758
+ ]
3759
+ }
3760
+ ),
3761
+ showSavedFilters && /* @__PURE__ */ jsxs5(
3762
+ "div",
3763
+ {
3764
+ style: {
3765
+ position: "absolute",
3766
+ top: "100%",
3767
+ right: 0,
3768
+ zIndex: 99999,
3769
+ minWidth: 240,
3770
+ maxWidth: 360,
3771
+ maxHeight: 320,
3772
+ overflowY: "auto",
3773
+ borderRadius: 8,
3774
+ border: "1px solid rgba(128,128,128,0.2)",
3775
+ boxShadow: "0 4px 24px rgba(0,0,0,0.12)",
3776
+ backdropFilter: "blur(16px)",
3777
+ WebkitBackdropFilter: "blur(16px)",
3778
+ backgroundColor: "rgba(128,128,128,0.08)",
3779
+ padding: "4px 0",
3780
+ marginTop: 4
3781
+ },
3782
+ children: [
3783
+ /* @__PURE__ */ jsx5("div", { style: { padding: "6px 12px", fontSize: 11, opacity: 0.5, fontWeight: 600 }, children: "Saved Filters" }),
3784
+ savedAIFilters.map((f, i) => /* @__PURE__ */ jsxs5(
3785
+ "div",
3786
+ {
3787
+ style: {
3788
+ display: "flex",
3789
+ alignItems: "center",
3790
+ gap: 6,
3791
+ padding: "6px 12px",
3792
+ cursor: "pointer",
3793
+ fontSize: 12
3794
+ },
3795
+ onMouseEnter: (e) => {
3796
+ e.currentTarget.style.backgroundColor = "rgba(128,128,128,0.15)";
3797
+ },
3798
+ onMouseLeave: (e) => {
3799
+ e.currentTarget.style.backgroundColor = "transparent";
3800
+ },
3801
+ children: [
3802
+ /* @__PURE__ */ jsx5(
3803
+ "span",
3804
+ {
3805
+ style: { flex: "1 1 0%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
3806
+ onClick: () => applySavedFilter(f),
3807
+ children: f.label
3808
+ }
3809
+ ),
3810
+ /* @__PURE__ */ jsx5(
3811
+ "button",
3812
+ {
3813
+ type: "button",
3814
+ onClick: (e) => {
3815
+ e.stopPropagation();
3816
+ removeSavedFilter(i);
3817
+ },
3818
+ style: {
3819
+ display: "flex",
3820
+ alignItems: "center",
3821
+ background: "none",
3822
+ border: "none",
3823
+ cursor: "pointer",
3824
+ padding: 2,
3825
+ color: "GrayText",
3826
+ flexShrink: 0
3827
+ },
3828
+ title: "Remove",
3829
+ children: icons?.close ?? /* @__PURE__ */ jsx5(XIcon, { style: { width: 10, height: 10 } })
3830
+ }
3831
+ )
3832
+ ]
3833
+ },
3834
+ i
3835
+ ))
3836
+ ]
3837
+ }
3838
+ )
3839
+ ] }),
3601
3840
  aiMode && /* @__PURE__ */ jsxs5(
3602
3841
  "button",
3603
3842
  {
@@ -3763,6 +4002,37 @@ function BoltTable({
3763
4002
  children: [
3764
4003
  /* @__PURE__ */ jsx5("span", { style: { color: accentColor, display: "flex", flexShrink: 0 }, children: icons?.sparkles ?? /* @__PURE__ */ jsx5(SparklesIcon, { style: { width: 14, height: 14 } }) }),
3765
4004
  /* @__PURE__ */ jsx5("span", { style: { flex: "1 1 0%", opacity: 0.85 }, children: aiResult.message }),
4005
+ /* @__PURE__ */ jsxs5(
4006
+ "button",
4007
+ {
4008
+ type: "button",
4009
+ onClick: saveCurrentAIFilter,
4010
+ style: {
4011
+ display: "flex",
4012
+ alignItems: "center",
4013
+ gap: 4,
4014
+ background: `${accentColor}12`,
4015
+ border: `1px solid ${accentColor}30`,
4016
+ borderRadius: 4,
4017
+ cursor: "pointer",
4018
+ padding: "2px 8px",
4019
+ color: accentColor,
4020
+ fontSize: 11,
4021
+ flexShrink: 0,
4022
+ fontWeight: 500,
4023
+ transition: "all 0.2s ease"
4024
+ },
4025
+ title: "Save this filter for quick access later",
4026
+ children: [
4027
+ /* @__PURE__ */ jsxs5("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4028
+ /* @__PURE__ */ jsx5("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }),
4029
+ /* @__PURE__ */ jsx5("polyline", { points: "17 21 17 13 7 13 7 21" }),
4030
+ /* @__PURE__ */ jsx5("polyline", { points: "7 3 7 8 15 8" })
4031
+ ] }),
4032
+ /* @__PURE__ */ jsx5("span", { children: "Save Filter" })
4033
+ ]
4034
+ }
4035
+ ),
3766
4036
  /* @__PURE__ */ jsxs5(
3767
4037
  "button",
3768
4038
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bolt-table",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Virtualized React table with column drag & drop, pinning, resizing, sorting, filtering, and pagination.",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",