@ethlete/components 0.1.0-next.6 → 0.1.0-next.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-next.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [`256b1b0`](https://github.com/ethlete-io/ethdk/commit/256b1b02d7598a0a6540af55447799a8ced469c4) Thanks [@TomTomB](https://github.com/TomTomB)! - Fix grid overlapping items with invalid base config
8
+
3
9
  ## 0.1.0-next.6
4
10
 
5
11
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { ChangeDetectionStrategy, ViewEncapsulation, Component, Directive, input, booleanAttribute, signal, effect, numberAttribute, computed, inject, ElementRef, InjectionToken, afterNextRender, untracked, DestroyRef, model, viewChild, Injector, output, ApplicationRef, EnvironmentInjector, DOCUMENT, createEnvironmentInjector, createComponent, TemplateRef, linkedSignal, afterRenderEffect, contentChild, inputBinding, PLATFORM_ID, isDevMode, contentChildren } from '@angular/core';
3
3
  import * as i2 from '@ethlete/core';
4
- import { injectStyleManager, createCanAnimateSignal, ColorInteractiveDirective, ProvideColorDirective, RuntimeError, injectErrorTheme, signalElementDimensions, createProvider, AnimatableDirective, ColorInteractiveContainerDirective, ColorInteractiveExcludeDirective, ProvideSurfaceDirective, SURFACE_PROVIDER, injectSurfaceThemes, resolveSurfaceByElevation, ColorInteractiveHasFocusDirective, createStaticRootProvider, signalHostElementDimensions, signalHostStyles, DragHandleDirective, injectRenderer, injectLocale, ResizeHandlesComponent, ANIMATED_LIFECYCLE_TOKEN, injectSurfaceContextTracker, AnimatedLifecycleDirective, createRootProvider, injectOverlayRuntime, typedBreakpointTransform, signalElementChildren, signalElementScrollState, signalElementIntersection, signalHostAttributes, signalHostClasses, getScrollContainerTarget, getScrollItemTarget, getElementScrollCoordinates, provideBreakpointInstance, signalClasses, useCursorDragScroll, signalStyles, getScrollSnapTarget, ScrollObserverDirective, ScrollObserverStartDirective, ScrollObserverEndDirective, injectViewportSize, createFlipAnimation, signalHostElementIntersection, injectHostElement, setInputSignal, SurfaceInteractiveDirective, injectRouterEvent, canUseSessionMemory, createAutoSessionMemoryKey, createSessionMemory, COLOR_PROVIDER, injectFocusVisibleTracker } from '@ethlete/core';
4
+ import { injectStyleManager, createCanAnimateSignal, ColorInteractiveDirective, ProvideColorDirective, RuntimeError, injectErrorTheme, signalElementDimensions, createProvider, AnimatableDirective, ColorInteractiveContainerDirective, ColorInteractiveExcludeDirective, ProvideSurfaceDirective, SURFACE_PROVIDER, injectSurfaceThemes, resolveSurfaceByElevation, ColorInteractiveHasFocusDirective, createStaticRootProvider, signalHostElementDimensions, DragHandleDirective, injectRenderer, injectLocale, ResizeHandlesComponent, ANIMATED_LIFECYCLE_TOKEN, injectSurfaceContextTracker, AnimatedLifecycleDirective, createRootProvider, injectOverlayRuntime, typedBreakpointTransform, signalElementChildren, signalElementScrollState, signalElementIntersection, signalHostAttributes, signalHostClasses, signalHostStyles, getScrollContainerTarget, getScrollItemTarget, getElementScrollCoordinates, provideBreakpointInstance, signalClasses, useCursorDragScroll, signalStyles, getScrollSnapTarget, ScrollObserverDirective, ScrollObserverStartDirective, ScrollObserverEndDirective, injectViewportSize, createFlipAnimation, signalHostElementIntersection, injectHostElement, setInputSignal, SurfaceInteractiveDirective, injectRouterEvent, canUseSessionMemory, createAutoSessionMemoryKey, createSessionMemory, COLOR_PROVIDER, injectFocusVisibleTracker } from '@ethlete/core';
5
5
  import { NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
6
6
  import { toObservable, takeUntilDestroyed, outputToObservable, outputFromObservable, rxResource } from '@angular/core/rxjs-interop';
7
7
  import { switchMap, EMPTY, filter, tap, timer, take, of, animationFrameScheduler, Subject, fromEvent, debounceTime, map, exhaustMap, takeUntil, finalize, distinctUntilChanged, Observable, shareReplay, catchError, interval } from 'rxjs';
@@ -2385,14 +2385,38 @@ const itemsCollide = (a, b) => {
2385
2385
  * Returns the first item that collides with the given position, or undefined if none.
2386
2386
  */
2387
2387
  const findCollision = (options) => options.entries.find((entry) => entry.id !== options.excludeId && itemsCollide(entry.position, options.position));
2388
+ /**
2389
+ * Clamps a position so it fits horizontally within the given column count.
2390
+ * Reduces an over-wide span and slides an out-of-bounds column back in range.
2391
+ */
2392
+ const clampToColumns = (position, columns) => {
2393
+ const colSpan = Math.max(1, Math.min(position.colSpan, columns));
2394
+ const col = Math.max(0, Math.min(position.col, columns - colSpan));
2395
+ return { ...position, col, colSpan };
2396
+ };
2388
2397
  /**
2389
2398
  * Compacts the layout vertically (moves items up as far as possible without collision).
2399
+ *
2400
+ * Also acts as a self-healing normaliser: positions that overflow the grid horizontally
2401
+ * (e.g. stale data from a wider breakpoint clamped into a narrower one — a colSpan of 12
2402
+ * or a col of 8 in a 6-column grid) are first clamped back into bounds, then any items
2403
+ * left overlapping are pushed down before the upward compaction runs. This guarantees the
2404
+ * returned layout is always in-bounds and overlap-free regardless of the input — clamping
2405
+ * a column alone is not enough, because it can drop an item on top of an existing one.
2390
2406
  */
2391
- const compactLayout = (entries, _columns) => {
2392
- const sorted = [...entries].sort((a, b) => a.position.row - b.position.row || a.position.col - b.position.col);
2407
+ const compactLayout = (entries, columns) => {
2408
+ const sorted = [...entries]
2409
+ .map((entry) => ({ ...entry, position: clampToColumns(entry.position, columns) }))
2410
+ .sort((a, b) => a.position.row - b.position.row || a.position.col - b.position.col);
2393
2411
  const compacted = [];
2394
2412
  for (const entry of sorted) {
2395
2413
  const candidate = { ...entry, position: { ...entry.position } };
2414
+ // Push down past any already-placed item it overlaps. This resolves overlaps introduced
2415
+ // by clamping out-of-bounds columns (which the upward pass below cannot fix on its own).
2416
+ while (findCollision({ entries: compacted, position: candidate.position, excludeId: candidate.id })) {
2417
+ candidate.position.row += 1;
2418
+ }
2419
+ // Then pull up as far as possible without colliding.
2396
2420
  while (candidate.position.row > 0) {
2397
2421
  const moved = { ...candidate, position: { ...candidate.position, row: candidate.position.row - 1 } };
2398
2422
  if (findCollision({ entries: compacted, position: moved.position, excludeId: candidate.id })) {
@@ -3302,10 +3326,8 @@ class GridItemDirective {
3302
3326
  this.currentRow = computed(() => this.renderPosition()?.row ?? 0, ...(ngDevMode ? [{ debugName: "currentRow" }] : /* istanbul ignore next */ []));
3303
3327
  this.currentColSpan = computed(() => this.renderPosition()?.colSpan ?? 1, ...(ngDevMode ? [{ debugName: "currentColSpan" }] : /* istanbul ignore next */ []));
3304
3328
  this.currentRowSpan = computed(() => this.renderPosition()?.rowSpan ?? 1, ...(ngDevMode ? [{ debugName: "currentRowSpan" }] : /* istanbul ignore next */ []));
3305
- this.hostStyles = signalHostStyles({
3306
- 'grid-column': computed(() => `${this.currentCol() + 1} / span ${this.currentColSpan()}`),
3307
- 'grid-row': computed(() => `${this.currentRow() + 1} / span ${this.currentRowSpan()}`),
3308
- });
3329
+ this.gridColumn = computed(() => `${this.currentCol() + 1} / span ${this.currentColSpan()}`, ...(ngDevMode ? [{ debugName: "gridColumn" }] : /* istanbul ignore next */ []));
3330
+ this.gridRow = computed(() => `${this.currentRow() + 1} / span ${this.currentRowSpan()}`, ...(ngDevMode ? [{ debugName: "gridRow" }] : /* istanbul ignore next */ []));
3309
3331
  effect((onCleanup) => {
3310
3332
  const id = this.itemId();
3311
3333
  const el = this.hostElement.nativeElement;
@@ -3336,7 +3358,7 @@ class GridItemDirective {
3336
3358
  });
3337
3359
  }
3338
3360
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: GridItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3339
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: GridItemDirective, isStandalone: true, selector: "[etGridItem]", inputs: { itemId: { classPropertyName: "itemId", publicName: "itemId", isSignal: true, isRequired: true, transformFunction: null }, minColSpan: { classPropertyName: "minColSpan", publicName: "minColSpan", isSignal: true, isRequired: false, transformFunction: null }, maxColSpan: { classPropertyName: "maxColSpan", publicName: "maxColSpan", isSignal: true, isRequired: false, transformFunction: null }, minRowSpan: { classPropertyName: "minRowSpan", publicName: "minRowSpan", isSignal: true, isRequired: false, transformFunction: null }, maxRowSpan: { classPropertyName: "maxRowSpan", publicName: "maxRowSpan", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-grid-item" }, exportAs: ["etGridItem"], ngImport: i0 }); }
3361
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: GridItemDirective, isStandalone: true, selector: "[etGridItem]", inputs: { itemId: { classPropertyName: "itemId", publicName: "itemId", isSignal: true, isRequired: true, transformFunction: null }, minColSpan: { classPropertyName: "minColSpan", publicName: "minColSpan", isSignal: true, isRequired: false, transformFunction: null }, maxColSpan: { classPropertyName: "maxColSpan", publicName: "maxColSpan", isSignal: true, isRequired: false, transformFunction: null }, minRowSpan: { classPropertyName: "minRowSpan", publicName: "minRowSpan", isSignal: true, isRequired: false, transformFunction: null }, maxRowSpan: { classPropertyName: "maxRowSpan", publicName: "maxRowSpan", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.grid-column": "gridColumn()", "style.grid-row": "gridRow()" }, classAttribute: "et-grid-item" }, exportAs: ["etGridItem"], ngImport: i0 }); }
3340
3362
  }
3341
3363
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: GridItemDirective, decorators: [{
3342
3364
  type: Directive,
@@ -3345,6 +3367,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
3345
3367
  exportAs: 'etGridItem',
3346
3368
  host: {
3347
3369
  class: 'et-grid-item',
3370
+ '[style.grid-column]': 'gridColumn()',
3371
+ '[style.grid-row]': 'gridRow()',
3348
3372
  },
3349
3373
  }]
3350
3374
  }], ctorParameters: () => [], propDecorators: { itemId: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemId", required: true }] }], minColSpan: [{ type: i0.Input, args: [{ isSignal: true, alias: "minColSpan", required: false }] }], maxColSpan: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxColSpan", required: false }] }], minRowSpan: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRowSpan", required: false }] }], maxRowSpan: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRowSpan", required: false }] }] } });
