@ethlete/core 5.0.0-next.23 → 5.0.0-next.25

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,37 @@
1
1
  # @ethlete/core
2
2
 
3
+ ## 5.0.0-next.25
4
+
5
+ ### Patch Changes
6
+
7
+ - [#3017](https://github.com/ethlete-io/ethdk/pull/3017) [`83853d6`](https://github.com/ethlete-io/ethdk/commit/83853d6138bc7d0a481b5b982c2a7137b9cc65e6) Thanks [@github-actions](https://github.com/apps/github-actions)! - Overlay: anchored overlays with `autoCloseIfReferenceHidden` now close instantly when their anchor is removed, instead of jumping to the viewport corner and animating out from there.
8
+
9
+ ## 5.0.0-next.24
10
+
11
+ ### Minor Changes
12
+
13
+ - [#3016](https://github.com/ethlete-io/ethdk/pull/3016) [`2f18e43`](https://github.com/ethlete-io/ethdk/commit/2f18e4344759dbbcd17ba0dbeca138f1f7043cdf) Thanks [@github-actions](https://github.com/apps/github-actions)! - Anchored overlays: the `shift` option now also accepts `{ crossAxis?: boolean }` in addition to a boolean. With `crossAxis: true`, an overlay that fits on neither side of its reference is shifted along the placement's cross axis to stay inside the viewport (it may then overlap the reference) instead of overflowing off-screen.
14
+
15
+ Menus enable this by default: a nested submenu near the viewport edge first flips to the other side and, when neither side fits, slides over its parent menu — matching native OS menu behavior — instead of being cut off by the viewport.
16
+
17
+ The `size` middleware (`autoResize`) now runs after `shift` instead of before it, so `--et-overlay-max-width` / `--et-overlay-max-height` are measured from the pane's shifted position. Previously a cross-axis-shifted pane had its max size capped to the unshifted leftover space, squeezing e.g. a submenu to a sliver instead of letting it keep its width while overlapping its parent.
18
+
19
+ ### Patch Changes
20
+
21
+ - [#3016](https://github.com/ethlete-io/ethdk/pull/3016) [`2f18e43`](https://github.com/ethlete-io/ethdk/commit/2f18e4344759dbbcd17ba0dbeca138f1f7043cdf) Thanks [@github-actions](https://github.com/apps/github-actions)! - Animations: fix overlays getting stuck in the DOM in an invisible but click-blocking state when their leave transition never reported completion.
22
+
23
+ When several anchored overlays close at once (e.g. clicking an item inside a nested menu closes the whole menu tree), destroying an ancestor pane shifts the anchored position of a still-leaving descendant. The browser then cancels and restarts the running transition, so its end event arrives either flagged as cancelled or under a stale transition id — and the animated lifecycle waited forever for an event that could no longer arrive, leaving the pane orphaned at `opacity: 0` while still intercepting pointer events. The lifecycle now treats the transition as settled once no animation is running anymore, matching the fallback the interrupted-transition path already had, so the overlay is torn down reliably.
24
+
25
+ - [#3016](https://github.com/ethlete-io/ethdk/pull/3016) [`3b359e5`](https://github.com/ethlete-io/ethdk/commit/3b359e52c5efa1cd9b0e79216fc578525aafa8e8) Thanks [@github-actions](https://github.com/apps/github-actions)! - Tailwind surface & color theme generators: fix the dynamic `--color-*-surface-*` /
26
+ `--color-*-theme-*` variables not resolving per scope. They were emitted only inside
27
+ `@theme`, which lands on `:root`, so `rgb(var(--<runtime>-surface-background))` resolved
28
+ once against the root surface and inherited that concrete color into descendants. As a
29
+ result `bg-<prefix>-surface-*` / `bg-<prefix>-theme-*` utilities ignored nested
30
+ `.<runtime>-surface--*` / `.<runtime>-color--*` scopes (e.g. an elevated surface would
31
+ still paint the root background). The dynamic colors are now also re-declared on each
32
+ surface/color selector in the alias block, so the utilities resolve against the nearest
33
+ scope while still being generated from `@theme`.
34
+
3
35
  ## 5.0.0-next.23
4
36
 
5
37
  ### Major Changes
package/README.md CHANGED
@@ -1,7 +1,33 @@
1
- # core
1
+ # @ethlete/core
2
2
 
3
- This library was generated with [Nx](https://nx.dev).
3
+ Angular framework primitives for the Ethlete SDK — signal-based DOM observation, the surface/color theming systems, the animation and overlay engines, and a toolbox of directives, pipes and utilities. Component-less; every other `@ethlete/*` UI library builds on it.
4
4
 
5
- ## Running unit tests
5
+ ## Installation
6
6
 
7
- Run `nx test core` to execute the unit tests.
7
+ ```bash
8
+ yarn add @ethlete/core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { injectCurrentBreakpoint, signalHostElementDimensions } from '@ethlete/core';
15
+
16
+ @Component({
17
+ /* … */
18
+ })
19
+ export class ExampleComponent {
20
+ breakpoint = injectCurrentBreakpoint();
21
+ dimensions = signalHostElementDimensions();
22
+ }
23
+ ```
24
+
25
+ ## Documentation
26
+
27
+ Full guides on the docs site:
28
+
29
+ - [Overview](https://ethlete-sdk-docs.web.app/core/)
30
+ - [Element signals](https://ethlete-sdk-docs.web.app/core/element-signals) & [signal utilities](https://ethlete-sdk-docs.web.app/core/signal-utils)
31
+ - [Theming](https://ethlete-sdk-docs.web.app/core/theming)
32
+ - [Animations](https://ethlete-sdk-docs.web.app/core/animations) & [overlay runtime](https://ethlete-sdk-docs.web.app/core/overlay-runtime)
33
+ - [Providers](https://ethlete-sdk-docs.web.app/core/providers), [SEO](https://ethlete-sdk-docs.web.app/core/seo) & [utilities](https://ethlete-sdk-docs.web.app/core/utilities)
@@ -3634,7 +3634,21 @@ class AnimatedLifecycleDirective {
3634
3634
  this.removeClass(fromClass);
3635
3635
  this.addClass(toClass);
3636
3636
  }
3637
- }), switchMap(() => this.animatable.animationEnd$), filter((e) => this.state$.value === expectedState && !e.cancelled && e.transitionId === transitionId), tap(onComplete), take(1), takeUntil(cancelSignal), takeUntilDestroyed(this.destroyRef))
3637
+ }), switchMap(() => this.animatable.animationEnd$), filter(() => this.state$.value === expectedState), switchMap((e) => {
3638
+ if (!e.cancelled && e.transitionId === transitionId) {
3639
+ return of(true);
3640
+ }
3641
+ // The batch ended cancelled or under a different id — e.g. the browser cancelled and
3642
+ // restarted a running transition mid-flight (an anchored overlay whose ancestor pane got
3643
+ // destroyed) or a foreign transition on the same element finished. If nothing is
3644
+ // animating anymore the expected end event can never arrive, so treat the transition as
3645
+ // settled instead of waiting forever (mirrors the fallback in handleInterruptedTransition).
3646
+ return this.animatable.isAnimating$.pipe(take(1), tap((stillAnimating) => {
3647
+ if (!stillAnimating) {
3648
+ this.debugLog(`${transitionId}: completes via fallback (batch id "${e.transitionId ?? 'none'}", cancelled ${e.cancelled})`);
3649
+ }
3650
+ }), map((stillAnimating) => !stillAnimating));
3651
+ }), filter((settled) => settled && this.state$.value === expectedState), tap(onComplete), take(1), takeUntil(cancelSignal), takeUntilDestroyed(this.destroyRef))
3638
3652
  .subscribe();
3639
3653
  }
3640
3654
  handleInterruptedTransition(config) {
@@ -4583,6 +4597,15 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
4583
4597
  fallbackPlacements: strategy.fallbackPlacements ?? undefined,
4584
4598
  fallbackAxisSideDirection: 'start',
4585
4599
  }));
4600
+ if (strategy.shift !== false) {
4601
+ middleware.push(shift({
4602
+ crossAxis: typeof strategy.shift === 'object' ? (strategy.shift.crossAxis ?? false) : false,
4603
+ limiter: limitShift(),
4604
+ padding: strategy.viewportPadding ?? 8,
4605
+ }));
4606
+ }
4607
+ // size must run AFTER shift so the available space is measured from the shifted position —
4608
+ // otherwise a cross-axis-shifted pane gets its max size capped to the unshifted leftover space
4586
4609
  if (strategy.autoResize) {
4587
4610
  middleware.push(size({
4588
4611
  padding: strategy.viewportPadding ?? 8,
@@ -4594,12 +4617,6 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
4594
4617
  },
4595
4618
  }));
4596
4619
  }
