@foxeltech/angular-ui 0.7.124 → 0.7.131

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/components.css CHANGED
@@ -361,11 +361,12 @@ input[type='password'] {
361
361
  inset 0 0 0 1px rgb(0 0 0 / 0.08),
362
362
  0 1px 2px rgb(0 0 0 / 0.12);
363
363
  }
364
- /* Icon-only button (no label): square h-8 × w-8, no side padding, so the
365
- glyph sits dead-centre (anh Tú 04/09). Consumers may still size it with
366
- utilities (`w-9 h-9`) those win in the utilities layer. */
364
+ /* Icon-only button (no label): square, no side padding, so the glyph sits
365
+ dead-centre (anh Tú 04/09). The width tracks `btn-common`'s height — 27px
366
+ since 17/09so it stays square; consumers may still size it with
367
+ utilities (`w-9 h-9`), which win in the utilities layer. */
367
368
  .fx-btn--icon {
368
- width: 2rem;
369
+ width: 27px;
369
370
  padding-left: 0;
370
371
  padding-right: 0;
371
372
  }
@@ -39,6 +39,14 @@ class FlowCanvasComponent {
39
39
  height = input('', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
40
40
  /** Caller-translated hint shown when the graph is empty. */
41
41
  emptyText = input('No chain data available', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
42
+ /**
43
+ * Work is running against this chain — validation, typically.
44
+ *
45
+ * The edges become a travelling dashed line ("marching ants") so the canvas
46
+ * says something is in flight without a spinner sitting on top of the graph.
47
+ * Purely presentational: the caller owns what "busy" means and when it ends.
48
+ */
49
+ busy = input(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
42
50
  nodeClick = output();
43
51
  flow = viewChild(VflowComponent, ...(ngDevMode ? [{ debugName: "flow" }] : /* istanbul ignore next */ []));
44
52
  host = inject(ElementRef);
@@ -62,20 +70,43 @@ class FlowCanvasComponent {
62
70
  }
63
71
  }
64
72
  fitHandle = null;
65
- /** Coalesces fit requests into one frame after vflow has measured its nodes. */
73
+ /**
74
+ * Coalesces fit requests, then fits TWICE — one frame apart.
75
+ *
76
+ * vflow frames the graph from `getNodesBounds`, which sums each node's
77
+ * `point()` and its `size()`. For an html-template node that size is the
78
+ * MEASURED box and is still 0 on the frame the nodes are created, so the
79
+ * first fit sees a bounds box running origin-to-origin: five 176px cards
80
+ * measured 864px instead of 1040px, and the graph was framed 88px — half a
81
+ * card — too far right, clipping the last one (MEASURED 16/09).
82
+ *
83
+ * The second pass runs once the cards have been laid out and measured, and
84
+ * is what actually frames them. The first is kept so a slow frame still
85
+ * shows something sensible rather than the graph pinned top-left.
86
+ */
66
87
  scheduleFit() {
67
88
  if (this.fitHandle !== null)
68
89
  return;
69
90
  this.fitHandle = requestAnimationFrame(() => {
70
- this.fitHandle = null;
71
- const flow = this.flow();
72
- if (!flow || this.isEmpty())
73
- return;
74
- // Design 04: cards fill the width with a little air (0.18 left them at
75
- // ~0.74×, visibly smaller than the mock — measured 04/09).
76
- flow.fitView({ padding: 0.1, duration: 0 });
91
+ this.fit();
92
+ this.fitHandle = requestAnimationFrame(() => {
93
+ this.fitHandle = null;
94
+ this.fit();
95
+ });
77
96
  });
78
97
  }
98
+ fit() {
99
+ const flow = this.flow();
100
+ if (!flow || this.isEmpty())
101
+ return;
102
+ // Design 04: cards fill the width with a little air (0.18 left them at
103
+ // ~0.74×, visibly smaller than the mock — measured 04/09).
104
+ flow.fitView({ padding: 0.1, duration: 0 });
105
+ // Never MAGNIFY: the cap is `[maxZoom]` on the vflow element, which
106
+ // fitView respects. Clamping afterwards does NOT work — both `zoomTo`
107
+ // and `viewportTo` return without changing `viewport()` here (MEASURED
108
+ // 16/09).
109
+ }
79
110
  /** Column index per node: pinned value, else longest path from a source. */
80
111
  columns = computed(() => {
81
112
  const nodes = this.nodes();
@@ -199,7 +230,7 @@ class FlowCanvasComponent {
199
230
  return this.activeId() != null && !this.isEdgeActive(edge);
200
231
  }
201
232
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FlowCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
202
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FlowCanvasComponent, isStandalone: true, selector: "fx-ui-flow-canvas", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick" }, viewQueries: [{ propertyName: "flow", first: true, predicate: VflowComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1.35\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '\u2713' : '\u25CB' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;width:100%}.fx-chain-canvas{background:rgba(var(--og-surface-sunken)/1)}.fx-chain-node{display:flex;cursor:pointer;flex-direction:column;justify-content:center;gap:.125rem;border-radius:var(--og-radius-control);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface));padding-inline:.5rem;padding-block:.25rem;text-align:left;transition-property:all;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .15s;transition-duration:.15s;width:176px;height:66px}.fx-chain-node:hover:not(.fx-chain-node--active){border-color:rgb(var(--border-strong))}.fx-chain-node--active{border-color:rgb(var(--og-accent));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-width:2px}.fx-chain-node--related{border-color:rgb(var(--og-accent));border-color:rgba(var(--og-accent)/.45)}.fx-chain-node--dim{opacity:.55}.fx-chain-edge{fill:none;stroke:rgba(var(--og-border-strong)/1);stroke-width:1.5;transition:stroke .15s ease,stroke-width .15s ease,opacity .15s ease}.fx-chain-edge--active{stroke:rgba(var(--og-accent)/1);stroke-width:2.5}.fx-chain-edge--dim{opacity:.35}.fx-chain-node__eyebrow{display:flex;align-items:center;gap:.125rem;overflow:hidden;white-space:nowrap;font-size:9px;line-height:1.2;letter-spacing:var(--og-tracking-label);text-transform:uppercase;font-weight:700}.fx-chain-node__dot{flex-shrink:0;border-radius:calc(infinity * 1px);width:5px;height:5px}.fx-chain-node__check{margin-left:auto;display:flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);width:13px;height:13px;font-size:9px}.fx-chain-node__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text));font-family:var(--og-font-mono);font-size:12px;font-weight:600}.fx-chain-node__caption{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text-muted));font-size:10px;line-height:1.3}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-duration: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: VflowComponent, selector: "vflow", inputs: ["view", "minZoom", "maxZoom", "background", "optimization", "entitiesSelectable", "keyboardShortcuts", "connection", "snapGrid", "elevateNodesOnSelect", "elevateEdgesOnSelect", "nodes", "alignmentHelper", "edges"], outputs: ["onComponentNodeEvent"] }, { kind: "directive", type: NodeHtmlTemplateDirective, selector: "ng-template[nodeHtml]" }, { kind: "directive", type: EdgeTemplateDirective, selector: "ng-template[edge]" }, { kind: "component", type: HandleComponent, selector: "handle", inputs: ["position", "type", "id", "template", "offsetX", "offsetY"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
233
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FlowCanvasComponent, isStandalone: true, selector: "fx-ui-flow-canvas", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick" }, viewQueries: [{ propertyName: "flow", first: true, predicate: VflowComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <!-- maxZoom is 1, not 1.35: fitView scales a short chain UP to fill the\n band, and at 1.35 five 176px cards came out at 1.18\u00D7 \u2014 oversized\n against the design and wide enough to clip the last card (anh T\u00FA\n 16/09, MEASURED). The cards have a designed size, so fitting should\n only ever shrink. -->\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '\u2713' : '\u25CB' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--busy]=\"busy()\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;width:100%}.fx-chain-canvas{background:rgba(var(--og-surface-sunken)/1)}.fx-chain-node{display:flex;cursor:pointer;flex-direction:column;justify-content:center;gap:.125rem;border-radius:var(--og-radius-control);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface));padding-inline:.5rem;padding-block:.25rem;text-align:left;transition-property:all;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .15s;transition-duration:.15s;width:176px;height:66px}.fx-chain-node:hover:not(.fx-chain-node--active){border-color:rgb(var(--border-strong))}.fx-chain-node--active{border-color:rgb(var(--og-accent));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-width:2px}.fx-chain-node--related{border-color:rgb(var(--og-accent));border-color:rgba(var(--og-accent)/.45)}.fx-chain-node--dim{opacity:.55}.fx-chain-edge{fill:none;stroke:rgba(var(--og-border-strong)/1);stroke-width:1.5;transition:stroke .15s ease,stroke-width .15s ease,opacity .15s ease}.fx-chain-edge--active{stroke:rgba(var(--og-accent)/1);stroke-width:2.5}.fx-chain-edge--dim{opacity:.35}.fx-chain-edge--busy{stroke:rgba(var(--og-accent)/1);stroke-dasharray:5 4;animation:fx-chain-ants .6s linear infinite}@keyframes fx-chain-ants{to{stroke-dashoffset:-9}}@media(prefers-reduced-motion:reduce){.fx-chain-edge--busy{animation:none}}.fx-chain-node__eyebrow{display:flex;align-items:center;gap:.125rem;overflow:hidden;white-space:nowrap;font-size:9px;line-height:1.2;letter-spacing:var(--og-tracking-label);text-transform:uppercase;font-weight:700}.fx-chain-node__dot{flex-shrink:0;border-radius:calc(infinity * 1px);width:5px;height:5px}.fx-chain-node__check{margin-left:auto;display:flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);width:13px;height:13px;font-size:9px}.fx-chain-node__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text));font-family:var(--og-font-mono);font-size:12px;font-weight:600}.fx-chain-node__caption{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text-muted));font-size:10px;line-height:1.3}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-duration: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: VflowComponent, selector: "vflow", inputs: ["view", "minZoom", "maxZoom", "background", "optimization", "entitiesSelectable", "keyboardShortcuts", "connection", "snapGrid", "elevateNodesOnSelect", "elevateEdgesOnSelect", "nodes", "alignmentHelper", "edges"], outputs: ["onComponentNodeEvent"] }, { kind: "directive", type: NodeHtmlTemplateDirective, selector: "ng-template[nodeHtml]" }, { kind: "directive", type: EdgeTemplateDirective, selector: "ng-template[edge]" }, { kind: "component", type: HandleComponent, selector: "handle", inputs: ["position", "type", "id", "template", "offsetX", "offsetY"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
203
234
  }
204
235
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FlowCanvasComponent, decorators: [{
205
236
  type: Component,
@@ -209,8 +240,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
209
240
  NodeHtmlTemplateDirective,
210
241
  EdgeTemplateDirective,
211
242
  HandleComponent,
212
- ], template: "<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1.35\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '\u2713' : '\u25CB' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;width:100%}.fx-chain-canvas{background:rgba(var(--og-surface-sunken)/1)}.fx-chain-node{display:flex;cursor:pointer;flex-direction:column;justify-content:center;gap:.125rem;border-radius:var(--og-radius-control);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface));padding-inline:.5rem;padding-block:.25rem;text-align:left;transition-property:all;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .15s;transition-duration:.15s;width:176px;height:66px}.fx-chain-node:hover:not(.fx-chain-node--active){border-color:rgb(var(--border-strong))}.fx-chain-node--active{border-color:rgb(var(--og-accent));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-width:2px}.fx-chain-node--related{border-color:rgb(var(--og-accent));border-color:rgba(var(--og-accent)/.45)}.fx-chain-node--dim{opacity:.55}.fx-chain-edge{fill:none;stroke:rgba(var(--og-border-strong)/1);stroke-width:1.5;transition:stroke .15s ease,stroke-width .15s ease,opacity .15s ease}.fx-chain-edge--active{stroke:rgba(var(--og-accent)/1);stroke-width:2.5}.fx-chain-edge--dim{opacity:.35}.fx-chain-node__eyebrow{display:flex;align-items:center;gap:.125rem;overflow:hidden;white-space:nowrap;font-size:9px;line-height:1.2;letter-spacing:var(--og-tracking-label);text-transform:uppercase;font-weight:700}.fx-chain-node__dot{flex-shrink:0;border-radius:calc(infinity * 1px);width:5px;height:5px}.fx-chain-node__check{margin-left:auto;display:flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);width:13px;height:13px;font-size:9px}.fx-chain-node__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text));font-family:var(--og-font-mono);font-size:12px;font-weight:600}.fx-chain-node__caption{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text-muted));font-size:10px;line-height:1.3}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-duration: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
213
- }], ctorParameters: () => [], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], edges: [{ type: i0.Input, args: [{ isSignal: true, alias: "edges", required: false }] }], activeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeId", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], nodeClick: [{ type: i0.Output, args: ["nodeClick"] }], flow: [{ type: i0.ViewChild, args: [i0.forwardRef(() => VflowComponent), { isSignal: true }] }] } });
243
+ ], template: "<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <!-- maxZoom is 1, not 1.35: fitView scales a short chain UP to fill the\n band, and at 1.35 five 176px cards came out at 1.18\u00D7 \u2014 oversized\n against the design and wide enough to clip the last card (anh T\u00FA\n 16/09, MEASURED). The cards have a designed size, so fitting should\n only ever shrink. -->\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '\u2713' : '\u25CB' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--busy]=\"busy()\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;width:100%}.fx-chain-canvas{background:rgba(var(--og-surface-sunken)/1)}.fx-chain-node{display:flex;cursor:pointer;flex-direction:column;justify-content:center;gap:.125rem;border-radius:var(--og-radius-control);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface));padding-inline:.5rem;padding-block:.25rem;text-align:left;transition-property:all;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .15s;transition-duration:.15s;width:176px;height:66px}.fx-chain-node:hover:not(.fx-chain-node--active){border-color:rgb(var(--border-strong))}.fx-chain-node--active{border-color:rgb(var(--og-accent));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-width:2px}.fx-chain-node--related{border-color:rgb(var(--og-accent));border-color:rgba(var(--og-accent)/.45)}.fx-chain-node--dim{opacity:.55}.fx-chain-edge{fill:none;stroke:rgba(var(--og-border-strong)/1);stroke-width:1.5;transition:stroke .15s ease,stroke-width .15s ease,opacity .15s ease}.fx-chain-edge--active{stroke:rgba(var(--og-accent)/1);stroke-width:2.5}.fx-chain-edge--dim{opacity:.35}.fx-chain-edge--busy{stroke:rgba(var(--og-accent)/1);stroke-dasharray:5 4;animation:fx-chain-ants .6s linear infinite}@keyframes fx-chain-ants{to{stroke-dashoffset:-9}}@media(prefers-reduced-motion:reduce){.fx-chain-edge--busy{animation:none}}.fx-chain-node__eyebrow{display:flex;align-items:center;gap:.125rem;overflow:hidden;white-space:nowrap;font-size:9px;line-height:1.2;letter-spacing:var(--og-tracking-label);text-transform:uppercase;font-weight:700}.fx-chain-node__dot{flex-shrink:0;border-radius:calc(infinity * 1px);width:5px;height:5px}.fx-chain-node__check{margin-left:auto;display:flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);width:13px;height:13px;font-size:9px}.fx-chain-node__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text));font-family:var(--og-font-mono);font-size:12px;font-weight:600}.fx-chain-node__caption{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgb(var(--og-text-muted));font-size:10px;line-height:1.3}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-duration: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
244
+ }], ctorParameters: () => [], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], edges: [{ type: i0.Input, args: [{ isSignal: true, alias: "edges", required: false }] }], activeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeId", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], nodeClick: [{ type: i0.Output, args: ["nodeClick"] }], flow: [{ type: i0.ViewChild, args: [i0.forwardRef(() => VflowComponent), { isSignal: true }] }] } });
214
245
 
