@ngxviewbuilder/plugin-logic-map 0.0.0-stage → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# NGX View Builder Designer Commercial License Agreement
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 NGX View Builder (https://ngxviewbuilder.io). All rights reserved.
|
|
4
|
+
|
|
5
|
+
This is a legal agreement between you (the **"Licensee"**) and NGX View Builder (the **"Licensor"**) covering the `@ngxviewbuilder/designer` package and the official `@ngxviewbuilder/plugin-*` packages (together the **"Software"**). By installing or using the Software you agree to these terms.
|
|
6
|
+
|
|
7
|
+
**This is commercial software. It is not open source.** The Software's source form being readable in the npm distribution does not grant any open-source rights.
|
|
8
|
+
|
|
9
|
+
The `@ngxviewbuilder/runtime` package is **not** covered by this agreement. It is free to use in production, requires no License Key, and has its own terms in that package's `LICENSE.md`. What you pay for here is the visual editor your team designs views in.
|
|
10
|
+
|
|
11
|
+
## 1. Definitions
|
|
12
|
+
|
|
13
|
+
- **"License Key"** a cryptographically signed key issued by Licensor that identifies the customer, plan, and expiry date (the **"License Term"**).
|
|
14
|
+
- **"Covered Version"** any version of the Software whose release date is earlier than the expiry date of Licensee's License Key.
|
|
15
|
+
- **"Application"** a software product that incorporates the Software, developed by Licensee for itself **or for a client of Licensee** (contract / agency work).
|
|
16
|
+
|
|
17
|
+
## 2. License grant
|
|
18
|
+
|
|
19
|
+
Subject to the purchase of a valid License Key and compliance with this agreement, Licensor grants Licensee a non-exclusive, non-transferable, worldwide license to:
|
|
20
|
+
|
|
21
|
+
1. use the Software to develop Applications, within the limits of the purchased plan;
|
|
22
|
+
2. deploy Covered Versions in any number of Applications and environments, unless the purchased plan states otherwise;
|
|
23
|
+
3. develop Applications **for third parties (Licensee's clients)** and deliver or transfer such Applications to them the client may operate, host, and use the delivered Application without a separate License Key;
|
|
24
|
+
4. keep using Covered Versions **perpetually** this right survives the end of the License Term.
|
|
25
|
+
|
|
26
|
+
End users of Licensee's Applications do **not** need their own License Key: an Application ships `@ngxviewbuilder/runtime`, which is free and shows no license UI.
|
|
27
|
+
|
|
28
|
+
For clarity, development with the Software must be covered by a License Key held by **either side of a contract-work relationship**:
|
|
29
|
+
|
|
30
|
+
- If Licensee is a development company or agency, its License Key covers the development it performs for its clients (Section 2.3).
|
|
31
|
+
- If Licensee is the end customer of an Application, Licensee's License Key also covers **contractors and outsourced developers** working on that Application on Licensee's behalf. Such contractor use is limited to Licensee's Applications and does not extend to the contractor's own or other clients' projects.
|
|
32
|
+
|
|
33
|
+
Any party that develops with the Software outside these two cases needs its own License Key.
|
|
34
|
+
|
|
35
|
+
## 3. Evaluation use
|
|
36
|
+
|
|
37
|
+
The Software may be used **without** a License Key for evaluation and development trial purposes only. In that mode the builder displays a watermark. Any production use requires a valid License Key.
|
|
38
|
+
|
|
39
|
+
## 4. Updates
|
|
40
|
+
|
|
41
|
+
Versions of the Software released **after** the end of the License Term are not Covered Versions. Using them requires renewing the License Key; without renewal they run in degraded (watermarked) mode.
|
|
42
|
+
|
|
43
|
+
## 5. Restrictions
|
|
44
|
+
|
|
45
|
+
Licensee may not:
|
|
46
|
+
|
|
47
|
+
1. redistribute, resell, rent, lease, or sublicense the Software on its own (i.e. other than embedded in an Application);
|
|
48
|
+
2. offer the Software, or a product derived from it, as a form-builder, view-builder, or low-code SDK/service that competes with the Software;
|
|
49
|
+
3. remove, disable, alter, or circumvent the license-enforcement mechanisms (License Keys, watermark, signature or server validation), nor assist or permit anyone else to do so;
|
|
50
|
+
4. exceed the seat, plan, or usage limits of the purchased plan;
|
|
51
|
+
5. remove or alter copyright or license notices.
|
|
52
|
+
|
|
53
|
+
## 6. Third-party software
|
|
54
|
+
|
|
55
|
+
The Software depends on third-party open-source packages installed separately from npm, each under its own license (MIT: `codemirror`, `@codemirror/*`, `jexl`, `prettier`, `lodash-es`; 0BSD: `tslib`). Those licenses govern those packages only; this agreement governs the Software.
|
|
56
|
+
|
|
57
|
+
## 7. Ownership
|
|
58
|
+
|
|
59
|
+
The Software is licensed, not sold. Licensor retains all right, title, and interest in and to the Software.
|
|
60
|
+
|
|
61
|
+
## 8. Warranty disclaimer
|
|
62
|
+
|
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
|
64
|
+
|
|
65
|
+
## 9. Limitation of liability
|
|
66
|
+
|
|
67
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, LICENSOR SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES, NOR FOR LOSS OF PROFITS, DATA, OR BUSINESS, ARISING FROM THE USE OF THE SOFTWARE. LICENSOR'S AGGREGATE LIABILITY UNDER THIS AGREEMENT SHALL NOT EXCEED THE LICENSE FEES PAID BY LICENSEE IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
|
|
68
|
+
|
|
69
|
+
## 10. Termination
|
|
70
|
+
|
|
71
|
+
This agreement terminates automatically if Licensee materially breaches it. Upon termination, Licensee must stop using the Software; rights to Covered Versions granted under Section 2.3 do not survive termination for breach. Sections 6–11 survive termination.
|
|
72
|
+
|
|
73
|
+
## 11. Governing law
|
|
74
|
+
|
|
75
|
+
This agreement is governed by the laws of the Republic of Lithuania. Any disputes shall be resolved by the competent courts of the Republic of Lithuania.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
Questions about licensing: info@heydelabs.com · Pricing and License Keys: https://ngxviewbuilder.io/pricing
|
package/README.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ngxviewbuilder/plugin-logic-map
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adds a **Logic map** tab to the [NGX View Builder](https://ngxviewbuilder.io) designer. Every condition, calculated value, validator, event action, data source and variable of the open view sits on one canvas, so an analyst can see how a large form works without opening field after field.
|
|
4
|
+
|
|
5
|
+
- Fields, data sources, variables and view rules are cards; links between them show what drives what, left to right.
|
|
6
|
+
- Each link type has its own colour and line style: shows or hides, makes required, disables or locks, calculates or sets a value, clears a value, validates, loads data, runs an action. The legend doubles as a filter.
|
|
7
|
+
- Click a field to trace it: everything it depends on and everything it changes stays highlighted, the rest steps back. The side panel reads every rule back in plain words ("Calculated as 4.5 when Annual revenue > 1000000, otherwise 6.9"), with the expression underneath.
|
|
8
|
+
- References to names that are not in the view, data sources that do not exist and calculations that feed each other in a loop are listed under **Needs attention**.
|
|
9
|
+
- Search, a page filter, a minimap, keyboard zoom (`+`, `-`, `0`, `F`) and an SVG download for wiki pages.
|
|
10
|
+
- AI clients connected over MCP read the same map (`nvb_read_plugin` with `logicMap`) and can open it on a field for the person (`show`).
|
|
11
|
+
|
|
12
|
+
Needs an active NGX View Builder license key in the builder settings (`licenseKey`). Without one the tab explains how to add it; nothing else in the designer changes.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i @ngxviewbuilder/plugin-logic-map
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { provideNgxViewBuilderLogicMap } from '@ngxviewbuilder/plugin-logic-map';
|
|
22
|
+
|
|
23
|
+
bootstrapApplication(AppComponent, {
|
|
24
|
+
providers: [
|
|
25
|
+
provideNgxViewBuilderRuntime(),
|
|
26
|
+
provideNgxViewBuilderLogicMap(),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The analysis behind the map is also exported from `@ngxviewbuilder/designer` as `analyzeViewLogic(structure)` and `traceViewLogic(graph, nodeId)`, for hosts that want the same picture in their own tooling.
|
|
32
|
+
|
|
33
|
+
## Translating
|
|
34
|
+
|
|
35
|
+
Every word the Logic map shows goes through the builder's UI dictionaries, so it follows the
|
|
36
|
+
language the builder is in. That includes the sentences written about each rule ("Visible
|
|
37
|
+
when Country is not US"), the issue messages and the exported SVG legend.
|
|
38
|
+
|
|
39
|
+
The English texts are exported as two dictionaries you can hand to a translator:
|
|
40
|
+
|
|
41
|
+
- `NGX_VIEW_BUILDER_LOGIC_MAP_UI_EN` from `@ngxviewbuilder/plugin-logic-map`: the tab itself.
|
|
42
|
+
- `NGX_VIEW_BUILDER_SHARED_UI_EN` from `@ngxviewbuilder/plugin-shared`: the rule sentences
|
|
43
|
+
and the license notice, shared with the Documentation plugin.
|
|
44
|
+
|
|
45
|
+
Give the same keys in your language next to the builder's own translations:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
provideNgxViewBuilderUiTranslations({
|
|
49
|
+
lt: {
|
|
50
|
+
'logicMap.title': 'Logikos žemėlapis',
|
|
51
|
+
'logic.visibleIf.when': 'Matomas, kai',
|
|
52
|
+
'logic.op.isNot': 'nėra',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A key you leave out stays in English. Words in braces, such as `{count}`, are filled in by
|
|
58
|
+
the plugin; keep them in the translation, in whatever place your language needs them.
|
|
59
|
+
Element type names ("Number input") come from the builder's own `element.<type>` keys.
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
Angular 22 or newer and `@ngxviewbuilder/designer` at a matching version.
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
67
|
+
https://ngxviewbuilder.io
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
Terms in [LICENSE.md](./LICENSE.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{NGX_VIEW_BUILDER_SHARED_UI_EN as ce,NVB_PLUGIN_ICONS as F,NvbPluginI18n as H,NvbProFeatureGate as W,NvbTranslatePipe as j,VIEW_LOGIC_KINDS as de,analyzeViewLogic as pe,elementNodeId as me,registerPluginEnglish as ue,traceViewLogic as G}from"@ngxviewbuilder/plugin-shared";import*as _ from"@angular/core";import{ChangeDetectionStrategy as he,Component as ge,Injectable as ve,computed as f,effect as U,inject as $,makeEnvironmentProviders as be,provideEnvironmentInitializer as fe,signal as T,untracked as X,viewChild as xe}from"@angular/core";import{BuilderIconButton as Y,BuilderSearchInput as q,CreatorStructureService as _e,ElementFocusService as ke,EventService as we,HeaderTabEnum as ye,LicenseService as Me,NgxViewBuilderAiPluginRegistryService as ze,NgxViewBuilderApiService as Te,NvbIcon as Z,NvbSelect as J,provideNgxViewBuilderExtensions as Ie}from"@ngxviewbuilder/designer";const E="logicMap";var O=class R{structureService=$(_e);api=$(Te);eventService=$(we);elementFocus=$(ke);i18n=$(H);revision=T(0,...ngDevMode?[{debugName:"revision"}]:[]);refreshScheduled=false;selectedId=T(null,...ngDevMode?[{debugName:"selectedId"}]:[]);graph=f(()=>{this.revision();return pe(this.structureService.getStructure()??{},{externalNames:this.externalNames(),translate:this.i18n.t})},...ngDevMode?[{debugName:"graph"}]:[]);constructor(){this.api.onStructureChanged.add(()=>this.scheduleRefresh());this.eventService.afterPropertyChanged.add(()=>this.scheduleRefresh())}refresh(){this.revision.update(e=>e+1)}show(e){this.refresh();this.selectedId.set(e);this.api.switchToTab(E)}openInBuilder(e){this.api.switchToTab(ye.EDITOR);setTimeout(()=>{const n=this.api.getElementModel(e)??this.api.getElementModel(e.split(".").pop());if(n)this.elementFocus.setFocus(n)},60)}scheduleRefresh(){if(this.refreshScheduled)return;this.refreshScheduled=true;setTimeout(()=>{this.refreshScheduled=false;this.refresh()},150)}externalNames(){try{return[...this.api.getRuntimeVariableDefinitions().map(e=>String(e?.name??"")),...Object.keys(this.api.getRuntimeVariableContext()??{})].filter(Boolean)}catch{return[]}}static ɵfac=_.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"22.2.1",ngImport:_,type:R,deps:[],target:_.ɵɵFactoryTarget.Injectable});static ɵprov=_.ɵɵngDeclareInjectable({minVersion:"12.0.0",version:"22.2.1",ngImport:_,type:R,providedIn:"root"})};_.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"22.2.1",ngImport:_,type:O,decorators:[{type:ve,args:[{providedIn:"root"}]}],ctorParameters:()=>[]});const Q=104;const Se=5;const $e=72;const Ne=40;const Le=["value","visibility","required","locked","reset","validation","data","action"];function Oe(l,e,n){const t=new Set(l.map(m=>m.id));const o=e.filter(m=>t.has(m.from)&&t.has(m.to));const i=new Set;o.forEach(m=>{i.add(m.from);i.add(m.to)});const a=l.filter(m=>i.has(m.id));const r=n?l.filter(m=>!i.has(m.id)):[];const s=new Map(l.map(m=>[m.id,m]));const u=Ee(a,o);const p=new Map;let d=0;let c=0;De(a,o).forEach(m=>{const y=new Set(m.map(h=>h.id));const k=new Map;const z=new Map;m.forEach(h=>{k.set(h.id,[]);z.set(h.id,[])});o.forEach(h=>{if(u.has(h.id)||!y.has(h.from))return;k.get(h.from)?.push(h.to);z.get(h.to)?.push(h.from)});const b=Ce(m,k,z);const I=[];m.forEach(h=>{const S=b.get(h.id)??0;(I[S]??=[]).push(h.id)});for(let h=0;h<I.length;h++)I[h]??=[];I.forEach(h=>h.sort((S,N)=>(s.get(S)?.order??0)-(s.get(N)?.order??0)));Be(I,k,z);const v=Pe(I,k,z);let M=0;I.forEach((h,S)=>h.forEach(N=>{const L=d+(v.get(N)??0);M=Math.max(M,L+64);p.set(N,{node:s.get(N),x:S*328,y:L,layer:S,isolated:false})}));c=Math.max(c,I.length);d=M+Ne});let g=Math.max(0,c*328-Q);let x=Math.max(0,...Array.from(p.values()).map(m=>m.y+64));let w=null;if(r.length>0){w=p.size>0?x+$e:28;const m=Math.max(1,Math.min(Se,Math.max(c,3)));r.forEach((y,k)=>{const z=k%m;const b=Math.floor(k/m);p.set(y.id,{node:y,x:z*276,y:w+b*82,layer:-1,isolated:true})});g=Math.max(g,m*276-Q/2);x=Math.max(...Array.from(p.values()).map(y=>y.y+64))}return{nodes:Array.from(p.values()),links:Ke(o,p,u),width:g,height:x,isolatedTop:w}}function De(l,e){const n=new Map(l.map(i=>[i.id,i.id]));const t=i=>{let a=i;while(n.get(a)!==a)a=n.get(a);n.set(i,a);return a};e.forEach(i=>{if(n.has(i.from)&&n.has(i.to))n.set(t(i.from),t(i.to))});const o=new Map;l.forEach(i=>{const a=t(i.id);o.set(a,[...o.get(a)??[],i])});return Array.from(o.values()).map(i=>i.sort((a,r)=>a.order-r.order)).sort((i,a)=>i[0].order-a[0].order)}function Ee(l,e){const n=new Map;e.forEach(a=>n.set(a.from,[...n.get(a.from)??[],a]));const t=new Map;const o=new Set;const i=a=>{t.set(a,1);(n.get(a)??[]).forEach(r=>{const s=t.get(r.to);if(s===1)o.add(r.id);else if(s===void 0)i(r.to)});t.set(a,2)};l.forEach(a=>{if(!t.has(a.id))i(a.id)});return o}function Ce(l,e,n){const t=new Map(l.map(a=>[a.id,n.get(a.id)?.length??0]));const o=new Map;const i=l.filter(a=>t.get(a.id)===0).map(a=>a.id);i.forEach(a=>o.set(a,0));while(i.length>0){const a=i.shift();(e.get(a)??[]).forEach(r=>{o.set(r,Math.max(o.get(r)??0,(o.get(a)??0)+1));t.set(r,(t.get(r)??1)-1);if(t.get(r)===0)i.push(r)})}l.forEach(a=>{const r=e.get(a.id)??[];if((n.get(a.id)?.length??0)===0&&r.length>0){const s=Math.min(...r.map(u=>o.get(u)??1));o.set(a.id,Math.max(0,s-1))}});return o}function Be(l,e,n){const t=new Map;const o=()=>l.forEach(a=>a.forEach((r,s)=>t.set(r,s)));o();const i=(a,r)=>{const s=new Map;a.forEach((u,p)=>{const d=(r.get(u)??[]).map(c=>t.get(c)).filter(c=>c!==void 0);s.set(u,d.length>0?d.reduce((c,g)=>c+g,0)/d.length:p)});a.sort((u,p)=>s.get(u)-s.get(p));a.forEach((u,p)=>t.set(u,p))};for(let a=0;a<4;a++){for(let r=1;r<l.length;r++)i(l[r],n);for(let r=l.length-2;r>=0;r--)i(l[r],e)}}function Pe(l,e,n){const t=82;const o=new Map;l.forEach(r=>r.forEach((s,u)=>o.set(s,u*t)));const i=(r,s)=>{const u=r.map(d=>{const c=(s.get(d)??[]).map(g=>o.get(g)).filter(g=>g!==void 0);return c.length>0?c.reduce((g,x)=>g+x,0)/c.length:o.get(d)});const p=[];u.forEach((d,c)=>{p[c]=c===0?Math.max(0,d):Math.max(d,p[c-1]+t)});for(let d=p.length-2;d>=0;d--)p[d]=Math.min(p[d],p[d+1]-t);p.forEach((d,c)=>o.set(r[c],Math.max(0,d)));for(let d=1;d<r.length;d++){const c=o.get(r[d-1]);if(o.get(r[d])<c+t)o.set(r[d],c+t)}};for(let r=0;r<3;r++){for(let s=1;s<l.length;s++)i(l[s],n);for(let s=l.length-2;s>=0;s--)i(l[s],e)}const a=Math.min(...Array.from(o.values()),0);o.forEach((r,s)=>o.set(s,r-a));return o}function Ke(l,e,n){const t=new Map;l.forEach(r=>{const s=`${r.from}>${r.to}`;const u=t.get(s)??{from:r.from,to:r.to,kinds:new Set,ruleIds:new Set,backward:false};u.kinds.add(r.kind);u.ruleIds.add(r.ruleId);u.backward||=n.has(r.id);t.set(s,u)});const o=new Map;const i=new Map;t.forEach((r,s)=>{o.set(r.from,[...o.get(r.from)??[],s]);i.set(r.to,[...i.get(r.to)??[],s])});const a=(r,s,u,p)=>{const d=e.get(r);const c=[...u.get(r)??[]].sort((w,m)=>(e.get(p(w))?.y??0)-(e.get(p(m))?.y??0));const g=c.indexOf(s);const x=c.length>1?(g/(c.length-1)-.5)*35.84:0;return d.y+32+x};return Array.from(t.entries()).map(([r,s])=>{const u=e.get(s.from);const p=e.get(s.to);const d=u.x+224;const c=a(s.from,r,o,b=>t.get(b)?.to??"");const g=p.x;const x=a(s.to,r,i,b=>t.get(b)?.from??"");const w=s.backward||g<=d;let m;let y;let k;if(w){const b=Math.max(c,x)+64;m=`M ${d} ${c} C ${d+60} ${c}, ${d+60} ${b}, ${d} ${b} L ${g} ${b} C ${g-60} ${b}, ${g-60} ${x}, ${g} ${x}`;y=(d+g)/2;k=b}else{const b=Math.max(36,(g-d)/2);m=`M ${d} ${c} C ${d+b} ${c}, ${g-b} ${x}, ${g} ${x}`;y=(d+g)/2;k=(c+x)/2}const z=Le.filter(b=>s.kinds.has(b));return{id:r,from:s.from,to:s.to,kinds:z,ruleIds:Array.from(s.ruleIds),path:m,backward:w,midX:y,midY:k}})}const Ae={light:{visibility:"#2a78d6",value:"#eb6834",reset:"#eb6834",data:"#1baf7a",required:"#eda100",validation:"#e87ba4",locked:"#008300",action:"#4a3aa7"},dark:{visibility:"#3987e5",value:"#d95926",reset:"#d95926",data:"#199e70",required:"#c98500",validation:"#d55181",locked:"#008300",action:"#9085e9"}};const C={visibility:"",required:"7 4",locked:"9 3 2 3",value:"",reset:"4 3",validation:"2 3",data:"",action:"12 4"};function Re(l,e,n,t){const o=Ae[n?"dark":"light"];const i=n?"#1a1a19":"#fcfcfb";const a=n?"#252524":"#ffffff";const r=n?"#3d3d3b":"#d4d4d1";const s=n?"#ffffff":"#0b0b0b";const u=n?"#c3c2b7":"#52514e";const p='Inter, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';const d=32;const c=Array.from(new Set(l.links.flatMap(v=>v.kinds)));const g=c.length>0?40:0;const x=l.width+64;const w=l.height+64+g;const m=Ve();const y=c.map(v=>`<marker id="a-${v}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="${o[v]}"/></marker>`).join("");const k=l.links.map(v=>{const M=v.kinds[0];const h=C[M]||(v.backward?"6 4":"");return`<path d="${v.path}" fill="none" stroke="${o[M]}" stroke-width="2" ${h?`stroke-dasharray="${h}"`:""} marker-end="url(#a-${M})"/>`}).join("");const z=l.nodes.map(({node:v,x:M,y:h})=>{const S=v.kind==="missing";const N=e.get(v.id)?.length??0;const L=m(v.label,224-(N?52:24),`600 13px ${p}`);const se=m(`${v.typeLabel??""}${v.kind==="element"?` · ${v.name}`:""}`,200,`400 11px ${p}`);return[`<g transform="translate(${M} ${h})">`,`<rect width="224" height="64" rx="8" fill="${a}" stroke="${S?"#d03b3b":r}" ${S?'stroke-dasharray="5 3"':""}/>`,v.context?`<text x="12" y="17" font-family='${p}' font-size="10.5" font-weight="600" fill="${n?"#6ea3ec":"#2c5aa0"}">${D(m(v.context,200,`600 10.5px ${p}`))}</text>`:"",`<text x="12" y="${v.context?34:27}" font-family='${p}' font-size="13" font-weight="600" fill="${s}">${D(L)}</text>`,`<text x="12" y="${v.context?51:45}" font-family='${p}' font-size="11" fill="${S?"#d03b3b":u}">${D(se)}</text>`,N?`<g transform="translate(188 23)"><rect width="26" height="18" rx="9" fill="${n?"#333331":"#f0f0ee"}"/><text x="13" y="13" text-anchor="middle" font-family='${p}' font-size="11" font-weight="600" fill="${u}">${N}</text></g>`:"","</g>"].join("")}).join("");let b=d;const I=c.map(v=>{const M=t(v);const h=`<g transform="translate(${b} ${w-24})"><line x1="0" y1="0" x2="22" y2="0" stroke="${o[v]}" stroke-width="2" ${C[v]?`stroke-dasharray="${C[v]}"`:""}/><text x="28" y="4" font-family='${p}' font-size="11" fill="${u}">${D(M)}</text></g>`;b+=40+m.measure(M,`400 11px ${p}`);return h}).join("");return[`<svg xmlns="http://www.w3.org/2000/svg" width="${x}" height="${w}" viewBox="0 0 ${x} ${w}">`,`<defs>${y}</defs>`,`<rect width="100%" height="100%" fill="${i}"/>`,`<g transform="translate(${d} ${d})">${k}${z}</g>`,I,"</svg>"].join("")}function Ve(){const l=document.createElement("canvas").getContext("2d");const e=(t,o)=>{if(!l)return t.length*7;l.font=o;return l.measureText(t).width};const n=(t,o,i)=>{if(e(t,i)<=o)return t;let a=0;let r=t.length;while(a<r){const s=Math.ceil((a+r)/2);if(e(`${t.slice(0,s)}…`,i)<=o)a=s;else r=s-1}return`${t.slice(0,a).trimEnd()}…`};return Object.assign(n,{measure:e})}function D(l){return l.replace(/[<>&"']/g,e=>`&#${e.charCodeAt(0)};`)}const Fe={visibility:"",required:"7 4",locked:"9 3 2 3",value:"",reset:"4 3",validation:"2 3",data:"",action:"12 4"};const ee=.2;const B=2;const P=48;const He=184;const We=120;const ne="nvb-logic-map.filters.v1";var te=class V{state=$(O);icons=F;canvasRef=xe("canvas",...ngDevMode?[{debugName:"canvasRef"}]:[]);kinds=de;kindDash=Fe;i18n=$(H);nodeWidth=224;nodeHeight=64;minimapWidth=He;minimapHeight=We;search=T("",...ngDevMode?[{debugName:"search"}]:[]);hiddenKinds=T(new Set,...ngDevMode?[{debugName:"hiddenKinds"}]:[]);showIsolated=T(false,...ngDevMode?[{debugName:"showIsolated"}]:[]);page=T("",...ngDevMode?[{debugName:"page"}]:[]);directOnly=T(false,...ngDevMode?[{debugName:"directOnly"}]:[]);hoveredNodeId=T(null,...ngDevMode?[{debugName:"hoveredNodeId"}]:[]);hoveredLink=T(null,...ngDevMode?[{debugName:"hoveredLink"}]:[]);viewport=T({x:0,y:0,zoom:1},...ngDevMode?[{debugName:"viewport"}]:[]);canvasSize=T({width:800,height:600},...ngDevMode?[{debugName:"canvasSize"}]:[]);resizeObserver=null;panStart=null;fittedOnce=false;userMoved=false;graph=this.state.graph;nodeById=f(()=>new Map(this.graph().nodes.map(e=>[e.id,e])),...ngDevMode?[{debugName:"nodeById"}]:[]);ruleById=f(()=>new Map(this.graph().rules.map(e=>[e.id,e])),...ngDevMode?[{debugName:"ruleById"}]:[]);pages=f(()=>this.graph().pages.filter(e=>e.name),...ngDevMode?[{debugName:"pages"}]:[]);pageNodes=f(()=>{const e=this.page();return this.graph().nodes.filter(n=>!e||n.kind!=="element"||n.pageName===e||!n.pageName)},...ngDevMode?[{debugName:"pageNodes"}]:[]);visibleEdges=f(()=>{const e=this.hiddenKinds();const n=new Set(this.pageNodes().map(t=>t.id));return this.graph().edges.filter(t=>!e.has(t.kind)&&n.has(t.from)&&n.has(t.to))},...ngDevMode?[{debugName:"visibleEdges"}]:[]);kindCounts=f(()=>{const e=new Map;this.graph().rules.forEach(n=>e.set(n.kind,(e.get(n.kind)??0)+1));return e},...ngDevMode?[{debugName:"kindCounts"}]:[]);layout=f(()=>Oe(this.pageNodes(),this.visibleEdges(),this.showIsolated()),...ngDevMode?[{debugName:"layout"}]:[]);linkedCount=f(()=>this.layout().nodes.filter(e=>!e.isolated).length,...ngDevMode?[{debugName:"linkedCount"}]:[]);elementCount=f(()=>this.graph().nodes.filter(e=>e.kind==="element").length,...ngDevMode?[{debugName:"elementCount"}]:[]);rulesByOwner=f(()=>{const e=new Map;this.graph().rules.forEach(n=>e.set(n.owner,[...e.get(n.owner)??[],n]));return e},...ngDevMode?[{debugName:"rulesByOwner"}]:[]);issuesByNode=f(()=>{const e=new Map;this.graph().issues.forEach(n=>n.nodeIds.forEach(t=>e.set(t,[...e.get(t)??[],n])));return e},...ngDevMode?[{debugName:"issuesByNode"}]:[]);errorCount=f(()=>this.graph().issues.filter(e=>e.severity==="error").length,...ngDevMode?[{debugName:"errorCount"}]:[]);selectedNode=f(()=>{const e=this.state.selectedId();return e?this.nodeById().get(e)??null:null},...ngDevMode?[{debugName:"selectedNode"}]:[]);trace=f(()=>{const e=this.state.selectedId();return e?G({...this.graph(),edges:this.visibleEdges()},e):null},...ngDevMode?[{debugName:"trace"}]:[]);focusSet=f(()=>{const e=this.trace();if(e){const t=o=>o.filter(i=>!this.directOnly()||i.depth===1).map(i=>i.id);return new Set([e.node.id,...t(e.upstream),...t(e.downstream)])}const n=this.hoveredNodeId();if(n){const t=new Set([n]);this.visibleEdges().forEach(o=>{if(o.from===n)t.add(o.to);if(o.to===n)t.add(o.from)});return t}return null},...ngDevMode?[{debugName:"focusSet"}]:[]);searchMatches=f(()=>{const e=this.search().trim().toLowerCase();const n=this.layout().nodes.map(t=>t.node);return(e?this.graph().nodes.filter(t=>t.label.toLowerCase().includes(e)||t.name.toLowerCase().includes(e)):n.filter(t=>(this.rulesByOwner().get(t.id)?.length??0)>0)).slice(0,200)},...ngDevMode?[{debugName:"searchMatches"}]:[]);searchHighlight=f(()=>{return this.search().trim()?new Set(this.searchMatches().map(e=>e.id)):null},...ngDevMode?[{debugName:"searchHighlight"}]:[]);minimapScale=f(()=>{const e=this.layout();return Math.min(172/Math.max(1,e.width),108/Math.max(1,e.height))},...ngDevMode?[{debugName:"minimapScale"}]:[]);minimapViewport=f(()=>{const e=this.viewport();const n=this.canvasSize();const t=this.minimapScale();return{x:6+-e.x/e.zoom*t,y:6+-e.y/e.zoom*t,width:n.width/e.zoom*t,height:n.height/e.zoom*t}},...ngDevMode?[{debugName:"minimapViewport"}]:[]);zoomPercent=f(()=>Math.round(this.viewport().zoom*100),...ngDevMode?[{debugName:"zoomPercent"}]:[]);constructor(){U(()=>{this.layout();this.canvasSize();X(()=>{if(this.fittedOnce&&!this.userMoved&&!this.state.selectedId())this.fit()})});U(()=>{const e=this.state.selectedId();const n=this.focusSet();const t=this.layout();this.canvasSize();if(e)X(()=>this.frame(n??new Set([e]),t))})}ngOnInit(){this.state.refresh();this.restoreFilters()}ngAfterViewInit(){const e=this.canvasRef()?.nativeElement;if(!e)return;this.resizeObserver=new ResizeObserver(()=>{this.canvasSize.set({width:e.clientWidth,height:e.clientHeight});if(!this.fittedOnce&&e.clientWidth>0){this.fittedOnce=true;if(!this.state.selectedId())this.fit()}});this.resizeObserver.observe(e)}ngOnDestroy(){this.resizeObserver?.disconnect()}toggleKind(e){this.hiddenKinds.update(n=>{const t=new Set(n);if(t.has(e))t.delete(e);else t.add(e);return t});this.saveFilters()}showOnlyKind(e){this.hiddenKinds.set(new Set(this.kinds.filter(n=>n!==e)));this.saveFilters()}showAllKinds(){this.hiddenKinds.set(new Set);this.saveFilters()}setShowIsolated(e){this.showIsolated.set(e);this.saveFilters();setTimeout(()=>this.fitToScreen())}setPage(e){this.page.set(e);setTimeout(()=>this.fitToScreen())}select(e){this.state.selectedId.set(e)}onNodeClick(e,n){e.stopPropagation();this.select(this.state.selectedId()===n?null:n)}isDimmed(e){const n=this.focusSet();const t=this.searchHighlight();return n!==null&&!n.has(e)||n===null&&t!==null&&!t.has(e)}isLinkDimmed(e){const n=this.focusSet();if(!n)return this.searchHighlight()!==null;if(!n.has(e.from)||!n.has(e.to))return true;const t=this.trace();if(!t)return false;const o=new Set(t.upstream.map(u=>u.id));const i=new Set(t.downstream.map(u=>u.id));const a=t.node.id;const r=(o.has(e.from)||e.from===a)&&(o.has(e.to)||e.to===a);const s=(i.has(e.from)||e.from===a)&&(i.has(e.to)||e.to===a);return!(r||s)}linkColor(e){return`var(--nvb-logic-${e.kinds[0]})`}rulesOf(e){return this.rulesByOwner().get(e)??[]}issuesOf(e){return this.issuesByNode().get(e)??[]}labelOf(e){return this.nodeById().get(e)?.label??e.replace(/^[a-z]+:/,"")}isRef(e){return"ref"in e}segmentText(e){return"text"in e?e.text:e.label}linkRules(e){return e.ruleIds.map(n=>this.ruleById().get(n)).filter(n=>!!n)}focusIssue(e){const n=e.nodeIds.find(t=>!t.startsWith("missing:"))??e.nodeIds[0];if(n)this.select(n)}openInBuilder(e){if(e.kind==="element")this.state.openInBuilder(e.name)}depthLabel(e){return e===1?this.i18n.t("logicMap.depth.direct"):this.i18n.t("logicMap.depth.steps",{count:e})}zoomBy(e,n){this.userMoved=true;const t=this.viewport();const o=this.canvasSize();const i=n??{x:o.width/2,y:o.height/2};const a=K(t.zoom*e,ee,B);const r=a/t.zoom;this.viewport.set({zoom:a,x:i.x-(i.x-t.x)*r,y:i.y-(i.y-t.y)*r})}resetZoom(){const e=this.viewport();this.zoomBy(1/e.zoom)}fitToScreen(){this.userMoved=false;this.fit()}fit(){const e=this.layout();const n=this.canvasSize();if(e.width===0||n.width===0){this.viewport.set({x:P,y:P,zoom:1});return}const t=K(Math.min((n.width-96)/e.width,(n.height-96)/e.height,1),ee,B);this.viewport.set({zoom:t,x:(n.width-e.width*t)/2,y:Math.max(P,(n.height-e.height*t)/2)})}frame(e,n){const t=n.nodes.filter(c=>e.has(c.node.id));const o=this.canvasSize();if(t.length===0||o.width===0)return;const i=Math.min(...t.map(c=>c.x));const a=Math.min(...t.map(c=>c.y));const r=Math.max(...t.map(c=>c.x+224));const s=Math.max(...t.map(c=>c.y+64));const u=this.viewport();const p=Math.min((o.width-96)/Math.max(1,r-i),(o.height-96)/Math.max(1,s-a));const d=K(Math.min(Math.max(u.zoom,.8),p),.5,B);this.viewport.set({zoom:d,x:o.width/2-(i+r)/2*d,y:o.height/2-(a+s)/2*d})}onWheel(e){e.preventDefault();this.userMoved=true;const n=e.currentTarget.getBoundingClientRect();if(e.ctrlKey||e.metaKey){this.zoomBy(Math.exp(-e.deltaY*.0022),{x:e.clientX-n.left,y:e.clientY-n.top});return}const t=this.viewport();const o=e.shiftKey&&e.deltaX===0?e.deltaY:e.deltaX;const i=e.shiftKey&&e.deltaX===0?0:e.deltaY;this.viewport.set({...t,x:t.x-o,y:t.y-i})}onPointerDown(e){if(e.button!==0||e.target.closest(".lm-node, .lm-overlay"))return;this.panStart={x:e.clientX,y:e.clientY,viewport:this.viewport(),pointerId:e.pointerId};e.currentTarget.setPointerCapture(e.pointerId)}onPointerMove(e){if(!this.panStart||e.pointerId!==this.panStart.pointerId)return;const{viewport:n}=this.panStart;this.userMoved=true;this.viewport.set({...n,x:n.x+e.clientX-this.panStart.x,y:n.y+e.clientY-this.panStart.y})}onPointerUp(e){if(!this.panStart)return;const n=Math.abs(e.clientX-this.panStart.x)+Math.abs(e.clientY-this.panStart.y);this.panStart=null;if(n<4)this.select(null)}onMinimapPointer(e){this.userMoved=true;if(e.type==="pointerdown")e.currentTarget.setPointerCapture(e.pointerId);else if(e.buttons!==1)return;const n=e.currentTarget.getBoundingClientRect();const t=this.minimapScale();const o=this.viewport();const i=this.canvasSize();const a=(e.clientX-n.left-6)/t;const r=(e.clientY-n.top-6)/t;this.viewport.set({...o,x:i.width/2-a*o.zoom,y:i.height/2-r*o.zoom})}onKeydown(e){const n=e.composedPath()[0];if(e.ctrlKey||e.metaKey||e.altKey||n?.closest?.('input, textarea, select, [contenteditable="true"]'))return;if(e.key==="Escape")this.select(null);else if(e.key==="+"||e.key==="=")this.zoomBy(1.2);else if(e.key==="-")this.zoomBy(1/1.2);else if(e.key==="0")this.resetZoom();else if(e.key==="f"||e.key==="F")this.fitToScreen();else return;e.preventDefault()}downloadSvg(){const e=Re(this.layout(),this.rulesByOwner(),this.isDarkTheme(),i=>this.i18n.t(`logicMap.kind.${i}`));const n=new Blob([e],{type:"image/svg+xml"});const t=URL.createObjectURL(n);const o=document.createElement("a");o.href=t;o.download="logic-map.svg";o.click();setTimeout(()=>URL.revokeObjectURL(t),1e3)}isDarkTheme(){return this.canvasRef()?.nativeElement.closest('[data-theme="dark"]')!==null}restoreFilters(){try{const e=JSON.parse(localStorage.getItem(ne)??"null");if(e){this.hiddenKinds.set(new Set((e.hidden??[]).filter(n=>this.kinds.includes(n))));this.showIsolated.set(e.isolated===true)}}catch{}}saveFilters(){try{localStorage.setItem(ne,JSON.stringify({hidden:Array.from(this.hiddenKinds()),isolated:this.showIsolated()}))}catch{}}static ɵfac=_.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"22.2.1",ngImport:_,type:V,deps:[],target:_.ɵɵFactoryTarget.Component});static ɵcmp=_.ɵɵngDeclareComponent({minVersion:"17.0.0",version:"22.2.1",type:V,isStandalone:true,selector:"nvb-logic-map",host:{listeners:{"keydown":"onKeydown($event)"}},viewQueries:[{propertyName:"canvasRef",first:true,predicate:["canvas"],descendants:true,isSignal:true}],ngImport:_,template:"<nvb-pro-feature-gate [feature]=\"'logicMap.title' | nvbT\">\n <div class=\"lm\" [class.lm--with-detail]=\"selectedNode()\" data-testid=\"logic-map-root\">\n <!-- ── Sidebar: find, filter, issues ─────────────────────────────────── -->\n <aside class=\"lm-side\" [attr.aria-label]=\"'logicMap.sidebar' | nvbT\">\n <header class=\"lm-side__header\">\n <h2 class=\"lm-side__title\">{{ 'logicMap.title' | nvbT }}</h2>\n <p class=\"lm-side__meta\" data-testid=\"logic-map-summary\">\n {{ linkedCount() }} {{ 'logicMap.meta.connected' | nvbT }} ·\n {{ graph().rules.length }} {{ 'logicMap.meta.rules' | nvbT }} ·\n {{ elementCount() }} {{ 'logicMap.meta.fields' | nvbT }}\n </p>\n </header>\n\n <div class=\"lm-side__search\">\n <builder-search-input\n [value]=\"search()\"\n [placeholder]=\"'logicMap.search' | nvbT\"\n [ariaLabel]=\"'logicMap.search' | nvbT\"\n [clearAriaLabel]=\"'logicMap.clearSearch' | nvbT\"\n testId=\"logic-map-search\"\n (valueChanged)=\"search.set($event)\"\n (cleared)=\"search.set('')\"\n />\n </div>\n\n <div class=\"lm-side__scroll\">\n <section class=\"lm-section\">\n <header class=\"lm-section__header\">\n <h3 class=\"lm-section__title\">{{ 'logicMap.kinds' | nvbT }}</h3>\n @if (hiddenKinds().size > 0) {\n <button type=\"button\" class=\"lm-link-button\" (click)=\"showAllKinds()\" data-testid=\"logic-map-kinds-all\">\n {{ 'logicMap.kinds.all' | nvbT }}\n </button>\n }\n </header>\n <ul class=\"lm-kinds\" role=\"list\">\n @for (kind of kinds; track kind) {\n @let count = kindCounts().get(kind) ?? 0;\n @let shown = !hiddenKinds().has(kind);\n <li class=\"lm-kind\" [class.lm-kind--off]=\"!shown\" [class.lm-kind--empty]=\"count === 0\">\n <button\n type=\"button\"\n role=\"checkbox\"\n class=\"lm-kind__toggle\"\n [attr.aria-checked]=\"shown\"\n (click)=\"toggleKind(kind)\"\n [attr.data-testid]=\"'logic-map-kind-' + kind\"\n >\n <span class=\"lm-kind__check\" aria-hidden=\"true\">\n @if (shown) {\n <nvb-icon name=\"check\" />\n }\n </span>\n <svg class=\"lm-kind__swatch\" width=\"26\" height=\"8\" style=\"width: 26px; height: 8px\" aria-hidden=\"true\">\n <line\n x1=\"1\"\n y1=\"4\"\n x2=\"25\"\n y2=\"4\"\n [style.stroke]=\"'var(--nvb-logic-' + kind + ')'\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"kindDash[kind] || null\"\n />\n </svg>\n <span class=\"lm-kind__label\">{{ 'logicMap.kind.' + kind | nvbT }}</span>\n <span class=\"lm-kind__count\">{{ count }}</span>\n </button>\n @if (count > 0) {\n <button type=\"button\" class=\"lm-kind__only\" (click)=\"showOnlyKind(kind)\">\n {{ 'logicMap.kinds.only' | nvbT }}\n </button>\n }\n </li>\n }\n </ul>\n </section>\n\n <section class=\"lm-section\">\n <h3 class=\"lm-section__title\">{{ 'logicMap.show' | nvbT }}</h3>\n <label class=\"lm-check\">\n <input\n type=\"checkbox\"\n [checked]=\"showIsolated()\"\n (change)=\"setShowIsolated($any($event.target).checked)\"\n data-testid=\"logic-map-show-isolated\"\n />\n <span>{{ 'logicMap.showIsolated' | nvbT }}</span>\n </label>\n @if (pages().length > 1) {\n <div class=\"lm-field\">\n <span>{{ 'logicMap.page' | nvbT }}</span>\n <nvb-select\n class=\"nvb-control__attribute-input\"\n [value]=\"page()\"\n (valueChange)=\"setPage($event)\"\n [attr.aria-label]=\"'logicMap.page' | nvbT\"\n testId=\"logic-map-page\"\n >\n <option value=\"\">{{ 'logicMap.page.all' | nvbT }}</option>\n @for (item of pages(); track item.name) {\n <option [value]=\"item.name\">{{ item.label }}</option>\n }\n </nvb-select>\n </div>\n }\n </section>\n\n @if (graph().issues.length > 0) {\n <section class=\"lm-section\" data-testid=\"logic-map-issues\">\n <h3 class=\"lm-section__title\">\n {{ 'logicMap.issues' | nvbT }}\n <span class=\"lm-count lm-count--issue\">{{ graph().issues.length }}</span>\n </h3>\n <ul class=\"lm-issues\" role=\"list\">\n @for (issue of graph().issues; track $index) {\n <li>\n <button type=\"button\" class=\"lm-issue\" [attr.data-severity]=\"issue.severity\" (click)=\"focusIssue(issue)\">\n <nvb-icon [name]=\"issue.severity === 'error' ? 'error' : 'warning'\" class=\"lm-issue__icon\" />\n <span>\n <span class=\"lm-issue__level\">{{ issue.severity === 'error' ? ('logicMap.issue.error' | nvbT) : ('logicMap.issue.warning' | nvbT) }}</span>\n {{ issue.message }}\n </span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"lm-section lm-section--list\">\n <h3 class=\"lm-section__title\">\n @if (search().trim()) {\n {{ 'logicMap.results' | nvbT }}\n } @else {\n {{ 'logicMap.withLogic' | nvbT }}\n }\n <span class=\"lm-count\">{{ searchMatches().length }}</span>\n </h3>\n @if (searchMatches().length === 0) {\n <p class=\"lm-muted\">\n @if (search().trim()) {\n {{ 'logicMap.results.none' | nvbT }}\n } @else {\n {{ 'logicMap.withLogic.none' | nvbT }}\n }\n </p>\n } @else {\n <ul class=\"lm-list\" role=\"list\">\n @for (node of searchMatches(); track node.id) {\n <li>\n <button\n type=\"button\"\n class=\"lm-list__item\"\n [class.lm-list__item--active]=\"state.selectedId() === node.id\"\n (click)=\"select(node.id)\"\n [attr.data-testid]=\"'logic-map-list-' + node.name\"\n >\n <span class=\"lm-list__label\">{{ node.label }}</span>\n <span class=\"lm-list__meta\">{{ node.kind === 'element' ? (node.context ? node.context + ' · ' : '') + node.name : ('logicMap.nodeKind.' + node.kind | nvbT) }}</span>\n @if (rulesOf(node.id).length > 0) {\n <span class=\"lm-count\">{{ rulesOf(node.id).length }}</span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </section>\n </div>\n </aside>\n\n <!-- ── Canvas ────────────────────────────────────────────────────────── -->\n <section\n #canvas\n class=\"lm-canvas\"\n [class.lm-canvas--panning]=\"false\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n [attr.aria-label]=\"'logicMap.canvas' | nvbT\"\n tabindex=\"0\"\n data-testid=\"logic-map-canvas\"\n >\n @let view = viewport();\n @let map = layout();\n <div\n class=\"lm-world\"\n [style.transform]=\"'translate(' + view.x + 'px, ' + view.y + 'px) scale(' + view.zoom + ')'\"\n >\n <svg\n class=\"lm-links\"\n [attr.width]=\"map.width + 200\"\n [attr.height]=\"map.height + 200\"\n [style.width.px]=\"map.width + 200\"\n [style.height.px]=\"map.height + 200\"\n aria-hidden=\"true\"\n >\n <defs>\n @for (kind of kinds; track kind) {\n <marker\n [attr.id]=\"'lm-arrow-' + kind\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"7\"\n markerHeight=\"7\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M0 0 L10 5 L0 10 z\" [style.fill]=\"'var(--nvb-logic-' + kind + ')'\" />\n </marker>\n }\n </defs>\n @for (link of map.links; track link.id) {\n @let dimmed = isLinkDimmed(link);\n <g class=\"lm-link\" [class.lm-link--dimmed]=\"dimmed\" [class.lm-link--hot]=\"hoveredLink() === link\">\n <path\n class=\"lm-link__line\"\n [attr.d]=\"link.path\"\n [style.stroke]=\"linkColor(link)\"\n [attr.stroke-dasharray]=\"kindDash[link.kinds[0]] || (link.backward ? '6 4' : null)\"\n [attr.marker-end]=\"'url(#lm-arrow-' + link.kinds[0] + ')'\"\n />\n <path\n class=\"lm-link__hit\"\n [attr.d]=\"link.path\"\n (mouseenter)=\"hoveredLink.set(link)\"\n (mouseleave)=\"hoveredLink.set(null)\"\n (click)=\"select(link.to)\"\n />\n </g>\n }\n </svg>\n\n @if (map.isolatedTop !== null) {\n <div class=\"lm-isolated-heading\" [style.top.px]=\"map.isolatedTop - 30\">\n {{ 'logicMap.isolated' | nvbT }}\n </div>\n }\n\n @for (item of map.nodes; track item.node.id) {\n @let node = item.node;\n @let ruleCount = rulesOf(node.id).length;\n @let nodeIssues = issuesOf(node.id);\n <button\n type=\"button\"\n class=\"lm-node\"\n [attr.data-kind]=\"node.kind\"\n [class.lm-node--selected]=\"state.selectedId() === node.id\"\n [class.lm-node--dimmed]=\"isDimmed(node.id)\"\n [class.lm-node--isolated]=\"item.isolated\"\n [style.left.px]=\"item.x\"\n [style.top.px]=\"item.y\"\n [style.width.px]=\"nodeWidth\"\n [style.height.px]=\"nodeHeight\"\n (click)=\"onNodeClick($event, node.id)\"\n (dblclick)=\"openInBuilder(node)\"\n (mouseenter)=\"hoveredNodeId.set(node.id)\"\n (mouseleave)=\"hoveredNodeId.set(null)\"\n [attr.aria-pressed]=\"state.selectedId() === node.id\"\n [attr.data-testid]=\"'logic-map-node-' + node.name\"\n [title]=\"(node.context ? node.context + ' › ' : '') + node.label\"\n >\n <span class=\"lm-node__icon\" aria-hidden=\"true\">\n @switch (node.kind) {\n @case ('dataSource') { <nvb-icon name=\"database\" /> }\n @case ('variable') { <nvb-icon [svg]=\"icons.functions\" /> }\n @case ('rule') { <nvb-icon name=\"rule\" /> }\n @case ('trigger') { <nvb-icon name=\"bolt\" /> }\n @case ('missing') { <nvb-icon name=\"error\" /> }\n @default { <nvb-icon name=\"edit_note\" /> }\n }\n </span>\n <span class=\"lm-node__text\">\n @if (node.context) {\n <span class=\"lm-node__context\">{{ node.context }}</span>\n }\n <span class=\"lm-node__label\">{{ node.label }}</span>\n <span class=\"lm-node__meta\">\n {{ node.typeLabel }}@if (node.kind === 'element' && node.name !== node.label) {<span class=\"lm-node__name\"> · {{ node.name }}</span>}\n </span>\n </span>\n @if (nodeIssues.length > 0) {\n <span class=\"lm-node__issue\" [attr.data-severity]=\"nodeIssues[0].severity\" [title]=\"nodeIssues[0].message\">\n <nvb-icon [name]=\"nodeIssues[0].severity === 'error' ? 'error' : 'warning'\" />\n </span>\n } @else if (ruleCount > 0) {\n <span class=\"lm-node__count\" [title]=\"ruleCount + ' ' + ('logicMap.rulesOnField' | nvbT)\">{{ ruleCount }}</span>\n }\n </button>\n }\n </div>\n\n @if (map.nodes.length === 0) {\n <div class=\"lm-empty lm-overlay\" data-testid=\"logic-map-empty\">\n <nvb-icon [svg]=\"icons.account_tree\" class=\"lm-empty__icon\" />\n @if (graph().rules.length === 0) {\n <h3>{{ 'logicMap.empty.title' | nvbT }}</h3>\n <p>\n {{\n 'logicMap.empty.text' | nvbT\n }}\n </p>\n } @else {\n <h3>{{ 'logicMap.empty.filtered' | nvbT }}</h3>\n <p>{{ 'logicMap.empty.filteredText' | nvbT }}</p>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"showAllKinds(); setPage('')\">\n {{ 'logicMap.empty.reset' | nvbT }}\n </button>\n }\n @if (!showIsolated() && elementCount() > 0) {\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"setShowIsolated(true)\">\n {{ 'logicMap.empty.showAll' | nvbT }}\n </button>\n }\n </div>\n }\n\n @if (hoveredLink(); as link) {\n <div\n class=\"lm-tooltip lm-overlay\"\n role=\"tooltip\"\n [style.left.px]=\"link.midX * view.zoom + view.x\"\n [style.top.px]=\"link.midY * view.zoom + view.y\"\n >\n <div class=\"lm-tooltip__route\">{{ labelOf(link.from) }} → {{ labelOf(link.to) }}</div>\n @for (rule of linkRules(link); track rule.id) {\n <div class=\"lm-tooltip__rule\">\n <span class=\"lm-dot\" [style.background]=\"'var(--nvb-logic-' + rule.kind + ')'\"></span>\n <span>{{ rule.summary }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"lm-toolbar lm-overlay\" role=\"toolbar\" [attr.aria-label]=\"'logicMap.view' | nvbT\">\n <builder-icon-button icon=\"zoom_out\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.zoomOut' | nvbT\" [title]=\"'logicMap.zoomOut' | nvbT\" (clicked)=\"zoomBy(1 / 1.2)\" testId=\"logic-map-zoom-out\" />\n <button type=\"button\" class=\"lm-toolbar__zoom\" (click)=\"resetZoom()\" [title]=\"'logicMap.zoomReset' | nvbT\" data-testid=\"logic-map-zoom-value\">{{ zoomPercent() }}%</button>\n <builder-icon-button icon=\"zoom_in\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.zoomIn' | nvbT\" [title]=\"'logicMap.zoomIn' | nvbT\" (clicked)=\"zoomBy(1.2)\" testId=\"logic-map-zoom-in\" />\n <builder-icon-button icon=\"fit_screen\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.fit' | nvbT\" [title]=\"'logicMap.fit' | nvbT\" (clicked)=\"fitToScreen()\" testId=\"logic-map-fit\" />\n <span class=\"lm-toolbar__divider\" aria-hidden=\"true\"></span>\n <builder-icon-button icon=\"download\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.download' | nvbT\" [title]=\"'logicMap.download' | nvbT\" (clicked)=\"downloadSvg()\" testId=\"logic-map-download\" />\n </div>\n\n @if (map.nodes.length > 0) {\n <div\n class=\"lm-minimap lm-overlay\"\n [style.width.px]=\"minimapWidth\"\n [style.height.px]=\"minimapHeight\"\n (pointerdown)=\"onMinimapPointer($event)\"\n (pointermove)=\"onMinimapPointer($event)\"\n aria-hidden=\"true\"\n >\n <svg [attr.width]=\"minimapWidth\" [attr.height]=\"minimapHeight\" [style.width.px]=\"minimapWidth\" [style.height.px]=\"minimapHeight\">\n @let scale = minimapScale();\n @for (item of map.nodes; track item.node.id) {\n <rect\n class=\"lm-minimap__node\"\n [class.lm-minimap__node--selected]=\"state.selectedId() === item.node.id\"\n [attr.x]=\"6 + item.x * scale\"\n [attr.y]=\"6 + item.y * scale\"\n [attr.width]=\"nodeWidth * scale\"\n [attr.height]=\"nodeHeight * scale\"\n rx=\"1\"\n />\n }\n @let frame = minimapViewport();\n <rect class=\"lm-minimap__frame\" [attr.x]=\"frame.x\" [attr.y]=\"frame.y\" [attr.width]=\"frame.width\" [attr.height]=\"frame.height\" rx=\"2\" />\n </svg>\n </div>\n }\n\n @if (hiddenKinds().size > 0 && map.nodes.length > 0) {\n <div class=\"lm-filter-note lm-overlay\" role=\"status\" data-testid=\"logic-map-filter-note\">\n <nvb-icon [svg]=\"icons.filter_list\" />\n {{ (hiddenKinds().size === 1 ? 'logicMap.hiddenOne' : 'logicMap.hiddenMany') | nvbT: { count: hiddenKinds().size } }}\n <button type=\"button\" (click)=\"showAllKinds()\">{{ 'logicMap.kinds.all' | nvbT }}</button>\n </div>\n }\n\n <p class=\"lm-hint lm-overlay\">\n {{ 'logicMap.hint' | nvbT }}\n </p>\n </section>\n\n <!-- ── Detail ────────────────────────────────────────────────────────── -->\n @if (selectedNode(); as node) {\n <aside class=\"lm-detail\" data-testid=\"logic-map-detail\" [attr.aria-label]=\"node.label\">\n <header class=\"lm-detail__header\">\n <div class=\"lm-detail__heading\">\n <p class=\"lm-detail__eyebrow\">\n @if (node.context) {\n {{ node.context }} ·\n }\n {{ node.typeLabel || ('logicMap.nodeKind.' + node.kind | nvbT) }}\n </p>\n <h3 class=\"lm-detail__title\">{{ node.label }}</h3>\n @if (node.kind === 'element') {\n <p class=\"lm-detail__meta\">\n <code>{{ node.name }}</code>\n @if (node.pageLabel) {\n · {{ node.pageLabel }}\n }\n </p>\n }\n </div>\n <builder-icon-button icon=\"close\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.close' | nvbT\" [title]=\"'logicMap.closeEsc' | nvbT\" (clicked)=\"select(null)\" testId=\"logic-map-detail-close\" />\n </header>\n\n <div class=\"lm-detail__body\">\n @if (node.kind === 'element') {\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm lm-detail__open\" (click)=\"openInBuilder(node)\" data-testid=\"logic-map-open-in-builder\">\n <nvb-icon name=\"edit\" />\n {{ 'logicMap.openInBuilder' | nvbT }}\n </button>\n }\n\n @for (issue of issuesOf(node.id); track $index) {\n <p class=\"lm-callout\" [attr.data-severity]=\"issue.severity\" role=\"note\">\n <nvb-icon [name]=\"issue.severity === 'error' ? 'error' : 'warning'\" />\n <span>{{ issue.message }}</span>\n </p>\n }\n\n <section class=\"lm-detail__section\">\n <h4 class=\"lm-detail__section-title\">{{ 'logicMap.behaviour' | nvbT }}</h4>\n @if (rulesOf(node.id).length === 0) {\n <p class=\"lm-muted\">\n @if (node.kind === 'missing') {\n {{ 'logicMap.behaviour.missing' | nvbT }}\n } @else {\n {{ 'logicMap.behaviour.none' | nvbT }}\n }\n </p>\n } @else {\n <ol class=\"lm-rules\" role=\"list\">\n @for (rule of rulesOf(node.id); track rule.id) {\n <li class=\"lm-rule\" [attr.data-testid]=\"'logic-map-rule-' + rule.property\">\n <span class=\"lm-rule__kind\">\n <svg width=\"18\" height=\"6\" style=\"width: 18px; height: 6px\" aria-hidden=\"true\"><line x1=\"1\" y1=\"3\" x2=\"17\" y2=\"3\" [style.stroke]=\"'var(--nvb-logic-' + rule.kind + ')'\" stroke-width=\"2.5\" stroke-linecap=\"round\" [attr.stroke-dasharray]=\"kindDash[rule.kind] || null\" /></svg>\n {{ 'logicMap.kind.' + rule.kind | nvbT }}\n </span>\n <p class=\"lm-rule__text\">\n @for (segment of rule.segments; track $index) {\n @if (isRef(segment)) {\n <button type=\"button\" class=\"lm-ref\" (click)=\"select(segment.ref)\">{{ segment.label }}</button>\n } @else {\n {{ segment.text }}\n }\n }\n </p>\n @if (rule.expression) {\n <code class=\"lm-rule__code\">{{ rule.expression }}</code>\n }\n </li>\n }\n </ol>\n }\n </section>\n\n @if (trace(); as nodeTrace) {\n <label class=\"lm-check lm-detail__direct\">\n <input type=\"checkbox\" [checked]=\"directOnly()\" (change)=\"directOnly.set($any($event.target).checked)\" />\n <span>{{ 'logicMap.directOnly' | nvbT }}</span>\n </label>\n\n <section class=\"lm-detail__section\">\n <h4 class=\"lm-detail__section-title\">\n {{ 'logicMap.dependsOn' | nvbT }}\n <span class=\"lm-count\">{{ nodeTrace.upstream.length }}</span>\n </h4>\n @if (nodeTrace.upstream.length === 0) {\n <p class=\"lm-muted\">{{ 'logicMap.dependsOn.none' | nvbT }}</p>\n } @else {\n <ul class=\"lm-trace\" role=\"list\">\n @for (item of nodeTrace.upstream; track item.id) {\n <li>\n <button type=\"button\" class=\"lm-trace__item\" (click)=\"select(item.id)\">\n <span class=\"lm-trace__label\">{{ labelOf(item.id) }}</span>\n <span class=\"lm-trace__depth\">{{ depthLabel(item.depth) }}</span>\n </button>\n </li>\n }\n </ul>\n }\n </section>\n\n <section class=\"lm-detail__section\">\n <h4 class=\"lm-detail__section-title\">\n {{ 'logicMap.affects' | nvbT }}\n <span class=\"lm-count\">{{ nodeTrace.downstream.length }}</span>\n </h4>\n @if (nodeTrace.downstream.length === 0) {\n <p class=\"lm-muted\">{{ 'logicMap.affects.none' | nvbT }}</p>\n } @else {\n <ul class=\"lm-trace\" role=\"list\">\n @for (item of nodeTrace.downstream; track item.id) {\n <li>\n <button type=\"button\" class=\"lm-trace__item\" (click)=\"select(item.id)\">\n <span class=\"lm-trace__label\">{{ labelOf(item.id) }}</span>\n <span class=\"lm-trace__depth\">{{ depthLabel(item.depth) }}</span>\n </button>\n </li>\n }\n </ul>\n }\n </section>\n }\n </div>\n </aside>\n }\n </div>\n</nvb-pro-feature-gate>\n",styles:['@charset "UTF-8";:host{--nvb-input-height: 32px;--nvb-logic-visibility: #2a78d6;--nvb-logic-value: #eb6834;--nvb-logic-reset: #eb6834;--nvb-logic-data: #1baf7a;--nvb-logic-required: #eda100;--nvb-logic-validation: #e87ba4;--nvb-logic-locked: #008300;--nvb-logic-action: #4a3aa7;--nvb-logic-critical: #d03b3b;--nvb-logic-warning: #b26b00;display:block;height:100%;min-height:0;overflow:hidden;color:var(--nvb-color-neutral-800);font-size:12px;background:color-mix(in oklab,var(--nvb-color-neutral-050) 84%,var(--nvb-color-neutral-000))}:host-context([data-theme=dark]){--nvb-logic-visibility: #3987e5;--nvb-logic-value: #d95926;--nvb-logic-reset: #d95926;--nvb-logic-data: #199e70;--nvb-logic-required: #c98500;--nvb-logic-validation: #d55181;--nvb-logic-locked: #008300;--nvb-logic-action: #9085e9;--nvb-logic-critical: #e66767;--nvb-logic-warning: #fab219}button:not(.nvb-button){font:inherit;color:inherit}.lm{position:relative;height:100%;min-height:0;display:grid;grid-template-columns:280px minmax(0,1fr);gap:10px;padding:10px;box-sizing:border-box}.lm--with-detail{grid-template-columns:280px minmax(0,1fr) 340px}.lm-side,.lm-detail,.lm-canvas{min-height:0;background:var(--nvb-card-bg, var(--nvb-color-neutral-000));border:1px solid var(--nvb-card-border-color, var(--nvb-color-neutral-200));border-radius:var(--nvb-card-radius, 8px);box-shadow:var(--nvb-card-shadow, none)}.lm-side{display:flex;flex-direction:column;overflow:hidden}.lm-side__header{padding:12px 12px 8px}.lm-side__title{margin:0;font-size:13px;font-weight:600;line-height:18px;color:var(--nvb-color-neutral-900)}.lm-side__meta{margin:2px 0 0;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500);font-variant-numeric:tabular-nums}.lm-side__search{padding:0 12px 10px;border-bottom:1px solid var(--nvb-color-neutral-200)}.lm-side__scroll{flex:1 1 auto;min-height:0;overflow:auto}.lm-section{padding:10px 12px;border-bottom:1px solid var(--nvb-color-neutral-100, var(--nvb-color-neutral-200))}.lm-section:last-child{border-bottom:0}.lm-section__header{display:flex;align-items:center;justify-content:space-between}.lm-section__title{display:flex;align-items:center;gap:6px;margin:0 0 6px;font-size:11px;font-weight:600;line-height:16px;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.lm-section__header .lm-section__title{margin-bottom:6px}.lm-link-button{margin-bottom:6px;padding:0;border:0;background:none;font-size:11px;color:var(--nvb-color-primary-600);cursor:pointer}.lm-link-button:hover{text-decoration:underline}.lm-count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 5px;border-radius:999px;background:var(--nvb-color-neutral-100);color:var(--nvb-color-neutral-600);font-size:10px;font-weight:600;letter-spacing:0;text-transform:none;font-variant-numeric:tabular-nums}.lm-count--issue{background:color-mix(in oklab,var(--nvb-logic-critical) 14%,transparent);color:var(--nvb-logic-critical)}.lm-kinds,.lm-issues,.lm-list,.lm-trace,.lm-rules{margin:0;padding:0;list-style:none}.lm-kind{position:relative;display:flex;align-items:center}.lm-kind__toggle{flex:1 1 auto;display:flex;align-items:center;gap:8px;min-width:0;height:28px;padding:0 6px;border:0;border-radius:var(--nvb-radius-sm, 4px);background:none;cursor:pointer;text-align:left}.lm-kind__toggle:hover{background:var(--nvb-color-neutral-050)}.lm-kind__toggle:focus-visible{outline:none;box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.lm-kind__check{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:1px solid var(--nvb-color-neutral-400);border-radius:3px;font-size:12px;color:var(--nvb-color-neutral-000)}.lm-kind:not(.lm-kind--off) .lm-kind__check{border-color:var(--nvb-color-primary-600);background:var(--nvb-color-primary-600)}.lm-kind__swatch{flex:0 0 auto}.lm-kind__label{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--nvb-color-neutral-800)}.lm-kind__count{flex:0 0 auto;color:var(--nvb-color-neutral-500);font-variant-numeric:tabular-nums}.lm-kind--off .lm-kind__label,.lm-kind--off .lm-kind__swatch{opacity:.45}.lm-kind--empty .lm-kind__label,.lm-kind--empty .lm-kind__count{color:var(--nvb-color-neutral-400)}.lm-kind__only{position:absolute;right:26px;display:none;height:20px;padding:0 6px;border:1px solid var(--nvb-color-neutral-200);border-radius:4px;background:var(--nvb-color-neutral-000);font-size:11px;color:var(--nvb-color-primary-600);cursor:pointer}.lm-kind:hover .lm-kind__only,.lm-kind__only:focus-visible{display:inline-flex;align-items:center}.lm-check{display:flex;align-items:center;gap:8px;min-height:26px;cursor:pointer}.lm-check input{margin:0;accent-color:var(--nvb-color-primary-600)}.lm-field{display:grid;gap:4px;margin-top:8px;color:var(--nvb-color-neutral-600)}.lm-field nvb-select{display:block;width:100%}.lm-issue{display:flex;gap:8px;width:100%;margin:0 0 4px;padding:6px 8px;border:0;border-radius:var(--nvb-radius-sm, 4px);background:var(--nvb-color-neutral-050);line-height:16px;text-align:left;cursor:pointer}.lm-issue:hover{background:var(--nvb-color-neutral-100)}.lm-issue__icon{flex:0 0 auto;margin-top:1px;font-size:14px}.lm-issue[data-severity=error] .lm-issue__icon{color:var(--nvb-logic-critical)}.lm-issue[data-severity=warning] .lm-issue__icon{color:var(--nvb-logic-warning)}.lm-issue__level{font-weight:600}.lm-muted{margin:0;color:var(--nvb-color-neutral-500);line-height:17px}.lm-list__item{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-rows:auto auto;column-gap:8px;width:100%;padding:5px 8px;border:0;border-radius:var(--nvb-radius-sm, 4px);background:none;text-align:left;cursor:pointer}.lm-list__item:hover{background:var(--nvb-color-neutral-050)}.lm-list__item--active{background:var(--nvb-color-primary-050);box-shadow:inset 2px 0 0 var(--nvb-color-primary-600)}.lm-list__label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-weight:500;color:var(--nvb-color-neutral-800)}.lm-list__meta{grid-row:2;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;color:var(--nvb-color-neutral-500)}.lm-list__item .lm-count{grid-row:1/span 2;grid-column:2;align-self:center}.lm-canvas{position:relative;overflow:hidden;cursor:grab;touch-action:none;background-color:var(--nvb-color-neutral-000);background-image:radial-gradient(var(--nvb-color-neutral-200) 1px,transparent 1px);background-size:16px 16px;outline:none}.lm-canvas:active{cursor:grabbing}.lm-canvas:focus-visible{box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.lm-world{position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform}.lm-links{position:absolute;top:0;left:0;max-width:none;max-height:none;overflow:visible}svg{max-width:none}.lm-link__line{fill:none;stroke-width:2;stroke-linecap:round;transition:opacity .12s ease}.lm-link__hit{fill:none;stroke:transparent;stroke-width:14;pointer-events:stroke;cursor:pointer}.lm-link--hot .lm-link__line{stroke-width:3}.lm-link--dimmed .lm-link__line{opacity:.1}.lm-node{position:absolute;display:flex;align-items:center;gap:10px;box-sizing:border-box;padding:0 10px;border:1px solid var(--nvb-color-neutral-300);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 1px 2px #0000000f;text-align:left;cursor:pointer;transition:opacity .12s ease,border-color .12s ease,box-shadow .12s ease}.lm-node:hover{border-color:var(--nvb-color-primary-400, var(--nvb-color-primary-500))}.lm-node:focus-visible{outline:none;box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.lm-node--selected{border-color:var(--nvb-color-primary-600);box-shadow:0 0 0 2px color-mix(in oklab,var(--nvb-color-primary-500) 30%,transparent),0 4px 12px #0000001a}.lm-node--dimmed{opacity:.28}.lm-node--isolated{border-style:dashed;box-shadow:none}.lm-node__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;background:var(--nvb-color-neutral-100);color:var(--nvb-color-neutral-600);font-size:16px}.lm-node[data-kind=dataSource] .lm-node__icon{background:color-mix(in oklab,var(--nvb-logic-data) 16%,transparent);color:var(--nvb-color-neutral-800)}.lm-node[data-kind=variable] .lm-node__icon,.lm-node[data-kind=rule] .lm-node__icon,.lm-node[data-kind=trigger] .lm-node__icon{background:color-mix(in oklab,var(--nvb-logic-action) 14%,transparent);color:var(--nvb-color-neutral-800)}.lm-node[data-kind=missing]{border-style:dashed;border-color:var(--nvb-logic-critical)}.lm-node[data-kind=missing] .lm-node__icon{background:color-mix(in oklab,var(--nvb-logic-critical) 12%,transparent);color:var(--nvb-logic-critical)}.lm-node__text{flex:1 1 auto;display:grid;min-width:0}.lm-node__context{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10.5px;font-weight:600;line-height:14px;color:var(--nvb-color-primary-700)}.lm-node__label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:600;line-height:18px;color:var(--nvb-color-neutral-900)}.lm-node__meta{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500)}.lm-node__name{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:10.5px}.lm-node[data-kind=missing] .lm-node__meta{color:var(--nvb-logic-critical)}.lm-node__count{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:18px;padding:0 6px;border-radius:999px;background:var(--nvb-color-neutral-100);color:var(--nvb-color-neutral-600);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.lm-node__issue{flex:0 0 auto;font-size:16px}.lm-node__issue[data-severity=error]{color:var(--nvb-logic-critical)}.lm-node__issue[data-severity=warning]{color:var(--nvb-logic-warning)}.lm-isolated-heading{position:absolute;left:0;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.lm-overlay{cursor:default}.lm-toolbar{position:absolute;top:10px;right:10px;display:flex;align-items:center;gap:2px;padding:3px;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 2px 8px #00000014}.lm-toolbar__zoom{min-width:48px;height:26px;padding:0 6px;border:0;border-radius:4px;background:none;font-size:12px;font-variant-numeric:tabular-nums;color:var(--nvb-color-neutral-700);cursor:pointer}.lm-toolbar__zoom:hover{background:var(--nvb-color-neutral-050)}.lm-toolbar__divider{width:1px;height:18px;margin:0 3px;background:var(--nvb-color-neutral-200)}.lm-minimap{position:absolute;right:10px;bottom:10px;overflow:hidden;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:color-mix(in oklab,var(--nvb-color-neutral-000) 92%,transparent);box-shadow:0 2px 8px #00000014;cursor:pointer}.lm-minimap svg{display:block}.lm-minimap__node{fill:var(--nvb-color-neutral-300)}.lm-minimap__node--selected{fill:var(--nvb-color-primary-600)}.lm-minimap__frame{fill:color-mix(in oklab,var(--nvb-color-primary-500) 10%,transparent);stroke:var(--nvb-color-primary-600);stroke-width:1.5}.lm-filter-note{position:absolute;top:10px;left:10px;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 2px 8px #00000014;color:var(--nvb-color-neutral-700)}.lm-filter-note nvb-icon{font-size:15px;color:var(--nvb-color-neutral-500)}.lm-filter-note button{padding:0;border:0;background:none;color:var(--nvb-color-primary-600);cursor:pointer}.lm-filter-note button:hover{text-decoration:underline}.lm-hint{position:absolute;left:12px;bottom:8px;margin:0;font-size:11px;color:var(--nvb-color-neutral-500);pointer-events:none}.lm-tooltip{position:absolute;z-index:2;max-width:340px;padding:8px 10px;border:1px solid var(--nvb-color-neutral-200);border-radius:6px;background:var(--nvb-color-neutral-000);box-shadow:0 6px 20px #00000024;transform:translate(-50%,calc(-100% - 10px));pointer-events:none;line-height:17px}.lm-tooltip__route{margin-bottom:4px;font-weight:600;color:var(--nvb-color-neutral-900)}.lm-tooltip__rule{display:flex;gap:6px;color:var(--nvb-color-neutral-700)}.lm-dot{flex:0 0 auto;width:8px;height:8px;margin-top:5px;border-radius:999px}.lm-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;text-align:center}.lm-empty__icon{font-size:32px;color:var(--nvb-color-neutral-400)}.lm-empty h3{margin:4px 0 0;font-size:14px;font-weight:600;color:var(--nvb-color-neutral-900)}.lm-empty p{max-width:420px;margin:0 0 6px;font-size:13px;line-height:19px;color:var(--nvb-color-neutral-600)}.lm-detail{display:flex;flex-direction:column;overflow:hidden}.lm-detail__header{display:flex;align-items:flex-start;gap:8px;padding:12px 8px 10px 14px;border-bottom:1px solid var(--nvb-color-neutral-200)}.lm-detail__heading{flex:1 1 auto;min-width:0}.lm-detail__eyebrow{margin:0;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500)}.lm-detail__title{margin:0;font-size:14px;font-weight:600;line-height:20px;color:var(--nvb-color-neutral-900);overflow-wrap:anywhere}.lm-detail__meta{margin:2px 0 0;font-size:11px;color:var(--nvb-color-neutral-500)}.lm-detail__meta code,.lm-rule__code{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)}.lm-detail__body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px 16px}.lm-detail__open{display:inline-flex;align-items:center;gap:6px;margin-bottom:12px}.lm-callout{display:flex;gap:8px;margin:0 0 10px;padding:8px 10px;border-radius:6px;line-height:17px}.lm-callout[data-severity=error]{background:color-mix(in oklab,var(--nvb-logic-critical) 10%,transparent)}.lm-callout[data-severity=error] nvb-icon{color:var(--nvb-logic-critical)}.lm-callout[data-severity=warning]{background:color-mix(in oklab,var(--nvb-logic-warning) 12%,transparent)}.lm-callout[data-severity=warning] nvb-icon{color:var(--nvb-logic-warning)}.lm-callout nvb-icon{flex:0 0 auto;margin-top:1px;font-size:15px}.lm-detail__section{margin-top:14px}.lm-detail__section:first-of-type{margin-top:0}.lm-detail__section-title{display:flex;align-items:center;gap:6px;margin:0 0 6px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.lm-detail__direct{margin-top:14px;padding-top:10px;border-top:1px solid var(--nvb-color-neutral-200)}.lm-rule{padding:8px 0;border-top:1px solid var(--nvb-color-neutral-100, var(--nvb-color-neutral-200))}.lm-rule:first-child{border-top:0;padding-top:0}.lm-rule__kind{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--nvb-color-neutral-500)}.lm-rule__text{margin:3px 0 0;font-size:13px;line-height:19px;color:var(--nvb-color-neutral-900);overflow-wrap:anywhere}.lm-ref{display:inline;padding:0 4px;border:0;border-radius:4px;background:var(--nvb-color-primary-050);color:var(--nvb-color-primary-700);font-weight:500;cursor:pointer}.lm-ref:hover{text-decoration:underline}.lm-rule__code{display:block;margin-top:4px;padding:4px 6px;border-radius:4px;background:var(--nvb-color-neutral-050);font-size:11px;line-height:16px;color:var(--nvb-color-neutral-600);white-space:pre-wrap;overflow-wrap:anywhere}.lm-trace__item{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:5px 8px;border:0;border-radius:4px;background:none;text-align:left;cursor:pointer}.lm-trace__item:hover{background:var(--nvb-color-neutral-050)}.lm-trace__label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--nvb-color-neutral-800)}.lm-trace__depth{flex:0 0 auto;font-size:11px;color:var(--nvb-color-neutral-500)}@media(max-width:1100px){.lm,.lm--with-detail{grid-template-columns:240px minmax(0,1fr)}.lm-detail{position:absolute;top:10px;right:10px;bottom:10px;width:min(340px,100% - 20px);z-index:3}}@media(max-width:720px){.lm,.lm--with-detail{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(320px,1fr)}.lm-side{max-height:40vh}}@media(prefers-reduced-motion:reduce){.lm-node,.lm-link__line{transition:none}}\n'],dependencies:[{kind:"component",type:Z,selector:"nvb-icon",inputs:["name","svg","ariaLabel"]},{kind:"component",type:W,selector:"nvb-pro-feature-gate",inputs:["feature","compact"]},{kind:"component",type:J,selector:"nvb-select",inputs:["value","disabled","required","filterable","filterThreshold","allowCustomValue","placeholder","testId"],outputs:["valueChange","change","focus","blur"]},{kind:"component",type:Y,selector:"builder-icon-button",inputs:["icon","ariaLabel","title","testId","iconSet","size","tone","ghost","dragHandle","active","disabled","className"],outputs:["clicked"]},{kind:"component",type:q,selector:"builder-search-input",inputs:["value","placeholder","ariaLabel","testId","clearAriaLabel","showSearchIcon"],outputs:["valueChanged","cleared"]},{kind:"pipe",type:j,name:"nvbT"}],changeDetection:_.ChangeDetectionStrategy.OnPush})};_.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"22.2.1",ngImport:_,type:te,decorators:[{type:ge,args:[{selector:"nvb-logic-map",imports:[j,Z,W,J,Y,q],changeDetection:he.OnPush,host:{"(keydown)":"onKeydown($event)"},template:"<nvb-pro-feature-gate [feature]=\"'logicMap.title' | nvbT\">\n <div class=\"lm\" [class.lm--with-detail]=\"selectedNode()\" data-testid=\"logic-map-root\">\n <!-- ── Sidebar: find, filter, issues ─────────────────────────────────── -->\n <aside class=\"lm-side\" [attr.aria-label]=\"'logicMap.sidebar' | nvbT\">\n <header class=\"lm-side__header\">\n <h2 class=\"lm-side__title\">{{ 'logicMap.title' | nvbT }}</h2>\n <p class=\"lm-side__meta\" data-testid=\"logic-map-summary\">\n {{ linkedCount() }} {{ 'logicMap.meta.connected' | nvbT }} ·\n {{ graph().rules.length }} {{ 'logicMap.meta.rules' | nvbT }} ·\n {{ elementCount() }} {{ 'logicMap.meta.fields' | nvbT }}\n </p>\n </header>\n\n <div class=\"lm-side__search\">\n <builder-search-input\n [value]=\"search()\"\n [placeholder]=\"'logicMap.search' | nvbT\"\n [ariaLabel]=\"'logicMap.search' | nvbT\"\n [clearAriaLabel]=\"'logicMap.clearSearch' | nvbT\"\n testId=\"logic-map-search\"\n (valueChanged)=\"search.set($event)\"\n (cleared)=\"search.set('')\"\n />\n </div>\n\n <div class=\"lm-side__scroll\">\n <section class=\"lm-section\">\n <header class=\"lm-section__header\">\n <h3 class=\"lm-section__title\">{{ 'logicMap.kinds' | nvbT }}</h3>\n @if (hiddenKinds().size > 0) {\n <button type=\"button\" class=\"lm-link-button\" (click)=\"showAllKinds()\" data-testid=\"logic-map-kinds-all\">\n {{ 'logicMap.kinds.all' | nvbT }}\n </button>\n }\n </header>\n <ul class=\"lm-kinds\" role=\"list\">\n @for (kind of kinds; track kind) {\n @let count = kindCounts().get(kind) ?? 0;\n @let shown = !hiddenKinds().has(kind);\n <li class=\"lm-kind\" [class.lm-kind--off]=\"!shown\" [class.lm-kind--empty]=\"count === 0\">\n <button\n type=\"button\"\n role=\"checkbox\"\n class=\"lm-kind__toggle\"\n [attr.aria-checked]=\"shown\"\n (click)=\"toggleKind(kind)\"\n [attr.data-testid]=\"'logic-map-kind-' + kind\"\n >\n <span class=\"lm-kind__check\" aria-hidden=\"true\">\n @if (shown) {\n <nvb-icon name=\"check\" />\n }\n </span>\n <svg class=\"lm-kind__swatch\" width=\"26\" height=\"8\" style=\"width: 26px; height: 8px\" aria-hidden=\"true\">\n <line\n x1=\"1\"\n y1=\"4\"\n x2=\"25\"\n y2=\"4\"\n [style.stroke]=\"'var(--nvb-logic-' + kind + ')'\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"kindDash[kind] || null\"\n />\n </svg>\n <span class=\"lm-kind__label\">{{ 'logicMap.kind.' + kind | nvbT }}</span>\n <span class=\"lm-kind__count\">{{ count }}</span>\n </button>\n @if (count > 0) {\n <button type=\"button\" class=\"lm-kind__only\" (click)=\"showOnlyKind(kind)\">\n {{ 'logicMap.kinds.only' | nvbT }}\n </button>\n }\n </li>\n }\n </ul>\n </section>\n\n <section class=\"lm-section\">\n <h3 class=\"lm-section__title\">{{ 'logicMap.show' | nvbT }}</h3>\n <label class=\"lm-check\">\n <input\n type=\"checkbox\"\n [checked]=\"showIsolated()\"\n (change)=\"setShowIsolated($any($event.target).checked)\"\n data-testid=\"logic-map-show-isolated\"\n />\n <span>{{ 'logicMap.showIsolated' | nvbT }}</span>\n </label>\n @if (pages().length > 1) {\n <div class=\"lm-field\">\n <span>{{ 'logicMap.page' | nvbT }}</span>\n <nvb-select\n class=\"nvb-control__attribute-input\"\n [value]=\"page()\"\n (valueChange)=\"setPage($event)\"\n [attr.aria-label]=\"'logicMap.page' | nvbT\"\n testId=\"logic-map-page\"\n >\n <option value=\"\">{{ 'logicMap.page.all' | nvbT }}</option>\n @for (item of pages(); track item.name) {\n <option [value]=\"item.name\">{{ item.label }}</option>\n }\n </nvb-select>\n </div>\n }\n </section>\n\n @if (graph().issues.length > 0) {\n <section class=\"lm-section\" data-testid=\"logic-map-issues\">\n <h3 class=\"lm-section__title\">\n {{ 'logicMap.issues' | nvbT }}\n <span class=\"lm-count lm-count--issue\">{{ graph().issues.length }}</span>\n </h3>\n <ul class=\"lm-issues\" role=\"list\">\n @for (issue of graph().issues; track $index) {\n <li>\n <button type=\"button\" class=\"lm-issue\" [attr.data-severity]=\"issue.severity\" (click)=\"focusIssue(issue)\">\n <nvb-icon [name]=\"issue.severity === 'error' ? 'error' : 'warning'\" class=\"lm-issue__icon\" />\n <span>\n <span class=\"lm-issue__level\">{{ issue.severity === 'error' ? ('logicMap.issue.error' | nvbT) : ('logicMap.issue.warning' | nvbT) }}</span>\n {{ issue.message }}\n </span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"lm-section lm-section--list\">\n <h3 class=\"lm-section__title\">\n @if (search().trim()) {\n {{ 'logicMap.results' | nvbT }}\n } @else {\n {{ 'logicMap.withLogic' | nvbT }}\n }\n <span class=\"lm-count\">{{ searchMatches().length }}</span>\n </h3>\n @if (searchMatches().length === 0) {\n <p class=\"lm-muted\">\n @if (search().trim()) {\n {{ 'logicMap.results.none' | nvbT }}\n } @else {\n {{ 'logicMap.withLogic.none' | nvbT }}\n }\n </p>\n } @else {\n <ul class=\"lm-list\" role=\"list\">\n @for (node of searchMatches(); track node.id) {\n <li>\n <button\n type=\"button\"\n class=\"lm-list__item\"\n [class.lm-list__item--active]=\"state.selectedId() === node.id\"\n (click)=\"select(node.id)\"\n [attr.data-testid]=\"'logic-map-list-' + node.name\"\n >\n <span class=\"lm-list__label\">{{ node.label }}</span>\n <span class=\"lm-list__meta\">{{ node.kind === 'element' ? (node.context ? node.context + ' · ' : '') + node.name : ('logicMap.nodeKind.' + node.kind | nvbT) }}</span>\n @if (rulesOf(node.id).length > 0) {\n <span class=\"lm-count\">{{ rulesOf(node.id).length }}</span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </section>\n </div>\n </aside>\n\n <!-- ── Canvas ────────────────────────────────────────────────────────── -->\n <section\n #canvas\n class=\"lm-canvas\"\n [class.lm-canvas--panning]=\"false\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n [attr.aria-label]=\"'logicMap.canvas' | nvbT\"\n tabindex=\"0\"\n data-testid=\"logic-map-canvas\"\n >\n @let view = viewport();\n @let map = layout();\n <div\n class=\"lm-world\"\n [style.transform]=\"'translate(' + view.x + 'px, ' + view.y + 'px) scale(' + view.zoom + ')'\"\n >\n <svg\n class=\"lm-links\"\n [attr.width]=\"map.width + 200\"\n [attr.height]=\"map.height + 200\"\n [style.width.px]=\"map.width + 200\"\n [style.height.px]=\"map.height + 200\"\n aria-hidden=\"true\"\n >\n <defs>\n @for (kind of kinds; track kind) {\n <marker\n [attr.id]=\"'lm-arrow-' + kind\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"7\"\n markerHeight=\"7\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M0 0 L10 5 L0 10 z\" [style.fill]=\"'var(--nvb-logic-' + kind + ')'\" />\n </marker>\n }\n </defs>\n @for (link of map.links; track link.id) {\n @let dimmed = isLinkDimmed(link);\n <g class=\"lm-link\" [class.lm-link--dimmed]=\"dimmed\" [class.lm-link--hot]=\"hoveredLink() === link\">\n <path\n class=\"lm-link__line\"\n [attr.d]=\"link.path\"\n [style.stroke]=\"linkColor(link)\"\n [attr.stroke-dasharray]=\"kindDash[link.kinds[0]] || (link.backward ? '6 4' : null)\"\n [attr.marker-end]=\"'url(#lm-arrow-' + link.kinds[0] + ')'\"\n />\n <path\n class=\"lm-link__hit\"\n [attr.d]=\"link.path\"\n (mouseenter)=\"hoveredLink.set(link)\"\n (mouseleave)=\"hoveredLink.set(null)\"\n (click)=\"select(link.to)\"\n />\n </g>\n }\n </svg>\n\n @if (map.isolatedTop !== null) {\n <div class=\"lm-isolated-heading\" [style.top.px]=\"map.isolatedTop - 30\">\n {{ 'logicMap.isolated' | nvbT }}\n </div>\n }\n\n @for (item of map.nodes; track item.node.id) {\n @let node = item.node;\n @let ruleCount = rulesOf(node.id).length;\n @let nodeIssues = issuesOf(node.id);\n <button\n type=\"button\"\n class=\"lm-node\"\n [attr.data-kind]=\"node.kind\"\n [class.lm-node--selected]=\"state.selectedId() === node.id\"\n [class.lm-node--dimmed]=\"isDimmed(node.id)\"\n [class.lm-node--isolated]=\"item.isolated\"\n [style.left.px]=\"item.x\"\n [style.top.px]=\"item.y\"\n [style.width.px]=\"nodeWidth\"\n [style.height.px]=\"nodeHeight\"\n (click)=\"onNodeClick($event, node.id)\"\n (dblclick)=\"openInBuilder(node)\"\n (mouseenter)=\"hoveredNodeId.set(node.id)\"\n (mouseleave)=\"hoveredNodeId.set(null)\"\n [attr.aria-pressed]=\"state.selectedId() === node.id\"\n [attr.data-testid]=\"'logic-map-node-' + node.name\"\n [title]=\"(node.context ? node.context + ' › ' : '') + node.label\"\n >\n <span class=\"lm-node__icon\" aria-hidden=\"true\">\n @switch (node.kind) {\n @case ('dataSource') { <nvb-icon name=\"database\" /> }\n @case ('variable') { <nvb-icon [svg]=\"icons.functions\" /> }\n @case ('rule') { <nvb-icon name=\"rule\" /> }\n @case ('trigger') { <nvb-icon name=\"bolt\" /> }\n @case ('missing') { <nvb-icon name=\"error\" /> }\n @default { <nvb-icon name=\"edit_note\" /> }\n }\n </span>\n <span class=\"lm-node__text\">\n @if (node.context) {\n <span class=\"lm-node__context\">{{ node.context }}</span>\n }\n <span class=\"lm-node__label\">{{ node.label }}</span>\n <span class=\"lm-node__meta\">\n {{ node.typeLabel }}@if (node.kind === 'element' && node.name !== node.label) {<span class=\"lm-node__name\"> · {{ node.name }}</span>}\n </span>\n </span>\n @if (nodeIssues.length > 0) {\n <span class=\"lm-node__issue\" [attr.data-severity]=\"nodeIssues[0].severity\" [title]=\"nodeIssues[0].message\">\n <nvb-icon [name]=\"nodeIssues[0].severity === 'error' ? 'error' : 'warning'\" />\n </span>\n } @else if (ruleCount > 0) {\n <span class=\"lm-node__count\" [title]=\"ruleCount + ' ' + ('logicMap.rulesOnField' | nvbT)\">{{ ruleCount }}</span>\n }\n </button>\n }\n </div>\n\n @if (map.nodes.length === 0) {\n <div class=\"lm-empty lm-overlay\" data-testid=\"logic-map-empty\">\n <nvb-icon [svg]=\"icons.account_tree\" class=\"lm-empty__icon\" />\n @if (graph().rules.length === 0) {\n <h3>{{ 'logicMap.empty.title' | nvbT }}</h3>\n <p>\n {{\n 'logicMap.empty.text' | nvbT\n }}\n </p>\n } @else {\n <h3>{{ 'logicMap.empty.filtered' | nvbT }}</h3>\n <p>{{ 'logicMap.empty.filteredText' | nvbT }}</p>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"showAllKinds(); setPage('')\">\n {{ 'logicMap.empty.reset' | nvbT }}\n </button>\n }\n @if (!showIsolated() && elementCount() > 0) {\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"setShowIsolated(true)\">\n {{ 'logicMap.empty.showAll' | nvbT }}\n </button>\n }\n </div>\n }\n\n @if (hoveredLink(); as link) {\n <div\n class=\"lm-tooltip lm-overlay\"\n role=\"tooltip\"\n [style.left.px]=\"link.midX * view.zoom + view.x\"\n [style.top.px]=\"link.midY * view.zoom + view.y\"\n >\n <div class=\"lm-tooltip__route\">{{ labelOf(link.from) }} → {{ labelOf(link.to) }}</div>\n @for (rule of linkRules(link); track rule.id) {\n <div class=\"lm-tooltip__rule\">\n <span class=\"lm-dot\" [style.background]=\"'var(--nvb-logic-' + rule.kind + ')'\"></span>\n <span>{{ rule.summary }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"lm-toolbar lm-overlay\" role=\"toolbar\" [attr.aria-label]=\"'logicMap.view' | nvbT\">\n <builder-icon-button icon=\"zoom_out\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.zoomOut' | nvbT\" [title]=\"'logicMap.zoomOut' | nvbT\" (clicked)=\"zoomBy(1 / 1.2)\" testId=\"logic-map-zoom-out\" />\n <button type=\"button\" class=\"lm-toolbar__zoom\" (click)=\"resetZoom()\" [title]=\"'logicMap.zoomReset' | nvbT\" data-testid=\"logic-map-zoom-value\">{{ zoomPercent() }}%</button>\n <builder-icon-button icon=\"zoom_in\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.zoomIn' | nvbT\" [title]=\"'logicMap.zoomIn' | nvbT\" (clicked)=\"zoomBy(1.2)\" testId=\"logic-map-zoom-in\" />\n <builder-icon-button icon=\"fit_screen\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.fit' | nvbT\" [title]=\"'logicMap.fit' | nvbT\" (clicked)=\"fitToScreen()\" testId=\"logic-map-fit\" />\n <span class=\"lm-toolbar__divider\" aria-hidden=\"true\"></span>\n <builder-icon-button icon=\"download\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.download' | nvbT\" [title]=\"'logicMap.download' | nvbT\" (clicked)=\"downloadSvg()\" testId=\"logic-map-download\" />\n </div>\n\n @if (map.nodes.length > 0) {\n <div\n class=\"lm-minimap lm-overlay\"\n [style.width.px]=\"minimapWidth\"\n [style.height.px]=\"minimapHeight\"\n (pointerdown)=\"onMinimapPointer($event)\"\n (pointermove)=\"onMinimapPointer($event)\"\n aria-hidden=\"true\"\n >\n <svg [attr.width]=\"minimapWidth\" [attr.height]=\"minimapHeight\" [style.width.px]=\"minimapWidth\" [style.height.px]=\"minimapHeight\">\n @let scale = minimapScale();\n @for (item of map.nodes; track item.node.id) {\n <rect\n class=\"lm-minimap__node\"\n [class.lm-minimap__node--selected]=\"state.selectedId() === item.node.id\"\n [attr.x]=\"6 + item.x * scale\"\n [attr.y]=\"6 + item.y * scale\"\n [attr.width]=\"nodeWidth * scale\"\n [attr.height]=\"nodeHeight * scale\"\n rx=\"1\"\n />\n }\n @let frame = minimapViewport();\n <rect class=\"lm-minimap__frame\" [attr.x]=\"frame.x\" [attr.y]=\"frame.y\" [attr.width]=\"frame.width\" [attr.height]=\"frame.height\" rx=\"2\" />\n </svg>\n </div>\n }\n\n @if (hiddenKinds().size > 0 && map.nodes.length > 0) {\n <div class=\"lm-filter-note lm-overlay\" role=\"status\" data-testid=\"logic-map-filter-note\">\n <nvb-icon [svg]=\"icons.filter_list\" />\n {{ (hiddenKinds().size === 1 ? 'logicMap.hiddenOne' : 'logicMap.hiddenMany') | nvbT: { count: hiddenKinds().size } }}\n <button type=\"button\" (click)=\"showAllKinds()\">{{ 'logicMap.kinds.all' | nvbT }}</button>\n </div>\n }\n\n <p class=\"lm-hint lm-overlay\">\n {{ 'logicMap.hint' | nvbT }}\n </p>\n </section>\n\n <!-- ── Detail ────────────────────────────────────────────────────────── -->\n @if (selectedNode(); as node) {\n <aside class=\"lm-detail\" data-testid=\"logic-map-detail\" [attr.aria-label]=\"node.label\">\n <header class=\"lm-detail__header\">\n <div class=\"lm-detail__heading\">\n <p class=\"lm-detail__eyebrow\">\n @if (node.context) {\n {{ node.context }} ·\n }\n {{ node.typeLabel || ('logicMap.nodeKind.' + node.kind | nvbT) }}\n </p>\n <h3 class=\"lm-detail__title\">{{ node.label }}</h3>\n @if (node.kind === 'element') {\n <p class=\"lm-detail__meta\">\n <code>{{ node.name }}</code>\n @if (node.pageLabel) {\n · {{ node.pageLabel }}\n }\n </p>\n }\n </div>\n <builder-icon-button icon=\"close\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'logicMap.close' | nvbT\" [title]=\"'logicMap.closeEsc' | nvbT\" (clicked)=\"select(null)\" testId=\"logic-map-detail-close\" />\n </header>\n\n <div class=\"lm-detail__body\">\n @if (node.kind === 'element') {\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm lm-detail__open\" (click)=\"openInBuilder(node)\" data-testid=\"logic-map-open-in-builder\">\n <nvb-icon name=\"edit\" />\n {{ 'logicMap.openInBuilder' | nvbT }}\n </button>\n }\n\n @for (issue of issuesOf(node.id); track $index) {\n <p class=\"lm-callout\" [attr.data-severity]=\"issue.severity\" role=\"note\">\n <nvb-icon [name]=\"issue.severity === 'error' ? 'error' : 'warning'\" />\n <span>{{ issue.message }}</span>\n </p>\n }\n\n <section class=\"lm-detail__section\">\n <h4 class=\"lm-detail__section-title\">{{ 'logicMap.behaviour' | nvbT }}</h4>\n @if (rulesOf(node.id).length === 0) {\n <p class=\"lm-muted\">\n @if (node.kind === 'missing') {\n {{ 'logicMap.behaviour.missing' | nvbT }}\n } @else {\n {{ 'logicMap.behaviour.none' | nvbT }}\n }\n </p>\n } @else {\n <ol class=\"lm-rules\" role=\"list\">\n @for (rule of rulesOf(node.id); track rule.id) {\n <li class=\"lm-rule\" [attr.data-testid]=\"'logic-map-rule-' + rule.property\">\n <span class=\"lm-rule__kind\">\n <svg width=\"18\" height=\"6\" style=\"width: 18px; height: 6px\" aria-hidden=\"true\"><line x1=\"1\" y1=\"3\" x2=\"17\" y2=\"3\" [style.stroke]=\"'var(--nvb-logic-' + rule.kind + ')'\" stroke-width=\"2.5\" stroke-linecap=\"round\" [attr.stroke-dasharray]=\"kindDash[rule.kind] || null\" /></svg>\n {{ 'logicMap.kind.' + rule.kind | nvbT }}\n </span>\n <p class=\"lm-rule__text\">\n @for (segment of rule.segments; track $index) {\n @if (isRef(segment)) {\n <button type=\"button\" class=\"lm-ref\" (click)=\"select(segment.ref)\">{{ segment.label }}</button>\n } @else {\n {{ segment.text }}\n }\n }\n </p>\n @if (rule.expression) {\n <code class=\"lm-rule__code\">{{ rule.expression }}</code>\n }\n </li>\n }\n </ol>\n }\n </section>\n\n @if (trace(); as nodeTrace) {\n <label class=\"lm-check lm-detail__direct\">\n <input type=\"checkbox\" [checked]=\"directOnly()\" (change)=\"directOnly.set($any($event.target).checked)\" />\n <span>{{ 'logicMap.directOnly' | nvbT }}</span>\n </label>\n\n <section class=\"lm-detail__section\">\n <h4 class=\"lm-detail__section-title\">\n {{ 'logicMap.dependsOn' | nvbT }}\n <span class=\"lm-count\">{{ nodeTrace.upstream.length }}</span>\n </h4>\n @if (nodeTrace.upstream.length === 0) {\n <p class=\"lm-muted\">{{ 'logicMap.dependsOn.none' | nvbT }}</p>\n } @else {\n <ul class=\"lm-trace\" role=\"list\">\n @for (item of nodeTrace.upstream; track item.id) {\n <li>\n <button type=\"button\" class=\"lm-trace__item\" (click)=\"select(item.id)\">\n <span class=\"lm-trace__label\">{{ labelOf(item.id) }}</span>\n <span class=\"lm-trace__depth\">{{ depthLabel(item.depth) }}</span>\n </button>\n </li>\n }\n </ul>\n }\n </section>\n\n <section class=\"lm-detail__section\">\n <h4 class=\"lm-detail__section-title\">\n {{ 'logicMap.affects' | nvbT }}\n <span class=\"lm-count\">{{ nodeTrace.downstream.length }}</span>\n </h4>\n @if (nodeTrace.downstream.length === 0) {\n <p class=\"lm-muted\">{{ 'logicMap.affects.none' | nvbT }}</p>\n } @else {\n <ul class=\"lm-trace\" role=\"list\">\n @for (item of nodeTrace.downstream; track item.id) {\n <li>\n <button type=\"button\" class=\"lm-trace__item\" (click)=\"select(item.id)\">\n <span class=\"lm-trace__label\">{{ labelOf(item.id) }}</span>\n <span class=\"lm-trace__depth\">{{ depthLabel(item.depth) }}</span>\n </button>\n </li>\n }\n </ul>\n }\n </section>\n }\n </div>\n </aside>\n }\n </div>\n</nvb-pro-feature-gate>\n",styles:['@charset "UTF-8";:host{--nvb-input-height: 32px;--nvb-logic-visibility: #2a78d6;--nvb-logic-value: #eb6834;--nvb-logic-reset: #eb6834;--nvb-logic-data: #1baf7a;--nvb-logic-required: #eda100;--nvb-logic-validation: #e87ba4;--nvb-logic-locked: #008300;--nvb-logic-action: #4a3aa7;--nvb-logic-critical: #d03b3b;--nvb-logic-warning: #b26b00;display:block;height:100%;min-height:0;overflow:hidden;color:var(--nvb-color-neutral-800);font-size:12px;background:color-mix(in oklab,var(--nvb-color-neutral-050) 84%,var(--nvb-color-neutral-000))}:host-context([data-theme=dark]){--nvb-logic-visibility: #3987e5;--nvb-logic-value: #d95926;--nvb-logic-reset: #d95926;--nvb-logic-data: #199e70;--nvb-logic-required: #c98500;--nvb-logic-validation: #d55181;--nvb-logic-locked: #008300;--nvb-logic-action: #9085e9;--nvb-logic-critical: #e66767;--nvb-logic-warning: #fab219}button:not(.nvb-button){font:inherit;color:inherit}.lm{position:relative;height:100%;min-height:0;display:grid;grid-template-columns:280px minmax(0,1fr);gap:10px;padding:10px;box-sizing:border-box}.lm--with-detail{grid-template-columns:280px minmax(0,1fr) 340px}.lm-side,.lm-detail,.lm-canvas{min-height:0;background:var(--nvb-card-bg, var(--nvb-color-neutral-000));border:1px solid var(--nvb-card-border-color, var(--nvb-color-neutral-200));border-radius:var(--nvb-card-radius, 8px);box-shadow:var(--nvb-card-shadow, none)}.lm-side{display:flex;flex-direction:column;overflow:hidden}.lm-side__header{padding:12px 12px 8px}.lm-side__title{margin:0;font-size:13px;font-weight:600;line-height:18px;color:var(--nvb-color-neutral-900)}.lm-side__meta{margin:2px 0 0;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500);font-variant-numeric:tabular-nums}.lm-side__search{padding:0 12px 10px;border-bottom:1px solid var(--nvb-color-neutral-200)}.lm-side__scroll{flex:1 1 auto;min-height:0;overflow:auto}.lm-section{padding:10px 12px;border-bottom:1px solid var(--nvb-color-neutral-100, var(--nvb-color-neutral-200))}.lm-section:last-child{border-bottom:0}.lm-section__header{display:flex;align-items:center;justify-content:space-between}.lm-section__title{display:flex;align-items:center;gap:6px;margin:0 0 6px;font-size:11px;font-weight:600;line-height:16px;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.lm-section__header .lm-section__title{margin-bottom:6px}.lm-link-button{margin-bottom:6px;padding:0;border:0;background:none;font-size:11px;color:var(--nvb-color-primary-600);cursor:pointer}.lm-link-button:hover{text-decoration:underline}.lm-count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 5px;border-radius:999px;background:var(--nvb-color-neutral-100);color:var(--nvb-color-neutral-600);font-size:10px;font-weight:600;letter-spacing:0;text-transform:none;font-variant-numeric:tabular-nums}.lm-count--issue{background:color-mix(in oklab,var(--nvb-logic-critical) 14%,transparent);color:var(--nvb-logic-critical)}.lm-kinds,.lm-issues,.lm-list,.lm-trace,.lm-rules{margin:0;padding:0;list-style:none}.lm-kind{position:relative;display:flex;align-items:center}.lm-kind__toggle{flex:1 1 auto;display:flex;align-items:center;gap:8px;min-width:0;height:28px;padding:0 6px;border:0;border-radius:var(--nvb-radius-sm, 4px);background:none;cursor:pointer;text-align:left}.lm-kind__toggle:hover{background:var(--nvb-color-neutral-050)}.lm-kind__toggle:focus-visible{outline:none;box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.lm-kind__check{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:1px solid var(--nvb-color-neutral-400);border-radius:3px;font-size:12px;color:var(--nvb-color-neutral-000)}.lm-kind:not(.lm-kind--off) .lm-kind__check{border-color:var(--nvb-color-primary-600);background:var(--nvb-color-primary-600)}.lm-kind__swatch{flex:0 0 auto}.lm-kind__label{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--nvb-color-neutral-800)}.lm-kind__count{flex:0 0 auto;color:var(--nvb-color-neutral-500);font-variant-numeric:tabular-nums}.lm-kind--off .lm-kind__label,.lm-kind--off .lm-kind__swatch{opacity:.45}.lm-kind--empty .lm-kind__label,.lm-kind--empty .lm-kind__count{color:var(--nvb-color-neutral-400)}.lm-kind__only{position:absolute;right:26px;display:none;height:20px;padding:0 6px;border:1px solid var(--nvb-color-neutral-200);border-radius:4px;background:var(--nvb-color-neutral-000);font-size:11px;color:var(--nvb-color-primary-600);cursor:pointer}.lm-kind:hover .lm-kind__only,.lm-kind__only:focus-visible{display:inline-flex;align-items:center}.lm-check{display:flex;align-items:center;gap:8px;min-height:26px;cursor:pointer}.lm-check input{margin:0;accent-color:var(--nvb-color-primary-600)}.lm-field{display:grid;gap:4px;margin-top:8px;color:var(--nvb-color-neutral-600)}.lm-field nvb-select{display:block;width:100%}.lm-issue{display:flex;gap:8px;width:100%;margin:0 0 4px;padding:6px 8px;border:0;border-radius:var(--nvb-radius-sm, 4px);background:var(--nvb-color-neutral-050);line-height:16px;text-align:left;cursor:pointer}.lm-issue:hover{background:var(--nvb-color-neutral-100)}.lm-issue__icon{flex:0 0 auto;margin-top:1px;font-size:14px}.lm-issue[data-severity=error] .lm-issue__icon{color:var(--nvb-logic-critical)}.lm-issue[data-severity=warning] .lm-issue__icon{color:var(--nvb-logic-warning)}.lm-issue__level{font-weight:600}.lm-muted{margin:0;color:var(--nvb-color-neutral-500);line-height:17px}.lm-list__item{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-rows:auto auto;column-gap:8px;width:100%;padding:5px 8px;border:0;border-radius:var(--nvb-radius-sm, 4px);background:none;text-align:left;cursor:pointer}.lm-list__item:hover{background:var(--nvb-color-neutral-050)}.lm-list__item--active{background:var(--nvb-color-primary-050);box-shadow:inset 2px 0 0 var(--nvb-color-primary-600)}.lm-list__label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-weight:500;color:var(--nvb-color-neutral-800)}.lm-list__meta{grid-row:2;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;color:var(--nvb-color-neutral-500)}.lm-list__item .lm-count{grid-row:1/span 2;grid-column:2;align-self:center}.lm-canvas{position:relative;overflow:hidden;cursor:grab;touch-action:none;background-color:var(--nvb-color-neutral-000);background-image:radial-gradient(var(--nvb-color-neutral-200) 1px,transparent 1px);background-size:16px 16px;outline:none}.lm-canvas:active{cursor:grabbing}.lm-canvas:focus-visible{box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.lm-world{position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform}.lm-links{position:absolute;top:0;left:0;max-width:none;max-height:none;overflow:visible}svg{max-width:none}.lm-link__line{fill:none;stroke-width:2;stroke-linecap:round;transition:opacity .12s ease}.lm-link__hit{fill:none;stroke:transparent;stroke-width:14;pointer-events:stroke;cursor:pointer}.lm-link--hot .lm-link__line{stroke-width:3}.lm-link--dimmed .lm-link__line{opacity:.1}.lm-node{position:absolute;display:flex;align-items:center;gap:10px;box-sizing:border-box;padding:0 10px;border:1px solid var(--nvb-color-neutral-300);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 1px 2px #0000000f;text-align:left;cursor:pointer;transition:opacity .12s ease,border-color .12s ease,box-shadow .12s ease}.lm-node:hover{border-color:var(--nvb-color-primary-400, var(--nvb-color-primary-500))}.lm-node:focus-visible{outline:none;box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.lm-node--selected{border-color:var(--nvb-color-primary-600);box-shadow:0 0 0 2px color-mix(in oklab,var(--nvb-color-primary-500) 30%,transparent),0 4px 12px #0000001a}.lm-node--dimmed{opacity:.28}.lm-node--isolated{border-style:dashed;box-shadow:none}.lm-node__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;background:var(--nvb-color-neutral-100);color:var(--nvb-color-neutral-600);font-size:16px}.lm-node[data-kind=dataSource] .lm-node__icon{background:color-mix(in oklab,var(--nvb-logic-data) 16%,transparent);color:var(--nvb-color-neutral-800)}.lm-node[data-kind=variable] .lm-node__icon,.lm-node[data-kind=rule] .lm-node__icon,.lm-node[data-kind=trigger] .lm-node__icon{background:color-mix(in oklab,var(--nvb-logic-action) 14%,transparent);color:var(--nvb-color-neutral-800)}.lm-node[data-kind=missing]{border-style:dashed;border-color:var(--nvb-logic-critical)}.lm-node[data-kind=missing] .lm-node__icon{background:color-mix(in oklab,var(--nvb-logic-critical) 12%,transparent);color:var(--nvb-logic-critical)}.lm-node__text{flex:1 1 auto;display:grid;min-width:0}.lm-node__context{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10.5px;font-weight:600;line-height:14px;color:var(--nvb-color-primary-700)}.lm-node__label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:600;line-height:18px;color:var(--nvb-color-neutral-900)}.lm-node__meta{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500)}.lm-node__name{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:10.5px}.lm-node[data-kind=missing] .lm-node__meta{color:var(--nvb-logic-critical)}.lm-node__count{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:18px;padding:0 6px;border-radius:999px;background:var(--nvb-color-neutral-100);color:var(--nvb-color-neutral-600);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.lm-node__issue{flex:0 0 auto;font-size:16px}.lm-node__issue[data-severity=error]{color:var(--nvb-logic-critical)}.lm-node__issue[data-severity=warning]{color:var(--nvb-logic-warning)}.lm-isolated-heading{position:absolute;left:0;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.lm-overlay{cursor:default}.lm-toolbar{position:absolute;top:10px;right:10px;display:flex;align-items:center;gap:2px;padding:3px;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 2px 8px #00000014}.lm-toolbar__zoom{min-width:48px;height:26px;padding:0 6px;border:0;border-radius:4px;background:none;font-size:12px;font-variant-numeric:tabular-nums;color:var(--nvb-color-neutral-700);cursor:pointer}.lm-toolbar__zoom:hover{background:var(--nvb-color-neutral-050)}.lm-toolbar__divider{width:1px;height:18px;margin:0 3px;background:var(--nvb-color-neutral-200)}.lm-minimap{position:absolute;right:10px;bottom:10px;overflow:hidden;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:color-mix(in oklab,var(--nvb-color-neutral-000) 92%,transparent);box-shadow:0 2px 8px #00000014;cursor:pointer}.lm-minimap svg{display:block}.lm-minimap__node{fill:var(--nvb-color-neutral-300)}.lm-minimap__node--selected{fill:var(--nvb-color-primary-600)}.lm-minimap__frame{fill:color-mix(in oklab,var(--nvb-color-primary-500) 10%,transparent);stroke:var(--nvb-color-primary-600);stroke-width:1.5}.lm-filter-note{position:absolute;top:10px;left:10px;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 2px 8px #00000014;color:var(--nvb-color-neutral-700)}.lm-filter-note nvb-icon{font-size:15px;color:var(--nvb-color-neutral-500)}.lm-filter-note button{padding:0;border:0;background:none;color:var(--nvb-color-primary-600);cursor:pointer}.lm-filter-note button:hover{text-decoration:underline}.lm-hint{position:absolute;left:12px;bottom:8px;margin:0;font-size:11px;color:var(--nvb-color-neutral-500);pointer-events:none}.lm-tooltip{position:absolute;z-index:2;max-width:340px;padding:8px 10px;border:1px solid var(--nvb-color-neutral-200);border-radius:6px;background:var(--nvb-color-neutral-000);box-shadow:0 6px 20px #00000024;transform:translate(-50%,calc(-100% - 10px));pointer-events:none;line-height:17px}.lm-tooltip__route{margin-bottom:4px;font-weight:600;color:var(--nvb-color-neutral-900)}.lm-tooltip__rule{display:flex;gap:6px;color:var(--nvb-color-neutral-700)}.lm-dot{flex:0 0 auto;width:8px;height:8px;margin-top:5px;border-radius:999px}.lm-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;text-align:center}.lm-empty__icon{font-size:32px;color:var(--nvb-color-neutral-400)}.lm-empty h3{margin:4px 0 0;font-size:14px;font-weight:600;color:var(--nvb-color-neutral-900)}.lm-empty p{max-width:420px;margin:0 0 6px;font-size:13px;line-height:19px;color:var(--nvb-color-neutral-600)}.lm-detail{display:flex;flex-direction:column;overflow:hidden}.lm-detail__header{display:flex;align-items:flex-start;gap:8px;padding:12px 8px 10px 14px;border-bottom:1px solid var(--nvb-color-neutral-200)}.lm-detail__heading{flex:1 1 auto;min-width:0}.lm-detail__eyebrow{margin:0;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500)}.lm-detail__title{margin:0;font-size:14px;font-weight:600;line-height:20px;color:var(--nvb-color-neutral-900);overflow-wrap:anywhere}.lm-detail__meta{margin:2px 0 0;font-size:11px;color:var(--nvb-color-neutral-500)}.lm-detail__meta code,.lm-rule__code{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)}.lm-detail__body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px 16px}.lm-detail__open{display:inline-flex;align-items:center;gap:6px;margin-bottom:12px}.lm-callout{display:flex;gap:8px;margin:0 0 10px;padding:8px 10px;border-radius:6px;line-height:17px}.lm-callout[data-severity=error]{background:color-mix(in oklab,var(--nvb-logic-critical) 10%,transparent)}.lm-callout[data-severity=error] nvb-icon{color:var(--nvb-logic-critical)}.lm-callout[data-severity=warning]{background:color-mix(in oklab,var(--nvb-logic-warning) 12%,transparent)}.lm-callout[data-severity=warning] nvb-icon{color:var(--nvb-logic-warning)}.lm-callout nvb-icon{flex:0 0 auto;margin-top:1px;font-size:15px}.lm-detail__section{margin-top:14px}.lm-detail__section:first-of-type{margin-top:0}.lm-detail__section-title{display:flex;align-items:center;gap:6px;margin:0 0 6px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.lm-detail__direct{margin-top:14px;padding-top:10px;border-top:1px solid var(--nvb-color-neutral-200)}.lm-rule{padding:8px 0;border-top:1px solid var(--nvb-color-neutral-100, var(--nvb-color-neutral-200))}.lm-rule:first-child{border-top:0;padding-top:0}.lm-rule__kind{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--nvb-color-neutral-500)}.lm-rule__text{margin:3px 0 0;font-size:13px;line-height:19px;color:var(--nvb-color-neutral-900);overflow-wrap:anywhere}.lm-ref{display:inline;padding:0 4px;border:0;border-radius:4px;background:var(--nvb-color-primary-050);color:var(--nvb-color-primary-700);font-weight:500;cursor:pointer}.lm-ref:hover{text-decoration:underline}.lm-rule__code{display:block;margin-top:4px;padding:4px 6px;border-radius:4px;background:var(--nvb-color-neutral-050);font-size:11px;line-height:16px;color:var(--nvb-color-neutral-600);white-space:pre-wrap;overflow-wrap:anywhere}.lm-trace__item{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:5px 8px;border:0;border-radius:4px;background:none;text-align:left;cursor:pointer}.lm-trace__item:hover{background:var(--nvb-color-neutral-050)}.lm-trace__label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--nvb-color-neutral-800)}.lm-trace__depth{flex:0 0 auto;font-size:11px;color:var(--nvb-color-neutral-500)}@media(max-width:1100px){.lm,.lm--with-detail{grid-template-columns:240px minmax(0,1fr)}.lm-detail{position:absolute;top:10px;right:10px;bottom:10px;width:min(340px,100% - 20px);z-index:3}}@media(max-width:720px){.lm,.lm--with-detail{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(320px,1fr)}.lm-side{max-height:40vh}}@media(prefers-reduced-motion:reduce){.lm-node,.lm-link__line{transition:none}}\n']}]}],ctorParameters:()=>[],propDecorators:{canvasRef:[{type:_.ViewChild,args:["canvas",{isSignal:true}]}]}});function K(l,e,n){return Math.min(n,Math.max(e,l))}const ie="logicMap";function je(l,e){return{id:ie,title:"Logic map",description:"How the open view works: every condition (visible, required, disabled, read-only, cleared), calculated value, validator, event action, data source and variable, as rules on elements and links between them, read back in plain words. Read it before explaining or changing the behaviour of a form, and to find broken references and calculation loops.",query:{element:"Optional element name or path. Returns that element with its rules and everything upstream (what it depends on) and downstream (what it changes).",kind:"Optional link type to keep: visibility, required, locked, value, reset, validation, data, action."},available:()=>e.proAccess()==="ready",unavailableReason:()=>"The logic map plugin needs an active NGX View Builder license key in the builder settings.",read:n=>{l.refresh();const t=l.graph();const o=String(n["element"]??"").trim();const i=String(n["kind"]??"").trim();if(o)return Ue(t,o);return Ge(t,i)},commands:[{name:"show",description:"Opens the Logic map tab for the person with one element selected and its chain highlighted. Use it while explaining how a field works.",params:{element:'Element name or path, e.g. "income" or "billing.city".'},example:{element:"income"},run:n=>{l.refresh();const t=oe(l.graph(),String(n["element"]??""));if(!t)throw new Error(`No element '${String(n["element"]??"")}' in this view.`);l.show(t.id);return{shown:t.name}}}]}}function oe(l,e){const n=e.trim();return l.nodes.find(t=>t.id===n)??l.nodes.find(t=>t.id===me(n))??l.nodes.find(t=>t.kind==="element"&&t.name.split(".").pop()===n)??l.nodes.find(t=>t.name===n)??null}function ae(l){return{id:l.id,kind:l.kind,name:l.name,label:l.label,...l.type?{type:l.type}:{},...l.pageName?{page:l.pageName}:{},...l.parent?{parent:l.parent}:{},...l.context?{context:l.context}:{}}}function Ge(l,e){const n=e?l.rules.filter(i=>i.kind===e):l.rules;const t=e?l.edges.filter(i=>i.kind===e):l.edges;const o=new Set;n.forEach(i=>{o.add(i.owner);i.sources.forEach(a=>o.add(a));i.targets.forEach(a=>o.add(a))});return{summary:{elements:l.nodes.filter(i=>i.kind==="element").length,withLogic:o.size,rules:n.length,links:t.length,issues:l.issues.length},nodes:l.nodes.filter(i=>o.has(i.id)).map(ae),rules:n.map(i=>({owner:i.owner,kind:i.kind,property:i.property,summary:i.summary,...i.expression?{expression:i.expression}:{},...i.sources.length?{reads:i.sources}:{},...i.targets.length?{changes:i.targets}:{}})),issues:l.issues}}function Ue(l,e){const n=oe(l,e);if(!n)throw new Error(`No element '${e}' in this view. Read the logic map without a query to see every name.`);const t=G(l,n.id);const o=i=>l.nodes.find(a=>a.id===i)?.label??i;return{element:ae(n),rules:l.rules.filter(i=>i.owner===n.id).map(i=>({kind:i.kind,property:i.property,summary:i.summary,expression:i.expression})),dependsOn:(t?.upstream??[]).map(i=>({id:i.id,label:o(i.id),steps:i.depth})),affects:(t?.downstream??[]).map(i=>({id:i.id,label:o(i.id),steps:i.depth})),issues:l.issues.filter(i=>i.nodeIds.includes(n.id))}}const A={"logicMap.affects":"Affects","logicMap.affects.none":"Nothing else in the view reacts to it.","logicMap.behaviour":"Rules on this element","logicMap.behaviour.missing":"Something in the view refers to this name, but the view has no element, data source or variable called that.","logicMap.behaviour.none":"None. Other elements can still depend on it.","logicMap.canvas":"Logic map canvas","logicMap.clearSearch":"Clear search","logicMap.close":"Close","logicMap.closeEsc":"Close (Esc)","logicMap.dependsOn":"Depends on","logicMap.dependsOn.none":"Nothing. It changes only when the user does.","logicMap.depth.direct":"Direct","logicMap.depth.steps":"{count} steps away","logicMap.directOnly":"Highlight direct links only","logicMap.download":"Download as SVG image","logicMap.empty.filtered":"Nothing to show with these filters","logicMap.empty.filteredText":"Every link type that has rules is switched off, or the page has none.","logicMap.empty.reset":"Reset filters","logicMap.empty.showAll":"Show every element","logicMap.empty.text":"Conditions, calculated values, events and data sources show up here as soon as an element has them. Add one under Properties → Logic or Events.","logicMap.empty.title":"No logic in this view yet","logicMap.fit":"Fit to screen (F)","logicMap.hiddenMany":"{count} link types hidden","logicMap.hiddenOne":"{count} link type hidden","logicMap.hint":"Drag to move · Ctrl + scroll to zoom · Double-click a field to edit it","logicMap.isolated":"Without logic","logicMap.issue.error":"Error","logicMap.issue.warning":"Warning","logicMap.issues":"Needs attention","logicMap.kind.action":"Runs an action","logicMap.kind.data":"Loads data","logicMap.kind.locked":"Disables or locks","logicMap.kind.required":"Makes required","logicMap.kind.reset":"Clears a value","logicMap.kind.validation":"Validates","logicMap.kind.value":"Calculates or sets a value","logicMap.kind.visibility":"Shows or hides","logicMap.kinds":"Link types","logicMap.kinds.all":"Show all","logicMap.kinds.only":"Only","logicMap.meta.connected":"connected","logicMap.meta.fields":"elements in the view","logicMap.meta.rules":"rules","logicMap.nodeKind.dataSource":"Data source","logicMap.nodeKind.element":"Field","logicMap.nodeKind.missing":"Not in this view","logicMap.nodeKind.rule":"View rule","logicMap.nodeKind.trigger":"Trigger","logicMap.nodeKind.variable":"Variable","logicMap.openInBuilder":"Edit in builder","logicMap.page":"Page","logicMap.page.all":"All pages","logicMap.results":"Results","logicMap.results.none":"Nothing matches that name.","logicMap.rulesOnField":"rules on this element","logicMap.search":"Find a field, data source or variable","logicMap.show":"Show","logicMap.showIsolated":"Elements without logic","logicMap.sidebar":"Logic map filters","logicMap.title":"Logic map","logicMap.view":"View","logicMap.withLogic":"With rules of their own","logicMap.withLogic.none":"No element has rules yet.","logicMap.zoomIn":"Zoom in (+)","logicMap.zoomOut":"Zoom out (-)","logicMap.zoomReset":"Actual size (0)","tab.logicMap":"Logic map","tab.logicMap.description":"How the fields depend on each other"};const le="ngx-view-builder-logic-map";const re="logicMap";function Xe(){ue(A);return be([Ie({featurePacks:[{id:le,title:"Logic map",capabilities:[re],builderTabs:[{id:E,label:"tab.logicMap",component:te,order:360,icon:F.account_tree,description:"tab.logicMap.description"}],uiDictionaries:{en:{...ce,...A}}}]}),fe(()=>{$(ze).register(je($(O),$(Me)))})])}export{O as LogicMapState,ie as NGX_VIEW_BUILDER_LOGIC_MAP_AI_ID,re as NGX_VIEW_BUILDER_LOGIC_MAP_CAPABILITY,le as NGX_VIEW_BUILDER_LOGIC_MAP_FEATURE_PACK_ID,E as NGX_VIEW_BUILDER_LOGIC_MAP_TAB_ID,A as NGX_VIEW_BUILDER_LOGIC_MAP_UI_EN,Xe as provideNgxViewBuilderLogicMap};
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngxviewbuilder/plugin-logic-map",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.13.1",
|
|
4
|
+
"description": "Logic map plugin for the NGX View Builder designer: every condition, calculation, event and data source of a view on one canvas, so anyone can see how a large form works.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"homepage": "https://ngxviewbuilder.io",
|
|
7
|
+
"author": "NGX View Builder (https://ngxviewbuilder.io)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ngxviewbuilder/ngx-view-builder-community.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ngxviewbuilder/ngx-view-builder-community/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"angular",
|
|
17
|
+
"view-builder",
|
|
18
|
+
"form-builder",
|
|
19
|
+
"logic",
|
|
20
|
+
"dependency-graph",
|
|
21
|
+
"plugin",
|
|
22
|
+
"low-code"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@angular/common": "^22.0.0",
|
|
29
|
+
"@angular/core": "^22.0.0",
|
|
30
|
+
"@ngxviewbuilder/designer": "^0.13.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"tslib": "^2.6.0",
|
|
34
|
+
"@ngxviewbuilder/plugin-shared": "^0.13.0"
|
|
35
|
+
},
|
|
36
|
+
"module": "fesm2022/ngxviewbuilder-plugin-logic-map.mjs",
|
|
37
|
+
"typings": "types/ngxviewbuilder-plugin-logic-map.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
"./package.json": {
|
|
40
|
+
"default": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./types/ngxviewbuilder-plugin-logic-map.d.ts",
|
|
44
|
+
"default": "./fesm2022/ngxviewbuilder-plugin-logic-map.mjs"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"type": "module"
|
|
6
49
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { EnvironmentProviders } from "@angular/core";
|
|
3
|
+
import { IViewLogicGraph } from "@ngxviewbuilder/plugin-shared";
|
|
4
|
+
import { INgxViewBuilderAiPlugin, LicenseService } from "@ngxviewbuilder/designer";
|
|
5
|
+
export declare const NGX_VIEW_BUILDER_LOGIC_MAP_FEATURE_PACK_ID = "ngx-view-builder-logic-map";
|
|
6
|
+
export declare const NGX_VIEW_BUILDER_LOGIC_MAP_CAPABILITY = "logicMap";
|
|
7
|
+
/**
|
|
8
|
+
* Adds the Logic map tab to the designer: every condition, calculation, event and data
|
|
9
|
+
* source of the open view on one canvas. Needs an active license key in the builder
|
|
10
|
+
* settings; without one the tab explains how to turn it on.
|
|
11
|
+
*/
|
|
12
|
+
export declare function provideNgxViewBuilderLogicMap(): EnvironmentProviders;
|
|
13
|
+
export declare const NGX_VIEW_BUILDER_LOGIC_MAP_TAB_ID = "logicMap";
|
|
14
|
+
/**
|
|
15
|
+
* The logic graph of the view the builder has open, kept current as the view changes.
|
|
16
|
+
* Shared by the tab and the AI plugin, so an AI client and the person look at the same
|
|
17
|
+
* picture, and an AI client can point the person at a field.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LogicMapState {
|
|
20
|
+
private readonly structureService;
|
|
21
|
+
private readonly api;
|
|
22
|
+
private readonly eventService;
|
|
23
|
+
private readonly elementFocus;
|
|
24
|
+
private readonly i18n;
|
|
25
|
+
private readonly revision;
|
|
26
|
+
private refreshScheduled;
|
|
27
|
+
/** The node the person (or an AI client) picked on the map. */
|
|
28
|
+
readonly selectedId: import("@angular/core").WritableSignal<string>;
|
|
29
|
+
readonly graph: import("@angular/core").Signal<IViewLogicGraph>;
|
|
30
|
+
constructor();
|
|
31
|
+
refresh(): void;
|
|
32
|
+
/** Opens the logic map with one field selected. */
|
|
33
|
+
show(nodeId: string): void;
|
|
34
|
+
/** Back to the canvas with the element selected, so its properties are one click away. */
|
|
35
|
+
openInBuilder(path: string): void;
|
|
36
|
+
/** Bursts of property changes (a paste, an AI batch) analyse the view once. */
|
|
37
|
+
private scheduleRefresh;
|
|
38
|
+
private externalNames;
|
|
39
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<LogicMapState, never>;
|
|
40
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
41
|
+
}
|
|
42
|
+
export declare const NGX_VIEW_BUILDER_LOGIC_MAP_AI_ID = "logicMap";
|
|
43
|
+
/**
|
|
44
|
+
* The logic map for AI clients: the same graph the person sees, in a compact shape, plus
|
|
45
|
+
* a command to put a field on the person's screen while explaining it.
|
|
46
|
+
*/
|
|
47
|
+
declare function createLogicMapAiPlugin(state: LogicMapState, license: LicenseService): INgxViewBuilderAiPlugin;
|
|
48
|
+
/**
|
|
49
|
+
* Every text of the Logic map in English, by key. To show the map in another language,
|
|
50
|
+
* give the same keys in that language, e.g.
|
|
51
|
+
* `provideNgxViewBuilderUiTranslations({ lt: { 'logicMap.title': 'Logikos žemėlapis' } })`.
|
|
52
|
+
* The sentences about the rules themselves come from NGX_VIEW_BUILDER_SHARED_UI_EN in
|
|
53
|
+
* `@ngxviewbuilder/plugin-shared`. Placeholders in braces stay as they are.
|
|
54
|
+
*/
|
|
55
|
+
export declare const NGX_VIEW_BUILDER_LOGIC_MAP_UI_EN: Record<string, string>;
|