mtrl-addons 0.2.1 → 0.2.2

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.
@@ -40,6 +40,9 @@ interface ViewportState {
40
40
  */
41
41
  export const createViewport = (config: ViewportConfig = {}) => {
42
42
  return <T extends ViewportContext>(component: T): T & ViewportComponent => {
43
+ // Track if viewport has been initialized to prevent multiple initializations
44
+ let isInitialized = false;
45
+
43
46
  // No normalization needed - we'll use the nested structure directly
44
47
 
45
48
  // Create shared viewport state
@@ -59,6 +62,12 @@ export const createViewport = (config: ViewportConfig = {}) => {
59
62
  const viewportAPI = {
60
63
  // Core API
61
64
  initialize: () => {
65
+ // Prevent multiple initializations
66
+ if (isInitialized) {
67
+ return false;
68
+ }
69
+ isInitialized = true;
70
+
62
71
  // console.log("[Viewport] Initializing with state:", {
63
72
  // element: !!component.element,
64
73
  // totalItems: component.totalItems,
@@ -96,12 +105,18 @@ export const createViewport = (config: ViewportConfig = {}) => {
96
105
  // Will be implemented by rendering feature
97
106
  },
98
107
 
108
+ // Check if already initialized
109
+ isInitialized: () => isInitialized,
110
+
111
+ // Allow features to check if init should proceed
112
+ _shouldInit: () => !isInitialized,
113
+
99
114
  // Scrolling API (will be overridden by scrolling feature)
100
115
  scrollToIndex: (
101
116
  index: number,
102
- alignment?: "start" | "center" | "end"
117
+ alignment?: "start" | "center" | "end",
103
118
  ) => {
104
- // Will be implemented by scrolling feature
119
+ // Placeholder - will be implemented by scrolling feature
105
120
  },
106
121
 
107
122
  scrollToPosition: (position: number) => {
@@ -152,7 +167,7 @@ export const createViewport = (config: ViewportConfig = {}) => {
152
167
  enhancers.push(
153
168
  withEvents({
154
169
  debug: config.debug,
155
- })
170
+ }),
156
171
  );
157
172
 
158
173
  // Base setup (creates DOM structure)
@@ -160,7 +175,7 @@ export const createViewport = (config: ViewportConfig = {}) => {
160
175
  withBase({
161
176
  className: config.className,
162
177
  orientation: config.scrolling?.orientation,
163
- })
178
+ }),
164
179
  );
165
180
 
166
181
  // Virtual scrolling (required for most features)
@@ -170,7 +185,8 @@ export const createViewport = (config: ViewportConfig = {}) => {
170
185
  overscan: config.virtual?.overscan,
171
186
  orientation: config.scrolling?.orientation,
172
187
  autoDetectItemSize: config.virtual?.autoDetectItemSize,
173
- })
188
+ initialScrollIndex: (config as any).initialScrollIndex,
189
+ }),
174
190
  );
175
191
 
176
192
  // Scrolling behavior
@@ -179,7 +195,7 @@ export const createViewport = (config: ViewportConfig = {}) => {
179
195
  orientation: config.scrolling?.orientation,
180
196
  sensitivity: config.scrolling?.sensitivity,
181
197
  smoothing: config.scrolling?.animation,
182
- })
198
+ }),
183
199
  );
184
200
 
185
201
  // Scrollbar (optional)
@@ -188,7 +204,7 @@ export const createViewport = (config: ViewportConfig = {}) => {
188
204
  withScrollbar({
189
205
  enabled: true,
190
206
  autoHide: config.scrollbar?.autoHide,
191
- })
207
+ }),
192
208
  );
193
209
  }
194
210
 
@@ -207,7 +223,10 @@ export const createViewport = (config: ViewportConfig = {}) => {
207
223
  cancelLoadThreshold: config.performance?.cancelLoadThreshold,
208
224
  maxConcurrentRequests: config.performance?.maxConcurrentRequests,
209
225
  enableRequestQueue: config.performance?.enableRequestQueue !== false,
210
- })
226
+ initialScrollIndex: (config as any).initialScrollIndex,
227
+ selectId: (config as any).selectId,
228
+ autoLoad: (config as any).autoLoad !== false,
229
+ }),
211
230
  );
212
231
  }
213
232
 
