@akcelik/strct 4.3.0 → 4.4.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, booleanAttribute, afterNextRender, Directive, model, output, HostListener, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, DestroyRef, TemplateRef, contentChild, viewChild, InjectionToken, untracked, Injector, viewChildren, Renderer2, Pipe, PLATFORM_ID, afterRenderEffect } from '@angular/core';
2
+ import { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, booleanAttribute, afterNextRender, Directive, model, output, HostListener, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID, forwardRef, DestroyRef, TemplateRef, contentChild, viewChild, InjectionToken, untracked, Injector, viewChildren, Renderer2, Pipe, afterRenderEffect } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { NgTemplateOutlet, DOCUMENT as DOCUMENT$1, isPlatformBrowser } from '@angular/common';
5
5
  import { RouterLink, RouterLinkActive } from '@angular/router';
@@ -1756,7 +1756,7 @@ class StrctProgress {
1756
1756
  >
1757
1757
  <div class="strct-progress__fill" [style.width.%]="clamped()"></div>
1758
1758
  </div>
1759
- `, isInline: true, styles: [".strct-progress{display:block}.strct-progress__track{height:6px;border-radius:var(--radius-sm);background:var(--bg-3);overflow:hidden}.strct-progress__fill{height:100%;border-radius:var(--radius-sm);background:var(--acc);transition:width .3s ease}.strct-progress--success .strct-progress__fill{background:var(--success)}.strct-progress--warning .strct-progress__fill{background:var(--warning)}.strct-progress--critical .strct-progress__fill{background:var(--critical)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1759
+ `, isInline: true, styles: [".strct-progress{display:block}.strct-progress__track{height:6px;border-radius:var(--radius-sm);background:var(--strct-progress-track, color-mix(in srgb, var(--t1) 12%, transparent));box-shadow:inset 0 0 0 1px var(--b2);overflow:hidden}.strct-progress__fill{height:100%;border-radius:var(--radius-sm);background:var(--acc);transition:width .3s ease}.strct-progress--success .strct-progress__fill{background:var(--success)}.strct-progress--warning .strct-progress__fill{background:var(--warning)}.strct-progress--critical .strct-progress__fill{background:var(--critical)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1760
1760
  }
1761
1761
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctProgress, decorators: [{
1762
1762
  type: Component,
@@ -1776,7 +1776,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
1776
1776
  '[class.strct-progress--success]': "resolvedStatus() === 'success'",
1777
1777
  '[class.strct-progress--warning]': "resolvedStatus() === 'warning'",
1778
1778
  '[class.strct-progress--critical]': "resolvedStatus() === 'critical'",
1779
- }, styles: [".strct-progress{display:block}.strct-progress__track{height:6px;border-radius:var(--radius-sm);background:var(--bg-3);overflow:hidden}.strct-progress__fill{height:100%;border-radius:var(--radius-sm);background:var(--acc);transition:width .3s ease}.strct-progress--success .strct-progress__fill{background:var(--success)}.strct-progress--warning .strct-progress__fill{background:var(--warning)}.strct-progress--critical .strct-progress__fill{background:var(--critical)}\n"] }]
1779
+ }, styles: [".strct-progress{display:block}.strct-progress__track{height:6px;border-radius:var(--radius-sm);background:var(--strct-progress-track, color-mix(in srgb, var(--t1) 12%, transparent));box-shadow:inset 0 0 0 1px var(--b2);overflow:hidden}.strct-progress__fill{height:100%;border-radius:var(--radius-sm);background:var(--acc);transition:width .3s ease}.strct-progress--success .strct-progress__fill{background:var(--success)}.strct-progress--warning .strct-progress__fill{background:var(--warning)}.strct-progress--critical .strct-progress__fill{background:var(--critical)}\n"] }]
1780
1780
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], thresholds: [{ type: i0.Input, args: [{ isSignal: true, alias: "thresholds", required: false }] }] } });
1781
1781
 
1782
1782
  /** Indeterminate loading ring. `<strct-spinner size="sm" />`. */
@@ -2791,6 +2791,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
2791
2791
  args: ['contextmenu', ['$event']]
2792
2792
  }] } });
2793
2793
 
