mtrl-addons 0.1.2 → 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.
Files changed (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -129,7 +129,7 @@ const PREFIX_WITH_DASH = `${PREFIX}-`;
129
129
  function getCachedClassName(
130
130
  type: string,
131
131
  property: string,
132
- value: string | number
132
+ value: string | number,
133
133
  ): string {
134
134
  const key = `${type}-${property}-${value}`;
135
135
  if (!classCache.has(key)) {
@@ -138,7 +138,7 @@ function getCachedClassName(
138
138
  key,
139
139
  property === ""
140
140
  ? `layout__item--${value}`
141
- : `layout__item--${property}-${value}`
141
+ : `layout__item--${property}-${value}`,
142
142
  );
143
143
  } else {
144
144
  // For layout classes, align uses layout--{type}-{value}
@@ -180,45 +180,80 @@ function releaseFragment(fragment: DocumentFragment): void {
180
180
 
181
181
  /**
182
182
  * Optimized class processing with minimal string operations
183
- * Handles arrays, strings, and className aliases efficiently
183
+ * Handles arrays, strings, className aliases, and rawClass efficiently
184
184
  */
185
185
  function processClassNames(
186
186
  options: Record<string, any>,
187
- skipPrefix = false
187
+ skipPrefix = false,
188
188
  ): Record<string, any> {
189
- if (!options || skipPrefix) return { ...options };
189
+ if (!options) return options;
190
190
 
191
- const hasClassProps = options.class || options.className;
192
- if (!hasClassProps) return { ...options };
191
+ const hasRawClass = options.rawClass;
192
+ const hasRegularClass = options.class || options.className;
193
193
 
194
- const processed = { ...options };
194
+ // Fast path: no class properties at all
195
+ if (!hasRawClass && !hasRegularClass) return options;
196
+
197
+ // Fast path: only rawClass and skipping prefix (most common rawClass scenario)
198
+ if (hasRawClass && !hasRegularClass && skipPrefix) {
199
+ const processed = { ...options };
200
+ delete processed.rawClass;
195
201
 
196
- // Combine class sources efficiently
197
- let classString = "";
198
- if (processed.class) {
199
- // Handle arrays by joining first
200
- if (Array.isArray(processed.class)) {
201
- classString += processed.class.join(" ");
202
+ // Direct assignment for simple string
203
+ if (typeof hasRawClass === "string") {
204
+ processed.class = hasRawClass;
202
205
  } else {
203
- classString += processed.class;
206
+ // Handle array case
207
+ processed.class = hasRawClass.join(" ");
204
208
  }
209
+ return processed;
205
210
  }
206
- if (processed.className) {
207
- classString += (classString ? " " : "") + processed.className;
211
+
212
+ // Full processing path (only when needed)
213
+ const processed = { ...options };
214
+ let finalClasses = "";
215
+
216
+ // Handle prefixed classes only if not skipping prefix
217
+ if (!skipPrefix && hasRegularClass) {
218
+ let prefixedString = "";
219
+
220
+ if (processed.class) {
221
+ prefixedString += Array.isArray(processed.class)
222
+ ? processed.class.join(" ")
223
+ : processed.class;
224
+ }
225
+ if (processed.className) {
226
+ prefixedString += (prefixedString ? " " : "") + processed.className;
227
+ }
228
+
229
+ if (prefixedString) {
230
+ finalClasses = prefixedString
231
+ .split(/\s+/)
232
+ .filter(Boolean)
233
+ .map((cls) =>
234
+ cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls,
235
+ )
236
+ .join(" ");
237
+ }
208
238
  }
209
239
 
210
- if (classString) {
211
- // Single pass prefix application
212
- processed.class = classString
213
- .split(/\s+/)
214
- .filter(Boolean)
215
- .map((cls) =>
216
- cls && !cls.startsWith(PREFIX_WITH_DASH) ? PREFIX_WITH_DASH + cls : cls
217
- )
218
- .join(" ");
240
+ // Handle rawClass (always processed when present)
241
+ if (hasRawClass) {
242
+ const rawString = Array.isArray(hasRawClass)
243
+ ? hasRawClass.filter(Boolean).join(" ")
244
+ : hasRawClass;
245
+
246
+ finalClasses += (finalClasses ? " " : "") + rawString;
247
+ }
248
+
249
+ if (finalClasses) {
250
+ processed.class = finalClasses;
219
251
  }
220
252
 
253
+ // Clean up in one operation
221
254
  delete processed.className;
255
+ delete processed.rawClass;
256
+
222
257
  return processed;
223
258
  }
224
259
 
@@ -236,7 +271,7 @@ interface ExtractedParameters {
236
271
  function extractParameters(
237
272
  schema: SchemaItem[],
238
273
  startIndex: number,
239
- defaultCreator: Function
274
+ defaultCreator: Function,
240
275
  ): ExtractedParameters {
241
276
  const items = schema.slice(startIndex, startIndex + 3);
242
277
  let creator, name, options;
@@ -272,7 +307,7 @@ function extractParameters(
272
307
  return {
273
308
  creator: creator || defaultCreator,
274
309
  name,
275
- options: options || {},
310
+ options: (options || {}) as Record<string, any>,
276
311
  consumed,
277
312
  };
278
313
  }
@@ -344,7 +379,7 @@ function hasClass(element: HTMLElement, className: string): boolean {
344
379
  */
345
380
  function createComponentInstance(
346
381
  Component: any,
347
- options: Record<string, any> = {}
382
+ options: Record<string, any> = {},
348
383
  ): any {
349
384
  try {
350
385
  // Destructure special configs in one operation
@@ -418,7 +453,7 @@ function createComponentInstance(
418
453
  }
419
454
  } else {
420
455
  for (const [eventName, handler] of Object.entries(
421
- finalEventsConfig
456
+ finalEventsConfig,
422
457
  )) {
423
458
  if (typeof handler === "function") {
424
459
  element.addEventListener(eventName, handler);
@@ -446,7 +481,7 @@ function createComponentInstance(
446
481
  */
447
482
  function applyLayoutClasses(
448
483
  element: HTMLElement,
449
- layoutConfig: LayoutConfig
484
+ layoutConfig: LayoutConfig,
450
485
  ): void {
451
486
  if (!element || !layoutConfig) return;
452
487
 
@@ -462,21 +497,21 @@ function applyLayoutClasses(
462
497
  addClass(
463
498
  element,
464
499
  PREFIX_WITH_DASH +
465
- getCachedClassName(layoutType, "gap", layoutConfig.gap)
500
+ getCachedClassName(layoutType, "gap", layoutConfig.gap),
466
501
  );
467
502
  }
468
503
  if (layoutConfig.align) {
469
504
  addClass(
470
505
  element,
471
506
  PREFIX_WITH_DASH +
472
- getCachedClassName(layoutType, "align", layoutConfig.align)
507
+ getCachedClassName(layoutType, "align", layoutConfig.align),
473
508
  );
474
509
  }
475
510
  if (layoutConfig.justify) {
476
511
  addClass(
477
512
  element,
478
513
  PREFIX_WITH_DASH +
479
- getCachedClassName(layoutType, "justify", layoutConfig.justify)
514
+ getCachedClassName(layoutType, "justify", layoutConfig.justify),
480
515
  );
481
516
  }
482
517
  }
@@ -487,17 +522,17 @@ function applyLayoutClasses(
487
522
  addClass(
488
523
  element,
489
524
  PREFIX_WITH_DASH +
490
- getCachedClassName("grid", "cols", layoutConfig.columns)
525
+ getCachedClassName("grid", "cols", layoutConfig.columns),
491
526
  );
492
527
  } else if (layoutConfig.columns === "auto-fill") {
493
528
  addClass(
494
529
  element,
495
- PREFIX_WITH_DASH + getCachedClassName("grid", "fill", "auto")
530
+ PREFIX_WITH_DASH + getCachedClassName("grid", "fill", "auto"),
496
531
  );
497
532
  } else if (layoutConfig.columns === "auto-fit") {
498
533
  addClass(
499
534
  element,
500
- PREFIX_WITH_DASH + getCachedClassName("grid", "cols", "auto-fit")
535
+ PREFIX_WITH_DASH + getCachedClassName("grid", "cols", "auto-fit"),
501
536
  );
502
537
  }
503
538
  if (layoutConfig.dense)
@@ -534,7 +569,7 @@ function applyLayoutClasses(
534
569
  */
535
570
  function applyLayoutItemClasses(
536
571
  element: HTMLElement,
537
- itemConfig: LayoutItemConfig
572
+ itemConfig: LayoutItemConfig,
538
573
  ): void {
539
574
  if (!element || !itemConfig) return;
540
575
 
@@ -544,53 +579,53 @@ function applyLayoutItemClasses(
544
579
  if (itemConfig.width && itemConfig.width >= 1 && itemConfig.width <= 12) {
545
580
  addClass(
546
581
  element,
547
- PREFIX_WITH_DASH + getCachedClassName("item", "", itemConfig.width)
582
+ PREFIX_WITH_DASH + getCachedClassName("item", "", itemConfig.width),
548
583
  );
549
584
  }
550
585
  if (itemConfig.sm)
551
586
  addClass(
552
587
  element,
553
- PREFIX_WITH_DASH + getCachedClassName("item", "sm", itemConfig.sm)
588
+ PREFIX_WITH_DASH + getCachedClassName("item", "sm", itemConfig.sm),
554
589
  );
555
590
  if (itemConfig.md)
556
591
  addClass(
557
592
  element,
558
- PREFIX_WITH_DASH + getCachedClassName("item", "md", itemConfig.md)
593
+ PREFIX_WITH_DASH + getCachedClassName("item", "md", itemConfig.md),
559
594
  );
560
595
  if (itemConfig.lg)
561
596
  addClass(
562
597
  element,
563
- PREFIX_WITH_DASH + getCachedClassName("item", "lg", itemConfig.lg)
598
+ PREFIX_WITH_DASH + getCachedClassName("item", "lg", itemConfig.lg),
564
599
  );
565
600
  if (itemConfig.xl)
566
601
  addClass(
567
602
  element,
568
- PREFIX_WITH_DASH + getCachedClassName("item", "xl", itemConfig.xl)
603
+ PREFIX_WITH_DASH + getCachedClassName("item", "xl", itemConfig.xl),
569
604
  );
570
605
 
571
606
  // Grid span classes
572
607
  if (itemConfig.span)
573
608
  addClass(
574
609
  element,
575
- PREFIX_WITH_DASH + getCachedClassName("item", "span", itemConfig.span)
610
+ PREFIX_WITH_DASH + getCachedClassName("item", "span", itemConfig.span),
576
611
  );
577
612
  if (itemConfig.rowSpan)
578
613
  addClass(
579
614
  element,
580
615
  PREFIX_WITH_DASH +
581
- getCachedClassName("item", "row-span", itemConfig.rowSpan)
616
+ getCachedClassName("item", "row-span", itemConfig.rowSpan),
582
617
  );
583
618
 
584
619
  // Order and alignment
585
620
  if (itemConfig.order)
586
621
  addClass(
587
622
  element,
588
- PREFIX_WITH_DASH + getCachedClassName("item", "order", itemConfig.order)
623
+ PREFIX_WITH_DASH + getCachedClassName("item", "order", itemConfig.order),
589
624
  );
590
625
  if (itemConfig.align)
591
626
  addClass(
592
627
  element,
593
- PREFIX_WITH_DASH + getCachedClassName("item", "self", itemConfig.align)
628
+ PREFIX_WITH_DASH + getCachedClassName("item", "self", itemConfig.align),
594
629
  );
595
630
  if (itemConfig.auto)
596
631
  addClass(element, `${PREFIX_WITH_DASH}layout__item--auto`);
@@ -603,10 +638,10 @@ function getLayoutType(element: HTMLElement): string {
603
638
  return hasClass(element, `${PREFIX_WITH_DASH}layout--stack`)
604
639
  ? "stack"
605
640
  : hasClass(element, `${PREFIX_WITH_DASH}layout--row`)
606
- ? "row"
607
- : hasClass(element, `${PREFIX_WITH_DASH}layout--grid`)
608
- ? "grid"
609
- : "";
641
+ ? "row"
642
+ : hasClass(element, `${PREFIX_WITH_DASH}layout--grid`)
643
+ ? "grid"
644
+ : "";
610
645
  }
611
646
 
612
647
  // ============================================================================
@@ -618,10 +653,10 @@ function getLayoutType(element: HTMLElement): string {
618
653
  * Optimized with parameter extraction and integrated configuration
619
654
  */
620
655
  function processArraySchema(
621
- schema: SchemaItem[],
656
+ schema: SchemaItem[] | any,
622
657
  parentElement: HTMLElement | null = null,
623
658
  level: number = 0,
624
- options: LayoutOptions = {}
659
+ options: LayoutOptions = {},
625
660
  ): LayoutResult {
626
661
  level++;
627
662
  const layout: Record<string, any> = {};
@@ -633,7 +668,7 @@ function processArraySchema(
633
668
  return createLayoutResult(layout);
634
669
  }
635
670
 
636
- const defaultCreator = options.creator || createElement;
671
+ const defaultCreator = (options as any).creator || createElement;
637
672
 
638
673
  for (let i = 0; i < schema.length; i++) {
639
674
  const item = schema[i];
@@ -643,6 +678,11 @@ function processArraySchema(
643
678
  if (Array.isArray(item)) {
644
679
  const container = component || parentElement;
645
680
  const result = processArraySchema(item, container, level, options);
681
+ // Merge nested components array instead of overwriting
682
+ if (Array.isArray(result.layout.components)) {
683
+ components.push(...result.layout.components);
684
+ delete result.layout.components;
685
+ }
646
686
  Object.assign(layout, result.layout);
647
687
  continue;
648
688
  }
@@ -668,12 +708,15 @@ function processArraySchema(
668
708
  // Advance index by consumed items minus 1 (loop increment handles the +1)
669
709
  i += consumed - 1;
670
710
 
671
- // Process options with prefix
711
+ // Process options with prefix - optimized decision logic
672
712
  const shouldApplyPrefix =
673
713
  "prefix" in itemOptions ? itemOptions.prefix : options.prefix !== false;
674
- const processedOptions = shouldApplyPrefix
675
- ? processClassNames(itemOptions)
676
- : { ...itemOptions };
714
+
715
+ // Fast path: process only when needed
716
+ const processedOptions =
717
+ shouldApplyPrefix || itemOptions.rawClass
718
+ ? processClassNames(itemOptions, !shouldApplyPrefix)
719
+ : itemOptions; // No copy needed if no processing
677
720
 
678
721
  // Add name to options if needed
679
722
  if (
@@ -734,16 +777,16 @@ function processArraySchema(
734
777
  * Simplified and optimized for better performance
735
778
  */
736
779
  function processObjectSchema(
737
- schema: Record<string, any>,
780
+ schema: Record<string, any> | string,
738
781
  parentElement: HTMLElement | null = null,
739
- options: LayoutOptions = {}
782
+ options: LayoutOptions = {},
740
783
  ): LayoutResult {
741
784
  const layout: Record<string, any> = {};
742
785
  const defaultCreator = options.creator || createElement;
743
786
 
744
787
  // Handle root element creation
745
- if (schema.element && !parentElement) {
746
- const elementDef = schema.element;
788
+ if ((schema as any).element && !parentElement) {
789
+ const elementDef = (schema as any).element;
747
790
  const createElementFn = elementDef.creator || defaultCreator;
748
791
 
749
792
  const elementOptions = elementDef.options || {};
@@ -754,7 +797,7 @@ function processObjectSchema(
754
797
 
755
798
  const rootComponent = createComponentInstance(
756
799
  createElementFn,
757
- processedOptions
800
+ processedOptions,
758
801
  );
759
802
  layout.element = rootComponent;
760
803
  if (elementDef.name) layout[elementDef.name] = rootComponent;
@@ -767,7 +810,7 @@ function processObjectSchema(
767
810
  const childResult = processObjectSchema(
768
811
  elementDef.children,
769
812
  rootElement,
770
- options
813
+ options,
771
814
  );
772
815
  Object.assign(layout, childResult.layout);
773
816
  }
@@ -778,8 +821,8 @@ function processObjectSchema(
778
821
  // Process normal schema elements
779
822
  const fragment = parentElement ? createFragment() : null;
780
823
 
781
- for (const key in schema) {
782
- const def = schema[key];
824
+ for (const key in schema as Record<string, any>) {
825
+ const def = (schema as Record<string, any>)[key];
783
826
  if (!def) continue;
784
827
 
785
828
  const elementCreator = def.creator || defaultCreator;
@@ -870,14 +913,47 @@ function createLayoutResult(layout: Record<string, any>): LayoutResult {
870
913
  },
871
914
 
872
915
  destroy(): void {
873
- // Call destroy on components
874
- for (const key in layout) {
875
- const component = layout[key];
876
- if (component && typeof component.destroy === "function") {
877
- component.destroy();
916
+ // Track destroyed components to avoid double-destroy
917
+ const destroyed = new Set<any>();
918
+
919
+ // Helper to safely destroy a component
920
+ const destroyComponent = (component: any): void => {
921
+ if (!component || destroyed.has(component)) return;
922
+
923
+ // Skip plain HTML elements and non-objects
924
+ if (component instanceof HTMLElement || typeof component !== "object")
925
+ return;
926
+
927
+ // Check if it's a component with destroy method
928
+ if (typeof component.destroy === "function") {
929
+ destroyed.add(component);
930
+ try {
931
+ component.destroy();
932
+ } catch (e) {
933
+ // Ignore destroy errors - component may already be cleaned up
934
+ }
935
+ }
936
+ };
937
+
938
+ // First destroy components from the components array (if present)
939
+ // This ensures all named components are destroyed even if nested
940
+ if (Array.isArray(layout.components)) {
941
+ for (const [name, component] of layout.components) {
942
+ destroyComponent(component);
878
943
  }
879
944
  }
880
945
 
946
+ // Then iterate over all layout keys for any missed components
947
+ for (const key in layout) {
948
+ if (key === "element" || key === "components") continue;
949
+ destroyComponent(layout[key]);
950
+ }
951
+
952
+ // Clear the components array
953
+ if (Array.isArray(layout.components)) {
954
+ layout.components.length = 0;
955
+ }
956
+
881
957
  // Remove root element from DOM
882
958
  if (layout.element) {
883
959
  const element = isComponent(layout.element)
@@ -887,6 +963,11 @@ function createLayoutResult(layout: Record<string, any>): LayoutResult {
887
963
  element.parentNode.removeChild(element);
888
964
  }
889
965
  }
966
+
967
+ // Clear references to help GC
968
+ for (const key in layout) {
969
+ delete layout[key];
970
+ }
890
971
  },
891
972
  };
892
973
  }
@@ -902,7 +983,7 @@ function createLayoutResult(layout: Record<string, any>): LayoutResult {
902
983
  export function createLayout(
903
984
  schema: any,
904
985
  parentElement: HTMLElement | null = null,
905
- options: LayoutOptions = {}
986
+ options: LayoutOptions = {},
906
987
  ): LayoutResult {
907
988
  // Handle function schemas
908
989
  if (typeof schema === "function") {
@@ -37,12 +37,15 @@ export interface ElementOptions extends Record<string, any> {
37
37
  /** Layout item configuration */
38
38
  layoutItem?: LayoutItemConfig;
39
39
 
40
- /** CSS classes to apply */
40
+ /** CSS classes to apply (with automatic mtrl- prefix) */
41
41
  class?: string;
42
42
 
43
43
  /** Additional CSS classes (alias for class) */
44
44
  className?: string;
45
45
 
46
+ /** CSS classes to apply without prefix */
47
+ rawClass?: string | string[];
48
+
46
49
  /** HTML tag name for createElement */
47
50
  tag?: string;
48
51
 
@@ -89,4 +92,4 @@ export interface Schema {
89
92
 
90
93
  /** Additional elements */
91
94
  [key: string]: ElementDefinition | undefined;
92
- }
95
+ }
@@ -0,0 +1,145 @@
1
+ // src/core/viewport/constants.ts
2
+
3
+ /**
4
+ * Viewport Constants
5
+ * Centralized constants for all viewport functionality
6
+ * Consolidated from viewport, viewport/features, and list-manager constants
7
+ */
8
+
9
+ export const VIEWPORT_CONSTANTS = {
10
+ // Virtual scrolling defaults
11
+ VIRTUAL_SCROLL: {
12
+ DEFAULT_ITEM_SIZE: 48,
13
+ OVERSCAN_BUFFER: 2,
14
+ SCROLL_SENSITIVITY: 0.7,
15
+ MAX_VIRTUAL_SIZE: 100 * 1000 * 1000, // 100M pixels - modern browsers can handle this
16
+ AUTO_DETECT_ITEM_SIZE: true, // Enable automatic item size detection
17
+ },
18
+
19
+ // Scrolling settings
20
+ SCROLLING: {
21
+ OVERSCAN: 1, // From features/constants
22
+ },
23
+
24
+ // Rendering settings
25
+ RENDERING: {
26
+ // Element recycling
27
+ DEFAULT_MAX_POOL_SIZE: 100,
28
+ CLASSES: {
29
+ ITEM: "viewport-item",
30
+ },
31
+ },
32
+
33
+ // Loading settings
34
+ LOADING: {
35
+ CANCEL_THRESHOLD: 25, // px/ms - velocity above which loads cancel
36
+ MAX_CONCURRENT_REQUESTS: 1, // Parallel requests allowed
37
+ DEFAULT_RANGE_SIZE: 20, // Items per request
38
+ DEBOUNCE_LOADING: 150, // Debounce delay (ms)
39
+ MIN_RANGE_SIZE: 10, // Minimum items per load
40
+ MAX_RANGE_SIZE: 100, // Maximum items per load
41
+ REQUEST_TIMEOUT: 5000, // Request timeout (ms)
42
+ RETRY_ATTEMPTS: 2, // Failed request retries
43
+ RETRY_DELAY: 1000, // Delay between retries (ms)
44
+ },
45
+
46
+ // Request queue configuration (from features/constants)
47
+ REQUEST_QUEUE: {
48
+ ENABLED: true, // Enable request queuing
49
+ MAX_QUEUE_SIZE: 1, // Max queued requests
50
+ MAX_ACTIVE_REQUESTS: 2, // Max concurrent active requests
51
+ },
52
+
53
+ // Placeholder settings
54
+ PLACEHOLDER: {
55
+ MASK_CHARACTER: "X", // Updated from list-manager
56
+ CLASS: "viewport-item--placeholder",
57
+ MAX_SAMPLE_SIZE: 20,
58
+ PLACEHOLDER_FLAG: "_placeholder",
59
+ RANDOM_LENGTH_VARIANCE: true,
60
+ },
61
+
62
+ // Speed tracking (from list-manager)
63
+ SPEED_TRACKING: {
64
+ // Velocity calculation
65
+ DECELERATION_FACTOR: 0.85, // velocity decay per frame
66
+ },
67
+
68
+ // Momentum settings
69
+ MOMENTUM: {
70
+ ENABLED: false, // Enable momentum by default
71
+ DECELERATION_FACTOR: 0.85, // How quickly velocity decreases per frame
72
+ MIN_VELOCITY: 0.1, // Minimum velocity before stopping (px/ms)
73
+ MIN_DURATION: 300, // Maximum gesture duration to trigger momentum (ms)
74
+ MIN_VELOCITY_THRESHOLD: 0.5, // Minimum velocity to trigger momentum (px/ms)
75
+ FRAME_TIME: 16, // Assumed frame time for calculations (ms)
76
+ },
77
+
78
+ // Initial load configuration (from list-manager)
79
+ INITIAL_LOAD: {
80
+ STRATEGY: "placeholders", // "placeholders" | "direct" | "progressive"
81
+ VIEWPORT_MULTIPLIER: 1.5, // load 1.5x viewport capacity
82
+ MIN_ITEMS: 10,
83
+ MAX_ITEMS: 100,
84
+ PLACEHOLDER_COUNT: 20, // default placeholder count
85
+ SHOW_LOADING_STATE: true,
86
+ LOADING_DELAY: 100, // ms - delay before showing loading state
87
+ },
88
+
89
+ // Selection settings
90
+ SELECTION: {
91
+ SELECTED_CLASS: "viewport-item--selected",
92
+ },
93
+
94
+ // Scrollbar settings (from list-manager)
95
+ SCROLLBAR: {
96
+ // CSS classes
97
+ CLASSES: {
98
+ SCROLLBAR: "viewport__scrollbar",
99
+ SCROLLBAR_TRACK: "viewport__scrollbar-track",
100
+ SCROLLBAR_THUMB: "viewport__scrollbar-thumb",
101
+ SCROLLBAR_VISIBLE: "viewport__scrollbar--visible",
102
+ SCROLLBAR_DRAGGING: "viewport__scrollbar--dragging",
103
+ SCROLLBAR_THUMB_DRAGGING: "viewport__scrollbar-thumb--dragging",
104
+ },
105
+ },
106
+
107
+ // Orientation (from list-manager)
108
+ ORIENTATION: {
109
+ DEFAULT_ORIENTATION: "vertical",
110
+ DEFAULT_CROSS_AXIS_ALIGNMENT: "stretch",
111
+ REVERSE_DIRECTION: false,
112
+ },
113
+
114
+ PAGINATION: {
115
+ DEFAULT_STRATEGY: "offset" as "offset" | "page" | "cursor",
116
+ DEFAULT_LIMIT: 20,
117
+ STRATEGIES: {
118
+ PAGE: "page" as const,
119
+ OFFSET: "offset" as const,
120
+ CURSOR: "cursor" as const,
121
+ },
122
+ CURSOR_CLEANUP_INTERVAL: 60000, // Clean up old cursors every minute
123
+ MAX_CURSOR_MAP_SIZE: 1000, // Maximum number of cursors to keep in memory
124
+ // Cursor-specific virtual sizing
125
+ CURSOR_SCROLL_MARGIN_MULTIPLIER: 3, // Multiply rangeSize by this for scroll margin
126
+ CURSOR_MIN_VIRTUAL_SIZE_MULTIPLIER: 3, // Minimum virtual size as multiplier of rangeSize
127
+ },
128
+ };
129
+
130
+ /**
131
+ * Type for overriding constants at runtime
132
+ */
133
+ export type ViewportConstants = typeof VIEWPORT_CONSTANTS;
134
+
135
+ /**
136
+ * Helper function to merge user constants with defaults
137
+ */
138
+ export function mergeConstants(
139
+ userConstants: Partial<ViewportConstants> = {},
140
+ ): ViewportConstants {
141
+ return {
142
+ ...VIEWPORT_CONSTANTS,
143
+ ...userConstants,
144
+ };
145
+ }