@@ -218,7 +237,7 @@ export const createViewport = (config: ViewportConfig = {}) => {
218
237
  enabled: true,
219
238
  analyzeFirstLoad: config.placeholders?.analyzeFirstLoad ?? true,
220
239
  maskCharacter: config.placeholders?.maskCharacter,
221
- })
240
+ }),
222
241
  );
223
242
  }
224
243
 
@@ -227,7 +246,7 @@ export const createViewport = (config: ViewportConfig = {}) => {
227
246
  withRendering({
228
247
  template: config.template,
229
248
  overscan: config.virtual?.overscan,
230
- })
249
+ }),
231
250
  );
232
251
 
233
252
  // Compose all enhancers
@@ -29,22 +29,33 @@ $opacity-transition: opacity $transition-duration $transition-easing;
29
29
  // ===========================
30
30
 
31
31
  @keyframes placeholder-pulse {
32
- 0%,
33
- 100% {
34
- opacity: 0.6;
35
- }
36
- 50% {
37
- opacity: 0.4;
38
- }
32
+ 0%,
33
+ 100% {
34
+ opacity: 0.6;
35
+ }
36
+ 50% {
37
+ opacity: 0.4;
38
+ }
39
39
  }
40
40
 
41
41
  @keyframes fade-in {
42
- from {
43
- opacity: 0.6;
44
- }
45
- to {
46
- opacity: 1;
47
- }
42
+ from {
43
+ opacity: 0.6;
44
+ }
45
+ to {
46
+ opacity: 1;
47
+ }
48
+ }
49
+
50
+ @keyframes item-updated {
51
+ 0% {
52
+ background-color: t.alpha("primary", 0.25);
53
+ box-shadow: inset 0 0 0 2px t.alpha("primary", 0.4);
54
+ }
55
+ 100% {
56
+ background-color: transparent;
57
+ box-shadow: inset 0 0 0 2px transparent;
58
+ }
48
59
  }
49
60
 
50
61
  // ===========================
@@ -52,58 +63,58 @@ $opacity-transition: opacity $transition-duration $transition-easing;
52
63
  // ===========================
53
64
 
54
65
  .#{$component} {
55
- position: relative;
56
- width: 100%;
57
- height: 100%;
58
- min-height: 100px;
59
- background-color: t.color("surface");
60
- border: 2px solid var(--mtrl-sys-color-outline-variant);
61
- border-radius: 3px;
62
- transition: $bg-transition;
63
-
64
- // Performance optimizations
65
- contain: layout style paint;
66
- transform: translateZ(0);
67
- backface-visibility: hidden;
68
-
69
- // Selection mode
70
- &--selection {
71
- cursor: pointer;
72
- .#{$viewport-item}:hover {
73
- background-color: t.alpha("on-surface-variant", 0.1);
74
- }
75
- // Selected state
76
- .#{$viewport-item}--selected,
77
- .#{$viewport-item}--selected:hover {
78
- background-color: t.color("secondary-container");
79
- color: t.color("on-secondary-container");
80
- transition: $bg-transition, $color-transition;
81
-
82
- // // Update state layer color for selected state
83
- // &::before {
84
- // background-color: t.color("on-secondary-container");
85
- // }
86
-
87
- // Update text and icon colors for selected state
88
- .#{$component}-item {
89
- &-leading,
90
- &-trailing,
91
- &-supporting,
92
- &-overline,
93
- &-meta {
94
- color: t.color("on-secondary-container");
95
- transition: $color-transition;
66
+ position: relative;
67
+ width: 100%;
68
+ height: 100%;
69
+ min-height: 100px;
70
+ background-color: t.color("surface");
71
+ border: 2px solid var(--mtrl-sys-color-outline-variant);
72
+ border-radius: 3px;
73
+ transition: $bg-transition;
74
+
75
+ // Performance optimizations
76
+ contain: layout style paint;
77
+ transform: translateZ(0);
78
+ backface-visibility: hidden;
79
+
80
+ // Selection mode
81
+ &--selection {
82
+ cursor: pointer;
83
+ .#{$viewport-item}:hover {
84
+ background-color: t.alpha("on-surface-variant", 0.1);
85
+ }
86
+ // Selected state
87
+ .#{$viewport-item}--selected,
88
+ .#{$viewport-item}--selected:hover {
89
+ background-color: t.color("secondary-container");
90
+ color: t.color("on-secondary-container");
91
+ transition: $bg-transition, $color-transition;
92
+
93
+ // // Update state layer color for selected state
94
+ // &::before {
95
+ // background-color: t.color("on-secondary-container");
96
+ // }
97
+
98
+ // Update text and icon colors for selected state
99
+ .#{$component}-item {
100
+ &-leading,
101
+ &-trailing,
102
+ &-supporting,
103
+ &-overline,
104
+ &-meta {
105
+ color: t.color("on-secondary-container");
106
+ transition: $color-transition;
107
+ }
108
+ }
96
109
  }