@@ -3359,12 +3383,10 @@ class GridDragDirective {
3359
3383
  this.renderer = injectRenderer();
3360
3384
  this.dragStartClient = signal(null, ...(ngDevMode ? [{ debugName: "dragStartClient" }] : /* istanbul ignore next */ []));
3361
3385
  this.dragPixelOffset = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "dragPixelOffset" }] : /* istanbul ignore next */ []));
3362
- this.hostStyles = signalHostStyles({
3363
- transform: computed(() => {
3364
- const offset = this.dragPixelOffset();
3365
- return `translate(${offset.x}px, ${offset.y}px)`;
3366
- }),
3367
- });
3386
+ this.dragTransform = computed(() => {
3387
+ const offset = this.dragPixelOffset();
3388
+ return `translate(${offset.x}px, ${offset.y}px)`;
3389
+ }, ...(ngDevMode ? [{ debugName: "dragTransform" }] : /* istanbul ignore next */ []));
3368
3390
  outputToObservable(this.dragHandle.dragStarted)
3369
3391
  .pipe(filter(() => !this.grid.readOnly()), tap(() => {
3370
3392
  this.dragPixelOffset.set({ x: 0, y: 0 });
@@ -3478,7 +3500,7 @@ class GridDragDirective {
3478
3500
  this.renderer.removeStyles(el, 'position', 'left', 'top', 'width', 'height');
3479
3501
  }
3480
3502
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: GridDragDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3481
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.13", type: GridDragDirective, isStandalone: true, selector: "[etGridDrag]", host: { properties: { "class.et-grid-drag--active": "!grid.readOnly() && dragHandle.isDragging()", "attr.aria-grabbed": "!grid.readOnly() && dragHandle.isDragging()" }, classAttribute: "et-grid-drag" }, hostDirectives: [{ directive: i2.DragHandleDirective, outputs: ["dragStarted", "dragStarted", "dragMoved", "dragMoved", "dragEnded", "dragEnded"] }], ngImport: i0 }); }
3503
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.13", type: GridDragDirective, isStandalone: true, selector: "[etGridDrag]", host: { properties: { "class.et-grid-drag--active": "!grid.readOnly() && dragHandle.isDragging()", "attr.aria-grabbed": "!grid.readOnly() && dragHandle.isDragging()", "style.transform": "dragTransform()" }, classAttribute: "et-grid-drag" }, hostDirectives: [{ directive: i2.DragHandleDirective, outputs: ["dragStarted", "dragStarted", "dragMoved", "dragMoved", "dragEnded", "dragEnded"] }], ngImport: i0 }); }
3482
3504
  }
3483
3505
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: GridDragDirective, decorators: [{
3484
3506
  type: Directive,
@@ -3494,6 +3516,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
3494
3516
  class: 'et-grid-drag',
3495
3517
  '[class.et-grid-drag--active]': '!grid.readOnly() && dragHandle.isDragging()',
3496
3518
  '[attr.aria-grabbed]': '!grid.readOnly() && dragHandle.isDragging()',
3519
+ '[style.transform]': 'dragTransform()',
3497
3520
  },
3498
3521
  }]
3499
3522
  }], ctorParameters: () => [] });