2794
+ /**
2795
+ * Screen-reader announcements for state changes that have no visible text —
2796
+ * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
2797
+ *
2798
+ * private announcer = inject(StrctAnnouncer);
2799
+ * this.announcer.announce('12 rows loaded');
2800
+ *
2801
+ * Maintains one visually-hidden live region per politeness level; repeated
2802
+ * identical messages are re-announced (the region is cleared first).
2803
+ */
2804
+ class StrctAnnouncer {
2805
+ regions = new Map();
2806
+ clearTimers = new Map();
2807
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2808
+ announce(message, politeness = 'polite') {
2809
+ if (!this.isBrowser)
2810
+ return;
2811
+ const region = this.regionFor(politeness);
2812
+ // Clear first so identical consecutive messages still fire.
2813
+ region.textContent = '';
2814
+ setTimeout(() => (region.textContent = message));
2815
+ clearTimeout(this.clearTimers.get(politeness));
2816
+ // Stale announcements should not linger for the next SR user to stumble on.
2817
+ this.clearTimers.set(politeness, setTimeout(() => (region.textContent = ''), 10_000));
2818
+ }
2819
+ regionFor(politeness) {
2820
+ let region = this.regions.get(politeness);
2821
+ if (!region) {
2822
+ region = document.createElement('div');
2823
+ region.setAttribute('aria-live', politeness);
2824
+ region.setAttribute('aria-atomic', 'true');
2825
+ region.className = 'strct-announcer';
2826
+ region.style.cssText =
2827
+ 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
2828
+ document.body.appendChild(region);
2829
+ this.regions.set(politeness, region);
2830
+ }
2831
+ return region;
2832
+ }
2833
+ ngOnDestroy() {
2834
+ for (const timer of this.clearTimers.values())
2835
+ clearTimeout(timer);
2836
+ this.clearTimers.clear();
2837
+ for (const region of this.regions.values())
2838
+ region.remove();
2839
+ this.regions.clear();
2840
+ }
2841
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2842
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
2843
+ }
2844
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, decorators: [{
2845
+ type: Injectable,
2846
+ args: [{ providedIn: 'root' }]
2847
+ }] });
2848
+
2794
2849
  /** Default chevron aria-label factory — mirrors the `chevronAriaLabel` input default. */
2795
2850
  const defaultChevronAriaLabel = (expanded, label) => `${expanded ? 'Collapse' : 'Expand'} ${label}`;
2796
2851
  let treeNodeCounter = 0;