4597
- if (strategy.shift !== false) {
4598
- middleware.push(shift({
4599
- limiter: limitShift(),
4600
- padding: strategy.viewportPadding ?? 8,
4601
- }));
4602
- }
4603
4620
  if (arrowElement) {
4604
4621
  middleware.push(arrow({
4605
4622
  element: arrowElement,
@@ -4616,9 +4633,24 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
4616
4633
  strategy: 'absolute',
4617
4634
  middleware,
4618
4635
  }).then(({ x, y, placement, middlewareData }) => {
4636
+ // A detached reference reports a zeroed rect, so floating-ui positions the pane at the
4637
+ // viewport's top-left. Tear the overlay down (without animating from that bogus position)
4638
+ // before applying the transform when auto-close is on, and otherwise leave the pane where it
4639
+ // last was rather than snapping it to the corner.
4640
+ if (middlewareData.hide?.referenceHidden) {
4641
+ if (strategy.autoCloseIfReferenceHidden) {
4642
+ overlayRef.close(undefined, 'reference-detached');
4643
+ return;
4644
+ }
4645
+ if (strategy.autoHide) {
4646
+ renderer.setStyle(paneElement, { visibility: 'hidden' });
4647
+ }
4648
+ return;
4649
+ }
4619
4650
  renderer.setStyle(paneElement, {
4620
4651
  transform: `translate3d(${x}px, ${y}px, 0)`,
4621
4652
  width: mirrorWidthElement ? `${mirrorWidthElement.offsetWidth}px` : null,
4653
+ visibility: null,
4622
4654
  });
4623
4655
  // exposed so animations can compose their transforms with the anchored position
4624
4656
  renderer.setCssProperties(paneElement, {
@@ -4629,19 +4661,6 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
4629
4661
  if (arrowElement && middlewareData.arrow) {
4630
4662
  renderer.setCssProperty(arrowElement, '--et-floating-arrow-translate', `translate3d(${middlewareData.arrow.x ?? 0}px, ${middlewareData.arrow.y ?? 0}px, 0)`);
4631
4663
  }
4632
- if (middlewareData.hide?.referenceHidden) {
4633
- if (strategy.autoCloseIfReferenceHidden) {
4634
- overlayRef.close(undefined, 'api');
4635
- return;
4636
- }
4637
- renderer.setStyle(paneElement, {
4638
- visibility: strategy.autoHide ? 'hidden' : null,
4639
- });
4640
- return;
4641
- }
4642
- renderer.setStyle(paneElement, {
4643
- visibility: null,
4644
- });
4645
4664
  });
4646
4665
  });
4647
4666
  return () => {
@@ -4896,7 +4915,9 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
4896
4915
  }
4897
4916
  const lifecycle = getAnimatedLifecycle(componentRef);
4898
4917
  animationDebugLog(`runtime ${config.id}`, `close requested (source "${closeEvent.source}", lifecycle state "${lifecycle?.state$.value ?? 'none'}", delegate ${config.animationDelegate?.leave ? 'yes' : 'no'})`);
4899
- if (!lifecycle) {
4918
+ // The reference element is gone — there is nothing left to animate away from, so tear the
4919
+ // overlay down synchronously instead of playing a leave transition from a stale position.
4920
+ if (!lifecycle || closeEvent.source === 'reference-detached') {
4900
4921
  destroyMountedOverlay(closeEvent);
4901
4922
  return;
4902
4923
  }