97
- }
98
110
  }
99
- }
100
111
 
101
- // Disabled state
102
- &--disabled {
103
- pointer-events: none;
104
- opacity: 0.38;
105
- transition: $opacity-transition;
106
- }
112
+ // Disabled state
113
+ &--disabled {
114
+ pointer-events: none;
115
+ opacity: 0.38;
116
+ transition: $opacity-transition;
117
+ }
107
118
  }
108
119
 
109
120
  // ===========================
@@ -111,86 +122,96 @@ $opacity-transition: opacity $transition-duration $transition-easing;
111
122
  // ===========================
112
123
 
113
124
  .#{$viewport} {
114
- position: relative;
115
- width: 100%;
116
- height: 100%;
117
- overflow: hidden;
118
-
119
- // Items container
120
- &-items {
121
125
  position: relative;
122
126
  width: 100%;
123
127
  height: 100%;
124
- padding: 8px 0;
125
- will-change: transform;
126
- }
128
+ overflow: hidden;
129
+
130
+ // Items container
131
+ &-items {
132
+ position: relative;
133
+ width: 100%;
134
+ height: 100%;
135
+ padding: 8px 0;
136
+ will-change: transform;
137
+ }
127
138
  }
128
139
 
129
140
  .#{$viewport-item} {
130
- user-select: none;
131
- opacity: 1;
132
- transition: $opacity-transition;
133
- will-change: transform;
134
- // Apply fade-in animation to items that replace placeholders
135
- &--replaced {
136
- animation: fade-in 0.3s ease-out;
137
- }
141
+ user-select: none;
142
+ opacity: 1;
143
+ transition: $opacity-transition;
144
+ will-change: transform;
145
+ // Apply fade-in animation to items that replace placeholders
146
+ &--replaced {
147
+ animation: fade-in 0.3s ease-out;
148
+ }
149
+ // Apply highlight animation when item is updated
150
+ &--updated,
151
+ &--updated > * {
152
+ animation: item-updated 0.6s ease-out;
153
+ }
154
+
155
+ // Inner item update animation (applied to template's root element)
156
+ .item--updated {
157
+ animation: item-updated 0.6s ease-out;
158
+ }
138
159
  }
139
160
 
140
161
  .#{$viewport-item} {
141
- padding: 11px 12px;
142
- display: flex;
143
- align-items: start;
144
- transition: background-color 0.2s ease;
145
- // align-items: center;
146
- min-height: 48px;
147
- left: 0;
148
- right: 0;
149
- width: 100%;
150
- will-change: transform;
151
- contain: layout style;
152
- gap: 16px;
153
- color: var(--mtrl-sys-color-on-surface);
154
- overflow: hidden;
162
+ padding: 11px 12px;
163
+ display: flex;
164
+ align-items: start;
165
+ transition: background-color 0.2s ease;
166
+ // align-items: center;
167
+ min-height: 48px;
168
+ left: 0;
169
+ right: 0;
170
+ width: 100%;
171
+ will-change: transform;
172
+ contain: layout style;
173
+ gap: 16px;
174
+ color: var(--mtrl-sys-color-on-surface);
175
+ overflow: hidden;
155
176
  }
156
177
 
157
178
  .#{$viewport-item}__avatar,
158
179
  .#{$viewport-item}__image {
159
- width: 40px;
160
- height: 40px;
161
- margin-top: 4px;
162
- border-radius: 50%;
163
- background-color: var(--mtrl-sys-color-primary-container);
164
- color: white;
165
- display: flex;
166
- align-items: center;
167
- justify-content: center;
168
- font-weight: bold;
169
- flex-shrink: 0;
180
+ width: 40px;
181
+ height: 40px;
182
+ margin-top: 4px;
183
+ border-radius: 50%;
184
+ background-color: var(--mtrl-sys-color-primary-container);
185
+ color: white;
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ font-weight: bold;
190
+ flex-shrink: 0;
170
191
  }