@@ -2831,6 +2886,67 @@ class StrctTreeNode {
2831
2886
  nodeActivated = output();
2832
2887
  /** Data-mode right-click menu selection (bubbles to the tree). */
2833
2888
  nodeMenuSelect = output();
2889
+ /** Whether this row can be picked up — the tree asks the consumer's `canDrag`.
2890
+ * Reads `nodes()` too, so a refresh that makes a node movable is honoured. */
2891
+ isDraggable = computed(() => {
2892
+ const n = this.node();
2893
+ this.tree?.nodes();
2894
+ return !!n && !!this.tree?.canDragNode(n);
2895
+ }, /* @ts-ignore */
2896
+ ...(ngDevMode ? [{ debugName: "isDraggable" }] : /* istanbul ignore next */ []));
2897
+ isDragSource = computed(() => {
2898
+ const n = this.node();
2899
+ return !!n && !!this.tree?.isDragSource(n);
2900
+ }, /* @ts-ignore */
2901
+ ...(ngDevMode ? [{ debugName: "isDragSource" }] : /* istanbul ignore next */ []));
2902
+ isDropTarget = computed(() => {
2903
+ const n = this.node();
2904
+ return !!n && !!this.tree?.isDropTarget(n);
2905
+ }, /* @ts-ignore */
2906
+ ...(ngDevMode ? [{ debugName: "isDropTarget" }] : /* istanbul ignore next */ []));
2907
+ onDragStart(event) {
2908
+ const n = this.node();
2909
+ if (!n || !this.tree || !this.isDraggable())
2910
+ return;
2911
+ event.stopPropagation();
2912
+ // Firefox starts no drag without data on the transfer.
2913
+ event.dataTransfer?.setData('text/plain', n.label);
2914
+ if (event.dataTransfer)
2915
+ event.dataTransfer.effectAllowed = 'move';
2916
+ this.tree.beginDrag(n);
2917
+ }
2918
+ onDragEnd() {
2919
+ this.tree?.endDrag();
2920
+ }
2921
+ onDragOver(event) {
2922
+ const n = this.node();
2923
+ if (!n || !this.tree)
2924
+ return;
2925
+ event.stopPropagation();
2926
+ this.tree.dragOver(n, event.clientY);
2927
+ if (!this.tree.acceptsDrop(n)) {
2928
+ // No preventDefault: the pointer keeps the browser's "not allowed" cursor.
2929
+ if (event.dataTransfer)
2930
+ event.dataTransfer.dropEffect = 'none';
2931
+ return;
2932
+ }
2933
+ event.preventDefault();
2934
+ if (event.dataTransfer)
2935
+ event.dataTransfer.dropEffect = 'move';
2936
+ }
2937
+ onDragLeave() {
2938
+ const n = this.node();
2939
+ if (n)
2940
+ this.tree?.dragLeave(n);
2941
+ }
2942
+ onDrop(event) {
2943
+ const n = this.node();
2944
+ if (!n || !this.tree)
2945
+ return;
2946
+ event.preventDefault();
2947
+ event.stopPropagation();
2948
+ this.tree.dropOn(n);
2949
+ }
2834
2950
  /** Right-click menu items for this node ([] when no resolver / not data mode). */
2835
2951
  menuItems = computed(() => {
2836
2952
  const fn = this.nodeMenu();
@@ -2992,9 +3108,17 @@ class StrctTreeNode {
2992
3108
  [attr.aria-selected]="displayActive()"
2993
3109
  [attr.aria-expanded]="hasChildren() ? isOpen() : null"
2994
3110
  [attr.aria-owns]="hasChildren() && isOpen() ? groupId : null"
3111
+ [attr.draggable]="isDraggable() ? 'true' : null"
3112
+ [class.strct-tnode__row--dragging]="isDragSource()"
3113
+ [class.strct-tnode__row--droptarget]="isDropTarget()"
2995
3114
  [strctContextMenu]="menuItems()"
2996
3115
  [strctContextMenuData]="node()"
2997
3116
  (menuSelect)="onMenuSelect($event)"
3117
+ (dragstart)="onDragStart($event)"
3118
+ (dragend)="onDragEnd()"
3119
+ (dragover)="onDragOver($event)"
3120
+ (dragleave)="onDragLeave()"
3121
+ (drop)="onDrop($event)"
2998
3122
  (click)="onActivate()"
2999
3123
  (focus)="onRowFocus()"
3000
3124
  (keydown)="onRowKeydown($event)"
@@ -3045,7 +3169,7 @@ class StrctTreeNode {
3045
3169
  }
3046
3170
  </div>
3047
3171
  }
3048
- `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded", "nodeMenu"], outputs: ["expandedChange", "activated", "nodeActivated", "nodeMenuSelect"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctContextMenuTrigger, selector: "[strctContextMenu]", inputs: ["strctContextMenu", "strctContextMenuData"], outputs: ["menuSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3172
+ `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--dragging{opacity:.45}.strct-tnode__row--droptarget,.strct-tnode__row--droptarget:hover{background:var(--acc-s);outline:1px solid var(--acc);outline-offset:-1px}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded", "nodeMenu"], outputs: ["expandedChange", "activated", "nodeActivated", "nodeMenuSelect"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctContextMenuTrigger, selector: "[strctContextMenu]", inputs: ["strctContextMenu", "strctContextMenuData"], outputs: ["menuSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3049
3173
  }
3050
3174
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTreeNode, decorators: [{
3051
3175
  type: Component,
@@ -3061,9 +3185,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
3061
3185
  [attr.aria-selected]="displayActive()"
3062
3186
  [attr.aria-expanded]="hasChildren() ? isOpen() : null"
3063
3187
  [attr.aria-owns]="hasChildren() && isOpen() ? groupId : null"
3188
+ [attr.draggable]="isDraggable() ? 'true' : null"
3189
+ [class.strct-tnode__row--dragging]="isDragSource()"
3190
+ [class.strct-tnode__row--droptarget]="isDropTarget()"
3064
3191
  [strctContextMenu]="menuItems()"
3065
3192
  [strctContextMenuData]="node()"
3066
3193
  (menuSelect)="onMenuSelect($event)"
3194
+ (dragstart)="onDragStart($event)"
3195
+ (dragend)="onDragEnd()"
3196
+ (dragover)="onDragOver($event)"
3197
+ (dragleave)="onDragLeave()"
3198
+ (drop)="onDrop($event)"
3067
3199
  (click)="onActivate()"
3068
3200
  (focus)="onRowFocus()"
3069
3201
  (keydown)="onRowKeydown($event)"
@@ -3120,7 +3252,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
3120
3252
  // siblings in the DOM, so the host must not appear in the a11y tree.
3121
3253
  role: 'none',
3122
3254
  '[attr.data-node-id]': 'node()?.id ?? null',
3123
- }, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"] }]
3255
+ }, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--dragging{opacity:.45}.strct-tnode__row--droptarget,.strct-tnode__row--droptarget:hover{background:var(--acc-s);outline:1px solid var(--acc);outline-offset:-1px}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"] }]
3124
3256
  }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], activated: [{ type: i0.Output, args: ["activated"] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], childNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { isSignal: true }] }], directChildNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { ...{ descendants: false }, isSignal: true }] }] } });
