intelliwaketssveltekitv25 1.0.82 → 1.0.83

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.
@@ -0,0 +1,304 @@
1
+ # MultiSelect Component
2
+
3
+ **Replaces:** Multiple `<select multiple>` and custom multi-select implementations
4
+
5
+ **Purpose:** Type-safe, searchable multi-select dropdown with dynamic item creation and keyboard navigation
6
+
7
+ **When to Use:**
8
+ - Selecting multiple items from a list (tags, categories, assignees)
9
+ - Searchable selection with many options
10
+ - Dynamic item creation (creating new tags on the fly)
11
+ - Single-select with search (set `isMulti={false}`)
12
+ - Type-safe selections with TypeScript generics
13
+
14
+ ## Key Props
15
+
16
+ - `possibles: T[]` - Array of available items to select from (required)
17
+ - `selected?: T[]` ($bindable) - Currently selected items array
18
+ - `selectedIDs?: (number | string)[]` ($bindable) - Selected item IDs (alternative to selected)
19
+ - `created?: T[]` ($bindable) - Items that were created (not in possibles)
20
+ - `existing?: T[]` ($bindable) - Items that exist in possibles
21
+ - `isMulti?: boolean` (default: true) - Allow multiple selections or single select
22
+ - `allowClearAll?: boolean` (default: true) - Show clear all button
23
+ - `create?: (value: string) => T | null` - Function to create new items from search text
24
+ - `createValid?: (value: string) => boolean | string` - Validate new item creation (return string for error message)
25
+ - `createPrefix?: string` (default: 'Create:') - Prefix text for create option
26
+ - `displayValue?: (item: T) => string | number` - How to display items (default: item.name or item.id)
27
+ - `idValue?: (item: T) => any` - How to get unique ID from items (default: item.id)
28
+ - `keyValue?: (item: T) => any` - Key for Svelte each blocks (default: same as idValue)
29
+ - `inputValue?: (item: T) => any` - Value for hidden form inputs (default: same as idValue)
30
+ - `headerValue?: (item: T) => string | null` - Extract header/category from items for grouping
31
+ - `show?: boolean` ($bindable) - Control dropdown open/closed state
32
+ - `placeholder?: string` - Input placeholder text
33
+ - `disabled?: boolean` - Disable all interactions
34
+ - `readonly?: boolean` - Display only, no interaction
35
+ - `required?: boolean` - Mark as required field
36
+ - `invalid?: boolean` - Show invalid state
37
+ - `autoFocus?: boolean` - Auto-focus input on mount
38
+ - `name?: string` - Form field name (creates hidden inputs for each selected item)
39
+ - `form?: string` - Associate with form by ID
40
+ - Callback props: `onadd`, `onselect`, `oncreate`, `onchange`, `onclear`, `onclearall`
41
+
42
+ **TypeScript:** Uses generics `<T extends TGenericMultiSelect>` for type safety
43
+
44
+ ## TGenericMultiSelect Interface
45
+
46
+ ```typescript
47
+ interface TGenericMultiSelect {
48
+ id?: string | number;
49
+ name?: string;
50
+ header?: string; // For grouping
51
+ [key: string]: any; // Additional properties
52
+ }
53
+ ```
54
+
55
+ ## Keyboard Navigation
56
+
57
+ - **Arrow Down/Up**: Navigate dropdown options
58
+ - **Enter**: Select highlighted option or create new item
59
+ - **Backspace**: Remove last selected item (when search is empty)
60
+ - **Type**: Filter options by search
61
+
62
+ ## Usage Examples
63
+
64
+ ```svelte
65
+ <script lang="ts">
66
+ import { MultiSelect } from 'intelliwaketssveltekitv25';
67
+
68
+ interface Tag {
69
+ id: number;
70
+ name: string;
71
+ }
72
+
73
+ const availableTags: Tag[] = [
74
+ { id: 1, name: 'JavaScript' },
75
+ { id: 2, name: 'TypeScript' },
76
+ { id: 3, name: 'Svelte' }
77
+ ];
78
+
79
+ let selectedTags = $state<Tag[]>([]);
80
+ </script>
81
+
82
+ <!-- Basic multi-select -->
83
+ <MultiSelect
84
+ possibles={availableTags}
85
+ bind:selected={selectedTags}
86
+ placeholder="Select tags..."
87
+ />
88
+
89
+ <!-- With dynamic item creation -->
90
+ <MultiSelect
91
+ possibles={availableTags}
92
+ bind:selected={selectedTags}
93
+ placeholder="Select or create tags..."
94
+ create={(name) => ({
95
+ id: Date.now(),
96
+ name: name
97
+ })}
98
+ />
99
+
100
+ <!-- With creation validation -->
101
+ <MultiSelect
102
+ possibles={availableTags}
103
+ bind:selected={selectedTags}
104
+ create={(name) => ({ id: Date.now(), name })}
105
+ createValid={(name) => {
106
+ if (name.length < 3) return 'Tag must be at least 3 characters';
107
+ if (availableTags.some(t => t.name.toLowerCase() === name.toLowerCase())) {
108
+ return 'Tag already exists';
109
+ }
110
+ return true;
111
+ }}
112
+ />
113
+
114
+ <!-- Single select mode -->
115
+ <MultiSelect
116
+ possibles={availableTags}
117
+ bind:selected={selectedTags}
118
+ isMulti={false}
119
+ placeholder="Select one..."
120
+ />
121
+
122
+ <!-- With selected IDs binding -->
123
+ <script>
124
+ let selectedTagIDs = $state<number[]>([1, 3]);
125
+ </script>
126
+
127
+ <MultiSelect
128
+ possibles={availableTags}
129
+ bind:selectedIDs={selectedTagIDs}
130
+ placeholder="Select tags..."
131
+ />
132
+
133
+ <!-- With custom display function -->
134
+ <script>
135
+ interface User {
136
+ id: number;
137
+ firstName: string;
138
+ lastName: string;
139
+ email: string;
140
+ }
141
+
142
+ const users: User[] = [
143
+ { id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
144
+ { id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }
145
+ ];
146
+
147
+ let assignees = $state<User[]>([]);
148
+ </script>
149
+
150
+ <MultiSelect
151
+ possibles={users}
152
+ bind:selected={assignees}
153
+ displayValue={(user) => `${user.firstName} ${user.lastName}`}
154
+ placeholder="Assign to..."
155
+ />
156
+
157
+ <!-- With grouping/headers -->
158
+ <script>
159
+ interface Item {
160
+ id: number;
161
+ name: string;
162
+ category: string;
163
+ }
164
+
165
+ const items: Item[] = [
166
+ { id: 1, name: 'Apple', category: 'Fruits' },
167
+ { id: 2, name: 'Banana', category: 'Fruits' },
168
+ { id: 3, name: 'Carrot', category: 'Vegetables' },
169
+ { id: 4, name: 'Lettuce', category: 'Vegetables' }
170
+ ];
171
+ </script>
172
+
173
+ <MultiSelect
174
+ possibles={items}
175
+ bind:selected={selectedItems}
176
+ headerValue={(item) => item.category}
177
+ placeholder="Select items..."
178
+ />
179
+
180
+ <!-- In a form -->
181
+ <form method="POST">
182
+ <MultiSelect
183
+ name="tags"
184
+ possibles={availableTags}
185
+ bind:selected={selectedTags}
186
+ required
187
+ placeholder="Select at least one tag..."
188
+ />
189
+ <button type="submit">Submit</button>
190
+ </form>
191
+
192
+ <!-- With callbacks -->
193
+ <MultiSelect
194
+ possibles={availableTags}
195
+ bind:selected={selectedTags}
196
+ onadd={(id) => console.log('Added:', id)}
197
+ onselect={(id) => console.log('Selected existing:', id)}
198
+ oncreate={(id) => console.log('Created new:', id)}
199
+ onchange={(items) => console.log('Selection changed:', items)}
200
+ onclear={(id) => console.log('Removed:', id)}
201
+ onclearall={() => console.log('Cleared all')}
202
+ />
203
+
204
+ <!-- Readonly/disabled -->
205
+ <MultiSelect
206
+ possibles={availableTags}
207
+ selected={[availableTags[0], availableTags[2]]}
208
+ readonly
209
+ />
210
+
211
+ <MultiSelect
212
+ possibles={availableTags}
213
+ bind:selected={selectedTags}
214
+ disabled
215
+ />
216
+
217
+ <!-- Without clear all button -->
218
+ <MultiSelect
219
+ possibles={availableTags}
220
+ bind:selected={selectedTags}
221
+ allowClearAll={false}
222
+ />
223
+ ```
224
+
225
+ ## Common Mistakes
226
+
227
+ - ❌ Not providing `possibles` array: `<MultiSelect bind:selected={x} />`
228
+ ✅ Correct: `<MultiSelect possibles={items} bind:selected={x} />`
229
+
230
+ - ❌ Binding to non-array for multi-select: `bind:selected={singleItem}`
231
+ ✅ Correct: `bind:selected={arrayOfItems}` (always an array, even for single select)
232
+
233
+ - ❌ Not using TypeScript generics: `possibles: any[]`
234
+ ✅ Correct: `possibles: Tag[]` with `interface Tag extends TGenericMultiSelect`
235
+
236
+ - ❌ Forgetting id or name in data objects: `possibles={[{label: 'Item'}]}`
237
+ ✅ Correct: `possibles={[{id: 1, name: 'Item'}]}` or provide custom `idValue` and `displayValue` functions
238
+
239
+ - ❌ Using create without validation when it can fail
240
+ ✅ Correct: Use `createValid` to validate and show error messages
241
+
242
+ - ❌ Not handling created vs existing items separately
243
+ ✅ Correct: Use `bind:created` and `bind:existing` to differentiate
244
+
245
+ ## Advanced Features
246
+
247
+ ### 1. Dynamic Item Creation
248
+
249
+ ```svelte
250
+ <MultiSelect
251
+ possibles={tags}
252
+ bind:selected={selectedTags}
253
+ bind:created={newlyCreatedTags}
254
+ bind:existing={existingSelectedTags}
255
+ create={(name) => ({ id: Date.now(), name })}
256
+ createPrefix="Add new:"
257
+ oncreate={(id) => {
258
+ // Save new tag to backend
259
+ saveNewTag(id);
260
+ }}
261
+ />
262
+ ```
263
+
264
+ ### 2. Custom Value Functions
265
+
266
+ ```svelte
267
+ <script>
268
+ interface ComplexItem {
269
+ uuid: string;
270
+ displayName: string;
271
+ sortOrder: number;
272
+ }
273
+ </script>
274
+
275
+ <MultiSelect
276
+ possibles={complexItems}
277
+ bind:selected={selectedItems}
278
+ idValue={(item) => item.uuid}
279
+ displayValue={(item) => item.displayName}
280
+ keyValue={(item) => item.uuid}
281
+ inputValue={(item) => item.uuid}
282
+ />
283
+ ```
284
+
285
+ ### 3. Grouped Options
286
+
287
+ ```svelte
288
+ <MultiSelect
289
+ possibles={groupedItems}
290
+ bind:selected={selectedItems}
291
+ headerValue={(item) => item.category}
292
+ />
293
+ <!-- Automatically adds category headers when category changes -->
294
+ ```
295
+
296
+ ## Related Components
297
+
298
+ - `DropDown` - For single action/selection without search
299
+ - `DropDownControl` - Lower-level dropdown control (used internally)
300
+ - `DisplayHTML` - Used to render item text
301
+
302
+ ## Storybook
303
+
304
+ See `Components/MultiSelect` stories
@@ -0,0 +1,332 @@
1
+ # Paginator Component
2
+
3
+ **Purpose:** Responsive pagination controls with SvelteKit integration
4
+
5
+ **When to Use:**
6
+ - Navigate through multi-page data sets
7
+ - Integrate with SvelteKit query parameters
8
+ - Automatic data invalidation on page change
9
+ - Responsive design adapting to screen width
10
+
11
+ ## Key Props
12
+
13
+ - `page: number` ($bindable, required) - Current page number (1-indexed)
14
+ - `pageCount: number` (required) - Total number of pages
15
+ - `updateQueryParams?: boolean | string` (default: false) - Sync with URL query params
16
+ - `invalidate?: string | string[] | 'All'` - SvelteKit invalidation targets
17
+ - `onPageChange?: (page: number) => void` - Callback when page changes
18
+ - `verbose?: boolean` (default: false) - Enable console logging for debugging
19
+ - `class?: string` - Additional CSS classes
20
+
21
+ ## Display Modes
22
+
23
+ The paginator automatically adapts based on screen width and page count:
24
+
25
+ 1. **Full Range** (few pages, wide screen)
26
+ - Shows all page buttons: `[1] [2] [3] [4] [5]`
27
+
28
+ 2. **Condensed Range** (many pages)
29
+ - Shows subset with ellipsis: `[1] ... [5] [6] [7] ... [20]`
30
+ - Centers around current page
31
+
32
+ 3. **Dropdown Mode** (narrow screen)
33
+ - Shows: `‹ [5 of 20 ▼] ›`
34
+ - Click dropdown to select page
35
+
36
+ 4. **With Fast Navigation** (many pages, wider screen)
37
+ - Shows: `« ‹ [5 of 20 ▼] › »`
38
+ - `«` - Jump back by 10/25/50 pages
39
+ - `»` - Jump forward by 10/25/50 pages
40
+
41
+ ## Usage Examples
42
+
43
+ ```svelte
44
+ <script>
45
+ import { Paginator } from 'intelliwaketssveltekitv25';
46
+
47
+ let currentPage = $state(1);
48
+ let totalPages = $state(10);
49
+ </script>
50
+
51
+ <!-- Basic usage -->
52
+ <Paginator
53
+ bind:page={currentPage}
54
+ pageCount={totalPages}
55
+ />
56
+
57
+ <!-- With URL query parameter sync -->
58
+ <Paginator
59
+ bind:page={currentPage}
60
+ pageCount={totalPages}
61
+ updateQueryParams
62
+ />
63
+ <!-- Updates ?page=5 in URL -->
64
+
65
+ <!-- Custom query parameter name -->
66
+ <Paginator
67
+ bind:page={currentPage}
68
+ pageCount={totalPages}
69
+ updateQueryParams="p"
70
+ />
71
+ <!-- Updates ?p=5 in URL -->
72
+
73
+ <!-- With SvelteKit invalidation -->
74
+ <Paginator
75
+ bind:page={currentPage}
76
+ pageCount={totalPages}
77
+ invalidate="app:data"
78
+ />
79
+
80
+ <!-- Multiple invalidation targets -->
81
+ <Paginator
82
+ bind:page={currentPage}
83
+ pageCount={totalPages}
84
+ invalidate={['app:products', 'app:stats']}
85
+ />
86
+
87
+ <!-- Invalidate all -->
88
+ <Paginator
89
+ bind:page={currentPage}
90
+ pageCount={totalPages}
91
+ invalidate="All"
92
+ />
93
+
94
+ <!-- With page change callback -->
95
+ <Paginator
96
+ bind:page={currentPage}
97
+ pageCount={totalPages}
98
+ onPageChange={(page) => {
99
+ console.log('Page changed to:', page);
100
+ fetchData(page);
101
+ }}
102
+ />
103
+
104
+ <!-- With custom styling -->
105
+ <Paginator
106
+ bind:page={currentPage}
107
+ pageCount={totalPages}
108
+ class="mt-4 border-t pt-4"
109
+ />
110
+
111
+ <!-- Enable debug logging -->
112
+ <Paginator
113
+ bind:page={currentPage}
114
+ pageCount={totalPages}
115
+ verbose
116
+ />
117
+ ```
118
+
119
+ ## Integration with SvelteKit
120
+
121
+ ### Basic Load Function Integration
122
+ ```svelte
123
+ <!-- +page.svelte -->
124
+ <script>
125
+ export let data;
126
+
127
+ let currentPage = $state(data.page);
128
+ </script>
129
+
130
+ <Paginator
131
+ bind:page={currentPage}
132
+ pageCount={data.totalPages}
133
+ updateQueryParams
134
+ invalidate="app:products"
135
+ />
136
+
137
+ <div>
138
+ {#each data.products as product}
139
+ <ProductCard {product} />
140
+ {/each}
141
+ </div>
142
+
143
+ <!-- +page.ts -->
144
+ export const load = async ({ depends, url }) => {
145
+ depends('app:products');
146
+
147
+ const page = parseInt(url.searchParams.get('page') || '1');
148
+ const pageSize = 20;
149
+
150
+ const { products, total } = await fetchProducts({ page, pageSize });
151
+
152
+ return {
153
+ products,
154
+ page,
155
+ totalPages: Math.ceil(total / pageSize)
156
+ };
157
+ };
158
+ ```
159
+
160
+ ### With Server-Side Pagination
161
+ ```svelte
162
+ <!-- +page.server.ts -->
163
+ export const load = async ({ depends, url }) => {
164
+ depends('app:items');
165
+
166
+ const page = parseInt(url.searchParams.get('page') || '1');
167
+ const limit = 25;
168
+ const offset = (page - 1) * limit;
169
+
170
+ const items = await db.items.findMany({
171
+ skip: offset,
172
+ take: limit
173
+ });
174
+
175
+ const total = await db.items.count();
176
+
177
+ return {
178
+ items,
179
+ page,
180
+ pageCount: Math.ceil(total / limit)
181
+ };
182
+ };
183
+ ```
184
+
185
+ ### Without Query Parameters (State Only)
186
+ ```svelte
187
+ <script>
188
+ let page = $state(1);
189
+ let pageSize = 20;
190
+
191
+ let data = $derived(allData.slice((page - 1) * pageSize, page * pageSize));
192
+ let pageCount = $derived(Math.ceil(allData.length / pageSize));
193
+ </script>
194
+
195
+ <Paginator
196
+ bind:page
197
+ {pageCount}
198
+ />
199
+
200
+ {#each data as item}
201
+ <div>{item.name}</div>
202
+ {/each}
203
+ ```
204
+
205
+ ## Responsive Behavior
206
+
207
+ ### Wide Screen (800px+)
208
+ ```
209
+ « ‹ [1] [2] [3] [4] [5] › »
210
+ ```
211
+
212
+ ### Medium Screen (500-800px)
213
+ ```
214
+ ‹ [1] ... [4] [5] [6] ... [20] ›
215
+ ```
216
+
217
+ ### Narrow Screen (<500px)
218
+ ```
219
+ ‹ [5 of 20 ▼] ›
220
+ ```
221
+
222
+ ## Common Patterns
223
+
224
+ ### Table Pagination
225
+ ```svelte
226
+ <ArrayTable {data} {columns} />
227
+
228
+ <Paginator
229
+ bind:page={currentPage}
230
+ pageCount={totalPages}
231
+ updateQueryParams
232
+ invalidate="app:tableData"
233
+ class="mt-4"
234
+ />
235
+ ```
236
+
237
+ ### Search Results
238
+ ```svelte
239
+ <Search bind:value={query} />
240
+
241
+ <SearchResults results={data.results} />
242
+
243
+ <Paginator
244
+ bind:page={currentPage}
245
+ pageCount={data.totalPages}
246
+ updateQueryParams="page"
247
+ invalidate="app:search"
248
+ />
249
+ ```
250
+
251
+ ### Infinite Scroll Alternative
252
+ ```svelte
253
+ <script>
254
+ let page = $state(1);
255
+
256
+ async function loadMore() {
257
+ page += 1;
258
+ // Paginator with invalidate will trigger data load
259
+ }
260
+ </script>
261
+
262
+ {#each data.items as item}
263
+ <Item {item} />
264
+ {/each}
265
+
266
+ <Paginator bind:page pageCount={totalPages} invalidate="app:items" />
267
+
268
+ <button onclick={loadMore}>Load More</button>
269
+ ```
270
+
271
+ ## Common Mistakes
272
+
273
+ - ❌ Using 0-indexed page numbers
274
+ ✅ Correct: Pages are 1-indexed (page 1, 2, 3...)
275
+
276
+ - ❌ Not using `bind:page` for two-way binding
277
+ ✅ Correct: `<Paginator bind:page={currentPage} />`
278
+
279
+ - ❌ Forgetting `depends()` in load function when using invalidate
280
+ ✅ Correct: Add `depends('app:identifier')` in load function
281
+
282
+ - ❌ Setting pageCount to 0
283
+ ✅ Correct: Always ensure pageCount ≥ 1, use conditional rendering if no data
284
+
285
+ - ❌ Using both `updateQueryParams` and manual `onPageChange` invalidation
286
+ ✅ Correct: Use one or the other, not both
287
+
288
+ ## Advanced Features
289
+
290
+ ### Fast Navigation
291
+ For large page counts (>15 pages), fast navigation buttons appear:
292
+ - `«` jumps back by calculated amount (10/25/50 depending on total pages)
293
+ - `»` jumps forward by same amount
294
+ - Auto-hides when not useful (near start/end)
295
+
296
+ ### Auto Page Bounds
297
+ - Page is automatically clamped to `1 ≤ page ≤ pageCount`
298
+ - Out-of-bounds pages are corrected on render
299
+
300
+ ### Query Parameter Behavior
301
+ When `updateQueryParams` is enabled:
302
+ - Page 1: Query parameter removed (clean URLs)
303
+ - Page 2+: Query parameter added
304
+ - Uses `replaceState` (no browser history clutter)
305
+
306
+ ## Props Reference
307
+
308
+ | Prop | Type | Default | Description |
309
+ |------|------|---------|-------------|
310
+ | `page` | `number` | (required) | Current page ($bindable) |
311
+ | `pageCount` | `number` | (required) | Total pages |
312
+ | `updateQueryParams` | `boolean \| string` | `false` | Query param name or true for 'page' |
313
+ | `invalidate` | `string \| string[] \| 'All'` | - | SvelteKit invalidation |
314
+ | `onPageChange` | `(page: number) => void` | - | Page change callback |
315
+ | `verbose` | `boolean` | `false` | Debug logging |
316
+ | `class` | `string` | `''` | Additional CSS classes |
317
+
318
+ ## Styling
319
+
320
+ Uses these classes:
321
+ - `pagination` - Container
322
+ - `btnClean` - Button base style
323
+ - `btnLink` - Dropdown button
324
+ - `active` - Active page button
325
+ - `invisible` - Hidden navigation buttons (vs `hidden` which removes from layout)
326
+
327
+ ## Performance
328
+
329
+ - Efficiently calculates visible page range
330
+ - Uses `$derived` for reactive page display
331
+ - Minimal re-renders on page change
332
+ - Responsive layout calculated from `clientWidth` measurement