@foxeltech/angular-ui 0.7.108 → 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 +24 -5
- package/fesm2022/foxeltech-angular-ui-chart.mjs +25 -1
- package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
- 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 +102 -28
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/package.json +3 -2
- package/src/lib/styles/components.css +24 -5
- package/src/lib/styles/tokens.css +3 -0
- package/src/lib/styles/utilities.css +9 -4
- package/src/lib/ui/components/bar-list/bar-list.component.html +19 -18
- package/src/lib/ui/components/bar-list/bar-list.component.scss +63 -0
- package/src/lib/ui/components/breadcrumb/breadcrumb.component.css +31 -0
- package/src/lib/ui/components/breadcrumb/breadcrumb.component.html +6 -1
- 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/drawer/drawer.component.html +32 -7
- package/src/lib/ui/components/drawer/drawer.component.scss +114 -17
- package/src/lib/ui/components/filter-pills/filter-pills.component.html +4 -1
- package/src/lib/ui/components/filter-pills/filter-pills.component.scss +23 -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/src/lib/ui/components/section-card/section-card.component.html +10 -5
- package/src/lib/ui/components/section-card/section-card.component.scss +28 -0
- package/src/lib/ui/components/stat-cards/stat-cards.component.css +48 -0
- package/src/lib/ui/components/stat-cards/stat-cards.component.html +16 -7
- package/tokens.css +3 -0
- package/types/foxeltech-angular-ui-flow.d.ts +24 -2
- package/types/foxeltech-angular-ui.d.ts +92 -5
- package/utilities.css +9 -4
|
@@ -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":"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"}
|
|
1
|
+
{"version":3,"file":"foxeltech-angular-ui-flow.mjs","sources":["../../../projects/ui/flow/src/flow-canvas/flow-canvas.component.ts","../../../projects/ui/flow/src/flow-canvas/flow-canvas.component.html","../../../projects/ui/flow/src/flow-connection/flow-connection.component.ts","../../../projects/ui/flow/src/flow-connection/flow-connection.component.html","../../../projects/ui/flow/src/foxeltech-angular-ui-flow.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n afterRenderEffect,\n computed,\n inject,\n input,\n output,\n untracked,\n viewChild,\n} from '@angular/core';\nimport { NgClass } from '@angular/common';\nimport {\n Edge,\n EdgeTemplateDirective,\n HandleComponent,\n Node,\n NodeHtmlTemplateDirective,\n VflowComponent,\n} from 'ngx-vflow';\n\n/**\n * What a chain node represents. Structural kinds (`entry`, `convergence`,\n * `impact`, `stage`) can be derived from the graph itself; the validation kinds\n * (`mitigation`, `review`, `not-reproduced`) come from an analyst verdict.\n */\nexport type FxChainNodeKind =\n | 'entry'\n | 'convergence'\n | 'mitigation'\n | 'review'\n | 'not-reproduced'\n | 'impact'\n | 'stage';\n\nexport interface FxChainNode {\n id: string;\n /** Node title — rendered in the figure/mono face. */\n label: string;\n kind: FxChainNodeKind;\n /** Caller-translated kind eyebrow; falls back to the raw kind key. */\n kindLabel?: string;\n /** Caller-translated muted subtitle. */\n caption?: string;\n /** true = verified badge, false = unverified badge, null/undefined = no badge. */\n verified?: boolean | null;\n /** Explicit column (0 = leftmost); omit to derive depth from the edges. */\n column?: number;\n /** Explicit row inside the column; omit to stack in declaration order. */\n row?: number;\n}\n\nexport interface FxChainEdge {\n source: string;\n target: string;\n}\n\n/** Kind → severity token. Exported so callers can tint matching chips (the\n stage list beside the canvas) from the same source of truth. */\nexport const FX_CHAIN_KIND_TONE: Record<FxChainNodeKind, string> = {\n entry: 'warning',\n convergence: 'information',\n mitigation: 'success',\n review: 'warning',\n 'not-reproduced': 'danger',\n impact: 'critical',\n stage: 'information',\n};\n\nconst COLUMN_GAP = 216;\nconst ROW_GAP = 104;\nconst NODE_WIDTH = 176;\nconst NODE_HEIGHT = 66;\n\n/**\n * V2 exploit-chain canvas — the attack-path graph of design refs 04..07:\n * kind-tinted node cards laid out left→right, curved edges, draggable nodes on\n * a pannable/zoomable surface.\n *\n * Presentational: it takes a graph plus the currently selected node and emits\n * clicks. Layout is derived from the edges (longest path from a source) unless\n * a node pins its own column/row, so callers can hand over a plain chain.\n *\n * <fx-ui-flow-canvas [nodes]=\"chainNodes\" [edges]=\"chainEdges\"\n * [activeId]=\"stageId()\" (nodeClick)=\"selectStage($event)\" />\n */\n@Component({\n selector: 'fx-ui-flow-canvas',\n templateUrl: './flow-canvas.component.html',\n styleUrl: './flow-canvas.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgClass,\n VflowComponent,\n NodeHtmlTemplateDirective,\n EdgeTemplateDirective,\n HandleComponent,\n ],\n})\nexport class FlowCanvasComponent {\n readonly nodes = input<FxChainNode[]>([]);\n readonly edges = input<FxChainEdge[]>([]);\n /** Selected/current node: full opacity + accent border; the rest dim. */\n readonly activeId = input<string | null>(null);\n /** Explicit canvas height; omit to size it from the number of stacked rows. */\n readonly height = input('');\n /** Caller-translated hint shown when the graph is empty. */\n readonly emptyText = input('No chain data available');\n /**\n * Work is running against this chain — validation, typically.\n *\n * The edges become a travelling dashed line (\"marching ants\") so the canvas\n * says something is in flight without a spinner sitting on top of the graph.\n * Purely presentational: the caller owns what \"busy\" means and when it ends.\n */\n readonly busy = input(false);\n\n readonly nodeClick = output<string>();\n\n private readonly flow = viewChild(VflowComponent);\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly destroyRef = inject(DestroyRef);\n\n constructor() {\n // `view=\"auto\"` only sizes vflow's viewport to the wrapper — it does not\n // frame the graph, so a taller canvas left the chain pinned top-left and\n // clipped (anh Tú 04/09). Fit whenever the graph or the box changes.\n afterRenderEffect(() => {\n const flow = this.flow();\n this.flowNodes();\n this.resolvedHeight();\n if (!flow) return;\n untracked(() => this.scheduleFit());\n });\n if (typeof ResizeObserver !== 'undefined') {\n const ro = new ResizeObserver(() => this.scheduleFit());\n ro.observe(this.host.nativeElement);\n this.destroyRef.onDestroy(() => ro.disconnect());\n }\n }\n\n private fitHandle: number | null = null;\n /**\n * Coalesces fit requests, then fits TWICE — one frame apart.\n *\n * vflow frames the graph from `getNodesBounds`, which sums each node's\n * `point()` and its `size()`. For an html-template node that size is the\n * MEASURED box and is still 0 on the frame the nodes are created, so the\n * first fit sees a bounds box running origin-to-origin: five 176px cards\n * measured 864px instead of 1040px, and the graph was framed 88px — half a\n * card — too far right, clipping the last one (MEASURED 16/09).\n *\n * The second pass runs once the cards have been laid out and measured, and\n * is what actually frames them. The first is kept so a slow frame still\n * shows something sensible rather than the graph pinned top-left.\n */\n private scheduleFit(): void {\n if (this.fitHandle !== null) return;\n this.fitHandle = requestAnimationFrame(() => {\n this.fit();\n this.fitHandle = requestAnimationFrame(() => {\n this.fitHandle = null;\n this.fit();\n });\n });\n }\n\n private fit(): void {\n const flow = this.flow();\n if (!flow || this.isEmpty()) return;\n // Design 04: cards fill the width with a little air (0.18 left them at\n // ~0.74×, visibly smaller than the mock — measured 04/09).\n flow.fitView({ padding: 0.1, duration: 0 });\n // Never MAGNIFY: the cap is `[maxZoom]` on the vflow element, which\n // fitView respects. Clamping afterwards does NOT work — both `zoomTo`\n // and `viewportTo` return without changing `viewport()` here (MEASURED\n // 16/09).\n }\n\n /** Column index per node: pinned value, else longest path from a source. */\n private readonly columns = computed<Record<string, number>>(() => {\n const nodes = this.nodes();\n const edges = this.edges();\n const incoming = new Map<string, string[]>();\n nodes.forEach((n) => incoming.set(n.id, []));\n edges.forEach((e) => incoming.get(e.target)?.push(e.source));\n\n const depth: Record<string, number> = {};\n const resolve = (id: string, seen: Set<string>): number => {\n if (depth[id] !== undefined) return depth[id];\n // Cycle guard: a malformed graph must not blow the stack.\n if (seen.has(id)) return 0;\n seen.add(id);\n const parents = incoming.get(id) ?? [];\n const value = parents.length\n ? Math.max(...parents.map((p) => resolve(p, seen))) + 1\n : 0;\n depth[id] = value;\n return value;\n };\n nodes.forEach((n) => {\n depth[n.id] = n.column ?? resolve(n.id, new Set());\n });\n return depth;\n });\n\n /** ngx-vflow nodes: html-template cards positioned by the derived layout. */\n readonly flowNodes = computed<Node[]>(() => {\n const columns = this.columns();\n const perColumn: Record<number, number> = {};\n // Tallest column decides the vertical centre every column is balanced on.\n const counts: Record<number, number> = {};\n this.nodes().forEach((n) => {\n const c = columns[n.id] ?? 0;\n counts[c] = (counts[c] ?? 0) + 1;\n });\n const tallest = Math.max(1, ...Object.values(counts));\n\n return this.nodes().map((node) => {\n const column = columns[node.id] ?? 0;\n const row = node.row ?? perColumn[column] ?? 0;\n perColumn[column] = row + 1;\n const rows = counts[column] ?? 1;\n const offset = ((tallest - rows) * ROW_GAP) / 2;\n return {\n id: node.id,\n type: 'html-template' as const,\n point: { x: 24 + column * COLUMN_GAP, y: 24 + offset + row * ROW_GAP },\n width: NODE_WIDTH,\n height: NODE_HEIGHT,\n data: node,\n };\n });\n });\n\n /**\n * Edges stay identity-stable across selections: state lives in the template\n * (read off `activeId`), so selecting a stage never re-creates them.\n */\n readonly flowEdges = computed<Edge[]>(() =>\n this.edges().map((e) => ({\n id: `${e.source}->${e.target}`,\n source: e.source,\n target: e.target,\n type: 'template' as const,\n curve: 'bezier' as const,\n markers: { end: { type: 'arrow-closed' as const, width: 11, height: 11 } },\n })),\n );\n\n /** Nodes one hop from the selection — they stay lit alongside it. */\n private readonly relatedIds = computed<Set<string>>(() => {\n const active = this.activeId();\n const related = new Set<string>();\n if (!active) return related;\n this.edges().forEach((e) => {\n if (e.source === active) related.add(e.target);\n if (e.target === active) related.add(e.source);\n });\n return related;\n });\n\n readonly isEmpty = computed(() => this.nodes().length === 0);\n\n /**\n * A linear chain needs one row, the design's converging entries need three —\n * sizing from the layout keeps the card from leaving a tall empty band.\n */\n readonly resolvedHeight = computed(() => {\n const explicit = this.height();\n if (explicit) return explicit;\n const counts: Record<number, number> = {};\n const columns = this.columns();\n this.nodes().forEach((n) => {\n const c = columns[n.id] ?? 0;\n counts[c] = (counts[c] ?? 0) + 1;\n });\n const rows = Math.max(1, ...Object.values(counts));\n // Floor at 300px: a one-row chain in a 152px band read too small (04/09).\n return `${Math.max(300, 48 + rows * ROW_GAP)}px`;\n });\n\n kindTone(kind: FxChainNodeKind): string {\n return FX_CHAIN_KIND_TONE[kind] ?? 'information';\n }\n\n isActive(node: FxChainNode): boolean {\n const active = this.activeId();\n return active != null && active === node.id;\n }\n\n /** One hop from the selection: lit, but without the selected accent frame. */\n isRelated(node: FxChainNode): boolean {\n return !this.isActive(node) && this.relatedIds().has(node.id);\n }\n\n /** Nothing selected → every node reads at full strength. */\n isDimmed(node: FxChainNode): boolean {\n return this.activeId() != null && !this.isActive(node) && !this.isRelated(node);\n }\n\n /** An edge touching the selection is part of the step being read. */\n isEdgeActive(edge: Edge): boolean {\n const active = this.activeId();\n return active != null && (edge.source === active || edge.target === active);\n }\n\n isEdgeDimmed(edge: Edge): boolean {\n return this.activeId() != null && !this.isEdgeActive(edge);\n }\n}\n","<ng-template #blankHandle></ng-template>\n\n@if (isEmpty()) {\n <div\n class=\"flex items-center justify-center rounded-card border border-border bg-surface-sunken txt-body-01 text-text-placeholder\"\n [style.height]=\"resolvedHeight()\"\n >\n {{ emptyText() }}\n </div>\n} @else {\n <!-- view=\"auto\" sizes the flow from this wrapper, so the height input is the\n single source of truth for the canvas box. -->\n <div class=\"fx-chain-canvas rounded-card border border-border overflow-hidden\" [style.height]=\"resolvedHeight()\">\n <!-- maxZoom is 1, not 1.35: fitView scales a short chain UP to fill the\n band, and at 1.35 five 176px cards came out at 1.18× — oversized\n against the design and wide enough to clip the last card (anh Tú\n 16/09, MEASURED). The cards have a designed size, so fitting should\n only ever shrink. -->\n <vflow\n view=\"auto\"\n [nodes]=\"flowNodes()\"\n [edges]=\"flowEdges()\"\n [minZoom]=\"0.4\"\n [maxZoom]=\"1\"\n [entitiesSelectable]=\"false\"\n background=\"rgba(var(--og-surface-sunken) / 1)\"\n >\n <ng-template let-ctx nodeHtml>\n <!-- Handles are what edges anchor to: without them vflow renders the\n cards but draws no connections. The graph is read-only here, so an\n empty template replaces vflow's default handle dot (it only draws\n one when the template is null) while keeping the anchor point. -->\n <handle type=\"target\" position=\"left\" [template]=\"blankHandle\" />\n <handle type=\"source\" position=\"right\" [template]=\"blankHandle\" />\n <button\n type=\"button\"\n class=\"fx-chain-node\"\n [ngClass]=\"{\n 'fx-chain-node--active': isActive(ctx.node.data),\n 'fx-chain-node--related': isRelated(ctx.node.data),\n 'fx-chain-node--dim': isDimmed(ctx.node.data),\n }\"\n (click)=\"nodeClick.emit(ctx.node.data.id)\"\n >\n <span class=\"fx-chain-node__eyebrow\">\n <span class=\"fx-chain-node__dot\" [ngClass]=\"'bg-' + kindTone(ctx.node.data.kind)\"></span>\n <span [ngClass]=\"'text-' + kindTone(ctx.node.data.kind)\">{{ ctx.node.data.kindLabel || ctx.node.data.kind }}</span>\n @if (ctx.node.data.verified !== null && ctx.node.data.verified !== undefined) {\n <span\n class=\"fx-chain-node__check\"\n [ngClass]=\"ctx.node.data.verified ? 'bg-success/20 text-success' : 'bg-bg-hover text-text-placeholder'\"\n >\n {{ ctx.node.data.verified ? '✓' : '○' }}\n </span>\n }\n </span>\n <span class=\"fx-chain-node__label\">{{ ctx.node.data.label }}</span>\n @if (ctx.node.data.caption) {\n <span class=\"fx-chain-node__caption\">{{ ctx.node.data.caption }}</span>\n }\n </button>\n </ng-template>\n\n <!-- Edge state is read from activeId here rather than baked into the edge\n objects, so selecting a stage restyles without re-creating them.\n The arrowhead only shows on the selected step (design 04). -->\n <ng-template let-ctx edge>\n <svg:path\n class=\"fx-chain-edge\"\n [class.fx-chain-edge--busy]=\"busy()\"\n [class.fx-chain-edge--active]=\"isEdgeActive(ctx.edge)\"\n [class.fx-chain-edge--dim]=\"isEdgeDimmed(ctx.edge)\"\n [attr.d]=\"ctx.path()\"\n [attr.marker-end]=\"isEdgeActive(ctx.edge) ? ctx.markerEnd() : null\"\n />\n </ng-template>\n </vflow>\n </div>\n}\n","import { Component, Input, ChangeDetectionStrategy, input } from '@angular/core';\nimport { Node, Edge, Vflow, Connection, ChangesControllerDirective, VflowComponent, ConnectionControllerDirective } from 'ngx-vflow';\n\n@Component({\n selector: 'fx-ui-flow-connection',\n templateUrl: './flow-connection.component.html',\n styleUrl: './flow-connection.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n ChangesControllerDirective,\n VflowComponent,\n ConnectionControllerDirective,\n ],\n})\nexport class FlowConnection {\n readonly nodes = input<Node[]>([]);\n @Input() edges: Edge[] = [];\n readonly backgroundColor = input<any>('--bg-hover-primary');\n readonly view = input<any>(['100%', '400px']);\n\n handleConnection(connection: Connection) {\n this.edges = [\n ...this.edges,\n {\n id: `${connection.source} -> ${connection.target}`,\n ...connection,\n },\n ];\n }\n}\n","<vflow\n [background]=\"{\n type: 'solid',\n color: `rgba(var(${backgroundColor()})`\n}\"\n [nodes]=\"nodes()\"\n [edges]=\"edges\"\n (onConnect)=\"handleConnection($event)\"\n [view]=\"view()\"\n/>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|