@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 +5 -4
- package/fesm2022/foxeltech-angular-ui-flow.mjs +42 -11
- package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui.mjs +29 -7
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/styles/components.css +5 -4
- package/src/lib/styles/utilities.css +9 -4
- package/src/lib/ui/components/button/button.component.html +37 -225
- package/src/lib/ui/components/button/button.component.scss +30 -0
- package/src/lib/ui/components/master-detail/master-detail.component.css +36 -8
- package/src/lib/ui/components/master-detail/master-detail.component.html +4 -2
- package/types/foxeltech-angular-ui-flow.d.ts +24 -2
- package/types/foxeltech-angular-ui.d.ts +22 -3
- package/utilities.css +9 -4
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
|
|
365
|
-
|
|
366
|
-
|
|
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/09 — so 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:
|
|
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
|
-
/**
|
|
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.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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":";;;;;AA2DA;AACmE;AAC5D,MAAM,kBAAkB,GAAoC;AACjE,IAAA,KAAK,EAAE,SAAS;AAChB,IAAA,WAAW,EAAE,aAAa;AAC1B,IAAA,UAAU,EAAE,SAAS;AACrB,IAAA,MAAM,EAAE,SAAS;AACjB,IAAA,gBAAgB,EAAE,QAAQ;AAC1B,IAAA,MAAM,EAAE,UAAU;AAClB,IAAA,KAAK,EAAE,aAAa;;AAGtB,MAAM,UAAU,GAAG,GAAG;AACtB,MAAM,OAAO,GAAG,GAAG;AACnB,MAAM,UAAU,GAAG,GAAG;AACtB,MAAM,WAAW,GAAG,EAAE;AAEtB;;;;;;;;;;;AAWG;MAcU,mBAAmB,CAAA;AACrB,IAAA,KAAK,GAAG,KAAK,CAAgB,EAAE,4EAAC;AAChC,IAAA,KAAK,GAAG,KAAK,CAAgB,EAAE,4EAAC;;AAEhC,IAAA,QAAQ,GAAG,KAAK,CAAgB,IAAI,+EAAC;;AAErC,IAAA,MAAM,GAAG,KAAK,CAAC,EAAE,6EAAC;;AAElB,IAAA,SAAS,GAAG,KAAK,CAAC,yBAAyB,gFAAC;IAE5C,SAAS,GAAG,MAAM,EAAU;AAEpB,IAAA,IAAI,GAAG,SAAS,CAAC,cAAc,2EAAC;AAChC,IAAA,IAAI,GAAG,MAAM,CAA0B,UAAU,CAAC;AAClD,IAAA,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AAEhD,IAAA,WAAA,GAAA;;;;QAIE,iBAAiB,CAAC,MAAK;AACrB,YAAA,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE;YACxB,IAAI,CAAC,SAAS,EAAE;YAChB,IAAI,CAAC,cAAc,EAAE;AACrB,YAAA,IAAI,CAAC,IAAI;gBAAE;YACX,SAAS,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;AACrC,QAAA,CAAC,CAAC;AACF,QAAA,IAAI,OAAO,cAAc,KAAK,WAAW,EAAE;AACzC,YAAA,MAAM,EAAE,GAAG,IAAI,cAAc,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACvD,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;AACnC,YAAA,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,UAAU,EAAE,CAAC;QAClD;IACF;IAEQ,SAAS,GAAkB,IAAI;;IAE/B,WAAW,GAAA;AACjB,QAAA,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI;YAAE;AAC7B,QAAA,IAAI,CAAC,SAAS,GAAG,qBAAqB,CAAC,MAAK;AAC1C,YAAA,IAAI,CAAC,SAAS,GAAG,IAAI;AACrB,YAAA,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE;AACxB,YAAA,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE;gBAAE;;;AAG7B,YAAA,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAC7C,QAAA,CAAC,CAAC;IACJ;;AAGiB,IAAA,OAAO,GAAG,QAAQ,CAAyB,MAAK;AAC/D,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,QAAA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB;AAC5C,QAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5C,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAE5D,MAAM,KAAK,GAA2B,EAAE;AACxC,QAAA,MAAM,OAAO,GAAG,CAAC,EAAU,EAAE,IAAiB,KAAY;AACxD,YAAA,IAAI,KAAK,CAAC,EAAE,CAAC,KAAK,SAAS;AAAE,gBAAA,OAAO,KAAK,CAAC,EAAE,CAAC;;AAE7C,YAAA,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AAAE,gBAAA,OAAO,CAAC;AAC1B,YAAA,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE;AACtC,YAAA,MAAM,KAAK,GAAG,OAAO,CAAC;kBAClB,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG;kBACpD,CAAC;AACL,YAAA,KAAK,CAAC,EAAE,CAAC,GAAG,KAAK;AACjB,YAAA,OAAO,KAAK;AACd,QAAA,CAAC;AACD,QAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAI;YAClB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,GAAG,EAAE,CAAC;AACpD,QAAA,CAAC,CAAC;AACF,QAAA,OAAO,KAAK;AACd,IAAA,CAAC,8EAAC;;AAGO,IAAA,SAAS,GAAG,QAAQ,CAAS,MAAK;AACzC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE;QAC9B,MAAM,SAAS,GAA2B,EAAE;;QAE5C,MAAM,MAAM,GAA2B,EAAE;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAI;YACzB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC;AAC5B,YAAA,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;AAClC,QAAA,CAAC,CAAC;AACF,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAErD,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAI;YAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;AACpC,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;AAC9C,YAAA,SAAS,CAAC,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;YAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;AAChC,YAAA,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,GAAG,IAAI,IAAI,OAAO,IAAI,CAAC;YAC/C,OAAO;gBACL,EAAE,EAAE,IAAI,CAAC,EAAE;AACX,gBAAA,IAAI,EAAE,eAAwB;AAC9B,gBAAA,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,MAAM,GAAG,UAAU,EAAE,CAAC,EAAE,EAAE,GAAG,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE;AACtE,gBAAA,KAAK,EAAE,UAAU;AACjB,gBAAA,MAAM,EAAE,WAAW;AACnB,gBAAA,IAAI,EAAE,IAAI;aACX;AACH,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC,gFAAC;AAEF;;;AAGG;AACM,IAAA,SAAS,GAAG,QAAQ,CAAS,MACpC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM;QACvB,EAAE,EAAE,GAAG,CAAC,CAAC,MAAM,CAAA,EAAA,EAAK,CAAC,CAAC,MAAM,CAAA,CAAE;QAC9B,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM;AAChB,QAAA,IAAI,EAAE,UAAmB;AACzB,QAAA,KAAK,EAAE,QAAiB;AACxB,QAAA,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,cAAuB,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE;KAC3E,CAAC,CAAC,gFACJ;;AAGgB,IAAA,UAAU,GAAG,QAAQ,CAAc,MAAK;AACvD,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE;AAC9B,QAAA,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU;AACjC,QAAA,IAAI,CAAC,MAAM;AAAE,YAAA,OAAO,OAAO;QAC3B,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAI;AACzB,YAAA,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;AAAE,gBAAA,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9C,YAAA,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;AAAE,gBAAA,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;AAChD,QAAA,CAAC,CAAC;AACF,QAAA,OAAO,OAAO;AAChB,IAAA,CAAC,iFAAC;AAEO,IAAA,OAAO,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,KAAK,CAAC,8EAAC;AAE5D;;;AAGG;AACM,IAAA,cAAc,GAAG,QAAQ,CAAC,MAAK;AACtC,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE;AAC9B,QAAA,IAAI,QAAQ;AAAE,YAAA,OAAO,QAAQ;QAC7B,MAAM,MAAM,GAA2B,EAAE;AACzC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE;QAC9B,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAI;YACzB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC;AAC5B,YAAA,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;AAClC,QAAA,CAAC,CAAC;AACF,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;;AAElD,QAAA,OAAO,CAAA,EAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI;AAClD,IAAA,CAAC,qFAAC;AAEF,IAAA,QAAQ,CAAC,IAAqB,EAAA;AAC5B,QAAA,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,aAAa;IAClD;AAEA,IAAA,QAAQ,CAAC,IAAiB,EAAA;AACxB,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE;QAC9B,OAAO,MAAM,IAAI,IAAI,IAAI,MAAM,KAAK,IAAI,CAAC,EAAE;IAC7C;;AAGA,IAAA,SAAS,CAAC,IAAiB,EAAA;AACzB,QAAA,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IAC/D;;AAGA,IAAA,QAAQ,CAAC,IAAiB,EAAA;QACxB,OAAO,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IACjF;;AAGA,IAAA,YAAY,CAAC,IAAU,EAAA;AACrB,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE;AAC9B,QAAA,OAAO,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC;IAC7E;AAEA,IAAA,YAAY,CAAC,IAAU,EAAA;AACrB,QAAA,OAAO,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;IAC5D;wGAlLW,mBAAmB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAnB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,mBAAmB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,mBAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,QAAA,EAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,UAAA,EAAA,UAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,iBAAA,EAAA,WAAA,EAAA,UAAA,EAAA,WAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,SAAA,EAAA,WAAA,EAAA,EAAA,WAAA,EAAA,CAAA,EAAA,YAAA,EAAA,MAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAYI,cAAc,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,ECjHlD,ivGAyEA,EAAA,MAAA,EAAA,CAAA,4tIAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDqBI,OAAO,EAAA,QAAA,EAAA,WAAA,EAAA,MAAA,EAAA,CAAA,OAAA,EAAA,SAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EACP,cAAc,EAAA,QAAA,EAAA,OAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,SAAA,EAAA,SAAA,EAAA,YAAA,EAAA,cAAA,EAAA,oBAAA,EAAA,mBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,sBAAA,EAAA,sBAAA,EAAA,OAAA,EAAA,iBAAA,EAAA,OAAA,CAAA,EAAA,OAAA,EAAA,CAAA,sBAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EACd,yBAAyB,EAAA,QAAA,EAAA,uBAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EACzB,qBAAqB,8DACrB,eAAe,EAAA,QAAA,EAAA,QAAA,EAAA,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAAA,UAAA,EAAA,SAAA,EAAA,SAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAGN,mBAAmB,EAAA,UAAA,EAAA,CAAA;kBAb/B,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,mBAAmB,EAAA,eAAA,EAGZ,uBAAuB,CAAC,MAAM,EAAA,OAAA,EACtC;wBACP,OAAO;wBACP,cAAc;wBACd,yBAAyB;wBACzB,qBAAqB;wBACrB,eAAe;AAChB,qBAAA,EAAA,QAAA,EAAA,ivGAAA,EAAA,MAAA,EAAA,CAAA,4tIAAA,CAAA,EAAA;ynBAciC,cAAc,CAAA,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;MEnGrC,cAAc,CAAA;AAChB,IAAA,KAAK,GAAG,KAAK,CAAS,EAAE,4EAAC;IACzB,KAAK,GAAW,EAAE;AAClB,IAAA,eAAe,GAAG,KAAK,CAAM,oBAAoB,sFAAC;IAClD,IAAI,GAAG,KAAK,CAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,MAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAC;AAE7C,IAAA,gBAAgB,CAAC,UAAsB,EAAA;QACrC,IAAI,CAAC,KAAK,GAAG;YACX,GAAG,IAAI,CAAC,KAAK;AACb,YAAA;gBACE,EAAE,EAAE,GAAG,UAAU,CAAC,MAAM,CAAA,IAAA,EAAO,UAAU,CAAC,MAAM,CAAA,CAAE;AAClD,gBAAA,GAAG,UAAU;AACd,aAAA;SACF;IACH;wGAdW,cAAc,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAd,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,cAAc,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,uBAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,KAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,eAAA,EAAA,EAAA,iBAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,IAAA,EAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,ECd3B,iNAUA,EAAA,MAAA,EAAA,CAAA,EAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDAQ,cAAc,qTACd,6BAA6B,EAAA,QAAA,EAAA,oDAAA,EAAA,OAAA,EAAA,CAAA,WAAA,EAAA,SAAA,EAAA,aAAA,EAAA,WAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAGxB,cAAc,EAAA,UAAA,EAAA,CAAA;kBAX1B,SAAS;AACI,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,uBAAuB,EAAA,eAAA,EAGhB,uBAAuB,CAAC,MAAM,EAAA,OAAA,EACtC;wBACL,0BAA0B;wBAC1B,cAAc;wBACd,6BAA6B;AAChC,qBAAA,EAAA,QAAA,EAAA,iNAAA,EAAA;;sBAIF;;;AEhBH;;AAEG;;;;"}
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
4762
|
-
|
|
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
|
|
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
|