@akcelik/strct 4.2.1 → 4.4.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.
@@ -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';
@@ -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,
@@ -21612,6 +21850,34 @@ class StrctHeatmap {
21612
21850
  /** Scale ceiling — the value that maps to full intensity. Auto (data max) when null. */
21613
21851
  max = input(null, /* @ts-ignore */
21614
21852
  ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
21853
+ /**
21854
+ * Label every n-th column (1 = every column, today's behaviour). 24 hourly
21855
+ * columns in a narrow card cannot all carry a readable label; labelling every
21856
+ * third keeps "14:00" legible instead of forcing it down to "14".
21857
+ */
21858
+ colLabelEvery = input(1, /* @ts-ignore */
21859
+ ...(ngDevMode ? [{ debugName: "colLabelEvery" }] : /* istanbul ignore next */ []));
21860
+ /**
21861
+ * Column label text, so a column can be keyed by an ISO time and shown as
21862
+ * "14:00". Return '' to drop a label. Applied after `colLabelEvery`.
21863
+ */
21864
+ colLabel = input(null, /* @ts-ignore */
21865
+ ...(ngDevMode ? [{ debugName: "colLabel" }] : /* istanbul ignore next */ []));
21866
+ /**
21867
+ * Cell tooltip text. Without it a cell reads `row × col: value`, which for a
21868
+ * utilisation grid has no unit — `hv-05 · 14:00 — 47% CPU` is the readable form.
21869
+ */
21870
+ valueFormat = input(null, /* @ts-ignore */
21871
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
21872
+ /**
21873
+ * Colour by band instead of one hue: `status` (accent by default) below
21874
+ * `warning`, the warning hue from there, the critical hue from `critical`.
21875
+ * Intensity is scaled by the value's position WITHIN its band, so a 96% cell
21876
+ * reads darker than an 81% one and both read as critical/warning at a glance.
21877
+ * Unset: today's single-hue ramp across the whole range.
21878
+ */
21879
+ thresholds = input(null, /* @ts-ignore */
21880
+ ...(ngDevMode ? [{ debugName: "thresholds" }] : /* istanbul ignore next */ []));
21615
21881
  /** Base color of the intensity ramp. */
21616
21882
  status = input('accent', /* @ts-ignore */
21617
21883
  ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
@@ -21710,7 +21976,7 @@ class StrctHeatmap {
21710
21976
  w: round(w),
21711
21977
  h: ch,
21712
21978
  fill: value === undefined ? 'var(--bg-2)' : this.fillFor(value),
21713
- tip: value === undefined ? null : `${row} × ${col}: ${value}`,
21979
+ tip: value === undefined ? null : this.tipFor(value, row, col),
21714
21980
  });
21715
21981
  });
21716
21982
  });
@@ -21732,9 +21998,24 @@ class StrctHeatmap {
21732
21998
  const gap = this.gap();
21733
21999
  const x0 = this.rowLabelWidth();
21734
22000
  const y = this.height() - 4;
21735
- return this.colOrder().map((text, c) => ({ text, x: round(x0 + c * (w + gap) + w / 2), y }));
22001
+ const every = Math.max(1, Math.trunc(this.colLabelEvery()));
22002
+ const format = this.colLabel();
22003
+ const out = [];
22004
+ this.colOrder().forEach((col, c) => {
22005
+ if (c % every !== 0)
22006
+ return;
22007
+ const text = format ? format(col, c) : col;
22008
+ if (text === '')
22009
+ return;
22010
+ out.push({ text, x: round(x0 + c * (w + gap) + w / 2), y });
22011
+ });
22012
+ return out;
21736
22013
  }, /* @ts-ignore */
21737
22014
  ...(ngDevMode ? [{ debugName: "colLabels" }] : /* istanbul ignore next */ []));
22015
+ tipFor(value, row, col) {
22016
+ const format = this.valueFormat();
22017
+ return format ? format(value, row, col) : `${row} × ${col}: ${value}`;
22018
+ }
21738
22019
  /** Screen-reader summary of the whole grid (role="img" name). */