171
192
 
172
193
  .#{$viewport-item}__details {
173
- flex: 1;
174
- min-width: 0;
175
- margin-left: 12px;
194
+ flex: 1;
195
+ min-width: 0;
196
+ margin-left: 12px;
176
197
  }
177
198
 
178
199
  .#{$viewport-item}__headline {
179
- font-weight: 500;
200
+ font-weight: 500;
180
201
  }
181
202
 
182
203
  .#{$viewport-item}__text {
183
- // color: #666;
184
- font-size: 14px;
185
- white-space: nowrap;
186
- overflow: hidden;
187
- text-overflow: ellipsis;
204
+ // color: #666;
205
+ font-size: 14px;
206
+ white-space: nowrap;
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
188
209
  }
189
210
 
190
211
  .#{$viewport-item}__meta {
191
- color: var(--mtrl-sys-color-on-surface-variant);
192
- font-size: 12px;
193
- margin-top: 2px;
212
+ color: var(--mtrl-sys-color-on-surface-variant);
213
+ font-size: 12px;
214
+ margin-top: 2px;
194
215
  }
195
216
 
196
217
  // ===========================
@@ -198,48 +219,48 @@ $opacity-transition: opacity $transition-duration $transition-easing;
198
219
  // ===========================
199
220
 
200
221
  .#{$viewport-item}--placeholder {
201
- opacity: 0.6; // Match the animation start/end opacity
202
- animation: placeholder-pulse 2s ease-in-out infinite;
222
+ opacity: 0.6; // Match the animation start/end opacity
223
+ animation: placeholder-pulse 2s ease-in-out infinite;
224
+
225
+ // Placeholder content blocks - style any text elements within
226
+ .#{$viewport-item}__headline,
227
+ .#{$viewport-item}__text,
228
+ .#{$viewport-item}__meta {
229
+ position: relative;
230
+ display: inline-block;
231
+ font-size: 0.8em;
232
+ color: transparent;
233
+ background-color: var(--mtrl-sys-color-on-surface);
234
+ border-radius: 0.1em;
235
+ opacity: 0.4;
236
+ text-decoration: none;
237
+ line-height: 1;
238
+ padding: 0 0 0.05em;
239
+ vertical-align: middle;
240
+ }
203
241
 
204
- // Placeholder content blocks - style any text elements within
205
- .#{$viewport-item}__headline,
206
- .#{$viewport-item}__text,
207
- .#{$viewport-item}__meta {
208
- position: relative;
209
- display: inline-block;
210
- font-size: 0.8em;
211
- color: transparent;
212
- background-color: var(--mtrl-sys-color-on-surface);
213
- border-radius: 0.1em;
214
- opacity: 0.4;
215
- text-decoration: none;
216
- line-height: 1;
217
- padding: 0 0 0.05em;
218
- vertical-align: middle;
219
- }
220
-
221
- // Layout adjustments
222
- .#{$viewport-item}__text,
223
- .#{$viewport-item}__meta {
224
- margin-top: 0.2em;
225
- }
226
-
227
- .#{$viewport-item}__meta {
228
- font-size: 10px;
229
- }
230
-
231
- // User-specific placeholder elements
232
- .#{$viewport-item}__user-headline,
233
- .#{$viewport-item}__user-text {
234
- text-transform: capitalize;
235
- }
236
-
237
- // Avatar placeholder
238
- .#{$viewport-item}__avatar {
239
- background-color: var(--mtrl-sys-color-primary-container);
240
- color: var(--mtrl-sys-color-primary-container);
241
- opacity: 1;
242
- }
242
+ // Layout adjustments
243
+ .#{$viewport-item}__text,
244
+ .#{$viewport-item}__meta {
245
+ margin-top: 0.2em;
246
+ }
247
+
248
+ .#{$viewport-item}__meta {
249
+ font-size: 10px;
250
+ }
251
+
252
+ // User-specific placeholder elements
253
+ .#{$viewport-item}__user-headline,
254
+ .#{$viewport-item}__user-text {
255
+ text-transform: capitalize;
256
+ }
257
+
258
+ // Avatar placeholder
259
+ .#{$viewport-item}__avatar {
260
+ background-color: var(--mtrl-sys-color-primary-container);
261
+ color: var(--mtrl-sys-color-primary-container);
262
+ opacity: 1;
263
+ }
243
264
  }
244
265
 
