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.
- package/README.md +2 -2
- package/dist/app.css +1 -1
- package/docs/DateRangePicker.md +272 -0
- package/docs/DisplayHTML.md +249 -0
- package/docs/DropDown.md +269 -0
- package/docs/Functions.md +796 -0
- package/docs/Home.md +109 -0
- package/docs/Icon.md +203 -0
- package/docs/Importer.md +328 -0
- package/docs/ImporterAnalysis.md +249 -0
- package/docs/ImporterLoad.md +288 -0
- package/docs/InputNumber.md +159 -0
- package/docs/Integration.md +215 -0
- package/docs/Modal.md +207 -0
- package/docs/MultiSelect.md +304 -0
- package/docs/Paginator.md +332 -0
- package/docs/Search.md +364 -0
- package/docs/SlideDown.md +358 -0
- package/docs/Svelte-5-Patterns.md +364 -0
- package/docs/Switch.md +107 -0
- package/docs/TabHeader.md +333 -0
- package/docs/TabHref.md +370 -0
- package/docs/TextArea.md +118 -0
- package/docs/_Sidebar.md +38 -0
- package/llms.txt +113 -0
- package/package.json +3 -2
- package/llm.txt +0 -1635
|
@@ -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
|