@classic-homes/theme-svelte 0.1.30 → 0.1.31

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.
@@ -61,6 +61,7 @@
61
61
  formatCellValue,
62
62
  getAriaSortValue,
63
63
  defaultGetRowId,
64
+ createMemoizedConverter,
64
65
  } from './data-table/utils.js';
65
66
  import type {
66
67
  DataTableProps,
@@ -187,6 +188,10 @@
187
188
  enableEditing?: boolean;
188
189
  /** Callback when a cell is edited */
189
190
  onCellEdit?: (rowId: string, columnId: string, value: unknown, row: T) => void;
191
+
192
+ // === Debug Props ===
193
+ /** Enable verbose debug logging to console */
194
+ debug?: boolean;
190
195
  }
191
196
 
192
197
  let {
@@ -241,39 +246,63 @@
241
246
  // New editing props
242
247
  enableEditing = false,
243
248
  onCellEdit,
249
+
250
+ // Debug props
251
+ debug = false,
244
252
  }: Props = $props();
245
253
 
246
- // Validate props in development
254
+ // Validate props
247
255
  $effect(() => {
248
256
  validateNonEmptyArray(columns, 'columns', 'DataTable');
249
- });
250
257
 
251
- /**
252
- * Column definition cache for memoization.
253
- * Key: column.id, Value: converted ColumnDef
254
- *
255
- * This prevents recreating column definitions on every render,
256
- * which is especially important for tables with many columns.
257
- */
258
- const columnDefCache = new Map<string, ColumnDef<T, unknown>>();
258
+ // Validate column IDs are unique - this MUST be enforced as duplicate IDs
259
+ // cause silent bugs in sorting, selection, and visibility features
260
+ const ids = columns.map((c) => c.id);
261
+ const seen = new Set<string>();
262
+ const duplicates: string[] = [];
263
+ for (const id of ids) {
264
+ if (seen.has(id)) {
265
+ duplicates.push(id);
266
+ }
267
+ seen.add(id);
268
+ }
269
+ if (duplicates.length > 0) {
270
+ throw new Error(
271
+ `[DataTable] Duplicate column IDs found: ${duplicates.join(', ')}. ` +
272
+ 'Each column must have a unique ID. Duplicate IDs cause bugs in sorting, ' +
273
+ 'selection, filtering, and column visibility features.'
274
+ );
275
+ }
276
+
277
+ // Warn when using server-side features without a custom getRowId
278
+ // Using array index as row ID breaks selection when data is reordered or paginated server-side
279
+ const isServerSide = onSort || manualFiltering || manualPagination;
280
+ const hasCustomGetRowId = !!getRowId;
281
+ if (isServerSide && enableRowSelection && !hasCustomGetRowId) {
282
+ console.warn(
283
+ '[DataTable] Row selection is enabled with server-side data operations, but no ' +
284
+ '`getRowId` function was provided. The default uses array index, which will cause ' +
285
+ 'selection to break when data is reordered, filtered, or paginated on the server. ' +
286
+ 'Provide a `getRowId` function that returns a stable unique identifier for each row ' +
287
+ '(e.g., `getRowId={(row) => row.id}`).'
288
+ );
289
+ }
290
+ });
259
291
 
260
292
  /**
261
- * Convert legacy DataTableColumn to TanStack ColumnDef with memoization.
293
+ * Memoized column definition converter with LRU cache.
262
294
  *
263
- * The conversion is cached by column ID. Cache is invalidated when:
264
- * - Column properties change (detected by comparing relevant fields)
265
- * - The columns array reference changes (cache is cleared)
295
+ * Features:
296
+ * - Maximum 100 cached entries (prevents unbounded growth)
297
+ * - LRU eviction when cache is full
298
+ * - Cache key includes all properties that affect the ColumnDef
299
+ * - Clear method for manual cache invalidation
266
300
  */
267
- function convertToColumnDef(col: DataTableColumn<T>): ColumnDef<T, unknown> {
268
- // Create a cache key that includes properties that affect the ColumnDef
269
- const cacheKey = `${col.id}:${col.header}:${col.sortable}:${col.enableColumnFilter}:${col.enableHiding}:${col.width}:${col.align}`;
270
-
271
- const cached = columnDefCache.get(cacheKey);
272
- if (cached) {
273
- return cached;
274
- }
275
-
276
- const columnDef: ColumnDef<T, unknown> = {
301
+ const memoizedConvertToColumnDef = createMemoizedConverter<
302
+ DataTableColumn<T>,
303
+ ColumnDef<T, unknown>
304
+ >(
305
+ (col) => ({
277
306
  id: col.id,
278
307
  header: col.header,
279
308
  accessorFn:
@@ -293,18 +322,26 @@
293
322
  filterPlaceholder: col.filterPlaceholder,
294
323
  ...col.meta,
295
324
  } satisfies ColumnMeta<T>,
296
- };
297
-
298
- columnDefCache.set(cacheKey, columnDef);
299
- return columnDef;
300
- }
325
+ }),
326
+ // Cache key includes all properties that affect the ColumnDef
327
+ (col) =>
328
+ `${col.id}:${col.header}:${col.sortable}:${col.enableColumnFilter}:${col.enableHiding}:${col.width}:${col.align}`,
329
+ 100 // Max 100 cached column definitions
330
+ );
301
331
 
302
332
  // Clear cache when columns array changes (new reference)
303
333
  let previousColumnsRef: DataTableColumn<T>[] | undefined;
304
334
  $effect(() => {
305
335
  if (previousColumnsRef !== columns) {
306
- columnDefCache.clear();
336
+ memoizedConvertToColumnDef.clear();
307
337
  previousColumnsRef = columns;
338
+
339
+ if (debug) {
340
+ console.log('[DataTable] Columns changed, cache cleared', {
341
+ columnCount: columns.length,
342
+ columnIds: columns.map((c) => c.id),
343
+ });
344
+ }
308
345
  }
309
346
  });
310
347
 
@@ -326,8 +363,8 @@
326
363
  // Create TanStack column definitions
327
364
  const columnDefs = $derived<ColumnDef<T, unknown>[]>(
328
365
  enableRowSelection
329
- ? [selectionColumnDef, ...columns.map(convertToColumnDef)]
330
- : columns.map(convertToColumnDef)
366
+ ? [selectionColumnDef, ...columns.map(memoizedConvertToColumnDef)]
367
+ : columns.map(memoizedConvertToColumnDef)
331
368
  );
332
369
 
333
370
  // Create the TanStack table instance
@@ -389,7 +426,32 @@
389
426
  }));
390
427
 
391
428
  // Get rows to display (respects pagination when enabled)
392
- const displayRows = $derived(table.instance.getRowModel().rows);
429
+ // Must depend on version to re-evaluate when data changes externally
430
+ const displayRows = $derived.by(() => {
431
+ void table.reactiveState.version;
432
+ return table.instance.getRowModel().rows;
433
+ });
434
+
435
+ // Debug logging for table state changes
436
+ $effect(() => {
437
+ if (!debug) return;
438
+
439
+ // Track version to log on every state change
440
+ const version = table.reactiveState.version;
441
+ const rowCount = displayRows.length;
442
+ const totalRows = data.length;
443
+
444
+ console.log('[DataTable] State updated', {
445
+ version,
446
+ displayedRows: rowCount,
447
+ totalRows,
448
+ sorting: table.sorting,
449
+ pagination: enablePagination ? table.pagination : 'disabled',
450
+ globalFilter: enableGlobalFilter ? table.globalFilter : 'disabled',
451
+ selectedCount: enableRowSelection ? table.selectedRowCount : 'disabled',
452
+ cacheSize: memoizedConvertToColumnDef.size(),
453
+ });
454
+ });
393
455
 
394
456
  // Handle row click with error boundary
395
457
  function handleRowClick(row: T) {
@@ -123,6 +123,8 @@ declare function $$render<T extends object>(): {
123
123
  enableEditing?: boolean;
124
124
  /** Callback when a cell is edited */
125
125
  onCellEdit?: (rowId: string, columnId: string, value: unknown, row: T) => void;
126
+ /** Enable verbose debug logging to console */
127
+ debug?: boolean;
126
128
  };
127
129
  exports: {};
128
130
  bindings: "columnVisibility" | "columnFilters" | "globalFilter" | "rowSelection" | "sortColumn" | "sortDirection" | "pageSize" | "pageIndex";
@@ -275,10 +275,14 @@ export function createDataTable(options) {
275
275
  // Use $derived to create a reactive options snapshot that tracks all dependencies
276
276
  const currentOptions = $derived.by(() => {
277
277
  const opts = options();
278
- // Read all state values to ensure they're tracked as dependencies
279
- // This creates a snapshot of the current state
278
+ // Explicitly read all values that should trigger re-renders.
279
+ // Svelte 5's fine-grained reactivity requires explicit property access
280
+ // to establish dependencies - the spread operator alone doesn't track them.
280
281
  return {
281
282
  ...opts,
283
+ // Track data and column changes (critical for external updates)
284
+ _data: opts.data,
285
+ _columns: opts.columns,
282
286
  // Explicitly read state values to track them (these establish reactive dependencies)
283
287
  _sorting: opts.state?.sorting ?? opts.sorting ?? internalSorting,
284
288
  _columnFilters: opts.state?.columnFilters ?? opts.columnFilters ?? internalColumnFilters,
@@ -16,7 +16,7 @@
16
16
  */
17
17
  export { createDataTable, type CreateDataTableOptions, type DataTableInstance, type Table, type ColumnDef, type SortingState, type ColumnFiltersState, type VisibilityState, type RowSelectionState, type PaginationState, type Row, type Cell, type Header, type HeaderGroup, type Column, type CellContext, type HeaderContext, createColumnHelper, } from './createDataTable.svelte.js';
18
18
  export { type ColumnMeta, type BaseColumnMeta, type DataColumnMeta, type FilterableColumnMeta, type SelectionColumnMeta, type DataTableProps, type DataTableCoreProps, type DataTableSortingProps, type DataTableSelectionProps, type DataTableVisibilityProps, type DataTableFilteringProps, type DataTablePaginationProps, type DataTableVirtualizationProps, type DataTableEditingProps, type DataTableInteractionProps, SVELTE_COMPONENT_SYMBOL, isMarkedComponent, markAsComponent, type MarkedSvelteComponent, type InferRowType, type FormatResult, } from './types.js';
19
- export { getAlignClass, getMetaAlign, formatCellValue, formatCellValueSafe, getColumnWidthStyle, getColumnFlexStyle, getAriaSortValue, defaultGetRowId, createMemoizedConverter, } from './utils.js';
19
+ export { getAlignClass, getMetaAlign, formatCellValue, formatCellValueSafe, getColumnWidthStyle, getColumnFlexStyle, getAriaSortValue, defaultGetRowId, createMemoizedConverter, type MemoizedFunction, } from './utils.js';
20
20
  export { default as FlexRender } from './FlexRender.svelte';
21
21
  export { default as DataTableToolbar } from './DataTableToolbar.svelte';
22
22
  export { default as DataTableSearch } from './DataTableSearch.svelte';
@@ -97,20 +97,66 @@ export interface DataTableSortingProps {
97
97
  }
98
98
  /**
99
99
  * Props for row selection functionality.
100
+ *
101
+ * ## Row ID Requirements
102
+ *
103
+ * **Critical**: When using row selection with server-side data (sorting, filtering, or pagination),
104
+ * you MUST provide a custom `getRowId` function. The default uses array index, which breaks
105
+ * selection when data is reordered.
106
+ *
107
+ * @example
108
+ * ```svelte
109
+ * <!-- Always provide getRowId for server-side data -->
110
+ * <DataTable
111
+ * data={serverData}
112
+ * columns={columns}
113
+ * enableRowSelection
114
+ * manualPagination
115
+ * getRowId={(row) => row.id}
116
+ * rowSelection={selectedRows}
117
+ * onRowSelectionChange={(selection) => selectedRows = selection}
118
+ * />
119
+ * ```
120
+ *
121
+ * ## Selection State Format
122
+ *
123
+ * The `rowSelection` state is a record mapping row IDs to booleans:
124
+ * ```ts
125
+ * { "user-1": true, "user-3": true } // rows with IDs "user-1" and "user-3" are selected
126
+ * ```
100
127
  */
101
128
  export interface DataTableSelectionProps<T> {
102
129
  /**
103
130
  * Enable row selection.
104
131
  * Can be a boolean or a function that returns whether a specific row is selectable.
132
+ *
133
+ * @example
134
+ * ```svelte
135
+ * <!-- Conditionally disable selection for certain rows -->
136
+ * <DataTable
137
+ * enableRowSelection={(row) => row.status !== 'locked'}
138
+ * />
139
+ * ```
105
140
  */
106
141
  enableRowSelection?: boolean | ((row: T) => boolean);
107
142
  /** Enable multi-row selection (default: true when selection enabled) */
108
143
  enableMultiRowSelection?: boolean;
109
- /** Controlled row selection state (record of rowId -> selected) */
144
+ /**
145
+ * Controlled row selection state.
146
+ * Record of rowId -> boolean (true = selected).
147
+ */
110
148
  rowSelection?: RowSelectionState;
111
149
  /** Callback when selection changes */
112
150
  onRowSelectionChange?: (selection: RowSelectionState) => void;
113
- /** Function to get unique row ID (defaults to index) */
151
+ /**
152
+ * Function to get unique row ID.
153
+ *
154
+ * **Important**: Required for server-side data! The default uses array index,
155
+ * which breaks selection when data is sorted, filtered, or paginated server-side.
156
+ *
157
+ * @default (row, index) => String(index)
158
+ * @example getRowId={(row) => row.id}
159
+ */
114
160
  getRowId?: (row: T, index: number) => string;
115
161
  }
116
162
  /**
@@ -126,13 +172,47 @@ export interface DataTableVisibilityProps {
126
172
  }
127
173
  /**
128
174
  * Props for filtering functionality (global and per-column).
175
+ *
176
+ * ## Global Filter Behavior
177
+ *
178
+ * The global filter searches across all visible columns by converting cell values to strings.
179
+ * This works well for primitive values (strings, numbers, booleans) but may produce
180
+ * unexpected results with complex objects:
181
+ *
182
+ * - Objects are stringified as `[object Object]` - not searchable
183
+ * - Arrays are joined with commas - `["a", "b"]` becomes `"a,b"`
184
+ * - Dates are converted to ISO strings
185
+ *
186
+ * **For complex data**: Use a custom `format` function on columns to control how values
187
+ * are displayed and searched, or implement server-side filtering with `manualFiltering`.
188
+ *
189
+ * ## Server-Side Filtering
190
+ *
191
+ * For large datasets or complex filtering logic, use server-side filtering:
192
+ *
193
+ * @example
194
+ * ```svelte
195
+ * <DataTable
196
+ * data={serverData}
197
+ * columns={columns}
198
+ * enableFiltering
199
+ * manualFiltering
200
+ * globalFilter={searchQuery}
201
+ * onGlobalFilterChange={(filter) => fetchFilteredData(filter)}
202
+ * />
203
+ * ```
129
204
  */
130
205
  export interface DataTableFilteringProps {
131
- /** Enable filtering features */
206
+ /** Enable filtering features (per-column filters) */
132
207
  enableFiltering?: boolean;
133
- /** Enable global filter input */
208
+ /** Enable global filter input (searches all columns) */
134
209
  enableGlobalFilter?: boolean;
135
- /** Controlled global filter value */
210
+ /**
211
+ * Controlled global filter value.
212
+ *
213
+ * The filter searches stringified cell values across all visible columns.
214
+ * For complex objects, use column `format` functions to control searchable text.
215
+ */
136
216
  globalFilter?: string;
137
217
  /** Callback when global filter changes */
138
218
  onGlobalFilterChange?: (filter: string) => void;
@@ -140,7 +220,12 @@ export interface DataTableFilteringProps {
140
220
  columnFilters?: ColumnFiltersState;
141
221
  /** Callback when column filters change */
142
222
  onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
143
- /** Use server-side filtering (manual mode) */
223
+ /**
224
+ * Use server-side filtering (manual mode).
225
+ *
226
+ * When enabled, the table will NOT filter data client-side. You must handle
227
+ * filtering in your data fetching logic and provide already-filtered data.
228
+ */
144
229
  manualFiltering?: boolean;
145
230
  }
146
231
  /**
@@ -162,13 +247,60 @@ export interface DataTablePaginationProps {
162
247
  }
163
248
  /**
164
249
  * Props for virtualization (large dataset support).
250
+ *
251
+ * ## Important Constraints
252
+ *
253
+ * **Fixed Row Height Required**: Virtualization uses windowing to only render visible rows.
254
+ * This requires ALL rows to have the same height. If your rows have variable heights
255
+ * (e.g., wrapping text, expandable content), virtualization will cause visual glitches.
256
+ *
257
+ * **When to Use**: Enable virtualization when displaying 1000+ rows. For smaller datasets,
258
+ * standard rendering is often more performant due to virtualization overhead.
259
+ *
260
+ * ## Overscan Tuning
261
+ *
262
+ * The `virtualOverscan` prop controls how many rows are rendered outside the visible area.
263
+ * - **Lower values (5)**: Better performance, but may show blank areas during fast scrolling
264
+ * - **Higher values (20)**: Smoother scrolling, but renders more DOM nodes
265
+ * - **Default (10)**: Good balance for most use cases
266
+ *
267
+ * @example
268
+ * ```svelte
269
+ * <!-- Large dataset with custom row height -->
270
+ * <DataTable
271
+ * {data}
272
+ * {columns}
273
+ * enableVirtualization
274
+ * virtualRowHeight={56}
275
+ * virtualOverscan={15}
276
+ * />
277
+ * ```
165
278
  */
166
279
  export interface DataTableVirtualizationProps {
167
- /** Enable row virtualization for large datasets (1000+ rows recommended) */
280
+ /**
281
+ * Enable row virtualization for large datasets.
282
+ *
283
+ * Recommended for 1000+ rows. Requires fixed row heights.
284
+ * @default false
285
+ */
168
286
  enableVirtualization?: boolean;
169
- /** Height of each row in pixels for virtualization (default: 48) */
287
+ /**
288
+ * Height of each row in pixels for virtualization.
289
+ *
290
+ * **Important**: ALL rows must have this exact height. Variable height rows
291
+ * will cause visual glitches (misaligned rows, flickering).
292
+ *
293
+ * @default 48
294
+ */
170
295
  virtualRowHeight?: number;
171
- /** Number of rows to render outside the visible viewport (default: 10) */
296
+ /**
297
+ * Number of rows to render outside the visible viewport.
298
+ *
299
+ * Higher values = smoother scrolling but more DOM nodes.
300
+ * Lower values = better performance but may flash during fast scrolling.
301
+ *
302
+ * @default 10
303
+ */
172
304
  virtualOverscan?: number;
173
305
  }
174
306
  /**
@@ -83,18 +83,69 @@ export declare function getAriaSortValue(sortDirection: false | 'asc' | 'desc'):
83
83
  /**
84
84
  * Default row ID function that uses the array index.
85
85
  *
86
+ * **Warning**: This is only suitable for static, client-side data where the order
87
+ * never changes. For any of these scenarios, provide a custom `getRowId`:
88
+ *
89
+ * - Server-side sorting, filtering, or pagination
90
+ * - Data that can be reordered by the user
91
+ * - Data that can be inserted or deleted
92
+ * - Row selection that must persist across data updates
93
+ *
94
+ * Using array index with dynamic data causes:
95
+ * - Selection jumping to wrong rows after sort/filter
96
+ * - Selected state appearing on different rows after pagination
97
+ * - Inconsistent behavior when rows are added/removed
98
+ *
86
99
  * @param _row - The row data (unused)
87
100
  * @param index - The row index
88
101
  * @returns String representation of the index
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * // DON'T use defaultGetRowId with server-side data
106
+ * // DO provide a custom getRowId:
107
+ * getRowId={(row) => row.id}
108
+ * getRowId={(row) => `${row.type}-${row.id}`}
109
+ * ```
89
110
  */
90
111
  export declare function defaultGetRowId<T>(_row: T, index: number): string;
91
112
  /**
92
- * Simple memoization for column definition conversion.
93
- * Caches the result based on a stable key derived from the column.
113
+ * A memoized function with cache management methods.
94
114
  */
95
- export declare function createMemoizedConverter<TInput, TOutput>(converter: (input: TInput) => TOutput, getKey: (input: TInput) => string): (input: TInput) => TOutput;
115
+ export interface MemoizedFunction<TInput, TOutput> {
116
+ /** Call the memoized function */
117
+ (input: TInput): TOutput;
118
+ /** Clear all cached entries */
119
+ clear: () => void;
120
+ /** Get the current cache size */
121
+ size: () => number;
122
+ }
96
123
  /**
97
- * Clear all entries from a memoization cache.
98
- * Call this when the underlying data structure changes significantly.
124
+ * Create a memoized converter with LRU cache eviction.
125
+ *
126
+ * Features:
127
+ * - Configurable maximum cache size (default: 100)
128
+ * - LRU (Least Recently Used) eviction when cache is full
129
+ * - Exposed `clear()` method to manually clear the cache
130
+ * - Exposed `size()` method to check current cache size
131
+ *
132
+ * @param converter - Function to convert input to output
133
+ * @param getKey - Function to generate a cache key from input
134
+ * @param maxSize - Maximum number of entries to cache (default: 100)
135
+ * @returns Memoized function with cache management methods
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const memoizedConvert = createMemoizedConverter(
140
+ * (col) => convertColumn(col),
141
+ * (col) => col.id,
142
+ * 50 // max 50 entries
143
+ * );
144
+ *
145
+ * memoizedConvert(column); // Converts and caches
146
+ * memoizedConvert(column); // Returns cached
147
+ * memoizedConvert.clear(); // Clears cache
148
+ * memoizedConvert.size(); // Returns 0
149
+ * ```
99
150
  */
100
- export declare function clearMemoCache<TInput, TOutput>(_memoizedFn: ReturnType<typeof createMemoizedConverter<TInput, TOutput>>): void;
151
+ export declare function createMemoizedConverter<TInput, TOutput>(converter: (input: TInput) => TOutput, getKey: (input: TInput) => string, maxSize?: number): MemoizedFunction<TInput, TOutput>;
@@ -181,39 +181,88 @@ export function getAriaSortValue(sortDirection) {
181
181
  /**
182
182
  * Default row ID function that uses the array index.
183
183
  *
184
+ * **Warning**: This is only suitable for static, client-side data where the order
185
+ * never changes. For any of these scenarios, provide a custom `getRowId`:
186
+ *
187
+ * - Server-side sorting, filtering, or pagination
188
+ * - Data that can be reordered by the user
189
+ * - Data that can be inserted or deleted
190
+ * - Row selection that must persist across data updates
191
+ *
192
+ * Using array index with dynamic data causes:
193
+ * - Selection jumping to wrong rows after sort/filter
194
+ * - Selected state appearing on different rows after pagination
195
+ * - Inconsistent behavior when rows are added/removed
196
+ *
184
197
  * @param _row - The row data (unused)
185
198
  * @param index - The row index
186
199
  * @returns String representation of the index
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * // DON'T use defaultGetRowId with server-side data
204
+ * // DO provide a custom getRowId:
205
+ * getRowId={(row) => row.id}
206
+ * getRowId={(row) => `${row.type}-${row.id}`}
207
+ * ```
187
208
  */
188
209
  export function defaultGetRowId(_row, index) {
189
210
  return String(index);
190
211
  }
191
- // =============================================================================
192
- // Memoization Utilities
193
- // =============================================================================
194
212
  /**
195
- * Simple memoization for column definition conversion.
196
- * Caches the result based on a stable key derived from the column.
213
+ * Create a memoized converter with LRU cache eviction.
214
+ *
215
+ * Features:
216
+ * - Configurable maximum cache size (default: 100)
217
+ * - LRU (Least Recently Used) eviction when cache is full
218
+ * - Exposed `clear()` method to manually clear the cache
219
+ * - Exposed `size()` method to check current cache size
220
+ *
221
+ * @param converter - Function to convert input to output
222
+ * @param getKey - Function to generate a cache key from input
223
+ * @param maxSize - Maximum number of entries to cache (default: 100)
224
+ * @returns Memoized function with cache management methods
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const memoizedConvert = createMemoizedConverter(
229
+ * (col) => convertColumn(col),
230
+ * (col) => col.id,
231
+ * 50 // max 50 entries
232
+ * );
233
+ *
234
+ * memoizedConvert(column); // Converts and caches
235
+ * memoizedConvert(column); // Returns cached
236
+ * memoizedConvert.clear(); // Clears cache
237
+ * memoizedConvert.size(); // Returns 0
238
+ * ```
197
239
  */
198
- export function createMemoizedConverter(converter, getKey) {
240
+ export function createMemoizedConverter(converter, getKey, maxSize = 100) {
199
241
  const cache = new Map();
200
- return (input) => {
242
+ const memoized = (input) => {
201
243
  const key = getKey(input);
244
+ // Check cache first
202
245
  const cached = cache.get(key);
203
246
  if (cached !== undefined) {
247
+ // Move to end for LRU (delete and re-add)
248
+ cache.delete(key);
249
+ cache.set(key, cached);
204
250
  return cached;
205
251
  }
252
+ // Evict oldest entry if cache is full (LRU eviction)
253
+ if (cache.size >= maxSize) {
254
+ const oldestKey = cache.keys().next().value;
255
+ if (oldestKey !== undefined) {
256
+ cache.delete(oldestKey);
257
+ }
258
+ }
259
+ // Convert and cache
206
260
  const result = converter(input);
207
261
  cache.set(key, result);
208
262
  return result;
209
263
  };
210
- }
211
- /**
212
- * Clear all entries from a memoization cache.
213
- * Call this when the underlying data structure changes significantly.
214
- */
215
- export function clearMemoCache(_memoizedFn) {
216
- // The cache is encapsulated, so we need to recreate the function
217
- // This is a design trade-off for simplicity
218
- // In practice, recreating the converter is the cleaner approach
264
+ // Attach cache management methods
265
+ memoized.clear = () => cache.clear();
266
+ memoized.size = () => cache.size;
267
+ return memoized;
219
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/theme-svelte",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Svelte components for the Classic theme system",
5
5
  "type": "module",
6
6
  "svelte": "./dist/lib/index.js",