@@ -3623,17 +3646,15 @@ class GridComponent {
3623
3646
  const label = this.grid.readOnly() ? this.gridConfig.readonlyAriaLabel : this.gridConfig.interactiveAriaLabel;
3624
3647
  return this.gridConfig.transformer(label, this.locale.currentLocale());
3625
3648
  }, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
3626
- this.hostStyles = signalHostStyles({
3627
- '--_et-grid-columns': computed(() => `${this.grid.activeColumns()}`),
3628
- '--et-grid-gap': computed(() => `${this.grid.gap()}px`),
3629
- '--et-grid-row-height': computed(() => `${this.grid.rowHeight()}px`),
3630
- });
3649
+ this.gridColumns = computed(() => this.grid.activeColumns(), ...(ngDevMode ? [{ debugName: "gridColumns" }] : /* istanbul ignore next */ []));
3650
+ this.gridGap = computed(() => `${this.grid.gap()}px`, ...(ngDevMode ? [{ debugName: "gridGap" }] : /* istanbul ignore next */ []));
3651
+ this.gridRowHeight = computed(() => `${this.grid.rowHeight()}px`, ...(ngDevMode ? [{ debugName: "gridRowHeight" }] : /* istanbul ignore next */ []));
3631
3652
  effect(() => {
3632
3653
  this.grid.setGhostElement(this.ghostRef()?.nativeElement ?? null);
3633
3654
  });