3125
3257
  /**
3126
3258
  * Root container for a tree. Either project `<strct-tree-node>` children, or
@@ -3141,6 +3273,29 @@ class StrctTree {
3141
3273
  /** Per-node right-click menu resolver. */
3142
3274
  nodeMenu = input(null, /* @ts-ignore */
3143
3275
  ...(ngDevMode ? [{ debugName: "nodeMenu" }] : /* istanbul ignore next */ []));
3276
+ /**
3277
+ * Which nodes can be picked up. Default: none, so a tree without this input
3278
+ * behaves exactly as before. Asked again whenever `nodes` changes, because
3279
+ * a refresh can make a node movable.
3280
+ */
3281
+ canDrag = input(null, /* @ts-ignore */
3282
+ ...(ngDevMode ? [{ debugName: "canDrag" }] : /* istanbul ignore next */ []));
3283
+ /**
3284
+ * Where the dragged node may land, asked during `dragover`. A browser does
3285
+ * not let `dragover` read the drag's data, so the tree keeps the source node
3286
+ * and hands it to you here. Without this input nothing accepts a drop — the
3287
+ * gesture belongs to the tree, the rule belongs to you.
3288
+ *
3289
+ * Two rules are built in whatever this returns: a node is never dropped on
3290
+ * itself, and never into its own subtree (that would be a cycle).
3291
+ */
3292
+ canDrop = input(null, /* @ts-ignore */
3293
+ ...(ngDevMode ? [{ debugName: "canDrop" }] : /* istanbul ignore next */ []));
3294
+ /** How long a collapsed node must be hovered mid-drag before it expands (ms). */
3295
+ dragExpandDelay = input(700, /* @ts-ignore */
3296
+ ...(ngDevMode ? [{ debugName: "dragExpandDelay" }] : /* istanbul ignore next */ []));
3297
+ /** Emitted for an accepted drop only. */
3298
+ nodeDrop = output();
3144
3299
  /** Accessible label of each node's chevron toggle; receives the expanded state and the node label. */
3145
3300
  chevronAriaLabel = input(defaultChevronAriaLabel, /* @ts-ignore */
3146
3301
  ...(ngDevMode ? [{ debugName: "chevronAriaLabel" }] : /* istanbul ignore next */ []));
@@ -3324,6 +3479,144 @@ class StrctTree {
3324
3479
  }
3325
3480
  }
3326
3481
  }