21739
22020
  aria = computed(() => {
21740
22021
  const info = {
@@ -21752,17 +22033,41 @@ class StrctHeatmap {
21752
22033
  /**
21753
22034
  * Intensity ramp: 8% of the status hue for the smallest non-zero value up
21754
22035
  * to 100% at the ceiling; zero stays on the empty-cell surface.
22036
+ *
22037
+ * With `thresholds`, the hue comes from the value's band and the intensity
22038
+ * from its position inside that band, floored at 45% — a band has to be
22039
+ * recognisable as itself, and a pale cell must not read as "no data".
21755
22040
  */
21756
22041
  fillFor(value) {
21757
22042
  if (value <= 0)
21758
22043
  return 'var(--bg-2)';
21759
22044
  const maxV = this.maxVal();
21760
- const t = maxV > 0 ? Math.min(value / maxV, 1) : 0;
21761
- const pct = 8 + Math.round(t * 92);
22045
+ const t = this.thresholds();
22046
+ if (t) {
22047
+ const { color, from, to } = this.bandFor(value, t, maxV);
22048
+ const span = to - from;
22049
+ const pos = span > 0 ? Math.min(Math.max((value - from) / span, 0), 1) : 1;
22050
+ const pct = 45 + Math.round(pos * 55);
22051
+ return `color-mix(in srgb, ${color} ${pct}%, var(--bg-1))`;
22052
+ }
22053
+ const ratio = maxV > 0 ? Math.min(value / maxV, 1) : 0;
22054
+ const pct = 8 + Math.round(ratio * 92);
21762
22055
  return `color-mix(in srgb, ${this.color()} ${pct}%, var(--bg-1))`;
21763
22056
  }
22057
+ /** The band a value falls in: its hue and the range intensity scales over. */
22058
+ bandFor(value, t, maxV) {
22059
+ const ceiling = Math.max(maxV, value);
22060
+ if (t.critical != null && value >= t.critical) {
22061
+ return { color: COLOR.critical, from: t.critical, to: ceiling };
22062
+ }
22063
+ if (t.warning != null && value >= t.warning) {
22064
+ return { color: COLOR.warning, from: t.warning, to: t.critical ?? ceiling };
22065
+ }
22066
+ const upper = t.warning ?? t.critical ?? ceiling;
22067
+ return { color: this.color(), from: 0, to: upper };
22068
+ }
21764
22069
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHeatmap, deps: [], target: i0.ɵɵFactoryTarget.Component });
21765
- 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 }, 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: `
22070
+ 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: `
21766
22071
  @if (isEmpty()) {
21767
22072
  <div class="strct-heatmap__empty">{{ emptyText() }}</div>
21768
22073
  } @else {
@@ -21774,7 +22079,10 @@ class StrctHeatmap {
21774
22079
  [attr.width]="width()"
21775
22080
  [attr.height]="height()"
21776
22081
  >
21777
- @for (l of rowLabels(); track l.text) {
22082
+ <!-- Tracked by index: label text is data and repeats legitimately (two
22083
+ hosts with the same name, hour 02 twice at the DST fall-back), and
22084
+ a thinned column axis repeats the empty string. -->
22085
+ @for (l of rowLabels(); track $index) {
21778
22086
  <text
21779
22087
  class="strct-heatmap__label strct-heatmap__label--row"
21780
22088
  [attr.x]="l.x"
@@ -21785,7 +22093,7 @@ class StrctHeatmap {
21785
22093
  {{ l.text }}
21786
22094
  </text>
21787
22095
  }
21788
- @for (l of colLabels(); track l.text) {
22096
+ @for (l of colLabels(); track $index) {
21789
22097
  <text
21790
22098
  class="strct-heatmap__label strct-heatmap__label--col"
21791
22099
  [attr.x]="l.x"
@@ -21829,7 +22137,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
21829
22137
  [attr.width]="width()"
21830
22138
  [attr.height]="height()"
21831
22139
  >
21832
- @for (l of rowLabels(); track l.text) {
22140
+ <!-- Tracked by index: label text is data and repeats legitimately (two
22141
+ hosts with the same name, hour 02 twice at the DST fall-back), and
22142
+ a thinned column axis repeats the empty string. -->
22143
+ @for (l of rowLabels(); track $index) {
21833
22144
  <text
21834
22145
  class="strct-heatmap__label strct-heatmap__label--row"
21835
22146
  [attr.x]="l.x"
@@ -21840,7 +22151,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
21840
22151
  {{ l.text }}
21841
22152
  </text>
21842
22153
  }
21843
- @for (l of colLabels(); track l.text) {
22154
+ @for (l of colLabels(); track $index) {
21844
22155
  <text
21845
22156
  class="strct-heatmap__label strct-heatmap__label--col"
21846
22157
  [attr.x]="l.x"
@@ -21869,7 +22180,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
21869
22180
  </svg>
21870
22181
  }
21871
22182
  `, host: { class: 'strct-heatmap' }, styles: [".strct-heatmap{display:block}.strct-heatmap__svg{width:100%;display:block}.strct-heatmap__empty{display:flex;align-items:center;justify-content:center;min-height:60px;font-size:12px;color:var(--t3)}.strct-heatmap__label{fill:var(--t3);font-size:10px}.strct-heatmap__label--row{font-family:var(--font)}.strct-heatmap__label--col{font-family:var(--mono);font-variant-numeric:tabular-nums}.strct-heatmap__cell{stroke:var(--bg-1);stroke-width:1}@media(prefers-reduced-motion:no-preference){.strct-heatmap__cell{transition:opacity .12s ease}}.strct-heatmap__cell:hover{opacity:.75}\n"] }]
21872
- }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], cols: [{ type: i0.Input, args: [{ isSignal: true, alias: "cols", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], summaryFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "summaryFormat", required: false }] }], cellHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellHeight", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], rowLabelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowLabelWidth", required: false }] }] } });
22183
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], cols: [{ type: i0.Input, args: [{ isSignal: true, alias: "cols", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], colLabelEvery: [{ type: i0.Input, args: [{ isSignal: true, alias: "colLabelEvery", required: false }] }], colLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "colLabel", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], thresholds: [{ type: i0.Input, args: [{ isSignal: true, alias: "thresholds", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], summaryFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "summaryFormat", required: false }] }], cellHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellHeight", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], rowLabelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowLabelWidth", required: false }] }] } });
21873
22184
 
21874
22185
  /**
21875
22186
  * Public API surface of the strct UI library.