3634
3655
  }
3635
3656
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: GridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3636
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: GridComponent, isStandalone: true, selector: "et-grid, [et-grid]", host: { attributes: { "role": "region" }, properties: { "attr.aria-label": "ariaLabel()" }, classAttribute: "et-grid" }, viewQueries: [{ propertyName: "ghostRef", first: true, predicate: ["ghostRef"], descendants: true, isSignal: true }], hostDirectives: [{ directive: GridDirective, inputs: ["breakpoints", "breakpoints", "rowHeight", "rowHeight", "gap", "gap", "initialItems", "initialItems", "readOnly", "readOnly"], outputs: ["layoutChange", "layoutChange"] }], ngImport: i0, template: `
3657
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: GridComponent, isStandalone: true, selector: "et-grid, [et-grid]", host: { attributes: { "role": "region" }, properties: { "attr.aria-label": "ariaLabel()", "style.--_et-grid-columns": "gridColumns()", "style.--et-grid-gap": "gridGap()", "style.--et-grid-row-height": "gridRowHeight()" }, classAttribute: "et-grid" }, viewQueries: [{ propertyName: "ghostRef", first: true, predicate: ["ghostRef"], descendants: true, isSignal: true }], hostDirectives: [{ directive: GridDirective, inputs: ["breakpoints", "breakpoints", "rowHeight", "rowHeight", "gap", "gap", "initialItems", "initialItems", "readOnly", "readOnly"], outputs: ["layoutChange", "layoutChange"] }], ngImport: i0, template: `
3637
3658
  <ng-content />
3638
3659
  @if (grid.ghostPosition(); as ghost) {
3639
3660
  <div
@@ -3667,6 +3688,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
3667
3688
  class: 'et-grid',
3668
3689
  role: 'region',
3669
3690
  '[attr.aria-label]': 'ariaLabel()',
3691
+ '[style.--_et-grid-columns]': 'gridColumns()',
3692
+ '[style.--et-grid-gap]': 'gridGap()',
3693
+ '[style.--et-grid-row-height]': 'gridRowHeight()',
3670
3694
  }, styles: ["@property --et-grid-gap{syntax: \"<length>\"; inherits: false; initial-value: 16px;}@property --et-grid-row-height{syntax: \"<length>\"; inherits: false; initial-value: 100px;}@property --et-grid-padding{syntax: \"<length>\"; inherits: false; initial-value: 0px;}.et-grid{display:grid;grid-template-columns:repeat(var(--_et-grid-columns, 12),1fr);grid-auto-rows:var(--et-grid-row-height);gap:var(--et-grid-gap);padding:var(--et-grid-padding);position:relative;min-height:0}.et-grid:has(.et-grid-item--dragging,.et-grid-item--resizing){-webkit-user-select:none;user-select:none;cursor:grabbing}.et-grid:has(.et-grid-item--resizing){cursor:nwse-resize}.et-grid-ghost{border-radius:8px;background:rgb(var(--et-surface-color, 23 23 23) / .08);border:2px dashed rgb(var(--et-surface-color, 23 23 23) / .2);pointer-events:none;transition:grid-column 0s,grid-row 0s}.et-grid--readonly .et-grid-item__drag-handle{cursor:default;pointer-events:none}.et-grid--readonly .et-grid-item__actions{display:none}.et-grid--readonly .et-grid-item:hover .et-resize-handle--e:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--s:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--w:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--n:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--se:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--sw:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--ne:after,.et-grid--readonly .et-grid-item:hover .et-resize-handle--nw:after{content:unset}\n"] }]
3671
3695
  }], ctorParameters: () => [], propDecorators: { ghostRef: [{ type: i0.ViewChild, args: ['ghostRef', { isSignal: true }] }] } });
3672
3696