intelliwaketssveltekitv25 1.0.82 → 1.0.84
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/DropDownControl.svelte +1 -0
- 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 +87 -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 +8 -7
- package/llm.txt +0 -1635
package/docs/Search.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Search Component
|
|
2
|
+
|
|
3
|
+
**Purpose:** Debounced search input with session storage persistence
|
|
4
|
+
|
|
5
|
+
**When to Use:**
|
|
6
|
+
- Filter lists, tables, or search results
|
|
7
|
+
- Debounced input to reduce API calls
|
|
8
|
+
- Persist search value across page navigation
|
|
9
|
+
- Clean, consistent search UI
|
|
10
|
+
|
|
11
|
+
## Key Props
|
|
12
|
+
|
|
13
|
+
- `value?: string` ($bindable, default: '') - Search query
|
|
14
|
+
- `delayMS?: number` (default: 500) - Debounce delay in milliseconds
|
|
15
|
+
- `placeholder?: string` (default: 'Search') - Input placeholder text
|
|
16
|
+
- `onChange?: (val: string) => void` - Callback when value changes (after debounce)
|
|
17
|
+
- `sessionKey?: string` - Session storage key for persistence
|
|
18
|
+
- `bordered?: boolean` (default: false) - Show input border
|
|
19
|
+
- `noMagnifyingGlass?: boolean` - Hide search icon
|
|
20
|
+
- `element?: HTMLInputElement` - Bind to input element reference
|
|
21
|
+
- `use?: ActionArray` - Svelte actions to apply
|
|
22
|
+
- `hidden?: boolean` - Hide the component
|
|
23
|
+
- `id?: string` - Input element ID
|
|
24
|
+
- `class?: string` - Additional CSS classes
|
|
25
|
+
- ...HTMLInputAttributes - All standard input props
|
|
26
|
+
|
|
27
|
+
## Key Features
|
|
28
|
+
|
|
29
|
+
### Debouncing
|
|
30
|
+
- User types → Wait for `delayMS` → Trigger `onChange` and update `value`
|
|
31
|
+
- Press Enter → Immediate trigger (bypass debounce)
|
|
32
|
+
- Blur input → Immediate trigger
|
|
33
|
+
- Reduces API calls and improves performance
|
|
34
|
+
|
|
35
|
+
### Session Storage
|
|
36
|
+
When `sessionKey` is provided:
|
|
37
|
+
- Saves search value to `sessionStorage`
|
|
38
|
+
- Restores value on component mount
|
|
39
|
+
- Clears storage when value returns to original state
|
|
40
|
+
- Persists across page refreshes
|
|
41
|
+
|
|
42
|
+
### Auto-Select on Focus
|
|
43
|
+
Built-in `selectOnFocus` action automatically selects text when input is focused.
|
|
44
|
+
|
|
45
|
+
## Usage Examples
|
|
46
|
+
|
|
47
|
+
```svelte
|
|
48
|
+
<script>
|
|
49
|
+
import { Search } from 'intelliwaketssveltekitv25';
|
|
50
|
+
|
|
51
|
+
let searchQuery = $state('');
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<!-- Basic search -->
|
|
55
|
+
<Search bind:value={searchQuery} />
|
|
56
|
+
|
|
57
|
+
<!-- With callback -->
|
|
58
|
+
<Search
|
|
59
|
+
bind:value={searchQuery}
|
|
60
|
+
onChange={(val) => {
|
|
61
|
+
console.log('Search for:', val);
|
|
62
|
+
performSearch(val);
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<!-- Custom debounce delay -->
|
|
67
|
+
<Search
|
|
68
|
+
bind:value={searchQuery}
|
|
69
|
+
delayMS={300}
|
|
70
|
+
/>
|
|
71
|
+
<!-- Triggers 300ms after typing stops -->
|
|
72
|
+
|
|
73
|
+
<!-- Immediate (no debounce) -->
|
|
74
|
+
<Search
|
|
75
|
+
bind:value={searchQuery}
|
|
76
|
+
delayMS={0}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<!-- With session storage -->
|
|
80
|
+
<Search
|
|
81
|
+
bind:value={searchQuery}
|
|
82
|
+
sessionKey="productSearch"
|
|
83
|
+
/>
|
|
84
|
+
<!-- Value persists across page refreshes -->
|
|
85
|
+
|
|
86
|
+
<!-- Custom placeholder -->
|
|
87
|
+
<Search
|
|
88
|
+
bind:value={searchQuery}
|
|
89
|
+
placeholder="Search products..."
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<!-- With border -->
|
|
93
|
+
<Search
|
|
94
|
+
bind:value={searchQuery}
|
|
95
|
+
bordered
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<!-- Without magnifying glass icon -->
|
|
99
|
+
<Search
|
|
100
|
+
bind:value={searchQuery}
|
|
101
|
+
noMagnifyingGlass
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<!-- Custom styling -->
|
|
105
|
+
<Search
|
|
106
|
+
bind:value={searchQuery}
|
|
107
|
+
class="w-full md:w-96"
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<!-- Get input element reference -->
|
|
111
|
+
<script>
|
|
112
|
+
let inputElement: HTMLInputElement | undefined;
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<Search
|
|
116
|
+
bind:value={searchQuery}
|
|
117
|
+
bind:element={inputElement}
|
|
118
|
+
/>
|
|
119
|
+
<button onclick={() => inputElement?.focus()}>
|
|
120
|
+
Focus Search
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
<!-- With additional input attributes -->
|
|
124
|
+
<Search
|
|
125
|
+
bind:value={searchQuery}
|
|
126
|
+
maxlength={50}
|
|
127
|
+
autocomplete="off"
|
|
128
|
+
autofocus
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Integration Patterns
|
|
133
|
+
|
|
134
|
+
### Filter Table Data
|
|
135
|
+
```svelte
|
|
136
|
+
<script>
|
|
137
|
+
let searchQuery = $state('');
|
|
138
|
+
let allItems = $state([...]);
|
|
139
|
+
|
|
140
|
+
let filteredItems = $derived(
|
|
141
|
+
allItems.filter(item =>
|
|
142
|
+
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<Search bind:value={searchQuery} />
|
|
148
|
+
|
|
149
|
+
<ArrayTable data={filteredItems} {columns} />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### API Search with SvelteKit
|
|
153
|
+
```svelte
|
|
154
|
+
<script>
|
|
155
|
+
let searchQuery = $state('');
|
|
156
|
+
|
|
157
|
+
async function handleSearch(query: string) {
|
|
158
|
+
if (query.length >= 2) {
|
|
159
|
+
await goto(`/search?q=${encodeURIComponent(query)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<Search
|
|
165
|
+
bind:value={searchQuery}
|
|
166
|
+
onChange={handleSearch}
|
|
167
|
+
delayMS={500}
|
|
168
|
+
sessionKey="globalSearch"
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### With Loading Indicator
|
|
173
|
+
```svelte
|
|
174
|
+
<script>
|
|
175
|
+
let searchQuery = $state('');
|
|
176
|
+
let searching = $state(false);
|
|
177
|
+
|
|
178
|
+
async function performSearch(query: string) {
|
|
179
|
+
searching = true;
|
|
180
|
+
try {
|
|
181
|
+
const results = await api.search(query);
|
|
182
|
+
// ...
|
|
183
|
+
} finally {
|
|
184
|
+
searching = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<div class="relative">
|
|
190
|
+
<Search
|
|
191
|
+
bind:value={searchQuery}
|
|
192
|
+
onChange={performSearch}
|
|
193
|
+
/>
|
|
194
|
+
{#if searching}
|
|
195
|
+
<div class="absolute right-2 top-1/2 -translate-y-1/2">
|
|
196
|
+
<Icon icon={faSpinner} spin />
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Multi-Field Search
|
|
203
|
+
```svelte
|
|
204
|
+
<script>
|
|
205
|
+
let nameSearch = $state('');
|
|
206
|
+
let emailSearch = $state('');
|
|
207
|
+
|
|
208
|
+
let filtered = $derived(
|
|
209
|
+
users.filter(user =>
|
|
210
|
+
user.name.includes(nameSearch) &&
|
|
211
|
+
user.email.includes(emailSearch)
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
</script>
|
|
215
|
+
|
|
216
|
+
<div class="grid grid-cols-2 gap-4">
|
|
217
|
+
<Search bind:value={nameSearch} placeholder="Search by name..." />
|
|
218
|
+
<Search bind:value={emailSearch} placeholder="Search by email..." />
|
|
219
|
+
</div>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### With Clear Button
|
|
223
|
+
```svelte
|
|
224
|
+
<script>
|
|
225
|
+
let searchQuery = $state('');
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<div class="flex gap-2">
|
|
229
|
+
<Search bind:value={searchQuery} class="flex-1" />
|
|
230
|
+
{#if searchQuery}
|
|
231
|
+
<button onclick={() => searchQuery = ''}>
|
|
232
|
+
Clear
|
|
233
|
+
</button>
|
|
234
|
+
{/if}
|
|
235
|
+
</div>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Keyboard Shortcuts
|
|
239
|
+
|
|
240
|
+
- **Enter** - Immediately trigger search (bypass debounce)
|
|
241
|
+
- **Escape** - (Native) Clear input
|
|
242
|
+
- **Focus** - Automatically select all text
|
|
243
|
+
|
|
244
|
+
## Session Storage Behavior
|
|
245
|
+
|
|
246
|
+
When `sessionKey="mySearch"`:
|
|
247
|
+
|
|
248
|
+
1. **On mount:** Restore value from `sessionStorage.getItem('mySearch')`
|
|
249
|
+
2. **On change:** Save to `sessionStorage.setItem('mySearch', value)`
|
|
250
|
+
3. **On clear:** Remove from `sessionStorage.removeItem('mySearch')`
|
|
251
|
+
|
|
252
|
+
Use cases:
|
|
253
|
+
- Preserve search across tab navigation
|
|
254
|
+
- Remember last search on page refresh
|
|
255
|
+
- Share search state between components
|
|
256
|
+
|
|
257
|
+
```svelte
|
|
258
|
+
<!-- Page 1 -->
|
|
259
|
+
<Search
|
|
260
|
+
bind:value={query1}
|
|
261
|
+
sessionKey="mainSearch"
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
<!-- Page 2 (preserves the same search) -->
|
|
265
|
+
<Search
|
|
266
|
+
bind:value={query2}
|
|
267
|
+
sessionKey="mainSearch"
|
|
268
|
+
/>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Common Patterns
|
|
272
|
+
|
|
273
|
+
### Master-Detail Layout Search
|
|
274
|
+
```svelte
|
|
275
|
+
<MasterDetailLayout>
|
|
276
|
+
{#snippet list()}
|
|
277
|
+
<Search bind:value={filterQuery} sessionKey="itemFilter" />
|
|
278
|
+
{#each filteredItems as item}
|
|
279
|
+
<ItemCard {item} />
|
|
280
|
+
{/each}
|
|
281
|
+
{/snippet}
|
|
282
|
+
</MasterDetailLayout>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Real-Time Filter
|
|
286
|
+
```svelte
|
|
287
|
+
<script>
|
|
288
|
+
let products = $state([...]);
|
|
289
|
+
let filter = $state('');
|
|
290
|
+
|
|
291
|
+
// Updates immediately as user types (after debounce)
|
|
292
|
+
let visible = $derived(
|
|
293
|
+
products.filter(p =>
|
|
294
|
+
p.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
295
|
+
p.sku.includes(filter)
|
|
296
|
+
)
|
|
297
|
+
);
|
|
298
|
+
</script>
|
|
299
|
+
|
|
300
|
+
<Search bind:value={filter} delayMS={300} />
|
|
301
|
+
|
|
302
|
+
<div class="text-sm text-gray-600">
|
|
303
|
+
Showing {visible.length} of {products.length} products
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{#each visible as product}
|
|
307
|
+
<ProductCard {product} />
|
|
308
|
+
{/each}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Common Mistakes
|
|
312
|
+
|
|
313
|
+
- ❌ Not using `bind:value` for two-way binding
|
|
314
|
+
✅ Correct: `<Search bind:value={searchQuery} />`
|
|
315
|
+
|
|
316
|
+
- ❌ Using very short debounce delays (< 200ms)
|
|
317
|
+
✅ Correct: Use 300-500ms for best UX and performance
|
|
318
|
+
|
|
319
|
+
- ❌ Triggering expensive operations in `onChange` without checks
|
|
320
|
+
✅ Correct: Check minimum query length: `if (query.length >= 2)`
|
|
321
|
+
|
|
322
|
+
- ❌ Not handling empty string in filter logic
|
|
323
|
+
✅ Correct: `if (!query) return allItems;`
|
|
324
|
+
|
|
325
|
+
- ❌ Using same `sessionKey` for different search contexts
|
|
326
|
+
✅ Correct: Use unique keys: `sessionKey="userSearch"`, `sessionKey="productSearch"`
|
|
327
|
+
|
|
328
|
+
## Related Components
|
|
329
|
+
|
|
330
|
+
- **MultiSelect** - Has built-in search functionality
|
|
331
|
+
- **ArrayTable** - Often paired with Search for filtering
|
|
332
|
+
- **MasterDetailLayout** - Common container for search + results
|
|
333
|
+
|
|
334
|
+
## Props Reference
|
|
335
|
+
|
|
336
|
+
| Prop | Type | Default | Description |
|
|
337
|
+
|------|------|---------|-------------|
|
|
338
|
+
| `value` | `string` | `''` | Search query ($bindable) |
|
|
339
|
+
| `delayMS` | `number` | `500` | Debounce delay (ms) |
|
|
340
|
+
| `placeholder` | `string` | `'Search'` | Input placeholder |
|
|
341
|
+
| `onChange` | `(val: string) => void` | - | Debounced callback |
|
|
342
|
+
| `sessionKey` | `string` | - | Session storage key |
|
|
343
|
+
| `bordered` | `boolean` | `false` | Show border |
|
|
344
|
+
| `noMagnifyingGlass` | `boolean` | `false` | Hide search icon |
|
|
345
|
+
| `element` | `HTMLInputElement` | - | Input element ref |
|
|
346
|
+
| `use` | `ActionArray` | `[]` | Svelte actions |
|
|
347
|
+
| `hidden` | `boolean` | `false` | Hide component |
|
|
348
|
+
| `class` | `string` | `''` | CSS classes |
|
|
349
|
+
| `id` | `string` | - | Input ID |
|
|
350
|
+
|
|
351
|
+
## Styling
|
|
352
|
+
|
|
353
|
+
- Uses `inputSearch` wrapper class
|
|
354
|
+
- Icon: Absolute positioned left with `text-slate-300`
|
|
355
|
+
- Input: Standard input styling with `ps-6` (left padding) when icon visible
|
|
356
|
+
- Border: Transparent by default, visible when `bordered={true}`
|
|
357
|
+
- Print: Hidden if value is empty (`forcePrintHidden`)
|
|
358
|
+
|
|
359
|
+
## Performance Tips
|
|
360
|
+
|
|
361
|
+
- Use appropriate `delayMS` (500ms recommended)
|
|
362
|
+
- Implement minimum query length checks in `onChange`
|
|
363
|
+
- Use `$derived` for reactive filtering (efficient)
|
|
364
|
+
- Consider virtualization for large result sets
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# SlideDown Component
|
|
2
|
+
|
|
3
|
+
**Purpose:** Animated dropdown menu with keyboard navigation and flexible content
|
|
4
|
+
|
|
5
|
+
**When to Use:**
|
|
6
|
+
- Dropdown menus without form input styling
|
|
7
|
+
- Standalone dropdown menus
|
|
8
|
+
- Custom menu implementations
|
|
9
|
+
- Menus with mixed content (actions + custom snippets)
|
|
10
|
+
|
|
11
|
+
## Key Props
|
|
12
|
+
|
|
13
|
+
- `ddActions: IDDAction[]` (required) - Menu items array
|
|
14
|
+
- `show?: boolean` ($bindable, default: false) - Control open/closed state
|
|
15
|
+
- `button?: Snippet` - Custom button content
|
|
16
|
+
- `actions?: Snippet` - Custom menu content (in addition to ddActions)
|
|
17
|
+
- `buttonTitle?: string | null` - Button text (if not using button snippet)
|
|
18
|
+
- `buttonClass?: string` (default: '') - CSS classes for button
|
|
19
|
+
- `width?: string` (default: 'auto') - Menu width
|
|
20
|
+
- `maxHeight?: string | null` (default: '60vh') - Max height for scrolling
|
|
21
|
+
- `caret?: boolean` (default: false) - Show dropdown caret icon
|
|
22
|
+
- `use?: ActionArray` - Svelte actions to apply
|
|
23
|
+
- `highlightedIndex?: number` (default: -1) - Keyboard navigation index
|
|
24
|
+
|
|
25
|
+
## IDDAction Interface
|
|
26
|
+
|
|
27
|
+
Same as **DropDown** component. See [DropDown.md](DropDown.md#iddaction-interface) for full details.
|
|
28
|
+
|
|
29
|
+
## Key Differences from DropDown
|
|
30
|
+
|
|
31
|
+
| Feature | SlideDown | DropDown |
|
|
32
|
+
|---------|-----------|----------|
|
|
33
|
+
| Styling | Clean, minimal | Input control styling option |
|
|
34
|
+
| Animation | Slide + fade | Slide only |
|
|
35
|
+
| Position | Centered, auto | Configurable position |
|
|
36
|
+
| Use Case | Menus, navigation | Form controls, selects |
|
|
37
|
+
| Complexity | Simpler | More features |
|
|
38
|
+
|
|
39
|
+
## Keyboard Navigation
|
|
40
|
+
|
|
41
|
+
- **Arrow Down** - Move to next item / Open menu
|
|
42
|
+
- **Arrow Up** - Move to previous item
|
|
43
|
+
- **Enter** - Execute highlighted action
|
|
44
|
+
- **Escape** - Close menu
|
|
45
|
+
- **Click outside** - Close menu
|
|
46
|
+
|
|
47
|
+
## Usage Examples
|
|
48
|
+
|
|
49
|
+
```svelte
|
|
50
|
+
<script>
|
|
51
|
+
import { SlideDown } from 'intelliwaketssveltekitv25';
|
|
52
|
+
import { faUser, faCog, faSignOut } from '@fortawesome/free-solid-svg-icons';
|
|
53
|
+
|
|
54
|
+
let showMenu = $state(false);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<!-- Basic menu -->
|
|
58
|
+
<SlideDown
|
|
59
|
+
bind:show={showMenu}
|
|
60
|
+
buttonTitle="Menu"
|
|
61
|
+
ddActions={[
|
|
62
|
+
{
|
|
63
|
+
title: 'Profile',
|
|
64
|
+
faProps: { icon: faUser },
|
|
65
|
+
action: () => goto('/profile')
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: 'Settings',
|
|
69
|
+
faProps: { icon: faCog },
|
|
70
|
+
action: () => goto('/settings')
|
|
71
|
+
},
|
|
72
|
+
{ divider: true },
|
|
73
|
+
{
|
|
74
|
+
title: 'Logout',
|
|
75
|
+
faProps: { icon: faSignOut },
|
|
76
|
+
action: () => logout()
|
|
77
|
+
}
|
|
78
|
+
]}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
<!-- With caret icon -->
|
|
82
|
+
<SlideDown
|
|
83
|
+
bind:show={showMenu}
|
|
84
|
+
buttonTitle="Options"
|
|
85
|
+
ddActions={actions}
|
|
86
|
+
caret
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<!-- Custom button content -->
|
|
90
|
+
<SlideDown
|
|
91
|
+
bind:show={showMenu}
|
|
92
|
+
ddActions={actions}
|
|
93
|
+
>
|
|
94
|
+
{#snippet button()}
|
|
95
|
+
<div class="flex items-center gap-2">
|
|
96
|
+
<Avatar src={user.avatar} />
|
|
97
|
+
<span>{user.name}</span>
|
|
98
|
+
</div>
|
|
99
|
+
{/snippet}
|
|
100
|
+
</SlideDown>
|
|
101
|
+
|
|
102
|
+
<!-- Custom menu content -->
|
|
103
|
+
<SlideDown
|
|
104
|
+
bind:show={showMenu}
|
|
105
|
+
ddActions={[]}
|
|
106
|
+
>
|
|
107
|
+
{#snippet actions()}
|
|
108
|
+
<div class="p-4">
|
|
109
|
+
<h3>Custom Content</h3>
|
|
110
|
+
<p>Any content here</p>
|
|
111
|
+
</div>
|
|
112
|
+
{/snippet}
|
|
113
|
+
</SlideDown>
|
|
114
|
+
|
|
115
|
+
<!-- Mixed custom + actions -->
|
|
116
|
+
<SlideDown
|
|
117
|
+
bind:show={showMenu}
|
|
118
|
+
ddActions={standardActions}
|
|
119
|
+
>
|
|
120
|
+
{#snippet actions()}
|
|
121
|
+
<div class="p-2 bg-gray-50">
|
|
122
|
+
<p class="text-xs">Recent Activity:</p>
|
|
123
|
+
{#each recentItems as item}
|
|
124
|
+
<div>{item.name}</div>
|
|
125
|
+
{/each}
|
|
126
|
+
</div>
|
|
127
|
+
{/snippet}
|
|
128
|
+
</SlideDown>
|
|
129
|
+
|
|
130
|
+
<!-- Fixed width -->
|
|
131
|
+
<SlideDown
|
|
132
|
+
bind:show={showMenu}
|
|
133
|
+
buttonTitle="Select"
|
|
134
|
+
ddActions={options}
|
|
135
|
+
width="300px"
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<!-- No max height (full scrollable) -->
|
|
139
|
+
<SlideDown
|
|
140
|
+
bind:show={showMenu}
|
|
141
|
+
buttonTitle="All Items"
|
|
142
|
+
ddActions={manyItems}
|
|
143
|
+
maxHeight={null}
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
<!-- With custom button styling -->
|
|
147
|
+
<SlideDown
|
|
148
|
+
bind:show={showMenu}
|
|
149
|
+
buttonTitle="Actions"
|
|
150
|
+
buttonClass="text-blue-600 font-bold hover:text-blue-800"
|
|
151
|
+
ddActions={actions}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Common Patterns
|
|
156
|
+
|
|
157
|
+
### User Menu
|
|
158
|
+
```svelte
|
|
159
|
+
<script>
|
|
160
|
+
let userMenuOpen = $state(false);
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<SlideDown
|
|
164
|
+
bind:show={userMenuOpen}
|
|
165
|
+
ddActions={[
|
|
166
|
+
{
|
|
167
|
+
title: user.name,
|
|
168
|
+
header: true
|
|
169
|
+
},
|
|
170
|
+
{ divider: true },
|
|
171
|
+
{
|
|
172
|
+
title: 'Profile',
|
|
173
|
+
faProps: { icon: faUser },
|
|
174
|
+
href: '/profile'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: 'Settings',
|
|
178
|
+
faProps: { icon: faCog },
|
|
179
|
+
href: '/settings'
|
|
180
|
+
},
|
|
181
|
+
{ divider: true },
|
|
182
|
+
{
|
|
183
|
+
title: 'Logout',
|
|
184
|
+
faProps: { icon: faSignOut },
|
|
185
|
+
action: () => handleLogout()
|
|
186
|
+
}
|
|
187
|
+
]}
|
|
188
|
+
>
|
|
189
|
+
{#snippet button()}
|
|
190
|
+
<Avatar src={user.avatar} size="sm" />
|
|
191
|
+
{/snippet}
|
|
192
|
+
</SlideDown>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Context Menu
|
|
196
|
+
```svelte
|
|
197
|
+
<script>
|
|
198
|
+
let contextMenu = $state(false);
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<div class="relative">
|
|
202
|
+
<button onclick={() => contextMenu = !contextMenu}>
|
|
203
|
+
<Icon icon={faEllipsisV} />
|
|
204
|
+
</button>
|
|
205
|
+
|
|
206
|
+
<SlideDown
|
|
207
|
+
bind:show={contextMenu}
|
|
208
|
+
ddActions={[
|
|
209
|
+
{ title: 'Edit', faProps: { icon: faEdit }, action: () => edit() },
|
|
210
|
+
{ title: 'Duplicate', faProps: { icon: faCopy }, action: () => duplicate() },
|
|
211
|
+
{ divider: true },
|
|
212
|
+
{ title: 'Delete', faProps: { icon: faTrash }, action: () => remove() }
|
|
213
|
+
]}
|
|
214
|
+
>
|
|
215
|
+
{#snippet button()}
|
|
216
|
+
<!-- Empty, controlled by external button -->
|
|
217
|
+
{/snippet}
|
|
218
|
+
</SlideDown>
|
|
219
|
+
</div>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Grouped Menu
|
|
223
|
+
```svelte
|
|
224
|
+
<SlideDown
|
|
225
|
+
buttonTitle="Tools"
|
|
226
|
+
ddActions={[
|
|
227
|
+
{ title: 'File Tools', header: true },
|
|
228
|
+
{ title: 'New File', action: () => {} },
|
|
229
|
+
{ title: 'Open File', action: () => {} },
|
|
230
|
+
{ title: 'Save File', action: () => {} },
|
|
231
|
+
{ divider: true },
|
|
232
|
+
{ title: 'Edit Tools', header: true },
|
|
233
|
+
{ title: 'Cut', action: () => {} },
|
|
234
|
+
{ title: 'Copy', action: () => {} },
|
|
235
|
+
{ title: 'Paste', action: () => {} }
|
|
236
|
+
]}
|
|
237
|
+
/>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### With Active State
|
|
241
|
+
```svelte
|
|
242
|
+
<script>
|
|
243
|
+
let selectedOption = $state('option1');
|
|
244
|
+
</script>
|
|
245
|
+
|
|
246
|
+
<SlideDown
|
|
247
|
+
buttonTitle={options.find(o => o.id === selectedOption)?.title}
|
|
248
|
+
ddActions={options.map(opt => ({
|
|
249
|
+
title: opt.title,
|
|
250
|
+
active: opt.id === selectedOption,
|
|
251
|
+
action: () => selectedOption = opt.id
|
|
252
|
+
}))}
|
|
253
|
+
/>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Navigation Dropdown
|
|
257
|
+
```svelte
|
|
258
|
+
<SlideDown
|
|
259
|
+
buttonTitle="Products"
|
|
260
|
+
ddActions={[
|
|
261
|
+
{ title: 'All Products', href: '/products' },
|
|
262
|
+
{ title: 'Electronics', href: '/products/electronics' },
|
|
263
|
+
{ title: 'Clothing', href: '/products/clothing' },
|
|
264
|
+
{ title: 'Food', href: '/products/food' }
|
|
265
|
+
]}
|
|
266
|
+
/>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Advanced Features
|
|
270
|
+
|
|
271
|
+
### Auto-Scroll to Active Item
|
|
272
|
+
When menu opens, automatically scrolls to the active item.
|
|
273
|
+
|
|
274
|
+
### Indentation Support
|
|
275
|
+
```svelte
|
|
276
|
+
<SlideDown
|
|
277
|
+
buttonTitle="Nested Menu"
|
|
278
|
+
ddActions={[
|
|
279
|
+
{ title: 'Parent Item', action: () => {} },
|
|
280
|
+
{ title: 'Child Item 1', indented: true, action: () => {} },
|
|
281
|
+
{ title: 'Child Item 2', indented: true, action: () => {} },
|
|
282
|
+
{ title: 'Another Parent', action: () => {} }
|
|
283
|
+
]}
|
|
284
|
+
/>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Auto Headers/Dividers
|
|
288
|
+
```svelte
|
|
289
|
+
<!-- Auto-adds headers when headerGroup changes -->
|
|
290
|
+
<SlideDown
|
|
291
|
+
buttonTitle="Grouped"
|
|
292
|
+
ddActions={[
|
|
293
|
+
{ title: 'Item A1', headerGroup: 'Group A', action: () => {} },
|
|
294
|
+
{ title: 'Item A2', headerGroup: 'Group A', action: () => {} },
|
|
295
|
+
{ title: 'Item B1', headerGroup: 'Group B', action: () => {} },
|
|
296
|
+
{ title: 'Item B2', headerGroup: 'Group B', action: () => {} }
|
|
297
|
+
]}
|
|
298
|
+
/>
|
|
299
|
+
<!-- Renders: Group A (header), Item A1, Item A2, Group B (header), Item B1, Item B2 -->
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Common Mistakes
|
|
303
|
+
|
|
304
|
+
- ❌ Not providing button content or buttonTitle
|
|
305
|
+
✅ Correct: Provide buttonTitle or button snippet
|
|
306
|
+
|
|
307
|
+
- ❌ Using SlideDown when DropDown is more appropriate
|
|
308
|
+
✅ Correct: Use DropDown for form inputs, SlideDown for menus
|
|
309
|
+
|
|
310
|
+
- ❌ Forgetting to bind `show` for controlled state
|
|
311
|
+
✅ Correct: `bind:show={isOpen}` to control externally
|
|
312
|
+
|
|
313
|
+
- ❌ Not providing action or href for menu items
|
|
314
|
+
✅ Correct: Every item needs action, href, or be a divider/header
|
|
315
|
+
|
|
316
|
+
- ❌ Very long menus without maxHeight
|
|
317
|
+
✅ Correct: Keep default maxHeight or set reasonable limit
|
|
318
|
+
|
|
319
|
+
## Related Components
|
|
320
|
+
|
|
321
|
+
- **DropDown** - More feature-rich dropdown with positioning
|
|
322
|
+
- **DropDownControl** - Lower-level dropdown primitive
|
|
323
|
+
- **Icon** - Used for menu item icons
|
|
324
|
+
- **IDDAction** - Shared action item interface
|
|
325
|
+
|
|
326
|
+
## Props Reference
|
|
327
|
+
|
|
328
|
+
| Prop | Type | Default | Description |
|
|
329
|
+
|------|------|---------|-------------|
|
|
330
|
+
| `ddActions` | `IDDAction[]` | (required) | Menu items |
|
|
331
|
+
| `show` | `boolean` | `false` | Open state ($bindable) |
|
|
332
|
+
| `button` | `Snippet` | - | Custom button |
|
|
333
|
+
| `actions` | `Snippet` | - | Custom menu content |
|
|
334
|
+
| `buttonTitle` | `string \| null` | `null` | Button text |
|
|
335
|
+
| `buttonClass` | `string` | `''` | Button CSS classes |
|
|
336
|
+
| `width` | `string` | `'auto'` | Menu width |
|
|
337
|
+
| `maxHeight` | `string \| null` | `'60vh'` | Max menu height |
|
|
338
|
+
| `caret` | `boolean` | `false` | Show caret icon |
|
|
339
|
+
| `use` | `ActionArray` | `[]` | Svelte actions |
|
|
340
|
+
| `highlightedIndex` | `number` | `-1` | Keyboard nav index |
|
|
341
|
+
|
|
342
|
+
## Styling
|
|
343
|
+
|
|
344
|
+
- Clean button styling (no default borders)
|
|
345
|
+
- White background on open (`bg-white`, `dark:bg-slate-700`)
|
|
346
|
+
- Shadow on open (`shadow-lg`)
|
|
347
|
+
- Slide animation (200ms cubic-in-out)
|
|
348
|
+
- Hover: `hover:bg-slate-100` for items
|
|
349
|
+
- Active: `bg-primary-main text-white`
|
|
350
|
+
- Disabled: `text-slate-300`
|
|
351
|
+
|
|
352
|
+
## Accessibility
|
|
353
|
+
|
|
354
|
+
- Keyboard navigation fully supported
|
|
355
|
+
- `role="button"` and `role="menuitem"` attributes
|
|
356
|
+
- `aria-expanded` on container
|
|
357
|
+
- `aria-haspopup="true"` on button
|
|
358
|
+
- Tab index managed for proper focus flow
|