ng-hub-ui-utils 22.15.2 β†’ 22.17.0

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/README.md CHANGED
@@ -93,31 +93,34 @@ Advanced system for creating overlays and floating components with flexible posi
93
93
  import { OverlayService, OverlayConfig } from 'ng-hub-ui-utils';
94
94
 
95
95
  @Component({
96
- selector: 'app-example'
96
+ selector: 'app-example'
97
97
  })
98
98
  export class ExampleComponent {
99
- constructor(private overlayService: OverlayService) {}
100
-
101
- openOverlay(elementRef: ElementRef) {
102
- // Create overlay with configuration
103
- const overlayRef = this.overlayService.create({
104
- hasBackdrop: true,
105
- backdropClass: 'custom-backdrop'
106
- });
107
-
108
- // Configure position strategy
109
- const positionStrategy = this.overlayService.position()
110
- .flexibleConnectedTo(elementRef)
111
- .withPositions([{
112
- originX: 'start',
113
- originY: 'bottom',
114
- overlayX: 'start',
115
- overlayY: 'top'
116
- }]);
117
-
118
- // Render a component (or a TemplateRef) into the overlay; you get the host element back
119
- const overlayElement = overlayRef.attach(MyComponent);
120
- }
99
+ constructor(private overlayService: OverlayService) {}
100
+
101
+ openOverlay(elementRef: ElementRef) {
102
+ // Create overlay with configuration
103
+ const overlayRef = this.overlayService.create({
104
+ hasBackdrop: true,
105
+ backdropClass: 'custom-backdrop'
106
+ });
107
+
108
+ // Configure position strategy
109
+ const positionStrategy = this.overlayService
110
+ .position()
111
+ .flexibleConnectedTo(elementRef)
112
+ .withPositions([
113
+ {
114
+ originX: 'start',
115
+ originY: 'bottom',
116
+ overlayX: 'start',
117
+ overlayY: 'top'
118
+ }
119
+ ]);
120
+
121
+ // Render a component (or a TemplateRef) into the overlay; you get the host element back
122
+ const overlayElement = overlayRef.attach(MyComponent);
123
+ }
121
124
  }
122
125
  ```
123
126
 
@@ -138,14 +141,14 @@ import { PopupService } from 'ng-hub-ui-utils';
138
141
 
139
142
  @Injectable()
140
143
  export class MyPopupService extends PopupService<MyPopupComponent> {
141
- constructor() {
142
- super(MyPopupComponent);
143
- }
144
-
145
- openPopup(content?: string | TemplateRef<any>) {
146
- const { windowRef, transition$ } = super.open(content, {}, true);
147
- return { windowRef, transition$ };
148
- }
144
+ constructor() {
145
+ super(MyPopupComponent);
146
+ }
147
+
148
+ openPopup(content?: string | TemplateRef<any>) {
149
+ const { windowRef, transition$ } = super.open(content, {}, true);
150
+ return { windowRef, transition$ };
151
+ }
149
152
  }
150
153
  ```
151
154
 
