@uxland/primary-shell 6.0.5 → 6.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ export interface VirtualScrollerOptions<T> {
2
+ items: T[];
3
+ estimatedItemHeight: number;
4
+ bufferSize: number;
5
+ }
6
+ export interface VirtualScrollerState {
7
+ viewportHeight: number;
8
+ scrollTop: number;
9
+ totalHeight: number;
10
+ visibleRange: {
11
+ start: number;
12
+ end: number;
13
+ };
14
+ containerOffset: number;
15
+ visibleItems: any[];
16
+ }
17
+ export declare class VirtualScroller<T = any> {
18
+ private _itemHeights;
19
+ private _itemOffsets;
20
+ private _totalHeight;
21
+ private _viewportHeight;
22
+ private _scrollTop;
23
+ private _visibleRange;
24
+ private _options;
25
+ private _viewport?;
26
+ private _content?;
27
+ private _resizeObserver?;
28
+ private _isScrolling;
29
+ private _scrollTimeout?;
30
+ constructor(options: VirtualScrollerOptions<T>);
31
+ private _initializeHeights;
32
+ setup(viewport: HTMLElement, content: HTMLElement, onStateChange: (state: VirtualScrollerState) => void): () => void;
33
+ updateItems(items: T[]): void;
34
+ private _measureActualHeights;
35
+ private _updateVisibleRange;
36
+ scrollToIndex(index: number, behavior?: ScrollBehavior): void;
37
+ getState(): VirtualScrollerState;
38
+ findClosestIndex<K>(getValue: (item: T) => K, targetValue: K, compare: (a: K, b: K) => number): number;
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxland/primary-shell",
3
- "version": "6.0.5",
3
+ "version": "6.0.6",
4
4
  "description": "Primaria Shell",
5
5
  "author": "UXLand <dev@uxland.es>",
6
6
  "homepage": "https://github.com/uxland/harmonix/tree/app#readme",
@@ -8,8 +8,7 @@ import styles from "./styles.css?inline";
8
8
  import { template } from "./template";
9
9
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
10
10
  import { isSomeCollectionLoadedAndOtherLoading, isCollectionInitialState } from "../../../add/handle-busy/selectors";
11
- import { virtualizerRef } from "@lit-labs/virtualizer/virtualize.js";
12
- import "@lit-labs/virtualizer";
11
+ import { VirtualScroller, VirtualScrollerState } from "./virtual-scroller";
13
12
 
14
13
  const createZIndexStyles = (max: number) => {
15
14
  let styles = "";
@@ -50,11 +49,27 @@ export class ActivityHistoryTimeline extends LitElement {
50
49
  @connectedProperty(activityHistorySearchStringSelector)
51
50
  searchString: string;
52
51
 
53
- @query(".virtualizer") private _virtualizer!: HTMLElement;
52
+ @query(".virtual-container") private _virtualContainer!: HTMLElement;
53
+ @query(".virtual-viewport") private _viewport!: HTMLElement;
54
+ @query(".virtual-content") private _content!: HTMLElement;
54
55
 
55
56
  @state()
56
57
  renderGroupsControlTimeExpired = false;
57
58
 
59
+ // Virtual scroller instance and state
60
+ private _virtualScroller?: VirtualScroller<IActivityHistoryGroup>;
61
+ private _cleanupVirtualScroller?: () => void;
62
+
63
+ @state()
64
+ _virtualScrollerState: VirtualScrollerState = {
65
+ viewportHeight: 0,
66
+ scrollTop: 0,
67
+ totalHeight: 0,
68
+ visibleRange: { start: 0, end: 0 },
69
+ containerOffset: 0,
70
+ visibleItems: []
71
+ };
72
+
58
73
  connectedCallback(): void {
59
74
  super.connectedCallback();
60
75
  setTimeout(() => {
@@ -64,12 +79,19 @@ export class ActivityHistoryTimeline extends LitElement {
64
79
 
65
80
  disconnectedCallback(): void {
66
81
  super.disconnectedCallback();
82
+ this._cleanupVirtualScroller?.();
67
83
  }
68
84
 
69
85
  async updated(changedProperties) {
70
86
  if (changedProperties.has("historyGroups") && this.historyGroups?.length) {
71
87
  // Resolver los elementos antes de actualizar el template
72
88
  await this.prepareComponents(this.historyGroups);
89
+ this._updateVirtualScroller();
90
+ }
91
+
92
+ // Setup virtualizer when DOM elements are available
93
+ if (this._viewport && this._content && !this._virtualScroller) {
94
+ setTimeout(() => this._setupVirtualScroller(), 0);
73
95
  }
74
96
  }
75
97
 
@@ -93,7 +115,6 @@ export class ActivityHistoryTimeline extends LitElement {
93
115
  );
94
116
  }
95
117
  }
96
- this.requestUpdate(); // 🔥 Forzar actualización del template
97
118
  }
98
119
 
99
120
  highlightMatch(text: string, searchString: string): string {
@@ -106,29 +127,70 @@ export class ActivityHistoryTimeline extends LitElement {
106
127
  return text ? unsafeHTML(this.highlightMatch(text, this.searchString)) : null;
107
128
  }
108
129
 
130
+ // Public API for scrolling to date (maintains original functionality)
109
131
  _scrollIntoDate(selectedDate: number) {
110
- let closestIndex = -1;
111
- let closestDifference = Number.POSITIVE_INFINITY;
112
-
113
- (this.historyGroups ?? []).forEach((group, index) => {
114
- const dateStr = group.items?.[0]?.date || group.subGroups?.[0]?.items?.[0]?.date;
115
- if (!dateStr) return;
132
+ if (!this._virtualScroller) return;
133
+
134
+ const closestIndex = this._virtualScroller.findClosestIndex(
135
+ (group) => {
136
+ const dateStr = group.items?.[0]?.date || group.subGroups?.[0]?.items?.[0]?.date;
137
+ return dateStr ? new Date(dateStr).getTime() : 0;
138
+ },
139
+ selectedDate,
140
+ (a, b) => a - b
141
+ );
142
+
143
+ if (closestIndex !== -1) {
144
+ this._virtualScroller.scrollToIndex(closestIndex);
145
+ }
146
+ }
116
147
 
117
- const groupDate = new Date(dateStr).getTime();
118
- const difference = Math.abs(groupDate - selectedDate);
148
+ // === VIRTUAL SCROLLER LOGIC ===
149
+
150
+ private _setupVirtualScroller() {
151
+ if (!this._viewport || !this._content || this._virtualScroller) return;
152
+
153
+ this._virtualScroller = new VirtualScroller({
154
+ items: this.historyGroups || [],
155
+ estimatedItemHeight: 200,
156
+ bufferSize: 50 // 50% buffer
157
+ });
119
158
 
120
- if (difference < closestDifference) {
121
- closestDifference = difference;
122
- closestIndex = index;
159
+ this._cleanupVirtualScroller = this._virtualScroller.setup(
160
+ this._viewport,
161
+ this._content,
162
+ (state) => {
163
+ this._virtualScrollerState = state;
164
+ this.requestUpdate();
123
165
  }
124
- });
166
+ );
125
167
 
126
- if (closestIndex !== -1 && this._virtualizer) {
127
- this._virtualizer[virtualizerRef].element(closestIndex).scrollIntoView({
128
- block: "nearest",
129
- inline: "nearest",
130
- behavior: "smooth",
131
- });
132
- }
168
+ // Get initial state
169
+ this._virtualScrollerState = this._virtualScroller.getState();
170
+ }
171
+
172
+ private _updateVirtualScroller() {
173
+ if (!this._virtualScroller || !this.historyGroups?.length) return;
174
+
175
+ this._virtualScroller.updateItems(this.historyGroups);
176
+ this._virtualScrollerState = this._virtualScroller.getState();
177
+ }
178
+
179
+ // Getters for template (maintaining original API)
180
+ get _visibleItems() {
181
+ return this._virtualScrollerState.visibleItems;
133
182
  }
134
- }
183
+
184
+ get _containerOffset() {
185
+ return this._virtualScrollerState.containerOffset;
186
+ }
187
+
188
+ get _visibleRange() {
189
+ return this._virtualScrollerState.visibleRange;
190
+ }
191
+
192
+ // Scroll handler for template
193
+ _onScroll = () => {
194
+ // Virtual scroller handles this internally
195
+ };
196
+ }
@@ -5,11 +5,26 @@
5
5
  flex: 1 1 0%;
6
6
  }
7
7
 
8
- .virtualizer {
8
+ .virtual-container {
9
9
  height: 100%;
10
+ position: relative;
11
+ }
12
+
13
+ .virtual-viewport {
14
+ height: 100%;
15
+ overflow: auto;
10
16
  padding-inline: 16px;
11
17
  }
12
18
 
19
+ .virtual-content {
20
+ position: relative;
21
+ width: 100%;
22
+ }
23
+
24
+ .virtual-items {
25
+ position: relative;
26
+ }
27
+
13
28
  .container {
14
29
  min-height: 1px;
15
30
  height: 100%;
@@ -27,53 +27,64 @@ export const template = (props: ActivityHistoryTimeline) => {
27
27
  ${
28
28
  props.historyGroups?.length > 0
29
29
  ? html`
30
- <lit-virtualizer class="virtualizer"
31
- ?scroller=${true}
32
- .items=${props.historyGroups}
33
- .renderItem=${(itemGroup: IActivityHistoryGroup, index: number) => html`
34
- <div class="visit zIndex${props.historyGroups.length - index}"
35
- data-date=${ifDefined(itemGroup?.items[0]?.date || itemGroup?.subGroups[0]?.items[0]?.date)}>
36
- ${visitHeaderTemplate(props, itemGroup?.items[0] || itemGroup?.subGroups[0]?.items[0])}
37
- <div class="visit__items">
30
+ <div class="virtual-container">
31
+ <div class="virtual-viewport" @scroll=${props._onScroll}>
32
+ <div class="virtual-content">
33
+ <div class="virtual-items" style="transform: translateY(${props._containerOffset}px)">
38
34
  ${repeat(
39
- mergeHistoryItemsAndSubgroups(itemGroup),
40
- (entry) => (entry.type === "item" ? entry.item.id : entry.subGroup.id),
41
- (entry: ActivityHistoryEntry) => {
42
- if (entry.type === "item") {
43
- const item = entry.item;
44
- return html`
45
- <div
46
- class="item"
47
- ?has-divider=${hasItemDivider(item, itemGroup?.items as IActivityHistoryItemWithComponent[])}
48
- >
49
- ${item.component}
50
- </div>
51
- `;
52
- }
53
- const subGroup = entry.subGroup;
35
+ props._visibleItems,
36
+ (itemGroup, index) => itemGroup?.items?.[0]?.id || itemGroup?.subGroups?.[0]?.items?.[0]?.id,
37
+ (itemGroup: IActivityHistoryGroup, localIndex: number) => {
38
+ const globalIndex = props._visibleRange.start + localIndex;
54
39
  return html`
55
- <div class="diagnostics">
56
- ${diagnosticHeaderTemplate(subGroup?.items[0])}
57
- <div class="diagnostics__items">
40
+ <div class="visit zIndex${props.historyGroups.length - globalIndex}"
41
+ data-date=${ifDefined(itemGroup?.items[0]?.date || itemGroup?.subGroups[0]?.items[0]?.date)}>
42
+ ${visitHeaderTemplate(props, itemGroup?.items[0] || itemGroup?.subGroups[0]?.items[0])}
43
+ <div class="visit__items">
58
44
  ${repeat(
59
- subGroup?.items,
60
- (item) => item.id,
61
- (item: IActivityHistoryItemWithComponent) => html`
62
- <div class="item"
63
- ?has-divider=${hasItemDivider(item, subGroup?.items as IActivityHistoryItemWithComponent[])}>
64
- ${item.component}
65
- </div>
66
- `,
45
+ mergeHistoryItemsAndSubgroups(itemGroup),
46
+ (entry) => (entry.type === "item" ? entry.item.id : entry.subGroup.id),
47
+ (entry: ActivityHistoryEntry) => {
48
+ if (entry.type === "item") {
49
+ const item = entry.item;
50
+ return html`
51
+ <div
52
+ class="item"
53
+ ?has-divider=${hasItemDivider(item, itemGroup?.items as IActivityHistoryItemWithComponent[])}
54
+ >
55
+ ${item.component}
56
+ </div>
57
+ `;
58
+ }
59
+ const subGroup = entry.subGroup;
60
+ return html`
61
+ <div class="diagnostics">
62
+ ${diagnosticHeaderTemplate(subGroup?.items[0])}
63
+ <div class="diagnostics__items">
64
+ ${repeat(
65
+ subGroup?.items,
66
+ (item) => item.id,
67
+ (item: IActivityHistoryItemWithComponent) => html`
68
+ <div class="item"
69
+ ?has-divider=${hasItemDivider(item, subGroup?.items as IActivityHistoryItemWithComponent[])}>
70
+ ${item.component}
71
+ </div>
72
+ `,
73
+ )}
74
+ </div>
75
+ </div>
76
+ `;
77
+ },
67
78
  )}
68
79
  </div>
69
80
  </div>
70
81
  `;
71
- },
82
+ }
72
83
  )}
73
84
  </div>
74
85
  </div>
75
- `}
76
- ></lit-virtualizer>
86
+ </div>
87
+ </div>
77
88
  `
78
89
  : noGroupsTemplate(props)
79
90
  }
@@ -0,0 +1,239 @@
1
+ export interface VirtualScrollerOptions<T> {
2
+ items: T[];
3
+ estimatedItemHeight: number;
4
+ bufferSize: number; // percentage of viewport height
5
+ }
6
+
7
+ export interface VirtualScrollerState {
8
+ viewportHeight: number;
9
+ scrollTop: number;
10
+ totalHeight: number;
11
+ visibleRange: { start: number; end: number };
12
+ containerOffset: number;
13
+ visibleItems: any[];
14
+ }
15
+
16
+ export class VirtualScroller<T = any> {
17
+ private _itemHeights: Map<number, number> = new Map();
18
+ private _itemOffsets: Map<number, number> = new Map();
19
+ private _totalHeight = 0;
20
+ private _viewportHeight = 0;
21
+ private _scrollTop = 0;
22
+ private _visibleRange = { start: 0, end: 0 };
23
+ private _options: VirtualScrollerOptions<T>;
24
+
25
+ private _viewport?: HTMLElement;
26
+ private _content?: HTMLElement;
27
+ private _resizeObserver?: ResizeObserver;
28
+ private _isScrolling = false;
29
+ private _scrollTimeout?: number;
30
+
31
+ constructor(options: VirtualScrollerOptions<T>) {
32
+ this._options = options;
33
+ this._initializeHeights();
34
+ }
35
+
36
+ // Initialize with estimated heights
37
+ private _initializeHeights() {
38
+ const { items, estimatedItemHeight } = this._options;
39
+ let totalHeight = 0;
40
+
41
+ this._itemHeights.clear();
42
+ this._itemOffsets.clear();
43
+
44
+ for (let i = 0; i < items.length; i++) {
45
+ this._itemOffsets.set(i, totalHeight);
46
+ this._itemHeights.set(i, estimatedItemHeight);
47
+ totalHeight += estimatedItemHeight;
48
+ }
49
+
50
+ this._totalHeight = totalHeight;
51
+ }
52
+
53
+ // Setup DOM references and observers
54
+ setup(viewport: HTMLElement, content: HTMLElement, onStateChange: (state: VirtualScrollerState) => void) {
55
+ this._viewport = viewport;
56
+ this._content = content;
57
+
58
+ // Update viewport height
59
+ this._viewportHeight = viewport.clientHeight;
60
+
61
+ // Set initial content height
62
+ content.style.height = this._totalHeight + 'px';
63
+
64
+ // Calculate initial visible range
65
+ this._updateVisibleRange();
66
+
67
+ // Setup scroll listener
68
+ const onScroll = () => {
69
+ if (!this._viewport) return;
70
+
71
+ this._scrollTop = this._viewport.scrollTop;
72
+ this._isScrolling = true;
73
+
74
+ if (this._scrollTimeout) {
75
+ clearTimeout(this._scrollTimeout);
76
+ }
77
+
78
+ this._scrollTimeout = window.setTimeout(() => {
79
+ this._isScrolling = false;
80
+ // Measure heights after scrolling stops
81
+ setTimeout(() => {
82
+ this._measureActualHeights();
83
+ onStateChange(this.getState());
84
+ }, 50);
85
+ }, 150);
86
+
87
+ const oldRange = {...this._visibleRange};
88
+ this._updateVisibleRange();
89
+
90
+ // Only update if visible range changed
91
+ if (Math.abs(oldRange.start - this._visibleRange.start) > 0 ||
92
+ Math.abs(oldRange.end - this._visibleRange.end) > 0) {
93
+ onStateChange(this.getState());
94
+ }
95
+ };
96
+
97
+ viewport.addEventListener('scroll', onScroll);
98
+
99
+ // Setup resize observer
100
+ this._resizeObserver = new ResizeObserver(() => {
101
+ this._viewportHeight = viewport.clientHeight;
102
+ this._updateVisibleRange();
103
+ onStateChange(this.getState());
104
+ });
105
+
106
+ this._resizeObserver.observe(viewport);
107
+
108
+ // Return cleanup function
109
+ return () => {
110
+ viewport.removeEventListener('scroll', onScroll);
111
+ this._resizeObserver?.disconnect();
112
+ if (this._scrollTimeout) {
113
+ clearTimeout(this._scrollTimeout);
114
+ }
115
+ };
116
+ }
117
+
118
+ // Update items (when data changes)
119
+ updateItems(items: T[]) {
120
+ this._options.items = items;
121
+ this._initializeHeights();
122
+
123
+ if (this._content) {
124
+ this._content.style.height = this._totalHeight + 'px';
125
+ }
126
+
127
+ this._updateVisibleRange();
128
+ }
129
+
130
+ // Measure actual heights of rendered items
131
+ private _measureActualHeights() {
132
+ if (!this._content || this._isScrolling) return;
133
+
134
+ const visitElements = this._content.querySelectorAll('.visit');
135
+ if (visitElements.length === 0) return;
136
+
137
+ let hasChanges = false;
138
+
139
+ visitElements.forEach((element, index) => {
140
+ const actualHeight = element.getBoundingClientRect().height;
141
+ const globalIndex = this._visibleRange.start + index;
142
+ const currentHeight = this._itemHeights.get(globalIndex) || 0;
143
+
144
+ // Only update if there's a significant difference
145
+ if (Math.abs(actualHeight - currentHeight) > 5) {
146
+ hasChanges = true;
147
+ this._itemHeights.set(globalIndex, actualHeight);
148
+ }
149
+ });
150
+
151
+ if (hasChanges) {
152
+ // Recalculate offsets and total height
153
+ let totalHeight = 0;
154
+ for (let i = 0; i < this._options.items.length; i++) {
155
+ this._itemOffsets.set(i, totalHeight);
156
+ const height = this._itemHeights.get(i) || this._options.estimatedItemHeight;
157
+ totalHeight += height;
158
+ }
159
+
160
+ this._totalHeight = totalHeight;
161
+ if (this._content) {
162
+ this._content.style.height = this._totalHeight + 'px';
163
+ }
164
+ }
165
+ }
166
+
167
+ // Calculate visible range based on scroll position
168
+ private _updateVisibleRange() {
169
+ if (!this._options.items.length || this._viewportHeight === 0) return;
170
+
171
+ const buffer = Math.floor(this._viewportHeight * (this._options.bufferSize / 100));
172
+ const startOffset = Math.max(0, this._scrollTop - buffer);
173
+ const endOffset = this._scrollTop + this._viewportHeight + buffer;
174
+
175
+ let start = 0;
176
+ let end = this._options.items.length - 1;
177
+
178
+ // Find start index
179
+ for (let i = 0; i < this._options.items.length; i++) {
180
+ const offset = this._itemOffsets.get(i) || 0;
181
+ if (offset >= startOffset) {
182
+ start = Math.max(0, i - 1);
183
+ break;
184
+ }
185
+ }
186
+
187
+ // Find end index
188
+ for (let i = start; i < this._options.items.length; i++) {
189
+ const offset = this._itemOffsets.get(i) || 0;
190
+ if (offset > endOffset) {
191
+ end = i;
192
+ break;
193
+ }
194
+ }
195
+
196
+ this._visibleRange = { start, end };
197
+ }
198
+
199
+ // Scroll to specific item index
200
+ scrollToIndex(index: number, behavior: ScrollBehavior = 'smooth') {
201
+ if (!this._viewport) return;
202
+
203
+ const offset = this._itemOffsets.get(index) || 0;
204
+ this._viewport.scrollTo({
205
+ top: offset,
206
+ behavior
207
+ });
208
+ }
209
+
210
+ // Get current state for rendering
211
+ getState(): VirtualScrollerState {
212
+ return {
213
+ viewportHeight: this._viewportHeight,
214
+ scrollTop: this._scrollTop,
215
+ totalHeight: this._totalHeight,
216
+ visibleRange: this._visibleRange,
217
+ containerOffset: this._itemOffsets.get(this._visibleRange.start) || 0,
218
+ visibleItems: this._options.items.slice(this._visibleRange.start, this._visibleRange.end + 1)
219
+ };
220
+ }
221
+
222
+ // Find closest item to a specific condition
223
+ findClosestIndex<K>(getValue: (item: T) => K, targetValue: K, compare: (a: K, b: K) => number): number {
224
+ let closestIndex = -1;
225
+ let closestDifference = Number.POSITIVE_INFINITY;
226
+
227
+ this._options.items.forEach((item, index) => {
228
+ const value = getValue(item);
229
+ const difference = Math.abs(compare(value, targetValue));
230
+
231
+ if (difference < closestDifference) {
232
+ closestDifference = difference;
233
+ closestIndex = index;
234
+ }
235
+ });
236
+
237
+ return closestIndex;
238
+ }
239
+ }