@ngxviewbuilder/plugin-docs 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,111 @@
1
- # Temporary Holding Version
1
+ # @ngxviewbuilder/plugin-docs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Field documentation for the [NGX View Builder](https://ngxviewbuilder.io) designer.
4
+
5
+ - Every element gets a **Documentation** section in the Properties panel: rich text about what the field means, who fills it in and where the value goes.
6
+ - The text is **not** written into the view JSON. It is kept in its own document, keyed by the view code and the element names, and saved through a storage you choose (your server, typically).
7
+ - A **Documentation** tab lists the documented elements, each as a small table: the element name across the top, the description over both columns, and under it the rows added with **Add a row**, as name and value. A row is either what the view's logic says (required rule, logic, validation, data source, events, in plain words) or one of the element's own properties (placeholder, options, minimum and so on). Rows are picked per element and saved with the documentation, so long logic stays readable and every element shows only what matters for it.
8
+ - A counter shows how many elements are documented out of how many, and **Document an element** picks one that has no description yet.
9
+ - **Copy for Confluence** puts the page on the clipboard as plain semantic HTML, which Confluence, Notion, Google Docs and Word keep when pasted. Markdown, an HTML page and the raw documentation JSON are one click away too.
10
+ - AI clients connected over MCP read the documentation and the generated table (`nvb_read_plugin` with `documentation`) and write descriptions (`setDescription`).
11
+
12
+ Needs an active NGX View Builder license key in the builder settings (`licenseKey`).
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm i @ngxviewbuilder/plugin-docs
18
+ ```
19
+
20
+ ```ts
21
+ import { provideNgxViewBuilderDocs, createHttpDocsStorage } from '@ngxviewbuilder/plugin-docs';
22
+
23
+ bootstrapApplication(AppComponent, {
24
+ providers: [
25
+ provideNgxViewBuilderRuntime(),
26
+ provideNgxViewBuilderDocs({
27
+ // GET /api/view-docs/{viewCode} reads, PUT /api/view-docs/{viewCode} saves the JSON.
28
+ storage: createHttpDocsStorage('/api/view-docs'),
29
+ }),
30
+ ],
31
+ });
32
+ ```
33
+
34
+ With Angular's `HttpClient` (interceptors and auth included), pass a function; it runs in the injection context:
35
+
36
+ ```ts
37
+ provideNgxViewBuilderDocs({
38
+ storage: () => {
39
+ const http = inject(HttpClient);
40
+ return {
41
+ load: (viewCode) => http.get<INgxViewBuilderViewDocs | null>(`/api/view-docs/${viewCode}`),
42
+ save: (docs) => http.put(`/api/view-docs/${docs.viewCode}`, docs),
43
+ };
44
+ },
45
+ // Optional: the id your app already uses for the view.
46
+ viewCode: (structure) => myCurrentViewId(),
47
+ });
48
+ ```
49
+
50
+ Without a storage the documentation stays in the browser's local storage, which is only good for trying it out; the tab says so.
51
+
52
+ ## The documentation file
53
+
54
+ ```json
55
+ {
56
+ "format": "ngx-view-builder-docs",
57
+ "version": 1,
58
+ "viewCode": "view-k3x9d2m1",
59
+ "title": "Business loan application",
60
+ "summary": "<p>Filled in by the applicant; reviewed by the credit team.</p>",
61
+ "updatedAt": "2026-10-12T09:39:29.617Z",
62
+ "elements": {
63
+ "vatNumber": {
64
+ "description": "<p>VAT number of the applicant company.</p>",
65
+ "rows": ["required", "validation", "prop:placeholder"],
66
+ "updatedAt": "2026-10-12T09:39:29.617Z"
67
+ },
68
+ "billing.city": { "description": "<p>City of the billing address.</p>" }
69
+ }
70
+ }
71
+ ```
72
+
73
+ When the host gives no `viewCode`, the plugin uses `settings.viewCode` from the view and writes a new one there the first time something is documented. That short code is the only thing the plugin adds to the view.
74
+
75
+ ## Translating
76
+
77
+ The Documentation tab, the Properties panel section and the table you copy or download all
78
+ follow the builder's language, rule sentences included. The English texts are exported as
79
+ dictionaries to hand to a translator:
80
+
81
+ - `NGX_VIEW_BUILDER_DOCS_UI_EN` from `@ngxviewbuilder/plugin-docs`: the tab, the panel, the
82
+ messages and the exported table (headings, "Yes"/"No", the summary line).
83
+ - `NGX_VIEW_BUILDER_SHARED_UI_EN` from `@ngxviewbuilder/plugin-shared`: the rule sentences
84
+ and the license notice, shared with the Logic map.
85
+
86
+ ```ts
87
+ provideNgxViewBuilderUiTranslations({
88
+ lt: {
89
+ 'docs.copyConfluence': 'Kopijuoti į Confluence',
90
+ 'docs.progress': 'Aprašyta {documented} iš {total} elementų',
91
+ 'logic.requireIf.when': 'Privalomas, kai',
92
+ },
93
+ });
94
+ ```
95
+
96
+ A key you leave out stays in English. Keep the words in braces, such as `{total}`, in the
97
+ translation. Property rows use the builder's own `property.<key>` names, so they read the
98
+ same as in the Properties panel. Dates in the export are written the way the builder's
99
+ language writes them.
100
+
101
+ ## Requirements
102
+
103
+ Angular 22 or newer and `@ngxviewbuilder/designer` at a matching version.
104
+
105
+ ## Documentation
106
+
107
+ https://ngxviewbuilder.io
108
+
109
+ ## License
110
+
111
+ Terms in [LICENSE.md](./LICENSE.md).
@@ -0,0 +1,20 @@
1
+ import*as c from"@angular/core";import{ChangeDetectionStrategy as R,Component as $,ElementRef as ke,Injectable as Te,InjectionToken as Se,computed as y,inject as b,input as Q,makeEnvironmentProviders as Ce,provideEnvironmentInitializer as De,signal as g}from"@angular/core";import{BuilderIconButton as Z,BuilderSearchInput as ee,CreatorStructureService as Le,ElementFocusService as Oe,EventService as Ie,HeaderTabEnum as Ne,LicenseService as Ee,MarkupSecurityService as Me,NgxViewBuilderAiPluginRegistryService as ze,NgxViewBuilderApiService as P,NvbIcon as k,NvbRichTextEditor as L,UiTranslationService as te,provideNgxViewBuilderExtensions as Re}from"@ngxviewbuilder/designer";import{NGX_VIEW_BUILDER_SHARED_UI_EN as $e,NVB_PLUGIN_ICONS as V,NvbPluginI18n as ne,NvbProFeatureGate as O,NvbTranslatePipe as T,analyzeViewLogic as Pe,elementNodeId as Ve,englishTranslate as I,humanize as oe,plainText as w,registerPluginEnglish as Ae,segmentsToText as re}from"@ngxviewbuilder/plugin-shared";import{firstValueFrom as Fe,isObservable as Be}from"rxjs";const ae="nvb-docs:";function se(){return{load(t){try{const e=localStorage.getItem(`${ae}${t}`);return e?JSON.parse(e):null}catch{return null}},save(t){localStorage.setItem(`${ae}${t.viewCode}`,JSON.stringify(t))}}}function je(t,e={}){const n=a=>`${t.replace(/\/+$/,"")}/${encodeURIComponent(a)}`;const o=async(a,s)=>{const r={Accept:"application/json",...await e.headers?.()??{}};return fetch(n(a),{...s,headers:{...r,...s.headers},credentials:e.withCredentials?"include":"same-origin"})};return{async load(a){const s=await o(a,{method:"GET"});if(s.status===404||s.status===204)return null;if(!s.ok)throw new A("load",s.status);return await s.json()},async save(a){const s=await o(a.viewCode,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)});if(!s.ok)throw new A("save",s.status)}}}var A=class extends Error{operation;status;constructor(t,e){super(`${t==="load"?"Loading":"Saving"} documentation failed (${e}).`);this.operation=t;this.status=e;this.name="NgxViewBuilderDocsHttpError"}};const F=new Se("NGX_VIEW_BUILDER_DOCS_CONFIG");const B="documentation";const He=800;var S=class G{config=b(F,{optional:true})??{};structureService=b(Le);api=b(P);eventService=b(Ie);markupSecurity=b(Me);storage=(typeof this.config.storage==="function"?this.config.storage():this.config.storage)??se();revision=g(0,...ngDevMode?[{debugName:"revision"}]:[]);loadedCode=null;loadToken=0;saveTimer=null;dirty=false;refreshScheduled=false;loadFailedFor=null;docs=g(null,...ngDevMode?[{debugName:"docs"}]:[]);saveState=g("idle",...ngDevMode?[{debugName:"saveState"}]:[]);saveError=g("",...ngDevMode?[{debugName:"saveError"}]:[]);lastSavedAt=g(null,...ngDevMode?[{debugName:"lastSavedAt"}]:[]);browserOnly=!this.config.storage;i18n=b(ne);storageLabel=y(()=>this.storage.label||this.i18n.t(this.browserOnly?"docs.storage.browser":"docs.storage.server"),...ngDevMode?[{debugName:"storageLabel"}]:[]);viewCode=g(null,...ngDevMode?[{debugName:"viewCode"}]:[]);graph=y(()=>{this.revision();return Pe(this.structureService.getStructure()??{},{externalNames:this.externalNames(),translate:this.i18n.t})},...ngDevMode?[{debugName:"graph"}]:[]);viewTitle=y(()=>{this.revision();const e=this.structureService.getStructure();const n=w(e?.header?.label);const o=e?.settings??{};const a=w(o["dialogTitle"]);const s=this.graph().pages[0]?.label;return n||a||s||this.i18n.t("docs.untitledView")},...ngDevMode?[{debugName:"viewTitle"}]:[]);errorText(e,n){if(e instanceof A)return this.i18n.t(e.operation==="load"?"docs.error.loadStatus":"docs.error.saveStatus",{status:e.status});return e instanceof Error&&e.message?e.message:this.i18n.t(n)}constructor(){this.api.onStructureChanged.add(()=>this.scheduleRefresh());this.eventService.afterPropertyChanged.add(()=>this.scheduleRefresh())}ensureLoaded(){let e=this.resolveViewCode(false);const n=this.docs();if(!e&&n&&(this.dirty||this.saveTimer))e=this.stampViewCode(n.viewCode);this.viewCode.set(e);if(e===this.loadedCode)return;this.loadedCode=e;if(!e){this.docs.set(null);this.saveState.set("idle");return}this.load(e)}descriptionOf(e){return this.docs()?.elements[e]?.description??""}setDescription(e,n){const o=String(e??"").trim();if(!o)return;const a=this.clean(n);const s=this.ensureDocs();const r={...s.elements};if(a){if(r[o]?.description===a)return;r[o]={description:a,...r[o]?.rows?.length?{rows:[...r[o].rows]}:{},updatedAt:new Date().toISOString()}}else if(r[o])delete r[o];else return;this.commit({...s,elements:r})}setRows(e,n){const o=String(e??"").trim();const a=this.docs();const s=a?.elements[o];if(!a||!s)return;const r=Array.from(new Set(n.map(l=>String(l??"").trim()).filter(Boolean)));if(JSON.stringify(r)===JSON.stringify(s.rows??[]))return;const d={...s,updatedAt:new Date().toISOString()};if(r.length>0)d.rows=r;else delete d.rows;this.commit({...a,elements:{...a.elements,[o]:d}})}rowsOf(e){return this.docs()?.elements[e]?.rows??[]}definitionLookup(){let e;return n=>{if(e===void 0)try{e=this.api.getStructure()??null}catch{e=null}const[o,...a]=String(n??"").split(".");let s=e?.elements?.[o]??null;for(const r of a){if(!s)return null;const d=s["template"];const l=s["columns"];s=d?.[r]??(Array.isArray(l)?l.find(h=>h?.["name"]===r)??null:null)}return s}}removeDescription(e){this.setDescription(e,"")}setSummary(e){const n=this.ensureDocs();const o=this.clean(e);if((n.summary??"")===o)return;this.commit({...n,summary:o||void 0})}setTitle(e){const n=this.ensureDocs();const o=String(e??"").trim();if((n.title??"")===o)return;this.commit({...n,title:o||void 0})}async saveNow(){if(this.saveTimer){clearTimeout(this.saveTimer);this.saveTimer=null}if(!this.docs())return;if(this.loadFailedFor&&this.loadFailedFor===this.docs()?.viewCode){await this.load(this.loadFailedFor);if(this.loadFailedFor){this.saveState.set("error");this.saveError.set(this.i18n.t("docs.error.unreadable"));return}}const e=this.docs();this.saveState.set("saving");this.saveError.set("");try{await ie(this.storage.save(e));this.dirty=false;this.lastSavedAt.set(new Date);this.saveState.set("saved")}catch(n){this.saveState.set("error");this.saveError.set(this.errorText(n,"docs.error.save"))}}refresh(){this.revision.update(e=>e+1);this.ensureLoaded()}async load(e){const n=++this.loadToken;this.saveState.set("loading");this.saveError.set("");try{const o=await ie(this.storage.load(e));if(n!==this.loadToken)return;this.loadFailedFor=null;const a=this.normalize(o,e);const s=this.docs();this.docs.set(this.dirty&&s?.viewCode===e?{...a,...s,elements:{...a.elements,...s.elements}}:a);this.saveState.set(this.dirty?"saving":"idle");if(this.dirty&&!this.saveTimer)this.scheduleSave()}catch(o){if(n!==this.loadToken)return;this.loadFailedFor=e;if(!this.dirty)this.docs.set(this.normalize(null,e));this.saveState.set("error");this.saveError.set(this.errorText(o,"docs.error.load"))}}commit(e){this.docs.set({...e,updatedAt:new Date().toISOString()});this.dirty=true;this.scheduleSave()}scheduleSave(){if(this.saveTimer)clearTimeout(this.saveTimer);this.saveState.set("saving");this.saveTimer=setTimeout(()=>{this.saveTimer=null;this.saveNow()},this.config.saveDelayMs??He)}ensureDocs(){const e=this.docs();if(e)return e;const n=this.resolveViewCode(true);this.viewCode.set(n);this.loadedCode=n;const o=this.normalize(null,n);this.docs.set(o);return o}resolveViewCode(e){const n=this.structureService.getStructure();const o=this.config.viewCode;const a=typeof o==="function"?o(n):o;const s=String(a??"").trim();if(s)return s;const r=n?.settings??null;const d=String(r?.["viewCode"]??"").trim();if(d||!e||!n)return d||null;return this.stampViewCode(`view-${qe()}`)}stampViewCode(e){const n=this.structureService.getStructure();if(!n)return null;if(!n.settings)n.settings={width:"1182",language:"en"};n.settings["viewCode"]=e;this.api.notifyStructureChanged();return e}normalize(e,n){const o={};Object.entries(e?.elements??{}).forEach(([a,s])=>{const r=this.clean(String(s?.description??""));if(a&&r){const d=Array.isArray(s?.rows)?s.rows.map(l=>String(l??"")).filter(Boolean):[];o[a]={description:r,...d.length?{rows:d}:{},...s?.updatedAt?{updatedAt:s.updatedAt}:{}}}});return{format:"ngx-view-builder-docs",version:1,viewCode:n,...e?.title?{title:String(e.title)}:{},...e?.summary?{summary:this.clean(String(e.summary))||void 0}:{},...e?.updatedAt?{updatedAt:e.updatedAt}:{},elements:o}}clean(e){const n=this.markupSecurity.sanitizeRichTextHtml(String(e??"")).replace(/\s+dir="ltr"/g,"").trim();return n.replace(/<(?!img\b)[^>]*>/gi,"").replace(/&nbsp;|\s/g,"")||/<img\b/i.test(n)?n:""}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=c.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:G,deps:[],target:c.ɵɵFactoryTarget.Injectable});static ɵprov=c.ɵɵngDeclareInjectable({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:G,providedIn:"root"})};c.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:S,decorators:[{type:Te,args:[{providedIn:"root"}]}],ctorParameters:()=>[]});async function ie(t){return Be(t)?await Fe(t):await t}function qe(){const t=new Uint8Array(6);if(globalThis.crypto?.getRandomValues)globalThis.crypto.getRandomValues(t);else t.forEach((e,n)=>t[n]=Math.floor(Math.random()*256));return Array.from(t,e=>(e%36).toString(36)).join("")+Date.now().toString(36).slice(-2)}const de=["undo","redo","bold","italic","underline","unorderedList","orderedList","link","unlink","clearFormatting"];var C=class K{docs=b(S);icons=V;ui=b(te);time=y(()=>{const e=this.docs.lastSavedAt();return e?e.toLocaleTimeString(this.ui.getLanguage()||void 0,{hour:"2-digit",minute:"2-digit"}):""},...ngDevMode?[{debugName:"time"}]:[]);static ɵfac=c.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:K,deps:[],target:c.ɵɵFactoryTarget.Component});static ɵcmp=c.ɵɵngDeclareComponent({minVersion:"17.0.0",version:"22.2.1",type:K,isStandalone:true,selector:"docs-save-status",ngImport:c,template:"\n @switch (docs.saveState()) {\n @case ('saving') {\n <span class=\"status\" role=\"status\" data-testid=\"docs-status-saving\">{{ 'docs.status.saving' | nvbT }}</span>\n }\n @case ('loading') {\n <span class=\"status\" role=\"status\">{{ 'docs.status.loading' | nvbT }}</span>\n }\n @case ('error') {\n <span class=\"status status--error\" role=\"alert\" [title]=\"docs.saveError()\" data-testid=\"docs-status-error\">\n <nvb-icon name=\"error\" />\n {{ 'docs.status.error' | nvbT }}\n <button type=\"button\" (click)=\"docs.saveNow()\">{{ 'docs.status.retry' | nvbT }}</button>\n </span>\n }\n @case ('saved') {\n <span class=\"status\" role=\"status\" data-testid=\"docs-status-saved\">\n <nvb-icon [svg]=\"docs.browserOnly ? icons.cloud_off : icons.cloud_done\" />\n {{ 'docs.status.savedTo' | nvbT: { storage: docs.storageLabel() } }}{{ time() ? ' · ' + time() : '' }}\n </span>\n }\n @default {\n @if (!docs.browserOnly && docs.docs()) {\n <span class=\"status\">\n <nvb-icon [svg]=\"icons.cloud_done\" />\n {{ 'docs.status.storedOn' | nvbT: { storage: docs.storageLabel() } }}\n </span>\n } @else if (docs.browserOnly) {\n <span class=\"status\" [title]=\"'docs.status.browserOnlyHint' | nvbT\">\n <nvb-icon [svg]=\"icons.cloud_off\" />\n {{ 'docs.status.browserOnly' | nvbT }}\n </span>\n }\n }\n }\n ",isInline:true,styles:[":host{display:inline-flex;min-width:0;font-size:11px;color:var(--nvb-color-neutral-500)}.status{display:inline-flex;align-items:center;gap:4px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}nvb-icon{flex:0 0 auto;font-size:14px}.status--error{color:var(--nvb-color-risk-700, #b42318)}.status--error button{padding:0;border:0;background:none;font:inherit;color:var(--nvb-color-primary-600);text-decoration:underline;cursor:pointer}\n"],dependencies:[{kind:"component",type:k,selector:"nvb-icon",inputs:["name","svg","ariaLabel"]},{kind:"pipe",type:T,name:"nvbT"}],changeDetection:c.ChangeDetectionStrategy.OnPush})};c.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:C,decorators:[{type:$,args:[{selector:"docs-save-status",imports:[k,T],template:"\n @switch (docs.saveState()) {\n @case ('saving') {\n <span class=\"status\" role=\"status\" data-testid=\"docs-status-saving\">{{ 'docs.status.saving' | nvbT }}</span>\n }\n @case ('loading') {\n <span class=\"status\" role=\"status\">{{ 'docs.status.loading' | nvbT }}</span>\n }\n @case ('error') {\n <span class=\"status status--error\" role=\"alert\" [title]=\"docs.saveError()\" data-testid=\"docs-status-error\">\n <nvb-icon name=\"error\" />\n {{ 'docs.status.error' | nvbT }}\n <button type=\"button\" (click)=\"docs.saveNow()\">{{ 'docs.status.retry' | nvbT }}</button>\n </span>\n }\n @case ('saved') {\n <span class=\"status\" role=\"status\" data-testid=\"docs-status-saved\">\n <nvb-icon [svg]=\"docs.browserOnly ? icons.cloud_off : icons.cloud_done\" />\n {{ 'docs.status.savedTo' | nvbT: { storage: docs.storageLabel() } }}{{ time() ? ' · ' + time() : '' }}\n </span>\n }\n @default {\n @if (!docs.browserOnly && docs.docs()) {\n <span class=\"status\">\n <nvb-icon [svg]=\"icons.cloud_done\" />\n {{ 'docs.status.storedOn' | nvbT: { storage: docs.storageLabel() } }}\n </span>\n } @else if (docs.browserOnly) {\n <span class=\"status\" [title]=\"'docs.status.browserOnlyHint' | nvbT\">\n <nvb-icon [svg]=\"icons.cloud_off\" />\n {{ 'docs.status.browserOnly' | nvbT }}\n </span>\n }\n }\n }\n ",changeDetection:R.OnPush,styles:[":host{display:inline-flex;min-width:0;font-size:11px;color:var(--nvb-color-neutral-500)}.status{display:inline-flex;align-items:center;gap:4px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}nvb-icon{flex:0 0 auto;font-size:14px}.status--error{color:var(--nvb-color-risk-700, #b42318)}.status--error button{padding:0;border:0;background:none;font:inherit;color:var(--nvb-color-primary-600);text-decoration:underline;cursor:pointer}\n"]}]}]});var le=class X{docs=b(S);icons=V;api=b(P);element=Q(null,...ngDevMode?[{debugName:"element"}]:[]);elementName=Q("",...ngDevMode?[{debugName:"elementName"}]:[]);tools=de;description=y(()=>this.docs.descriptionOf(this.elementName()),...ngDevMode?[{debugName:"description"}]:[]);generated=y(()=>this.docs.graph().rules.filter(e=>e.owner===Ve(this.elementName())).map(e=>e.summary),...ngDevMode?[{debugName:"generated"}]:[]);ngOnInit(){this.docs.ensureLoaded()}onChange(e){this.docs.setDescription(this.elementName(),e)}openDocumentation(){this.api.switchToTab(B)}static ɵfac=c.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:X,deps:[],target:c.ɵɵFactoryTarget.Component});static ɵcmp=c.ɵɵngDeclareComponent({minVersion:"17.0.0",version:"22.2.1",type:X,isStandalone:true,selector:"nvb-docs-panel",inputs:{element:{classPropertyName:"element",publicName:"element",isSignal:true,isRequired:false,transformFunction:null},elementName:{classPropertyName:"elementName",publicName:"elementName",isSignal:true,isRequired:false,transformFunction:null}},ngImport:c,template:"<nvb-pro-feature-gate [feature]=\"'docs.title' | nvbT\" [compact]=\"true\">\n <div class=\"docs-panel\" data-testid=\"docs-panel\">\n <p class=\"docs-panel__hint\">\n {{ 'docs.panel.hint' | nvbT }}\n </p>\n\n <nvb-rich-text-editor\n class=\"docs-panel__editor\"\n [html]=\"description()\"\n [enabledTools]=\"tools\"\n [placeholder]=\"'docs.panel.placeholder' | nvbT\"\n (htmlChange)=\"onChange($event)\"\n testId=\"docs-panel-editor\"\n />\n\n <div class=\"docs-panel__footer\">\n <docs-save-status />\n <button type=\"button\" class=\"docs-panel__link\" (click)=\"openDocumentation()\" data-testid=\"docs-panel-open\">\n {{ 'docs.panel.open' | nvbT }}\n <nvb-icon [svg]=\"icons.arrow_forward\" />\n </button>\n </div>\n\n @if (generated().length > 0) {\n <div class=\"docs-panel__generated\">\n <p class=\"docs-panel__generated-title\">\n {{ 'docs.panel.generated' | nvbT }}\n </p>\n <ul>\n @for (line of generated(); track $index) {\n <li>{{ line }}</li>\n }\n </ul>\n </div>\n }\n </div>\n</nvb-pro-feature-gate>\n",styles:[":host{display:block;min-width:0;font-size:12px;color:var(--nvb-color-neutral-800)}.docs-panel{display:grid;grid-template-columns:minmax(0,1fr);gap:8px;min-width:0}.docs-panel__hint{margin:0;line-height:17px;color:var(--nvb-color-neutral-500)}.docs-panel__editor{display:block;min-width:0;min-height:140px}.docs-panel__footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:4px 8px;min-height:20px}.docs-panel__link{display:inline-flex;align-items:center;gap:4px;padding:0;border:0;background:none;font:inherit;color:var(--nvb-color-primary-600);cursor:pointer;white-space:nowrap}.docs-panel__link:hover{text-decoration:underline}.docs-panel__link:focus-visible{outline:none;border-radius:3px;box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.docs-panel__generated{padding:8px 10px;border-radius:6px;background:var(--nvb-color-neutral-050)}.docs-panel__generated-title{margin:0 0 4px;font-size:11px;font-weight:600;color:var(--nvb-color-neutral-600)}.docs-panel__generated ul{margin:0;padding-left:16px;line-height:17px;color:var(--nvb-color-neutral-600)}\n"],dependencies:[{kind:"component",type:O,selector:"nvb-pro-feature-gate",inputs:["feature","compact"]},{kind:"component",type:L,selector:"nvb-rich-text-editor",inputs:["html","viewerHtml","htmlCss","readOnly","showToolbar","emitOnInput","placeholder","minimalViewer","showWordCount","enabledTools","testId"],outputs:["htmlChange","htmlCssChange","editorFocus","editorBlur"]},{kind:"component",type:k,selector:"nvb-icon",inputs:["name","svg","ariaLabel"]},{kind:"component",type:C,selector:"docs-save-status"},{kind:"pipe",type:T,name:"nvbT"}],changeDetection:c.ChangeDetectionStrategy.OnPush})};c.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:le,decorators:[{type:$,args:[{selector:"nvb-docs-panel",imports:[O,L,k,T,C],changeDetection:R.OnPush,template:"<nvb-pro-feature-gate [feature]=\"'docs.title' | nvbT\" [compact]=\"true\">\n <div class=\"docs-panel\" data-testid=\"docs-panel\">\n <p class=\"docs-panel__hint\">\n {{ 'docs.panel.hint' | nvbT }}\n </p>\n\n <nvb-rich-text-editor\n class=\"docs-panel__editor\"\n [html]=\"description()\"\n [enabledTools]=\"tools\"\n [placeholder]=\"'docs.panel.placeholder' | nvbT\"\n (htmlChange)=\"onChange($event)\"\n testId=\"docs-panel-editor\"\n />\n\n <div class=\"docs-panel__footer\">\n <docs-save-status />\n <button type=\"button\" class=\"docs-panel__link\" (click)=\"openDocumentation()\" data-testid=\"docs-panel-open\">\n {{ 'docs.panel.open' | nvbT }}\n <nvb-icon [svg]=\"icons.arrow_forward\" />\n </button>\n </div>\n\n @if (generated().length > 0) {\n <div class=\"docs-panel__generated\">\n <p class=\"docs-panel__generated-title\">\n {{ 'docs.panel.generated' | nvbT }}\n </p>\n <ul>\n @for (line of generated(); track $index) {\n <li>{{ line }}</li>\n }\n </ul>\n </div>\n }\n </div>\n</nvb-pro-feature-gate>\n",styles:[":host{display:block;min-width:0;font-size:12px;color:var(--nvb-color-neutral-800)}.docs-panel{display:grid;grid-template-columns:minmax(0,1fr);gap:8px;min-width:0}.docs-panel__hint{margin:0;line-height:17px;color:var(--nvb-color-neutral-500)}.docs-panel__editor{display:block;min-width:0;min-height:140px}.docs-panel__footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:4px 8px;min-height:20px}.docs-panel__link{display:inline-flex;align-items:center;gap:4px;padding:0;border:0;background:none;font:inherit;color:var(--nvb-color-primary-600);cursor:pointer;white-space:nowrap}.docs-panel__link:hover{text-decoration:underline}.docs-panel__link:focus-visible{outline:none;border-radius:3px;box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-300))}.docs-panel__generated{padding:8px 10px;border-radius:6px;background:var(--nvb-color-neutral-050)}.docs-panel__generated-title{margin:0 0 4px;font-size:11px;font-weight:600;color:var(--nvb-color-neutral-600)}.docs-panel__generated ul{margin:0;padding-left:16px;line-height:17px;color:var(--nvb-color-neutral-600)}\n"]}]}],propDecorators:{element:[{type:c.Input,args:[{isSignal:true,alias:"element",required:false}]}],elementName:[{type:c.Input,args:[{isSignal:true,alias:"elementName",required:false}]}]}});const N=["required","logic","validation","data","events"];const j={groupByPage:true};const Ue=new Set(["singleLineInput","choiceInputs","tables","options"]);const We=new Set(["page","panel","tabs","tabsPro","accordion","splitter","emptyBlock","row","column","divider","spacer","pageTitle"]);const Je=new Set(["name","type","label","events","validators","visibleIf","requireIf","disableIf","readonlyIf","resetIf","expression","dataSource","dataSourceName","required","template","rows","parentName","path","dependsOn","logicExecutionMode","validationExecutionMode","propertyType","schemaPath","dataPath"]);const Ge=/css|class|style|color|colour|width|height|padding|margin|font|radius|shadow|border|icon|align|variant|tone|testid|spacing|gap|size|elevation|density/i;const Ke=new Set(["value","defaultValue","readOnly"]);function H(t,e,n,o,a=new Set,s=()=>null,r=I){const d=new Map;t.rules.forEach(i=>d.set(i.owner,[...d.get(i.owner)??[],i]));const l=e?.elements??{};const h=t.nodes.filter(i=>i.kind==="element"&&i.type!=="page").map(i=>{const m=l[i.name];const v=!!m?.description||a.has(i.name);return Qe(i,d.get(i.id)??[],m?.description??"",m?.rows??[],v?s(i.name):null,v,r)});const x=h.filter(i=>i.description||Xe(i.node)||Ye(i));const _=h.filter(i=>i.description||a.has(i.key));const u=[];if(n.groupByPage){const i=new Map;_.forEach(m=>{const v=m.node.pageName??"";i.set(v,[...i.get(v)??[],m])});[...t.pages.map(m=>m.name),""].forEach(m=>{const v=i.get(m);if(v?.length)u.push({name:m,label:m?t.pages.find(D=>D.name===m)?.label??m:r("docs.notOnPage"),entries:v})})}else if(_.length>0)u.push({name:"",label:"",entries:_});const p=new Set(h.map(i=>i.key));return{title:e?.title||o,viewCode:e?.viewCode??"",summary:e?.summary??"",sections:u,totalCount:x.length,documentedCount:h.filter(i=>i.description).length,undocumented:x.filter(i=>!i.description&&!a.has(i.key)),orphans:Object.entries(l).filter(([i])=>!p.has(i)).map(([i,m])=>({key:i,description:m.description}))}}function Xe(t){return Ue.has(t.group??"")||!We.has(t.type??"")&&t.group===""}function Ye(t){return N.some(e=>e!=="required"&&t.generated[e].length>0)}function Qe(t,e,n,o,a,s,r){const d=p=>({segments:p.segments,expression:p.expression});const l=e.find(p=>p.property==="requireIf");const h=e.find(p=>p.kind==="validation"&&p.subtype==="required");const x={required:t.required||h?[{segments:[{text:r("docs.yes")}]}]:l?[{segments:[{text:`${r("docs.required.when")} `},...l.segments.slice(1)],expression:l.expression}]:[],logic:e.filter(p=>Ze(p.property)&&p.property!=="requireIf").map(d),validation:e.filter(p=>p.property.startsWith("validators[")&&p!==h).map(d),data:e.filter(p=>p.property==="dataSource").map(d),events:e.filter(p=>p.property.startsWith("events[")).map(d)};const _=o.map(p=>{if(p.startsWith("prop:")){const v=p.slice(5);const D=a&&v in a?pe(a[v],r):"";return{id:p,label:ce(v,r),text:D,empty:!D}}const i=p;const m=x[i]??[];return{id:p,label:N.includes(i)?r(`docs.row.${i}`):oe(p),cells:m,empty:m.length===0}});const u=[];if(s){const p=new Set(o);N.forEach(i=>{if(!p.has(i)&&x[i].length>0)u.push({id:i,label:r(`docs.row.${i}`),preview:x[i].map(m=>tt(m.segments)).join("; "),group:"logic"})});Object.entries(a??{}).filter(([i])=>!p.has(`prop:${i}`)&&et(i)).forEach(([i,m])=>{const v=m===false||Array.isArray(m)&&m.length===0?"":pe(m,r);if(v&&!(m!==null&&typeof m==="object"&&!Array.isArray(m)))u.push({id:`prop:${i}`,label:ce(i,r),preview:v,group:"property"})})}return{node:t,key:t.name,description:n,rows:_,options:u,generated:x}}function Ze(t){return["visibleIf","requireIf","disableIf","readonlyIf","resetIf","expression"].includes(t)}function et(t){return!Je.has(t)&&!t.startsWith("_")&&!Ge.test(t)}function ce(t,e){return Ke.has(t)?e(`docs.prop.${t}`):e(`property.${t}`,void 0,oe(t))}function pe(t,e){if(t===null||t===void 0||t==="")return"";if(typeof t==="boolean")return t?e("docs.yes"):e("docs.no");if(typeof t==="number")return String(t);if(typeof t==="string")return q(w(t),300);if(Array.isArray(t))return q(t.map(n=>n&&typeof n==="object"?w(n["label"]??n["name"]??n["value"]):w(n)).filter(Boolean).join(", "),300);try{return q(JSON.stringify(t),200)}catch{return""}}function q(t,e){return t.length>e?`${t.slice(0,e-1).trimEnd()}…`:t}function tt(t){return t.map(e=>"ref"in e?e.label:e.text).join("").replace(/\s+/g," ").trim()}function f(t){return String(t??"").replace(/[&<>"]/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[e])}function ue(t,e){return t.ref.startsWith("missing:")?e("docs.export.notInView",{name:t.label}):t.label}function me(t,e){return t.map(n=>"ref"in n?`<strong>${f(ue(n,e))}</strong>`:f(n.text)).join("").trim()}function nt(t,e){if(t.empty)return"—";if(t.text!==void 0)return f(t.text);const n=t.cells??[];return n.length===1?me(n[0].segments,e):`<ul>${n.map(o=>`<li>${me(o.segments,e)}</li>`).join("")}</ul>`}function he(t){return[t.node.context,t.node.typeLabel].filter(Boolean).join(" · ")}function be(t,e,n){return n("docs.export.summary",{code:t.viewCode||n("docs.export.noCode"),documented:t.documentedCount,total:t.totalCount,date:e})}function ve(t){return t.sections.map(e=>({...e,entries:e.entries.filter(n=>n.description)})).filter(e=>e.entries.length>0)}function E(t,e,n,o=I){const a=[`<h1>${f(t.title)}</h1>`,`<p>${f(be(t,n,o))}</p>`];if(t.summary)a.push(t.summary);ve(t).forEach(s=>{if(s.label)a.push(`<h2>${f(s.label)}</h2>`);s.entries.forEach(r=>{const d=[`<tr><td colspan="2">${r.description}</td></tr>`,...r.rows.filter(l=>!l.empty).map(l=>`<tr><th>${f(l.label)}</th><td>${nt(l,o)}</td></tr>`)];a.push(`<table><thead><tr><th colspan="2">${f(r.node.label)} <code>${f(r.node.name)}</code> · ${f(he(r))}</th></tr></thead><tbody>${d.join("")}</tbody></table>`)})});return a.join("\n")}function ot(t,e,n,o=I,a="en"){return`<!doctype html>
2
+ <html lang="${f(a)}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>${f(t.title)}</title>
7
+ <style>
8
+ body{font:14px/1.55 -apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#1f2328;max-width:900px;margin:40px auto;padding:0 24px}
9
+ h1{font-size:26px;margin:0 0 8px}h2{font-size:18px;margin:32px 0 10px}
10
+ table{border-collapse:collapse;width:100%;margin:0 0 16px}th,td{border:1px solid #d0d7de;padding:8px 10px;text-align:left;vertical-align:top}
11
+ thead th{background:#f6f8fa;font-weight:600}tbody th{width:160px;font-weight:600;color:#57606a}
12
+ code{font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#f6f8fa;padding:1px 4px;border-radius:4px;font-weight:400}
13
+ td ul{margin:0;padding-left:18px}td p{margin:0 0 6px}td p:last-child{margin:0}p{margin:0 0 12px}
14
+ </style>
15
+ </head>
16
+ <body>
17
+ ${E(t,e,n,o)}
18
+ </body>
19
+ </html>
20
+ `}function ge(t){return String(t??"").replace(/<br\s*\/?>/gi," ").replace(/<\/(p|div|li|h[1-6])>/gi," ").replace(/<li[^>]*>/gi,"• ").replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi,"**$2**").replace(/<(em|i)>([\s\S]*?)<\/\1>/gi,"_$2_").replace(/<a [^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi,"[$2]($1)").replace(/<[^>]+>/g,"").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/\s+/g," ").trim()}function rt(t,e){return t.map(n=>"ref"in n?`**${ue(n,e)}**`:n.text).join("").trim()}function M(t){return t.replace(/\|/g,"\\|")}function at(t,e){if(t.empty)return"—";if(t.text!==void 0)return t.text;return(t.cells??[]).map(n=>rt(n.segments,e)).join("<br>")}function z(t,e,n,o=I){const a=[`# ${t.title}`,"",be(t,n,o),""];if(t.summary)a.push(ge(t.summary),"");ve(t).forEach(s=>{if(s.label)a.push(`## ${s.label}`,"");s.entries.forEach(r=>{a.push(`### ${r.node.label} \`${r.node.name}\``,"",`_${he(r)}_`,"",ge(r.description),"");const d=r.rows.filter(l=>!l.empty);if(d.length>0)a.push(`| ${M(o("docs.export.property"))} | ${M(o("docs.export.value"))} |`,"| --- | --- |",...d.map(l=>`| ${M(l.label)} | ${M(at(l,o))} |`),"")})});return a.join("\n")}const fe="nvb-docs.options.v3";var ye=class Y{docs=b(S);icons=V;api=b(P);elementFocus=b(Oe);host=b(ke);i18n=b(ne);ui=b(te);tools=de;options=g(this.restoreOptions(),...ngDevMode?[{debugName:"options"}]:[]);search=g("",...ngDevMode?[{debugName:"search"}]:[]);addSearch=g("",...ngDevMode?[{debugName:"addSearch"}]:[]);rowMenuKey=g(null,...ngDevMode?[{debugName:"rowMenuKey"}]:[]);rowSearch=g("",...ngDevMode?[{debugName:"rowSearch"}]:[]);menu=g(null,...ngDevMode?[{debugName:"menu"}]:[]);editing=g(null,...ngDevMode?[{debugName:"editing"}]:[]);editingSummary=g(false,...ngDevMode?[{debugName:"editingSummary"}]:[]);feedback=g(null,...ngDevMode?[{debugName:"feedback"}]:[]);feedbackTimer=null;model=y(()=>{const e=this.editing();return H(this.docs.graph(),this.docs.docs(),this.options(),this.docs.viewTitle(),e?new Set([e]):new Set,this.docs.definitionLookup(),this.i18n.t)},...ngDevMode?[{debugName:"model"}]:[]);visibleSections=y(()=>{const e=this.search().trim().toLowerCase();if(!e)return this.model().sections;return this.model().sections.map(n=>({...n,entries:n.entries.filter(o=>o.node.label.toLowerCase().includes(e)||o.node.name.toLowerCase().includes(e)||o.description.toLowerCase().includes(e))})).filter(n=>n.entries.length>0)},...ngDevMode?[{debugName:"visibleSections"}]:[]);addCandidates=y(()=>{const e=this.addSearch().trim().toLowerCase();const n=this.model().undocumented;return e?n.filter(o=>o.node.label.toLowerCase().includes(e)||o.node.name.toLowerCase().includes(e)):n},...ngDevMode?[{debugName:"addCandidates"}]:[]);progress=y(()=>{const e=this.model();return e.totalCount===0?0:Math.round(e.documentedCount/e.totalCount*100)},...ngDevMode?[{debugName:"progress"}]:[]);ngOnInit(){this.docs.refresh()}toggleMenu(e){this.menu.update(n=>n===e?null:e);this.addSearch.set("")}toggleRowMenu(e){const n=this.menu()==="row"&&this.rowMenuKey()===e.key;this.menu.set(n?null:"row");this.rowMenuKey.set(n?null:e.key);this.rowSearch.set("")}rowOptions(e,n){const o=this.rowSearch().trim().toLowerCase();return e.options.filter(a=>a.group===n&&(!o||a.label.toLowerCase().includes(o)||a.preview.toLowerCase().includes(o)))}addRow(e,n){this.docs.setRows(e.key,[...e.rows.map(o=>o.id),n.id]);this.menu.set(null)}removeRow(e,n){this.docs.setRows(e.key,e.rows.map(o=>o.id).filter(o=>o!==n))}onDocumentPointerDown(e){if(!this.menu())return;if(!e.composedPath().some(n=>n instanceof HTMLElement&&n.classList.contains("dt-menu-anchor")))this.menu.set(null)}updateOptions(e){this.options.update(n=>({...n,...e}));try{localStorage.setItem(fe,JSON.stringify(this.options()))}catch{}}restoreOptions(){try{const e=JSON.parse(localStorage.getItem(fe)??"null");if(e)return{...j,...e}}catch{}return{...j}}addElement(e){this.menu.set(null);this.search.set("");this.editing.set(e.key);setTimeout(()=>{const n=this.host.nativeElement.querySelector(`[data-key="${CSS.escape(e.key)}"]`);n?.scrollIntoView({block:"center",behavior:"smooth"});n?.querySelector('[contenteditable="true"]')?.focus()},50)}edit(e){this.editing.set(e.key)}onDescriptionChange(e,n){this.docs.setDescription(e.key,n)}finishEditing(){this.editing.set(null);this.docs.saveNow()}onSummaryChange(e){this.docs.setSummary(e)}onTitleChange(e){this.docs.setTitle(e===this.docs.viewTitle()?"":e)}removeOrphan(e){this.docs.removeDescription(e)}openInBuilder(e){this.api.switchToTab(Ne.EDITOR);setTimeout(()=>{const n=this.api.getElementModel(e.key)??this.api.getElementModel(e.key.split(".").pop());if(n)this.elementFocus.setFocus(n)},60)}isRef(e){return"ref"in e}generatedOn(){return new Date().toLocaleDateString(this.ui.getLanguage()||void 0,{year:"numeric",month:"short",day:"numeric"})}async copyForConfluence(){const e=E(this.model(),this.options(),this.generatedOn(),this.i18n.t);const n=z(this.model(),this.options(),this.generatedOn(),this.i18n.t);const o=this.i18n.t("docs.copied",{count:this.model().documentedCount});const a=this.i18n.t("docs.clipboardBlocked");try{if(typeof ClipboardItem!=="undefined"&&navigator.clipboard?.write){await navigator.clipboard.write([new ClipboardItem({"text/html":new Blob([e],{type:"text/html"}),"text/plain":new Blob([n],{type:"text/plain"})})]);this.flash("ok",o)}else if(this.copyWithSelection(e))this.flash("ok",o);else this.flash("error",a)}catch{this.copyWithSelection(e)?this.flash("ok",o):this.flash("error",a)}}async copyMarkdown(){try{await navigator.clipboard.writeText(z(this.model(),this.options(),this.generatedOn(),this.i18n.t));this.flash("ok",this.i18n.t("docs.markdownCopied"))}catch{this.flash("error",this.i18n.t("docs.clipboardBlocked"))}}downloadHtml(){this.download(ot(this.model(),this.options(),this.generatedOn(),this.i18n.t,this.ui.getLanguage()||"en"),`${this.fileName()}.html`,"text/html")}downloadJson(){const e=this.docs.docs();if(!e){this.flash("error",this.i18n.t("docs.nothingToDownload"));return}this.download(JSON.stringify(e,null,2),`${this.fileName()}.docs.json`,"application/json")}fileName(){return(this.model().viewCode||this.model().title).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"view-documentation"}download(e,n,o){const a=URL.createObjectURL(new Blob([e],{type:o}));const s=document.createElement("a");s.href=a;s.download=n;s.click();setTimeout(()=>URL.revokeObjectURL(a),1e3)}copyWithSelection(e){const n=document.createElement("div");n.innerHTML=e;n.style.cssText="position:fixed;left:-9999px;top:0;";document.body.appendChild(n);const o=document.createRange();o.selectNodeContents(n);const a=window.getSelection();a?.removeAllRanges();a?.addRange(o);let s=false;try{s=document.execCommand("copy")}catch{s=false}a?.removeAllRanges();n.remove();return s}flash(e,n){this.feedback.set({kind:e,text:n});if(this.feedbackTimer)clearTimeout(this.feedbackTimer);this.feedbackTimer=setTimeout(()=>this.feedback.set(null),5e3)}static ɵfac=c.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:Y,deps:[],target:c.ɵɵFactoryTarget.Component});static ɵcmp=c.ɵɵngDeclareComponent({minVersion:"17.0.0",version:"22.2.1",type:Y,isStandalone:true,selector:"nvb-docs-tab",host:{listeners:{"document:pointerdown":"onDocumentPointerDown($event)","keydown.escape":"menu.set(null)"}},ngImport:c,template:"<nvb-pro-feature-gate [feature]=\"'docs.title' | nvbT\">\n @let doc = model();\n <div class=\"dt\" data-testid=\"docs-tab-root\">\n <!-- ── Toolbar ───────────────────────────────────────────────────────── -->\n <header class=\"dt-bar\">\n <div class=\"dt-bar__top\">\n <div class=\"dt-bar__info\">\n <h2 class=\"dt-bar__title\">{{ 'docs.title' | nvbT }}</h2>\n <div class=\"dt-bar__meta\">\n <span class=\"dt-progress\">\n <span class=\"dt-progress__track\" aria-hidden=\"true\">\n <span class=\"dt-progress__fill\" [style.width.%]=\"progress()\"></span>\n </span>\n <span data-testid=\"docs-progress\">\n {{ 'docs.progress' | nvbT: { documented: doc.documentedCount, total: doc.totalCount } }}\n </span>\n </span>\n <span class=\"dt-bar__dot\" aria-hidden=\"true\">·</span>\n <docs-save-status />\n @if (doc.viewCode) {\n <span class=\"dt-bar__dot\" aria-hidden=\"true\">·</span>\n <span class=\"dt-code\" [title]=\"'docs.viewCode.hint' | nvbT\">\n {{ 'docs.viewCode' | nvbT }} <code>{{ doc.viewCode }}</code>\n </span>\n }\n </div>\n </div>\n\n <div class=\"dt-bar__actions\">\n @if (feedback(); as message) {\n <span class=\"dt-feedback\" [attr.data-kind]=\"message.kind\" role=\"status\" data-testid=\"docs-feedback\">\n <nvb-icon [name]=\"message.kind === 'ok' ? 'check' : 'error'\" />\n {{ message.text }}\n </span>\n }\n <button\n type=\"button\"\n class=\"nvb-button nvb-button--primary nvb-button--sm dt-icon-button\"\n (click)=\"copyForConfluence()\"\n [disabled]=\"doc.documentedCount === 0\"\n data-testid=\"docs-copy-confluence\"\n >\n <nvb-icon name=\"content_copy\" />\n {{ 'docs.copyConfluence' | nvbT }}\n </button>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"copyMarkdown()\" [disabled]=\"doc.documentedCount === 0\" data-testid=\"docs-copy-markdown\">\n {{ 'docs.copyMarkdown' | nvbT }}\n </button>\n <span class=\"dt-bar__divider\" aria-hidden=\"true\"></span>\n <builder-icon-button icon=\"download\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'docs.downloadHtml' | nvbT\" [title]=\"'docs.downloadHtml' | nvbT\" (clicked)=\"downloadHtml()\" testId=\"docs-download-html\" />\n <builder-icon-button icon=\"data_object\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'docs.downloadJson' | nvbT\" [title]=\"'docs.downloadJson' | nvbT\" (clicked)=\"downloadJson()\" testId=\"docs-download-json\" />\n </div>\n </div>\n\n <div class=\"dt-bar__filters\">\n <div class=\"dt-search\">\n <builder-search-input\n [value]=\"search()\"\n [placeholder]=\"'docs.search' | nvbT\"\n [ariaLabel]=\"'docs.search' | nvbT\"\n [clearAriaLabel]=\"'docs.clearSearch' | nvbT\"\n testId=\"docs-search\"\n (valueChanged)=\"search.set($event)\"\n (cleared)=\"search.set('')\"\n />\n </div>\n\n <!-- Document an element: the undocumented ones, searchable -->\n <div class=\"dt-menu-anchor\">\n <button\n type=\"button\"\n class=\"nvb-button nvb-button--neutral nvb-button--sm dt-icon-button\"\n [attr.aria-expanded]=\"menu() === 'add'\"\n aria-haspopup=\"listbox\"\n (click)=\"toggleMenu('add')\"\n data-testid=\"docs-add-element\"\n >\n <nvb-icon name=\"add\" />\n {{ 'docs.addElement' | nvbT }}\n <span class=\"dt-count\">{{ doc.undocumented.length }}</span>\n </button>\n @if (menu() === 'add') {\n <div class=\"dt-menu dt-menu--add\" role=\"dialog\" [attr.aria-label]=\"'docs.addElement' | nvbT\">\n <builder-search-input\n class=\"dt-menu__search\"\n [value]=\"addSearch()\"\n [placeholder]=\"'docs.addElement.search' | nvbT\"\n [ariaLabel]=\"'docs.addElement.search' | nvbT\"\n [clearAriaLabel]=\"'docs.clearSearch' | nvbT\"\n testId=\"docs-add-search\"\n (valueChanged)=\"addSearch.set($event)\"\n (cleared)=\"addSearch.set('')\"\n />\n @if (addCandidates().length === 0) {\n <p class=\"dt-menu__empty\">\n @if (doc.undocumented.length === 0) {\n {{ 'docs.addElement.allDone' | nvbT }}\n } @else {\n {{ 'docs.addElement.none' | nvbT }}\n }\n </p>\n } @else {\n <ul class=\"dt-menu__list\" role=\"listbox\">\n @for (entry of addCandidates(); track entry.key) {\n <li role=\"option\" aria-selected=\"false\">\n <button type=\"button\" class=\"dt-menu__item\" (click)=\"addElement(entry)\" [attr.data-testid]=\"'docs-add-option-' + entry.key\">\n <span class=\"dt-menu__label\">{{ entry.node.label }}</span>\n <span class=\"dt-menu__meta\">\n <code>{{ entry.node.name }}</code> · @if (entry.node.context) {{{ entry.node.context }} · }{{ entry.node.typeLabel }}@if (entry.node.pageLabel) { · {{ entry.node.pageLabel }}}\n </span>\n </button>\n </li>\n }\n </ul>\n }\n </div>\n }\n </div>\n\n <label class=\"dt-check dt-check--inline\">\n <input type=\"checkbox\" [checked]=\"options().groupByPage\" (change)=\"updateOptions({ groupByPage: $any($event.target).checked })\" data-testid=\"docs-group-by-page\" />\n <span>{{ 'docs.groupByPage' | nvbT }}</span>\n </label>\n\n @if (docs.browserOnly) {\n <span class=\"dt-local-note\" [title]=\"'docs.browserOnly.text' | nvbT\">\n <nvb-icon [svg]=\"icons.cloud_off\" />\n {{ 'docs.browserOnly.title' | nvbT }}\n </span>\n }\n </div>\n </header>\n\n <!-- ── The document ─────────────────────────────────────────────────── -->\n <main class=\"dt-sheet-wrap\">\n <article class=\"dt-sheet\" data-testid=\"docs-sheet\">\n <input\n class=\"dt-sheet__title\"\n [value]=\"doc.title\"\n (change)=\"onTitleChange($any($event.target).value)\"\n [placeholder]=\"docs.viewTitle()\"\n [attr.aria-label]=\"'docs.titleField' | nvbT\"\n data-testid=\"docs-title\"\n />\n\n <section class=\"dt-summary\">\n @if (editingSummary()) {\n <nvb-rich-text-editor\n [html]=\"doc.summary\"\n [enabledTools]=\"tools\"\n [placeholder]=\"'docs.summary.placeholder' | nvbT\"\n (htmlChange)=\"onSummaryChange($event)\"\n testId=\"docs-summary-editor\"\n />\n <div class=\"dt-edit-actions\">\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"editingSummary.set(false)\">\n {{ 'docs.done' | nvbT }}\n </button>\n </div>\n } @else if (doc.summary) {\n <button type=\"button\" class=\"dt-rich dt-rich--button\" (click)=\"editingSummary.set(true)\" [attr.aria-label]=\"'docs.summary.edit' | nvbT\">\n <div [innerHTML]=\"doc.summary\"></div>\n </button>\n } @else {\n <button type=\"button\" class=\"dt-add\" (click)=\"editingSummary.set(true)\" data-testid=\"docs-summary-add\">\n <nvb-icon name=\"add\" />\n {{ 'docs.summary.add' | nvbT }}\n </button>\n }\n </section>\n\n @if (doc.sections.length === 0) {\n <div class=\"dt-empty\" data-testid=\"docs-empty\">\n <nvb-icon name=\"description\" class=\"dt-empty__icon\" />\n <h3>{{ 'docs.empty.title' | nvbT }}</h3>\n <p>\n {{\n 'docs.empty.text' | nvbT\n }}\n </p>\n @if (doc.undocumented.length > 0) {\n <button type=\"button\" class=\"nvb-button nvb-button--primary nvb-button--sm dt-icon-button dt-menu-anchor\" (click)=\"toggleMenu('add')\">\n <nvb-icon name=\"add\" />\n {{ 'docs.addElement' | nvbT }}\n </button>\n }\n </div>\n } @else if (visibleSections().length === 0) {\n <div class=\"dt-empty\">\n <h3>{{ 'docs.noMatch.title' | nvbT }}</h3>\n <p>{{ 'docs.noMatch.text' | nvbT }}</p>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"search.set('')\">\n {{ 'docs.noMatch.clear' | nvbT }}\n </button>\n </div>\n }\n\n @for (section of visibleSections(); track section.name) {\n <section class=\"dt-section\">\n @if (section.label) {\n <h2 class=\"dt-section__title\">\n {{ section.label }}\n <span class=\"dt-count\">{{ section.entries.length }}</span>\n </h2>\n }\n\n @for (entry of section.entries; track entry.key) {\n <table class=\"dt-card\" [attr.data-key]=\"entry.key\" [attr.data-testid]=\"'docs-card-' + entry.key\" [class.dt-card--editing]=\"editing() === entry.key\">\n <thead>\n <tr>\n <th colspan=\"2\" scope=\"colgroup\">\n <div class=\"dt-card__head\">\n <button type=\"button\" class=\"dt-card__title\" (click)=\"openInBuilder(entry)\" [title]=\"'docs.openInBuilder' | nvbT\">\n {{ entry.node.label }}\n </button>\n <code class=\"dt-card__name\">{{ entry.node.name }}</code>\n <span class=\"dt-card__type\">\n @if (entry.node.context) {\n <span class=\"dt-card__context\">{{ entry.node.context }}</span> ·\n }\n {{ entry.node.typeLabel }}\n </span>\n </div>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr class=\"dt-card__description\">\n <td colspan=\"2\">\n @if (editing() === entry.key) {\n <nvb-rich-text-editor\n [html]=\"entry.description\"\n [enabledTools]=\"tools\"\n [placeholder]=\"'docs.panel.placeholder' | nvbT\"\n (htmlChange)=\"onDescriptionChange(entry, $event)\"\n [testId]=\"'docs-editor-' + entry.key\"\n />\n <div class=\"dt-edit-actions\">\n <span class=\"dt-edit-actions__hint\">{{ 'docs.emptyRemoves' | nvbT }}</span>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"finishEditing()\" [attr.data-testid]=\"'docs-done-' + entry.key\">\n {{ 'docs.done' | nvbT }}\n </button>\n </div>\n } @else {\n <button type=\"button\" class=\"dt-rich dt-rich--button\" (click)=\"edit(entry)\" [attr.data-testid]=\"'docs-edit-' + entry.key\" [attr.aria-label]=\"'docs.edit' | nvbT: { name: entry.node.label }\">\n <div [innerHTML]=\"entry.description\"></div>\n </button>\n }\n </td>\n </tr>\n @for (row of entry.rows; track row.id) {\n <tr class=\"dt-card__row\" [attr.data-row]=\"row.id\">\n <th scope=\"row\">{{ row.label }}</th>\n <td>\n <div class=\"dt-card__value\">\n <div class=\"dt-card__value-text\">\n @if (row.empty) {\n <span class=\"dt-none\" [title]=\"'docs.row.emptyHint' | nvbT\">{{ 'docs.row.empty' | nvbT }}</span>\n } @else if (row.text !== undefined) {\n {{ row.text }}\n } @else {\n <ul class=\"dt-rules\">\n @for (cell of row.cells; track $index) {\n <li [title]=\"cell.expression ?? ''\">\n @for (segment of cell.segments; track $index) {\n @if (isRef(segment)) {<strong>{{ segment.label }}</strong>} @else {{{ segment.text }}}\n }\n </li>\n }\n </ul>\n }\n </div>\n <builder-icon-button\n class=\"dt-card__remove\"\n icon=\"close\"\n size=\"sm\"\n [ghost]=\"true\"\n [ariaLabel]=\"'docs.row.removeNamed' | nvbT: { name: row.label }\"\n [title]=\"'docs.row.remove' | nvbT\"\n (clicked)=\"removeRow(entry, row.id)\"\n [testId]=\"'docs-row-remove-' + entry.key + '-' + row.id\"\n />\n </div>\n </td>\n </tr>\n }\n @if (entry.description) {\n <tr class=\"dt-card__add\">\n <td colspan=\"2\">\n <div class=\"dt-menu-anchor\">\n <button\n type=\"button\"\n class=\"dt-card__add-button\"\n [attr.aria-expanded]=\"menu() === 'row' && rowMenuKey() === entry.key\"\n aria-haspopup=\"listbox\"\n (click)=\"toggleRowMenu(entry)\"\n [disabled]=\"entry.options.length === 0\"\n [title]=\"entry.options.length === 0 ? ('docs.row.nothingLeft' | nvbT) : ''\"\n [attr.data-testid]=\"'docs-row-add-' + entry.key\"\n >\n <nvb-icon name=\"add\" />\n {{ 'docs.row.add' | nvbT }}\n </button>\n @if (menu() === 'row' && rowMenuKey() === entry.key) {\n <div class=\"dt-menu dt-menu--row\" role=\"dialog\" [attr.aria-label]=\"'docs.row.add' | nvbT\">\n <builder-search-input\n class=\"dt-menu__search\"\n [value]=\"rowSearch()\"\n [placeholder]=\"'docs.row.search' | nvbT\"\n [ariaLabel]=\"'docs.row.search' | nvbT\"\n [clearAriaLabel]=\"'docs.clearSearch' | nvbT\"\n [testId]=\"'docs-row-search-' + entry.key\"\n (valueChanged)=\"rowSearch.set($event)\"\n (cleared)=\"rowSearch.set('')\"\n />\n <div class=\"dt-menu__list\">\n @let fromLogic = rowOptions(entry, 'logic');\n @let fromProperties = rowOptions(entry, 'property');\n @if (fromLogic.length > 0) {\n <p class=\"dt-menu__title\">{{ 'docs.row.fromLogic' | nvbT }}</p>\n @for (option of fromLogic; track option.id) {\n <button type=\"button\" class=\"dt-menu__item\" (click)=\"addRow(entry, option)\" [attr.data-testid]=\"'docs-row-option-' + entry.key + '-' + option.id\">\n <span class=\"dt-menu__label\">{{ option.label }}</span>\n <span class=\"dt-menu__meta\">{{ option.preview }}</span>\n </button>\n }\n }\n @if (fromProperties.length > 0) {\n <p class=\"dt-menu__title\">{{ 'docs.row.fromProperties' | nvbT }}</p>\n @for (option of fromProperties; track option.id) {\n <button type=\"button\" class=\"dt-menu__item\" (click)=\"addRow(entry, option)\" [attr.data-testid]=\"'docs-row-option-' + entry.key + '-' + option.id\">\n <span class=\"dt-menu__label\">{{ option.label }}</span>\n <span class=\"dt-menu__meta\">{{ option.preview }}</span>\n </button>\n }\n }\n @if (fromLogic.length === 0 && fromProperties.length === 0) {\n <p class=\"dt-menu__empty\">{{ 'docs.row.noMatch' | nvbT }}</p>\n }\n </div>\n </div>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n </section>\n }\n\n @if (doc.orphans.length > 0) {\n <section class=\"dt-section dt-orphans\" data-testid=\"docs-orphans\">\n <h2 class=\"dt-section__title\">\n {{ 'docs.orphans' | nvbT }}\n <span class=\"dt-count\">{{ doc.orphans.length }}</span>\n </h2>\n <p class=\"dt-note\">\n {{ 'docs.orphans.text' | nvbT }}\n </p>\n <ul class=\"dt-orphan-list\">\n @for (orphan of doc.orphans; track orphan.key) {\n <li>\n <code>{{ orphan.key }}</code>\n <div class=\"dt-rich\" [innerHTML]=\"orphan.description\"></div>\n <builder-icon-button icon=\"delete\" tone=\"danger\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'docs.orphans.remove' | nvbT\" [title]=\"'docs.orphans.remove' | nvbT\" (clicked)=\"removeOrphan(orphan.key)\" [testId]=\"'docs-orphan-remove-' + orphan.key\" />\n </li>\n }\n </ul>\n </section>\n }\n </article>\n </main>\n </div>\n</nvb-pro-feature-gate>\n",styles:['@charset "UTF-8";:host{--nvb-input-height: 32px;display:block;height:100%;min-height:0;overflow:hidden;font-size:12px;color:var(--nvb-color-neutral-800);background:color-mix(in oklab,var(--nvb-color-neutral-050) 84%,var(--nvb-color-neutral-000))}button:not(.nvb-button){font:inherit;color:inherit}.dt{height:100%;min-height:0;display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;padding:10px;box-sizing:border-box}.dt-bar{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)}.dt-bar__top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px 16px;padding:10px 12px}.dt-bar__filters{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:8px 12px;border-top:1px solid var(--nvb-color-neutral-200)}.dt-bar__info{min-width:0}.dt-bar__title{margin:0;font-size:13px;font-weight:600;line-height:18px;color:var(--nvb-color-neutral-900)}.dt-bar__meta{display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;margin-top:3px;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-bar__dot{color:var(--nvb-color-neutral-300)}.dt-progress{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}.dt-progress__track{position:relative;width:64px;height:4px;overflow:hidden;border-radius:999px;background:var(--nvb-color-neutral-200)}.dt-progress__fill{position:absolute;inset:0 auto 0 0;border-radius:999px;background:var(--nvb-color-primary-600)}.dt-code code{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--nvb-color-neutral-700)}.dt-bar__actions{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.dt-bar__primary{display:inline-flex;align-items:center;gap:6px}.dt-bar__actions .nvb-button[disabled]{opacity:.5;cursor:not-allowed}.dt-bar__divider{width:1px;height:18px;margin:0 2px;background:var(--nvb-color-neutral-200)}.dt-feedback{display:inline-flex;align-items:center;gap:4px;margin-right:6px;font-size:12px;color:var(--nvb-color-success-700, #1a7f37)}.dt-feedback[data-kind=error]{color:var(--nvb-color-risk-700, #b42318)}.dt-feedback nvb-icon{font-size:15px}.dt-icon-button{display:inline-flex;align-items:center;gap:6px}.dt-icon-button nvb-icon{font-size:16px}.dt-search{width:260px;max-width:100%}.dt-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;font-variant-numeric:tabular-nums}.dt-menu-anchor{position:relative}.dt-menu{position:absolute;top:calc(100% + 6px);left:0;z-index:20;box-sizing:border-box;padding:8px;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 10px 28px #00000029}.dt-menu--add{width:340px}.dt-menu--rows{width:260px;padding:10px 12px}.dt-menu__search{display:block;margin-bottom:6px}.dt-menu__list{max-height:300px;margin:0;padding:0;overflow:auto;list-style:none}.dt-menu__item{display:grid;width:100%;padding:6px 8px;border:0;border-radius:6px;background:none;text-align:left;cursor:pointer}.dt-menu__item:hover,.dt-menu__item:focus-visible{outline:none;background:var(--nvb-color-primary-050)}.dt-menu__label{font-weight:500;color:var(--nvb-color-neutral-900)}.dt-menu__meta{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-menu__meta code,.dt-card__name{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)}.dt-menu__empty,.dt-menu__hint{margin:6px 4px;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500)}.dt-menu__title{margin:0 0 4px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.dt-menu__rule{margin:8px 0;border:0;border-top:1px solid var(--nvb-color-neutral-200)}.dt-check{display:flex;align-items:center;gap:8px;min-height:26px;cursor:pointer}.dt-check input{margin:0;accent-color:var(--nvb-color-primary-600)}.dt-check--fixed{cursor:default;color:var(--nvb-color-neutral-500)}.dt-local-note{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-local-note nvb-icon{font-size:14px}.dt-sheet-wrap{min-height:0;overflow:auto;border:1px solid var(--nvb-card-border-color, var(--nvb-color-neutral-200));border-radius:var(--nvb-card-radius, 8px);background:var(--nvb-color-neutral-000)}.dt-sheet{max-width:920px;margin:0 auto;padding:28px 32px 48px;font-size:13px;line-height:20px}.dt-sheet__title{display:block;width:100%;padding:2px 6px;margin:0 0 6px -6px;border:1px solid transparent;border-radius:6px;background:transparent;font:inherit;font-size:24px;font-weight:600;line-height:32px;color:var(--nvb-color-neutral-900)}.dt-sheet__title:hover{border-color:var(--nvb-color-neutral-200)}.dt-sheet__title:focus{outline:none;border-color:var(--nvb-color-primary-500);box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-200))}.dt-summary{margin:4px 0 20px}.dt-add{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 3px 4px;border:1px dashed var(--nvb-color-neutral-300);border-radius:6px;background:none;font-size:12px;color:var(--nvb-color-neutral-500);cursor:pointer}.dt-add:hover{border-color:var(--nvb-color-primary-500);color:var(--nvb-color-primary-700);background:var(--nvb-color-primary-050)}.dt-add nvb-icon{font-size:15px}.dt-rich{overflow-wrap:anywhere}.dt-rich :is(p,ul,ol){margin:0 0 6px}.dt-rich :is(p,ul,ol):last-child{margin-bottom:0}.dt-rich :is(ul,ol){padding-left:18px}.dt-rich--button{display:block;width:100%;margin:-4px -6px;padding:4px 6px;border:1px solid transparent;border-radius:6px;background:none;text-align:left;cursor:text}.dt-rich--button:hover{border-color:var(--nvb-color-neutral-200);background:var(--nvb-color-neutral-025, var(--nvb-color-neutral-050))}.dt-edit-actions{display:flex;justify-content:flex-end;margin-top:6px}.dt-section{margin-top:28px}.dt-section__title{display:flex;align-items:center;gap:8px;margin:0 0 10px;font-size:17px;font-weight:600;line-height:24px;color:var(--nvb-color-neutral-900)}.dt-card{width:100%;margin:0 0 12px;border-collapse:separate;border-spacing:0;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;overflow:hidden;font-size:13px;line-height:19px}.dt-card--editing{border-color:var(--nvb-color-primary-500);box-shadow:0 0 0 2px color-mix(in oklab,var(--nvb-color-primary-500) 18%,transparent)}.dt-card thead th{padding:8px 12px;border-bottom:1px solid var(--nvb-color-neutral-200);background:var(--nvb-color-neutral-050);text-align:left;font-weight:400}.dt-card__head{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px}.dt-card__title{padding:0;border:0;background:none;font-size:14px;font-weight:600;color:var(--nvb-color-neutral-900);cursor:pointer}.dt-card__title:hover{color:var(--nvb-color-primary-700);text-decoration:underline}.dt-card__name{font-size:11.5px;color:var(--nvb-color-neutral-600)}.dt-card__context{font-weight:600;color:var(--nvb-color-primary-700)}.dt-card__type{font-size:11.5px;color:var(--nvb-color-neutral-500)}.dt-card tbody th,.dt-card tbody td{padding:8px 12px;border-top:1px solid var(--nvb-color-neutral-100, var(--nvb-color-neutral-200));text-align:left;vertical-align:top}.dt-card tbody tr:first-child>*{border-top:0}.dt-card tbody th{width:150px;font-size:12px;font-weight:600;color:var(--nvb-color-neutral-500);white-space:nowrap}.dt-card tbody td{color:var(--nvb-color-neutral-800)}.dt-card strong{font-weight:600;color:var(--nvb-color-neutral-900)}.dt-rules{margin:0;padding:0;list-style:none}.dt-rules li+li{margin-top:4px;padding-top:4px;border-top:1px dashed var(--nvb-color-neutral-200)}.dt-edit-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:6px}.dt-edit-actions__hint{margin-right:auto;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-note{margin:0;font-size:12px;line-height:17px;color:var(--nvb-color-neutral-500)}.dt-empty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:48px 16px;text-align:center}.dt-empty__icon{font-size:30px;color:var(--nvb-color-neutral-400)}.dt-empty h3{margin:0;font-size:14px;color:var(--nvb-color-neutral-900)}.dt-empty p{max-width:440px;margin:0 0 6px;color:var(--nvb-color-neutral-600)}.dt-orphan-list{margin:8px 0 0;padding:0;list-style:none}.dt-orphan-list li{display:grid;grid-template-columns:160px minmax(0,1fr) auto;gap:12px;align-items:start;padding:8px 0;border-top:1px solid var(--nvb-color-neutral-200)}@media(max-width:720px){.dt-sheet{padding:18px 16px 32px}.dt-search{width:100%}.dt-menu--add{width:min(340px,100vw - 40px)}.dt-card tbody th{width:90px}}.dt-check--inline{min-height:30px;padding:0 4px;font-size:12px;color:var(--nvb-color-neutral-700)}.dt-card__description td{padding:10px 12px}.dt-card__value{display:flex;align-items:flex-start;gap:8px}.dt-card__value-text{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}.dt-card__remove{flex:0 0 auto;margin:-4px -6px 0 0;opacity:0;transition:opacity .12s ease}.dt-card__row:hover .dt-card__remove,.dt-card__remove:focus-within{opacity:1}.dt-card__add td{padding:4px 8px 6px;background:var(--nvb-color-neutral-025, var(--nvb-color-neutral-050))}.dt-card__add-button{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 3px 4px;border:0;border-radius:6px;background:none;font-size:12px;color:var(--nvb-color-neutral-500);cursor:pointer}.dt-card__add-button:hover:not([disabled]){background:var(--nvb-color-primary-050);color:var(--nvb-color-primary-700)}.dt-card__add-button[disabled]{opacity:.5;cursor:default}.dt-card__add-button nvb-icon{font-size:16px}.dt-card{overflow:visible}.dt-menu--row{width:360px}.dt-menu--row .dt-menu__list{max-height:320px}.dt-menu--row .dt-menu__title{margin:8px 4px 2px}.dt-none{color:var(--nvb-color-neutral-400);font-style:italic}@media(hover:none){.dt-card__remove{opacity:1}}.dt-card thead th{border-radius:7px 7px 0 0}.dt-card tbody tr:last-child>:first-child{border-bottom-left-radius:7px}.dt-card tbody tr:last-child>:last-child{border-bottom-right-radius:7px}\n'],dependencies:[{kind:"component",type:O,selector:"nvb-pro-feature-gate",inputs:["feature","compact"]},{kind:"component",type:L,selector:"nvb-rich-text-editor",inputs:["html","viewerHtml","htmlCss","readOnly","showToolbar","emitOnInput","placeholder","minimalViewer","showWordCount","enabledTools","testId"],outputs:["htmlChange","htmlCssChange","editorFocus","editorBlur"]},{kind:"component",type:k,selector:"nvb-icon",inputs:["name","svg","ariaLabel"]},{kind:"component",type:Z,selector:"builder-icon-button",inputs:["icon","ariaLabel","title","testId","iconSet","size","tone","ghost","dragHandle","active","disabled","className"],outputs:["clicked"]},{kind:"component",type:ee,selector:"builder-search-input",inputs:["value","placeholder","ariaLabel","testId","clearAriaLabel","showSearchIcon"],outputs:["valueChanged","cleared"]},{kind:"component",type:C,selector:"docs-save-status"},{kind:"pipe",type:T,name:"nvbT"}],changeDetection:c.ChangeDetectionStrategy.OnPush})};c.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"22.2.1",ngImport:c,type:ye,decorators:[{type:$,args:[{selector:"nvb-docs-tab",imports:[O,L,k,T,Z,ee,C],changeDetection:R.OnPush,host:{"(document:pointerdown)":"onDocumentPointerDown($event)","(keydown.escape)":"menu.set(null)"},template:"<nvb-pro-feature-gate [feature]=\"'docs.title' | nvbT\">\n @let doc = model();\n <div class=\"dt\" data-testid=\"docs-tab-root\">\n <!-- ── Toolbar ───────────────────────────────────────────────────────── -->\n <header class=\"dt-bar\">\n <div class=\"dt-bar__top\">\n <div class=\"dt-bar__info\">\n <h2 class=\"dt-bar__title\">{{ 'docs.title' | nvbT }}</h2>\n <div class=\"dt-bar__meta\">\n <span class=\"dt-progress\">\n <span class=\"dt-progress__track\" aria-hidden=\"true\">\n <span class=\"dt-progress__fill\" [style.width.%]=\"progress()\"></span>\n </span>\n <span data-testid=\"docs-progress\">\n {{ 'docs.progress' | nvbT: { documented: doc.documentedCount, total: doc.totalCount } }}\n </span>\n </span>\n <span class=\"dt-bar__dot\" aria-hidden=\"true\">·</span>\n <docs-save-status />\n @if (doc.viewCode) {\n <span class=\"dt-bar__dot\" aria-hidden=\"true\">·</span>\n <span class=\"dt-code\" [title]=\"'docs.viewCode.hint' | nvbT\">\n {{ 'docs.viewCode' | nvbT }} <code>{{ doc.viewCode }}</code>\n </span>\n }\n </div>\n </div>\n\n <div class=\"dt-bar__actions\">\n @if (feedback(); as message) {\n <span class=\"dt-feedback\" [attr.data-kind]=\"message.kind\" role=\"status\" data-testid=\"docs-feedback\">\n <nvb-icon [name]=\"message.kind === 'ok' ? 'check' : 'error'\" />\n {{ message.text }}\n </span>\n }\n <button\n type=\"button\"\n class=\"nvb-button nvb-button--primary nvb-button--sm dt-icon-button\"\n (click)=\"copyForConfluence()\"\n [disabled]=\"doc.documentedCount === 0\"\n data-testid=\"docs-copy-confluence\"\n >\n <nvb-icon name=\"content_copy\" />\n {{ 'docs.copyConfluence' | nvbT }}\n </button>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"copyMarkdown()\" [disabled]=\"doc.documentedCount === 0\" data-testid=\"docs-copy-markdown\">\n {{ 'docs.copyMarkdown' | nvbT }}\n </button>\n <span class=\"dt-bar__divider\" aria-hidden=\"true\"></span>\n <builder-icon-button icon=\"download\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'docs.downloadHtml' | nvbT\" [title]=\"'docs.downloadHtml' | nvbT\" (clicked)=\"downloadHtml()\" testId=\"docs-download-html\" />\n <builder-icon-button icon=\"data_object\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'docs.downloadJson' | nvbT\" [title]=\"'docs.downloadJson' | nvbT\" (clicked)=\"downloadJson()\" testId=\"docs-download-json\" />\n </div>\n </div>\n\n <div class=\"dt-bar__filters\">\n <div class=\"dt-search\">\n <builder-search-input\n [value]=\"search()\"\n [placeholder]=\"'docs.search' | nvbT\"\n [ariaLabel]=\"'docs.search' | nvbT\"\n [clearAriaLabel]=\"'docs.clearSearch' | nvbT\"\n testId=\"docs-search\"\n (valueChanged)=\"search.set($event)\"\n (cleared)=\"search.set('')\"\n />\n </div>\n\n <!-- Document an element: the undocumented ones, searchable -->\n <div class=\"dt-menu-anchor\">\n <button\n type=\"button\"\n class=\"nvb-button nvb-button--neutral nvb-button--sm dt-icon-button\"\n [attr.aria-expanded]=\"menu() === 'add'\"\n aria-haspopup=\"listbox\"\n (click)=\"toggleMenu('add')\"\n data-testid=\"docs-add-element\"\n >\n <nvb-icon name=\"add\" />\n {{ 'docs.addElement' | nvbT }}\n <span class=\"dt-count\">{{ doc.undocumented.length }}</span>\n </button>\n @if (menu() === 'add') {\n <div class=\"dt-menu dt-menu--add\" role=\"dialog\" [attr.aria-label]=\"'docs.addElement' | nvbT\">\n <builder-search-input\n class=\"dt-menu__search\"\n [value]=\"addSearch()\"\n [placeholder]=\"'docs.addElement.search' | nvbT\"\n [ariaLabel]=\"'docs.addElement.search' | nvbT\"\n [clearAriaLabel]=\"'docs.clearSearch' | nvbT\"\n testId=\"docs-add-search\"\n (valueChanged)=\"addSearch.set($event)\"\n (cleared)=\"addSearch.set('')\"\n />\n @if (addCandidates().length === 0) {\n <p class=\"dt-menu__empty\">\n @if (doc.undocumented.length === 0) {\n {{ 'docs.addElement.allDone' | nvbT }}\n } @else {\n {{ 'docs.addElement.none' | nvbT }}\n }\n </p>\n } @else {\n <ul class=\"dt-menu__list\" role=\"listbox\">\n @for (entry of addCandidates(); track entry.key) {\n <li role=\"option\" aria-selected=\"false\">\n <button type=\"button\" class=\"dt-menu__item\" (click)=\"addElement(entry)\" [attr.data-testid]=\"'docs-add-option-' + entry.key\">\n <span class=\"dt-menu__label\">{{ entry.node.label }}</span>\n <span class=\"dt-menu__meta\">\n <code>{{ entry.node.name }}</code> · @if (entry.node.context) {{{ entry.node.context }} · }{{ entry.node.typeLabel }}@if (entry.node.pageLabel) { · {{ entry.node.pageLabel }}}\n </span>\n </button>\n </li>\n }\n </ul>\n }\n </div>\n }\n </div>\n\n <label class=\"dt-check dt-check--inline\">\n <input type=\"checkbox\" [checked]=\"options().groupByPage\" (change)=\"updateOptions({ groupByPage: $any($event.target).checked })\" data-testid=\"docs-group-by-page\" />\n <span>{{ 'docs.groupByPage' | nvbT }}</span>\n </label>\n\n @if (docs.browserOnly) {\n <span class=\"dt-local-note\" [title]=\"'docs.browserOnly.text' | nvbT\">\n <nvb-icon [svg]=\"icons.cloud_off\" />\n {{ 'docs.browserOnly.title' | nvbT }}\n </span>\n }\n </div>\n </header>\n\n <!-- ── The document ─────────────────────────────────────────────────── -->\n <main class=\"dt-sheet-wrap\">\n <article class=\"dt-sheet\" data-testid=\"docs-sheet\">\n <input\n class=\"dt-sheet__title\"\n [value]=\"doc.title\"\n (change)=\"onTitleChange($any($event.target).value)\"\n [placeholder]=\"docs.viewTitle()\"\n [attr.aria-label]=\"'docs.titleField' | nvbT\"\n data-testid=\"docs-title\"\n />\n\n <section class=\"dt-summary\">\n @if (editingSummary()) {\n <nvb-rich-text-editor\n [html]=\"doc.summary\"\n [enabledTools]=\"tools\"\n [placeholder]=\"'docs.summary.placeholder' | nvbT\"\n (htmlChange)=\"onSummaryChange($event)\"\n testId=\"docs-summary-editor\"\n />\n <div class=\"dt-edit-actions\">\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"editingSummary.set(false)\">\n {{ 'docs.done' | nvbT }}\n </button>\n </div>\n } @else if (doc.summary) {\n <button type=\"button\" class=\"dt-rich dt-rich--button\" (click)=\"editingSummary.set(true)\" [attr.aria-label]=\"'docs.summary.edit' | nvbT\">\n <div [innerHTML]=\"doc.summary\"></div>\n </button>\n } @else {\n <button type=\"button\" class=\"dt-add\" (click)=\"editingSummary.set(true)\" data-testid=\"docs-summary-add\">\n <nvb-icon name=\"add\" />\n {{ 'docs.summary.add' | nvbT }}\n </button>\n }\n </section>\n\n @if (doc.sections.length === 0) {\n <div class=\"dt-empty\" data-testid=\"docs-empty\">\n <nvb-icon name=\"description\" class=\"dt-empty__icon\" />\n <h3>{{ 'docs.empty.title' | nvbT }}</h3>\n <p>\n {{\n 'docs.empty.text' | nvbT\n }}\n </p>\n @if (doc.undocumented.length > 0) {\n <button type=\"button\" class=\"nvb-button nvb-button--primary nvb-button--sm dt-icon-button dt-menu-anchor\" (click)=\"toggleMenu('add')\">\n <nvb-icon name=\"add\" />\n {{ 'docs.addElement' | nvbT }}\n </button>\n }\n </div>\n } @else if (visibleSections().length === 0) {\n <div class=\"dt-empty\">\n <h3>{{ 'docs.noMatch.title' | nvbT }}</h3>\n <p>{{ 'docs.noMatch.text' | nvbT }}</p>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"search.set('')\">\n {{ 'docs.noMatch.clear' | nvbT }}\n </button>\n </div>\n }\n\n @for (section of visibleSections(); track section.name) {\n <section class=\"dt-section\">\n @if (section.label) {\n <h2 class=\"dt-section__title\">\n {{ section.label }}\n <span class=\"dt-count\">{{ section.entries.length }}</span>\n </h2>\n }\n\n @for (entry of section.entries; track entry.key) {\n <table class=\"dt-card\" [attr.data-key]=\"entry.key\" [attr.data-testid]=\"'docs-card-' + entry.key\" [class.dt-card--editing]=\"editing() === entry.key\">\n <thead>\n <tr>\n <th colspan=\"2\" scope=\"colgroup\">\n <div class=\"dt-card__head\">\n <button type=\"button\" class=\"dt-card__title\" (click)=\"openInBuilder(entry)\" [title]=\"'docs.openInBuilder' | nvbT\">\n {{ entry.node.label }}\n </button>\n <code class=\"dt-card__name\">{{ entry.node.name }}</code>\n <span class=\"dt-card__type\">\n @if (entry.node.context) {\n <span class=\"dt-card__context\">{{ entry.node.context }}</span> ·\n }\n {{ entry.node.typeLabel }}\n </span>\n </div>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr class=\"dt-card__description\">\n <td colspan=\"2\">\n @if (editing() === entry.key) {\n <nvb-rich-text-editor\n [html]=\"entry.description\"\n [enabledTools]=\"tools\"\n [placeholder]=\"'docs.panel.placeholder' | nvbT\"\n (htmlChange)=\"onDescriptionChange(entry, $event)\"\n [testId]=\"'docs-editor-' + entry.key\"\n />\n <div class=\"dt-edit-actions\">\n <span class=\"dt-edit-actions__hint\">{{ 'docs.emptyRemoves' | nvbT }}</span>\n <button type=\"button\" class=\"nvb-button nvb-button--neutral nvb-button--sm\" (click)=\"finishEditing()\" [attr.data-testid]=\"'docs-done-' + entry.key\">\n {{ 'docs.done' | nvbT }}\n </button>\n </div>\n } @else {\n <button type=\"button\" class=\"dt-rich dt-rich--button\" (click)=\"edit(entry)\" [attr.data-testid]=\"'docs-edit-' + entry.key\" [attr.aria-label]=\"'docs.edit' | nvbT: { name: entry.node.label }\">\n <div [innerHTML]=\"entry.description\"></div>\n </button>\n }\n </td>\n </tr>\n @for (row of entry.rows; track row.id) {\n <tr class=\"dt-card__row\" [attr.data-row]=\"row.id\">\n <th scope=\"row\">{{ row.label }}</th>\n <td>\n <div class=\"dt-card__value\">\n <div class=\"dt-card__value-text\">\n @if (row.empty) {\n <span class=\"dt-none\" [title]=\"'docs.row.emptyHint' | nvbT\">{{ 'docs.row.empty' | nvbT }}</span>\n } @else if (row.text !== undefined) {\n {{ row.text }}\n } @else {\n <ul class=\"dt-rules\">\n @for (cell of row.cells; track $index) {\n <li [title]=\"cell.expression ?? ''\">\n @for (segment of cell.segments; track $index) {\n @if (isRef(segment)) {<strong>{{ segment.label }}</strong>} @else {{{ segment.text }}}\n }\n </li>\n }\n </ul>\n }\n </div>\n <builder-icon-button\n class=\"dt-card__remove\"\n icon=\"close\"\n size=\"sm\"\n [ghost]=\"true\"\n [ariaLabel]=\"'docs.row.removeNamed' | nvbT: { name: row.label }\"\n [title]=\"'docs.row.remove' | nvbT\"\n (clicked)=\"removeRow(entry, row.id)\"\n [testId]=\"'docs-row-remove-' + entry.key + '-' + row.id\"\n />\n </div>\n </td>\n </tr>\n }\n @if (entry.description) {\n <tr class=\"dt-card__add\">\n <td colspan=\"2\">\n <div class=\"dt-menu-anchor\">\n <button\n type=\"button\"\n class=\"dt-card__add-button\"\n [attr.aria-expanded]=\"menu() === 'row' && rowMenuKey() === entry.key\"\n aria-haspopup=\"listbox\"\n (click)=\"toggleRowMenu(entry)\"\n [disabled]=\"entry.options.length === 0\"\n [title]=\"entry.options.length === 0 ? ('docs.row.nothingLeft' | nvbT) : ''\"\n [attr.data-testid]=\"'docs-row-add-' + entry.key\"\n >\n <nvb-icon name=\"add\" />\n {{ 'docs.row.add' | nvbT }}\n </button>\n @if (menu() === 'row' && rowMenuKey() === entry.key) {\n <div class=\"dt-menu dt-menu--row\" role=\"dialog\" [attr.aria-label]=\"'docs.row.add' | nvbT\">\n <builder-search-input\n class=\"dt-menu__search\"\n [value]=\"rowSearch()\"\n [placeholder]=\"'docs.row.search' | nvbT\"\n [ariaLabel]=\"'docs.row.search' | nvbT\"\n [clearAriaLabel]=\"'docs.clearSearch' | nvbT\"\n [testId]=\"'docs-row-search-' + entry.key\"\n (valueChanged)=\"rowSearch.set($event)\"\n (cleared)=\"rowSearch.set('')\"\n />\n <div class=\"dt-menu__list\">\n @let fromLogic = rowOptions(entry, 'logic');\n @let fromProperties = rowOptions(entry, 'property');\n @if (fromLogic.length > 0) {\n <p class=\"dt-menu__title\">{{ 'docs.row.fromLogic' | nvbT }}</p>\n @for (option of fromLogic; track option.id) {\n <button type=\"button\" class=\"dt-menu__item\" (click)=\"addRow(entry, option)\" [attr.data-testid]=\"'docs-row-option-' + entry.key + '-' + option.id\">\n <span class=\"dt-menu__label\">{{ option.label }}</span>\n <span class=\"dt-menu__meta\">{{ option.preview }}</span>\n </button>\n }\n }\n @if (fromProperties.length > 0) {\n <p class=\"dt-menu__title\">{{ 'docs.row.fromProperties' | nvbT }}</p>\n @for (option of fromProperties; track option.id) {\n <button type=\"button\" class=\"dt-menu__item\" (click)=\"addRow(entry, option)\" [attr.data-testid]=\"'docs-row-option-' + entry.key + '-' + option.id\">\n <span class=\"dt-menu__label\">{{ option.label }}</span>\n <span class=\"dt-menu__meta\">{{ option.preview }}</span>\n </button>\n }\n }\n @if (fromLogic.length === 0 && fromProperties.length === 0) {\n <p class=\"dt-menu__empty\">{{ 'docs.row.noMatch' | nvbT }}</p>\n }\n </div>\n </div>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n </section>\n }\n\n @if (doc.orphans.length > 0) {\n <section class=\"dt-section dt-orphans\" data-testid=\"docs-orphans\">\n <h2 class=\"dt-section__title\">\n {{ 'docs.orphans' | nvbT }}\n <span class=\"dt-count\">{{ doc.orphans.length }}</span>\n </h2>\n <p class=\"dt-note\">\n {{ 'docs.orphans.text' | nvbT }}\n </p>\n <ul class=\"dt-orphan-list\">\n @for (orphan of doc.orphans; track orphan.key) {\n <li>\n <code>{{ orphan.key }}</code>\n <div class=\"dt-rich\" [innerHTML]=\"orphan.description\"></div>\n <builder-icon-button icon=\"delete\" tone=\"danger\" size=\"sm\" [ghost]=\"true\" [ariaLabel]=\"'docs.orphans.remove' | nvbT\" [title]=\"'docs.orphans.remove' | nvbT\" (clicked)=\"removeOrphan(orphan.key)\" [testId]=\"'docs-orphan-remove-' + orphan.key\" />\n </li>\n }\n </ul>\n </section>\n }\n </article>\n </main>\n </div>\n</nvb-pro-feature-gate>\n",styles:['@charset "UTF-8";:host{--nvb-input-height: 32px;display:block;height:100%;min-height:0;overflow:hidden;font-size:12px;color:var(--nvb-color-neutral-800);background:color-mix(in oklab,var(--nvb-color-neutral-050) 84%,var(--nvb-color-neutral-000))}button:not(.nvb-button){font:inherit;color:inherit}.dt{height:100%;min-height:0;display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;padding:10px;box-sizing:border-box}.dt-bar{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)}.dt-bar__top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px 16px;padding:10px 12px}.dt-bar__filters{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:8px 12px;border-top:1px solid var(--nvb-color-neutral-200)}.dt-bar__info{min-width:0}.dt-bar__title{margin:0;font-size:13px;font-weight:600;line-height:18px;color:var(--nvb-color-neutral-900)}.dt-bar__meta{display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;margin-top:3px;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-bar__dot{color:var(--nvb-color-neutral-300)}.dt-progress{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}.dt-progress__track{position:relative;width:64px;height:4px;overflow:hidden;border-radius:999px;background:var(--nvb-color-neutral-200)}.dt-progress__fill{position:absolute;inset:0 auto 0 0;border-radius:999px;background:var(--nvb-color-primary-600)}.dt-code code{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--nvb-color-neutral-700)}.dt-bar__actions{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.dt-bar__primary{display:inline-flex;align-items:center;gap:6px}.dt-bar__actions .nvb-button[disabled]{opacity:.5;cursor:not-allowed}.dt-bar__divider{width:1px;height:18px;margin:0 2px;background:var(--nvb-color-neutral-200)}.dt-feedback{display:inline-flex;align-items:center;gap:4px;margin-right:6px;font-size:12px;color:var(--nvb-color-success-700, #1a7f37)}.dt-feedback[data-kind=error]{color:var(--nvb-color-risk-700, #b42318)}.dt-feedback nvb-icon{font-size:15px}.dt-icon-button{display:inline-flex;align-items:center;gap:6px}.dt-icon-button nvb-icon{font-size:16px}.dt-search{width:260px;max-width:100%}.dt-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;font-variant-numeric:tabular-nums}.dt-menu-anchor{position:relative}.dt-menu{position:absolute;top:calc(100% + 6px);left:0;z-index:20;box-sizing:border-box;padding:8px;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;background:var(--nvb-color-neutral-000);box-shadow:0 10px 28px #00000029}.dt-menu--add{width:340px}.dt-menu--rows{width:260px;padding:10px 12px}.dt-menu__search{display:block;margin-bottom:6px}.dt-menu__list{max-height:300px;margin:0;padding:0;overflow:auto;list-style:none}.dt-menu__item{display:grid;width:100%;padding:6px 8px;border:0;border-radius:6px;background:none;text-align:left;cursor:pointer}.dt-menu__item:hover,.dt-menu__item:focus-visible{outline:none;background:var(--nvb-color-primary-050)}.dt-menu__label{font-weight:500;color:var(--nvb-color-neutral-900)}.dt-menu__meta{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-menu__meta code,.dt-card__name{font-family:var(--nvb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)}.dt-menu__empty,.dt-menu__hint{margin:6px 4px;font-size:11px;line-height:16px;color:var(--nvb-color-neutral-500)}.dt-menu__title{margin:0 0 4px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--nvb-color-neutral-500)}.dt-menu__rule{margin:8px 0;border:0;border-top:1px solid var(--nvb-color-neutral-200)}.dt-check{display:flex;align-items:center;gap:8px;min-height:26px;cursor:pointer}.dt-check input{margin:0;accent-color:var(--nvb-color-primary-600)}.dt-check--fixed{cursor:default;color:var(--nvb-color-neutral-500)}.dt-local-note{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-local-note nvb-icon{font-size:14px}.dt-sheet-wrap{min-height:0;overflow:auto;border:1px solid var(--nvb-card-border-color, var(--nvb-color-neutral-200));border-radius:var(--nvb-card-radius, 8px);background:var(--nvb-color-neutral-000)}.dt-sheet{max-width:920px;margin:0 auto;padding:28px 32px 48px;font-size:13px;line-height:20px}.dt-sheet__title{display:block;width:100%;padding:2px 6px;margin:0 0 6px -6px;border:1px solid transparent;border-radius:6px;background:transparent;font:inherit;font-size:24px;font-weight:600;line-height:32px;color:var(--nvb-color-neutral-900)}.dt-sheet__title:hover{border-color:var(--nvb-color-neutral-200)}.dt-sheet__title:focus{outline:none;border-color:var(--nvb-color-primary-500);box-shadow:var(--nvb-focus-ring, 0 0 0 2px var(--nvb-color-primary-200))}.dt-summary{margin:4px 0 20px}.dt-add{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 3px 4px;border:1px dashed var(--nvb-color-neutral-300);border-radius:6px;background:none;font-size:12px;color:var(--nvb-color-neutral-500);cursor:pointer}.dt-add:hover{border-color:var(--nvb-color-primary-500);color:var(--nvb-color-primary-700);background:var(--nvb-color-primary-050)}.dt-add nvb-icon{font-size:15px}.dt-rich{overflow-wrap:anywhere}.dt-rich :is(p,ul,ol){margin:0 0 6px}.dt-rich :is(p,ul,ol):last-child{margin-bottom:0}.dt-rich :is(ul,ol){padding-left:18px}.dt-rich--button{display:block;width:100%;margin:-4px -6px;padding:4px 6px;border:1px solid transparent;border-radius:6px;background:none;text-align:left;cursor:text}.dt-rich--button:hover{border-color:var(--nvb-color-neutral-200);background:var(--nvb-color-neutral-025, var(--nvb-color-neutral-050))}.dt-edit-actions{display:flex;justify-content:flex-end;margin-top:6px}.dt-section{margin-top:28px}.dt-section__title{display:flex;align-items:center;gap:8px;margin:0 0 10px;font-size:17px;font-weight:600;line-height:24px;color:var(--nvb-color-neutral-900)}.dt-card{width:100%;margin:0 0 12px;border-collapse:separate;border-spacing:0;border:1px solid var(--nvb-color-neutral-200);border-radius:8px;overflow:hidden;font-size:13px;line-height:19px}.dt-card--editing{border-color:var(--nvb-color-primary-500);box-shadow:0 0 0 2px color-mix(in oklab,var(--nvb-color-primary-500) 18%,transparent)}.dt-card thead th{padding:8px 12px;border-bottom:1px solid var(--nvb-color-neutral-200);background:var(--nvb-color-neutral-050);text-align:left;font-weight:400}.dt-card__head{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px}.dt-card__title{padding:0;border:0;background:none;font-size:14px;font-weight:600;color:var(--nvb-color-neutral-900);cursor:pointer}.dt-card__title:hover{color:var(--nvb-color-primary-700);text-decoration:underline}.dt-card__name{font-size:11.5px;color:var(--nvb-color-neutral-600)}.dt-card__context{font-weight:600;color:var(--nvb-color-primary-700)}.dt-card__type{font-size:11.5px;color:var(--nvb-color-neutral-500)}.dt-card tbody th,.dt-card tbody td{padding:8px 12px;border-top:1px solid var(--nvb-color-neutral-100, var(--nvb-color-neutral-200));text-align:left;vertical-align:top}.dt-card tbody tr:first-child>*{border-top:0}.dt-card tbody th{width:150px;font-size:12px;font-weight:600;color:var(--nvb-color-neutral-500);white-space:nowrap}.dt-card tbody td{color:var(--nvb-color-neutral-800)}.dt-card strong{font-weight:600;color:var(--nvb-color-neutral-900)}.dt-rules{margin:0;padding:0;list-style:none}.dt-rules li+li{margin-top:4px;padding-top:4px;border-top:1px dashed var(--nvb-color-neutral-200)}.dt-edit-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:6px}.dt-edit-actions__hint{margin-right:auto;font-size:11px;color:var(--nvb-color-neutral-500)}.dt-note{margin:0;font-size:12px;line-height:17px;color:var(--nvb-color-neutral-500)}.dt-empty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:48px 16px;text-align:center}.dt-empty__icon{font-size:30px;color:var(--nvb-color-neutral-400)}.dt-empty h3{margin:0;font-size:14px;color:var(--nvb-color-neutral-900)}.dt-empty p{max-width:440px;margin:0 0 6px;color:var(--nvb-color-neutral-600)}.dt-orphan-list{margin:8px 0 0;padding:0;list-style:none}.dt-orphan-list li{display:grid;grid-template-columns:160px minmax(0,1fr) auto;gap:12px;align-items:start;padding:8px 0;border-top:1px solid var(--nvb-color-neutral-200)}@media(max-width:720px){.dt-sheet{padding:18px 16px 32px}.dt-search{width:100%}.dt-menu--add{width:min(340px,100vw - 40px)}.dt-card tbody th{width:90px}}.dt-check--inline{min-height:30px;padding:0 4px;font-size:12px;color:var(--nvb-color-neutral-700)}.dt-card__description td{padding:10px 12px}.dt-card__value{display:flex;align-items:flex-start;gap:8px}.dt-card__value-text{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}.dt-card__remove{flex:0 0 auto;margin:-4px -6px 0 0;opacity:0;transition:opacity .12s ease}.dt-card__row:hover .dt-card__remove,.dt-card__remove:focus-within{opacity:1}.dt-card__add td{padding:4px 8px 6px;background:var(--nvb-color-neutral-025, var(--nvb-color-neutral-050))}.dt-card__add-button{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 3px 4px;border:0;border-radius:6px;background:none;font-size:12px;color:var(--nvb-color-neutral-500);cursor:pointer}.dt-card__add-button:hover:not([disabled]){background:var(--nvb-color-primary-050);color:var(--nvb-color-primary-700)}.dt-card__add-button[disabled]{opacity:.5;cursor:default}.dt-card__add-button nvb-icon{font-size:16px}.dt-card{overflow:visible}.dt-menu--row{width:360px}.dt-menu--row .dt-menu__list{max-height:320px}.dt-menu--row .dt-menu__title{margin:8px 4px 2px}.dt-none{color:var(--nvb-color-neutral-400);font-style:italic}@media(hover:none){.dt-card__remove{opacity:1}}.dt-card thead th{border-radius:7px 7px 0 0}.dt-card tbody tr:last-child>:first-child{border-bottom-left-radius:7px}.dt-card tbody tr:last-child>:last-child{border-bottom-right-radius:7px}\n']}]}]});const U={groupByPage:true};const xe="documentation";function st(t,e){const n=(r=U)=>{t.refresh();return H(t.graph(),t.docs(),r,t.viewTitle(),new Set,t.definitionLookup())};const o=()=>new Date().toISOString().slice(0,10);const a=r=>{const d=String(r??"").trim();const l=we(n());return l.find(h=>h.key===d)??l.find(h=>h.key.split(".").pop()===d)??null};const s=r=>{const d=String(r["html"]??"").trim();if(d)return d;return String(r["text"]??"").trim().split(/\n{2,}/).map(l=>`<p>${it(l).replace(/\n/g,"<br>")}</p>`).join("")};return{id:xe,title:"Documentation",description:"Field documentation of the open view, kept outside the view JSON under the view code: a description per element (what it means, who fills it in, where the value goes), a summary of the whole form, and the generated field reference table with the rules of each field. Write descriptions with setDescription; never put documentation text into element properties.",query:{element:"Optional element name or path: just that element, with its description and rules.",format:'Optional "json" (default), "markdown" or "html": markdown and html return the finished table as the person would copy it.',missingOnly:"Optional true: only fields that have no description yet."},available:()=>e.proAccess()==="ready",unavailableReason:()=>"The documentation plugin needs an active NGX View Builder license key in the builder settings.",read:r=>{const d=n();const l=String(r["format"]??"json");if(l==="markdown")return z(d,U,o());if(l==="html")return E(d,U,o());const h=String(r["element"]??"").trim();const x=r["missingOnly"]===true||r["missingOnly"]==="true";const _=we(d).map(u=>({row:u,page:u.node.pageLabel??""})).filter(({row:u})=>(!h||u.key===h||u.key.split(".").pop()===h)&&(!x||!u.description));if(h&&_.length===0)throw new Error(`No documented field '${h}'. Read without a query to see every field name.`);return{viewCode:d.viewCode||null,title:d.title,summary:w(d.summary)||null,storage:t.browserOnly?"browser":t.storageLabel(),documented:d.documentedCount,elementsCounted:d.totalCount,elements:_.map(({row:u,page:p})=>({name:u.key,label:u.node.label,type:u.node.typeLabel,...u.node.context?{context:u.node.context}:{},...p?{page:p}:{},description:w(u.description)||null,...u.description?{descriptionHtml:u.description}:{},required:u.generated.required.map(i=>re(i.segments)).join("; ")||"No",rules:N.filter(i=>i!=="required").flatMap(i=>u.generated[i]).map(i=>re(i.segments)),...u.rows.length?{rowsInTable:u.rows.map(i=>i.id)}:{}})),...d.orphans.length?{descriptionsWithoutField:d.orphans.map(u=>u.key)}:{}}},commands:[{name:"setDescription",description:"Writes the description of one element. Plain text with blank lines between paragraphs, or simple HTML (p, strong, em, ul, ol, li, a). Replaces what was there; an empty text removes it. Saved for the whole team right away.",params:{element:'Element name or path, e.g. "vatNumber" or "billing.city".',text:"The description as plain text.",html:"Or the description as HTML instead of text."},example:{element:"vatNumber",text:"VAT number of the applicant company. Required for EU companies above the VAT threshold; finance checks it against VIES."},run:r=>{t.ensureLoaded();const d=a(String(r["element"]??""));if(!d)throw new Error(`No element '${String(r["element"]??"")}' in this view.`);t.setDescription(d.key,s(r));return{element:d.key,description:w(t.descriptionOf(d.key))||null}}},{name:"setRows",description:"Sets the rows shown under one element's description in the documentation table, in order. Use \"required\", \"logic\", \"validation\", \"data\", \"events\" for what the view's logic says, or \"prop:<key>\" for one of the element's own properties, such as \"prop:placeholder\". The element needs a description first.",params:{element:"Element name or path.",rows:'Array of row ids, e.g. ["required", "logic", "prop:placeholder"]. An empty array clears them.'},example:{element:"vatNumber",rows:["required","validation"]},run:r=>{t.ensureLoaded();const d=a(String(r["element"]??""));if(!d||!t.descriptionOf(d.key))throw new Error(`Element '${String(r["element"]??"")}' has no description yet. Write one with setDescription first.`);const l=Array.isArray(r["rows"])?r["rows"].map(h=>String(h)):[];t.setRows(d.key,l);return{element:d.key,rows:t.rowsOf(d.key)}}},{name:"setSummary",description:"Writes the summary at the top of the documentation: what the form is for, who uses it, what happens after it is sent.",params:{text:"The summary as plain text.",html:"Or the summary as HTML."},run:r=>{t.ensureLoaded();t.setSummary(s(r));return{summary:w(t.docs()?.summary)||null}}},{name:"setTitle",description:"Sets the title of the generated documentation page. An empty title goes back to the view title.",params:{title:"The page title."},run:r=>{t.ensureLoaded();t.setTitle(String(r["title"]??""));return{title:n().title}}}]}}function we(t){return[...t.sections.flatMap(e=>e.entries),...t.undocumented].sort((e,n)=>e.node.order-n.node.order)}function it(t){return t.replace(/[&<>"]/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[e])}const W={"docs.addElement":"Document an element","docs.addElement.allDone":"Every element is documented.","docs.addElement.none":"No undocumented element matches.","docs.addElement.search":"Element name or label","docs.browserOnly.text":"Give provideNgxViewBuilderDocs() a storage, such as createHttpDocsStorage('/api/view-docs'), and the team shares one copy on your server.","docs.browserOnly.title":"Kept in this browser only","docs.clearSearch":"Clear search","docs.clipboardBlocked":"The browser blocked the clipboard. Use Download HTML instead.","docs.copied":"Copied {count} elements. Paste into a Confluence page with Ctrl+V.","docs.copyConfluence":"Copy for Confluence","docs.copyMarkdown":"Copy Markdown","docs.done":"Done","docs.downloadHtml":"Download as HTML page","docs.downloadJson":"Download the documentation file (JSON)","docs.edit":"Edit the description of {name}","docs.empty.text":"Select an element on the Builder tab and write in Properties → Documentation, or pick one here. Only documented elements appear on this page.","docs.empty.title":"No element is documented yet","docs.emptyRemoves":"Leave it empty to remove the element from the documentation.","docs.error.load":"The documentation could not be loaded.","docs.error.loadStatus":"Loading documentation failed ({status}).","docs.error.save":"The documentation could not be saved.","docs.error.saveStatus":"Saving documentation failed ({status}).","docs.error.unreadable":"The saved documentation could not be read, so nothing was saved over it. Check the connection and try again.","docs.export.noCode":"not set","docs.export.notInView":"{name} (not in this view)","docs.export.property":"Property","docs.export.summary":"View code: {code} · Documented: {documented} of {total} elements · Generated: {date}","docs.export.value":"Value","docs.groupByPage":"A heading for each page","docs.markdownCopied":"Markdown copied.","docs.no":"No","docs.noMatch.clear":"Clear search","docs.noMatch.text":"No documented element fits the search.","docs.noMatch.title":"Nothing matches","docs.notOnPage":"Not on a page","docs.nothingToDownload":"Nothing is documented yet, so there is no file to download.","docs.openInBuilder":"Edit in builder","docs.orphans":"Descriptions without an element","docs.orphans.remove":"Remove this description","docs.orphans.text":"These elements were renamed or removed from the view. They are not copied or downloaded. Rename the element back to reuse the text, or remove it here.","docs.panel.generated":"Added to the table from the view","docs.panel.hint":"What this field means, who fills it in and where the value goes. Kept in the documentation, never in the view JSON.","docs.panel.open":"Open documentation","docs.panel.placeholder":"Describe this field…","docs.progress":"{documented} of {total} elements documented","docs.prop.defaultValue":"Default value","docs.prop.readOnly":"Read only","docs.prop.value":"Default value","docs.required.when":"When","docs.row.add":"Add a row","docs.row.data":"Data source","docs.row.empty":"Not set in the view","docs.row.emptyHint":"The view has nothing for this any more. Remove the row or put it back in the builder.","docs.row.events":"Events","docs.row.fromLogic":"From the logic","docs.row.fromProperties":"Properties","docs.row.logic":"Logic","docs.row.noMatch":"Nothing matches.","docs.row.nothingLeft":"Everything this element has is already in the table.","docs.row.remove":"Remove the row","docs.row.removeNamed":"Remove the row {name}","docs.row.required":"Required","docs.row.search":"Find a property","docs.row.validation":"Validation","docs.search":"Find in the documentation","docs.status.browserOnly":"Kept in this browser","docs.status.browserOnlyHint":"Pass a storage to provideNgxViewBuilderDocs() so the whole team shares the documentation.","docs.status.error":"Not saved.","docs.status.loading":"Loading…","docs.status.retry":"Try again","docs.status.savedTo":"Saved to {storage}","docs.status.saving":"Saving…","docs.status.storedOn":"Stored on {storage}","docs.storage.browser":"this browser","docs.storage.server":"server","docs.summary.add":"Add a summary of the whole form","docs.summary.edit":"Edit the summary","docs.summary.placeholder":"What is this form for, who uses it and what happens after it is sent?","docs.title":"Documentation","docs.titleField":"Document title","docs.untitledView":"View","docs.viewCode":"View code","docs.viewCode.hint":"The documentation is stored under this code, apart from the view JSON.","docs.yes":"Yes","propertyCategory.documentation":"Documentation","tab.documentation":"Documentation","tab.documentation.description":"Field descriptions, ready for Confluence"};const _e="ngx-view-builder-docs";const J="documentation";function dt(t={}){Ae(W);return Ce([{provide:F,useValue:t},Re({featurePacks:[{id:_e,title:"Documentation",capabilities:[J],builderTabs:[{id:B,label:"tab.documentation",component:ye,order:370,icon:"description",description:"tab.documentation.description"}],propertyPanels:[{id:J,label:"propertyCategory.documentation",icon:"description",component:le}],uiDictionaries:{en:{...$e,...W}}}]}),De(()=>{b(ze).register(st(b(S),b(Ee)))})])}export{j as DEFAULT_DOCS_OPTIONS,xe as NGX_VIEW_BUILDER_DOCS_AI_ID,J as NGX_VIEW_BUILDER_DOCS_CAPABILITY,F as NGX_VIEW_BUILDER_DOCS_CONFIG,_e as NGX_VIEW_BUILDER_DOCS_FEATURE_PACK_ID,B as NGX_VIEW_BUILDER_DOCS_TAB_ID,W as NGX_VIEW_BUILDER_DOCS_UI_EN,S as NgxViewBuilderDocsService,H as buildDocsModel,je as createHttpDocsStorage,se as createLocalDocsStorage,E as docsToHtml,z as docsToMarkdown,dt as provideNgxViewBuilderDocs};
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@ngxviewbuilder/plugin-docs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.13.1",
4
+ "description": "Documentation plugin for the NGX View Builder designer: describe every field next to its properties and get a ready field reference table for Confluence, kept outside the view JSON.",
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
+ "documentation",
20
+ "confluence",
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
+ "rxjs": "^7.4.0"
32
+ },
33
+ "dependencies": {
34
+ "tslib": "^2.6.0",
35
+ "@ngxviewbuilder/plugin-shared": "^0.13.0"
36
+ },
37
+ "module": "fesm2022/ngxviewbuilder-plugin-docs.mjs",
38
+ "typings": "types/ngxviewbuilder-plugin-docs.d.ts",
39
+ "exports": {
40
+ "./package.json": {
41
+ "default": "./package.json"
42
+ },
43
+ ".": {
44
+ "types": "./types/ngxviewbuilder-plugin-docs.d.ts",
45
+ "default": "./fesm2022/ngxviewbuilder-plugin-docs.mjs"
46
+ }
47
+ },
48
+ "sideEffects": false,
49
+ "type": "module"
6
50
  }
@@ -0,0 +1,272 @@
1
+ import * as i0 from "@angular/core";
2
+ import { EnvironmentProviders, InjectionToken } from "@angular/core";
3
+ import { INgxViewBuilderAiPlugin, IStructure, LicenseService } from "@ngxviewbuilder/designer";
4
+ import { Observable } from "rxjs";
5
+ import { IViewLogicGraph, IViewLogicNode, NvbTranslate, ViewLogicSegment } from "@ngxviewbuilder/plugin-shared";
6
+ /**
7
+ * The documentation of one view. Kept apart from the view JSON on purpose: the view
8
+ * stays what the runtime renders, and the text people write about it lives in its own
9
+ * file keyed by the view code and the element names.
10
+ */
11
+ export interface INgxViewBuilderViewDocs {
12
+ format: 'ngx-view-builder-docs';
13
+ version: 1;
14
+ /** The view this documentation belongs to. */
15
+ viewCode: string;
16
+ /** Title for the generated page; falls back to the view's own title. */
17
+ title?: string;
18
+ /** What the whole view is for, as rich text (HTML). */
19
+ summary?: string;
20
+ updatedAt?: string;
21
+ /** Descriptions keyed by element name, or by path for panel children (`billing.city`). */
22
+ elements: Record<string, INgxViewBuilderElementDoc>;
23
+ }
24
+ export interface INgxViewBuilderElementDoc {
25
+ /** Rich text (HTML): what the field means, who fills it in, where the value goes. */
26
+ description: string;
27
+ /**
28
+ * Extra rows shown under the description, in order, picked per element: `required`,
29
+ * `logic`, `validation`, `data`, `events` (filled in from the view's logic) or
30
+ * `prop:<key>` for one of the element's own properties, such as `prop:placeholder`.
31
+ */
32
+ rows?: string[];
33
+ updatedAt?: string;
34
+ }
35
+ type MaybeAsync<T> = T | Promise<T> | Observable<T>;
36
+ /**
37
+ * Where documentation is kept. Point it at your server so the whole team shares it; an
38
+ * Angular HttpClient call returns an Observable, which is accepted as is.
39
+ */
40
+ export interface INgxViewBuilderDocsStorage {
41
+ /** The stored documentation of a view, or null when there is none yet. */
42
+ load(viewCode: string): MaybeAsync<INgxViewBuilderViewDocs | null | undefined>;
43
+ save(docs: INgxViewBuilderViewDocs): MaybeAsync<unknown>;
44
+ /** Shown next to the save status, e.g. "Saved to Confluence sync". Defaults to "server". */
45
+ label?: string;
46
+ }
47
+ export interface INgxViewBuilderDocsConfig {
48
+ /**
49
+ * Where to keep documentation. Without it, it stays in this browser only. A function
50
+ * runs in Angular's injection context, so it can `inject(HttpClient)`:
51
+ * `storage: () => { const http = inject(HttpClient); return { load: …, save: … }; }`.
52
+ */
53
+ storage?: INgxViewBuilderDocsStorage | (() => INgxViewBuilderDocsStorage);
54
+ /**
55
+ * The code that identifies the open view, when the host has one (its id in your
56
+ * database). Without it the plugin uses `settings.viewCode` from the view, and writes a
57
+ * new one there the first time something is documented.
58
+ */
59
+ viewCode?: string | ((structure: IStructure | null) => string | null | undefined);
60
+ /** How long typing pauses before a save, in ms. Defaults to 800. */
61
+ saveDelayMs?: number;
62
+ }
63
+ export declare const NGX_VIEW_BUILDER_DOCS_CONFIG: InjectionToken<INgxViewBuilderDocsConfig>;
64
+ export type NgxViewBuilderDocsSaveState = 'idle' | 'loading' | 'saving' | 'saved' | 'error';
65
+ export declare const NGX_VIEW_BUILDER_DOCS_FEATURE_PACK_ID = "ngx-view-builder-docs";
66
+ export declare const NGX_VIEW_BUILDER_DOCS_CAPABILITY = "documentation";
67
+ /**
68
+ * Adds field documentation to the designer: a Documentation section in the Properties
69
+ * panel of every element, and a Documentation tab with the generated field reference
70
+ * table, ready to paste into Confluence. The text is stored apart from the view JSON,
71
+ * through `config.storage`. Needs an active license key in the builder settings.
72
+ */
73
+ export declare function provideNgxViewBuilderDocs(config?: INgxViewBuilderDocsConfig): EnvironmentProviders;
74
+ /** Keeps documentation in this browser. The default, and only meant for trying it out. */
75
+ export declare function createLocalDocsStorage(): INgxViewBuilderDocsStorage;
76
+ interface INgxViewBuilderHttpDocsStorageOptions {
77
+ /** Extra request headers, such as an Authorization header. Called before every request. */
78
+ headers?: () => Record<string, string> | Promise<Record<string, string>>;
79
+ /** Sent with cookies (`credentials: 'include'`) for session based auth. */
80
+ withCredentials?: boolean;
81
+ }
82
+ /**
83
+ * A ready REST storage: `GET {baseUrl}/{viewCode}` reads the documentation (404 means
84
+ * none yet) and `PUT {baseUrl}/{viewCode}` stores the whole JSON document.
85
+ */
86
+ export declare function createHttpDocsStorage(baseUrl: string, options?: INgxViewBuilderHttpDocsStorageOptions): INgxViewBuilderDocsStorage;
87
+ /**
88
+ * A failed load or save of the HTTP storage. The builder shows it in its own language
89
+ * (`docs.error.loadStatus` / `docs.error.saveStatus`); the message is the English text.
90
+ */
91
+ declare class NgxViewBuilderDocsHttpError extends Error {
92
+ readonly operation: 'load' | 'save';
93
+ readonly status: number;
94
+ constructor(operation: 'load' | 'save', status: number);
95
+ }
96
+ export declare const NGX_VIEW_BUILDER_DOCS_TAB_ID = "documentation";
97
+ /**
98
+ * The documentation of the view the builder has open: loading it when the view changes,
99
+ * keeping edits, and saving them a moment after typing stops. The Properties panel, the
100
+ * Documentation tab and AI clients all go through this one service, so they never
101
+ * disagree about what is written.
102
+ */
103
+ export declare class NgxViewBuilderDocsService {
104
+ private readonly config;
105
+ private readonly structureService;
106
+ private readonly api;
107
+ private readonly eventService;
108
+ private readonly markupSecurity;
109
+ private readonly storage;
110
+ private readonly revision;
111
+ private loadedCode;
112
+ private loadToken;
113
+ private saveTimer;
114
+ private dirty;
115
+ private refreshScheduled;
116
+ /** A view whose stored copy could not be read: saving it would overwrite what is there. */
117
+ private loadFailedFor;
118
+ readonly docs: import("@angular/core").WritableSignal<INgxViewBuilderViewDocs>;
119
+ readonly saveState: import("@angular/core").WritableSignal<NgxViewBuilderDocsSaveState>;
120
+ readonly saveError: import("@angular/core").WritableSignal<string>;
121
+ readonly lastSavedAt: import("@angular/core").WritableSignal<Date>;
122
+ /** True when nothing but this browser keeps the documentation. */
123
+ readonly browserOnly: boolean;
124
+ private readonly i18n;
125
+ /** Where the documentation is kept, in words: the host's label, or "server" / "this browser". */
126
+ readonly storageLabel: import("@angular/core").Signal<string>;
127
+ readonly viewCode: import("@angular/core").WritableSignal<string>;
128
+ /** The logic of the open view, for the rule columns of the table. */
129
+ readonly graph: import("@angular/core").Signal<IViewLogicGraph>;
130
+ /** What the view calls itself, for the page title when the documentation sets none. */
131
+ readonly viewTitle: import("@angular/core").Signal<string>;
132
+ /** A storage error in the builder's language; a host's own message as it is. */
133
+ private errorText;
134
+ constructor();
135
+ /** Loads the documentation of the open view, unless it is already the loaded one. */
136
+ ensureLoaded(): void;
137
+ descriptionOf(path: string): string;
138
+ setDescription(path: string, html: string): void;
139
+ /** The rows a documented element shows under its description, in this order. */
140
+ setRows(path: string, rows: string[]): void;
141
+ rowsOf(path: string): string[];
142
+ /**
143
+ * Looks up elements' own definitions in the open view as plain JSON, by path: a root
144
+ * element, a panel child (`billing.city`) or a dynamic table column (`lines.qty`). One
145
+ * snapshot serves every lookup made through the returned function.
146
+ */
147
+ definitionLookup(): (path: string) => Record<string, unknown> | null;
148
+ removeDescription(path: string): void;
149
+ setSummary(html: string): void;
150
+ setTitle(title: string): void;
151
+ /** Saves now instead of waiting for typing to stop; also the retry after a failure. */
152
+ saveNow(): Promise<void>;
153
+ refresh(): void;
154
+ private load;
155
+ private commit;
156
+ private scheduleSave;
157
+ /** The documentation to write into, creating it (and the view code) on first use. */
158
+ private ensureDocs;
159
+ private resolveViewCode;
160
+ /** Writes the code into the open view's settings; returns null when there is no view. */
161
+ private stampViewCode;
162
+ private normalize;
163
+ /** Sanitized markup, or '' when nothing but empty paragraphs is left. */
164
+ private clean;
165
+ private scheduleRefresh;
166
+ private externalNames;
167
+ static ɵfac: i0.ɵɵFactoryDeclaration<NgxViewBuilderDocsService, never>;
168
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
169
+ }
170
+ export declare const NGX_VIEW_BUILDER_DOCS_AI_ID = "documentation";
171
+ /**
172
+ * The documentation for AI clients: read what every field is for, write descriptions,
173
+ * and get the same table the person copies into Confluence.
174
+ */
175
+ declare function createDocsAiPlugin(docs: NgxViewBuilderDocsService, license: LicenseService): INgxViewBuilderAiPlugin;
176
+ /** Rows filled in from the view's logic. */
177
+ type DocsRowKind = 'required' | 'logic' | 'validation' | 'data' | 'events';
178
+ declare const DOCS_ROW_KINDS: DocsRowKind[];
179
+ interface IDocsOptions {
180
+ groupByPage: boolean;
181
+ }
182
+ export declare const DEFAULT_DOCS_OPTIONS: IDocsOptions;
183
+ interface IDocsCell {
184
+ segments: ViewLogicSegment[];
185
+ expression?: string;
186
+ }
187
+ /** A row picked for an element, resolved against the view as it is now. */
188
+ interface IDocsRowValue {
189
+ /** `required`, `logic`… or `prop:<key>`. */
190
+ id: string;
191
+ label: string;
192
+ /** Rules, for the rows that come from the logic. */
193
+ cells?: IDocsCell[];
194
+ /** The value, for a property row. */
195
+ text?: string;
196
+ /** Nothing to show any more (the property was removed, the rule deleted). */
197
+ empty: boolean;
198
+ }
199
+ /** A row that can still be added to an element, with a preview of what it would show. */
200
+ interface IDocsRowOption {
201
+ id: string;
202
+ label: string;
203
+ preview: string;
204
+ group: 'logic' | 'property';
205
+ }
206
+ /** One element: its description, the rows picked for it, and what the view says about it. */
207
+ interface IDocsEntry {
208
+ node: IViewLogicNode;
209
+ /** Path under which the description is stored. */
210
+ key: string;
211
+ description: string;
212
+ rows: IDocsRowValue[];
213
+ /** Rows that can be added; only computed for documented elements. */
214
+ options: IDocsRowOption[];
215
+ /** Everything the logic says about the element, by kind, whatever is picked. */
216
+ generated: Record<DocsRowKind, IDocsCell[]>;
217
+ }
218
+ interface IDocsSection {
219
+ name: string;
220
+ label: string;
221
+ entries: IDocsEntry[];
222
+ }
223
+ interface IDocsModel {
224
+ title: string;
225
+ viewCode: string;
226
+ summary: string;
227
+ /** Documented elements only, grouped by page when asked. */
228
+ sections: IDocsSection[];
229
+ /** Elements that count towards the progress: fields, and anything with logic or a description. */
230
+ totalCount: number;
231
+ documentedCount: number;
232
+ /** Counted elements without a description yet, in form order, for the "Add element" picker. */
233
+ undocumented: IDocsEntry[];
234
+ /** Descriptions whose element is no longer in the view. */
235
+ orphans: Array<{
236
+ key: string;
237
+ description: string;
238
+ }>;
239
+ }
240
+ /** Reads an element's own definition by path, so property rows can show their value. */
241
+ type DocsDefinitionLookup = (path: string) => Record<string, unknown> | null;
242
+ /** Builds the documentation from the view logic and the written descriptions. */
243
+ export declare function buildDocsModel(graph: IViewLogicGraph, docs: INgxViewBuilderViewDocs | null, options: IDocsOptions, fallbackTitle: string,
244
+ /** Elements to show although they have no description yet: the one being written. */
245
+ drafts?: ReadonlySet<string>, definitionOf?: DocsDefinitionLookup,
246
+ /** The words of the documentation, in the builder's language. */
247
+ t?: NvbTranslate): IDocsModel;
248
+ /**
249
+ * Plain semantic HTML: a heading per page and a small table per element, its name across
250
+ * the top, the description over both columns, then the picked rows as name and value.
251
+ * Confluence, Notion, Google Docs and Word all keep exactly this when it is pasted.
252
+ */
253
+ export declare function docsToHtml(model: IDocsModel, _options: IDocsOptions, generatedOn: string, t?: NvbTranslate): string;
254
+ /** A standalone page around the same HTML, for the download. */
255
+ declare function docsToHtmlFile(model: IDocsModel, options: IDocsOptions, generatedOn: string, t?: NvbTranslate, language?: string): string;
256
+ /**
257
+ * GitHub flavoured Markdown, which Confluence, Jira, GitLab and GitHub all read. Markdown
258
+ * tables cannot span columns, so the description is a paragraph under the element heading
259
+ * and the picked rows follow as a two column table.
260
+ */
261
+ export declare function docsToMarkdown(model: IDocsModel, _options: IDocsOptions, generatedOn: string, t?: NvbTranslate): string;
262
+ /**
263
+ * Every text of the Documentation plugin in English, by key: the tab, the Properties
264
+ * panel section, the messages and the copied or downloaded table. To show it in another
265
+ * language, give the same keys in that language, e.g.
266
+ * `provideNgxViewBuilderUiTranslations({ lt: { 'docs.copyConfluence': 'Kopijuoti į Confluence' } })`.
267
+ * The sentences about the rules come from NGX_VIEW_BUILDER_SHARED_UI_EN in
268
+ * `@ngxviewbuilder/plugin-shared`; property names from the builder's own `property.<key>`.
269
+ * Placeholders in braces stay as they are.
270
+ */
271
+ export declare const NGX_VIEW_BUILDER_DOCS_UI_EN: Record<string, string>;
272
+ export type { DocsRowKind, IDocsEntry, IDocsModel, IDocsOptions, INgxViewBuilderHttpDocsStorageOptions };