245
266
  // ===========================
@@ -247,51 +268,51 @@ $opacity-transition: opacity $transition-duration $transition-easing;
247
268
  // ===========================
248
269
 
249
270
  .#{$viewport}__scrollbar {
250
- position: absolute;
251
- top: 0;
252
- right: 0;
253
- width: 8px;
254
- height: 100%;
255
- padding: 0;
256
- opacity: 0;
257
- transition: opacity 0.3s ease;
258
- cursor: pointer;
259
- z-index: 10;
260
-
261
- // Visibility states
262
- &--visible,
263
- &--dragging,
264
- &:hover {
265
- opacity: 1;
266
- }
267
-
268
- &:hover {
269
- background: rgba(0, 0, 0, 0.05);
270
- }
271
-
272
- // Scrollbar thumb
273
- &-thumb {
274
271
  position: absolute;
275
272
  top: 0;
276
- width: 6px;
277
- padding: 1px;
278
- background: rgba(0, 0, 0, 0.3);
279
- border-radius: 4px;
280
- will-change: transform;
281
- cursor: grab;
282
- transition: background 0.2s ease;
273
+ right: 0;
274
+ width: 8px;
275
+ height: 100%;
276
+ padding: 0;
277
+ opacity: 0;
278
+ transition: opacity 0.3s ease;
279
+ cursor: pointer;
280
+ z-index: 10;
281
+
282
+ // Visibility states
283
+ &--visible,
284
+ &--dragging,
285
+ &:hover {
286
+ opacity: 1;
287
+ }
283
288
 
284
289
  &:hover {
285
- background: rgba(0, 0, 0, 0.5);
290
+ background: rgba(0, 0, 0, 0.05);
286
291
  }
287
292
 
288
- &:active,
289
- &--dragging {
290
- cursor: grabbing;
291
- background: rgba(0, 0, 0, 0.6);
292
- transition: none;
293
+ // Scrollbar thumb
294
+ &-thumb {
295
+ position: absolute;
296
+ top: 0;
297
+ width: 6px;
298
+ padding: 1px;
299
+ background: rgba(0, 0, 0, 0.3);
300
+ border-radius: 4px;
301
+ will-change: transform;
302
+ cursor: grab;
303
+ transition: background 0.2s ease;
304
+
305
+ &:hover {
306
+ background: rgba(0, 0, 0, 0.5);
307
+ }
308
+
309
+ &:active,
310
+ &--dragging {
311
+ cursor: grabbing;
312
+ background: rgba(0, 0, 0, 0.6);
313
+ transition: none;
314
+ }
293
315
  }
294
- }
295
316
  }
296
317
 
297
318
  // ===========================
@@ -299,22 +320,22 @@ $opacity-transition: opacity $transition-duration $transition-easing;
299
320
  // ===========================
300
321
 
301
322
  @media (prefers-color-scheme: dark) {
302
- .#{$viewport}__scrollbar {
303
- background: transparent;
323
+ .#{$viewport}__scrollbar {
324
+ background: transparent;
304
325
 
305
- &-thumb {
306
- background: rgba(255, 255, 255, 0.4);
326
+ &-thumb {
327
+ background: rgba(255, 255, 255, 0.4);
307
328
 
308
- &:hover {
309
- background: rgba(255, 255, 255, 0.6);
310
- }
329
+ &:hover {
330
+ background: rgba(255, 255, 255, 0.6);
331
+ }
311
332
 
312
- &:active,
313
- &--dragging {
314
- background: rgba(255, 255, 255, 0.4);
315
- }
333
+ &:active,
334
+ &--dragging {
335
+ background: rgba(255, 255, 255, 0.4);
336
+ }
337
+ }
316
338
  }
317
- }
318
339
  }
319
340
 
320
341
  // ===========================
@@ -323,9 +344,9 @@ $opacity-transition: opacity $transition-duration $transition-easing;
323
344
 
324
345
  // High-density displays
325
346
  @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
326
- .#{$viewport-item} {
327
- text-rendering: optimizeLegibility;
328
- -webkit-font-smoothing: antialiased;
329
- -moz-osx-font-smoothing: grayscale;
330
- }
347
+ .#{$viewport-item} {
348
+ text-rendering: optimizeLegibility;
349
+ -webkit-font-smoothing: antialiased;
350
+ -moz-osx-font-smoothing: grayscale;
351
+ }
331
352
  }