215
246
  class FlowConnection {
216
247
  nodes = input([], ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
@@ -1 +1 @@
1
- {"version":3,"file":"foxeltech-angular-ui-flow.mjs","sources":["../../../projects/ui/flow/src/flow-canvas/flow-canvas.component.ts","../../../projects/ui/flow/src/flow-canvas/flow-canvas.component.html","../../../projects/ui/flow/src/flow-connection/flow-connection.component.ts","../../../projects/ui/flow/src/flow-connection/flow-connection.component.html","../../../projects/ui/flow/src/foxeltech-angular-ui-flow.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n afterRenderEffect,\n computed,\n inject,\n input,\n output,\n untracked,\n viewChild,\n} from '@angular/core';\nimport { NgClass } from '@angular/common';\nimport {\n Edge,\n EdgeTemplateDirective,\n HandleComponent,\n Node,\n NodeHtmlTemplateDirective,\n VflowComponent,\n} from 'ngx-vflow';\n\n/**\n * What a chain node represents. Structural kinds (`entry`, `convergence`,\n * `impact`, `stage`) can be derived from the graph itself; the validation kinds\n * (`mitigation`, `review`, `not-reproduced`) come from an analyst verdict.\n */\nexport type FxChainNodeKind =\n | 'entry'\n | 'convergence'\n | 'mitigation'\n | 'review'\n | 'not-reproduced'\n | 'impact'\n | 'stage';\n\nexport interface FxChainNode {\n id: string;\n /** Node title — rendered in the figure/mono face. */\n label: string;\n kind: FxChainNodeKind;\n /** Caller-translated kind eyebrow; falls back to the raw kind key. */\n kindLabel?: string;\n /** Caller-translated muted subtitle. */\n caption?: string;\n /** true = verified badge, false = unverified badge, null/undefined = no badge. */\n verified?: boolean | null;\n /** Explicit column (0 = leftmost); omit to derive depth from the edges. */\n column?: number;\n /** Explicit row inside the column; omit to stack in declaration order. */\n row?: number;\n}\n\nexport interface FxChainEdge {\n source: string;\n target: string;\n}\n\n/** Kind → severity token. Exported so callers can tint matching chips (the\n stage list beside the canvas) from the same source of truth. */\nexport const FX_CHAIN_KIND_TONE: Record<FxChainNodeKind, string> = {\n entry: 'warning',\n convergence: 'information',\n mitigation: 'success',\n review: 'warning',\n 'not-reproduced': 'danger',\n impact: 'critical',\n stage: 'information',\n};\n\nconst COLUMN_GAP = 216;\nconst ROW_GAP = 104;\nconst NODE_WIDTH = 176;\nconst NODE_HEIGHT = 66;\n\n/**\n * V2 exploit-chain canvas — the attack-path graph of design refs 04..07:\n * kind-tinted node cards laid out left→right, curved edges, draggable nodes on\n * a pannable/zoomable surface.\n *\n * Presentational: it takes a graph plus the currently selected node and emits\n * clicks. Layout is derived from the edges (longest path from a source) unless\n * a node pins its own column/row, so callers can hand over a plain chain.\n *\n * <fx-ui-flow-canvas [nodes]=\"chainNodes\" [edges]=\"chainEdges\"\n * [activeId]=\"stageId()\" (nodeClick)=\"selectStage($event)\" />\n */\n@Component({\n selector: 'fx-ui-flow-canvas',\n templateUrl: './flow-canvas.component.html',\n styleUrl: './flow-canvas.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgClass,\n VflowComponent,\n NodeHtmlTemplateDirective,\n EdgeTemplateDirective,\n HandleComponent,\n ],\n})\nexport class FlowCanvasComponent {\n readonly nodes = input<FxChainNode[]>([]);\n readonly edges = input<FxChainEdge[]>([]);\n /** Selected/current node: full opacity + accent border; the rest dim. */\n readonly activeId = input<string | null>(null);\n /** Explicit canvas height; omit to size it from the number of stacked rows. */\n readonly height = input('');\n /** Caller-translated hint shown when the graph is empty. */\n readonly emptyText = input('No chain data available');\n\n readonly nodeClick = output<string>();\n\n private readonly flow = viewChild(VflowComponent);\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly destroyRef = inject(DestroyRef);\n\n constructor() {\n // `view=\"auto\"` only sizes vflow's viewport to the wrapper — it does not\n // frame the graph, so a taller canvas left the chain pinned top-left and\n // clipped (anh Tú 04/09). Fit whenever the graph or the box changes.\n afterRenderEffect(() => {\n const flow = this.flow();\n this.flowNodes();\n this.resolvedHeight();\n if (!flow) return;\n untracked(() => this.scheduleFit());\n });\n if (typeof ResizeObserver !== 'undefined') {\n const ro = new ResizeObserver(() => this.scheduleFit());\n ro.observe(this.host.nativeElement);\n this.destroyRef.onDestroy(() => ro.disconnect());\n }\n }\n\n private fitHandle: number | null = null;\n /** Coalesces fit requests into one frame after vflow has measured its nodes. */\n private scheduleFit(): void {\n if (this.fitHandle !== null) return;\n this.fitHandle = requestAnimationFrame(() => {\n this.fitHandle = null;\n const flow = this.flow();\n if (!flow || this.isEmpty()) return;\n // Design 04: cards fill the width with a little air (0.18 left them at\n // ~0.74×, visibly smaller than the mock — measured 04/09).\n flow.fitView({ padding: 0.1, duration: 0 });\n });\n }\n\n /** Column index per node: pinned value, else longest path from a source. */\n private readonly columns = computed<Record<string, number>>(() => {\n const nodes = this.nodes();\n const edges = this.edges();\n const incoming = new Map<string, string[]>();\n nodes.forEach((n) => incoming.set(n.id, []));\n edges.forEach((e) => incoming.get(e.target)?.push(e.source));\n\n const depth: Record<string, number> = {};\n const resolve = (id: string, seen: Set<string>): number => {\n if (depth[id] !== undefined) return depth[id];\n // Cycle guard: a malformed graph must not blow the stack.\n if (seen.has(id)) return 0;\n seen.add(id);\n const parents = incoming.get(id) ?? [];\n const value = parents.length\n ? Math.max(...parents.map((p) => resolve(p, seen))) + 1\n : 0;\n depth[id] = value;\n return value;\n };\n nodes.forEach((n) => {\n depth[n.id] = n.column ?? resolve(n.id, new Set());\n });\n return depth;\n });\n\n /** ngx-vflow nodes: html-template cards positioned by the derived layout. */\n readonly flowNodes = computed<Node[]>(() => {\n const columns = this.columns();\n const perColumn: Record<number, number> = {};\n // Tallest column decides the vertical centre every column is balanced on.\n const counts: Record<number, number> = {};\n this.nodes().forEach((n) => {\n const c = columns[n.id] ?? 0;\n counts[c] = (counts[c] ?? 0) + 1;\n });\n const tallest = Math.max(1, ...Object.values(counts));\n\n return this.nodes().map((node) => {\n const column = columns[node.id] ?? 0;\n const row = node.row ?? perColumn[column] ?? 0;\n perColumn[column] = row + 1;\n const rows = counts[column] ?? 1;\n const offset = ((tallest - rows) * ROW_GAP) / 2;\n return {\n id: node.id,\n type: 'html-template' as const,\n point: { x: 24 + column * COLUMN_GAP, y: 24 + offset + row * ROW_GAP },\n width: NODE_WIDTH,\n height: NODE_HEIGHT,\n data: node,\n };\n });\n });\n\n /**\n * Edges stay identity-stable across selections: state lives in the template\n * (read off `activeId`), so selecting a stage never re-creates them.\n */\n readonly flowEdges = computed<Edge[]>(() =>\n this.edges().map((e) => ({\n id: `${e.source}->${e.target}`,\n source: e.source,\n target: e.target,\n type: 'template' as const,\n curve: 'bezier' as const,\n markers: { end: { type: 'arrow-closed' as const, width: 11, height: 11 } },\n })),\n );\n\n /** Nodes one hop from the selection — they stay lit alongside it. */\n private readonly relatedIds = computed<Set<string>>(() => {\n const active = this.activeId();\n const related = new Set<string>();\n if (!active) return related;\n this.edges().forEach((e) => {\n if (e.source === active) related.add(e.target);\n if (e.target === active) related.add(e.source);\n });\n return related;\n });\n\n readonly isEmpty = computed(() => this.nodes().length === 0);\n\n /**\n * A linear chain needs one row, the design's converging entries need three —\n * sizing from the layout keeps the card from leaving a tall empty band.\n */\n readonly resolvedHeight = computed(() => {\n const explicit = this.height();\n if (explicit) return explicit;\n const counts: Record<number, number> = {};\n const columns = this.columns();\n this.nodes().forEach((n) => {\n const c = columns[n.id] ?? 0;\n counts[c] = (counts[c] ?? 0) + 1;\n });\n const rows = Math.max(1, ...Object.values(counts));\n // Floor at 300px: a one-row chain in a 152px band read too small (04/09).\n return `${Math.max(300, 48 + rows * ROW_GAP)}px`;\n });\n\n kindTone(kind: FxChainNodeKind): string {\n return FX_CHAIN_KIND_TONE[kind] ?? 'information';\n }\n\n isActive(node: FxChainNode): boolean {\n const active = this.activeId();\n return active != null && active === node.id;\n }\n\n /** One hop from the selection: lit, but without the selected accent frame. */\n isRelated(node: FxChainNode): boolean {\n return !this.isActive(node) && this.relatedIds().has(node.id);\n }\n\n /** Nothing selected → every node reads at full strength. */\n isDimmed(node: FxChainNode): boolean {\n return this.activeId() != null && !this.isActive(node) && !this.isRelated(node);\n }\n\n /** An edge touching the selection is part of the step being read. */\n isEdgeActive(edge: Edge): boolean {\n const active = this.activeId();\n return active != null && (edge.source === active || edge.target === active);\n }\n\n isEdgeDimmed(edge: Edge): boolean {\n return this.activeId() != null && !this.isEdgeActive(edge);\n }\n}\n","<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1.35\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '✓' : '○' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n","import { Component, Input, ChangeDetectionStrategy, input } from '@angular/core';\nimport { Node, Edge, Vflow, Connection, ChangesControllerDirective, VflowComponent, ConnectionControllerDirective } from 'ngx-vflow';\n\n@Component({\n selector: 'fx-ui-flow-connection',\n templateUrl: './flow-connection.component.html',\n styleUrl: './flow-connection.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n ChangesControllerDirective,\n VflowComponent,\n ConnectionControllerDirective,\n ],\n})\nexport class FlowConnection {\n readonly nodes = input<Node[]>([]);\n @Input() edges: Edge[] = [];\n readonly backgroundColor = input<any>('--bg-hover-primary');\n readonly view = input<any>(['100%', '400px']);\n\n handleConnection(connection: Connection) {\n this.edges = [\n ...this.edges,\n {\n id: `${connection.source} -> ${connection.target}`,\n ...connection,\n },\n ];\n }\n}\n","<vflow\n [background]=\"{\n type: 'solid',\n color: `rgba(var(${backgroundColor()})`\n}\"\n [nodes]=\"nodes()\"\n [edges]=\"edges\"\n (onConnect)=\"handleConnection($event)\"\n [view]=\"view()\"\n/>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"foxeltech-angular-ui-flow.mjs","sources":["../../../projects/ui/flow/src/flow-canvas/flow-canvas.component.ts","../../../projects/ui/flow/src/flow-canvas/flow-canvas.component.html","../../../projects/ui/flow/src/flow-connection/flow-connection.component.ts","../../../projects/ui/flow/src/flow-connection/flow-connection.component.html","../../../projects/ui/flow/src/foxeltech-angular-ui-flow.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n afterRenderEffect,\n computed,\n inject,\n input,\n output,\n untracked,\n viewChild,\n} from '@angular/core';\nimport { NgClass } from '@angular/common';\nimport {\n Edge,\n EdgeTemplateDirective,\n HandleComponent,\n Node,\n NodeHtmlTemplateDirective,\n VflowComponent,\n} from 'ngx-vflow';\n\n/**\n * What a chain node represents. Structural kinds (`entry`, `convergence`,\n * `impact`, `stage`) can be derived from the graph itself; the validation kinds\n * (`mitigation`, `review`, `not-reproduced`) come from an analyst verdict.\n */\nexport type FxChainNodeKind =\n | 'entry'\n | 'convergence'\n | 'mitigation'\n | 'review'\n | 'not-reproduced'\n | 'impact'\n | 'stage';\n\nexport interface FxChainNode {\n id: string;\n /** Node title — rendered in the figure/mono face. */\n label: string;\n kind: FxChainNodeKind;\n /** Caller-translated kind eyebrow; falls back to the raw kind key. */\n kindLabel?: string;\n /** Caller-translated muted subtitle. */\n caption?: string;\n /** true = verified badge, false = unverified badge, null/undefined = no badge. */\n verified?: boolean | null;\n /** Explicit column (0 = leftmost); omit to derive depth from the edges. */\n column?: number;\n /** Explicit row inside the column; omit to stack in declaration order. */\n row?: number;\n}\n\nexport interface FxChainEdge {\n source: string;\n target: string;\n}\n\n/** Kind → severity token. Exported so callers can tint matching chips (the\n stage list beside the canvas) from the same source of truth. */\nexport const FX_CHAIN_KIND_TONE: Record<FxChainNodeKind, string> = {\n entry: 'warning',\n convergence: 'information',\n mitigation: 'success',\n review: 'warning',\n 'not-reproduced': 'danger',\n impact: 'critical',\n stage: 'information',\n};\n\nconst COLUMN_GAP = 216;\nconst ROW_GAP = 104;\nconst NODE_WIDTH = 176;\nconst NODE_HEIGHT = 66;\n\n/**\n * V2 exploit-chain canvas — the attack-path graph of design refs 04..07:\n * kind-tinted node cards laid out left→right, curved edges, draggable nodes on\n * a pannable/zoomable surface.\n *\n * Presentational: it takes a graph plus the currently selected node and emits\n * clicks. Layout is derived from the edges (longest path from a source) unless\n * a node pins its own column/row, so callers can hand over a plain chain.\n *\n * <fx-ui-flow-canvas [nodes]=\"chainNodes\" [edges]=\"chainEdges\"\n * [activeId]=\"stageId()\" (nodeClick)=\"selectStage($event)\" />\n */\n@Component({\n selector: 'fx-ui-flow-canvas',\n templateUrl: './flow-canvas.component.html',\n styleUrl: './flow-canvas.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgClass,\n VflowComponent,\n NodeHtmlTemplateDirective,\n EdgeTemplateDirective,\n HandleComponent,\n ],\n})\nexport class FlowCanvasComponent {\n readonly nodes = input<FxChainNode[]>([]);\n readonly edges = input<FxChainEdge[]>([]);\n /** Selected/current node: full opacity + accent border; the rest dim. */\n readonly activeId = input<string | null>(null);\n /** Explicit canvas height; omit to size it from the number of stacked rows. */\n readonly height = input('');\n /** Caller-translated hint shown when the graph is empty. */\n readonly emptyText = input('No chain data available');\n /**\n * Work is running against this chain — validation, typically.\n *\n * The edges become a travelling dashed line (\"marching ants\") so the canvas\n * says something is in flight without a spinner sitting on top of the graph.\n * Purely presentational: the caller owns what \"busy\" means and when it ends.\n */\n readonly busy = input(false);\n\n readonly nodeClick = output<string>();\n\n private readonly flow = viewChild(VflowComponent);\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly destroyRef = inject(DestroyRef);\n\n constructor() {\n // `view=\"auto\"` only sizes vflow's viewport to the wrapper — it does not\n // frame the graph, so a taller canvas left the chain pinned top-left and\n // clipped (anh Tú 04/09). Fit whenever the graph or the box changes.\n afterRenderEffect(() => {\n const flow = this.flow();\n this.flowNodes();\n this.resolvedHeight();\n if (!flow) return;\n untracked(() => this.scheduleFit());\n });\n if (typeof ResizeObserver !== 'undefined') {\n const ro = new ResizeObserver(() => this.scheduleFit());\n ro.observe(this.host.nativeElement);\n this.destroyRef.onDestroy(() => ro.disconnect());\n }\n }\n\n private fitHandle: number | null = null;\n /**\n * Coalesces fit requests, then fits TWICE — one frame apart.\n *\n * vflow frames the graph from `getNodesBounds`, which sums each node's\n * `point()` and its `size()`. For an html-template node that size is the\n * MEASURED box and is still 0 on the frame the nodes are created, so the\n * first fit sees a bounds box running origin-to-origin: five 176px cards\n * measured 864px instead of 1040px, and the graph was framed 88px — half a\n * card — too far right, clipping the last one (MEASURED 16/09).\n *\n * The second pass runs once the cards have been laid out and measured, and\n * is what actually frames them. The first is kept so a slow frame still\n * shows something sensible rather than the graph pinned top-left.\n */\n private scheduleFit(): void {\n if (this.fitHandle !== null) return;\n this.fitHandle = requestAnimationFrame(() => {\n this.fit();\n this.fitHandle = requestAnimationFrame(() => {\n this.fitHandle = null;\n this.fit();\n });\n });\n }\n\n private fit(): void {\n const flow = this.flow();\n if (!flow || this.isEmpty()) return;\n // Design 04: cards fill the width with a little air (0.18 left them at\n // ~0.74×, visibly smaller than the mock — measured 04/09).\n flow.fitView({ padding: 0.1, duration: 0 });\n // Never MAGNIFY: the cap is `[maxZoom]` on the vflow element, which\n // fitView respects. Clamping afterwards does NOT work — both `zoomTo`\n // and `viewportTo` return without changing `viewport()` here (MEASURED\n // 16/09).\n }\n\n /** Column index per node: pinned value, else longest path from a source. */\n private readonly columns = computed<Record<string, number>>(() => {\n const nodes = this.nodes();\n const edges = this.edges();\n const incoming = new Map<string, string[]>();\n nodes.forEach((n) => incoming.set(n.id, []));\n edges.forEach((e) => incoming.get(e.target)?.push(e.source));\n\n const depth: Record<string, number> = {};\n const resolve = (id: string, seen: Set<string>): number => {\n if (depth[id] !== undefined) return depth[id];\n // Cycle guard: a malformed graph must not blow the stack.\n if (seen.has(id)) return 0;\n seen.add(id);\n const parents = incoming.get(id) ?? [];\n const value = parents.length\n ? Math.max(...parents.map((p) => resolve(p, seen))) + 1\n : 0;\n depth[id] = value;\n return value;\n };\n nodes.forEach((n) => {\n depth[n.id] = n.column ?? resolve(n.id, new Set());\n });\n return depth;\n });\n\n /** ngx-vflow nodes: html-template cards positioned by the derived layout. */\n readonly flowNodes = computed<Node[]>(() => {\n const columns = this.columns();\n const perColumn: Record<number, number> = {};\n // Tallest column decides the vertical centre every column is balanced on.\n const counts: Record<number, number> = {};\n this.nodes().forEach((n) => {\n const c = columns[n.id] ?? 0;\n counts[c] = (counts[c] ?? 0) + 1;\n });\n const tallest = Math.max(1, ...Object.values(counts));\n\n return this.nodes().map((node) => {\n const column = columns[node.id] ?? 0;\n const row = node.row ?? perColumn[column] ?? 0;\n perColumn[column] = row + 1;\n const rows = counts[column] ?? 1;\n const offset = ((tallest - rows) * ROW_GAP) / 2;\n return {\n id: node.id,\n type: 'html-template' as const,\n point: { x: 24 + column * COLUMN_GAP, y: 24 + offset + row * ROW_GAP },\n width: NODE_WIDTH,\n height: NODE_HEIGHT,\n data: node,\n };\n });\n });\n\n /**\n * Edges stay identity-stable across selections: state lives in the template\n * (read off `activeId`), so selecting a stage never re-creates them.\n */\n readonly flowEdges = computed<Edge[]>(() =>\n this.edges().map((e) => ({\n id: `${e.source}->${e.target}`,\n source: e.source,\n target: e.target,\n type: 'template' as const,\n curve: 'bezier' as const,\n markers: { end: { type: 'arrow-closed' as const, width: 11, height: 11 } },\n })),\n );\n\n /** Nodes one hop from the selection — they stay lit alongside it. */\n private readonly relatedIds = computed<Set<string>>(() => {\n const active = this.activeId();\n const related = new Set<string>();\n if (!active) return related;\n this.edges().forEach((e) => {\n if (e.source === active) related.add(e.target);\n if (e.target === active) related.add(e.source);\n });\n return related;\n });\n\n readonly isEmpty = computed(() => this.nodes().length === 0);\n\n /**\n * A linear chain needs one row, the design's converging entries need three —\n * sizing from the layout keeps the card from leaving a tall empty band.\n */\n readonly resolvedHeight = computed(() => {\n const explicit = this.height();\n if (explicit) return explicit;\n const counts: Record<number, number> = {};\n const columns = this.columns();\n this.nodes().forEach((n) => {\n const c = columns[n.id] ?? 0;\n counts[c] = (counts[c] ?? 0) + 1;\n });\n const rows = Math.max(1, ...Object.values(counts));\n // Floor at 300px: a one-row chain in a 152px band read too small (04/09).\n return `${Math.max(300, 48 + rows * ROW_GAP)}px`;\n });\n\n kindTone(kind: FxChainNodeKind): string {\n return FX_CHAIN_KIND_TONE[kind] ?? 'information';\n }\n\n isActive(node: FxChainNode): boolean {\n const active = this.activeId();\n return active != null && active === node.id;\n }\n\n /** One hop from the selection: lit, but without the selected accent frame. */\n isRelated(node: FxChainNode): boolean {\n return !this.isActive(node) && this.relatedIds().has(node.id);\n }\n\n /** Nothing selected → every node reads at full strength. */\n isDimmed(node: FxChainNode): boolean {\n return this.activeId() != null && !this.isActive(node) && !this.isRelated(node);\n }\n\n /** An edge touching the selection is part of the step being read. */\n isEdgeActive(edge: Edge): boolean {\n const active = this.activeId();\n return active != null && (edge.source === active || edge.target === active);\n }\n\n isEdgeDimmed(edge: Edge): boolean {\n return this.activeId() != null && !this.isEdgeActive(edge);\n }\n}\n","<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <!-- maxZoom is 1, not 1.35: fitView scales a short chain UP to fill the\n band, and at 1.35 five 176px cards came out at 1.18× — oversized\n against the design and wide enough to clip the last card (anh Tú\n 16/09, MEASURED). The cards have a designed size, so fitting should\n only ever shrink. -->\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '✓' : '○' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--busy]=\"busy()\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n","import { Component, Input, ChangeDetectionStrategy, input } from '@angular/core';\nimport { Node, Edge, Vflow, Connection, ChangesControllerDirective, VflowComponent, ConnectionControllerDirective } from 'ngx-vflow';\n\n@Component({\n selector: 'fx-ui-flow-connection',\n templateUrl: './flow-connection.component.html',\n styleUrl: './flow-connection.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n ChangesControllerDirective,\n VflowComponent,\n ConnectionControllerDirective,\n ],\n})\nexport class FlowConnection {\n readonly nodes = input<Node[]>([]);\n @Input() edges: Edge[] = [];\n readonly backgroundColor = input<any>('--bg-hover-primary');\n readonly view = input<any>(['100%', '400px']);\n\n handleConnection(connection: Connection) {\n this.edges = [\n ...this.edges,\n {\n id: `${connection.source} -> ${connection.target}`,\n ...connection,\n },\n ];\n }\n}\n","<vflow\n [background]=\"{\n type: 'solid',\n color: `rgba(var(${backgroundColor()})`\n}\"\n [nodes]=\"nodes()\"\n [edges]=\"edges\"\n (onConnect)=\"handleConnection($event)\"\n [view]=\"view()\"\n/>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -2581,11 +2581,11 @@ class ButtonComponent {
2581
2581
  this.clicked.emit();
2582
2582
  }
2583
2583
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2584
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'fx-btn--icon': !!icon && !label,\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'fx-btn--icon': !!icon && !label,\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n <!-- Loading keeps the LABEL, as the sign-in button does: a button that\n swaps its text for a spinner collapses to a stub, and the row jumps.\n The ring is that button's `.li-spin`, in the foreground the variant\n already uses for its icon so it reads on a coloured ground. -->\n <!-- 6px, the reference button's own gap; `gap-small` is 4px. -->\n <div class=\"flex items-center justify-center gap-[6px]\">\n @if (loading()) {\n <span\n class=\"fx-btn__spin\"\n aria-hidden=\"true\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></span>\n } @else if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"13\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n</button>\n", styles: ["@charset \"UTF-8\";.fx-btn__spin{display:inline-block;width:12px;height:12px;flex:0 0 auto;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;opacity:.9;animation:fx-btn-spin .8s linear infinite}@keyframes fx-btn-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.fx-btn__spin{animation:none}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2585
2585
  }
2586
2586
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
2587
2587
  type: Component,
2588
- args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'fx-btn--icon': !!icon && !label,\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2588
+ args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'fx-btn--icon': !!icon && !label,\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n <!-- Loading keeps the LABEL, as the sign-in button does: a button that\n swaps its text for a spinner collapses to a stub, and the row jumps.\n The ring is that button's `.li-spin`, in the foreground the variant\n already uses for its icon so it reads on a coloured ground. -->\n <!-- 6px, the reference button's own gap; `gap-small` is 4px. -->\n <div class=\"flex items-center justify-center gap-[6px]\">\n @if (loading()) {\n <span\n class=\"fx-btn__spin\"\n aria-hidden=\"true\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></span>\n } @else if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"13\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n</button>\n", styles: ["@charset \"UTF-8\";.fx-btn__spin{display:inline-block;width:12px;height:12px;flex:0 0 auto;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;opacity:.9;animation:fx-btn-spin .8s linear infinite}@keyframes fx-btn-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.fx-btn__spin{animation:none}}\n"] }]
2589
2589
  }], propDecorators: { label: [{
2590
2590
  type: Input
2591
2591
  }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], buttonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonVariant", required: false }] }], icon: [{
@@ -4758,8 +4758,30 @@ class MasterDetailComponent {
4758
4758
  /** True when the host projected a [mdFacets] block — switches to 3 columns. */
4759
4759
  hasFacets = false;
4760
4760
  table;
4761
- /** Fixed list-pane width on lg+ viewports (stacked full-width below). */
4762
- listWidth = input('420px', ...(ngDevMode ? [{ debugName: "listWidth" }] : /* istanbul ignore next */ []));
4761
+ /**
4762
+ * Optional CAP on the list pane.
4763
+ *
4764
+ * The list is the flexible track now (the design's `204px 1fr 340px`), so it
4765
+ * takes whatever the rail and the detail leave. Set this only for a screen
4766
+ * whose rows genuinely stop reading better past some width; left unset the
4767
+ * property is not emitted at all and the pane simply fills.
4768
+ */
4769
+ listWidth = input(null, ...(ngDevMode ? [{ debugName: "listWidth" }] : /* istanbul ignore next */ []));
4770
+ /**
4771
+ * The detail pane's fixed track — the design's own 340px, and 450px on the
4772
+ * screens anh Tú widened (17/09).
4773
+ *
4774
+ * Pass `'auto'` (or null) to give the detail the SPARE width instead and cap
4775
+ * the list at `listWidth` — the pre-0.7.130 behaviour, which Binaries and
4776
+ * Remediation keep because their panes carry header blocks, metric grids and
4777
+ * step lists that a narrow track cannot hold.
4778
+ */
4779
+ detailWidth = input('340px', ...(ngDevMode ? [{ debugName: "detailWidth" }] : /* istanbul ignore next */ []));
4780
+ /** true = detail takes the spare width; false = it is a fixed track. */
4781
+ detailFluid() {
4782
+ const w = this.detailWidth();
4783
+ return !w || w === 'auto';
4784
+ }
4763
4785
  /**
4764
4786
  * Chiều cao CHUNG của cả ba cụm (facets / list / detail). Mặc định trừ phần
4765
4787
  * chrome phía trên (topbar + tab strip + context line) khỏi viewport để
@@ -4861,7 +4883,7 @@ class MasterDetailComponent {
4861
4883
  el?.scrollIntoView({ block: 'nearest' });
4862
4884
  }
4863
4885
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4864
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLabel: { classPropertyName: "pageLabel", publicName: "pageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, facetsOpen: { classPropertyName: "facetsOpen", publicName: "facetsOpen", isSignal: true, isRequired: false, transformFunction: null }, facetsLabel: { classPropertyName: "facetsLabel", publicName: "facetsLabel", isSignal: true, isRequired: false, transformFunction: null }, expandFacetsLabel: { classPropertyName: "expandFacetsLabel", publicName: "expandFacetsLabel", isSignal: true, isRequired: false, transformFunction: null }, collapseFacetsLabel: { classPropertyName: "collapseFacetsLabel", publicName: "collapseFacetsLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- ONE frame, three columns (designs 11/14/17): facets rail \u00B7 list \u00B7 detail,\n separated by hairlines, sharing one height; only each pane's body scrolls.\n Row direction / height come from the CONTAINER QUERY in the CSS (the row's\n own width), never from `lg:*` \u2014 see the note at the top of the .css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch\"\n [class.fx-md-row--rail-open]=\"railOpen()\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail \u2014 collapsible (\u00BB / \u00AB), collapsed = 40px strip with the label\n set vertically. The [mdFacets] projection is unconditional (hasFacets is\n detected by querying it) and only hidden while collapsed. -->\n <aside\n class=\"fx-md-facets flex flex-col min-h-0 min-w-0\"\n [class.hidden]=\"!hasFacets\"\n [class.fx-md-facets--open]=\"railOpen()\"\n >\n <div class=\"fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0\" [class.hidden]=\"!railOpen()\">\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0\"\n [attr.aria-label]=\"collapseFacetsLabel()\"\n (click)=\"railOpen.set(false)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-left\" [size]=\"14\" class=\"flex\" />\n </button>\n <span class=\"txt-eyebrow truncate\">{{ facetsLabel() }}</span>\n </div>\n <div class=\"flex-1 min-h-0 overflow-auto px-normal py-normal\" [class.hidden]=\"!railOpen()\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n <button\n type=\"button\"\n class=\"fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors\"\n [class.hidden]=\"railOpen()\"\n [attr.aria-label]=\"expandFacetsLabel()\"\n [attr.aria-expanded]=\"false\"\n (click)=\"railOpen.set(true)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-right\" [size]=\"14\" class=\"flex shrink-0\" />\n <span class=\"fx-md-rail-label txt-eyebrow whitespace-nowrap\">{{ facetsLabel() }}</span>\n </button>\n </aside>\n\n <!-- Master: list pane -->\n <section class=\"fx-md-list flex flex-col min-h-0 min-w-0\">\n <div class=\"fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n </div>\n <div\n class=\"flex flex-col overflow-auto flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <div class=\"px-large py-normal\">\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n </div>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <!-- Hairline rows; the selected one is tinted with an accent bar on\n the left (design 11/14 selected finding / component). -->\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-l-accent bg-accent-soft/60'\n : 'border-l-transparent hover:bg-surface-sunken'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <div class=\"px-large py-large\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"fx-md-paginator\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </section>\n\n <!-- Detail pane -->\n <section class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto px-large py-large\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </section>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-row{background:rgb(var(--og-surface));border:1px solid rgb(var(--border-default));border-radius:.5rem;overflow:hidden;box-shadow:0 1px 2px #0000000a,0 3px 8px #0000000d}.fx-md-facets,.fx-md-list{border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-md-rail-toggle{width:100%;padding:10px 12px;flex-direction:row;justify-content:flex-start}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 720px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets,.fx-md-list{border-bottom:0;border-right:1px solid rgb(var(--og-border-subtle))}.fx-md-facets{flex:0 0 40px;width:40px;min-width:40px;max-width:40px}.fx-md-facets--open{flex:0 1 230px;width:auto;min-width:200px;max-width:260px}.fx-md-rail-toggle{height:100%;padding:12px 0;flex-direction:column;justify-content:flex-start}.fx-md-rail-label{writing-mode:vertical-rl;transform:rotate(180deg)}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:260px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 360px;min-width:360px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.fx-md-paginator{padding:0 12px;border-top:1px solid rgb(var(--og-border-subtle))}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }] });
4886
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, detailWidth: { classPropertyName: "detailWidth", publicName: "detailWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLabel: { classPropertyName: "pageLabel", publicName: "pageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, facetsOpen: { classPropertyName: "facetsOpen", publicName: "facetsOpen", isSignal: true, isRequired: false, transformFunction: null }, facetsLabel: { classPropertyName: "facetsLabel", publicName: "facetsLabel", isSignal: true, isRequired: false, transformFunction: null }, expandFacetsLabel: { classPropertyName: "expandFacetsLabel", publicName: "expandFacetsLabel", isSignal: true, isRequired: false, transformFunction: null }, collapseFacetsLabel: { classPropertyName: "collapseFacetsLabel", publicName: "collapseFacetsLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- ONE frame, three columns (designs 11/14/17): facets rail \u00B7 list \u00B7 detail,\n separated by hairlines, sharing one height; only each pane's body scrolls.\n Row direction / height come from the CONTAINER QUERY in the CSS (the row's\n own width), never from `lg:*` \u2014 see the note at the top of the .css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch\"\n [class.fx-md-row--rail-open]=\"railOpen()\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [class.fx-md-row--detail-fluid]=\"detailFluid()\"\n [style.--fx-md-detail-width]=\"detailFluid() ? null : detailWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail \u2014 collapsible (\u00BB / \u00AB), collapsed = 40px strip with the label\n set vertically. The [mdFacets] projection is unconditional (hasFacets is\n detected by querying it) and only hidden while collapsed. -->\n <aside\n class=\"fx-md-facets flex flex-col min-h-0 min-w-0\"\n [class.hidden]=\"!hasFacets\"\n [class.fx-md-facets--open]=\"railOpen()\"\n >\n <div class=\"fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0\" [class.hidden]=\"!railOpen()\">\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0\"\n [attr.aria-label]=\"collapseFacetsLabel()\"\n (click)=\"railOpen.set(false)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-left\" [size]=\"14\" class=\"flex\" />\n </button>\n <span class=\"txt-eyebrow truncate\">{{ facetsLabel() }}</span>\n </div>\n <div class=\"flex-1 min-h-0 overflow-auto px-normal py-normal\" [class.hidden]=\"!railOpen()\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n <button\n type=\"button\"\n class=\"fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors\"\n [class.hidden]=\"railOpen()\"\n [attr.aria-label]=\"expandFacetsLabel()\"\n [attr.aria-expanded]=\"false\"\n (click)=\"railOpen.set(true)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-right\" [size]=\"14\" class=\"flex shrink-0\" />\n <span class=\"fx-md-rail-label txt-eyebrow whitespace-nowrap\">{{ facetsLabel() }}</span>\n </button>\n </aside>\n\n <!-- Master: list pane -->\n <section class=\"fx-md-list flex flex-col min-h-0 min-w-0\">\n <div class=\"fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n </div>\n <div\n class=\"flex flex-col overflow-auto flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <div class=\"px-large py-normal\">\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n </div>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <!-- Hairline rows; the selected one is tinted with an accent bar on\n the left (design 11/14 selected finding / component). -->\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-l-accent bg-accent-soft/60'\n : 'border-l-transparent hover:bg-surface-sunken'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <div class=\"px-large py-large\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"fx-md-paginator\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </section>\n\n <!-- Detail pane -->\n <section class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto px-large py-large\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </section>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-row{background:rgb(var(--og-surface));border:1px solid rgb(var(--border-default));border-radius:.5rem;overflow:hidden;box-shadow:0 1px 2px #0000000a,0 3px 8px #0000000d}.fx-md-facets,.fx-md-list{border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-md-rail-toggle{width:100%;padding:10px 12px;flex-direction:row;justify-content:flex-start}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 720px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets,.fx-md-list{border-bottom:0;border-right:1px solid rgb(var(--og-border-subtle))}.fx-md-facets{flex:0 0 40px;width:40px;min-width:40px;max-width:40px}.fx-md-facets--open{flex:0 1 204px;width:auto;min-width:180px;max-width:204px}.fx-md-rail-toggle{height:100%;padding:12px 0;flex-direction:column;justify-content:flex-start}.fx-md-rail-label{writing-mode:vertical-rl;transform:rotate(180deg)}.fx-md-list{width:auto;flex:1 1 0;min-width:260px;max-width:var(--fx-md-list-width, none)}.fx-md-detail{width:auto;flex:0 1 var(--fx-md-detail-width, 340px);min-width:300px;max-width:var(--fx-md-detail-width, 340px)}.fx-md-row--detail-fluid .fx-md-list{flex:0 1 var(--fx-md-list-width, 420px);max-width:var(--fx-md-list-width, 420px)}.fx-md-row--detail-fluid .fx-md-detail{flex:1 1 360px;min-width:360px;max-width:none}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.fx-md-paginator{padding:0 12px;border-top:1px solid rgb(var(--og-border-subtle))}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }] });
4865
4887
  }
4866
4888
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
4867
4889
  type: Component,
@@ -4873,10 +4895,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4873
4895
  MatPaginator,
4874
4896
  SkeletonDetailComponent,
4875
4897
  HeroIconComponent,
4876
- ], template: "<!-- ONE frame, three columns (designs 11/14/17): facets rail \u00B7 list \u00B7 detail,\n separated by hairlines, sharing one height; only each pane's body scrolls.\n Row direction / height come from the CONTAINER QUERY in the CSS (the row's\n own width), never from `lg:*` \u2014 see the note at the top of the .css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch\"\n [class.fx-md-row--rail-open]=\"railOpen()\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail \u2014 collapsible (\u00BB / \u00AB), collapsed = 40px strip with the label\n set vertically. The [mdFacets] projection is unconditional (hasFacets is\n detected by querying it) and only hidden while collapsed. -->\n <aside\n class=\"fx-md-facets flex flex-col min-h-0 min-w-0\"\n [class.hidden]=\"!hasFacets\"\n [class.fx-md-facets--open]=\"railOpen()\"\n >\n <div class=\"fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0\" [class.hidden]=\"!railOpen()\">\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0\"\n [attr.aria-label]=\"collapseFacetsLabel()\"\n (click)=\"railOpen.set(false)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-left\" [size]=\"14\" class=\"flex\" />\n </button>\n <span class=\"txt-eyebrow truncate\">{{ facetsLabel() }}</span>\n </div>\n <div class=\"flex-1 min-h-0 overflow-auto px-normal py-normal\" [class.hidden]=\"!railOpen()\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n <button\n type=\"button\"\n class=\"fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors\"\n [class.hidden]=\"railOpen()\"\n [attr.aria-label]=\"expandFacetsLabel()\"\n [attr.aria-expanded]=\"false\"\n (click)=\"railOpen.set(true)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-right\" [size]=\"14\" class=\"flex shrink-0\" />\n <span class=\"fx-md-rail-label txt-eyebrow whitespace-nowrap\">{{ facetsLabel() }}</span>\n </button>\n </aside>\n\n <!-- Master: list pane -->\n <section class=\"fx-md-list flex flex-col min-h-0 min-w-0\">\n <div class=\"fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n </div>\n <div\n class=\"flex flex-col overflow-auto flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <div class=\"px-large py-normal\">\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n </div>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <!-- Hairline rows; the selected one is tinted with an accent bar on\n the left (design 11/14 selected finding / component). -->\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-l-accent bg-accent-soft/60'\n : 'border-l-transparent hover:bg-surface-sunken'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <div class=\"px-large py-large\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"fx-md-paginator\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </section>\n\n <!-- Detail pane -->\n <section class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto px-large py-large\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </section>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-row{background:rgb(var(--og-surface));border:1px solid rgb(var(--border-default));border-radius:.5rem;overflow:hidden;box-shadow:0 1px 2px #0000000a,0 3px 8px #0000000d}.fx-md-facets,.fx-md-list{border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-md-rail-toggle{width:100%;padding:10px 12px;flex-direction:row;justify-content:flex-start}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 720px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets,.fx-md-list{border-bottom:0;border-right:1px solid rgb(var(--og-border-subtle))}.fx-md-facets{flex:0 0 40px;width:40px;min-width:40px;max-width:40px}.fx-md-facets--open{flex:0 1 230px;width:auto;min-width:200px;max-width:260px}.fx-md-rail-toggle{height:100%;padding:12px 0;flex-direction:column;justify-content:flex-start}.fx-md-rail-label{writing-mode:vertical-rl;transform:rotate(180deg)}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:260px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 360px;min-width:360px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.fx-md-paginator{padding:0 12px;border-top:1px solid rgb(var(--og-border-subtle))}\n"] }]
4898
+ ], template: "<!-- ONE frame, three columns (designs 11/14/17): facets rail \u00B7 list \u00B7 detail,\n separated by hairlines, sharing one height; only each pane's body scrolls.\n Row direction / height come from the CONTAINER QUERY in the CSS (the row's\n own width), never from `lg:*` \u2014 see the note at the top of the .css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch\"\n [class.fx-md-row--rail-open]=\"railOpen()\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [class.fx-md-row--detail-fluid]=\"detailFluid()\"\n [style.--fx-md-detail-width]=\"detailFluid() ? null : detailWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail \u2014 collapsible (\u00BB / \u00AB), collapsed = 40px strip with the label\n set vertically. The [mdFacets] projection is unconditional (hasFacets is\n detected by querying it) and only hidden while collapsed. -->\n <aside\n class=\"fx-md-facets flex flex-col min-h-0 min-w-0\"\n [class.hidden]=\"!hasFacets\"\n [class.fx-md-facets--open]=\"railOpen()\"\n >\n <div class=\"fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0\" [class.hidden]=\"!railOpen()\">\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0\"\n [attr.aria-label]=\"collapseFacetsLabel()\"\n (click)=\"railOpen.set(false)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-left\" [size]=\"14\" class=\"flex\" />\n </button>\n <span class=\"txt-eyebrow truncate\">{{ facetsLabel() }}</span>\n </div>\n <div class=\"flex-1 min-h-0 overflow-auto px-normal py-normal\" [class.hidden]=\"!railOpen()\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n <button\n type=\"button\"\n class=\"fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors\"\n [class.hidden]=\"railOpen()\"\n [attr.aria-label]=\"expandFacetsLabel()\"\n [attr.aria-expanded]=\"false\"\n (click)=\"railOpen.set(true)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-right\" [size]=\"14\" class=\"flex shrink-0\" />\n <span class=\"fx-md-rail-label txt-eyebrow whitespace-nowrap\">{{ facetsLabel() }}</span>\n </button>\n </aside>\n\n <!-- Master: list pane -->\n <section class=\"fx-md-list flex flex-col min-h-0 min-w-0\">\n <div class=\"fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n </div>\n <div\n class=\"flex flex-col overflow-auto flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <div class=\"px-large py-normal\">\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n </div>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <!-- Hairline rows; the selected one is tinted with an accent bar on\n the left (design 11/14 selected finding / component). -->\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-l-accent bg-accent-soft/60'\n : 'border-l-transparent hover:bg-surface-sunken'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <div class=\"px-large py-large\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"fx-md-paginator\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </section>\n\n <!-- Detail pane -->\n <section class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto px-large py-large\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </section>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-row{background:rgb(var(--og-surface));border:1px solid rgb(var(--border-default));border-radius:.5rem;overflow:hidden;box-shadow:0 1px 2px #0000000a,0 3px 8px #0000000d}.fx-md-facets,.fx-md-list{border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-md-rail-toggle{width:100%;padding:10px 12px;flex-direction:row;justify-content:flex-start}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 720px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets,.fx-md-list{border-bottom:0;border-right:1px solid rgb(var(--og-border-subtle))}.fx-md-facets{flex:0 0 40px;width:40px;min-width:40px;max-width:40px}.fx-md-facets--open{flex:0 1 204px;width:auto;min-width:180px;max-width:204px}.fx-md-rail-toggle{height:100%;padding:12px 0;flex-direction:column;justify-content:flex-start}.fx-md-rail-label{writing-mode:vertical-rl;transform:rotate(180deg)}.fx-md-list{width:auto;flex:1 1 0;min-width:260px;max-width:var(--fx-md-list-width, none)}.fx-md-detail{width:auto;flex:0 1 var(--fx-md-detail-width, 340px);min-width:300px;max-width:var(--fx-md-detail-width, 340px)}.fx-md-row--detail-fluid .fx-md-list{flex:0 1 var(--fx-md-list-width, 420px);max-width:var(--fx-md-list-width, 420px)}.fx-md-row--detail-fluid .fx-md-detail{flex:1 1 360px;min-width:360px;max-width:none}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.fx-md-paginator{padding:0 12px;border-top:1px solid rgb(var(--og-border-subtle))}\n"] }]
4877
4899
  }], propDecorators: { table: [{
4878
4900
  type: Input
4879
- }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], pageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], facetsOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "facetsOpen", required: false }] }], facetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "facetsLabel", required: false }] }], expandFacetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandFacetsLabel", required: false }] }], collapseFacetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseFacetsLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4901
+ }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], detailWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], pageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], facetsOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "facetsOpen", required: false }] }], facetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "facetsLabel", required: false }] }], expandFacetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandFacetsLabel", required: false }] }], collapseFacetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseFacetsLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4880
4902
 
4881
4903
  /**
4882
4904
  * Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook