@autumnsgrove/groveengine 0.8.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/components/admin/GutterManager.svelte +213 -101
  2. package/dist/components/admin/MarkdownEditor.svelte +6 -3
  3. package/dist/components/custom/GutterItem.svelte +8 -2
  4. package/dist/components/quota/UpgradePrompt.svelte +1 -0
  5. package/dist/config/domain-blocklist.d.ts +59 -0
  6. package/dist/config/domain-blocklist.js +731 -0
  7. package/dist/config/index.d.ts +3 -1
  8. package/dist/config/index.js +2 -1
  9. package/dist/config/offensive-blocklist.d.ts +44 -0
  10. package/dist/config/offensive-blocklist.js +751 -0
  11. package/dist/config/terrarium.d.ts +109 -0
  12. package/dist/config/terrarium.js +125 -0
  13. package/dist/styles/tokens.css +90 -0
  14. package/dist/types/dom-to-image-more.d.ts +39 -0
  15. package/dist/ui/components/chrome/Footer.svelte +137 -0
  16. package/dist/ui/components/chrome/Footer.svelte.d.ts +11 -0
  17. package/dist/ui/components/chrome/FooterMinimal.svelte +75 -0
  18. package/dist/ui/components/chrome/FooterMinimal.svelte.d.ts +10 -0
  19. package/dist/ui/components/chrome/Header.svelte +113 -0
  20. package/dist/ui/components/chrome/Header.svelte.d.ts +11 -0
  21. package/dist/ui/components/chrome/HeaderMinimal.svelte +68 -0
  22. package/dist/ui/components/chrome/HeaderMinimal.svelte.d.ts +9 -0
  23. package/dist/ui/components/chrome/MobileMenu.svelte +145 -0
  24. package/dist/ui/components/chrome/MobileMenu.svelte.d.ts +9 -0
  25. package/dist/ui/components/chrome/ThemeToggle.svelte +34 -0
  26. package/dist/ui/components/chrome/ThemeToggle.svelte.d.ts +3 -0
  27. package/dist/ui/components/chrome/defaults.d.ts +6 -0
  28. package/dist/ui/components/chrome/defaults.js +65 -0
  29. package/dist/ui/components/chrome/index.d.ts +13 -0
  30. package/dist/ui/components/chrome/index.js +14 -0
  31. package/dist/ui/components/chrome/types.d.ts +19 -0
  32. package/dist/ui/components/chrome/types.js +8 -0
  33. package/dist/ui/components/content/RoadmapPreview.svelte +2 -1
  34. package/dist/ui/components/forms/ContentSearch.svelte +406 -0
  35. package/dist/ui/components/forms/ContentSearch.svelte.d.ts +71 -0
  36. package/dist/ui/components/forms/filterUtils.d.ts +138 -0
  37. package/dist/ui/components/forms/filterUtils.js +240 -0
  38. package/dist/ui/components/forms/index.d.ts +2 -0
  39. package/dist/ui/components/forms/index.js +5 -1
  40. package/dist/ui/components/gallery/ImageGallery.svelte +3 -0
  41. package/dist/ui/components/gallery/Lightbox.svelte +3 -0
  42. package/dist/ui/components/gallery/ZoomableImage.svelte +1 -0
  43. package/dist/ui/components/icons/index.d.ts +2 -1
  44. package/dist/ui/components/icons/index.js +14 -3
  45. package/dist/ui/components/icons/lucide.d.ts +213 -0
  46. package/dist/ui/components/icons/lucide.js +224 -0
  47. package/dist/ui/components/terrarium/AssetPalette.svelte +207 -0
  48. package/dist/ui/components/terrarium/AssetPalette.svelte.d.ts +7 -0
  49. package/dist/ui/components/terrarium/Canvas.svelte +231 -0
  50. package/dist/ui/components/terrarium/Canvas.svelte.d.ts +14 -0
  51. package/dist/ui/components/terrarium/ExportDialog.svelte +307 -0
  52. package/dist/ui/components/terrarium/ExportDialog.svelte.d.ts +18 -0
  53. package/dist/ui/components/terrarium/PaletteItem.svelte +169 -0
  54. package/dist/ui/components/terrarium/PaletteItem.svelte.d.ts +9 -0
  55. package/dist/ui/components/terrarium/PlacedAsset.svelte +222 -0
  56. package/dist/ui/components/terrarium/PlacedAsset.svelte.d.ts +11 -0
  57. package/dist/ui/components/terrarium/Terrarium.svelte +266 -0
  58. package/dist/ui/components/terrarium/Terrarium.svelte.d.ts +3 -0
  59. package/dist/ui/components/terrarium/Toolbar.svelte +299 -0
  60. package/dist/ui/components/terrarium/Toolbar.svelte.d.ts +24 -0
  61. package/dist/ui/components/terrarium/index.d.ts +31 -0
  62. package/dist/ui/components/terrarium/index.js +33 -0
  63. package/dist/ui/components/terrarium/terrariumState.svelte.d.ts +45 -0
  64. package/dist/ui/components/terrarium/terrariumState.svelte.js +291 -0
  65. package/dist/ui/components/terrarium/types.d.ts +139 -0
  66. package/dist/ui/components/terrarium/types.js +43 -0
  67. package/dist/ui/components/terrarium/utils/export.d.ts +48 -0
  68. package/dist/ui/components/terrarium/utils/export.js +148 -0
  69. package/dist/ui/components/ui/CollapsibleSection.svelte +2 -0
  70. package/dist/ui/components/ui/GlassConfirmDialog.svelte +9 -0
  71. package/dist/ui/components/ui/GlassOverlay.svelte +2 -1
  72. package/dist/ui/components/ui/Input.svelte +9 -1
  73. package/dist/ui/components/ui/Input.svelte.d.ts +2 -0
  74. package/dist/ui/components/ui/Textarea.svelte +9 -1
  75. package/dist/ui/components/ui/Textarea.svelte.d.ts +2 -0
  76. package/dist/ui/stores/index.d.ts +6 -0
  77. package/dist/ui/stores/index.js +6 -0
  78. package/dist/ui/stores/season.d.ts +14 -0
  79. package/dist/ui/stores/season.js +65 -0
  80. package/package.json +27 -4
@@ -0,0 +1,406 @@
1
+ <!--
2
+ ContentSearch Component
3
+
4
+ A reusable, accessible search component with debouncing, URL synchronization, and custom filtering.
5
+ Part of the Grove UI design system - "a place to Be"
6
+
7
+ @example Basic usage
8
+ ```svelte
9
+ <ContentSearch
10
+ items={posts}
11
+ filterFn={(post, query) => post.title.toLowerCase().includes(query.toLowerCase())}
12
+ bind:searchQuery
13
+ placeholder="Search posts..."
14
+ />
15
+ ```
16
+
17
+ @example With URL sync and custom filters
18
+ ```svelte
19
+ <ContentSearch
20
+ items={docs}
21
+ filterFn={filterDoc}
22
+ bind:searchQuery
23
+ syncWithUrl={true}
24
+ onSearchChange={(query, results) => filteredResults = results}
25
+ >
26
+ {#snippet children()}
27
+ <div class="custom-filters">
28
+ Add tag filters, date filters, etc. here
29
+ </div>
30
+ {/snippet}
31
+ </ContentSearch>
32
+ ```
33
+
34
+ @example With pre-computed lowercase fields (performance optimization)
35
+ ```svelte
36
+ <script>
37
+ // Pre-compute lowercase fields for better performance
38
+ let postsWithLowercase = $derived.by(() => {
39
+ return posts.map(post => ({
40
+ ...post,
41
+ titleLower: post.title.toLowerCase(),
42
+ tagsLower: post.tags.map(t => t.toLowerCase())
43
+ }));
44
+ });
45
+
46
+ function filterPost(post, query) {
47
+ const q = query.toLowerCase();
48
+ return post.titleLower.includes(q) || post.tagsLower.some(tag => tag.includes(q));
49
+ }
50
+ </script>
51
+
52
+ <ContentSearch items={postsWithLowercase} filterFn={filterPost} />
53
+ ```
54
+ -->
55
+ <script lang="ts" generics="T extends Record<string, unknown>">
56
+ import { page } from '$app/stores';
57
+ import { goto } from '$app/navigation';
58
+ import type { Snippet } from 'svelte';
59
+
60
+ interface Props {
61
+ /** Array of items to search through */
62
+ items: T[];
63
+ /** Filter function that determines if an item matches the search query */
64
+ filterFn: (item: T, query: string) => boolean;
65
+ /** Current search query (bindable) */
66
+ searchQuery?: string;
67
+ /** Placeholder text for search input */
68
+ placeholder?: string;
69
+ /**
70
+ * Type of results for screen reader announcements (singular form)
71
+ * @example "post" → "Found 1 post" / "Found 5 posts"
72
+ * @example "document" → "Found 1 document" / "Found 5 documents"
73
+ * @example "match" → Use with resultsTypePlural for "Found 1 match" / "Found 5 matches"
74
+ */
75
+ resultsType?: string;
76
+ /**
77
+ * Plural form of resultsType (defaults to resultsType + 's')
78
+ *
79
+ * **Use this for irregular plurals:**
80
+ * @example resultsType="match" resultsTypePlural="matches"
81
+ * @example resultsType="person" resultsTypePlural="people"
82
+ * @example resultsType="child" resultsTypePlural="children"
83
+ * @example resultsType="datum" resultsTypePlural="data"
84
+ *
85
+ * Screen reader will announce: "Found 1 match" / "Found 5 matches"
86
+ */
87
+ resultsTypePlural?: string;
88
+ /** Whether to sync search query with URL params */
89
+ syncWithUrl?: boolean;
90
+ /** URL parameter name for search query */
91
+ queryParam?: string;
92
+ /** Debounce delay in milliseconds */
93
+ debounceDelay?: number;
94
+ /** Whether to show search icon */
95
+ showIcon?: boolean;
96
+ /** Whether to show clear button when there's a query */
97
+ showClearButton?: boolean;
98
+ /** CSS class for the search input wrapper */
99
+ wrapperClass?: string;
100
+ /** CSS class for the search input */
101
+ inputClass?: string;
102
+ /** Callback when search query changes */
103
+ onSearchChange?: (query: string, results: T[]) => void;
104
+ /** Optional snippet for filters */
105
+ children?: Snippet;
106
+ }
107
+
108
+ let {
109
+ items = [],
110
+ filterFn,
111
+ searchQuery = $bindable(''),
112
+ placeholder = 'Search...',
113
+ resultsType = 'result',
114
+ resultsTypePlural,
115
+ syncWithUrl = false,
116
+ queryParam = 'q',
117
+ debounceDelay = 250,
118
+ showIcon = true,
119
+ showClearButton = true,
120
+ wrapperClass = '',
121
+ inputClass = '',
122
+ onSearchChange,
123
+ children
124
+ }: Props = $props();
125
+
126
+ // Generate unique ID for accessibility
127
+ const searchId = `content-search-${Math.random().toString(36).substring(2, 9)}`;
128
+ const clearButtonId = `${searchId}-clear`;
129
+ const resultsId = `${searchId}-results`;
130
+
131
+ // Initialize from URL if syncing
132
+ $effect(() => {
133
+ if (syncWithUrl && typeof window !== 'undefined') {
134
+ const urlQuery = $page.url.searchParams.get(queryParam);
135
+ if (urlQuery && urlQuery !== searchQuery) {
136
+ searchQuery = urlQuery;
137
+ debouncedQuery = urlQuery;
138
+ }
139
+ }
140
+ });
141
+
142
+ // Debounced search query for performance
143
+ let debouncedQuery = $state(searchQuery);
144
+ let debounceTimer: ReturnType<typeof setTimeout> | null = $state(null);
145
+
146
+ // Cleanup timer on component destruction to prevent memory leaks
147
+ // This is separate from the handleInput cleanup because:
148
+ // - handleInput clears the timer when user types (normal debounce behavior)
149
+ // - This cleanup runs when component is destroyed (prevents timer firing after unmount)
150
+ $effect(() => {
151
+ return () => {
152
+ if (debounceTimer) {
153
+ clearTimeout(debounceTimer);
154
+ }
155
+ };
156
+ });
157
+
158
+ function handleInput(event: Event) {
159
+ const target = event.target as HTMLInputElement;
160
+ searchQuery = target.value;
161
+
162
+ // Clear existing timer to restart debounce window
163
+ if (debounceTimer) {
164
+ clearTimeout(debounceTimer);
165
+ }
166
+
167
+ // Set new timer for debounced update
168
+ debounceTimer = setTimeout(() => {
169
+ debouncedQuery = searchQuery;
170
+ if (syncWithUrl) {
171
+ updateUrl();
172
+ }
173
+ }, debounceDelay);
174
+ }
175
+
176
+ // Filter items based on debounced search query
177
+ let filteredItems = $derived.by(() => {
178
+ if (!debouncedQuery.trim()) {
179
+ return items;
180
+ }
181
+ return items.filter(item => filterFn(item, debouncedQuery.trim()));
182
+ });
183
+
184
+ // Call onSearchChange when results change
185
+ $effect(() => {
186
+ if (onSearchChange) {
187
+ onSearchChange(debouncedQuery, filteredItems);
188
+ }
189
+ });
190
+
191
+ // Update URL when search query changes (optimized to avoid unnecessary navigation)
192
+ function updateUrl() {
193
+ if (!syncWithUrl) return;
194
+
195
+ const currentQuery = $page.url.searchParams.get(queryParam) || '';
196
+ const newQuery = searchQuery.trim();
197
+
198
+ // Skip navigation if query hasn't actually changed
199
+ if (currentQuery === newQuery) return;
200
+
201
+ const params = new URLSearchParams($page.url.searchParams);
202
+ if (newQuery) {
203
+ params.set(queryParam, newQuery);
204
+ } else {
205
+ params.delete(queryParam);
206
+ }
207
+
208
+ const newUrl = params.toString() ? `?${params.toString()}` : $page.url.pathname;
209
+ goto(newUrl, { replaceState: true, keepFocus: true });
210
+ }
211
+
212
+ function clearSearch() {
213
+ searchQuery = '';
214
+ debouncedQuery = '';
215
+ if (debounceTimer) {
216
+ clearTimeout(debounceTimer);
217
+ debounceTimer = null;
218
+ }
219
+ if (syncWithUrl) {
220
+ updateUrl();
221
+ }
222
+ }
223
+ </script>
224
+
225
+ <div class="content-search-wrapper {wrapperClass}" role="search">
226
+ <div class="content-search-input-container">
227
+ {#if showIcon}
228
+ <svg
229
+ class="content-search-icon"
230
+ xmlns="http://www.w3.org/2000/svg"
231
+ width="20"
232
+ height="20"
233
+ viewBox="0 0 24 24"
234
+ fill="none"
235
+ stroke="currentColor"
236
+ stroke-width="2"
237
+ stroke-linecap="round"
238
+ stroke-linejoin="round"
239
+ aria-hidden="true"
240
+ >
241
+ <circle cx="11" cy="11" r="8"></circle>
242
+ <path d="m21 21-4.3-4.3"></path>
243
+ </svg>
244
+ {/if}
245
+ <input
246
+ type="search"
247
+ id={searchId}
248
+ aria-label={placeholder}
249
+ aria-describedby={showClearButton && searchQuery ? clearButtonId : undefined}
250
+ {placeholder}
251
+ value={searchQuery}
252
+ oninput={handleInput}
253
+ class="content-search-input {inputClass}"
254
+ />
255
+ {#if showClearButton && searchQuery}
256
+ <button
257
+ type="button"
258
+ id={clearButtonId}
259
+ onclick={clearSearch}
260
+ class="content-search-clear"
261
+ aria-label="Clear search query"
262
+ >
263
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
264
+ <path d="M18 6 6 18"></path>
265
+ <path d="m6 6 12 12"></path>
266
+ </svg>
267
+ </button>
268
+ {/if}
269
+ </div>
270
+
271
+ {#if children}
272
+ <div class="content-search-filters">
273
+ {@render children()}
274
+ </div>
275
+ {/if}
276
+
277
+ <!-- Screen reader announcement for search results -->
278
+ <div id={resultsId} role="status" aria-live="polite" aria-atomic="true" aria-label="Search results" class="sr-only">
279
+ {#if debouncedQuery}
280
+ Found {filteredItems.length} {filteredItems.length === 1 ? resultsType : (resultsTypePlural || resultsType + 's')} matching "{debouncedQuery}"
281
+ {/if}
282
+ </div>
283
+ </div>
284
+
285
+ <style>
286
+ .content-search-wrapper {
287
+ width: 100%;
288
+ }
289
+
290
+ .content-search-input-container {
291
+ position: relative;
292
+ display: flex;
293
+ align-items: center;
294
+ }
295
+
296
+ .content-search-icon {
297
+ position: absolute;
298
+ left: 1rem;
299
+ color: var(--light-text-light, #999);
300
+ pointer-events: none;
301
+ transition: color 0.3s ease;
302
+ }
303
+
304
+ .content-search-input {
305
+ width: 100%;
306
+ padding: 0.75rem 1rem;
307
+ padding-left: 3rem;
308
+ font-size: 1rem;
309
+ border: 2px solid var(--light-border-primary, #e5e7eb);
310
+ border-radius: 0.75rem;
311
+ background: white;
312
+ color: var(--light-text-primary, #1f2937);
313
+ transition: border-color 0.2s ease, background-color 0.3s ease, color 0.3s ease;
314
+ }
315
+
316
+ .content-search-input:focus {
317
+ outline: none;
318
+ border-color: var(--accent, #2c5f2d);
319
+ }
320
+
321
+ .content-search-input::-moz-placeholder {
322
+ color: var(--light-text-muted, #9ca3af);
323
+ }
324
+
325
+ .content-search-input::placeholder {
326
+ color: var(--light-text-muted, #9ca3af);
327
+ }
328
+
329
+ .content-search-clear {
330
+ position: absolute;
331
+ right: 0.75rem;
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ padding: 0.5rem;
336
+ background: transparent;
337
+ border: none;
338
+ cursor: pointer;
339
+ color: var(--light-text-light, #999);
340
+ transition: color 0.2s ease;
341
+ border-radius: 0.375rem;
342
+ }
343
+
344
+ .content-search-clear:hover {
345
+ color: var(--light-text-primary, #1f2937);
346
+ background-color: var(--light-bg-secondary, #f3f4f6);
347
+ }
348
+
349
+ .content-search-clear:focus {
350
+ outline: 2px solid var(--accent, #2c5f2d);
351
+ outline-offset: 2px;
352
+ }
353
+
354
+ .content-search-filters {
355
+ margin-top: 1rem;
356
+ }
357
+
358
+ /* Visually hidden but accessible to screen readers */
359
+ .sr-only {
360
+ position: absolute;
361
+ width: 1px;
362
+ height: 1px;
363
+ padding: 0;
364
+ margin: -1px;
365
+ overflow: hidden;
366
+ clip: rect(0, 0, 0, 0);
367
+ white-space: nowrap;
368
+ border-width: 0;
369
+ }
370
+
371
+ /* Dark mode support */
372
+ :global(.dark) .content-search-icon {
373
+ color: var(--dark-text-light, #6b7280);
374
+ }
375
+
376
+ :global(.dark) .content-search-input {
377
+ background: var(--dark-bg-secondary, #1f2937);
378
+ border-color: var(--dark-border-primary, #374151);
379
+ color: var(--dark-text-primary, #f9fafb);
380
+ }
381
+
382
+ :global(.dark) .content-search-input:focus {
383
+ border-color: var(--accent, #4ade80);
384
+ }
385
+
386
+ :global(.dark) .content-search-input::-moz-placeholder {
387
+ color: var(--dark-text-muted, #6b7280);
388
+ }
389
+
390
+ :global(.dark) .content-search-input::placeholder {
391
+ color: var(--dark-text-muted, #6b7280);
392
+ }
393
+
394
+ :global(.dark) .content-search-clear {
395
+ color: var(--dark-text-light, #6b7280);
396
+ }
397
+
398
+ :global(.dark) .content-search-clear:hover {
399
+ color: var(--dark-text-primary, #f9fafb);
400
+ background-color: var(--dark-bg-tertiary, #111827);
401
+ }
402
+
403
+ :global(.dark) .content-search-clear:focus {
404
+ outline-color: var(--accent, #4ade80);
405
+ }
406
+ </style>
@@ -0,0 +1,71 @@
1
+ import type { Snippet } from 'svelte';
2
+ declare function $$render<T extends Record<string, unknown>>(): {
3
+ props: {
4
+ /** Array of items to search through */
5
+ items: T[];
6
+ /** Filter function that determines if an item matches the search query */
7
+ filterFn: (item: T, query: string) => boolean;
8
+ /** Current search query (bindable) */
9
+ searchQuery?: string;
10
+ /** Placeholder text for search input */
11
+ placeholder?: string;
12
+ /**
13
+ * Type of results for screen reader announcements (singular form)
14
+ * @example "post" → "Found 1 post" / "Found 5 posts"
15
+ * @example "document" → "Found 1 document" / "Found 5 documents"
16
+ * @example "match" → Use with resultsTypePlural for "Found 1 match" / "Found 5 matches"
17
+ */
18
+ resultsType?: string;
19
+ /**
20
+ * Plural form of resultsType (defaults to resultsType + 's')
21
+ *
22
+ * **Use this for irregular plurals:**
23
+ * @example resultsType="match" resultsTypePlural="matches"
24
+ * @example resultsType="person" resultsTypePlural="people"
25
+ * @example resultsType="child" resultsTypePlural="children"
26
+ * @example resultsType="datum" resultsTypePlural="data"
27
+ *
28
+ * Screen reader will announce: "Found 1 match" / "Found 5 matches"
29
+ */
30
+ resultsTypePlural?: string;
31
+ /** Whether to sync search query with URL params */
32
+ syncWithUrl?: boolean;
33
+ /** URL parameter name for search query */
34
+ queryParam?: string;
35
+ /** Debounce delay in milliseconds */
36
+ debounceDelay?: number;
37
+ /** Whether to show search icon */
38
+ showIcon?: boolean;
39
+ /** Whether to show clear button when there's a query */
40
+ showClearButton?: boolean;
41
+ /** CSS class for the search input wrapper */
42
+ wrapperClass?: string;
43
+ /** CSS class for the search input */
44
+ inputClass?: string;
45
+ /** Callback when search query changes */
46
+ onSearchChange?: (query: string, results: T[]) => void;
47
+ /** Optional snippet for filters */
48
+ children?: Snippet;
49
+ };
50
+ exports: {};
51
+ bindings: "searchQuery";
52
+ slots: {};
53
+ events: {};
54
+ };
55
+ declare class __sveltets_Render<T extends Record<string, unknown>> {
56
+ props(): ReturnType<typeof $$render<T>>['props'];
57
+ events(): ReturnType<typeof $$render<T>>['events'];
58
+ slots(): ReturnType<typeof $$render<T>>['slots'];
59
+ bindings(): "searchQuery";
60
+ exports(): {};
61
+ }
62
+ interface $$IsomorphicComponent {
63
+ new <T extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
64
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
65
+ } & ReturnType<__sveltets_Render<T>['exports']>;
66
+ <T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
67
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
68
+ }
69
+ declare const ContentSearch: $$IsomorphicComponent;
70
+ type ContentSearch<T extends Record<string, unknown>> = InstanceType<typeof ContentSearch<T>>;
71
+ export default ContentSearch;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Common filter utilities for use with ContentSearch component
3
+ * Part of the Grove UI design system
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { createTextFilter, createMultiFieldFilter } from '@autumnsgrove/groveengine';
8
+ *
9
+ * const filterPost = createTextFilter(['title', 'description']);
10
+ * <ContentSearch items={posts} filterFn={filterPost} />
11
+ * ```
12
+ */
13
+ /**
14
+ * Normalize text for searching (lowercase, trim)
15
+ */
16
+ export declare function normalizeSearchText(text: string): string;
17
+ /**
18
+ * Check if normalized text includes a normalized query
19
+ * Note: Both inputs should already be normalized via normalizeSearchText()
20
+ *
21
+ * @param normalizedText - Already normalized text to search in
22
+ * @param normalizedQuery - Already normalized query to search for
23
+ */
24
+ export declare function includesNormalized(normalizedText: string, normalizedQuery: string): boolean;
25
+ /**
26
+ * Check if text includes a search query (case-insensitive)
27
+ * This is a convenience function that normalizes both inputs
28
+ *
29
+ * @param text - Text to search in (will be normalized)
30
+ * @param query - Query to search for (will be normalized)
31
+ */
32
+ export declare function textIncludes(text: string, query: string): boolean;
33
+ /**
34
+ * Create a filter function that searches multiple text fields
35
+ *
36
+ * @param fields - Array of field names to search
37
+ * @returns Filter function for use with ContentSearch
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const filterPost = createTextFilter(['title', 'description', 'author']);
42
+ * <ContentSearch items={posts} filterFn={filterPost} />
43
+ * ```
44
+ */
45
+ export declare function createTextFilter<T extends Record<string, unknown>>(fields: (keyof T)[]): (item: T, query: string) => boolean;
46
+ /**
47
+ * Create a filter function that searches text fields and array fields (like tags)
48
+ *
49
+ * @param textFields - Array of text field names to search
50
+ * @param arrayFields - Array of array field names to search (e.g., tags)
51
+ * @returns Filter function for use with ContentSearch
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const filterPost = createMultiFieldFilter(['title', 'description'], ['tags', 'categories']);
56
+ * <ContentSearch items={posts} filterFn={filterPost} />
57
+ * ```
58
+ */
59
+ export declare function createMultiFieldFilter<T extends Record<string, unknown>>(textFields: (keyof T)[], arrayFields?: (keyof T)[]): (item: T, query: string) => boolean;
60
+ /**
61
+ * Pre-compute lowercase versions of searchable fields for performance
62
+ *
63
+ * **When to use this optimization:**
64
+ * - **Large datasets (100+ items)**: Repeated toLowerCase() calls during filtering become expensive
65
+ * - **Frequent searches**: Users actively searching/filtering the same dataset multiple times
66
+ * - **Multiple searchable fields**: More fields = more normalization = more performance gain
67
+ * - **Real-time filtering**: When debouncing isn't enough and you need instant results
68
+ *
69
+ * **When NOT to use:**
70
+ * - Small datasets (<50 items): Minimal performance benefit, adds memory overhead
71
+ * - Static lists that don't change: Consider precomputing at build time instead
72
+ * - Infrequent searches: The setup cost may outweigh the benefit
73
+ *
74
+ * @param items - Array of items to optimize
75
+ * @param fields - Fields to pre-compute lowercase versions of
76
+ * @returns Array of items with lowercase fields added (suffixed with 'Lower')
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * // In your Svelte component with large dataset (100+ posts)
81
+ * let postsWithLowercase = $derived.by(() => {
82
+ * return precomputeLowercaseFields(posts, ['title', 'description', 'tags']);
83
+ * });
84
+ * // Creates: { ...post, titleLower: '...', descriptionLower: '...', tagsLower: [...] }
85
+ *
86
+ * function filterPost(post, query) {
87
+ * const q = query.toLowerCase();
88
+ * return post.titleLower.includes(q) ||
89
+ * post.descriptionLower.includes(q) ||
90
+ * post.tagsLower.some(tag => tag.includes(q));
91
+ * }
92
+ *
93
+ * <ContentSearch items={postsWithLowercase} filterFn={filterPost} />
94
+ * ```
95
+ */
96
+ export declare function precomputeLowercaseFields<T extends Record<string, unknown>>(items: T[], fields: (keyof T)[]): (T & Record<string, unknown>)[];
97
+ /**
98
+ * Create a fuzzy filter that matches partial words
99
+ *
100
+ * @param fields - Array of field names to search
101
+ * @param minMatchLength - Minimum length of query before fuzzy matching (default: 2)
102
+ * @returns Filter function for use with ContentSearch
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const filterPost = createFuzzyFilter(['title', 'description']);
107
+ * // Matches: "jav" in "JavaScript", "scr" in "TypeScript"
108
+ * ```
109
+ */
110
+ export declare function createFuzzyFilter<T extends Record<string, unknown>>(fields: (keyof T)[], minMatchLength?: number): (item: T, query: string) => boolean;
111
+ /**
112
+ * Combine multiple filter functions with AND logic
113
+ *
114
+ * @param filters - Array of filter functions to combine
115
+ * @returns Combined filter function
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const hasTag = (item, tag) => item.tags.includes(tag);
120
+ * const matchesText = createTextFilter(['title']);
121
+ * const combinedFilter = combineFilters([matchesText, (item) => hasTag(item, 'featured')]);
122
+ * ```
123
+ */
124
+ export declare function combineFilters<T>(filters: Array<(item: T, query: string) => boolean>): (item: T, query: string) => boolean;
125
+ /**
126
+ * Create a date range filter
127
+ *
128
+ * @param dateField - Field name containing the date
129
+ * @param startDate - Start of date range (optional)
130
+ * @param endDate - End of date range (optional)
131
+ * @returns Filter function for use with ContentSearch
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const recentPostsFilter = createDateFilter('publishedAt', new Date('2024-01-01'));
136
+ * ```
137
+ */
138
+ export declare function createDateFilter<T extends Record<string, unknown>>(dateField: keyof T, startDate?: Date, endDate?: Date): (item: T, query: string) => boolean;