3482
+ // ── Drag and drop ──────────────────────────────────────────────
3483
+ announcer = inject(StrctAnnouncer);
3484
+ hostEl = inject(ElementRef);
3485
+ dragSourceNode = signal(null, /* @ts-ignore */
3486
+ ...(ngDevMode ? [{ debugName: "dragSourceNode" }] : /* istanbul ignore next */ []));
3487
+ dropTargetNode = signal(null, /* @ts-ignore */
3488
+ ...(ngDevMode ? [{ debugName: "dropTargetNode" }] : /* istanbul ignore next */ []));
3489
+ /** Pending hover-expand for a collapsed target. */
3490
+ expandTimer = null;
3491
+ expandTimerKey = null;
3492
+ /** Edge auto-scroll while a drag is in flight. */
3493
+ scrollFrame = null;
3494
+ scrollStep = 0;
3495
+ canDragNode(node) {
3496
+ return this.canDrag()?.(node) ?? false;
3497
+ }
3498
+ isDragSource(node) {
3499
+ const src = this.dragSourceNode();
3500
+ return !!src && this.keyOf(src) === this.keyOf(node);
3501
+ }
3502
+ isDropTarget(node) {
3503
+ const t = this.dropTargetNode();
3504
+ return !!t && this.keyOf(t) === this.keyOf(node);
3505
+ }
3506
+ /** Whether the current drag may land on `target`: the built-in guards first,
3507
+ * then the consumer's rule. */
3508
+ acceptsDrop(target) {
3509
+ const source = this.dragSourceNode();
3510
+ if (!source)
3511
+ return false;
3512
+ if (this.keyOf(source) === this.keyOf(target))
3513
+ return false;
3514
+ if (this.containsNode(source, target))
3515
+ return false;
3516
+ return this.canDrop()?.(source, target) ?? false;
3517
+ }
3518
+ /** Whether `node` is somewhere inside `root`'s subtree. */
3519
+ containsNode(root, node) {
3520
+ const key = this.keyOf(node);
3521
+ const walk = (list) => (list ?? []).some((n) => this.keyOf(n) === key || walk(n.children));
3522
+ return walk(root.children);
3523
+ }
3524
+ beginDrag(node) {
3525
+ this.dragSourceNode.set(node);
3526
+ this.announcer.announce(`Dragging ${node.label}`);
3527
+ }
3528
+ /** Called from every `dragover` on a row: highlight, hover-expand, scroll. */
3529
+ dragOver(target, clientY) {
3530
+ if (!this.dragSourceNode())
3531
+ return;
3532
+ const accepts = this.acceptsDrop(target);
3533
+ this.dropTargetNode.set(accepts ? target : null);
3534
+ this.scheduleHoverExpand(target);
3535
+ this.edgeScroll(clientY);
3536
+ }
3537
+ dragLeave(target) {
3538
+ if (this.isDropTarget(target))
3539
+ this.dropTargetNode.set(null);
3540
+ if (this.expandTimerKey === this.keyOf(target))
3541
+ this.clearExpandTimer();
3542
+ this.stopEdgeScroll();
3543
+ }
3544
+ dropOn(target) {
3545
+ const source = this.dragSourceNode();
3546
+ const accepted = !!source && this.acceptsDrop(target);
3547
+ if (source && accepted) {
3548
+ this.nodeDrop.emit({ source, target, position: 'into' });
3549
+ this.announcer.announce(`Dropped ${source.label} on ${target.label}`);
3550
+ }
3551
+ this.endDrag();
3552
+ }
3553
+ /** Always runs, including when the drop lands outside the tree (dragend). */
3554
+ endDrag() {
3555
+ this.dragSourceNode.set(null);
3556
+ this.dropTargetNode.set(null);
3557
+ this.clearExpandTimer();
3558
+ this.stopEdgeScroll();
3559
+ }
3560
+ /** A collapsed node hovered long enough opens, so a target that is not yet
3561
+ * visible can be reached at all — the normal case in a big inventory. */
3562
+ scheduleHoverExpand(target) {
3563
+ const key = this.keyOf(target);
3564
+ if (this.expandTimerKey === key)
3565
+ return;
3566
+ this.clearExpandTimer();
3567
+ if (!target.children?.length || this.currentSet().has(key))
3568
+ return;
3569
+ this.expandTimerKey = key;
3570
+ this.expandTimer = setTimeout(() => {
3571
+ this.expandTimer = null;
3572
+ this.expandTimerKey = null;
3573
+ if (this.dragSourceNode() && !this.currentSet().has(key))
3574
+ this.toggleNode(target);
3575
+ }, this.dragExpandDelay());
3576
+ }
3577
+ clearExpandTimer() {
3578
+ if (this.expandTimer != null)
3579
+ clearTimeout(this.expandTimer);
3580
+ this.expandTimer = null;
3581
+ this.expandTimerKey = null;
3582
+ }
3583
+ /** Nearest scrolling ancestor, so a long tree can be traversed mid-drag. */
3584
+ scrollParent() {
3585
+ let el = this.hostEl.nativeElement;
3586
+ while (el) {
3587
+ const canScroll = el.scrollHeight > el.clientHeight + 1;
3588
+ if (canScroll && /auto|scroll|overlay/.test(getComputedStyle(el).overflowY))
3589
+ return el;
3590
+ el = el.parentElement;
3591
+ }
3592
+ return null;
3593
+ }
3594
+ edgeScroll(clientY) {
3595
+ const box = this.scrollParent();
3596
+ if (!box)
3597
+ return;
3598
+ const r = box.getBoundingClientRect();
3599
+ const zone = 36;
3600
+ const step = clientY < r.top + zone ? -12 : clientY > r.bottom - zone ? 12 : 0;
3601
+ this.scrollStep = step;
3602
+ if (!step)
3603
+ return this.stopEdgeScroll();
3604
+ if (this.scrollFrame != null)
3605
+ return;
3606
+ const tick = () => {
3607
+ if (!this.scrollStep || !this.dragSourceNode())
3608
+ return this.stopEdgeScroll();
3609
+ box.scrollTop += this.scrollStep;
3610
+ this.scrollFrame = requestAnimationFrame(tick);
3611
+ };
3612
+ this.scrollFrame = requestAnimationFrame(tick);
3613
+ }
3614
+ stopEdgeScroll() {
3615
+ if (this.scrollFrame != null)
3616
+ cancelAnimationFrame(this.scrollFrame);
3617
+ this.scrollFrame = null;
3618
+ this.scrollStep = 0;
3619
+ }
3327
3620
  /** Toggle a node's expansion, updating state and emitting outputs. */
