@styloviz/list-view 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StyloViz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @styloviz/list-view
2
+
3
+ > Market-ready list patterns: default, checklist, nested tree, virtualized, grouped, template cards, call history and infinite scrolling.
4
+
5
+ Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @styloviz/core @styloviz/list-view
11
+ ```
12
+
13
+ Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { SvListViewComponent } from '@styloviz/list-view';
19
+
20
+ @Component({
21
+ standalone: true,
22
+ imports: [SvListViewComponent],
23
+ template: `
24
+ <sv-list-view [items]="items" />
25
+ `,
26
+ })
27
+ export class DemoComponent {}
28
+ ```
29
+
30
+ ## Inputs
31
+
32
+ | Input | Type | Default | Description |
33
+ | --- | --- | --- | --- |
34
+ | `items` | `ListViewItem[]` | `[]` | Items to render (label, meta, optional children/avatar/actions). |
35
+ | `mode` | `ListViewMode` | `'default'` | Preset pattern: default, checklist, nested, card, etc. |
36
+ | `variant` | `ListViewVariant` | `'default'` | Visual variant. |
37
+ | `density` | `ListViewDensity` | `'comfortable'` | Row density. |
38
+ | `selectionMode` | `ListViewSelectionMode` | `'none'` | Selection behaviour: none, single or multiple. |
39
+ | `checkable` | `boolean` | `false` | Show checkboxes on rows. |
40
+ | `nested` | `boolean` | `false` | Enable nested (tree) rows. |
41
+ | `virtualized` | `boolean` | `false` | Virtualize long lists for performance. |
42
+ | `grouped` | `boolean` | `false` | Group rows under section headers. |
43
+ | `searchable` | `boolean` | `false` | Show a search box to filter items. |
44
+ | `stickyGroupHeaders` | `boolean` | `true` | Keep group headers pinned while scrolling. |
45
+ | `infiniteScroll` | `boolean` | `false` | Load more items as the user scrolls to the end. |
46
+ | `hasMore` | `boolean` | `false` | Whether more items are available to load. |
47
+ | `loadingMore` | `boolean` | `false` | Show a loading indicator while fetching more. |
48
+ | `viewportHeight` | `number` | `360` | Viewport height (px) for the virtualized list. |
49
+ | `itemHeight` | `number` | `68` | Estimated row height (px) for virtualization. |
50
+ | `overscan` | `number` | `5` | Extra rows rendered above/below the viewport. |
51
+ | `searchPlaceholder` | `string` | `'Search list items'` | Placeholder for the search box. |
52
+ | `emptyMessage` | `string` | `'No items match your filters.'` | Message shown when no items match. |
53
+ | `customClass` | `string` | `''` | Extra CSS classes for the root. |
54
+ | `ariaLabel` | `string` | `'List view'` | Accessible label for the list. |
55
+ | `announceActions` | `boolean` | `true` | Announce row actions to screen readers. |
56
+ | `showQuickActions` | `boolean` | `false` | Show per-row quick actions on hover/focus. |
57
+ | `quickActions` | `ListViewQuickAction[]` | `[]` | Per-row quick actions. |
58
+ | `bulkActions` | `ListViewBulkAction[]` | `[]` | Bulk actions for checked rows. |
59
+ | `enableBulkHotkeys` | `boolean` | `false` | Enable keyboard hotkeys for bulk actions. |
60
+ | `enableRovingFocus` | `boolean` | `true` | Enable roving-tabindex keyboard navigation. |
61
+
62
+ ## Outputs
63
+
64
+ | Output | Type | Description |
65
+ | --- | --- | --- |
66
+ | `itemClick` | `ListViewItemClickEvent` | Emitted with the clicked item. |
67
+ | `checkChange` | `ListViewCheckChangeEvent` | Emitted when a row's checkbox changes. |
68
+ | `loadMore` | `ListViewLoadMoreEvent` | Emitted when more items should be loaded (infinite scroll). |
69
+ | `quickActionClick` | `ListViewQuickActionClickEvent` | Emitted when a quick action is clicked. |
70
+ | `bulkActionClick` | `ListViewBulkActionClickEvent` | Emitted when a bulk action is triggered. |
71
+
72
+ ## Documentation
73
+
74
+ Full API reference and live demos: https://styloviz.dev/docs/list-view
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,471 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, model, output, signal, inject, ElementRef, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgClass } from '@angular/common';
4
+
5
+ class SvListViewComponent {
6
+ /** Items to render (label, meta, optional children/avatar/actions). */
7
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
8
+ /** Preset pattern: default, checklist, nested, card, etc. @default 'default' */
9
+ mode = input('default', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
10
+ /** Visual variant. @default 'default' */
11
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
12
+ /** Row density. @default 'comfortable' */
13
+ density = input('comfortable', ...(ngDevMode ? [{ debugName: "density" }] : /* istanbul ignore next */ []));
14
+ /** Selection behaviour: none, single or multiple. @default 'none' */
15
+ selectionMode = input('none', ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
16
+ /** Show checkboxes on rows. @default false */
17
+ checkable = input(false, ...(ngDevMode ? [{ debugName: "checkable" }] : /* istanbul ignore next */ []));
18
+ /** Enable nested (tree) rows. @default false */
19
+ nested = input(false, ...(ngDevMode ? [{ debugName: "nested" }] : /* istanbul ignore next */ []));
20
+ /** Virtualize long lists for performance. @default false */
21
+ virtualized = input(false, ...(ngDevMode ? [{ debugName: "virtualized" }] : /* istanbul ignore next */ []));
22
+ /** Group rows under section headers. @default false */
23
+ grouped = input(false, ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
24
+ /** Show a search box to filter items. @default false */
25
+ searchable = input(false, ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
26
+ /** Keep group headers pinned while scrolling. @default true */
27
+ stickyGroupHeaders = input(true, ...(ngDevMode ? [{ debugName: "stickyGroupHeaders" }] : /* istanbul ignore next */ []));
28
+ /** Load more items as the user scrolls to the end. @default false */
29
+ infiniteScroll = input(false, ...(ngDevMode ? [{ debugName: "infiniteScroll" }] : /* istanbul ignore next */ []));
30
+ /** Whether more items are available to load. @default false */
31
+ hasMore = input(false, ...(ngDevMode ? [{ debugName: "hasMore" }] : /* istanbul ignore next */ []));
32
+ /** Show a loading indicator while fetching more. @default false */
33
+ loadingMore = input(false, ...(ngDevMode ? [{ debugName: "loadingMore" }] : /* istanbul ignore next */ []));
34
+ /** Viewport height (px) for the virtualized list. @default 360 */
35
+ viewportHeight = input(360, ...(ngDevMode ? [{ debugName: "viewportHeight" }] : /* istanbul ignore next */ []));
36
+ /** Estimated row height (px) for virtualization. @default 68 */
37
+ itemHeight = input(68, ...(ngDevMode ? [{ debugName: "itemHeight" }] : /* istanbul ignore next */ []));
38
+ /** Extra rows rendered above/below the viewport. @default 5 */
39
+ overscan = input(5, ...(ngDevMode ? [{ debugName: "overscan" }] : /* istanbul ignore next */ []));
40
+ /** Placeholder for the search box. @default 'Search list items' */
41
+ searchPlaceholder = input('Search list items', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
42
+ /** Message shown when no items match. @default 'No items match your filters.' */
43
+ emptyMessage = input('No items match your filters.', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
44
+ /** Extra CSS classes for the root. */
45
+ customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
46
+ /** Accessible label for the list. @default 'List view' */
47
+ ariaLabel = input('List view', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
48
+ /** Announce row actions to screen readers. @default true */
49
+ announceActions = input(true, ...(ngDevMode ? [{ debugName: "announceActions" }] : /* istanbul ignore next */ []));
50
+ /** Show per-row quick actions on hover/focus. @default false */
51
+ showQuickActions = input(false, ...(ngDevMode ? [{ debugName: "showQuickActions" }] : /* istanbul ignore next */ []));
52
+ /** Per-row quick actions. */
53
+ quickActions = input([], ...(ngDevMode ? [{ debugName: "quickActions" }] : /* istanbul ignore next */ []));
54
+ /** Bulk actions for checked rows. */
55
+ bulkActions = input([], ...(ngDevMode ? [{ debugName: "bulkActions" }] : /* istanbul ignore next */ []));
56
+ /** Enable keyboard hotkeys for bulk actions. @default false */
57
+ enableBulkHotkeys = input(false, ...(ngDevMode ? [{ debugName: "enableBulkHotkeys" }] : /* istanbul ignore next */ []));
58
+ /** Map of hotkey → bulk action id. */
59
+ bulkHotkeys = input({}, ...(ngDevMode ? [{ debugName: "bulkHotkeys" }] : /* istanbul ignore next */ []));
60
+ /** Enable roving-tabindex keyboard navigation. @default true */
61
+ enableRovingFocus = input(true, ...(ngDevMode ? [{ debugName: "enableRovingFocus" }] : /* istanbul ignore next */ []));
62
+ /** Selected row ids (two-way). */
63
+ selectedIds = model(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
64
+ /** Checked row ids (two-way). */
65
+ checkedIds = model(new Set(), ...(ngDevMode ? [{ debugName: "checkedIds" }] : /* istanbul ignore next */ []));
66
+ /** Emitted with the clicked item. */
67
+ itemClick = output();
68
+ /** Emitted with the current selection set. */
69
+ selectionChange = output();
70
+ /** Emitted when a row's checkbox changes. */
71
+ checkChange = output();
72
+ /** Emitted when more items should be loaded (infinite scroll). */
73
+ loadMore = output();
74
+ /** Emitted when a quick action is clicked. */
75
+ quickActionClick = output();
76
+ /** Emitted when a bulk action is triggered. */
77
+ bulkActionClick = output();
78
+ query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
79
+ liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : /* istanbul ignore next */ []));
80
+ host = inject((ElementRef));
81
+ scrollTop = signal(0, ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
82
+ expandedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedIds" }] : /* istanbul ignore next */ []));
83
+ loadingGuard = signal(false, ...(ngDevMode ? [{ debugName: "loadingGuard" }] : /* istanbul ignore next */ []));
84
+ isCheckable = computed(() => this.checkable() || this.mode() === 'checklist', ...(ngDevMode ? [{ debugName: "isCheckable" }] : /* istanbul ignore next */ []));
85
+ isSelectable = computed(() => this.selectionMode() !== 'none', ...(ngDevMode ? [{ debugName: "isSelectable" }] : /* istanbul ignore next */ []));
86
+ selectedCount = computed(() => this.selectedIds().size, ...(ngDevMode ? [{ debugName: "selectedCount" }] : /* istanbul ignore next */ []));
87
+ bulkSelectedIds = computed(() => (this.isCheckable() ? this.checkedIds() : this.selectedIds()), ...(ngDevMode ? [{ debugName: "bulkSelectedIds" }] : /* istanbul ignore next */ []));
88
+ bulkSelectedCount = computed(() => this.bulkSelectedIds().size, ...(ngDevMode ? [{ debugName: "bulkSelectedCount" }] : /* istanbul ignore next */ []));
89
+ hasQuickActions = computed(() => this.showQuickActions() && this.quickActions().length > 0, ...(ngDevMode ? [{ debugName: "hasQuickActions" }] : /* istanbul ignore next */ []));
90
+ isNested = computed(() => this.nested(), ...(ngDevMode ? [{ debugName: "isNested" }] : /* istanbul ignore next */ []));
91
+ isVirtualized = computed(() => this.virtualized() || this.mode() === 'virtualization', ...(ngDevMode ? [{ debugName: "isVirtualized" }] : /* istanbul ignore next */ []));
92
+ isGrouped = computed(() => this.grouped(), ...(ngDevMode ? [{ debugName: "isGrouped" }] : /* istanbul ignore next */ []));
93
+ isTemplateMode = computed(() => this.mode() === 'template', ...(ngDevMode ? [{ debugName: "isTemplateMode" }] : /* istanbul ignore next */ []));
94
+ isCallHistoryMode = computed(() => this.mode() === 'call-history', ...(ngDevMode ? [{ debugName: "isCallHistoryMode" }] : /* istanbul ignore next */ []));
95
+ isScrollingMode = computed(() => false, ...(ngDevMode ? [{ debugName: "isScrollingMode" }] : /* istanbul ignore next */ []));
96
+ filteredItems = computed(() => {
97
+ const q = this.query().trim().toLowerCase();
98
+ const source = this.items();
99
+ if (!q)
100
+ return source;
101
+ if (this.isNested()) {
102
+ return this.filterNested(source, q);
103
+ }
104
+ return source.filter(item => this.itemMatches(item, q));
105
+ }, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
106
+ flattenedNestedItems = computed(() => {
107
+ if (!this.isNested())
108
+ return [];
109
+ return this.flattenTree(this.filteredItems(), 0);
110
+ }, ...(ngDevMode ? [{ debugName: "flattenedNestedItems" }] : /* istanbul ignore next */ []));
111
+ groupedItems = computed(() => {
112
+ if (!this.isGrouped())
113
+ return [];
114
+ const map = new Map();
115
+ for (const item of this.filteredItems()) {
116
+ const key = item.group?.trim() || 'Ungrouped';
117
+ const groupItems = map.get(key) ?? [];
118
+ groupItems.push(item);
119
+ map.set(key, groupItems);
120
+ }
121
+ return [...map.entries()].map(([label, items]) => ({ label, items }));
122
+ }, ...(ngDevMode ? [{ debugName: "groupedItems" }] : /* istanbul ignore next */ []));
123
+ flatItems = computed(() => {
124
+ if (this.isNested())
125
+ return [];
126
+ if (this.isGrouped())
127
+ return this.groupedItems().flatMap(g => g.items);
128
+ return this.filteredItems();
129
+ }, ...(ngDevMode ? [{ debugName: "flatItems" }] : /* istanbul ignore next */ []));
130
+ totalVirtualHeight = computed(() => this.flatItems().length * this.itemHeight(), ...(ngDevMode ? [{ debugName: "totalVirtualHeight" }] : /* istanbul ignore next */ []));
131
+ virtualRange = computed(() => {
132
+ if (!this.isVirtualized() || this.isNested()) {
133
+ return { start: 0, end: this.flatItems().length };
134
+ }
135
+ const rowHeight = Math.max(1, this.itemHeight());
136
+ const total = this.flatItems().length;
137
+ const visibleRows = Math.ceil(this.viewportHeight() / rowHeight);
138
+ const start = Math.max(0, Math.floor(this.scrollTop() / rowHeight) - this.overscan());
139
+ const end = Math.min(total, start + visibleRows + this.overscan() * 2);
140
+ return { start, end };
141
+ }, ...(ngDevMode ? [{ debugName: "virtualRange" }] : /* istanbul ignore next */ []));
142
+ virtualItems = computed(() => {
143
+ const range = this.virtualRange();
144
+ return this.flatItems().slice(range.start, range.end);
145
+ }, ...(ngDevMode ? [{ debugName: "virtualItems" }] : /* istanbul ignore next */ []));
146
+ topSpacerHeight = computed(() => this.virtualRange().start * this.itemHeight(), ...(ngDevMode ? [{ debugName: "topSpacerHeight" }] : /* istanbul ignore next */ []));
147
+ bottomSpacerHeight = computed(() => {
148
+ const range = this.virtualRange();
149
+ const trailing = Math.max(0, this.flatItems().length - range.end);
150
+ return trailing * this.itemHeight();
151
+ }, ...(ngDevMode ? [{ debugName: "bottomSpacerHeight" }] : /* istanbul ignore next */ []));
152
+ renderItems = computed(() => this.isVirtualized() && !this.isNested() ? this.virtualItems() : this.flatItems(), ...(ngDevMode ? [{ debugName: "renderItems" }] : /* istanbul ignore next */ []));
153
+ allVisibleIds = computed(() => {
154
+ if (this.isNested()) {
155
+ return this.flattenedNestedItems().map(n => n.item.id);
156
+ }
157
+ return this.flatItems().map(item => item.id);
158
+ }, ...(ngDevMode ? [{ debugName: "allVisibleIds" }] : /* istanbul ignore next */ []));
159
+ allChecked = computed(() => {
160
+ const ids = this.allVisibleIds();
161
+ if (!ids.length)
162
+ return false;
163
+ const checked = this.checkedIds();
164
+ return ids.every(id => checked.has(id));
165
+ }, ...(ngDevMode ? [{ debugName: "allChecked" }] : /* istanbul ignore next */ []));
166
+ checkedCount = computed(() => this.checkedIds().size, ...(ngDevMode ? [{ debugName: "checkedCount" }] : /* istanbul ignore next */ []));
167
+ visibleItemCount = computed(() => {
168
+ if (this.isNested()) {
169
+ return this.countTreeItems(this.filteredItems());
170
+ }
171
+ return this.flatItems().length;
172
+ }, ...(ngDevMode ? [{ debugName: "visibleItemCount" }] : /* istanbul ignore next */ []));
173
+ rootClasses = computed(() => {
174
+ const base = 'rounded-2xl border bg-white/90 shadow-sm backdrop-blur-sm dark:bg-gray-900/80 dark:shadow-none';
175
+ const variantMap = {
176
+ default: 'border-gray-200/80 dark:border-gray-700/70',
177
+ soft: 'border-primary-100/80 bg-primary-50/40 dark:border-primary-900/50 dark:bg-primary-950/25',
178
+ outlined: 'border-2 border-gray-300 dark:border-gray-600',
179
+ };
180
+ return [base, variantMap[this.variant()], this.customClass()].filter(Boolean).join(' ');
181
+ }, ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
182
+ rowPadding = computed(() => {
183
+ const densityMap = {
184
+ compact: 'px-3 py-2',
185
+ comfortable: 'px-4 py-3',
186
+ spacious: 'px-5 py-4',
187
+ };
188
+ return densityMap[this.density()];
189
+ }, ...(ngDevMode ? [{ debugName: "rowPadding" }] : /* istanbul ignore next */ []));
190
+ onSearchInput(value) {
191
+ this.query.set(value);
192
+ }
193
+ onContainerScroll(event) {
194
+ const target = event.target;
195
+ this.scrollTop.set(target.scrollTop);
196
+ if (!this.infiniteScroll() || !this.hasMore() || this.loadingMore() || this.loadingGuard()) {
197
+ return;
198
+ }
199
+ const threshold = 100;
200
+ const nearBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - threshold;
201
+ if (!nearBottom)
202
+ return;
203
+ this.loadingGuard.set(true);
204
+ this.loadMore.emit({ visibleCount: this.renderItems().length, totalCount: this.flatItems().length });
205
+ queueMicrotask(() => this.loadingGuard.set(false));
206
+ }
207
+ onItemActivate(item) {
208
+ if (item.disabled)
209
+ return;
210
+ this.itemClick.emit({ item });
211
+ if (this.selectionMode() === 'single') {
212
+ const next = new Set([item.id]);
213
+ this.selectedIds.set(next);
214
+ this.selectionChange.emit(next);
215
+ this.announce(`Selected ${item.title}.`);
216
+ return;
217
+ }
218
+ if (this.selectionMode() === 'multiple') {
219
+ const next = new Set(this.selectedIds());
220
+ if (next.has(item.id))
221
+ next.delete(item.id);
222
+ else
223
+ next.add(item.id);
224
+ this.selectedIds.set(next);
225
+ this.selectionChange.emit(next);
226
+ this.announce(next.has(item.id) ? `Selected ${item.title}.` : `Deselected ${item.title}.`);
227
+ }
228
+ }
229
+ toggleCheck(item, checked) {
230
+ if (item.disabled)
231
+ return;
232
+ const next = new Set(this.checkedIds());
233
+ if (checked)
234
+ next.add(item.id);
235
+ else
236
+ next.delete(item.id);
237
+ this.checkedIds.set(next);
238
+ this.checkChange.emit({ item, checked });
239
+ this.announce(checked ? `Checked ${item.title}.` : `Unchecked ${item.title}.`);
240
+ }
241
+ toggleAllChecks(checked) {
242
+ const next = checked ? new Set(this.allVisibleIds()) : new Set();
243
+ this.checkedIds.set(next);
244
+ this.announce(checked ? `Selected all ${next.size} visible items.` : 'Cleared all selected items.');
245
+ }
246
+ toggleExpand(item) {
247
+ if (!item.children?.length)
248
+ return;
249
+ const next = new Set(this.expandedIds());
250
+ if (next.has(item.id))
251
+ next.delete(item.id);
252
+ else
253
+ next.add(item.id);
254
+ this.expandedIds.set(next);
255
+ }
256
+ isExpanded(item) {
257
+ return this.expandedIds().has(item.id);
258
+ }
259
+ isSelected(item) {
260
+ return this.selectedIds().has(item.id);
261
+ }
262
+ isChecked(item) {
263
+ return this.checkedIds().has(item.id);
264
+ }
265
+ toggleSelected(item, checked) {
266
+ if (item.disabled || this.selectionMode() === 'none') {
267
+ return;
268
+ }
269
+ if (this.selectionMode() === 'single') {
270
+ const next = checked ? new Set([item.id]) : new Set();
271
+ this.selectedIds.set(next);
272
+ this.selectionChange.emit(next);
273
+ return;
274
+ }
275
+ const next = new Set(this.selectedIds());
276
+ if (checked)
277
+ next.add(item.id);
278
+ else
279
+ next.delete(item.id);
280
+ this.selectedIds.set(next);
281
+ this.selectionChange.emit(next);
282
+ this.announce(checked ? `Selected ${item.title}.` : `Deselected ${item.title}.`);
283
+ }
284
+ onQuickAction(item, action) {
285
+ if (item.disabled)
286
+ return;
287
+ this.quickActionClick.emit({ item, action });
288
+ this.announce(`${action.label} action triggered for ${item.title}.`);
289
+ }
290
+ onBulkAction(action) {
291
+ const selected = new Set(this.bulkSelectedIds());
292
+ this.bulkActionClick.emit({ action, selectedIds: selected });
293
+ this.announce(`${action.label} applied to ${selected.size} selected item${selected.size === 1 ? '' : 's'}.`);
294
+ }
295
+ onRootKeydown(event) {
296
+ if (event.metaKey || event.ctrlKey || event.altKey) {
297
+ return;
298
+ }
299
+ const target = event.target;
300
+ const tag = target?.tagName.toLowerCase();
301
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || target?.isContentEditable) {
302
+ return;
303
+ }
304
+ if (this.enableRovingFocus() && this.handleRovingFocusKey(event, target)) {
305
+ return;
306
+ }
307
+ if (!this.enableBulkHotkeys() || this.bulkSelectedCount() === 0 || this.bulkActions().length === 0) {
308
+ return;
309
+ }
310
+ const key = event.key.toLowerCase();
311
+ const explicit = this.bulkHotkeys()[key];
312
+ const fallbackMap = {
313
+ delete: 'delete',
314
+ backspace: 'delete',
315
+ a: 'archive',
316
+ r: 'mark-read',
317
+ };
318
+ const actionId = explicit ?? fallbackMap[key];
319
+ if (!actionId) {
320
+ return;
321
+ }
322
+ const action = this.bulkActions().find(item => item.id === actionId);
323
+ if (!action) {
324
+ return;
325
+ }
326
+ event.preventDefault();
327
+ this.onBulkAction(action);
328
+ }
329
+ handleRovingFocusKey(event, target) {
330
+ const key = event.key;
331
+ if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Home' && key !== 'End') {
332
+ return false;
333
+ }
334
+ const rowButtons = this.rowButtons();
335
+ if (!rowButtons.length) {
336
+ return false;
337
+ }
338
+ const current = target?.closest('[data-lv-row-btn="1"]');
339
+ const currentIndex = current ? rowButtons.indexOf(current) : -1;
340
+ let nextIndex = 0;
341
+ if (key === 'ArrowDown') {
342
+ nextIndex = currentIndex < 0 ? 0 : Math.min(rowButtons.length - 1, currentIndex + 1);
343
+ }
344
+ else if (key === 'ArrowUp') {
345
+ nextIndex = currentIndex < 0 ? rowButtons.length - 1 : Math.max(0, currentIndex - 1);
346
+ }
347
+ else if (key === 'Home') {
348
+ nextIndex = 0;
349
+ }
350
+ else {
351
+ nextIndex = rowButtons.length - 1;
352
+ }
353
+ event.preventDefault();
354
+ rowButtons[nextIndex].focus();
355
+ return true;
356
+ }
357
+ rowButtons() {
358
+ const hostEl = this.host.nativeElement;
359
+ return Array.from(hostEl.querySelectorAll('button[data-lv-row-btn="1"]:not([disabled])'));
360
+ }
361
+ trackById(_, item) {
362
+ return item.id;
363
+ }
364
+ trackByNested(_, node) {
365
+ return node.item.id;
366
+ }
367
+ callTypeIcon(type) {
368
+ if (type === 'incoming')
369
+ return 'M5 12l5-5m0 0l5 5m-5-5v12';
370
+ if (type === 'outgoing')
371
+ return 'M19 12l-5 5m0 0l-5-5m5 5V5';
372
+ if (type === 'video')
373
+ return 'M15 10l4.553-2.276A1 1 0 0 1 21 8.618v6.764a1 1 0 0 1-1.447.894L15 14';
374
+ return 'M6 18L18 6M6 6l12 12';
375
+ }
376
+ callTypeClass(type) {
377
+ if (type === 'incoming')
378
+ return 'text-emerald-600 dark:text-emerald-400';
379
+ if (type === 'outgoing')
380
+ return 'text-primary-600 dark:text-primary-400';
381
+ if (type === 'video')
382
+ return 'text-violet-600 dark:text-violet-400';
383
+ return 'text-red-600 dark:text-red-400';
384
+ }
385
+ initials(item) {
386
+ const source = item.title.trim().split(/\s+/).slice(0, 2).map(part => part[0] ?? '').join('');
387
+ return source.toUpperCase() || 'NA';
388
+ }
389
+ nestedIndent(depth) {
390
+ const baseIndent = 0.5;
391
+ return `${baseIndent + depth * 1.2}rem`;
392
+ }
393
+ actionToneClass(tone) {
394
+ if (tone === 'primary') {
395
+ return 'border-primary-200 bg-primary-50 text-primary-700 hover:bg-primary-100 dark:border-primary-900/60 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45';
396
+ }
397
+ if (tone === 'danger') {
398
+ return 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100 dark:border-red-900/60 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/45';
399
+ }
400
+ return 'border-gray-200 bg-white text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900/60 dark:text-gray-300 dark:hover:bg-gray-800';
401
+ }
402
+ announce(message) {
403
+ if (!this.announceActions()) {
404
+ return;
405
+ }
406
+ this.liveAnnouncement.set('');
407
+ queueMicrotask(() => this.liveAnnouncement.set(message));
408
+ }
409
+ itemMatches(item, query) {
410
+ const haystack = [
411
+ item.title,
412
+ item.subtitle,
413
+ item.description,
414
+ item.group,
415
+ item.badge,
416
+ item.meta,
417
+ item.timestamp,
418
+ item.duration,
419
+ item.amount,
420
+ ...(item.tags ?? []),
421
+ ]
422
+ .filter(Boolean)
423
+ .join(' ')
424
+ .toLowerCase();
425
+ return haystack.includes(query);
426
+ }
427
+ filterNested(items, query) {
428
+ const result = [];
429
+ for (const item of items) {
430
+ const filteredChildren = item.children?.length ? this.filterNested(item.children, query) : [];
431
+ if (this.itemMatches(item, query) || filteredChildren.length > 0) {
432
+ result.push({ ...item, children: filteredChildren });
433
+ }
434
+ }
435
+ return result;
436
+ }
437
+ flattenTree(items, depth) {
438
+ const rows = [];
439
+ for (const item of items) {
440
+ const hasChildren = Boolean(item.children?.length);
441
+ rows.push({ item, depth, hasChildren });
442
+ if (hasChildren && this.expandedIds().has(item.id)) {
443
+ rows.push(...this.flattenTree(item.children ?? [], depth + 1));
444
+ }
445
+ }
446
+ return rows;
447
+ }
448
+ countTreeItems(items) {
449
+ let total = 0;
450
+ for (const item of items) {
451
+ total += 1;
452
+ if (item.children?.length) {
453
+ total += this.countTreeItems(item.children);
454
+ }
455
+ }
456
+ return total;
457
+ }
458
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvListViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
459
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvListViewComponent, isStandalone: true, selector: "sv-list-view", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, nested: { classPropertyName: "nested", publicName: "nested", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, grouped: { classPropertyName: "grouped", publicName: "grouped", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, stickyGroupHeaders: { classPropertyName: "stickyGroupHeaders", publicName: "stickyGroupHeaders", isSignal: true, isRequired: false, transformFunction: null }, infiniteScroll: { classPropertyName: "infiniteScroll", publicName: "infiniteScroll", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, viewportHeight: { classPropertyName: "viewportHeight", publicName: "viewportHeight", isSignal: true, isRequired: false, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, announceActions: { classPropertyName: "announceActions", publicName: "announceActions", isSignal: true, isRequired: false, transformFunction: null }, showQuickActions: { classPropertyName: "showQuickActions", publicName: "showQuickActions", isSignal: true, isRequired: false, transformFunction: null }, quickActions: { classPropertyName: "quickActions", publicName: "quickActions", isSignal: true, isRequired: false, transformFunction: null }, bulkActions: { classPropertyName: "bulkActions", publicName: "bulkActions", isSignal: true, isRequired: false, transformFunction: null }, enableBulkHotkeys: { classPropertyName: "enableBulkHotkeys", publicName: "enableBulkHotkeys", isSignal: true, isRequired: false, transformFunction: null }, bulkHotkeys: { classPropertyName: "bulkHotkeys", publicName: "bulkHotkeys", isSignal: true, isRequired: false, transformFunction: null }, enableRovingFocus: { classPropertyName: "enableRovingFocus", publicName: "enableRovingFocus", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null }, checkedIds: { classPropertyName: "checkedIds", publicName: "checkedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIds: "selectedIdsChange", checkedIds: "checkedIdsChange", itemClick: "itemClick", selectionChange: "selectionChange", checkChange: "checkChange", loadMore: "loadMore", quickActionClick: "quickActionClick", bulkActionClick: "bulkActionClick" }, ngImport: i0, template: "<div\n [ngClass]=\"rootClasses()\"\n class=\"focus-within:ring-2 focus-within:ring-primary-500/30\"\n role=\"region\"\n [attr.aria-label]=\"ariaLabel()\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n>\n <p class=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveAnnouncement() }}</p>\n\n <div class=\"flex flex-wrap items-center justify-between gap-2 border-b border-gray-200/80 px-4 py-3 dark:border-gray-700/70\">\n @if (searchable() || mode() === 'scrolling' || mode() === 'virtualization') {\n <div class=\"relative min-w-[220px] flex-1\">\n <svg\n class=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"M21 21l-4.3-4.3\" />\n </svg>\n\n <input\n type=\"search\"\n [value]=\"query()\"\n (input)=\"onSearchInput(($any($event.target)).value)\"\n [placeholder]=\"searchPlaceholder()\"\n class=\"h-10 w-full rounded-xl border border-gray-200 bg-white pl-9 pr-3 text-sm text-gray-700 placeholder:text-gray-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100\"\n />\n </div>\n }\n\n @if (isCheckable()) {\n <label class=\"inline-flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-300\">\n <input\n type=\"checkbox\"\n [checked]=\"allChecked()\"\n (change)=\"toggleAllChecks(($any($event.target)).checked)\"\n class=\"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n Select all\n </label>\n }\n\n <span class=\"rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ visibleItemCount() }} items\n </span>\n\n @if (isSelectable()) {\n <span class=\"rounded-full bg-primary-100 px-2.5 py-1 text-xs font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ selectedCount() }} selected\n </span>\n }\n </div>\n\n @if (bulkActions().length > 0 && bulkSelectedCount() > 0) {\n <div class=\"sticky top-0 z-20 flex flex-wrap items-center gap-2 border-b border-primary-200/80 bg-primary-50/90 px-4 py-2 backdrop-blur dark:border-primary-900/40 dark:bg-primary-950/70\">\n <span class=\"text-xs font-semibold text-primary-700 dark:text-primary-300\">{{ bulkSelectedCount() }} selected</span>\n @for (action of bulkActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onBulkAction(action)\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-xs font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n\n @if (isNested()) {\n <div\n class=\"max-h-[420px] overflow-auto\"\n [style.height.px]=\"isVirtualized() ? viewportHeight() : null\"\n (scroll)=\"onContainerScroll($event)\"\n >\n @if (flattenedNestedItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n <div class=\"pt-2 divide-y divide-gray-100 dark:divide-gray-800\">\n @for (node of flattenedNestedItems(); track trackByNested($index, node)) {\n <div\n class=\"flex items-start gap-2\"\n [ngClass]=\"[\n rowPadding(),\n node.item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/60',\n isSelected(node.item) ? 'bg-primary-50/80 dark:bg-primary-950/35' : ''\n ]\"\n [style.padding-left]=\"nestedIndent(node.depth)\"\n >\n @if (node.hasChildren) {\n <button\n type=\"button\"\n class=\"mt-1 inline-flex h-6 w-6 items-center justify-center rounded-lg border border-gray-200 text-gray-500 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800\"\n (click)=\"toggleExpand(node.item)\"\n [attr.aria-label]=\"isExpanded(node.item) ? 'Collapse node' : 'Expand node'\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\">\n @if (isExpanded(node.item)) {\n <path d=\"M19 12H5\" />\n } @else {\n <path d=\"M12 5v14M5 12h14\" />\n }\n </svg>\n </button>\n } @else {\n <span class=\"mt-1 h-6 w-6\"></span>\n }\n\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(node.item)\"\n (change)=\"toggleCheck(node.item, ($any($event.target)).checked)\"\n [disabled]=\"node.item.disabled\"\n class=\"mt-1.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select-nested\"\n [checked]=\"isSelected(node.item)\"\n (change)=\"toggleSelected(node.item, ($any($event.target)).checked)\"\n [disabled]=\"node.item.disabled\"\n class=\"mt-1.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n <button\n type=\"button\"\n class=\"min-w-0 flex-1 text-left\"\n data-lv-row-btn=\"1\"\n [disabled]=\"node.item.disabled\"\n (click)=\"onItemActivate(node.item)\"\n >\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ node.item.title }}</p>\n @if (node.item.subtitle || node.item.description) {\n <p class=\"mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400\">\n {{ node.item.subtitle || node.item.description }}\n </p>\n }\n </button>\n\n @if (node.item.badge) {\n <span class=\"rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ node.item.badge }}\n </span>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isGrouped()) {\n <div class=\"max-h-[420px] overflow-auto\" (scroll)=\"onContainerScroll($event)\">\n @if (groupedItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n <div>\n @for (group of groupedItems(); track group.label) {\n <section>\n <header\n [ngClass]=\"stickyGroupHeaders() ? 'sticky top-0 z-10 bg-gray-50/90 dark:bg-gray-950/90 backdrop-blur' : 'bg-gray-50 dark:bg-gray-950/70'\"\n class=\"border-y border-gray-200/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:border-gray-700/80 dark:text-gray-400\"\n >\n {{ group.label }}\n </header>\n\n @for (item of group.items; track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3 border-b border-gray-100/80 dark:border-gray-800',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n >\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta }}</span>\n }\n </div>\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n </button>\n </div>\n }\n </section>\n }\n </div>\n }\n </div>\n } @else {\n <div\n class=\"overflow-auto\"\n [style.height.px]=\"(isVirtualized() || isScrollingMode()) ? viewportHeight() : null\"\n [style.max-height.px]=\"!isVirtualized() && !isScrollingMode() ? viewportHeight() : null\"\n (scroll)=\"onContainerScroll($event)\"\n >\n @if (renderItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n @if (isVirtualized()) {\n <div [style.height.px]=\"totalVirtualHeight()\" class=\"relative\">\n <div [style.height.px]=\"topSpacerHeight()\"></div>\n\n @for (item of renderItems(); track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3 border-b border-gray-100/80 dark:border-gray-800',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n isSelected(item) ? 'bg-primary-100/60 dark:bg-primary-900/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n [style.height.px]=\"itemHeight()\"\n >\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(item)\"\n (change)=\"toggleCheck(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select\"\n [checked]=\"isSelected(item)\"\n (change)=\"toggleSelected(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta || item.timestamp) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta || item.timestamp }}</span>\n }\n </div>\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 truncate text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n </button>\n </div>\n }\n\n <div [style.height.px]=\"bottomSpacerHeight()\"></div>\n </div>\n } @else {\n <div class=\"divide-y divide-gray-100 dark:divide-gray-800\">\n @for (item of renderItems(); track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n isSelected(item) ? 'bg-primary-100/60 dark:bg-primary-900/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n >\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(item)\"\n (change)=\"toggleCheck(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select\"\n [checked]=\"isSelected(item)\"\n (change)=\"toggleSelected(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (isCallHistoryMode()) {\n <span [ngClass]=\"['mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-800', callTypeClass(item.callType)]\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path [attr.d]=\"callTypeIcon(item.callType)\" />\n </svg>\n </span>\n } @else if (item.avatarUrl) {\n <img [src]=\"item.avatarUrl\" [alt]=\"item.title\" class=\"h-9 w-9 rounded-xl object-cover\" loading=\"lazy\" />\n } @else {\n <span class=\"inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gray-100 text-xs font-bold text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ initials(item) }}\n </span>\n }\n\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta || item.timestamp) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta || item.timestamp }}</span>\n }\n </div>\n\n @if (isTemplateMode()) {\n @if (item.description) {\n <p class=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">{{ item.description }}</p>\n }\n @if (item.tags?.length) {\n <div class=\"mt-2 flex flex-wrap gap-1.5\">\n @for (tag of item.tags; track tag) {\n <span class=\"rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ tag }}\n </span>\n }\n </div>\n }\n } @else {\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 truncate text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n }\n </button>\n\n <div class=\"flex shrink-0 flex-col items-end gap-1.5\">\n @if (item.badge) {\n <span class=\"rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ item.badge }}\n </span>\n }\n @if (item.duration || item.amount) {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">{{ item.duration || item.amount }}</span>\n }\n </div>\n\n @if (hasQuickActions()) {\n <div class=\"hidden shrink-0 items-center gap-1 sm:flex\">\n @for (action of quickActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onQuickAction(item, action); $event.stopPropagation()\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-[11px] font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n [attr.aria-label]=\"action.label\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n </div>\n\n @if (hasQuickActions()) {\n <div class=\"mt-2 flex flex-wrap gap-1 sm:hidden\">\n @for (action of quickActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onQuickAction(item, action); $event.stopPropagation()\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-[11px] font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n [attr.aria-label]=\"action.label\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n }\n </div>\n }\n }\n\n @if ((isScrollingMode() || infiniteScroll()) && (loadingMore() || hasMore())) {\n <div class=\"border-t border-gray-200/70 px-4 py-3 text-center dark:border-gray-700/80\">\n @if (loadingMore()) {\n <span class=\"inline-flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400\">\n <svg class=\"h-4 w-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"2\" opacity=\"0.2\"></circle>\n <path d=\"M12 3a9 9 0 0 1 9 9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"></path>\n </svg>\n Loading more items...\n </span>\n } @else {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">Scroll to load more</span>\n }\n </div>\n }\n </div>\n }\n\n @if (isCheckable()) {\n <div class=\"border-t border-gray-200/80 px-4 py-2 text-xs text-gray-500 dark:border-gray-700/70 dark:text-gray-400\">\n {{ checkedCount() }} checked\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
460
+ }
461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvListViewComponent, decorators: [{
462
+ type: Component,
463
+ args: [{ selector: 'sv-list-view', standalone: true, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n [ngClass]=\"rootClasses()\"\n class=\"focus-within:ring-2 focus-within:ring-primary-500/30\"\n role=\"region\"\n [attr.aria-label]=\"ariaLabel()\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n>\n <p class=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveAnnouncement() }}</p>\n\n <div class=\"flex flex-wrap items-center justify-between gap-2 border-b border-gray-200/80 px-4 py-3 dark:border-gray-700/70\">\n @if (searchable() || mode() === 'scrolling' || mode() === 'virtualization') {\n <div class=\"relative min-w-[220px] flex-1\">\n <svg\n class=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"M21 21l-4.3-4.3\" />\n </svg>\n\n <input\n type=\"search\"\n [value]=\"query()\"\n (input)=\"onSearchInput(($any($event.target)).value)\"\n [placeholder]=\"searchPlaceholder()\"\n class=\"h-10 w-full rounded-xl border border-gray-200 bg-white pl-9 pr-3 text-sm text-gray-700 placeholder:text-gray-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100\"\n />\n </div>\n }\n\n @if (isCheckable()) {\n <label class=\"inline-flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-300\">\n <input\n type=\"checkbox\"\n [checked]=\"allChecked()\"\n (change)=\"toggleAllChecks(($any($event.target)).checked)\"\n class=\"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n Select all\n </label>\n }\n\n <span class=\"rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ visibleItemCount() }} items\n </span>\n\n @if (isSelectable()) {\n <span class=\"rounded-full bg-primary-100 px-2.5 py-1 text-xs font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ selectedCount() }} selected\n </span>\n }\n </div>\n\n @if (bulkActions().length > 0 && bulkSelectedCount() > 0) {\n <div class=\"sticky top-0 z-20 flex flex-wrap items-center gap-2 border-b border-primary-200/80 bg-primary-50/90 px-4 py-2 backdrop-blur dark:border-primary-900/40 dark:bg-primary-950/70\">\n <span class=\"text-xs font-semibold text-primary-700 dark:text-primary-300\">{{ bulkSelectedCount() }} selected</span>\n @for (action of bulkActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onBulkAction(action)\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-xs font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n\n @if (isNested()) {\n <div\n class=\"max-h-[420px] overflow-auto\"\n [style.height.px]=\"isVirtualized() ? viewportHeight() : null\"\n (scroll)=\"onContainerScroll($event)\"\n >\n @if (flattenedNestedItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n <div class=\"pt-2 divide-y divide-gray-100 dark:divide-gray-800\">\n @for (node of flattenedNestedItems(); track trackByNested($index, node)) {\n <div\n class=\"flex items-start gap-2\"\n [ngClass]=\"[\n rowPadding(),\n node.item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/60',\n isSelected(node.item) ? 'bg-primary-50/80 dark:bg-primary-950/35' : ''\n ]\"\n [style.padding-left]=\"nestedIndent(node.depth)\"\n >\n @if (node.hasChildren) {\n <button\n type=\"button\"\n class=\"mt-1 inline-flex h-6 w-6 items-center justify-center rounded-lg border border-gray-200 text-gray-500 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800\"\n (click)=\"toggleExpand(node.item)\"\n [attr.aria-label]=\"isExpanded(node.item) ? 'Collapse node' : 'Expand node'\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\">\n @if (isExpanded(node.item)) {\n <path d=\"M19 12H5\" />\n } @else {\n <path d=\"M12 5v14M5 12h14\" />\n }\n </svg>\n </button>\n } @else {\n <span class=\"mt-1 h-6 w-6\"></span>\n }\n\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(node.item)\"\n (change)=\"toggleCheck(node.item, ($any($event.target)).checked)\"\n [disabled]=\"node.item.disabled\"\n class=\"mt-1.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select-nested\"\n [checked]=\"isSelected(node.item)\"\n (change)=\"toggleSelected(node.item, ($any($event.target)).checked)\"\n [disabled]=\"node.item.disabled\"\n class=\"mt-1.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n <button\n type=\"button\"\n class=\"min-w-0 flex-1 text-left\"\n data-lv-row-btn=\"1\"\n [disabled]=\"node.item.disabled\"\n (click)=\"onItemActivate(node.item)\"\n >\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ node.item.title }}</p>\n @if (node.item.subtitle || node.item.description) {\n <p class=\"mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400\">\n {{ node.item.subtitle || node.item.description }}\n </p>\n }\n </button>\n\n @if (node.item.badge) {\n <span class=\"rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ node.item.badge }}\n </span>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isGrouped()) {\n <div class=\"max-h-[420px] overflow-auto\" (scroll)=\"onContainerScroll($event)\">\n @if (groupedItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n <div>\n @for (group of groupedItems(); track group.label) {\n <section>\n <header\n [ngClass]=\"stickyGroupHeaders() ? 'sticky top-0 z-10 bg-gray-50/90 dark:bg-gray-950/90 backdrop-blur' : 'bg-gray-50 dark:bg-gray-950/70'\"\n class=\"border-y border-gray-200/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:border-gray-700/80 dark:text-gray-400\"\n >\n {{ group.label }}\n </header>\n\n @for (item of group.items; track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3 border-b border-gray-100/80 dark:border-gray-800',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n >\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta }}</span>\n }\n </div>\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n </button>\n </div>\n }\n </section>\n }\n </div>\n }\n </div>\n } @else {\n <div\n class=\"overflow-auto\"\n [style.height.px]=\"(isVirtualized() || isScrollingMode()) ? viewportHeight() : null\"\n [style.max-height.px]=\"!isVirtualized() && !isScrollingMode() ? viewportHeight() : null\"\n (scroll)=\"onContainerScroll($event)\"\n >\n @if (renderItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n @if (isVirtualized()) {\n <div [style.height.px]=\"totalVirtualHeight()\" class=\"relative\">\n <div [style.height.px]=\"topSpacerHeight()\"></div>\n\n @for (item of renderItems(); track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3 border-b border-gray-100/80 dark:border-gray-800',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n isSelected(item) ? 'bg-primary-100/60 dark:bg-primary-900/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n [style.height.px]=\"itemHeight()\"\n >\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(item)\"\n (change)=\"toggleCheck(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select\"\n [checked]=\"isSelected(item)\"\n (change)=\"toggleSelected(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta || item.timestamp) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta || item.timestamp }}</span>\n }\n </div>\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 truncate text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n </button>\n </div>\n }\n\n <div [style.height.px]=\"bottomSpacerHeight()\"></div>\n </div>\n } @else {\n <div class=\"divide-y divide-gray-100 dark:divide-gray-800\">\n @for (item of renderItems(); track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n isSelected(item) ? 'bg-primary-100/60 dark:bg-primary-900/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n >\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(item)\"\n (change)=\"toggleCheck(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select\"\n [checked]=\"isSelected(item)\"\n (change)=\"toggleSelected(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (isCallHistoryMode()) {\n <span [ngClass]=\"['mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-800', callTypeClass(item.callType)]\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path [attr.d]=\"callTypeIcon(item.callType)\" />\n </svg>\n </span>\n } @else if (item.avatarUrl) {\n <img [src]=\"item.avatarUrl\" [alt]=\"item.title\" class=\"h-9 w-9 rounded-xl object-cover\" loading=\"lazy\" />\n } @else {\n <span class=\"inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gray-100 text-xs font-bold text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ initials(item) }}\n </span>\n }\n\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta || item.timestamp) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta || item.timestamp }}</span>\n }\n </div>\n\n @if (isTemplateMode()) {\n @if (item.description) {\n <p class=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">{{ item.description }}</p>\n }\n @if (item.tags?.length) {\n <div class=\"mt-2 flex flex-wrap gap-1.5\">\n @for (tag of item.tags; track tag) {\n <span class=\"rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ tag }}\n </span>\n }\n </div>\n }\n } @else {\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 truncate text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n }\n </button>\n\n <div class=\"flex shrink-0 flex-col items-end gap-1.5\">\n @if (item.badge) {\n <span class=\"rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ item.badge }}\n </span>\n }\n @if (item.duration || item.amount) {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">{{ item.duration || item.amount }}</span>\n }\n </div>\n\n @if (hasQuickActions()) {\n <div class=\"hidden shrink-0 items-center gap-1 sm:flex\">\n @for (action of quickActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onQuickAction(item, action); $event.stopPropagation()\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-[11px] font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n [attr.aria-label]=\"action.label\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n </div>\n\n @if (hasQuickActions()) {\n <div class=\"mt-2 flex flex-wrap gap-1 sm:hidden\">\n @for (action of quickActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onQuickAction(item, action); $event.stopPropagation()\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-[11px] font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n [attr.aria-label]=\"action.label\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n }\n </div>\n }\n }\n\n @if ((isScrollingMode() || infiniteScroll()) && (loadingMore() || hasMore())) {\n <div class=\"border-t border-gray-200/70 px-4 py-3 text-center dark:border-gray-700/80\">\n @if (loadingMore()) {\n <span class=\"inline-flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400\">\n <svg class=\"h-4 w-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"2\" opacity=\"0.2\"></circle>\n <path d=\"M12 3a9 9 0 0 1 9 9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"></path>\n </svg>\n Loading more items...\n </span>\n } @else {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">Scroll to load more</span>\n }\n </div>\n }\n </div>\n }\n\n @if (isCheckable()) {\n <div class=\"border-t border-gray-200/80 px-4 py-2 text-xs text-gray-500 dark:border-gray-700/70 dark:text-gray-400\">\n {{ checkedCount() }} checked\n </div>\n }\n</div>\n" }]
464
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], checkable: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkable", required: false }] }], nested: [{ type: i0.Input, args: [{ isSignal: true, alias: "nested", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], grouped: [{ type: i0.Input, args: [{ isSignal: true, alias: "grouped", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], stickyGroupHeaders: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyGroupHeaders", required: false }] }], infiniteScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "infiniteScroll", required: false }] }], hasMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasMore", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], announceActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "announceActions", required: false }] }], showQuickActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showQuickActions", required: false }] }], quickActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickActions", required: false }] }], bulkActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "bulkActions", required: false }] }], enableBulkHotkeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableBulkHotkeys", required: false }] }], bulkHotkeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "bulkHotkeys", required: false }] }], enableRovingFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableRovingFocus", required: false }] }], selectedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIds", required: false }] }, { type: i0.Output, args: ["selectedIdsChange"] }], checkedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkedIds", required: false }] }, { type: i0.Output, args: ["checkedIdsChange"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], checkChange: [{ type: i0.Output, args: ["checkChange"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], quickActionClick: [{ type: i0.Output, args: ["quickActionClick"] }], bulkActionClick: [{ type: i0.Output, args: ["bulkActionClick"] }] } });
465
+
466
+ /**
467
+ * Generated bundle index. Do not edit.
468
+ */
469
+
470
+ export { SvListViewComponent };
471
+ //# sourceMappingURL=styloviz-list-view.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styloviz-list-view.mjs","sources":["../../../../projects/list-view-free/src/lib/list-view.component.ts","../../../../projects/list-view-free/src/lib/list-view.component.html","../../../../projects/list-view-free/src/styloviz-list-view.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n computed,\n inject,\n input,\n model,\n output,\n signal,\n} from '@angular/core';\nimport { NgClass } from '@angular/common';\n\nexport type ListViewMode =\n | 'default'\n | 'checklist'\n | 'nested'\n | 'virtualization'\n | 'scrolling'\n | 'grouped'\n | 'template'\n | 'call-history';\n\nexport type ListViewVariant = 'default' | 'soft' | 'outlined';\nexport type ListViewDensity = 'compact' | 'comfortable' | 'spacious';\nexport type ListViewSelectionMode = 'none' | 'single' | 'multiple';\nexport type ListViewCallType = 'incoming' | 'outgoing' | 'missed' | 'video';\nexport type ListViewActionTone = 'neutral' | 'primary' | 'danger';\n\nexport interface ListViewItem {\n id: string | number;\n title: string;\n subtitle?: string;\n description?: string;\n group?: string;\n badge?: string;\n meta?: string;\n timestamp?: string;\n duration?: string;\n avatarUrl?: string;\n iconPath?: string;\n tags?: string[];\n amount?: string;\n callType?: ListViewCallType;\n unread?: boolean;\n disabled?: boolean;\n children?: ListViewItem[];\n}\n\nexport interface ListViewItemClickEvent {\n item: ListViewItem;\n}\n\nexport interface ListViewCheckChangeEvent {\n item: ListViewItem;\n checked: boolean;\n}\n\nexport interface ListViewLoadMoreEvent {\n visibleCount: number;\n totalCount: number;\n}\n\nexport interface ListViewQuickAction {\n id: string;\n label: string;\n iconPath?: string;\n tone?: ListViewActionTone;\n}\n\nexport interface ListViewQuickActionClickEvent {\n item: ListViewItem;\n action: ListViewQuickAction;\n}\n\nexport interface ListViewBulkAction {\n id: string;\n label: string;\n iconPath?: string;\n tone?: ListViewActionTone;\n}\n\nexport interface ListViewBulkActionClickEvent {\n action: ListViewBulkAction;\n selectedIds: Set<string | number>;\n}\n\ninterface ListViewGroup {\n label: string;\n items: ListViewItem[];\n}\n\ninterface FlattenedItem {\n item: ListViewItem;\n depth: number;\n hasChildren: boolean;\n}\n\n@Component({\n selector: 'sv-list-view',\n standalone: true,\n imports: [NgClass],\n templateUrl: './list-view.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvListViewComponent {\n /** Items to render (label, meta, optional children/avatar/actions). */\n items = input<ListViewItem[]>([]);\n /** Preset pattern: default, checklist, nested, card, etc. @default 'default' */\n mode = input<ListViewMode>('default');\n /** Visual variant. @default 'default' */\n variant = input<ListViewVariant>('default');\n /** Row density. @default 'comfortable' */\n density = input<ListViewDensity>('comfortable');\n\n /** Selection behaviour: none, single or multiple. @default 'none' */\n selectionMode = input<ListViewSelectionMode>('none');\n /** Show checkboxes on rows. @default false */\n checkable = input<boolean>(false);\n /** Enable nested (tree) rows. @default false */\n nested = input<boolean>(false);\n /** Virtualize long lists for performance. @default false */\n virtualized = input<boolean>(false);\n /** Group rows under section headers. @default false */\n grouped = input<boolean>(false);\n /** Show a search box to filter items. @default false */\n searchable = input<boolean>(false);\n /** Keep group headers pinned while scrolling. @default true */\n stickyGroupHeaders = input<boolean>(true);\n\n /** Load more items as the user scrolls to the end. @default false */\n infiniteScroll = input<boolean>(false);\n /** Whether more items are available to load. @default false */\n hasMore = input<boolean>(false);\n /** Show a loading indicator while fetching more. @default false */\n loadingMore = input<boolean>(false);\n /** Viewport height (px) for the virtualized list. @default 360 */\n viewportHeight = input<number>(360);\n /** Estimated row height (px) for virtualization. @default 68 */\n itemHeight = input<number>(68);\n /** Extra rows rendered above/below the viewport. @default 5 */\n overscan = input<number>(5);\n\n /** Placeholder for the search box. @default 'Search list items' */\n searchPlaceholder = input<string>('Search list items');\n /** Message shown when no items match. @default 'No items match your filters.' */\n emptyMessage = input<string>('No items match your filters.');\n /** Extra CSS classes for the root. */\n customClass = input<string>('');\n /** Accessible label for the list. @default 'List view' */\n ariaLabel = input<string>('List view');\n /** Announce row actions to screen readers. @default true */\n announceActions = input<boolean>(true);\n /** Show per-row quick actions on hover/focus. @default false */\n showQuickActions = input<boolean>(false);\n /** Per-row quick actions. */\n quickActions = input<ListViewQuickAction[]>([]);\n /** Bulk actions for checked rows. */\n bulkActions = input<ListViewBulkAction[]>([]);\n /** Enable keyboard hotkeys for bulk actions. @default false */\n enableBulkHotkeys = input<boolean>(false);\n /** Map of hotkey → bulk action id. */\n bulkHotkeys = input<Record<string, string>>({});\n /** Enable roving-tabindex keyboard navigation. @default true */\n enableRovingFocus = input<boolean>(true);\n\n /** Selected row ids (two-way). */\n selectedIds = model<Set<string | number>>(new Set());\n /** Checked row ids (two-way). */\n checkedIds = model<Set<string | number>>(new Set());\n\n /** Emitted with the clicked item. */\n itemClick = output<ListViewItemClickEvent>();\n /** Emitted with the current selection set. */\n selectionChange = output<Set<string | number>>();\n /** Emitted when a row's checkbox changes. */\n checkChange = output<ListViewCheckChangeEvent>();\n /** Emitted when more items should be loaded (infinite scroll). */\n loadMore = output<ListViewLoadMoreEvent>();\n /** Emitted when a quick action is clicked. */\n quickActionClick = output<ListViewQuickActionClickEvent>();\n /** Emitted when a bulk action is triggered. */\n bulkActionClick = output<ListViewBulkActionClickEvent>();\n\n readonly query = signal<string>('');\n readonly liveAnnouncement = signal<string>('');\n private readonly host = inject(ElementRef<HTMLElement>);\n private readonly scrollTop = signal<number>(0);\n private readonly expandedIds = signal<Set<string | number>>(new Set());\n private readonly loadingGuard = signal<boolean>(false);\n\n readonly isCheckable = computed(() => this.checkable() || this.mode() === 'checklist');\n readonly isSelectable = computed(() => this.selectionMode() !== 'none');\n readonly selectedCount = computed(() => this.selectedIds().size);\n readonly bulkSelectedIds = computed(() => (this.isCheckable() ? this.checkedIds() : this.selectedIds()));\n readonly bulkSelectedCount = computed(() => this.bulkSelectedIds().size);\n readonly hasQuickActions = computed(() => this.showQuickActions() && this.quickActions().length > 0);\n readonly isNested = computed(() => this.nested());\n readonly isVirtualized = computed(() => this.virtualized() || this.mode() === 'virtualization');\n readonly isGrouped = computed(() => this.grouped());\n readonly isTemplateMode = computed(() => this.mode() === 'template');\n readonly isCallHistoryMode = computed(() => this.mode() === 'call-history');\n readonly isScrollingMode = computed(() => false);\n\n readonly filteredItems = computed<ListViewItem[]>(() => {\n const q = this.query().trim().toLowerCase();\n const source = this.items();\n if (!q) return source;\n\n if (this.isNested()) {\n return this.filterNested(source, q);\n }\n\n return source.filter(item => this.itemMatches(item, q));\n });\n\n readonly flattenedNestedItems = computed<FlattenedItem[]>(() => {\n if (!this.isNested()) return [];\n return this.flattenTree(this.filteredItems(), 0);\n });\n\n readonly groupedItems = computed<ListViewGroup[]>(() => {\n if (!this.isGrouped()) return [];\n\n const map = new Map<string, ListViewItem[]>();\n for (const item of this.filteredItems()) {\n const key = item.group?.trim() || 'Ungrouped';\n const groupItems = map.get(key) ?? [];\n groupItems.push(item);\n map.set(key, groupItems);\n }\n\n return [...map.entries()].map(([label, items]) => ({ label, items }));\n });\n\n readonly flatItems = computed<ListViewItem[]>(() => {\n if (this.isNested()) return [];\n if (this.isGrouped()) return this.groupedItems().flatMap(g => g.items);\n return this.filteredItems();\n });\n\n readonly totalVirtualHeight = computed(() => this.flatItems().length * this.itemHeight());\n\n readonly virtualRange = computed(() => {\n if (!this.isVirtualized() || this.isNested()) {\n return { start: 0, end: this.flatItems().length };\n }\n\n const rowHeight = Math.max(1, this.itemHeight());\n const total = this.flatItems().length;\n const visibleRows = Math.ceil(this.viewportHeight() / rowHeight);\n const start = Math.max(0, Math.floor(this.scrollTop() / rowHeight) - this.overscan());\n const end = Math.min(total, start + visibleRows + this.overscan() * 2);\n\n return { start, end };\n });\n\n readonly virtualItems = computed<ListViewItem[]>(() => {\n const range = this.virtualRange();\n return this.flatItems().slice(range.start, range.end);\n });\n\n readonly topSpacerHeight = computed(() => this.virtualRange().start * this.itemHeight());\n readonly bottomSpacerHeight = computed(() => {\n const range = this.virtualRange();\n const trailing = Math.max(0, this.flatItems().length - range.end);\n return trailing * this.itemHeight();\n });\n\n readonly renderItems = computed<ListViewItem[]>(() =>\n this.isVirtualized() && !this.isNested() ? this.virtualItems() : this.flatItems()\n );\n\n readonly allVisibleIds = computed<(string | number)[]>(() => {\n if (this.isNested()) {\n return this.flattenedNestedItems().map(n => n.item.id);\n }\n return this.flatItems().map(item => item.id);\n });\n\n readonly allChecked = computed(() => {\n const ids = this.allVisibleIds();\n if (!ids.length) return false;\n const checked = this.checkedIds();\n return ids.every(id => checked.has(id));\n });\n\n readonly checkedCount = computed(() => this.checkedIds().size);\n readonly visibleItemCount = computed(() => {\n if (this.isNested()) {\n return this.countTreeItems(this.filteredItems());\n }\n return this.flatItems().length;\n });\n\n readonly rootClasses = computed(() => {\n const base =\n 'rounded-2xl border bg-white/90 shadow-sm backdrop-blur-sm dark:bg-gray-900/80 dark:shadow-none';\n\n const variantMap: Record<ListViewVariant, string> = {\n default: 'border-gray-200/80 dark:border-gray-700/70',\n soft: 'border-primary-100/80 bg-primary-50/40 dark:border-primary-900/50 dark:bg-primary-950/25',\n outlined: 'border-2 border-gray-300 dark:border-gray-600',\n };\n\n return [base, variantMap[this.variant()], this.customClass()].filter(Boolean).join(' ');\n });\n\n readonly rowPadding = computed(() => {\n const densityMap: Record<ListViewDensity, string> = {\n compact: 'px-3 py-2',\n comfortable: 'px-4 py-3',\n spacious: 'px-5 py-4',\n };\n return densityMap[this.density()];\n });\n\n onSearchInput(value: string): void {\n this.query.set(value);\n }\n\n onContainerScroll(event: Event): void {\n const target = event.target as HTMLElement;\n this.scrollTop.set(target.scrollTop);\n\n if (!this.infiniteScroll() || !this.hasMore() || this.loadingMore() || this.loadingGuard()) {\n return;\n }\n\n const threshold = 100;\n const nearBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - threshold;\n if (!nearBottom) return;\n\n this.loadingGuard.set(true);\n this.loadMore.emit({ visibleCount: this.renderItems().length, totalCount: this.flatItems().length });\n queueMicrotask(() => this.loadingGuard.set(false));\n }\n\n onItemActivate(item: ListViewItem): void {\n if (item.disabled) return;\n\n this.itemClick.emit({ item });\n\n if (this.selectionMode() === 'single') {\n const next = new Set<string | number>([item.id]);\n this.selectedIds.set(next);\n this.selectionChange.emit(next);\n this.announce(`Selected ${item.title}.`);\n return;\n }\n\n if (this.selectionMode() === 'multiple') {\n const next = new Set(this.selectedIds());\n if (next.has(item.id)) next.delete(item.id);\n else next.add(item.id);\n this.selectedIds.set(next);\n this.selectionChange.emit(next);\n this.announce(next.has(item.id) ? `Selected ${item.title}.` : `Deselected ${item.title}.`);\n }\n }\n\n toggleCheck(item: ListViewItem, checked: boolean): void {\n if (item.disabled) return;\n\n const next = new Set(this.checkedIds());\n if (checked) next.add(item.id);\n else next.delete(item.id);\n\n this.checkedIds.set(next);\n this.checkChange.emit({ item, checked });\n this.announce(checked ? `Checked ${item.title}.` : `Unchecked ${item.title}.`);\n }\n\n toggleAllChecks(checked: boolean): void {\n const next = checked ? new Set(this.allVisibleIds()) : new Set<string | number>();\n this.checkedIds.set(next);\n this.announce(checked ? `Selected all ${next.size} visible items.` : 'Cleared all selected items.');\n }\n\n toggleExpand(item: ListViewItem): void {\n if (!item.children?.length) return;\n\n const next = new Set(this.expandedIds());\n if (next.has(item.id)) next.delete(item.id);\n else next.add(item.id);\n this.expandedIds.set(next);\n }\n\n isExpanded(item: ListViewItem): boolean {\n return this.expandedIds().has(item.id);\n }\n\n isSelected(item: ListViewItem): boolean {\n return this.selectedIds().has(item.id);\n }\n\n isChecked(item: ListViewItem): boolean {\n return this.checkedIds().has(item.id);\n }\n\n toggleSelected(item: ListViewItem, checked: boolean): void {\n if (item.disabled || this.selectionMode() === 'none') {\n return;\n }\n\n if (this.selectionMode() === 'single') {\n const next = checked ? new Set<string | number>([item.id]) : new Set<string | number>();\n this.selectedIds.set(next);\n this.selectionChange.emit(next);\n return;\n }\n\n const next = new Set(this.selectedIds());\n if (checked) next.add(item.id);\n else next.delete(item.id);\n this.selectedIds.set(next);\n this.selectionChange.emit(next);\n this.announce(checked ? `Selected ${item.title}.` : `Deselected ${item.title}.`);\n }\n\n onQuickAction(item: ListViewItem, action: ListViewQuickAction): void {\n if (item.disabled) return;\n this.quickActionClick.emit({ item, action });\n this.announce(`${action.label} action triggered for ${item.title}.`);\n }\n\n onBulkAction(action: ListViewBulkAction): void {\n const selected = new Set(this.bulkSelectedIds());\n this.bulkActionClick.emit({ action, selectedIds: selected });\n this.announce(`${action.label} applied to ${selected.size} selected item${selected.size === 1 ? '' : 's'}.`);\n }\n\n onRootKeydown(event: KeyboardEvent): void {\n if (event.metaKey || event.ctrlKey || event.altKey) {\n return;\n }\n\n const target = event.target as HTMLElement | null;\n const tag = target?.tagName.toLowerCase();\n if (tag === 'input' || tag === 'textarea' || tag === 'select' || target?.isContentEditable) {\n return;\n }\n\n if (this.enableRovingFocus() && this.handleRovingFocusKey(event, target)) {\n return;\n }\n\n if (!this.enableBulkHotkeys() || this.bulkSelectedCount() === 0 || this.bulkActions().length === 0) {\n return;\n }\n\n const key = event.key.toLowerCase();\n const explicit = this.bulkHotkeys()[key];\n\n const fallbackMap: Record<string, string> = {\n delete: 'delete',\n backspace: 'delete',\n a: 'archive',\n r: 'mark-read',\n };\n\n const actionId = explicit ?? fallbackMap[key];\n if (!actionId) {\n return;\n }\n\n const action = this.bulkActions().find(item => item.id === actionId);\n if (!action) {\n return;\n }\n\n event.preventDefault();\n this.onBulkAction(action);\n }\n\n private handleRovingFocusKey(event: KeyboardEvent, target: HTMLElement | null): boolean {\n const key = event.key;\n if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Home' && key !== 'End') {\n return false;\n }\n\n const rowButtons = this.rowButtons();\n if (!rowButtons.length) {\n return false;\n }\n\n const current = target?.closest('[data-lv-row-btn=\"1\"]') as HTMLButtonElement | null;\n const currentIndex = current ? rowButtons.indexOf(current) : -1;\n\n let nextIndex = 0;\n if (key === 'ArrowDown') {\n nextIndex = currentIndex < 0 ? 0 : Math.min(rowButtons.length - 1, currentIndex + 1);\n } else if (key === 'ArrowUp') {\n nextIndex = currentIndex < 0 ? rowButtons.length - 1 : Math.max(0, currentIndex - 1);\n } else if (key === 'Home') {\n nextIndex = 0;\n } else {\n nextIndex = rowButtons.length - 1;\n }\n\n event.preventDefault();\n rowButtons[nextIndex].focus();\n return true;\n }\n\n private rowButtons(): HTMLButtonElement[] {\n const hostEl = this.host.nativeElement as HTMLElement;\n return Array.from(hostEl.querySelectorAll('button[data-lv-row-btn=\"1\"]:not([disabled])')) as HTMLButtonElement[];\n }\n\n trackById(_: number, item: ListViewItem): string | number {\n return item.id;\n }\n\n trackByNested(_: number, node: FlattenedItem): string | number {\n return node.item.id;\n }\n\n callTypeIcon(type: ListViewCallType | undefined): string {\n if (type === 'incoming') return 'M5 12l5-5m0 0l5 5m-5-5v12';\n if (type === 'outgoing') return 'M19 12l-5 5m0 0l-5-5m5 5V5';\n if (type === 'video') return 'M15 10l4.553-2.276A1 1 0 0 1 21 8.618v6.764a1 1 0 0 1-1.447.894L15 14';\n return 'M6 18L18 6M6 6l12 12';\n }\n\n callTypeClass(type: ListViewCallType | undefined): string {\n if (type === 'incoming') return 'text-emerald-600 dark:text-emerald-400';\n if (type === 'outgoing') return 'text-primary-600 dark:text-primary-400';\n if (type === 'video') return 'text-violet-600 dark:text-violet-400';\n return 'text-red-600 dark:text-red-400';\n }\n\n initials(item: ListViewItem): string {\n const source = item.title.trim().split(/\\s+/).slice(0, 2).map(part => part[0] ?? '').join('');\n return source.toUpperCase() || 'NA';\n }\n\n nestedIndent(depth: number): string {\n const baseIndent = 0.5;\n return `${baseIndent + depth * 1.2}rem`;\n }\n\n actionToneClass(tone: ListViewActionTone | undefined): string {\n if (tone === 'primary') {\n return 'border-primary-200 bg-primary-50 text-primary-700 hover:bg-primary-100 dark:border-primary-900/60 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45';\n }\n if (tone === 'danger') {\n return 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100 dark:border-red-900/60 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/45';\n }\n return 'border-gray-200 bg-white text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900/60 dark:text-gray-300 dark:hover:bg-gray-800';\n }\n\n private announce(message: string): void {\n if (!this.announceActions()) {\n return;\n }\n\n this.liveAnnouncement.set('');\n queueMicrotask(() => this.liveAnnouncement.set(message));\n }\n\n private itemMatches(item: ListViewItem, query: string): boolean {\n const haystack = [\n item.title,\n item.subtitle,\n item.description,\n item.group,\n item.badge,\n item.meta,\n item.timestamp,\n item.duration,\n item.amount,\n ...(item.tags ?? []),\n ]\n .filter(Boolean)\n .join(' ')\n .toLowerCase();\n\n return haystack.includes(query);\n }\n\n private filterNested(items: ListViewItem[], query: string): ListViewItem[] {\n const result: ListViewItem[] = [];\n\n for (const item of items) {\n const filteredChildren = item.children?.length ? this.filterNested(item.children, query) : [];\n if (this.itemMatches(item, query) || filteredChildren.length > 0) {\n result.push({ ...item, children: filteredChildren });\n }\n }\n\n return result;\n }\n\n private flattenTree(items: ListViewItem[], depth: number): FlattenedItem[] {\n const rows: FlattenedItem[] = [];\n\n for (const item of items) {\n const hasChildren = Boolean(item.children?.length);\n rows.push({ item, depth, hasChildren });\n\n if (hasChildren && this.expandedIds().has(item.id)) {\n rows.push(...this.flattenTree(item.children ?? [], depth + 1));\n }\n }\n\n return rows;\n }\n\n private countTreeItems(items: ListViewItem[]): number {\n let total = 0;\n for (const item of items) {\n total += 1;\n if (item.children?.length) {\n total += this.countTreeItems(item.children);\n }\n }\n return total;\n }\n}\n","<div\n [ngClass]=\"rootClasses()\"\n class=\"focus-within:ring-2 focus-within:ring-primary-500/30\"\n role=\"region\"\n [attr.aria-label]=\"ariaLabel()\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n>\n <p class=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveAnnouncement() }}</p>\n\n <div class=\"flex flex-wrap items-center justify-between gap-2 border-b border-gray-200/80 px-4 py-3 dark:border-gray-700/70\">\n @if (searchable() || mode() === 'scrolling' || mode() === 'virtualization') {\n <div class=\"relative min-w-[220px] flex-1\">\n <svg\n class=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"M21 21l-4.3-4.3\" />\n </svg>\n\n <input\n type=\"search\"\n [value]=\"query()\"\n (input)=\"onSearchInput(($any($event.target)).value)\"\n [placeholder]=\"searchPlaceholder()\"\n class=\"h-10 w-full rounded-xl border border-gray-200 bg-white pl-9 pr-3 text-sm text-gray-700 placeholder:text-gray-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100\"\n />\n </div>\n }\n\n @if (isCheckable()) {\n <label class=\"inline-flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-300\">\n <input\n type=\"checkbox\"\n [checked]=\"allChecked()\"\n (change)=\"toggleAllChecks(($any($event.target)).checked)\"\n class=\"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n Select all\n </label>\n }\n\n <span class=\"rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ visibleItemCount() }} items\n </span>\n\n @if (isSelectable()) {\n <span class=\"rounded-full bg-primary-100 px-2.5 py-1 text-xs font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ selectedCount() }} selected\n </span>\n }\n </div>\n\n @if (bulkActions().length > 0 && bulkSelectedCount() > 0) {\n <div class=\"sticky top-0 z-20 flex flex-wrap items-center gap-2 border-b border-primary-200/80 bg-primary-50/90 px-4 py-2 backdrop-blur dark:border-primary-900/40 dark:bg-primary-950/70\">\n <span class=\"text-xs font-semibold text-primary-700 dark:text-primary-300\">{{ bulkSelectedCount() }} selected</span>\n @for (action of bulkActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onBulkAction(action)\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-xs font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n\n @if (isNested()) {\n <div\n class=\"max-h-[420px] overflow-auto\"\n [style.height.px]=\"isVirtualized() ? viewportHeight() : null\"\n (scroll)=\"onContainerScroll($event)\"\n >\n @if (flattenedNestedItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n <div class=\"pt-2 divide-y divide-gray-100 dark:divide-gray-800\">\n @for (node of flattenedNestedItems(); track trackByNested($index, node)) {\n <div\n class=\"flex items-start gap-2\"\n [ngClass]=\"[\n rowPadding(),\n node.item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/60',\n isSelected(node.item) ? 'bg-primary-50/80 dark:bg-primary-950/35' : ''\n ]\"\n [style.padding-left]=\"nestedIndent(node.depth)\"\n >\n @if (node.hasChildren) {\n <button\n type=\"button\"\n class=\"mt-1 inline-flex h-6 w-6 items-center justify-center rounded-lg border border-gray-200 text-gray-500 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800\"\n (click)=\"toggleExpand(node.item)\"\n [attr.aria-label]=\"isExpanded(node.item) ? 'Collapse node' : 'Expand node'\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\">\n @if (isExpanded(node.item)) {\n <path d=\"M19 12H5\" />\n } @else {\n <path d=\"M12 5v14M5 12h14\" />\n }\n </svg>\n </button>\n } @else {\n <span class=\"mt-1 h-6 w-6\"></span>\n }\n\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(node.item)\"\n (change)=\"toggleCheck(node.item, ($any($event.target)).checked)\"\n [disabled]=\"node.item.disabled\"\n class=\"mt-1.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select-nested\"\n [checked]=\"isSelected(node.item)\"\n (change)=\"toggleSelected(node.item, ($any($event.target)).checked)\"\n [disabled]=\"node.item.disabled\"\n class=\"mt-1.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n <button\n type=\"button\"\n class=\"min-w-0 flex-1 text-left\"\n data-lv-row-btn=\"1\"\n [disabled]=\"node.item.disabled\"\n (click)=\"onItemActivate(node.item)\"\n >\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ node.item.title }}</p>\n @if (node.item.subtitle || node.item.description) {\n <p class=\"mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400\">\n {{ node.item.subtitle || node.item.description }}\n </p>\n }\n </button>\n\n @if (node.item.badge) {\n <span class=\"rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ node.item.badge }}\n </span>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isGrouped()) {\n <div class=\"max-h-[420px] overflow-auto\" (scroll)=\"onContainerScroll($event)\">\n @if (groupedItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n <div>\n @for (group of groupedItems(); track group.label) {\n <section>\n <header\n [ngClass]=\"stickyGroupHeaders() ? 'sticky top-0 z-10 bg-gray-50/90 dark:bg-gray-950/90 backdrop-blur' : 'bg-gray-50 dark:bg-gray-950/70'\"\n class=\"border-y border-gray-200/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:border-gray-700/80 dark:text-gray-400\"\n >\n {{ group.label }}\n </header>\n\n @for (item of group.items; track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3 border-b border-gray-100/80 dark:border-gray-800',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n >\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta }}</span>\n }\n </div>\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n </button>\n </div>\n }\n </section>\n }\n </div>\n }\n </div>\n } @else {\n <div\n class=\"overflow-auto\"\n [style.height.px]=\"(isVirtualized() || isScrollingMode()) ? viewportHeight() : null\"\n [style.max-height.px]=\"!isVirtualized() && !isScrollingMode() ? viewportHeight() : null\"\n (scroll)=\"onContainerScroll($event)\"\n >\n @if (renderItems().length === 0) {\n <div class=\"px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400\">{{ emptyMessage() }}</div>\n } @else {\n @if (isVirtualized()) {\n <div [style.height.px]=\"totalVirtualHeight()\" class=\"relative\">\n <div [style.height.px]=\"topSpacerHeight()\"></div>\n\n @for (item of renderItems(); track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3 border-b border-gray-100/80 dark:border-gray-800',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n isSelected(item) ? 'bg-primary-100/60 dark:bg-primary-900/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n [style.height.px]=\"itemHeight()\"\n >\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(item)\"\n (change)=\"toggleCheck(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select\"\n [checked]=\"isSelected(item)\"\n (change)=\"toggleSelected(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta || item.timestamp) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta || item.timestamp }}</span>\n }\n </div>\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 truncate text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n </button>\n </div>\n }\n\n <div [style.height.px]=\"bottomSpacerHeight()\"></div>\n </div>\n } @else {\n <div class=\"divide-y divide-gray-100 dark:divide-gray-800\">\n @for (item of renderItems(); track trackById($index, item)) {\n <div\n [ngClass]=\"[\n rowPadding(),\n 'flex items-start gap-3',\n item.unread ? 'bg-primary-50/45 dark:bg-primary-950/25' : '',\n isSelected(item) ? 'bg-primary-100/60 dark:bg-primary-900/25' : '',\n item.disabled ? 'opacity-50' : 'hover:bg-gray-50/70 dark:hover:bg-gray-800/55'\n ]\"\n >\n @if (isCheckable()) {\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(item)\"\n (change)=\"toggleCheck(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (!isCheckable() && isSelectable()) {\n <input\n [type]=\"selectionMode() === 'single' ? 'radio' : 'checkbox'\"\n name=\"list-view-select\"\n [checked]=\"isSelected(item)\"\n (change)=\"toggleSelected(item, ($any($event.target)).checked)\"\n [disabled]=\"item.disabled\"\n class=\"mt-1 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800\"\n />\n }\n\n @if (isCallHistoryMode()) {\n <span [ngClass]=\"['mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-800', callTypeClass(item.callType)]\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path [attr.d]=\"callTypeIcon(item.callType)\" />\n </svg>\n </span>\n } @else if (item.avatarUrl) {\n <img [src]=\"item.avatarUrl\" [alt]=\"item.title\" class=\"h-9 w-9 rounded-xl object-cover\" loading=\"lazy\" />\n } @else {\n <span class=\"inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gray-100 text-xs font-bold text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ initials(item) }}\n </span>\n }\n\n <button type=\"button\" class=\"min-w-0 flex-1 text-left\" data-lv-row-btn=\"1\" [disabled]=\"item.disabled\" (click)=\"onItemActivate(item)\">\n <div class=\"flex items-center justify-between gap-2\">\n <p class=\"truncate text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ item.title }}</p>\n @if (item.meta || item.timestamp) {\n <span class=\"shrink-0 text-xs text-gray-500 dark:text-gray-400\">{{ item.meta || item.timestamp }}</span>\n }\n </div>\n\n @if (isTemplateMode()) {\n @if (item.description) {\n <p class=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">{{ item.description }}</p>\n }\n @if (item.tags?.length) {\n <div class=\"mt-2 flex flex-wrap gap-1.5\">\n @for (tag of item.tags; track tag) {\n <span class=\"rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300\">\n {{ tag }}\n </span>\n }\n </div>\n }\n } @else {\n @if (item.subtitle || item.description) {\n <p class=\"mt-1 truncate text-xs text-gray-500 dark:text-gray-400\">{{ item.subtitle || item.description }}</p>\n }\n }\n </button>\n\n <div class=\"flex shrink-0 flex-col items-end gap-1.5\">\n @if (item.badge) {\n <span class=\"rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-900/35 dark:text-primary-300\">\n {{ item.badge }}\n </span>\n }\n @if (item.duration || item.amount) {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">{{ item.duration || item.amount }}</span>\n }\n </div>\n\n @if (hasQuickActions()) {\n <div class=\"hidden shrink-0 items-center gap-1 sm:flex\">\n @for (action of quickActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onQuickAction(item, action); $event.stopPropagation()\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-[11px] font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n [attr.aria-label]=\"action.label\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n </div>\n\n @if (hasQuickActions()) {\n <div class=\"mt-2 flex flex-wrap gap-1 sm:hidden\">\n @for (action of quickActions(); track action.id) {\n <button\n type=\"button\"\n (click)=\"onQuickAction(item, action); $event.stopPropagation()\"\n [ngClass]=\"[\n 'inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-[11px] font-medium transition-colors',\n actionToneClass(action.tone)\n ]\"\n [attr.aria-label]=\"action.label\"\n >\n @if (action.iconPath) {\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\n <path [attr.d]=\"action.iconPath\" />\n </svg>\n }\n {{ action.label }}\n </button>\n }\n </div>\n }\n }\n </div>\n }\n }\n\n @if ((isScrollingMode() || infiniteScroll()) && (loadingMore() || hasMore())) {\n <div class=\"border-t border-gray-200/70 px-4 py-3 text-center dark:border-gray-700/80\">\n @if (loadingMore()) {\n <span class=\"inline-flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400\">\n <svg class=\"h-4 w-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"2\" opacity=\"0.2\"></circle>\n <path d=\"M12 3a9 9 0 0 1 9 9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"></path>\n </svg>\n Loading more items...\n </span>\n } @else {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">Scroll to load more</span>\n }\n </div>\n }\n </div>\n }\n\n @if (isCheckable()) {\n <div class=\"border-t border-gray-200/80 px-4 py-2 text-xs text-gray-500 dark:border-gray-700/70 dark:text-gray-400\">\n {{ checkedCount() }} checked\n </div>\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@styloviz/list-view",
3
+ "version": "0.1.1",
4
+ "description": "Market-ready list patterns: default, checklist, nested tree, virtualized, grouped, template cards, call history and infinite scrolling.",
5
+ "license": "MIT",
6
+ "author": "sazzad-bs23",
7
+ "homepage": "https://styloviz.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
11
+ "directory": "projects/list-view-free"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
15
+ },
16
+ "peerDependencies": {
17
+ "@angular/common": ">=21.0.0",
18
+ "@angular/core": ">=21.0.0",
19
+ "@angular/forms": ">=21.0.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "engines": {
25
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
26
+ },
27
+ "sideEffects": false,
28
+ "module": "fesm2022/styloviz-list-view.mjs",
29
+ "typings": "types/styloviz-list-view.d.ts",
30
+ "exports": {
31
+ "./package.json": {
32
+ "default": "./package.json"
33
+ },
34
+ ".": {
35
+ "types": "./types/styloviz-list-view.d.ts",
36
+ "default": "./fesm2022/styloviz-list-view.mjs"
37
+ }
38
+ },
39
+ "type": "module",
40
+ "dependencies": {
41
+ "tslib": "^2.3.0"
42
+ }
43
+ }
@@ -0,0 +1,210 @@
1
+ import * as _angular_core from '@angular/core';
2
+
3
+ type ListViewMode = 'default' | 'checklist' | 'nested' | 'virtualization' | 'scrolling' | 'grouped' | 'template' | 'call-history';
4
+ type ListViewVariant = 'default' | 'soft' | 'outlined';
5
+ type ListViewDensity = 'compact' | 'comfortable' | 'spacious';
6
+ type ListViewSelectionMode = 'none' | 'single' | 'multiple';
7
+ type ListViewCallType = 'incoming' | 'outgoing' | 'missed' | 'video';
8
+ type ListViewActionTone = 'neutral' | 'primary' | 'danger';
9
+ interface ListViewItem {
10
+ id: string | number;
11
+ title: string;
12
+ subtitle?: string;
13
+ description?: string;
14
+ group?: string;
15
+ badge?: string;
16
+ meta?: string;
17
+ timestamp?: string;
18
+ duration?: string;
19
+ avatarUrl?: string;
20
+ iconPath?: string;
21
+ tags?: string[];
22
+ amount?: string;
23
+ callType?: ListViewCallType;
24
+ unread?: boolean;
25
+ disabled?: boolean;
26
+ children?: ListViewItem[];
27
+ }
28
+ interface ListViewItemClickEvent {
29
+ item: ListViewItem;
30
+ }
31
+ interface ListViewCheckChangeEvent {
32
+ item: ListViewItem;
33
+ checked: boolean;
34
+ }
35
+ interface ListViewLoadMoreEvent {
36
+ visibleCount: number;
37
+ totalCount: number;
38
+ }
39
+ interface ListViewQuickAction {
40
+ id: string;
41
+ label: string;
42
+ iconPath?: string;
43
+ tone?: ListViewActionTone;
44
+ }
45
+ interface ListViewQuickActionClickEvent {
46
+ item: ListViewItem;
47
+ action: ListViewQuickAction;
48
+ }
49
+ interface ListViewBulkAction {
50
+ id: string;
51
+ label: string;
52
+ iconPath?: string;
53
+ tone?: ListViewActionTone;
54
+ }
55
+ interface ListViewBulkActionClickEvent {
56
+ action: ListViewBulkAction;
57
+ selectedIds: Set<string | number>;
58
+ }
59
+ interface ListViewGroup {
60
+ label: string;
61
+ items: ListViewItem[];
62
+ }
63
+ interface FlattenedItem {
64
+ item: ListViewItem;
65
+ depth: number;
66
+ hasChildren: boolean;
67
+ }
68
+ declare class SvListViewComponent {
69
+ /** Items to render (label, meta, optional children/avatar/actions). */
70
+ items: _angular_core.InputSignal<ListViewItem[]>;
71
+ /** Preset pattern: default, checklist, nested, card, etc. @default 'default' */
72
+ mode: _angular_core.InputSignal<ListViewMode>;
73
+ /** Visual variant. @default 'default' */
74
+ variant: _angular_core.InputSignal<ListViewVariant>;
75
+ /** Row density. @default 'comfortable' */
76
+ density: _angular_core.InputSignal<ListViewDensity>;
77
+ /** Selection behaviour: none, single or multiple. @default 'none' */
78
+ selectionMode: _angular_core.InputSignal<ListViewSelectionMode>;
79
+ /** Show checkboxes on rows. @default false */
80
+ checkable: _angular_core.InputSignal<boolean>;
81
+ /** Enable nested (tree) rows. @default false */
82
+ nested: _angular_core.InputSignal<boolean>;
83
+ /** Virtualize long lists for performance. @default false */
84
+ virtualized: _angular_core.InputSignal<boolean>;
85
+ /** Group rows under section headers. @default false */
86
+ grouped: _angular_core.InputSignal<boolean>;
87
+ /** Show a search box to filter items. @default false */
88
+ searchable: _angular_core.InputSignal<boolean>;
89
+ /** Keep group headers pinned while scrolling. @default true */
90
+ stickyGroupHeaders: _angular_core.InputSignal<boolean>;
91
+ /** Load more items as the user scrolls to the end. @default false */
92
+ infiniteScroll: _angular_core.InputSignal<boolean>;
93
+ /** Whether more items are available to load. @default false */
94
+ hasMore: _angular_core.InputSignal<boolean>;
95
+ /** Show a loading indicator while fetching more. @default false */
96
+ loadingMore: _angular_core.InputSignal<boolean>;
97
+ /** Viewport height (px) for the virtualized list. @default 360 */
98
+ viewportHeight: _angular_core.InputSignal<number>;
99
+ /** Estimated row height (px) for virtualization. @default 68 */
100
+ itemHeight: _angular_core.InputSignal<number>;
101
+ /** Extra rows rendered above/below the viewport. @default 5 */
102
+ overscan: _angular_core.InputSignal<number>;
103
+ /** Placeholder for the search box. @default 'Search list items' */
104
+ searchPlaceholder: _angular_core.InputSignal<string>;
105
+ /** Message shown when no items match. @default 'No items match your filters.' */
106
+ emptyMessage: _angular_core.InputSignal<string>;
107
+ /** Extra CSS classes for the root. */
108
+ customClass: _angular_core.InputSignal<string>;
109
+ /** Accessible label for the list. @default 'List view' */
110
+ ariaLabel: _angular_core.InputSignal<string>;
111
+ /** Announce row actions to screen readers. @default true */
112
+ announceActions: _angular_core.InputSignal<boolean>;
113
+ /** Show per-row quick actions on hover/focus. @default false */
114
+ showQuickActions: _angular_core.InputSignal<boolean>;
115
+ /** Per-row quick actions. */
116
+ quickActions: _angular_core.InputSignal<ListViewQuickAction[]>;
117
+ /** Bulk actions for checked rows. */
118
+ bulkActions: _angular_core.InputSignal<ListViewBulkAction[]>;
119
+ /** Enable keyboard hotkeys for bulk actions. @default false */
120
+ enableBulkHotkeys: _angular_core.InputSignal<boolean>;
121
+ /** Map of hotkey → bulk action id. */
122
+ bulkHotkeys: _angular_core.InputSignal<Record<string, string>>;
123
+ /** Enable roving-tabindex keyboard navigation. @default true */
124
+ enableRovingFocus: _angular_core.InputSignal<boolean>;
125
+ /** Selected row ids (two-way). */
126
+ selectedIds: _angular_core.ModelSignal<Set<string | number>>;
127
+ /** Checked row ids (two-way). */
128
+ checkedIds: _angular_core.ModelSignal<Set<string | number>>;
129
+ /** Emitted with the clicked item. */
130
+ itemClick: _angular_core.OutputEmitterRef<ListViewItemClickEvent>;
131
+ /** Emitted with the current selection set. */
132
+ selectionChange: _angular_core.OutputEmitterRef<Set<string | number>>;
133
+ /** Emitted when a row's checkbox changes. */
134
+ checkChange: _angular_core.OutputEmitterRef<ListViewCheckChangeEvent>;
135
+ /** Emitted when more items should be loaded (infinite scroll). */
136
+ loadMore: _angular_core.OutputEmitterRef<ListViewLoadMoreEvent>;
137
+ /** Emitted when a quick action is clicked. */
138
+ quickActionClick: _angular_core.OutputEmitterRef<ListViewQuickActionClickEvent>;
139
+ /** Emitted when a bulk action is triggered. */
140
+ bulkActionClick: _angular_core.OutputEmitterRef<ListViewBulkActionClickEvent>;
141
+ readonly query: _angular_core.WritableSignal<string>;
142
+ readonly liveAnnouncement: _angular_core.WritableSignal<string>;
143
+ private readonly host;
144
+ private readonly scrollTop;
145
+ private readonly expandedIds;
146
+ private readonly loadingGuard;
147
+ readonly isCheckable: _angular_core.Signal<boolean>;
148
+ readonly isSelectable: _angular_core.Signal<boolean>;
149
+ readonly selectedCount: _angular_core.Signal<number>;
150
+ readonly bulkSelectedIds: _angular_core.Signal<Set<string | number>>;
151
+ readonly bulkSelectedCount: _angular_core.Signal<number>;
152
+ readonly hasQuickActions: _angular_core.Signal<boolean>;
153
+ readonly isNested: _angular_core.Signal<boolean>;
154
+ readonly isVirtualized: _angular_core.Signal<boolean>;
155
+ readonly isGrouped: _angular_core.Signal<boolean>;
156
+ readonly isTemplateMode: _angular_core.Signal<boolean>;
157
+ readonly isCallHistoryMode: _angular_core.Signal<boolean>;
158
+ readonly isScrollingMode: _angular_core.Signal<boolean>;
159
+ readonly filteredItems: _angular_core.Signal<ListViewItem[]>;
160
+ readonly flattenedNestedItems: _angular_core.Signal<FlattenedItem[]>;
161
+ readonly groupedItems: _angular_core.Signal<ListViewGroup[]>;
162
+ readonly flatItems: _angular_core.Signal<ListViewItem[]>;
163
+ readonly totalVirtualHeight: _angular_core.Signal<number>;
164
+ readonly virtualRange: _angular_core.Signal<{
165
+ start: number;
166
+ end: number;
167
+ }>;
168
+ readonly virtualItems: _angular_core.Signal<ListViewItem[]>;
169
+ readonly topSpacerHeight: _angular_core.Signal<number>;
170
+ readonly bottomSpacerHeight: _angular_core.Signal<number>;
171
+ readonly renderItems: _angular_core.Signal<ListViewItem[]>;
172
+ readonly allVisibleIds: _angular_core.Signal<(string | number)[]>;
173
+ readonly allChecked: _angular_core.Signal<boolean>;
174
+ readonly checkedCount: _angular_core.Signal<number>;
175
+ readonly visibleItemCount: _angular_core.Signal<number>;
176
+ readonly rootClasses: _angular_core.Signal<string>;
177
+ readonly rowPadding: _angular_core.Signal<string>;
178
+ onSearchInput(value: string): void;
179
+ onContainerScroll(event: Event): void;
180
+ onItemActivate(item: ListViewItem): void;
181
+ toggleCheck(item: ListViewItem, checked: boolean): void;
182
+ toggleAllChecks(checked: boolean): void;
183
+ toggleExpand(item: ListViewItem): void;
184
+ isExpanded(item: ListViewItem): boolean;
185
+ isSelected(item: ListViewItem): boolean;
186
+ isChecked(item: ListViewItem): boolean;
187
+ toggleSelected(item: ListViewItem, checked: boolean): void;
188
+ onQuickAction(item: ListViewItem, action: ListViewQuickAction): void;
189
+ onBulkAction(action: ListViewBulkAction): void;
190
+ onRootKeydown(event: KeyboardEvent): void;
191
+ private handleRovingFocusKey;
192
+ private rowButtons;
193
+ trackById(_: number, item: ListViewItem): string | number;
194
+ trackByNested(_: number, node: FlattenedItem): string | number;
195
+ callTypeIcon(type: ListViewCallType | undefined): string;
196
+ callTypeClass(type: ListViewCallType | undefined): string;
197
+ initials(item: ListViewItem): string;
198
+ nestedIndent(depth: number): string;
199
+ actionToneClass(tone: ListViewActionTone | undefined): string;
200
+ private announce;
201
+ private itemMatches;
202
+ private filterNested;
203
+ private flattenTree;
204
+ private countTreeItems;
205
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvListViewComponent, never>;
206
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvListViewComponent, "sv-list-view", never, { "items": { "alias": "items"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "density": { "alias": "density"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "checkable": { "alias": "checkable"; "required": false; "isSignal": true; }; "nested": { "alias": "nested"; "required": false; "isSignal": true; }; "virtualized": { "alias": "virtualized"; "required": false; "isSignal": true; }; "grouped": { "alias": "grouped"; "required": false; "isSignal": true; }; "searchable": { "alias": "searchable"; "required": false; "isSignal": true; }; "stickyGroupHeaders": { "alias": "stickyGroupHeaders"; "required": false; "isSignal": true; }; "infiniteScroll": { "alias": "infiniteScroll"; "required": false; "isSignal": true; }; "hasMore": { "alias": "hasMore"; "required": false; "isSignal": true; }; "loadingMore": { "alias": "loadingMore"; "required": false; "isSignal": true; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; "isSignal": true; }; "itemHeight": { "alias": "itemHeight"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "customClass": { "alias": "customClass"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "announceActions": { "alias": "announceActions"; "required": false; "isSignal": true; }; "showQuickActions": { "alias": "showQuickActions"; "required": false; "isSignal": true; }; "quickActions": { "alias": "quickActions"; "required": false; "isSignal": true; }; "bulkActions": { "alias": "bulkActions"; "required": false; "isSignal": true; }; "enableBulkHotkeys": { "alias": "enableBulkHotkeys"; "required": false; "isSignal": true; }; "bulkHotkeys": { "alias": "bulkHotkeys"; "required": false; "isSignal": true; }; "enableRovingFocus": { "alias": "enableRovingFocus"; "required": false; "isSignal": true; }; "selectedIds": { "alias": "selectedIds"; "required": false; "isSignal": true; }; "checkedIds": { "alias": "checkedIds"; "required": false; "isSignal": true; }; }, { "selectedIds": "selectedIdsChange"; "checkedIds": "checkedIdsChange"; "itemClick": "itemClick"; "selectionChange": "selectionChange"; "checkChange": "checkChange"; "loadMore": "loadMore"; "quickActionClick": "quickActionClick"; "bulkActionClick": "bulkActionClick"; }, never, never, true, never>;
207
+ }
208
+
209
+ export { SvListViewComponent };
210
+ export type { ListViewActionTone, ListViewBulkAction, ListViewBulkActionClickEvent, ListViewCallType, ListViewDensity, ListViewItem, ListViewMode, ListViewQuickAction, ListViewQuickActionClickEvent, ListViewSelectionMode, ListViewVariant };