@@ -248,14 +251,14 @@ import { GetPipe, IsStringPipe, IsObjectPipe, IsObservablePipe, UcfirstPipe, Unw
248
251
  imports: [GetPipe, IsStringPipe, UcfirstPipe, UnwrapAsyncPipe],
249
252
  template: `
250
253
  <!-- Safe nested property access -->
251
- <p>{{ user | get : 'address.city' : 'Unknown' }}</p>
254
+ <p>{{ user | get: 'address.city' : 'Unknown' }}</p>
252
255
 
253
256
  <!-- Capitalize first letter -->
254
257
  <h1>{{ title | ucfirst }}</h1>
255
258
 
256
259
  <!-- Type checking in templates -->
257
260
  @if (value | isString) {
258
- <span>It's a string: {{ value }}</span>
261
+ <span>It's a string: {{ value }}</span>
259
262
  }
260
263
 
261
264
  <!-- Unwrap Observable or direct value -->
@@ -272,12 +275,12 @@ export class ExampleComponent {
272
275
 
273
276
  **Available Pipes:**
274
277
 
275
- - **GetPipe** (`get`): Safe nested property access with default values
276
- - **IsStringPipe** (`isString`): Check if value is a string
277
- - **IsObjectPipe** (`isObject`): Check if value is an object
278
- - **IsObservablePipe** (`isObservable`): Check if value is an Observable
279
- - **UcfirstPipe** (`ucfirst`): Capitalize first letter of a string
280
- - **UnwrapAsyncPipe** (`unwrapAsync`): Unwrap Observable or return direct value
278
+ - **GetPipe** (`get`): Safe nested property access with default values
279
+ - **IsStringPipe** (`isString`): Check if value is a string
280
+ - **IsObjectPipe** (`isObject`): Check if value is an object
281
+ - **IsObservablePipe** (`isObservable`): Check if value is an Observable
282
+ - **UcfirstPipe** (`ucfirst`): Capitalize first letter of a string
283
+ - **UnwrapAsyncPipe** (`unwrapAsync`): Unwrap Observable or return direct value
281
284
 
282
285
  ### πŸ› οΈ General Utility Functions
283
286
 
@@ -383,6 +386,19 @@ export class ExampleComponent {}
383
386
  Inputs: `hubTooltip` (text), `hubTooltipPlacement` (`top` | `bottom` | `left` | `right`,
384
387
  default `top`), `hubTooltipDelay` (fade ms, default `150`), `hubTooltipOffset` (px, default `8`).
385
388
 
389
+ **The placement is a preference, not an instruction (since 22.16.0).** A tooltip that would open
390
+ off the edge of the window opens on the opposite side instead, at all four edges, and one centred
391
+ on a host near the inline edge is slid back inside rather than flipped β€” flipping does nothing for
392
+ an overflow on the cross axis. It gives way only when it genuinely has no room, so a tooltip that
393
+ did not need to move does not, and the fit is re-run while the label is open, on window resize and
394
+ on a scroll anywhere above it. Under RTL the inline edge is the other one and the flip follows it,
395
+ while `hubTooltipPlacement="left"` still means the host's left edge.
396
+
397
+ If you have been writing `hubTooltipPlacement="bottom"` by hand on every hint in a header, you can
398
+ stop: the attribute is still honoured wherever it fits, but it is no longer what keeps the label
399
+ on screen. The class on the bubble (`hub-tooltip--top`, `--bottom`, `--left`, `--right`) names the
400
+ side it ended up on, so an arrow styled off it follows the flip.
401
+
386
402
  > **`TooltipDirective` (`[tooltip]`) was removed in 22.14.0**, having been deprecated since
387
403
  > 22.9.0. Its bare input names (`tooltip`, `placement`, `delay`, `offset`) belonged to every
388
404
  > directive on the element that declared them, which is how it collided with `[hubDropdown]`'s
@@ -449,10 +465,7 @@ import { hubTooltipAdapter } from 'ng-hub-ui-utils';
449
465
  import { provideHubBadgeTooltip } from 'ng-hub-ui-badges';
450
466
  import { provideHubBreadcrumbTooltip } from 'ng-hub-ui-breadcrumbs';
451
467
 
452
- providers: [
453
- provideHubBadgeTooltip(hubTooltipAdapter),
454
- provideHubBreadcrumbTooltip(hubTooltipAdapter)
455
- ];
468
+ providers: [provideHubBadgeTooltip(hubTooltipAdapter), provideHubBreadcrumbTooltip(hubTooltipAdapter)];
456
469
  ```
457
470
 
458
471
  Inside this package the same token works the other way round: `[hubOverflowTooltip]`
@@ -497,7 +510,7 @@ import { toInteger, isString, ScrollBar, getFocusableBoundaryElements, GetPipe,
497
510
  template: `
498
511
  <div #container>
499
512
  <h1>{{ title | ucfirst }}</h1>
500
- <p>{{ user | get : 'name' : 'Anonymous' }}</p>
513
+ <p>{{ user | get: 'name' : 'Anonymous' }}</p>
501
514
  </div>
502
515
  `
503
516
  })