3328
3621
  toggleNode(node) {
3329
3622
  const key = this.keyOf(node);
@@ -3345,7 +3638,7 @@ class StrctTree {
3345
3638
  this.expandedChange.emit([...this.currentSet()]);
3346
3639
  }
3347
3640
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
3348
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, nodeMenu: { classPropertyName: "nodeMenu", publicName: "nodeMenu", isSignal: true, isRequired: false, transformFunction: null }, chevronAriaLabel: { classPropertyName: "chevronAriaLabel", publicName: "chevronAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, expandedIds: { classPropertyName: "expandedIds", publicName: "expandedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeActivated: "nodeActivated", nodeMenuSelect: "nodeMenuSelect", expandedIds: "expandedIdsChange", expandedChange: "expandedChange", nodeToggled: "nodeToggled" }, host: { attributes: { "role": "tree" }, properties: { "class.strct-tree--comfortable": "density() === 'comfortable'" }, classAttribute: "strct-tree" }, queries: [{ propertyName: "projectedNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
3641
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, nodeMenu: { classPropertyName: "nodeMenu", publicName: "nodeMenu", isSignal: true, isRequired: false, transformFunction: null }, canDrag: { classPropertyName: "canDrag", publicName: "canDrag", isSignal: true, isRequired: false, transformFunction: null }, canDrop: { classPropertyName: "canDrop", publicName: "canDrop", isSignal: true, isRequired: false, transformFunction: null }, dragExpandDelay: { classPropertyName: "dragExpandDelay", publicName: "dragExpandDelay", isSignal: true, isRequired: false, transformFunction: null }, chevronAriaLabel: { classPropertyName: "chevronAriaLabel", publicName: "chevronAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, expandedIds: { classPropertyName: "expandedIds", publicName: "expandedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeDrop: "nodeDrop", nodeActivated: "nodeActivated", nodeMenuSelect: "nodeMenuSelect", expandedIds: "expandedIdsChange", expandedChange: "expandedChange", nodeToggled: "nodeToggled" }, host: { attributes: { "role": "tree" }, properties: { "class.strct-tree--comfortable": "density() === 'comfortable'" }, classAttribute: "strct-tree" }, queries: [{ propertyName: "projectedNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
3349
3642
  @if (nodes(); as ns) {
3350
3643
  @for (n of ns; track n.id ?? n.label) {
3351
3644
  <strct-tree-node
@@ -3380,7 +3673,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
3380
3673
  role: 'tree',
3381
3674
  '[class.strct-tree--comfortable]': "density() === 'comfortable'",
3382
3675
  }, styles: [".strct-tree{display:block}.strct-tree--comfortable .strct-tnode__row{gap:7px;padding:6px 10px;border-radius:6px;font-size:14px}.strct-tree--comfortable .strct-tnode__chevron{margin:-4px;margin-inline-end:-9px}.strct-tree--comfortable .strct-tnode__spacer{width:16px}.strct-tree--comfortable .strct-tnode__children{margin-inline-start:19px}\n"] }]
3383
- }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], chevronAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "chevronAriaLabel", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], expandedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedIds", required: false }] }, { type: i0.Output, args: ["expandedIdsChange"] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }], nodeToggled: [{ type: i0.Output, args: ["nodeToggled"] }], projectedNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { ...{ descendants: false }, isSignal: true }] }] } });
3676
+ }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], canDrag: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDrag", required: false }] }], canDrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDrop", required: false }] }], dragExpandDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragExpandDelay", required: false }] }], nodeDrop: [{ type: i0.Output, args: ["nodeDrop"] }], chevronAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "chevronAriaLabel", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], expandedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedIds", required: false }] }, { type: i0.Output, args: ["expandedIdsChange"] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }], nodeToggled: [{ type: i0.Output, args: ["nodeToggled"] }], projectedNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { ...{ descendants: false }, isSignal: true }] }] } });
3384
3677
 