@@ -598,7 +611,7 @@ class HubTranslationService {
598
611
  ```typescript
599
612
  import { HubTranslationService } from 'ng-hub-ui-utils';
600
613
 
601
- @Component({ /* ... */ })
614
+ @Component({/* ... */})
602
615
  export class LanguageSwitcherComponent {
603
616
  private translationSvc = inject(HubTranslationService);
604
617
 
@@ -638,81 +651,81 @@ These functions back the i18n system and are exported for direct use:
638
651
 
639
652
  ### Conversion Functions
640
653
 
641
- - `toInteger(value: any): number` - Safely converts to integer
642
- - `toString(value: any): string` - Converts to string handling null/undefined
643
- - `getValueInRange(value: number, max: number, min?: number): number` - Limits value to range
644
- - `padNumber(value: number): string` - Adds leading zero to numbers
654
+ - `toInteger(value: any): number` - Safely converts to integer
655
+ - `toString(value: any): string` - Converts to string handling null/undefined
656
+ - `getValueInRange(value: number, max: number, min?: number): number` - Limits value to range
657
+ - `padNumber(value: number): string` - Adds leading zero to numbers
645
658
 
646
659
  ### Validation Functions
647
660
 
648
- - `isString(value: any): value is string` - Checks if value is a string
649
- - `isNumber(value: any): value is number` - Checks if value is a valid number
650
- - `isInteger(value: any): value is number` - Checks if value is an integer
651
- - `isDefined(value: any): boolean` - Checks if not null/undefined
652
- - `isPromise<T>(v: any): v is Promise<T>` - Checks if value is a Promise
661
+ - `isString(value: any): value is string` - Checks if value is a string
662
+ - `isNumber(value: any): value is number` - Checks if value is a valid number
663
+ - `isInteger(value: any): value is number` - Checks if value is an integer
664
+ - `isDefined(value: any): boolean` - Checks if not null/undefined
665
+ - `isPromise<T>(v: any): v is Promise<T>` - Checks if value is a Promise
653
666
 
654
667
  ### String Functions
655
668
 
656
- - `regExpEscape(text: string): string` - Escapes special characters for RegExp
657
- - `removeAccents(str: string): string` - Removes accents from text
658
- - `interpolateString(expr?: string, params?: any, templateMatcher?: RegExp): string` - Replaces `{{ token }}` placeholders
659
- - `generateUniqueId(length: number): string` - Random alphanumeric id, for a DOM node that needs one
669
+ - `regExpEscape(text: string): string` - Escapes special characters for RegExp
670
+ - `removeAccents(str: string): string` - Removes accents from text
671
+ - `interpolateString(expr?: string, params?: any, templateMatcher?: RegExp): string` - Replaces `{{ token }}` placeholders
672
+ - `generateUniqueId(length: number): string` - Random alphanumeric id, for a DOM node that needs one
660
673
 
661
674
  ### Object Functions
662
675
 
663
- - `equals(o1: any, o2: any): boolean` - Deep equality
664
- - `getValue(target: any, key: string): any` - Reads a nested value by dot-notation key
665
- - `isObject(item: any): boolean` - Whether the value is a non-array object
666
- - `mergeDeep(target: any, source: any): any` - Recursive merge; the only deep object helper in the package
676
+ - `equals(o1: any, o2: any): boolean` - Deep equality
677
+ - `getValue(target: any, key: string): any` - Reads a nested value by dot-notation key
678
+ - `isObject(item: any): boolean` - Whether the value is a non-array object
679
+ - `mergeDeep(target: any, source: any): any` - Recursive merge; the only deep object helper in the package
667
680
 
668
681
  ### Signal Utilities
669
682
 
670
- - `debouncedSignal<T>(source: Signal<T>, delay?: number | Signal<number>): Signal<T>` - Mirrors a signal, delaying each change; the delay can itself be a signal
683
+ - `debouncedSignal<T>(source: Signal<T>, delay?: number | Signal<number>): Signal<T>` - Mirrors a signal, delaying each change; the delay can itself be a signal
671
684
 
672
685
  ### DOM Functions
673
686
 
674
- - `closest(element: HTMLElement, selector?: string): HTMLElement | null` - Finds parent element by selector
675
- - `reflow(element: HTMLElement): DOMRect` - Forces browser reflow
676
- - `getActiveElement(root?: Document | ShadowRoot): Element | null` - Gets active element including Shadow DOM
687
+ - `closest(element: HTMLElement, selector?: string): HTMLElement | null` - Finds parent element by selector
688
+ - `reflow(element: HTMLElement): DOMRect` - Forces browser reflow
689
+ - `getActiveElement(root?: Document | ShadowRoot): Element | null` - Gets active element including Shadow DOM
677
690
 
678
691
  ### Accent Resolution
679
692
 
680
- - `resolveHubAccent(value: string | null | undefined): string | null` - The "any colour" accent resolver shared across the family: a bareword becomes `var(--hub-sys-color-<name>, <name>)`, a literal `#hex` / `rgb()` / `oklch()` / `var()` passes through unchanged, and an empty value yields `null`
693
+ - `resolveHubAccent(value: string | null | undefined): string | null` - The "any colour" accent resolver shared across the family: a bareword becomes `var(--hub-sys-color-<name>, <name>)`, a literal `#hex` / `rgb()` / `oklch()` / `var()` passes through unchanged, and an empty value yields `null`
681
694
 
682
695
  ### Colour Functions
683
696
 
684
- - `parseColor(value): HubRgb | null` - Parses hex (3/4/6/8), `rgb()`, `hsl()`, `oklch()`, `oklab()`, the 148 CSS named colours and `transparent`, in modern and legacy syntax. No DOM, so it runs under SSR. Returns `null` β€” never throws β€” for anything it cannot resolve, `var()` and `currentColor` included
685
- - `toRgb(color): HubRgb | null` - Normalises a string or parsed colour to channels
686
- - `toHex(color): string | null` - Renders as `#rrggbb`, or `#rrggbbaa` when translucent
687
- - `isValidColor(value): boolean` - Whether the parser can resolve the string
688
- - `HUB_NAMED_COLORS: Readonly<Record<string, string>>` - The 148 CSS named colours
697
+ - `parseColor(value): HubRgb | null` - Parses hex (3/4/6/8), `rgb()`, `hsl()`, `oklch()`, `oklab()`, the 148 CSS named colours and `transparent`, in modern and legacy syntax. No DOM, so it runs under SSR. Returns `null` β€” never throws β€” for anything it cannot resolve, `var()` and `currentColor` included
698
+ - `toRgb(color): HubRgb | null` - Normalises a string or parsed colour to channels
699
+ - `toHex(color): string | null` - Renders as `#rrggbb`, or `#rrggbbaa` when translucent
700
+ - `isValidColor(value): boolean` - Whether the parser can resolve the string
701
+ - `HUB_NAMED_COLORS: Readonly<Record<string, string>>` - The 148 CSS named colours
689
702
 
690
703
  ### Contrast Functions
691
704
 
692
- - `relativeLuminance(color): number | null` - WCAG 2 relative luminance, 0 to 1
693
- - `contrastRatio(a, b): number | null` - WCAG 2 contrast ratio, 1 to 21
694
- - `contrastAPCA(text, background): number | null` - APCA lightness contrast, polarity-aware
695
- - `compositeOver(foreground, background): HubColor` - Blends translucent over opaque
696
- - `readableOn(background, metric?): string` - Black or white, whichever reads better. Defaults to `'lightness'`, the same decision `--hub-sys-color-*-on` makes in CSS; `'apca'` and `'wcag'` are also available
697
- - `HUB_INK_LIGHTNESS_THRESHOLD: number` - The OKLCh lightness above which a surface takes dark ink
705
+ - `relativeLuminance(color): number | null` - WCAG 2 relative luminance, 0 to 1
706
+ - `contrastRatio(a, b): number | null` - WCAG 2 contrast ratio, 1 to 21
707
+ - `contrastAPCA(text, background): number | null` - APCA lightness contrast, polarity-aware
708
+ - `compositeOver(foreground, background): HubColor` - Blends translucent over opaque
709
+ - `readableOn(background, metric?): string` - Black or white, whichever reads better. Defaults to `'lightness'`, the same decision `--hub-sys-color-*-on` makes in CSS; `'apca'` and `'wcag'` are also available
710
+ - `HUB_INK_LIGHTNESS_THRESHOLD: number` - The OKLCh lightness above which a surface takes dark ink
698
711
 
699
712
  ### OKLCh Functions
700
713
 
701
- - `rgbToOklch(color): HubOklch` / `oklchToRgb(color): HubRgb` - Conversions in the space the design system mixes in
702
- - `maxSrgbChroma(l, h): number` - Highest in-gamut chroma for a hue at a lightness. The sRGB gamut is not a cylinder β€” at L 0.578 blue reaches 0.232 and amber only 0.119 β€” so a palette cannot give every hue the same absolute chroma
703
- - `isInSrgbGamut(color): boolean` - Whether the colour survives the trip to sRGB
704
- - `clampToSrgbGamut(color): HubOklch` - Reduces chroma until it fits, preserving lightness and hue
714
+ - `rgbToOklch(color): HubOklch` / `oklchToRgb(color): HubRgb` - Conversions in the space the design system mixes in
715
+ - `maxSrgbChroma(l, h): number` - Highest in-gamut chroma for a hue at a lightness. The sRGB gamut is not a cylinder β€” at L 0.578 blue reaches 0.232 and amber only 0.119 β€” so a palette cannot give every hue the same absolute chroma
716
+ - `isInSrgbGamut(color): boolean` - Whether the colour survives the trip to sRGB
717
+ - `clampToSrgbGamut(color): HubOklch` - Reduces chroma until it fits, preserving lightness and hue
705
718
 
706
719
  ### Palette Derivation
707
720
 
708
721
  One brand colour, the whole palette β€” and the two numbers that keep it honest.
709
722
 
710
- - `harmoniseSemantics(primary, options?): HubSemanticPalette | null` - Rotates `success`, `warning`, `danger` and `info` towards the brand's hue and returns them as hex. Lightness is left exactly where the anchor had it, because it is what carries the contrast each role was chosen for; chroma is reduced only when the new hue cannot hold it inside sRGB
711
- - `tintNeutrals(primary, options?): HubNeutralRamp | null` - Leans the grey ramp (`100` … `900`) the same way, keeping each step's lightness
712
- - `HUB_MAX_HUE_SHIFT: number` (15) - How far a role may rotate, in degrees. Not taste: success and danger sit 135.8Β° apart in OKLCh, and a viewer with deuteranopia separates them by hue alone. A brand hue between the two pulls both inwards, so the gap closes by up to twice the cap; at 22.9Β° it would reach the 90Β° floor. 15Β° leaves the worst case at 105.8Β°
713
- - `HUB_MAX_NEUTRAL_CHROMA: number` (0.015) - The most chroma a tinted neutral may carry. Anchored on the ramp the design system already ships β€” `gray-600` measures 0.0165 and `gray-500` 0.0145 β€” so a tinted ramp is never more colourful than the grey people already accept as grey
714
- - `HUB_SEMANTIC_ANCHORS` / `HUB_NEUTRAL_ANCHORS` - The untinted starting points, so a product that harmonises nothing still gets the palette the stylesheet ships
715
- - Types: `HubSemanticRole`, `HubSemanticPalette`, `HubNeutralStep`, `HubNeutralRamp`, `HubHarmoniseOptions`, `HubTintNeutralsOptions`
723
+ - `harmoniseSemantics(primary, options?): HubSemanticPalette | null` - Rotates `success`, `warning`, `danger` and `info` towards the brand's hue and returns them as hex. Lightness is left exactly where the anchor had it, because it is what carries the contrast each role was chosen for; chroma is reduced only when the new hue cannot hold it inside sRGB
724
+ - `tintNeutrals(primary, options?): HubNeutralRamp | null` - Leans the grey ramp (`100` … `900`) the same way, keeping each step's lightness
725
+ - `HUB_MAX_HUE_SHIFT: number` (15) - How far a role may rotate, in degrees. Not taste: success and danger sit 135.8Β° apart in OKLCh, and a viewer with deuteranopia separates them by hue alone. A brand hue between the two pulls both inwards, so the gap closes by up to twice the cap; at 22.9Β° it would reach the 90Β° floor. 15Β° leaves the worst case at 105.8Β°
726
+ - `HUB_MAX_NEUTRAL_CHROMA: number` (0.015) - The most chroma a tinted neutral may carry. Anchored on the ramp the design system already ships β€” `gray-600` measures 0.0165 and `gray-500` 0.0145 β€” so a tinted ramp is never more colourful than the grey people already accept as grey
727
+ - `HUB_SEMANTIC_ANCHORS` / `HUB_NEUTRAL_ANCHORS` - The untinted starting points, so a product that harmonises nothing still gets the palette the stylesheet ships
728
+ - Types: `HubSemanticRole`, `HubSemanticPalette`, `HubNeutralStep`, `HubNeutralRamp`, `HubHarmoniseOptions`, `HubTintNeutralsOptions`
716
729
 
717
730
  ```typescript
718
731
  import { harmoniseSemantics, tintNeutrals } from 'ng-hub-ui-utils';
@@ -728,9 +741,9 @@ is rounding noise, and harmonising towards it would rotate every role in a direc
728
741
 
729
742
  ### Focus Functions
730
743
 
731
- - `getFocusableBoundaryElements(element: HTMLElement): HTMLElement[]` - Gets first and last focusable elements
732
- - `hubFocusTrap(zone, element, stopFocusTrap$, refocusOnClick?)` - Creates focus trap for modals/overlays
733
- - `FOCUSABLE_ELEMENTS_SELECTOR: string` - CSS selector for focusable elements
744
+ - `getFocusableBoundaryElements(element: HTMLElement): HTMLElement[]` - Gets first and last focusable elements
745
+ - `hubFocusTrap(zone, element, stopFocusTrap$, refocusOnClick?)` - Creates focus trap for modals/overlays
746
+ - `FOCUSABLE_ELEMENTS_SELECTOR: string` - CSS selector for focusable elements
734
747
 
735
748
  ### Drag and Drop
736
749
 
@@ -738,19 +751,19 @@ The engine-agnostic half of native HTML5 drag and drop, shared by the libraries
738
751
  implement it. The UI primitives β€” handle, placeholder and preview directives β€” stay in each
739
752
  library, because their selectors and data models differ.
740
753
 
741
- - `HubDragDropService` - Root-provided coordinator. A drag spans two component instances and the native `dataTransfer` payload is unreadable during `dragover`, so a shared service is the only reliable channel for what is being dragged and from where. Owners `register()` / `unregister()`; `begin()`, `setTarget()` and the readonly `active` / `target` / `isDragging` signals report the drag in progress. It coordinates state only β€” it never mutates your collections
742
- - `moveItemInArray<T>(array, fromIndex, toIndex): void` / `transferArrayItem<T>(source, target, fromIndex, toIndex): void` / `copyArrayItem<T>(source, target, fromIndex, toIndex): void` - In-place array moves, mirroring the `@angular/cdk` helpers of the same names
743
- - `clamp(value, max)`, `computeTargetIndex(...)`, `toAbsoluteIndex(...)`, `containsNode(...)` - Index arithmetic for sliced and nested lists
744
- - `resolveDropPosition(...)` with `DropRect` and `DragAxis` - Where a pointer sits relative to an item: `'before'` or `'after'`, on a vertical, horizontal or grid axis
745
- - `createNativeDragImage(...)` returning `DragImageResult` - Renders the drag preview the browser shows
746
- - `createPointerDragSession(config: PointerDragSessionConfig): PointerDragSession` - Pointer Events fallback for touch, where native drag events are not delivered
747
- - Types: `DropPosition`, `DragPointerMode`, `DragContainerRef<T>`, `ActiveDrag<T>`, `DragTarget<T>`, `DragRegistration`
754
+ - `HubDragDropService` - Root-provided coordinator. A drag spans two component instances and the native `dataTransfer` payload is unreadable during `dragover`, so a shared service is the only reliable channel for what is being dragged and from where. Owners `register()` / `unregister()`; `begin()`, `setTarget()` and the readonly `active` / `target` / `isDragging` signals report the drag in progress. It coordinates state only β€” it never mutates your collections
755
+ - `moveItemInArray<T>(array, fromIndex, toIndex): void` / `transferArrayItem<T>(source, target, fromIndex, toIndex): void` / `copyArrayItem<T>(source, target, fromIndex, toIndex): void` - In-place array moves, mirroring the `@angular/cdk` helpers of the same names
756
+ - `clamp(value, max)`, `computeTargetIndex(...)`, `toAbsoluteIndex(...)`, `containsNode(...)` - Index arithmetic for sliced and nested lists
757
+ - `resolveDropPosition(...)` with `DropRect` and `DragAxis` - Where a pointer sits relative to an item: `'before'` or `'after'`, on a vertical, horizontal or grid axis
758
+ - `createNativeDragImage(...)` returning `DragImageResult` - Renders the drag preview the browser shows
759
+ - `createPointerDragSession(config: PointerDragSessionConfig): PointerDragSession` - Pointer Events fallback for touch, where native drag events are not delivered
760
+ - Types: `DropPosition`, `DragPointerMode`, `DragContainerRef<T>`, `ActiveDrag<T>`, `DragTarget<T>`, `DragRegistration`
748
761
 
749
762
  ### Directives
750
763
 
751
- - `HubTooltipDirective` (`[hubTooltip]`) - Tooltip on hover/focus. Inputs: `hubTooltip`, `hubTooltipPlacement`, `hubTooltipDelay`, `hubTooltipOffset`
752
- - `HubOverflowTooltipDirective` (`[hubOverflowTooltip]`) - Tooltip shown only while the label is truncated. Inputs: `hubOverflowTooltip`, `placement`, `hubOverflowTooltipMeasure` (CSS selector, resolved inside the host, naming the box whose truncation decides it; defaults to the host)
753
- - `provideHubTooltip(adapter: HubTooltipAdapter)` and `HUB_TOOLTIP_ADAPTER` - Swap the implementation behind `[hubOverflowTooltip]`, app-wide or per subtree; defaults to `hubTooltipAdapter`
764
+ - `HubTooltipDirective` (`[hubTooltip]`) - Tooltip on hover/focus, flipped away from the window edges. Inputs: `hubTooltip`, `hubTooltipPlacement`, `hubTooltipDelay`, `hubTooltipOffset`
765
+ - `HubOverflowTooltipDirective` (`[hubOverflowTooltip]`) - Tooltip shown only while the label is truncated. Inputs: `hubOverflowTooltip`, `placement`, `hubOverflowTooltipMeasure` (CSS selector, resolved inside the host, naming the box whose truncation decides it; defaults to the host)
766
+ - `provideHubTooltip(adapter: HubTooltipAdapter)` and `HUB_TOOLTIP_ADAPTER` - Swap the implementation behind `[hubOverflowTooltip]`, app-wide or per subtree; defaults to `hubTooltipAdapter`
754
767
 
755
768
  ### Pipes
756
769
 
@@ -807,31 +820,31 @@ library, because their selectors and data models differ.
807
820
  ```typescript
808
821
  @Injectable({ providedIn: 'root' })
809
822
  class OverlayService {
810
- create(config?: OverlayConfig): OverlayRef;
811
- position(): OverlayPosition;
823
+ create(config?: OverlayConfig): OverlayRef;
824
+ position(): OverlayPosition;
812
825
  }
813
826
 
814
827
  class OverlayRef {
815
- // Renders a template or a component into the overlay and returns the host element,
816
- // not a ComponentRef: the overlay owns the view it created and tears it down itself.
817
- attach(content: TemplateRef<unknown> | Type<unknown>, viewContainerRef?: ViewContainerRef): HTMLElement;
818
- detach(): void;
819
- dispose(): void;
820
- hasAttached(): boolean;
821
- updatePosition(): void;
822
- onBackdropClick(callback: () => void): void;
823
- // Only the topmost open overlay is told, so a dropdown inside a dialog takes Escape
824
- // for itself and leaves the dialog open.
825
- onKeydown(callback: (event: KeyboardEvent) => void): void;
828
+ // Renders a template or a component into the overlay and returns the host element,
829
+ // not a ComponentRef: the overlay owns the view it created and tears it down itself.
830
+ attach(content: TemplateRef<unknown> | Type<unknown>, viewContainerRef?: ViewContainerRef): HTMLElement;
831
+ detach(): void;
832
+ dispose(): void;
833
+ hasAttached(): boolean;
834
+ updatePosition(): void;
835
+ onBackdropClick(callback: () => void): void;
836
+ // Only the topmost open overlay is told, so a dropdown inside a dialog takes Escape
837
+ // for itself and leaves the dialog open.
838
+ onKeydown(callback: (event: KeyboardEvent) => void): void;
826
839
  }
827
840
 
828
841
  class OverlayPosition {
829
- // The element the panel is anchored to. The overlay watches it and follows it when it moves.
830
- readonly origin: HTMLElement | null;
831
- flexibleConnectedTo(origin: ElementRef | HTMLElement): this;
832
- withPositions(positions: ConnectionPosition[]): this;
833
- // `start` / `end` are logical and read from the origin element; this overrides that.
834
- withDirection(direction: 'ltr' | 'rtl' | null): this;
842
+ // The element the panel is anchored to. The overlay watches it and follows it when it moves.
843
+ readonly origin: HTMLElement | null;
844
+ flexibleConnectedTo(origin: ElementRef | HTMLElement): this;
845
+ withPositions(positions: ConnectionPosition[]): this;
846
+ // `start` / `end` are logical and read from the origin element; this overrides that.
847
+ withDirection(direction: 'ltr' | 'rtl' | null): this;
835
848
  }
836
849
  ```
837
850
 
@@ -842,15 +855,55 @@ both text directions:
842
855
  ```typescript
843
856
  import { HUB_DROPDOWN_POSITIONS } from 'ng-hub-ui-utils';
844
857
 
845
- overlayService.position().flexibleConnectedTo(origin).withPositions([...HUB_DROPDOWN_POSITIONS]);
858
+ overlayService
859
+ .position()
860
+ .flexibleConnectedTo(origin)
861
+ .withPositions([...HUB_DROPDOWN_POSITIONS]);
846
862
  ```
847
863
 
864
+ #### Viewport fitting (`viewport-fit`)
865
+
866
+ The arithmetic behind both the overlay strategy and the tooltip, exported on its own so a floating
867
+ element that is neither does not have to write a third copy. It takes plain numbers, touches no DOM
868
+ and runs unchanged on a server render.
869
+
870
+ ```typescript
871
+ import { hubAnchorToViewport, hubToAnchorSide, hubToPhysicalSide, hubViewportOf } from 'ng-hub-ui-utils';
872
+
873
+ const rtl = getComputedStyle(host).direction === 'rtl';
874
+
875
+ const placed = hubAnchorToViewport({
876
+ anchor: host.getBoundingClientRect(),
877
+ box: { width: panel.offsetWidth, height: panel.offsetHeight },
878
+ viewport: hubViewportOf(panel), // null on a server render: nothing flips
879
+ side: hubToAnchorSide('bottom', rtl), // 'block-start' | 'block-end' | 'inline-start' | 'inline-end'
880
+ align: 'start', // logical: mirrors under RTL
881
+ offset: 8,
882
+ margin: 0,
883
+ rtl
884
+ });
885
+
886
+ // placed.x / placed.y are viewport coordinates β€” add scrollX / scrollY for an absolutely
887
+ // positioned element. placed.side is where it ended up, placed.flipped whether that was a flip.
888
+ panel.dataset['side'] = hubToPhysicalSide(placed.side, rtl);
889
+ ```
890
+
891
+ The requested side is kept unless it genuinely cannot hold the box **and** the opposite side has
892
+ more room, so nothing flips for free; the cross axis is clamped rather than flipped, since a box
893
+ centred on an anchor near the inline edge overflows there whichever side it opens on. Sides are
894
+ logical so a single fallback rule serves both text directions β€” `hubToAnchorSide` and
895
+ `hubToPhysicalSide` round-trip a physical edge through that axis without changing what the caller
896
+ asked for.
897
+
898
+ Also exported: `hubFitsInViewport(point, size, viewport, margin?)`,
899
+ `hubClampToViewport(point, size, viewport, margin?)` and `hubOppositeSide(side)`.
900
+
848
901
  #### ScrollBar Service
849
902
 
850
903
  ```typescript
851
904
  @Injectable({ providedIn: 'root' })
852
905
  class ScrollBar {
853
- hide(): ScrollbarReverter; // Hides scrollbar with compensation
906
+ hide(): ScrollbarReverter; // Hides scrollbar with compensation
854
907
  }
855
908
  ```
856
909
 
@@ -862,48 +915,48 @@ an injection context β€” as an `@Injectable()` subclass, or from a factory provi
862
915
 
863
916
  ```typescript
864
917
  class PopupService<T> {
865
- constructor(componentType: Type<T>);
866
- open(
867
- content?: string | TemplateRef<any>,
868
- templateContext?: any,
869
- animation?: boolean
870
- ): { windowRef: ComponentRef<T>; transition$: Observable<void> };
871
- close(animation?: boolean): Observable<void>;
918
+ constructor(componentType: Type<T>);
919
+ open(
920
+ content?: string | TemplateRef<any>,
921
+ templateContext?: any,
922
+ animation?: boolean
923
+ ): { windowRef: ComponentRef<T>; transition$: Observable<void> };
924
+ close(animation?: boolean): Observable<void>;
872
925
  }
873
926
 
874
927
  // The nodes and view a popup projects, returned internally by the content resolver.
875
928
  class ContentRef {
876
- constructor(nodes: Node[][], viewRef?: ViewRef, componentRef?: ComponentRef<any>);
929
+ constructor(nodes: Node[][], viewRef?: ViewRef, componentRef?: ComponentRef<any>);
877
930
  }
878
931
  ```
879
932
 
880
933
  ### Transition Utilities
881
934
 
882
- - `hubRunTransition<T>(zone, element, startFn, options)` - Advanced transition system with Observable
883
- - `hubCompleteTransition(element)` - Completes a running transition on an element
884
- - `getTransitionDurationMs(element)` - Gets CSS transition duration in milliseconds
885
- - `runInZone<T>(zone)` - RxJS operator to execute observables inside NgZone
935
+ - `hubRunTransition<T>(zone, element, startFn, options)` - Advanced transition system with Observable
936
+ - `hubCompleteTransition(element)` - Completes a running transition on an element
937
+ - `getTransitionDurationMs(element)` - Gets CSS transition duration in milliseconds
938
+ - `runInZone<T>(zone)` - RxJS operator to execute observables inside NgZone
886
939
 
887
940
  ## 🎨 Support Components
888
941
 
889
942
  This library doesn't include visual components, but support utilities used by other components in the Hub UI ecosystem:
890
943
 
891
- | Utility | Description | Used by |
892
- | --------------- | ----------------------------------- | -------------------------------------- |
893
- | Overlay Service | Flexible overlay positioning system | ng-hub-ui-modal, ng-hub-ui-portal |
894
- | Focus Trap | Focus management in modals/overlays | ng-hub-ui-modal, ng-hub-ui-portal |
895
- | Scrollbar | Scrollbar compensation | ng-hub-ui-modal, ng-hub-ui-portal |
896
- | Popup Service | Host for dynamically created popups | ng-hub-ui-modal, ng-hub-ui-portal |
897
- | Transitions | Smooth animations | ng-hub-ui-accordion, ng-hub-ui-modal |
898
- | Type Guards | Type validation functions | ng-hub-ui-stepper |
899
- | Pipes | Template utilities | All Hub UI components |
944
+ | Utility | Description | Used by |
945
+ | --------------- | ----------------------------------- | ------------------------------------ |
946
+ | Overlay Service | Flexible overlay positioning system | ng-hub-ui-modal, ng-hub-ui-portal |
947
+ | Focus Trap | Focus management in modals/overlays | ng-hub-ui-modal, ng-hub-ui-portal |
948
+ | Scrollbar | Scrollbar compensation | ng-hub-ui-modal, ng-hub-ui-portal |
949
+ | Popup Service | Host for dynamically created popups | ng-hub-ui-modal, ng-hub-ui-portal |
950
+ | Transitions | Smooth animations | ng-hub-ui-accordion, ng-hub-ui-modal |
951
+ | Type Guards | Type validation functions | ng-hub-ui-stepper |
952
+ | Pipes | Template utilities | All Hub UI components |
900
953
 
901
954
  ## 🀝 Compatibility
902
955
 
903
- - Angular 16+
904
- - TypeScript 4.8+
905
- - Node.js 16+
906
- - Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
956
+ - Angular 16+
957
+ - TypeScript 4.8+
958
+ - Node.js 16+
959
+ - Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
907
960
 
908
961
  ## πŸ› οΈ Development
909
962
 
@@ -977,10 +1030,10 @@ Recent highlights:
977
1030
 
978
1031
  ## πŸ› Issues and Support
979
1032
 
980
- - [Report a bug](https://github.com/hub-env/hub-ui/issues)
981
- - [Request a feature](https://github.com/hub-env/hub-ui/issues/new?template=feature_request.yml)
982
- - [Repository](https://github.com/hub-env/ng-hub-ui-utils)
983
- - **Author**: [Carlos Morcillo](https://www.carlosmorcillo.com)
1033
+ - [Report a bug](https://github.com/hub-env/hub-ui/issues)
1034
+ - [Request a feature](https://github.com/hub-env/hub-ui/issues/new?template=feature_request.yml)
1035
+ - [Repository](https://github.com/hub-env/ng-hub-ui-utils)
1036
+ - **Author**: [Carlos Morcillo](https://www.carlosmorcillo.com)
984
1037
 
985
1038
  ## β˜• Support the Project
986
1039
 
@@ -991,10 +1044,10 @@ If Hub UI has been useful to you, consider supporting its development:
991
1044
 
992
1045
  Your support helps to:
993
1046
 
994
- - πŸš€ Keep the project active
995
- - πŸ› Fix bugs faster
996
- - ✨ Develop new features
997
- - πŸ“š Improve documentation
1047
+ - πŸš€ Keep the project active
1048
+ - πŸ› Fix bugs faster
1049
+ - ✨ Develop new features
1050
+ - πŸ“š Improve documentation
998
1051
 
999
1052
  ## 🀝 Contributions
1000
1053
 
@@ -1008,6 +1061,14 @@ Contributions are welcome! Please:
1008
1061
 
1009
1062
  Check our [contribution guidelines](CONTRIBUTING.md) for more details.
1010
1063
 
1064
+ ## πŸ’Ό Commercial support
1065
+
1066
+ These libraries are maintained by [Carlos Morcillo FernΓ‘ndez](https://www.carlosmorcillo.com), a freelance frontend architect working with teams that build and maintain Angular applications.
1067
+
1068
+ If your team depends on Hub-UI and needs more than an issue thread can solve, that is my day job: architecture audits, design systems, Angular migrations and team mentoring. For projects that also need design and a full team, I run them through [Frog Hub](https://froghub.es), my development studio.
1069
+
1070
+ Have a look at [the services](https://www.carlosmorcillo.com/en/services/) or [tell me about your project](https://www.carlosmorcillo.com/en/contact/).
1071
+
1011
1072
  ## πŸ“„ License
1012
1073
 
1013
1074
  MIT Β© Hub UI contributors