3385
3678
  /**
3386
3679
  * Dev-mode diagnostics for combinations a component accepts but cannot honour.
@@ -19254,61 +19547,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
19254
19547
  args: ['document:keydown.escape', ['$event']]
19255
19548
  }] } });
19256
19549
 
19257
- /**
19258
- * Screen-reader announcements for state changes that have no visible text —
19259
- * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
19260
- *
19261
- * private announcer = inject(StrctAnnouncer);
19262
- * this.announcer.announce('12 rows loaded');
19263
- *
19264
- * Maintains one visually-hidden live region per politeness level; repeated
19265
- * identical messages are re-announced (the region is cleared first).
19266
- */
19267
- class StrctAnnouncer {
19268
- regions = new Map();
19269
- clearTimers = new Map();
19270
- isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
19271
- announce(message, politeness = 'polite') {
19272
- if (!this.isBrowser)
19273
- return;
19274
- const region = this.regionFor(politeness);
19275
- // Clear first so identical consecutive messages still fire.
19276
- region.textContent = '';
19277
- setTimeout(() => (region.textContent = message));
19278
- clearTimeout(this.clearTimers.get(politeness));
19279
- // Stale announcements should not linger for the next SR user to stumble on.
19280
- this.clearTimers.set(politeness, setTimeout(() => (region.textContent = ''), 10_000));
19281
- }
19282
- regionFor(politeness) {
19283
- let region = this.regions.get(politeness);
19284
- if (!region) {
19285
- region = document.createElement('div');
19286
- region.setAttribute('aria-live', politeness);
19287
- region.setAttribute('aria-atomic', 'true');
19288
- region.className = 'strct-announcer';
19289
- region.style.cssText =
19290
- 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
19291
- document.body.appendChild(region);
19292
- this.regions.set(politeness, region);
19293
- }
19294
- return region;
19295
- }
19296
- ngOnDestroy() {
19297
- for (const timer of this.clearTimers.values())
19298
- clearTimeout(timer);
19299
- this.clearTimers.clear();
19300
- for (const region of this.regions.values())
19301
- region.remove();
19302
- this.regions.clear();
19303
- }
19304
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
19305
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
19306
- }
19307
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, decorators: [{
19308
- type: Injectable,
19309
- args: [{ providedIn: 'root' }]
19310
- }] });
19311
-
19312
19550
  function normalize(combo) {
19313
19551
  return combo
19314
19552
  .toLowerCase()
@@ -20678,7 +20916,7 @@ class StrctTreeSelect {
20678
20916
  }
20679
20917
  </div>
20680
20918
  }
20681
- `, isInline: true, styles: [".strct-tsel{position:relative;display:block;width:100%}.strct-tsel__field{position:relative}.strct-tsel__trigger{display:flex;align-items:center;gap:8px;padding-inline-end:30px;text-align:start;cursor:pointer}.strct-tsel__trigger--clearable{padding-inline-end:48px}.strct-tsel__trigger:disabled{cursor:not-allowed}.strct-tsel__value{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tsel__value--empty{color:var(--t3)}.strct-tsel__caret{position:absolute;inset-inline-end:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-tsel__clear{position:absolute;inset-inline-end:26px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:var(--radius-sm);cursor:pointer}.strct-tsel__clear:hover{color:var(--t1)}.strct-tsel__panel{z-index:var(--z-dropdown);max-height:260px;overflow-y:auto;padding:var(--space-1);background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-lg);box-shadow:var(--shh)}.strct-tsel__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }, { kind: "component", type: StrctTree, selector: "strct-tree", inputs: ["nodes", "density", "nodeMenu", "chevronAriaLabel", "expandedIds"], outputs: ["nodeActivated", "nodeMenuSelect", "expandedIdsChange", "expandedChange", "nodeToggled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20919
+ `, isInline: true, styles: [".strct-tsel{position:relative;display:block;width:100%}.strct-tsel__field{position:relative}.strct-tsel__trigger{display:flex;align-items:center;gap:8px;padding-inline-end:30px;text-align:start;cursor:pointer}.strct-tsel__trigger--clearable{padding-inline-end:48px}.strct-tsel__trigger:disabled{cursor:not-allowed}.strct-tsel__value{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tsel__value--empty{color:var(--t3)}.strct-tsel__caret{position:absolute;inset-inline-end:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-tsel__clear{position:absolute;inset-inline-end:26px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:var(--radius-sm);cursor:pointer}.strct-tsel__clear:hover{color:var(--t1)}.strct-tsel__panel{z-index:var(--z-dropdown);max-height:260px;overflow-y:auto;padding:var(--space-1);background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-lg);box-shadow:var(--shh)}.strct-tsel__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }, { kind: "component", type: StrctTree, selector: "strct-tree", inputs: ["nodes", "density", "nodeMenu", "canDrag", "canDrop", "dragExpandDelay", "chevronAriaLabel", "expandedIds"], outputs: ["nodeDrop", "nodeActivated", "nodeMenuSelect", "expandedIdsChange", "expandedChange", "nodeToggled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20682
20920
  }
20683
20921
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTreeSelect, decorators: [{
20684
20922
  type: Component,
@@ -21806,27 +22044,47 @@ class StrctHeatmap {
21806
22044
  const maxV = this.maxVal();
21807
22045
  const t = this.thresholds();
21808
22046
  if (t) {
21809
- const { color, from, to } = this.bandFor(value, t, maxV);
22047
+ const { color, from, to, floor, ceilingPct } = this.bandFor(value, t, maxV);
21810
22048
  const span = to - from;
21811
22049
  const pos = span > 0 ? Math.min(Math.max((value - from) / span, 0), 1) : 1;
21812
- const pct = 45 + Math.round(pos * 55);
22050
+ const pct = floor + Math.round(pos * (ceilingPct - floor));
21813
22051
  return `color-mix(in srgb, ${color} ${pct}%, var(--bg-1))`;
21814
22052
  }
21815
22053
  const ratio = maxV > 0 ? Math.min(value / maxV, 1) : 0;
21816
22054
  const pct = 8 + Math.round(ratio * 92);
21817
22055
  return `color-mix(in srgb, ${this.color()} ${pct}%, var(--bg-1))`;
21818
22056
  }
21819
- /** The band a value falls in: its hue and the range intensity scales over. */
22057
+ /**
22058
+ * The band a value falls in: its hue, the value range intensity scales over,
22059
+ * and the intensity range itself.
22060
+ *
22061
+ * FR-44-02: the bands' intensity ranges ASCEND and do not overlap, so
22062
+ * intensity never falls as the value rises. Restarting every band at one
22063
+ * floor (4.3.0) made a cell just past a threshold paler than the cell just
22064
+ * below it — an 86% warning cell read quieter than an 84% accent one, which
22065
+ * is backwards for the hours that matter most. The accent band still starts
22066
+ * near-empty; warning and critical start where accent ends, so neither can
22067
+ * be mistaken for no-data either.
22068
+ */
21820
22069
  bandFor(value, t, maxV) {
21821
22070
  const ceiling = Math.max(maxV, value);
21822
22071
  if (t.critical != null && value >= t.critical) {
21823
- return { color: COLOR.critical, from: t.critical, to: ceiling };
22072
+ return { color: COLOR.critical, from: t.critical, to: ceiling, floor: 92, ceilingPct: 100 };
21824
22073
  }
21825
22074
  if (t.warning != null && value >= t.warning) {
21826
- return { color: COLOR.warning, from: t.warning, to: t.critical ?? ceiling };
22075
+ const to = t.critical ?? ceiling;
22076
+ // With a critical bound above it the warning band stops short of it;
22077
+ // without one it owns the top of the ramp.
22078
+ return {
22079
+ color: COLOR.warning,
22080
+ from: t.warning,
22081
+ to,
22082
+ floor: 80,
22083
+ ceilingPct: t.critical != null ? 92 : 100,
22084
+ };
21827
22085
  }
21828
22086
  const upper = t.warning ?? t.critical ?? ceiling;
21829
- return { color: this.color(), from: 0, to: upper };
22087
+ return { color: this.color(), from: 0, to: upper, floor: 8, ceilingPct: 80 };
21830
22088
  }
21831
22089
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHeatmap, deps: [], target: i0.ɵɵFactoryTarget.Component });
21832
22090
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctHeatmap, isStandalone: true, selector: "strct-heatmap", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, colLabelEvery: { classPropertyName: "colLabelEvery", publicName: "colLabelEvery", isSignal: true, isRequired: false, transformFunction: null }, colLabel: { classPropertyName: "colLabel", publicName: "colLabel", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, thresholds: { classPropertyName: "thresholds", publicName: "thresholds", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, summaryFormat: { classPropertyName: "summaryFormat", publicName: "summaryFormat", isSignal: true, isRequired: false, transformFunction: null }, cellHeight: { classPropertyName: "cellHeight", publicName: "cellHeight", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, rowLabelWidth: { classPropertyName: "rowLabelWidth", publicName: "rowLabelWidth", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-heatmap" }, ngImport: i0, template: `