@miguimono/json-schema 2.0.0

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.
Files changed (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/dist/schema/README.md +421 -0
  4. package/dist/schema/fesm2022/json-schema.mjs +1654 -0
  5. package/dist/schema/fesm2022/json-schema.mjs.map +1 -0
  6. package/dist/schema/index.d.ts +5 -0
  7. package/dist/schema/lib/components/schema-card.component.d.ts +128 -0
  8. package/dist/schema/lib/components/schema-links.component.d.ts +61 -0
  9. package/dist/schema/lib/components/schema.component.d.ts +115 -0
  10. package/dist/schema/lib/models.d.ts +1 -0
  11. package/dist/schema/lib/services/json-adapter.service.d.ts +1 -0
  12. package/dist/schema/lib/services/schema-layout.service.d.ts +1 -0
  13. package/dist/schema/lib/shared/models.d.ts +319 -0
  14. package/dist/schema/lib/shared/services/json-adapter.service.d.ts +12 -0
  15. package/dist/schema/lib/shared/services/schema-layout.service.d.ts +21 -0
  16. package/dist/schema/ng16/fesm2022/json-schema-ng16.mjs +1656 -0
  17. package/dist/schema/ng16/fesm2022/json-schema-ng16.mjs.map +1 -0
  18. package/dist/schema/ng16/index.d.ts +5 -0
  19. package/dist/schema/ng16/lib/components/schema-card.component.d.ts +64 -0
  20. package/dist/schema/ng16/lib/components/schema-links.component.d.ts +28 -0
  21. package/dist/schema/ng16/lib/components/schema.component.d.ts +132 -0
  22. package/dist/schema/ng16/lib/models.d.ts +1 -0
  23. package/dist/schema/ng16/lib/services/json-adapter.service.d.ts +1 -0
  24. package/dist/schema/ng16/lib/services/schema-layout.service.d.ts +1 -0
  25. package/dist/schema/ng16/lib/shared/models.d.ts +319 -0
  26. package/dist/schema/ng16/lib/shared/services/json-adapter.service.d.ts +12 -0
  27. package/dist/schema/ng16/lib/shared/services/schema-layout.service.d.ts +21 -0
  28. package/dist/schema/ng16/public-api.d.ts +6 -0
  29. package/dist/schema/public-api.d.ts +5 -0
  30. package/package.json +61 -0
@@ -0,0 +1,1656 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, EventEmitter, computed, Output, Input, ChangeDetectionStrategy, Component, Injectable, ViewChild } from '@angular/core';
3
+ import * as i1 from '@angular/common';
4
+ import { CommonModule, NgIf, NgTemplateOutlet, NgClass, NgFor } from '@angular/common';
5
+
6
+ /**
7
+ * Tipos y modelos base de la librería Schema.
8
+ * ---------------------------------------------------------------------------
9
+ * Este archivo define:
10
+ * - Tipos de configuración (dirección del layout, estilos, depuración, etc.)
11
+ * - Estructuras del grafo normalizado (nodos y aristas)
12
+ * - Estructura de configuración SchemaSettings (por secciones)
13
+ * - Valores por defecto (DEFAULT_SETTINGS)
14
+ *
15
+ * CONVENCIONES:
16
+ * - No se realizan operaciones lógicas aquí; solo contratos y defaults.
17
+ * - No modifiques nombres de propiedades existentes: otros módulos dependen
18
+ * de ellas. Cualquier cambio es breaking.
19
+ * - Todos los ejemplos son ilustrativos y no afectan el comportamiento.
20
+ */
21
+ /* =========================================
22
+ * Valores por defecto
23
+ * ========================================= */
24
+ /**
25
+ * Valores por defecto (seguros) de configuración.
26
+ * - Estos valores son fusionados con `SchemaSettings` provistos por el usuario.
27
+ * - No contienen lógica condicional; son constantes.
28
+ */
29
+ const DEFAULT_SETTINGS = {
30
+ messages: {
31
+ isLoading: false,
32
+ isError: false,
33
+ loadingMessage: "Cargando…",
34
+ errorMessage: "Error al cargar el esquema",
35
+ emptyMessage: "No hay datos para mostrar",
36
+ },
37
+ colors: {
38
+ linkStroke: "#019df4",
39
+ linkStrokeWidth: 2,
40
+ accentByKey: null,
41
+ accentInverse: false,
42
+ accentFill: false,
43
+ showColorTrue: false,
44
+ showColorFalse: false,
45
+ showColorNull: false,
46
+ },
47
+ layout: {
48
+ layoutDirection: "RIGHT",
49
+ layoutAlign: "firstChild",
50
+ linkStyle: "curve",
51
+ curveTension: 30,
52
+ straightThresholdDx: 60,
53
+ columnGapPx: 64,
54
+ rowGapPx: 32,
55
+ },
56
+ dataView: {
57
+ /* a) Extracción */
58
+ titleKeyPriority: [],
59
+ hiddenKeysGlobal: [],
60
+ treatScalarArraysAsAttribute: true,
61
+ maxDepth: null,
62
+ labelData: {},
63
+ /* b) Presentación general */
64
+ previewMaxKeys: 999,
65
+ valueMaxChars: null,
66
+ valueShowTooltip: false,
67
+ noWrapKeys: [],
68
+ maxCardWidth: null,
69
+ maxCardHeight: null,
70
+ defaultNodeSize: { width: 256, height: 64 },
71
+ /* c) Presentación de imagen */
72
+ showImage: null,
73
+ imageSizePx: 32,
74
+ imageShape: "rounded",
75
+ imageBorder: false,
76
+ imageBg: "transparent",
77
+ imageFit: "contain",
78
+ imageFallback: null,
79
+ /* d) Interacción */
80
+ enableCollapse: true,
81
+ /* e) Medición */
82
+ autoResizeCards: true,
83
+ paddingWidthPx: 16,
84
+ paddingHeightPx: 0,
85
+ },
86
+ viewport: {
87
+ height: 800,
88
+ minHeight: 480,
89
+ showToolbar: true,
90
+ toolbarControls: {
91
+ showLinkStyle: true,
92
+ showLayoutAlign: true,
93
+ showLayoutDirection: true,
94
+ },
95
+ },
96
+ };
97
+
98
+ // URL: projects/schema-ng16/src/lib/models.ts
99
+
100
+ // URL: projects/schema-ng16/src/lib/components/schema-card.component.ts
101
+ class SchemaCardComponent {
102
+ constructor() {
103
+ /* ============================ Inputs (con alias para evitar conflictos con métodos) ============================ */
104
+ this._node = signal(null);
105
+ this._cardTemplate = signal(null);
106
+ this._settings = signal(DEFAULT_SETTINGS);
107
+ this._hasChildren = signal(false);
108
+ this._showCollapseControls = signal(false);
109
+ this._isCollapsed = signal(false);
110
+ /* ============================ Outputs =========================== */
111
+ this.nodeClick = new EventEmitter();
112
+ this.toggleRequest = new EventEmitter();
113
+ /* ============================ View derivada ===================== */
114
+ this.view = computed(() => {
115
+ const s = this.settings(); // nunca null; siempre hay defaults
116
+ // Grupo de imagen
117
+ const imageSizePx = s.dataView?.imageSizePx ?? DEFAULT_SETTINGS.dataView.imageSizePx;
118
+ const imageShape = (s.dataView?.imageShape ?? DEFAULT_SETTINGS.dataView.imageShape);
119
+ const imageBorder = s.dataView?.imageBorder ?? DEFAULT_SETTINGS.dataView.imageBorder;
120
+ const imageBg = s.dataView?.imageBg ?? DEFAULT_SETTINGS.dataView.imageBg;
121
+ const imageFit = s.dataView?.imageFit ?? DEFAULT_SETTINGS.dataView.imageFit;
122
+ // Presentación general
123
+ const maxCardWidth = s.dataView?.maxCardWidth ?? null;
124
+ const maxCardHeight = s.dataView?.maxCardHeight ?? null;
125
+ const noWrapKeys = s.dataView?.noWrapKeys ?? [];
126
+ const labelData = s.dataView?.labelData ?? {};
127
+ const valueShowTooltip = s.dataView?.valueShowTooltip ?? false;
128
+ const valueMaxChars = s.dataView?.valueMaxChars ?? null;
129
+ // Acentos
130
+ const accentByKey = s.colors?.accentByKey ?? null;
131
+ const accentFill = s.colors?.accentFill ?? false;
132
+ const accentInverse = s.colors?.accentInverse ?? false;
133
+ const reqTrue = s.colors?.showColorTrue ?? false;
134
+ const reqFalse = s.colors?.showColorFalse ?? false;
135
+ const reqNull = s.colors?.showColorNull ?? false;
136
+ let showTrue = reqTrue;
137
+ let showFalse = reqFalse;
138
+ let showNull = reqNull;
139
+ const anyRequested = reqTrue || reqFalse || reqNull;
140
+ if (accentByKey && !anyRequested) {
141
+ showTrue = true;
142
+ showFalse = true;
143
+ showNull = true;
144
+ }
145
+ return {
146
+ // Imagen
147
+ showImageKey: s.dataView?.showImage ?? null,
148
+ imageSizePx,
149
+ imageShape,
150
+ imageBorder,
151
+ imageBg,
152
+ imageFit,
153
+ imageFallback: s.dataView?.imageFallback ?? null,
154
+ // Presentación
155
+ maxCardWidth,
156
+ maxCardHeight,
157
+ noWrapKeys,
158
+ labelData,
159
+ valueShowTooltip,
160
+ valueMaxChars,
161
+ // Defaults de tamaño
162
+ defaultNodeW: DEFAULT_SETTINGS.dataView.defaultNodeSize?.width ?? 120,
163
+ defaultNodeH: DEFAULT_SETTINGS.dataView.defaultNodeSize?.height ?? 60,
164
+ // Acentos
165
+ accentByKey,
166
+ accentFill,
167
+ accentInverse,
168
+ showColorTrue: showTrue,
169
+ showColorFalse: showFalse,
170
+ showColorNull: showNull,
171
+ };
172
+ });
173
+ }
174
+ set nodeInput(value) {
175
+ this._node.set(value ?? null);
176
+ }
177
+ node() {
178
+ return this._node();
179
+ }
180
+ set cardTemplateInput(value) {
181
+ this._cardTemplate.set(value ?? null);
182
+ }
183
+ cardTemplate() {
184
+ return this._cardTemplate();
185
+ }
186
+ set settingsInput(value) {
187
+ this._settings.set(value ?? DEFAULT_SETTINGS);
188
+ }
189
+ settings() {
190
+ return this._settings();
191
+ }
192
+ set hasChildrenInput(value) {
193
+ this._hasChildren.set(!!value);
194
+ }
195
+ hasChildren() {
196
+ return this._hasChildren();
197
+ }
198
+ set showCollapseControlsInput(value) {
199
+ this._showCollapseControls.set(!!value);
200
+ }
201
+ showCollapseControls() {
202
+ return this._showCollapseControls();
203
+ }
204
+ set isCollapsedInput(value) {
205
+ this._isCollapsed.set(!!value);
206
+ }
207
+ isCollapsed() {
208
+ return this._isCollapsed();
209
+ }
210
+ /* ============================ API interna ======================= */
211
+ onClick(event) {
212
+ event.stopPropagation();
213
+ const n = this.node();
214
+ if (n)
215
+ this.nodeClick.emit(n);
216
+ }
217
+ onToggle(event) {
218
+ event.stopPropagation();
219
+ const n = this.node();
220
+ if (n)
221
+ this.toggleRequest.emit(n);
222
+ }
223
+ objToPairs(obj) {
224
+ const entries = Object.entries(obj ?? {});
225
+ const imgKey = this.view().showImageKey;
226
+ const accentKey = this.view().accentByKey;
227
+ return entries.filter(([k]) => {
228
+ if (imgKey && k === imgKey)
229
+ return false;
230
+ if (accentKey && k === accentKey)
231
+ return false;
232
+ return true;
233
+ });
234
+ }
235
+ showImageKey() {
236
+ const k = this.view().showImageKey;
237
+ return k && typeof k === "string" && k.trim() ? k : null;
238
+ }
239
+ imageSrc() {
240
+ const key = this.showImageKey();
241
+ if (!key)
242
+ return null;
243
+ const v = this.node()?.data?.[key];
244
+ return typeof v === "string" && v.trim() !== "" ? v : null;
245
+ }
246
+ imageAlt() {
247
+ const n = this.node();
248
+ if (!n)
249
+ return "imagen";
250
+ return n.jsonMeta?.title || n.label || "imagen";
251
+ }
252
+ getAccentClasses() {
253
+ const v = this.view();
254
+ const k = v.accentByKey;
255
+ if (!k)
256
+ return [];
257
+ const n = this.node();
258
+ const val = n?.data?.[k];
259
+ const classes = [];
260
+ const pushIf = (cond, cls) => cond && classes.push(cls);
261
+ if (!v.accentInverse) {
262
+ pushIf(val === true && v.showColorTrue, "accent-true");
263
+ pushIf(val === false && v.showColorFalse, "accent-false");
264
+ pushIf(val === null && v.showColorNull, "accent-null");
265
+ if (v.accentFill) {
266
+ pushIf(val === true && v.showColorTrue, "accent-fill-true");
267
+ pushIf(val === false && v.showColorFalse, "accent-fill-false");
268
+ pushIf(val === null && v.showColorNull, "accent-fill-null");
269
+ }
270
+ }
271
+ else {
272
+ pushIf(val === true && v.showColorTrue, "accent-false");
273
+ pushIf(val === false && v.showColorFalse, "accent-true");
274
+ pushIf(val === null && v.showColorNull, "accent-null");
275
+ if (v.accentFill) {
276
+ pushIf(val === true && v.showColorTrue, "accent-fill-false");
277
+ pushIf(val === false && v.showColorFalse, "accent-fill-true");
278
+ pushIf(val === null && v.showColorNull, "accent-fill-null");
279
+ }
280
+ }
281
+ return classes;
282
+ }
283
+ isNoWrapKey(key) {
284
+ const arr = this.view().noWrapKeys ?? [];
285
+ return Array.isArray(arr) ? arr.includes(key) : false;
286
+ }
287
+ arrowGlyph() {
288
+ const dir = this.settings().layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection;
289
+ const collapsed = !!this.isCollapsed();
290
+ if (dir === "DOWN")
291
+ return collapsed ? "▼" : "▲";
292
+ return collapsed ? "▶" : "◀";
293
+ }
294
+ displayValue(val) {
295
+ const str = val == null ? String(val) : String(val);
296
+ const limit = this.view().valueMaxChars;
297
+ if (typeof limit === "number" && limit > 0 && str.length > limit) {
298
+ return str.slice(0, limit) + "…";
299
+ }
300
+ return str;
301
+ }
302
+ valueTitle(val) {
303
+ if (!this.view().valueShowTooltip)
304
+ return null;
305
+ return val == null ? String(val) : String(val);
306
+ }
307
+ displayKey(key) {
308
+ const map = this.view().labelData ?? {};
309
+ return Object.prototype.hasOwnProperty.call(map, key) ? map[key] : key;
310
+ }
311
+ hasComputedTitle() {
312
+ const t = this.node()?.jsonMeta?.title;
313
+ return !!t && String(t).trim() !== "";
314
+ }
315
+ onImgError(ev) {
316
+ const el = ev.target;
317
+ if (!el)
318
+ return;
319
+ const tried = el.dataset["fallbackApplied"] === "1";
320
+ const fallback = this.view().imageFallback;
321
+ if (fallback && !tried) {
322
+ el.dataset["fallbackApplied"] = "1";
323
+ el.src = fallback;
324
+ return;
325
+ }
326
+ el.removeAttribute("src");
327
+ if (!this.view().imageBg)
328
+ el.style.background = "#e2e8f0";
329
+ if (this.view().imageBorder === undefined)
330
+ el.style.border = "1px solid rgba(0,0,0,0.06)";
331
+ }
332
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
333
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.18", type: SchemaCardComponent, isStandalone: true, selector: "schema-card", inputs: { nodeInput: ["node", "nodeInput"], cardTemplateInput: ["cardTemplate", "cardTemplateInput"], settingsInput: ["settings", "settingsInput"], hasChildrenInput: ["hasChildren", "hasChildrenInput"], showCollapseControlsInput: ["showCollapseControls", "showCollapseControlsInput"], isCollapsedInput: ["isCollapsed", "isCollapsedInput"] }, outputs: { nodeClick: "nodeClick", toggleRequest: "toggleRequest" }, ngImport: i0, template: "<div\n class=\"schema-card\"\n [attr.data-node-id]=\"node()?.id ?? ''\"\n [ngClass]=\"getAccentClasses()\"\n [style.left.px]=\"node()?.x ?? 0\"\n [style.top.px]=\"node()?.y ?? 0\"\n [style.width.px]=\"node()?.width ?? view().defaultNodeW\"\n [style.height.px]=\"node()?.height ?? view().defaultNodeH\"\n [style.maxWidth.px]=\"view().maxCardWidth\"\n [style.maxHeight.px]=\"view().maxCardHeight\"\n (click)=\"onClick($event)\"\n style=\"z-index: 1; position: absolute;\"\n>\n <button\n *ngIf=\"showCollapseControls() && hasChildren()\"\n type=\"button\"\n class=\"collapse-btn\"\n [attr.aria-pressed]=\"isCollapsed()\"\n (click)=\"onToggle($event)\"\n [attr.title]=\"isCollapsed() ? 'Expandir' : 'Colapsar'\"\n >\n <span class=\"chev\">{{ arrowGlyph() }}</span>\n </button>\n\n <!-- Template externo opcional -->\n <ng-container\n *ngIf=\"cardTemplate(); else defaultTpl\"\n [ngTemplateOutlet]=\"cardTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: node() }\"\n ></ng-container>\n\n <!-- Template por defecto -->\n <ng-template #defaultTpl>\n <div class=\"card-body\">\n <div class=\"card-row\">\n <!-- Miniatura opcional -->\n <img\n *ngIf=\"imageSrc() as src\"\n class=\"thumb\"\n [class.shape-square]=\"view().imageShape === 'square'\"\n [class.shape-rounded]=\"view().imageShape === 'rounded'\"\n [class.shape-circle]=\"view().imageShape === 'circle'\"\n [attr.width]=\"view().imageSizePx\"\n [attr.height]=\"view().imageSizePx\"\n [style.width.px]=\"view().imageSizePx\"\n [style.height.px]=\"view().imageSizePx\"\n [style.background]=\"view().imageBg ?? null\"\n [style.border]=\"view().imageBorder ? '1px solid rgba(0,0,0,0.06)' : 'none'\"\n [style.object-fit]=\"view().imageFit\"\n [src]=\"src\"\n [alt]=\"imageAlt()\"\n loading=\"lazy\"\n decoding=\"async\"\n (error)=\"onImgError($event)\"\n />\n\n <div class=\"card-col\">\n <div class=\"card-title\" *ngIf=\"hasComputedTitle()\">\n {{ node()?.jsonMeta?.title }}\n </div>\n\n <!-- Vista previa de atributos -->\n <div class=\"card-preview\" *ngIf=\"node()?.jsonMeta?.attributes as attrs\">\n <div *ngFor=\"let kv of objToPairs(attrs)\" class=\"kv\">\n <span class=\"k\">{{ displayKey(kv[0]) }}:</span>\n <span\n class=\"v\"\n [class.v-true]=\"kv[1] === true\"\n [class.v-false]=\"kv[1] === false\"\n [class.v-null]=\"kv[1] === null\"\n [class.nowrap]=\"isNoWrapKey(kv[0])\"\n [attr.title]=\"valueTitle(kv[1])\"\n >\n {{ displayValue(kv[1]) }}\n </span>\n </div>\n </div>\n\n <!-- Badges con conteos de arrays no escalares -->\n <div class=\"array-badges\" *ngIf=\"node()?.jsonMeta?.arrayCounts as arrs\">\n <ng-container *ngFor=\"let e of objToPairs(arrs)\">\n <span class=\"arr-badge\">\n {{ displayKey(e[0]) }}: {{ e[1] }} {{ e[1] === 1 ? \"item\" : \"items\" }}\n </span>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n</div>\n", styles: [".schema-card{border-radius:12px;border:1px solid rgba(0,0,0,.08);background:#fff;box-shadow:0 2px 8px #00000014;-webkit-user-select:none;user-select:none;box-sizing:border-box;word-break:normal;overflow-wrap:normal;overflow:hidden;transition:left .16s ease,top .16s ease,opacity .12s ease}.collapse-btn{position:absolute;top:6px;right:8px;width:24px;height:24px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f8fafc;cursor:pointer;display:grid;place-items:center;padding:0;line-height:1;z-index:2}.collapse-btn span{font-size:.5rem}.chev{display:inline-block;transition:transform .16s ease}.chev.collapsed{transform:rotate(180deg)}.card-body{padding:12px 20px}.card-row{display:flex;align-items:flex-start;gap:10px}.card-col{flex:1 1 auto;min-width:0}.thumb{flex:0 0 auto;display:block;object-fit:contain;background:transparent;border:1px solid rgba(0,0,0,.06)}.shape-square{border-radius:4px}.shape-rounded{border-radius:8px}.shape-circle{border-radius:999px}.card-title{font-weight:700;font-size:14px;margin-bottom:6px}.card-preview{font-size:12px;line-height:1.28}.kv{display:grid;grid-template-columns:max-content max-content;column-gap:10px;align-items:baseline;margin:3px 0;padding-right:8px}.k{opacity:.66;font-weight:600;font-size:12px}.v{font-size:10.75px;line-height:1.25;letter-spacing:.1px;padding-right:6px;display:inline-block}.v-true{color:#16a34a;font-weight:700}.v-false{color:#dc2626;font-weight:700}.v-null{color:#6b7280;font-weight:700}.array-badges{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px}.arr-badge{font-size:10px;background:#eef2ff;color:#3730a3;padding:2px 6px;border-radius:999px}.nowrap{white-space:nowrap!important;word-break:keep-all!important;overflow-wrap:normal!important}.schema-card.accent-true{border-color:#16a34a;box-shadow:0 2px 10px #16a34a26}.schema-card.accent-false{border-color:#dc2626;box-shadow:0 2px 10px #dc262626}.schema-card.accent-null{border-color:#6b7280;box-shadow:0 2px 10px #6b72802e}.schema-card.accent-fill-true{background:#16a34a1a}.schema-card.accent-fill-false{background:#dc26261a}.schema-card.accent-fill-null{background:#6b72801f}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
334
+ }
335
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaCardComponent, decorators: [{
336
+ type: Component,
337
+ args: [{ selector: "schema-card", standalone: true, imports: [CommonModule, NgIf, NgTemplateOutlet, NgClass], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"schema-card\"\n [attr.data-node-id]=\"node()?.id ?? ''\"\n [ngClass]=\"getAccentClasses()\"\n [style.left.px]=\"node()?.x ?? 0\"\n [style.top.px]=\"node()?.y ?? 0\"\n [style.width.px]=\"node()?.width ?? view().defaultNodeW\"\n [style.height.px]=\"node()?.height ?? view().defaultNodeH\"\n [style.maxWidth.px]=\"view().maxCardWidth\"\n [style.maxHeight.px]=\"view().maxCardHeight\"\n (click)=\"onClick($event)\"\n style=\"z-index: 1; position: absolute;\"\n>\n <button\n *ngIf=\"showCollapseControls() && hasChildren()\"\n type=\"button\"\n class=\"collapse-btn\"\n [attr.aria-pressed]=\"isCollapsed()\"\n (click)=\"onToggle($event)\"\n [attr.title]=\"isCollapsed() ? 'Expandir' : 'Colapsar'\"\n >\n <span class=\"chev\">{{ arrowGlyph() }}</span>\n </button>\n\n <!-- Template externo opcional -->\n <ng-container\n *ngIf=\"cardTemplate(); else defaultTpl\"\n [ngTemplateOutlet]=\"cardTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: node() }\"\n ></ng-container>\n\n <!-- Template por defecto -->\n <ng-template #defaultTpl>\n <div class=\"card-body\">\n <div class=\"card-row\">\n <!-- Miniatura opcional -->\n <img\n *ngIf=\"imageSrc() as src\"\n class=\"thumb\"\n [class.shape-square]=\"view().imageShape === 'square'\"\n [class.shape-rounded]=\"view().imageShape === 'rounded'\"\n [class.shape-circle]=\"view().imageShape === 'circle'\"\n [attr.width]=\"view().imageSizePx\"\n [attr.height]=\"view().imageSizePx\"\n [style.width.px]=\"view().imageSizePx\"\n [style.height.px]=\"view().imageSizePx\"\n [style.background]=\"view().imageBg ?? null\"\n [style.border]=\"view().imageBorder ? '1px solid rgba(0,0,0,0.06)' : 'none'\"\n [style.object-fit]=\"view().imageFit\"\n [src]=\"src\"\n [alt]=\"imageAlt()\"\n loading=\"lazy\"\n decoding=\"async\"\n (error)=\"onImgError($event)\"\n />\n\n <div class=\"card-col\">\n <div class=\"card-title\" *ngIf=\"hasComputedTitle()\">\n {{ node()?.jsonMeta?.title }}\n </div>\n\n <!-- Vista previa de atributos -->\n <div class=\"card-preview\" *ngIf=\"node()?.jsonMeta?.attributes as attrs\">\n <div *ngFor=\"let kv of objToPairs(attrs)\" class=\"kv\">\n <span class=\"k\">{{ displayKey(kv[0]) }}:</span>\n <span\n class=\"v\"\n [class.v-true]=\"kv[1] === true\"\n [class.v-false]=\"kv[1] === false\"\n [class.v-null]=\"kv[1] === null\"\n [class.nowrap]=\"isNoWrapKey(kv[0])\"\n [attr.title]=\"valueTitle(kv[1])\"\n >\n {{ displayValue(kv[1]) }}\n </span>\n </div>\n </div>\n\n <!-- Badges con conteos de arrays no escalares -->\n <div class=\"array-badges\" *ngIf=\"node()?.jsonMeta?.arrayCounts as arrs\">\n <ng-container *ngFor=\"let e of objToPairs(arrs)\">\n <span class=\"arr-badge\">\n {{ displayKey(e[0]) }}: {{ e[1] }} {{ e[1] === 1 ? \"item\" : \"items\" }}\n </span>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n</div>\n", styles: [".schema-card{border-radius:12px;border:1px solid rgba(0,0,0,.08);background:#fff;box-shadow:0 2px 8px #00000014;-webkit-user-select:none;user-select:none;box-sizing:border-box;word-break:normal;overflow-wrap:normal;overflow:hidden;transition:left .16s ease,top .16s ease,opacity .12s ease}.collapse-btn{position:absolute;top:6px;right:8px;width:24px;height:24px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f8fafc;cursor:pointer;display:grid;place-items:center;padding:0;line-height:1;z-index:2}.collapse-btn span{font-size:.5rem}.chev{display:inline-block;transition:transform .16s ease}.chev.collapsed{transform:rotate(180deg)}.card-body{padding:12px 20px}.card-row{display:flex;align-items:flex-start;gap:10px}.card-col{flex:1 1 auto;min-width:0}.thumb{flex:0 0 auto;display:block;object-fit:contain;background:transparent;border:1px solid rgba(0,0,0,.06)}.shape-square{border-radius:4px}.shape-rounded{border-radius:8px}.shape-circle{border-radius:999px}.card-title{font-weight:700;font-size:14px;margin-bottom:6px}.card-preview{font-size:12px;line-height:1.28}.kv{display:grid;grid-template-columns:max-content max-content;column-gap:10px;align-items:baseline;margin:3px 0;padding-right:8px}.k{opacity:.66;font-weight:600;font-size:12px}.v{font-size:10.75px;line-height:1.25;letter-spacing:.1px;padding-right:6px;display:inline-block}.v-true{color:#16a34a;font-weight:700}.v-false{color:#dc2626;font-weight:700}.v-null{color:#6b7280;font-weight:700}.array-badges{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px}.arr-badge{font-size:10px;background:#eef2ff;color:#3730a3;padding:2px 6px;border-radius:999px}.nowrap{white-space:nowrap!important;word-break:keep-all!important;overflow-wrap:normal!important}.schema-card.accent-true{border-color:#16a34a;box-shadow:0 2px 10px #16a34a26}.schema-card.accent-false{border-color:#dc2626;box-shadow:0 2px 10px #dc262626}.schema-card.accent-null{border-color:#6b7280;box-shadow:0 2px 10px #6b72802e}.schema-card.accent-fill-true{background:#16a34a1a}.schema-card.accent-fill-false{background:#dc26261a}.schema-card.accent-fill-null{background:#6b72801f}\n"] }]
338
+ }], propDecorators: { nodeInput: [{
339
+ type: Input,
340
+ args: ["node"]
341
+ }], cardTemplateInput: [{
342
+ type: Input,
343
+ args: ["cardTemplate"]
344
+ }], settingsInput: [{
345
+ type: Input,
346
+ args: ["settings"]
347
+ }], hasChildrenInput: [{
348
+ type: Input,
349
+ args: ["hasChildren"]
350
+ }], showCollapseControlsInput: [{
351
+ type: Input,
352
+ args: ["showCollapseControls"]
353
+ }], isCollapsedInput: [{
354
+ type: Input,
355
+ args: ["isCollapsed"]
356
+ }], nodeClick: [{
357
+ type: Output
358
+ }], toggleRequest: [{
359
+ type: Output
360
+ }] } });
361
+
362
+ // URL: projects/schema-ng16/src/lib/components/schema-links.component.ts
363
+ class SchemaLinksComponent {
364
+ constructor() {
365
+ /* ============================ Inputs ============================ */
366
+ this._edges = signal([]);
367
+ this._settings = signal(DEFAULT_SETTINGS);
368
+ // Importante: públicos para el template (antes eran private)
369
+ this._width = signal(4000);
370
+ this._height = signal(2000);
371
+ /* ============================ Outputs =========================== */
372
+ this.linkClick = new EventEmitter();
373
+ /* ============================ View derivada ===================== */
374
+ this.view = computed(() => {
375
+ const s = this.settings();
376
+ const colors = s.colors ?? DEFAULT_SETTINGS.colors;
377
+ const layout = s.layout ?? DEFAULT_SETTINGS.layout;
378
+ return {
379
+ linkStroke: colors.linkStroke ?? DEFAULT_SETTINGS.colors.linkStroke,
380
+ linkStrokeWidth: colors.linkStrokeWidth ?? DEFAULT_SETTINGS.colors.linkStrokeWidth,
381
+ linkStyle: layout.linkStyle ?? DEFAULT_SETTINGS.layout.linkStyle,
382
+ curveTension: layout.curveTension ?? DEFAULT_SETTINGS.layout.curveTension,
383
+ straightThresholdDx: layout.straightThresholdDx ?? DEFAULT_SETTINGS.layout.straightThresholdDx,
384
+ };
385
+ });
386
+ /* ============================ Render helpers ==================== */
387
+ this.trackEdgeId = (_, e) => e.id;
388
+ }
389
+ set edgesInput(value) {
390
+ this._edges.set(Array.isArray(value) ? value : []);
391
+ }
392
+ edges() {
393
+ return this._edges();
394
+ }
395
+ set settingsInput(value) {
396
+ this._settings.set(value ?? DEFAULT_SETTINGS);
397
+ }
398
+ settings() {
399
+ return this._settings();
400
+ }
401
+ set width(value) {
402
+ this._width.set(typeof value === "number" && isFinite(value) ? value : 4000);
403
+ }
404
+ set height(value) {
405
+ this._height.set(typeof value === "number" && isFinite(value) ? value : 2000);
406
+ }
407
+ pathFor(e) {
408
+ const pts = e.points ?? [];
409
+ if (pts.length === 0)
410
+ return "";
411
+ const v = this.view();
412
+ const style = v.linkStyle;
413
+ if (style === "line") {
414
+ const a = pts[0];
415
+ const b = pts[pts.length - 1];
416
+ return `M ${a.x},${a.y} L ${b.x},${b.y}`;
417
+ }
418
+ if (style === "curve") {
419
+ const a = pts[0];
420
+ const b = pts[pts.length - 1];
421
+ const threshold = Number.isFinite(v.straightThresholdDx)
422
+ ? v.straightThresholdDx
423
+ : DEFAULT_SETTINGS.layout.straightThresholdDx;
424
+ const dxAbs = Math.abs(b.x - a.x);
425
+ if (dxAbs < (threshold ?? 0)) {
426
+ return `M ${a.x},${a.y} L ${b.x},${b.y}`;
427
+ }
428
+ const baseT = Number.isFinite(v.curveTension) ? v.curveTension : DEFAULT_SETTINGS.layout.curveTension;
429
+ const t = Math.max(20, Math.min(200, baseT ?? 30));
430
+ const dir = Math.sign(b.x - a.x) || 1;
431
+ const dy = b.y - a.y;
432
+ let c1x = a.x + dir * t;
433
+ let c1y = a.y;
434
+ let c2x = b.x - dir * t;
435
+ let c2y = b.y;
436
+ if (Math.abs(dy) < 1) {
437
+ const bow = Math.max(8, Math.min(96, t * 0.5));
438
+ c1y = a.y - bow;
439
+ c2y = b.y + bow;
440
+ }
441
+ return `M ${a.x},${a.y} C ${c1x},${c1y} ${c2x},${c2y} ${b.x},${b.y}`;
442
+ }
443
+ if (pts.length === 1)
444
+ return `M ${pts[0].x},${pts[0].y}`;
445
+ const first = pts[0];
446
+ let d = `M ${first.x},${first.y}`;
447
+ for (let i = 1; i < pts.length; i++)
448
+ d += ` L ${pts[i].x},${pts[i].y}`;
449
+ return d;
450
+ }
451
+ /* ============================ Eventos ========================== */
452
+ onLinkClick(e, ev) {
453
+ ev.stopPropagation();
454
+ this.linkClick.emit(e);
455
+ }
456
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLinksComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
457
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.18", type: SchemaLinksComponent, isStandalone: true, selector: "schema-links", inputs: { edgesInput: ["edges", "edgesInput"], settingsInput: ["settings", "settingsInput"], width: "width", height: "height" }, outputs: { linkClick: "linkClick" }, ngImport: i0, template: "<svg class=\"schema-links\" [attr.width]=\"_width()\" [attr.height]=\"_height()\">\n <g>\n <path\n *ngFor=\"let e of edges(); trackBy: trackEdgeId\"\n [attr.d]=\"pathFor(e)\"\n [attr.stroke]=\"view().linkStroke\"\n [attr.stroke-width]=\"view().linkStrokeWidth\"\n fill=\"none\"\n (click)=\"onLinkClick(e, $event)\"\n />\n </g>\n</svg>\n", styles: [".schema-links{position:absolute;left:0;top:0;pointer-events:auto;overflow:visible;z-index:0}path{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
458
+ }
459
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLinksComponent, decorators: [{
460
+ type: Component,
461
+ args: [{ selector: "schema-links", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg class=\"schema-links\" [attr.width]=\"_width()\" [attr.height]=\"_height()\">\n <g>\n <path\n *ngFor=\"let e of edges(); trackBy: trackEdgeId\"\n [attr.d]=\"pathFor(e)\"\n [attr.stroke]=\"view().linkStroke\"\n [attr.stroke-width]=\"view().linkStrokeWidth\"\n fill=\"none\"\n (click)=\"onLinkClick(e, $event)\"\n />\n </g>\n</svg>\n", styles: [".schema-links{position:absolute;left:0;top:0;pointer-events:auto;overflow:visible;z-index:0}path{cursor:pointer}\n"] }]
462
+ }], propDecorators: { edgesInput: [{
463
+ type: Input,
464
+ args: ["edges"]
465
+ }], settingsInput: [{
466
+ type: Input,
467
+ args: ["settings"]
468
+ }], width: [{
469
+ type: Input
470
+ }], height: [{
471
+ type: Input
472
+ }], linkClick: [{
473
+ type: Output
474
+ }] } });
475
+
476
+ /**
477
+ * Servicio: JsonAdapterService
478
+ * Convierte un JSON arbitrario en un grafo normalizado (nodes + edges).
479
+ * - Aplica reglas de extracción (titleKeyPriority, hiddenKeysGlobal, etc.).
480
+ * - Conservar rutas únicas (`jsonPath`) como IDs de nodos.
481
+ * - Incluye metadatos útiles (childOrder, arrayCounts, atributos).
482
+ * - No calcula layout.
483
+ */
484
+ class JsonAdapterService {
485
+ /**
486
+ * Normaliza un JSON a grafo de nodos y aristas.
487
+ * @param input Objeto JSON arbitrario.
488
+ * @param opts Configuración parcial (`SchemaSettings`).
489
+ */
490
+ normalize(input, opts = {}) {
491
+ // Merge por secciones
492
+ const settings = {
493
+ colors: { ...DEFAULT_SETTINGS.colors, ...(opts.colors ?? {}) },
494
+ layout: { ...DEFAULT_SETTINGS.layout, ...(opts.layout ?? {}) },
495
+ dataView: { ...DEFAULT_SETTINGS.dataView, ...(opts.dataView ?? {}) },
496
+ messages: { ...DEFAULT_SETTINGS.messages, ...(opts.messages ?? {}) },
497
+ viewport: { ...DEFAULT_SETTINGS.viewport, ...(opts.viewport ?? {}) },
498
+ };
499
+ // Accesos rápidos
500
+ const dv = settings.dataView;
501
+ const titleKeyPriority = dv.titleKeyPriority ?? [];
502
+ const hiddenKeysGlobal = dv.hiddenKeysGlobal ?? [];
503
+ const treatScalarArraysAsAttribute = dv.treatScalarArraysAsAttribute ?? false;
504
+ const previewMaxKeys = dv.previewMaxKeys ?? 999;
505
+ const defaultNodeSize = dv.defaultNodeSize ?? { width: 256, height: 64 };
506
+ const maxDepth = dv.maxDepth ?? null;
507
+ // Claves a ocultar explícitamente en preview
508
+ const imageKey = dv.showImage && dv.showImage.trim() !== "" ? dv.showImage : null;
509
+ const accentKey = settings.colors.accentByKey && settings.colors.accentByKey.trim() !== "" ? settings.colors.accentByKey : null;
510
+ const nodes = [];
511
+ const edges = [];
512
+ // Helpers
513
+ const isScalar = (v) => v === null || ["string", "number", "boolean"].includes(typeof v);
514
+ /** Determina si un array contiene solo escalares. */
515
+ const arrayIsScalar = (arr) => Array.isArray(arr) && arr.length > 0 && arr.every(isScalar);
516
+ /**
517
+ * Escoge un título a partir de prioridades de clave.
518
+ * Retorna también la clave usada para no duplicar en atributos.
519
+ */ const pickTitle = (obj, priorities) => {
520
+ if (Array.isArray(priorities) && priorities.length > 0) {
521
+ for (const k of priorities) {
522
+ const v = obj?.[k];
523
+ if (v != null && String(v).trim() !== "") {
524
+ return { title: String(v), usedKey: k };
525
+ }
526
+ }
527
+ }
528
+ return { title: "", usedKey: undefined };
529
+ };
530
+ /**
531
+ * Construye un subconjunto de atributos (clave → valor) para vista previa.
532
+ * - Excluye claves ocultas o la usada como título.
533
+ * - Incluye escalares y arrays escalares (si está habilitado).
534
+ */
535
+ const buildPreviewAttributes = (obj, usedKey) => {
536
+ const toHide = new Set(hiddenKeysGlobal);
537
+ if (usedKey)
538
+ toHide.add(usedKey);
539
+ if (imageKey)
540
+ toHide.add(imageKey);
541
+ if (accentKey)
542
+ toHide.add(accentKey);
543
+ const entries = [];
544
+ for (const [k, v] of Object.entries(obj ?? {})) {
545
+ if (toHide.has(k))
546
+ continue;
547
+ if (isScalar(v)) {
548
+ entries.push([k, v]);
549
+ }
550
+ else if (Array.isArray(v) && treatScalarArraysAsAttribute && arrayIsScalar(v)) {
551
+ entries.push([k, v.join(", ")]);
552
+ }
553
+ }
554
+ return Object.fromEntries(entries.slice(0, previewMaxKeys));
555
+ };
556
+ /** Determina si un objeto puede representarse como entidad (nodo). */
557
+ const isEntity = (obj) => {
558
+ if (!obj || typeof obj !== "object" || Array.isArray(obj))
559
+ return false;
560
+ return Object.values(obj).some(isScalar);
561
+ };
562
+ /** Retorna un mapa clave → longitud de arrays no escalares. */
563
+ const arrayCountsOf = (obj) => {
564
+ const out = {};
565
+ for (const [k, v] of Object.entries(obj)) {
566
+ if (Array.isArray(v) && !(v.length > 0 && v.every(isScalar))) {
567
+ out[k] = v.length;
568
+ }
569
+ }
570
+ return out;
571
+ };
572
+ // Contadores auxiliares
573
+ const childCounter = new Map();
574
+ const childOrderByParent = new Map();
575
+ /**
576
+ * Crea un nodo y opcionalmente una arista hacia su padre.
577
+ */
578
+ const addNode = (jsonPath, obj, parentId) => {
579
+ const { title, usedKey } = pickTitle(obj, titleKeyPriority);
580
+ const attrs = buildPreviewAttributes(obj, usedKey);
581
+ // Orden relativo entre hermanos
582
+ let childOrder = undefined;
583
+ if (parentId) {
584
+ const idx = childOrderByParent.get(parentId) ?? 0;
585
+ childOrder = idx;
586
+ childOrderByParent.set(parentId, idx + 1);
587
+ }
588
+ const node = {
589
+ id: jsonPath,
590
+ jsonPath,
591
+ label: title,
592
+ data: obj,
593
+ jsonMeta: {
594
+ title,
595
+ titleKeyUsed: usedKey,
596
+ attributes: attrs,
597
+ childrenCount: 0,
598
+ arrayCounts: arrayCountsOf(obj),
599
+ childOrder,
600
+ },
601
+ width: defaultNodeSize.width,
602
+ height: defaultNodeSize.height,
603
+ };
604
+ nodes.push(node);
605
+ if (parentId) {
606
+ edges.push({
607
+ id: `${parentId}__${node.id}`,
608
+ source: parentId,
609
+ target: node.id,
610
+ });
611
+ }
612
+ return node.id;
613
+ };
614
+ /**
615
+ * Recorrido recursivo del JSON.
616
+ * - Usa `jsonPath` para mantener rutas únicas.
617
+ * - Crea nodos solo cuando `isEntity(obj)` es verdadero.
618
+ */
619
+ const traverse = (val, path, parentId, depth = 0) => {
620
+ if (maxDepth !== null && depth > maxDepth)
621
+ return;
622
+ if (Array.isArray(val)) {
623
+ val.forEach((c, i) => traverse(c, `${path}[${i}]`, parentId, depth + 1));
624
+ return;
625
+ }
626
+ if (val && typeof val === "object") {
627
+ const obj = val;
628
+ let myId = parentId;
629
+ if (isEntity(obj)) {
630
+ myId = addNode(path, obj, parentId);
631
+ if (parentId)
632
+ childCounter.set(parentId, (childCounter.get(parentId) ?? 0) + 1);
633
+ }
634
+ for (const [k, v] of Object.entries(obj)) {
635
+ if (isScalar(v))
636
+ continue;
637
+ if (Array.isArray(v)) {
638
+ const scalarArr = v.length > 0 && v.every(isScalar);
639
+ if (scalarArr && treatScalarArraysAsAttribute)
640
+ continue;
641
+ v.forEach((c, i) => traverse(c, `${path}.${k}[${i}]`, myId, depth + 1));
642
+ }
643
+ else {
644
+ traverse(v, `${path}.${k}`, myId, depth + 1);
645
+ }
646
+ }
647
+ }
648
+ };
649
+ traverse(input, "$", undefined, 0);
650
+ // Completar metadatos (childrenCount)
651
+ nodes.forEach((n) => {
652
+ n.jsonMeta = n.jsonMeta ?? {};
653
+ n.jsonMeta.childrenCount = childCounter.get(n.id) ?? 0;
654
+ });
655
+ return { nodes, edges, meta: {} };
656
+ }
657
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: JsonAdapterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
658
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: JsonAdapterService, providedIn: "root" }); }
659
+ }
660
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: JsonAdapterService, decorators: [{
661
+ type: Injectable,
662
+ args: [{ providedIn: "root" }]
663
+ }] });
664
+
665
+ // URL: projects/schema-ng16/src/lib/services/json-adapter.service.ts
666
+
667
+ /**
668
+ * Servicio: SchemaLayoutService
669
+ * ----------------------------------------------------------------------------
670
+ * Calcula posiciones (x,y) para un grafo normalizado (nodos/aristas) usando
671
+ * un layout jerárquico tipo "tidy tree" en dos variantes:
672
+ * - layoutDirection = "RIGHT": el árbol crece de izquierda → derecha.
673
+ * - layoutDirection = "DOWN" : el árbol crece de arriba → abajo.
674
+ *
675
+ * Características:
676
+ * - Respeta el orden de los hijos según `jsonMeta.childOrder` (estable, derivado del JSON).
677
+ * - Soporta alineación del padre con respecto a los hijos: "firstChild" o "center".
678
+ * - Genera puntos para aristas compatibles con estilos: "orthogonal", "curve" y "line".
679
+ * - Mantiene "pins" en `meta.pinY` o `meta.pinX` (pre-alistado por el componente) para anclajes.
680
+ */
681
+ /**
682
+ * Servicio de layout tipo “tidy tree” para RIGHT/DOWN.
683
+ * No gestiona pan/zoom ni overlays; solo posiciones y trayectorias.
684
+ */
685
+ class SchemaLayoutService {
686
+ /**
687
+ * Aplica layout al grafo.
688
+ * @param g Grafo normalizado (nodos/aristas).
689
+ * @param settings Configuración parcial. Se fusiona con DEFAULT_SETTINGS.
690
+ * @returns Grafo con nodos posicionados y aristas con `points`.
691
+ */
692
+ async layout(g, settings = DEFAULT_SETTINGS) {
693
+ const s = this.mergeSettings(settings);
694
+ // Dirección principal y opciones de alineación/estilo
695
+ const dir = s.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection;
696
+ const alignFirstChild = (s.layout?.layoutAlign ?? DEFAULT_SETTINGS.layout.layoutAlign) === "firstChild";
697
+ const linkStyle = s.layout?.linkStyle ?? DEFAULT_SETTINGS.layout.linkStyle;
698
+ // Gaps (separaciones) con defaults seguros
699
+ const GAP_X = s.layout?.columnGapPx ?? DEFAULT_SETTINGS.layout.columnGapPx ?? 0;
700
+ const GAP_Y = s.layout?.rowGapPx ?? DEFAULT_SETTINGS.layout.rowGapPx ?? 0;
701
+ // Índices de consulta rápida
702
+ const nodesById = new Map(g.nodes.map((n) => [n.id, n]));
703
+ const childrenById = new Map();
704
+ const parentsById = new Map();
705
+ for (const n of g.nodes) {
706
+ childrenById.set(n.id, []);
707
+ parentsById.set(n.id, []);
708
+ }
709
+ for (const e of g.edges) {
710
+ if (childrenById.has(e.source))
711
+ childrenById.get(e.source).push(e.target);
712
+ if (parentsById.has(e.target))
713
+ parentsById.get(e.target).push(e.source);
714
+ }
715
+ // Orden estable por childOrder (si empatan, ordena por id)
716
+ for (const [pid, arr] of childrenById) {
717
+ arr.sort((aId, bId) => {
718
+ const a = nodesById.get(aId);
719
+ const b = nodesById.get(bId);
720
+ const ao = a?.jsonMeta?.childOrder ?? 0;
721
+ const bo = b?.jsonMeta?.childOrder ?? 0;
722
+ return ao === bo ? (a?.id ?? "").localeCompare(b?.id ?? "") : ao - bo;
723
+ });
724
+ }
725
+ // Raíces (nodos sin padres)
726
+ const roots = g.nodes.filter((n) => (parentsById.get(n.id)?.length ?? 0) === 0);
727
+ // Profundidad por nodo (BFS)
728
+ const depthById = new Map();
729
+ const q = [...roots];
730
+ for (const r of roots)
731
+ depthById.set(r.id, 0);
732
+ while (q.length) {
733
+ const n = q.shift();
734
+ const d = depthById.get(n.id) ?? 0;
735
+ for (const cid of childrenById.get(n.id) ?? []) {
736
+ if (!depthById.has(cid)) {
737
+ depthById.set(cid, d + 1);
738
+ const cn = nodesById.get(cid);
739
+ if (cn)
740
+ q.push(cn);
741
+ }
742
+ }
743
+ }
744
+ // Helpers de tamaño (con fallback a defaults)
745
+ const getW = (n) => {
746
+ const fallback = DEFAULT_SETTINGS.dataView.defaultNodeSize?.width ?? 1;
747
+ const w = n.width ?? fallback;
748
+ return Number.isFinite(w) && w > 0 ? w : fallback;
749
+ };
750
+ const getH = (n) => {
751
+ const fallback = DEFAULT_SETTINGS.dataView.defaultNodeSize?.height ?? 1;
752
+ const h = n.height ?? fallback;
753
+ return Number.isFinite(h) && h > 0 ? h : fallback;
754
+ };
755
+ // Tamaño acumulado de cada subárbol en el eje secundario (para apilado)
756
+ const subtreeSize = new Map();
757
+ const measureSubtree = (id) => {
758
+ const node = nodesById.get(id);
759
+ const kids = childrenById.get(id) ?? [];
760
+ if (!node) {
761
+ subtreeSize.set(id, 0);
762
+ return 0;
763
+ }
764
+ if (kids.length === 0) {
765
+ const leafSize = dir === "RIGHT" ? getH(node) : getW(node);
766
+ subtreeSize.set(id, leafSize);
767
+ return leafSize;
768
+ }
769
+ let sum = 0;
770
+ for (let i = 0; i < kids.length; i++) {
771
+ sum += measureSubtree(kids[i]);
772
+ if (i < kids.length - 1)
773
+ sum += GAP_Y; // espacio entre hermanos
774
+ }
775
+ subtreeSize.set(id, sum);
776
+ return sum;
777
+ };
778
+ for (const r of roots)
779
+ measureSubtree(r.id);
780
+ // Offset acumulado por profundidad en el eje principal
781
+ const depths = Array.from(depthById.values());
782
+ const maxDepth = depths.length ? Math.max(0, ...depths) : 0;
783
+ const sizeByDepth = new Array(maxDepth + 1).fill(0);
784
+ for (let d = 0; d <= maxDepth; d++) {
785
+ const nodesAtD = g.nodes.filter((n) => (depthById.get(n.id) ?? 0) === d);
786
+ sizeByDepth[d] = dir === "RIGHT" ? Math.max(1, ...nodesAtD.map(getW), 1) : Math.max(1, ...nodesAtD.map(getH), 1);
787
+ }
788
+ const mainOffset = new Array(maxDepth + 1).fill(0);
789
+ for (let d = 1; d <= maxDepth; d++) {
790
+ mainOffset[d] = mainOffset[d - 1] + sizeByDepth[d - 1] + GAP_X;
791
+ }
792
+ // Preparación de "pin" en meta (mapa de centros por eje secundario)
793
+ const meta = g.meta ?? {};
794
+ const pinKey = dir === "RIGHT" ? "pinY" : "pinX";
795
+ if (!meta[pinKey])
796
+ meta[pinKey] = {};
797
+ const pin = meta[pinKey] ?? {};
798
+ /**
799
+ * Coloca recursivamente un subárbol:
800
+ * - depth: profundidad actual
801
+ * - start: posición inicial en el eje secundario para apilar hijos
802
+ * Retorna el tamaño del subárbol en el eje secundario.
803
+ */
804
+ const placeSubtree = (id, depth, start) => {
805
+ const node = nodesById.get(id);
806
+ const kids = childrenById.get(id) ?? [];
807
+ const mySize = subtreeSize.get(id) ?? 0;
808
+ const mainPos = mainOffset[depth] ?? 0;
809
+ if (!node)
810
+ return mySize;
811
+ // Hoja: centra sobre el rango [start, start+mySize]
812
+ if (kids.length === 0) {
813
+ const centerSec = start + mySize / 2;
814
+ if (dir === "RIGHT") {
815
+ node.x = Math.round(mainPos);
816
+ node.y = Math.round(centerSec - getH(node) / 2);
817
+ pin[node.id] = Math.round((node.y ?? 0) + getH(node) / 2);
818
+ }
819
+ else {
820
+ node.y = Math.round(mainPos);
821
+ node.x = Math.round(centerSec - getW(node) / 2);
822
+ pin[node.id] = Math.round((node.x ?? 0) + getW(node) / 2);
823
+ }
824
+ return mySize;
825
+ }
826
+ // Nodo con hijos: posiciona hijos apilados y centra el padre
827
+ let cursor = start;
828
+ const childCenters = [];
829
+ for (let i = 0; i < kids.length; i++) {
830
+ const cid = kids[i];
831
+ const cNode = nodesById.get(cid);
832
+ const cSize = subtreeSize.get(cid) ?? (cNode ? (dir === "RIGHT" ? getH(cNode) : getW(cNode)) : 0);
833
+ placeSubtree(cid, depth + 1, cursor);
834
+ if (cNode) {
835
+ const cCenter = dir === "RIGHT" ? (cNode.y ?? 0) + getH(cNode) / 2 : (cNode.x ?? 0) + getW(cNode) / 2;
836
+ childCenters.push(cCenter);
837
+ }
838
+ cursor += cSize + (i < kids.length - 1 ? GAP_Y : 0);
839
+ }
840
+ // Objetivo de centrado del padre
841
+ const targetCenter = alignFirstChild || childCenters.length === 0
842
+ ? (childCenters[0] ?? 0)
843
+ : childCenters.reduce((a, b) => a + b, 0) / Math.max(1, childCenters.length);
844
+ if (dir === "RIGHT") {
845
+ node.x = Math.round(mainPos);
846
+ node.y = Math.round(targetCenter - getH(node) / 2);
847
+ pin[node.id] = Math.round(targetCenter);
848
+ }
849
+ else {
850
+ node.y = Math.round(mainPos);
851
+ node.x = Math.round(targetCenter - getW(node) / 2);
852
+ pin[node.id] = Math.round(targetCenter);
853
+ }
854
+ return mySize;
855
+ };
856
+ // Coloca cada raíz con un margen superior/izquierdo inicial
857
+ let globalCursor = 40;
858
+ for (let i = 0; i < roots.length; i++) {
859
+ const r = roots[i];
860
+ const rSize = subtreeSize.get(r.id) ?? (dir === "RIGHT" ? getH(r) : getW(r));
861
+ placeSubtree(r.id, 0, globalCursor);
862
+ globalCursor += rSize + (i < roots.length - 1 ? (s.layout.rowGapPx ?? 0) : 0);
863
+ }
864
+ // Generación de puntos para aristas en función del estilo
865
+ const edges = g.edges.map((e) => {
866
+ const a = nodesById.get(e.source);
867
+ const b = nodesById.get(e.target);
868
+ if (!a || !b)
869
+ return { ...e, points: [] };
870
+ if (dir === "RIGHT") {
871
+ const ax = (a.x ?? 0) + getW(a);
872
+ const ay = (a.y ?? 0) + Math.round(getH(a) / 2);
873
+ const bx = b.x ?? 0;
874
+ const by = (b.y ?? 0) + Math.round(getH(b) / 2);
875
+ if (linkStyle === "orthogonal") {
876
+ const midX = Math.round((ax + bx) / 2);
877
+ return {
878
+ ...e,
879
+ points: [
880
+ { x: ax, y: ay },
881
+ { x: midX, y: ay },
882
+ { x: midX, y: by },
883
+ { x: bx, y: by },
884
+ ],
885
+ };
886
+ }
887
+ // curve/line: el componente define la forma final (curva o línea)
888
+ return {
889
+ ...e,
890
+ points: [
891
+ { x: ax, y: ay },
892
+ { x: bx, y: by },
893
+ ],
894
+ };
895
+ }
896
+ else {
897
+ const ax = (a.x ?? 0) + Math.round(getW(a) / 2);
898
+ const ay = (a.y ?? 0) + getH(a);
899
+ const bx = (b.x ?? 0) + Math.round(getW(b) / 2);
900
+ const by = b.y ?? 0;
901
+ if (linkStyle === "orthogonal") {
902
+ const midY = Math.round((ay + by) / 2);
903
+ return {
904
+ ...e,
905
+ points: [
906
+ { x: ax, y: ay },
907
+ { x: ax, y: midY },
908
+ { x: bx, y: midY },
909
+ { x: bx, y: by },
910
+ ],
911
+ };
912
+ }
913
+ return {
914
+ ...e,
915
+ points: [
916
+ { x: ax, y: ay },
917
+ { x: bx, y: by },
918
+ ],
919
+ };
920
+ }
921
+ });
922
+ // Retorna copias superficiales (no muta el original `g`)
923
+ return { nodes: g.nodes.map((n) => ({ ...n })), edges, meta: { ...(g.meta ?? {}) } };
924
+ }
925
+ /**
926
+ * Fusiona settings parciales con DEFAULT_SETTINGS, sección por sección.
927
+ */
928
+ mergeSettings(s) {
929
+ return {
930
+ colors: { ...DEFAULT_SETTINGS.colors, ...(s.colors ?? {}) },
931
+ layout: { ...DEFAULT_SETTINGS.layout, ...(s.layout ?? {}) },
932
+ dataView: { ...DEFAULT_SETTINGS.dataView, ...(s.dataView ?? {}) },
933
+ messages: { ...DEFAULT_SETTINGS.messages, ...(s.messages ?? {}) },
934
+ viewport: { ...DEFAULT_SETTINGS.viewport, ...(s.viewport ?? {}) },
935
+ };
936
+ }
937
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
938
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLayoutService, providedIn: "root" }); }
939
+ }
940
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLayoutService, decorators: [{
941
+ type: Injectable,
942
+ args: [{ providedIn: "root" }]
943
+ }] });
944
+
945
+ // URL: projects/schema-ng16/src/lib/components/schema.component.ts
946
+ class SchemaComponent {
947
+ set dataInput(v) {
948
+ this._data.set(v);
949
+ }
950
+ data() {
951
+ return this._data();
952
+ }
953
+ set settingsInput(v) {
954
+ this._settings.set(v ?? DEFAULT_SETTINGS);
955
+ }
956
+ settings() {
957
+ return this._settings();
958
+ }
959
+ set cardTemplateInput(tpl) {
960
+ this._cardTemplate.set(tpl ?? null);
961
+ }
962
+ cardTemplate() {
963
+ return this._cardTemplate();
964
+ }
965
+ set isLoadingInput(v) {
966
+ this._isLoading.set(!!v);
967
+ }
968
+ isLoading() {
969
+ return this._isLoading();
970
+ }
971
+ set isErrorInput(v) {
972
+ this._isError.set(!!v);
973
+ }
974
+ isError() {
975
+ return this._isError();
976
+ }
977
+ set emptyMessageInput(v) {
978
+ this._emptyMessage.set(typeof v === "string" ? v : "No hay datos para mostrar");
979
+ }
980
+ emptyMessage() {
981
+ return this._emptyMessage();
982
+ }
983
+ set loadingMessageInput(v) {
984
+ this._loadingMessage.set(typeof v === "string" ? v : "Cargando…");
985
+ }
986
+ loadingMessage() {
987
+ return this._loadingMessage();
988
+ }
989
+ set errorMessageInput(v) {
990
+ this._errorMessage.set(typeof v === "string" ? v : "Error al cargar el esquema");
991
+ }
992
+ errorMessage() {
993
+ return this._errorMessage();
994
+ }
995
+ constructor(adapter, layoutService) {
996
+ this.adapter = adapter;
997
+ this.layoutService = layoutService;
998
+ // ===== Inputs (Angular 16 con alias, alimentan señales internas) =====
999
+ this._data = signal(null);
1000
+ this._settings = signal(DEFAULT_SETTINGS);
1001
+ this._cardTemplate = signal(null);
1002
+ // Overlays (mantienen compatibilidad con propiedades existentes)
1003
+ this._isLoading = signal(false);
1004
+ this._isError = signal(false);
1005
+ this._emptyMessage = signal("No hay datos para mostrar");
1006
+ this._loadingMessage = signal("Cargando…");
1007
+ this._errorMessage = signal("Error al cargar el esquema");
1008
+ // Viewport (no son inputs; se derivan de settings)
1009
+ this.viewportHeight = signal(800);
1010
+ this.minViewportHeight = signal(480);
1011
+ this.showToolbar = signal(true);
1012
+ // ===== Outputs =====
1013
+ this.nodeClick = new EventEmitter();
1014
+ this.linkClick = new EventEmitter();
1015
+ // ===== Estado de grafo =====
1016
+ this.fullGraph = signal({ nodes: [], edges: [] });
1017
+ this.graph = signal({ nodes: [], edges: [] });
1018
+ this.nodes = computed(() => this.graph().nodes);
1019
+ this.edges = computed(() => this.graph().edges);
1020
+ // ===== Pan/zoom =====
1021
+ this.scale = signal(1);
1022
+ this.minScale = signal(0.2);
1023
+ this.maxScale = signal(3);
1024
+ this.tx = signal(0);
1025
+ this.ty = signal(0);
1026
+ this.dragging = false;
1027
+ this.lastX = 0;
1028
+ this.lastY = 0;
1029
+ this.lastViewport = null;
1030
+ /** Último punto de interacción sobre la stage (coordenadas de pantalla relativas al root). */
1031
+ this.lastPointerScreen = null;
1032
+ this.wheelRaf = null;
1033
+ this.pendingWheel = null;
1034
+ this.transform = computed(() => `translate(${this.tx()}px, ${this.ty()}px) scale(${this.scale()})`);
1035
+ this.virtualWidth = 12000;
1036
+ this.virtualHeight = 6000;
1037
+ // ===== Toolbar overrides =====
1038
+ this.opt_linkStyle = signal("orthogonal");
1039
+ this.opt_layoutAlign = signal("firstChild");
1040
+ this.opt_layoutDirection = signal("RIGHT");
1041
+ // ===== Mensajes derivados (prioriza settings.messages si están definidos) =====
1042
+ this.isLoadingView = computed(() => this.settings().messages?.isLoading ?? this.isLoading());
1043
+ this.isErrorView = computed(() => this.settings().messages?.isError ?? this.isError());
1044
+ this.emptyMessageView = computed(() => this.settings().messages?.emptyMessage ?? this.emptyMessage());
1045
+ this.loadingMessageView = computed(() => this.settings().messages?.loadingMessage ?? this.loadingMessage());
1046
+ this.errorMessageView = computed(() => this.settings().messages?.errorMessage ?? this.errorMessage());
1047
+ // ===== Collapse/expand =====
1048
+ this.childrenById = new Map();
1049
+ this.parentsById = new Map();
1050
+ this.collapsed = new Set();
1051
+ this.measureIdsToCheck = null;
1052
+ this.hasComputedOnce = false;
1053
+ this.enableCollapse = computed(() => {
1054
+ const b = this.baseSettings();
1055
+ return b.dataView?.enableCollapse ?? DEFAULT_SETTINGS.dataView.enableCollapse;
1056
+ });
1057
+ // ===== Toolbar controls visibility =====
1058
+ this.toolbarShowLinkStyle = computed(() => {
1059
+ const b = this.baseSettings();
1060
+ return b.viewport?.toolbarControls?.showLinkStyle ?? true;
1061
+ });
1062
+ this.toolbarShowLayoutAlign = computed(() => {
1063
+ const b = this.baseSettings();
1064
+ return b.viewport?.toolbarControls?.showLayoutAlign ?? true;
1065
+ });
1066
+ this.toolbarShowLayoutDirection = computed(() => {
1067
+ const b = this.baseSettings();
1068
+ return b.viewport?.toolbarControls?.showLayoutDirection ?? true;
1069
+ });
1070
+ // ===== Settings efectivos =====
1071
+ this.baseSettings = computed(() => {
1072
+ const s = this.settings() ?? {};
1073
+ return {
1074
+ messages: { ...DEFAULT_SETTINGS.messages, ...s.messages },
1075
+ viewport: { ...DEFAULT_SETTINGS.viewport, ...s.viewport },
1076
+ colors: { ...DEFAULT_SETTINGS.colors, ...s.colors },
1077
+ layout: { ...DEFAULT_SETTINGS.layout, ...s.layout },
1078
+ dataView: { ...DEFAULT_SETTINGS.dataView, ...s.dataView },
1079
+ };
1080
+ });
1081
+ this.effectiveSettings = computed(() => {
1082
+ const b = this.baseSettings();
1083
+ return {
1084
+ ...b,
1085
+ layout: {
1086
+ ...b.layout,
1087
+ linkStyle: this.opt_linkStyle(),
1088
+ layoutAlign: this.opt_layoutAlign(),
1089
+ layoutDirection: this.opt_layoutDirection(),
1090
+ },
1091
+ };
1092
+ });
1093
+ /** Devuelve si el nodo tiene hijos (para pintar botón de colapso). */
1094
+ this.hasChildren = (id) => (this.childrenById.get(id)?.length ?? 0) > 0;
1095
+ /** Devuelve si el nodo está colapsado (estado visual en la card). */
1096
+ this.isNodeCollapsed = (id) => this.collapsed.has(id);
1097
+ this.trackByNodeId = (_, n) => n?.id;
1098
+ }
1099
+ recomputeFromSettings() {
1100
+ const b = this.baseSettings();
1101
+ this.viewportHeight.set(b.viewport?.height ?? DEFAULT_SETTINGS.viewport.height);
1102
+ this.minViewportHeight.set(b.viewport?.minHeight ?? DEFAULT_SETTINGS.viewport.minHeight);
1103
+ this.showToolbar.set(b.viewport?.showToolbar ?? DEFAULT_SETTINGS.viewport.showToolbar);
1104
+ this.opt_linkStyle.set((b.layout?.linkStyle ?? DEFAULT_SETTINGS.layout.linkStyle));
1105
+ this.opt_layoutAlign.set((b.layout?.layoutAlign ?? DEFAULT_SETTINGS.layout.layoutAlign));
1106
+ this.opt_layoutDirection.set((b.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection));
1107
+ }
1108
+ ngAfterViewInit() {
1109
+ this.recomputeFromSettings();
1110
+ const rootEl = this.rootRef?.nativeElement;
1111
+ if (rootEl && "ResizeObserver" in window) {
1112
+ const r0 = rootEl.getBoundingClientRect();
1113
+ this.lastViewport = { w: r0.width, h: r0.height };
1114
+ this.resizeObs = new ResizeObserver(() => {
1115
+ const rect = rootEl.getBoundingClientRect();
1116
+ this.preserveCenterOnResize(rect.width, rect.height);
1117
+ });
1118
+ this.resizeObs.observe(rootEl);
1119
+ }
1120
+ this.compute();
1121
+ }
1122
+ ngOnChanges(_) {
1123
+ this.recomputeFromSettings();
1124
+ this.compute();
1125
+ }
1126
+ ngOnDestroy() {
1127
+ if (this.resizeObs) {
1128
+ try {
1129
+ this.resizeObs.disconnect();
1130
+ }
1131
+ catch { }
1132
+ }
1133
+ }
1134
+ // ===== Pipeline principal =====
1135
+ async compute() {
1136
+ if (this.isLoadingView())
1137
+ return;
1138
+ if (this.hasComputedOnce && this.data() === this.lastDataRef && this.settings() === this.lastSettingsRef) {
1139
+ return;
1140
+ }
1141
+ const s = this.effectiveSettings();
1142
+ const normalized = this.adapter.normalize(this.data(), s);
1143
+ this.ensurePinMeta(normalized, s);
1144
+ this.fullGraph.set(this.cloneGraph(normalized));
1145
+ this.buildIndices();
1146
+ if (!this.enableCollapse())
1147
+ this.collapsed.clear();
1148
+ const visible = this.buildVisibleGraphFromCollapsed();
1149
+ let laid = await this.layoutService.layout(visible, s);
1150
+ this.graph.set(this.cloneGraph(laid));
1151
+ if (s.dataView?.autoResizeCards ?? DEFAULT_SETTINGS.dataView.autoResizeCards) {
1152
+ const maxPasses = 6;
1153
+ this.measureIdsToCheck = null; // primera pasada: medir todo
1154
+ for (let pass = 1; pass <= maxPasses; pass++) {
1155
+ await this.nextFrame();
1156
+ const changed = this.measureAndApply(pass);
1157
+ if (!changed)
1158
+ break;
1159
+ laid = await this.layoutService.layout(this.graph(), s);
1160
+ this.graph.set(this.cloneGraph(laid));
1161
+ }
1162
+ }
1163
+ this.updateVirtualSizeFromGraph(laid);
1164
+ this.fitToViewByBounds(); // asegura minScale y encuadre base
1165
+ this.centerOnFirstNodeOrFit(); // centra como doble clic
1166
+ this.lastDataRef = this.data();
1167
+ this.lastSettingsRef = this.settings();
1168
+ this.hasComputedOnce = true;
1169
+ }
1170
+ async relayoutVisible(anchorId, anchorScreen) {
1171
+ const s = this.effectiveSettings();
1172
+ const visible = this.buildVisibleGraphFromCollapsed();
1173
+ let laid = await this.layoutService.layout(visible, s);
1174
+ this.graph.set(this.cloneGraph(laid));
1175
+ if (s.dataView?.autoResizeCards ?? DEFAULT_SETTINGS.dataView.autoResizeCards) {
1176
+ const maxPasses = 4;
1177
+ this.measureIdsToCheck = null; // primera pasada: medir todo
1178
+ for (let pass = 1; pass <= maxPasses; pass++) {
1179
+ await this.nextFrame();
1180
+ const changed = this.measureAndApply(pass);
1181
+ if (!changed)
1182
+ break;
1183
+ laid = await this.layoutService.layout(this.graph(), s);
1184
+ this.graph.set(this.cloneGraph(laid));
1185
+ }
1186
+ }
1187
+ this.updateVirtualSizeFromGraph(laid);
1188
+ await this.animateToGraph(laid, 260, anchorId, anchorScreen);
1189
+ }
1190
+ async onCardToggle(n) {
1191
+ if (!this.enableCollapse() || !n?.id)
1192
+ return;
1193
+ const anchorBefore = this.getNodeScreenCenter(n);
1194
+ if (this.collapsed.has(n.id))
1195
+ this.collapsed.delete(n.id);
1196
+ else
1197
+ this.collapsed.add(n.id);
1198
+ await this.relayoutVisible(n.id, anchorBefore);
1199
+ }
1200
+ // --- Animación de transición entre grafos (requerido por relayoutVisible) ---
1201
+ async animateToGraph(target, durationMs = 260, anchorId, anchorScreen) {
1202
+ const start = this.cloneGraph(this.graph());
1203
+ const startNodeById = new Map(start.nodes.map((n) => [n.id, n]));
1204
+ const startEdgeById = new Map(start.edges.map((e) => [e.id, e]));
1205
+ const lerp = (a, b, t) => a + (b - a) * t;
1206
+ const easeInOut = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
1207
+ const alignPoints = (a = [], b = []) => {
1208
+ const aa = a.length ? [...a] : [];
1209
+ const bb = b.length ? [...b] : [];
1210
+ const len = Math.max(aa.length, bb.length, 2);
1211
+ while (aa.length < len)
1212
+ aa.push(aa[aa.length - 1] ?? { x: 0, y: 0 });
1213
+ while (bb.length < len)
1214
+ bb.push(bb[bb.length - 1] ?? { x: 0, y: 0 });
1215
+ return { aa, bb, len };
1216
+ };
1217
+ const t0 = performance.now();
1218
+ const run = (resolve) => {
1219
+ const now = performance.now();
1220
+ const raw = Math.min(1, (now - t0) / Math.max(1, durationMs));
1221
+ const t = easeInOut(raw);
1222
+ const frame = { nodes: [], edges: [], meta: { ...(target.meta ?? {}) } };
1223
+ for (const endNode of target.nodes) {
1224
+ const sNode = startNodeById.get(endNode.id) ?? endNode;
1225
+ const xn = lerp(sNode.x ?? 0, endNode.x ?? 0, t);
1226
+ const yn = lerp(sNode.y ?? 0, endNode.y ?? 0, t);
1227
+ const wn = lerp(sNode.width ?? 0, endNode.width ?? 0, t);
1228
+ const hn = lerp(sNode.height ?? 0, endNode.height ?? 0, t);
1229
+ frame.nodes.push({
1230
+ ...endNode,
1231
+ x: Math.round(xn),
1232
+ y: Math.round(yn),
1233
+ width: Math.round(wn),
1234
+ height: Math.round(hn),
1235
+ });
1236
+ }
1237
+ for (const endEdge of target.edges) {
1238
+ const sEdge = startEdgeById.get(endEdge.id) ?? endEdge;
1239
+ const { aa, bb, len } = alignPoints(sEdge.points, endEdge.points);
1240
+ const pts = new Array(len)
1241
+ .fill(0)
1242
+ .map((_, i) => ({ x: lerp(aa[i].x, bb[i].x, t), y: lerp(aa[i].y, bb[i].y, t) }));
1243
+ frame.edges.push({ ...endEdge, points: pts });
1244
+ }
1245
+ this.graph.set(frame);
1246
+ if (anchorId && anchorScreen)
1247
+ this.applyAnchorAfterLayout(anchorId, anchorScreen);
1248
+ if (raw < 1) {
1249
+ requestAnimationFrame(() => run(resolve));
1250
+ }
1251
+ else {
1252
+ this.graph.set(this.cloneGraph(target));
1253
+ if (anchorId && anchorScreen)
1254
+ this.applyAnchorAfterLayout(anchorId, anchorScreen);
1255
+ resolve();
1256
+ }
1257
+ };
1258
+ await new Promise((resolve) => requestAnimationFrame(() => run(resolve)));
1259
+ }
1260
+ // ===== Interacción (solo en .stage) =====
1261
+ onWheel(e) {
1262
+ e.preventDefault();
1263
+ this.pendingWheel = { deltaY: e.deltaY, clientX: e.clientX, clientY: e.clientY };
1264
+ if (this.wheelRaf !== null)
1265
+ return;
1266
+ this.wheelRaf = requestAnimationFrame(() => {
1267
+ const ev = this.pendingWheel;
1268
+ this.pendingWheel = null;
1269
+ this.wheelRaf = null;
1270
+ if (!ev)
1271
+ return;
1272
+ const rect = this.rootRef.nativeElement.getBoundingClientRect();
1273
+ const mouseX = ev.clientX - rect.left;
1274
+ const mouseY = ev.clientY - rect.top;
1275
+ this.lastPointerScreen = { x: mouseX, y: mouseY };
1276
+ const oldScale = this.scale();
1277
+ const factor = 1 + (-ev.deltaY > 0 ? 0.08 : -0.08);
1278
+ const newScale = Math.max(this.minScale(), Math.min(this.maxScale(), oldScale * factor));
1279
+ const worldX = (mouseX - this.tx()) / oldScale;
1280
+ const worldY = (mouseY - this.ty()) / oldScale;
1281
+ this.tx.set(mouseX - worldX * newScale);
1282
+ this.ty.set(mouseY - worldY * newScale);
1283
+ this.scale.set(newScale);
1284
+ });
1285
+ }
1286
+ onPointerDown(e) {
1287
+ e.preventDefault();
1288
+ this.dragging = true;
1289
+ const el = e.target;
1290
+ if (el && el.closest && el.closest(".collapse-btn")) {
1291
+ this.dragging = false;
1292
+ return;
1293
+ }
1294
+ try {
1295
+ e.target.setPointerCapture?.(e.pointerId);
1296
+ }
1297
+ catch { }
1298
+ const rect = this.rootRef.nativeElement.getBoundingClientRect();
1299
+ this.lastX = e.clientX;
1300
+ this.lastY = e.clientY;
1301
+ this.lastPointerScreen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1302
+ }
1303
+ onPointerMove(e) {
1304
+ const rect = this.rootRef.nativeElement.getBoundingClientRect();
1305
+ this.lastPointerScreen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1306
+ if (!this.dragging)
1307
+ return;
1308
+ const dx = e.clientX - this.lastX;
1309
+ const dy = e.clientY - this.lastY;
1310
+ this.tx.set(this.tx() + dx);
1311
+ this.ty.set(this.ty() + dy);
1312
+ this.lastX = e.clientX;
1313
+ this.lastY = e.clientY;
1314
+ }
1315
+ onPointerUp() {
1316
+ this.dragging = false;
1317
+ }
1318
+ onPointerLeave() {
1319
+ this.dragging = false;
1320
+ this.lastPointerScreen = null;
1321
+ }
1322
+ onDblClick() {
1323
+ this.centerOnFirstNodeOrFit();
1324
+ }
1325
+ // ===== Toolbar actions =====
1326
+ zoomIn() {
1327
+ this.applyZoom(1.15);
1328
+ }
1329
+ zoomOut() {
1330
+ this.applyZoom(1 / 1.15);
1331
+ }
1332
+ resetView() {
1333
+ this.centerOnFirstNodeOrFit();
1334
+ }
1335
+ setLinkStyle(v) {
1336
+ const ok = v === "orthogonal" || v === "curve" || v === "line";
1337
+ this.opt_linkStyle.set(ok ? v : "orthogonal");
1338
+ this.relayoutVisible();
1339
+ }
1340
+ setLayoutAlign(v) {
1341
+ const ok = v === "firstChild" || v === "center";
1342
+ this.opt_layoutAlign.set(ok ? v : "center");
1343
+ this.relayoutVisible();
1344
+ }
1345
+ setLayoutDirection(v) {
1346
+ const ok = v === "RIGHT" || v === "DOWN";
1347
+ this.opt_layoutDirection.set(ok ? v : "RIGHT");
1348
+ this.relayoutVisible();
1349
+ }
1350
+ /* ========================================================================
1351
+ * Helpers
1352
+ * ===================================================================== */
1353
+ cloneGraph(g) {
1354
+ return {
1355
+ nodes: g.nodes.map((n) => ({ ...n })),
1356
+ edges: g.edges.map((e) => ({ ...e })),
1357
+ meta: { ...(g.meta ?? {}) },
1358
+ };
1359
+ }
1360
+ nextFrame() {
1361
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
1362
+ }
1363
+ ensurePinMeta(g, s) {
1364
+ if (!g.meta)
1365
+ g.meta = {};
1366
+ const dir = s.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection;
1367
+ const key = dir === "RIGHT" ? "pinY" : "pinX";
1368
+ if (!g.meta[key])
1369
+ g.meta[key] = {};
1370
+ }
1371
+ updateVirtualSizeFromGraph(g) {
1372
+ const pad = 200;
1373
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1374
+ for (const n of g.nodes) {
1375
+ const x = n.x ?? 0;
1376
+ const y = n.y ?? 0;
1377
+ const w = n.width ?? 0;
1378
+ const h = n.height ?? 0;
1379
+ minX = Math.min(minX, x);
1380
+ minY = Math.min(minY, y);
1381
+ maxX = Math.max(maxX, x + w);
1382
+ maxY = Math.max(maxY, y + h);
1383
+ }
1384
+ if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) {
1385
+ this.virtualWidth = 12000;
1386
+ this.virtualHeight = 6000;
1387
+ return;
1388
+ }
1389
+ const neededW = Math.max(1, Math.ceil(maxX - Math.min(0, minX)) + pad);
1390
+ const neededH = Math.max(1, Math.ceil(maxY - Math.min(0, minY)) + pad);
1391
+ this.virtualWidth = Math.max(neededW, 2000);
1392
+ this.virtualHeight = Math.max(neededH, 1200);
1393
+ }
1394
+ buildIndices() {
1395
+ this.childrenById.clear();
1396
+ this.parentsById.clear();
1397
+ const g = this.fullGraph();
1398
+ for (const n of g.nodes) {
1399
+ this.childrenById.set(n.id, []);
1400
+ this.parentsById.set(n.id, []);
1401
+ }
1402
+ for (const e of g.edges) {
1403
+ const ch = this.childrenById.get(e.source);
1404
+ if (ch)
1405
+ ch.push(e.target);
1406
+ const pr = this.parentsById.get(e.target);
1407
+ if (pr)
1408
+ pr.push(e.source);
1409
+ }
1410
+ }
1411
+ isVisibleNodeByCollapsedAncestors(id) {
1412
+ if (!this.enableCollapse())
1413
+ return true;
1414
+ const stack = [...(this.parentsById.get(id) ?? [])];
1415
+ const seen = new Set();
1416
+ while (stack.length) {
1417
+ const p = stack.pop();
1418
+ if (seen.has(p))
1419
+ continue;
1420
+ seen.add(p);
1421
+ if (this.collapsed.has(p))
1422
+ return false;
1423
+ const pp = this.parentsById.get(p);
1424
+ if (pp && pp.length)
1425
+ stack.push(...pp);
1426
+ }
1427
+ return true;
1428
+ }
1429
+ buildVisibleGraphFromCollapsed() {
1430
+ const full = this.fullGraph();
1431
+ if (!this.enableCollapse())
1432
+ return this.cloneGraph(full);
1433
+ const visibleNodeSet = new Set();
1434
+ for (const n of full.nodes) {
1435
+ if (this.isVisibleNodeByCollapsedAncestors(n.id))
1436
+ visibleNodeSet.add(n.id);
1437
+ }
1438
+ const nodes = full.nodes.filter((n) => visibleNodeSet.has(n.id));
1439
+ const edges = full.edges.filter((e) => visibleNodeSet.has(e.source) && visibleNodeSet.has(e.target));
1440
+ return { nodes, edges, meta: full.meta };
1441
+ }
1442
+ measureAndApply(pass) {
1443
+ const s = this.effectiveSettings();
1444
+ const extraW = s.dataView?.paddingWidthPx ?? 0;
1445
+ const extraH = s.dataView?.paddingHeightPx ?? 0;
1446
+ const maxW = s.dataView?.maxCardWidth ?? Infinity;
1447
+ const maxH = s.dataView?.maxCardHeight ?? Infinity;
1448
+ const root = this.rootRef.nativeElement;
1449
+ const cards = Array.from(root.querySelectorAll(".schema-card"));
1450
+ const visMap = new Map(this.graph().nodes.map((n) => [n.id, n]));
1451
+ const fullMap = new Map(this.fullGraph().nodes.map((n) => [n.id, n]));
1452
+ let changed = false;
1453
+ const idsToMeasure = pass === 1 ? null : this.measureIdsToCheck;
1454
+ const nextChanged = new Set();
1455
+ const measures = [];
1456
+ for (const el of cards) {
1457
+ const id = el.getAttribute("data-node-id") ?? undefined;
1458
+ if (!id)
1459
+ continue;
1460
+ if (idsToMeasure && !idsToMeasure.has(id))
1461
+ continue;
1462
+ const node = visMap.get(id);
1463
+ if (!node)
1464
+ continue;
1465
+ const prevW = el.style.width;
1466
+ const prevH = el.style.height;
1467
+ el.style.width = "auto";
1468
+ el.style.height = "auto";
1469
+ const wIntrinsic = Math.ceil(el.scrollWidth);
1470
+ const hIntrinsic = Math.ceil(el.scrollHeight);
1471
+ el.style.width = prevW;
1472
+ el.style.height = prevH;
1473
+ const targetW = Math.min(wIntrinsic + extraW, maxW);
1474
+ const targetH = Math.min(hIntrinsic + extraH, maxH);
1475
+ measures.push({ id, w: targetW, h: targetH });
1476
+ }
1477
+ for (const m of measures) {
1478
+ const node = visMap.get(m.id);
1479
+ if (!node)
1480
+ continue;
1481
+ if ((node.width ?? 0) !== m.w || (node.height ?? 0) !== m.h) {
1482
+ node.width = m.w;
1483
+ node.height = m.h;
1484
+ changed = true;
1485
+ nextChanged.add(m.id);
1486
+ const full = fullMap.get(m.id);
1487
+ if (full) {
1488
+ full.width = m.w;
1489
+ full.height = m.h;
1490
+ }
1491
+ }
1492
+ }
1493
+ this.measureIdsToCheck = nextChanged.size ? nextChanged : null;
1494
+ return changed;
1495
+ }
1496
+ getViewportSize() {
1497
+ const el = this.rootRef.nativeElement;
1498
+ const rect = el.getBoundingClientRect();
1499
+ return { w: rect.width, h: rect.height };
1500
+ }
1501
+ getGraphBounds() {
1502
+ const ns = this.nodes();
1503
+ if (!ns.length)
1504
+ return { minX: 0, minY: 0, maxX: 1, maxY: 1 };
1505
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1506
+ for (const n of ns) {
1507
+ const nx = n.x ?? 0;
1508
+ const ny = n.y ?? 0;
1509
+ const nw = n.width ?? 0;
1510
+ const nh = n.height ?? 0;
1511
+ minX = Math.min(minX, nx);
1512
+ minY = Math.min(minY, ny);
1513
+ maxX = Math.max(maxX, nx + nw);
1514
+ maxY = Math.max(maxY, ny + nh);
1515
+ }
1516
+ return { minX, minY, maxX, maxY };
1517
+ }
1518
+ computeScaleToFit(pad = 24) {
1519
+ const { w, h } = this.getViewportSize();
1520
+ const { minX, minY, maxX, maxY } = this.getGraphBounds();
1521
+ const gw = Math.max(1, maxX - minX);
1522
+ const gh = Math.max(1, maxY - minY);
1523
+ const sx = (w - pad) / gw;
1524
+ const sy = (h - pad) / gh;
1525
+ const fit = Math.max(0.05, Math.min(sx, sy));
1526
+ return { fit, minX, minY, maxX, maxY, w, h };
1527
+ }
1528
+ fitToViewByBounds() {
1529
+ const { fit, minX, minY, maxX, maxY, w, h } = this.computeScaleToFit(24);
1530
+ this.minScale.set(Math.min(fit, 1));
1531
+ this.scale.set(Math.max(this.scale(), this.minScale()));
1532
+ const cx = (minX + maxX) / 2;
1533
+ const cy = (minY + maxY) / 2;
1534
+ const s = this.scale();
1535
+ this.tx.set(w / 2 - cx * s);
1536
+ this.ty.set(h / 2 - cy * s);
1537
+ }
1538
+ /** Zoom anclado al último punto de interacción sobre la stage; si no hay, usa el centro del viewport. */
1539
+ applyZoom(factor) {
1540
+ const rect = this.rootRef.nativeElement.getBoundingClientRect();
1541
+ const anchor = this.lastPointerScreen ?? { x: rect.width / 2, y: rect.height / 2 };
1542
+ const oldScale = this.scale();
1543
+ const newScale = Math.max(this.minScale(), Math.min(this.maxScale(), oldScale * factor));
1544
+ const worldX = (anchor.x - this.tx()) / oldScale;
1545
+ const worldY = (anchor.y - this.ty()) / oldScale;
1546
+ this.tx.set(anchor.x - worldX * newScale);
1547
+ this.ty.set(anchor.y - worldY * newScale);
1548
+ this.scale.set(newScale);
1549
+ }
1550
+ preserveCenterOnResize(newW, newH) {
1551
+ if (!this.lastViewport) {
1552
+ this.lastViewport = { w: newW, h: newH };
1553
+ return;
1554
+ }
1555
+ const oldW = this.lastViewport.w;
1556
+ const oldH = this.lastViewport.h;
1557
+ const s = this.scale();
1558
+ const worldCx = (oldW / 2 - this.tx()) / s;
1559
+ const worldCy = (oldH / 2 - this.ty()) / s;
1560
+ this.tx.set(newW / 2 - worldCx * s);
1561
+ this.ty.set(newH / 2 - worldCy * s);
1562
+ this.lastViewport = { w: newW, h: newH };
1563
+ }
1564
+ getNodeById(id) {
1565
+ if (!id)
1566
+ return undefined;
1567
+ return this.graph().nodes.find((n) => n.id === id);
1568
+ }
1569
+ getNodeScreenCenter(n) {
1570
+ const s = this.scale();
1571
+ const cx = (n.x ?? 0) + (n.width ?? 0) / 2;
1572
+ const cy = (n.y ?? 0) + (n.height ?? 0) / 2;
1573
+ return { x: cx * s + this.tx(), y: cy * s + this.ty() };
1574
+ }
1575
+ applyAnchorAfterLayout(nodeId, targetScreen) {
1576
+ const n = this.getNodeById(nodeId);
1577
+ if (!n)
1578
+ return;
1579
+ this.centerOnNodeAtScreen(n, targetScreen);
1580
+ }
1581
+ getFirstVisibleNode() {
1582
+ const list = this.nodes();
1583
+ return list.length ? list[0] : null;
1584
+ }
1585
+ centerOnNode(n) {
1586
+ const rect = this.rootRef.nativeElement.getBoundingClientRect();
1587
+ const viewportCx = rect.width / 2;
1588
+ const viewportCy = rect.height / 2;
1589
+ const s = this.scale();
1590
+ const nodeCx = (n.x ?? 0) + (n.width ?? 0) / 2;
1591
+ const nodeCy = (n.y ?? 0) + (n.height ?? 0) / 2;
1592
+ this.tx.set(viewportCx - nodeCx * s);
1593
+ this.ty.set(viewportCy - nodeCy * s);
1594
+ }
1595
+ /** Centra un nodo de forma que su centro quede en una posición de pantalla dada (x,y). */
1596
+ centerOnNodeAtScreen(n, screen) {
1597
+ const s = this.scale();
1598
+ const nodeCx = (n.x ?? 0) + (n.width ?? 0) / 2;
1599
+ const nodeCy = (n.y ?? 0) + (n.height ?? 0) / 2;
1600
+ this.tx.set(screen.x - nodeCx * s);
1601
+ this.ty.set(screen.y - nodeCy * s);
1602
+ }
1603
+ centerOnFirstNodeOrFit() {
1604
+ const first = this.getFirstVisibleNode();
1605
+ if (first)
1606
+ this.centerOnNode(first);
1607
+ else
1608
+ this.fitToViewByBounds();
1609
+ }
1610
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaComponent, deps: [{ token: JsonAdapterService }, { token: SchemaLayoutService }], target: i0.ɵɵFactoryTarget.Component }); }
1611
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.18", type: SchemaComponent, isStandalone: true, selector: "schema", inputs: { dataInput: ["data", "dataInput"], settingsInput: ["settings", "settingsInput"], cardTemplateInput: ["cardTemplate", "cardTemplateInput"], isLoadingInput: ["isLoading", "isLoadingInput"], isErrorInput: ["isError", "isErrorInput"], emptyMessageInput: ["emptyMessage", "emptyMessageInput"], loadingMessageInput: ["loadingMessage", "loadingMessageInput"], errorMessageInput: ["errorMessage", "errorMessageInput"] }, outputs: { nodeClick: "nodeClick", linkClick: "linkClick" }, viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["root"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- URL: projects/schema-ng16/src/lib/components/schema.component.html -->\n<div class=\"schema-root\" #root [style.height.px]=\"viewportHeight()\" [style.minHeight.px]=\"minViewportHeight()\">\n <div class=\"schema-toolbar\" *ngIf=\"showToolbar() && !isLoadingView() && !isErrorView()\">\n <div class=\"toolbar-actions\">\n <button type=\"button\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button type=\"button\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <button type=\"button\" (click)=\"resetView()\" title=\"Centrar\">\u293E</button>\n </div>\n\n <div class=\"toolbar-selectors\">\n <label *ngIf=\"toolbarShowLinkStyle()\">\n Enlaces:\n <select #ls [value]=\"opt_linkStyle()\" (change)=\"setLinkStyle(ls.value)\">\n <option value=\"curve\">Curvo</option>\n <option value=\"orthogonal\">Ortogonal</option>\n <option value=\"line\">Lineal</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutAlign()\">\n Alineaci\u00F3n:\n <select #la [value]=\"opt_layoutAlign()\" (change)=\"setLayoutAlign(la.value)\">\n <option value=\"firstChild\">Superior</option>\n <option value=\"center\">Centrado</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutDirection()\">\n Direcci\u00F3n:\n <select #ld [value]=\"opt_layoutDirection()\" (change)=\"setLayoutDirection(ld.value)\">\n <option value=\"RIGHT\">Derecha</option>\n <option value=\"DOWN\">Abajo</option>\n </select>\n </label>\n </div>\n </div>\n\n <!-- Overlays -->\n <div class=\"overlay loading\" *ngIf=\"isLoadingView()\">\n <div class=\"loading-banner\">\n <div class=\"shimmer\"></div>\n <span class=\"msg\">{{ loadingMessageView() }}</span>\n </div>\n </div>\n\n <div class=\"overlay empty\" *ngIf=\"!isLoadingView() && data == null\">\n <div class=\"empty-banner\">{{ emptyMessageView() }}</div>\n </div>\n\n <div class=\"overlay error\" *ngIf=\"!isLoadingView() && isErrorView()\">\n <div class=\"error-banner\">{{ errorMessageView() }}</div>\n </div>\n\n <!-- Stage -->\n <div\n class=\"stage\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp()\"\n (pointerleave)=\"onPointerLeave()\"\n (dblclick)=\"onDblClick()\"\n [style.transform]=\"transform()\"\n [style.width.px]=\"virtualWidth\"\n [style.height.px]=\"virtualHeight\"\n *ngIf=\"!isLoadingView() && !isErrorView()\"\n >\n <schema-links\n [edges]=\"edges()\"\n [settings]=\"effectiveSettings()\"\n (linkClick)=\"linkClick.emit($event)\"\n [width]=\"virtualWidth\"\n [height]=\"virtualHeight\"\n ></schema-links>\n\n <ng-container *ngFor=\"let n of nodes(); trackBy: trackByNodeId\">\n <schema-card\n [node]=\"n\"\n [settings]=\"effectiveSettings()\"\n [cardTemplate]=\"cardTemplate()\"\n [hasChildren]=\"hasChildren(n.id)\"\n [showCollapseControls]=\"enableCollapse()\"\n [isCollapsed]=\"isNodeCollapsed(n.id)\"\n (toggleRequest)=\"onCardToggle($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n ></schema-card>\n </ng-container>\n </div>\n</div>\n", styles: [".toolbar-actions button{border:0;background:transparent;padding:0;margin:0;font:inherit;color:inherit}.toolbar-selectors select{border:0;background:transparent;appearance:none;font:inherit;color:inherit}.schema-toolbar{border-radius:10px;border:1px solid rgba(0,0,0,.06);box-shadow:0 2px 8px #00000014}.loading-banner,.empty-banner,.error-banner{pointer-events:auto;border-radius:10px;padding:16px 20px;box-shadow:0 6px 20px #00000014;font-weight:600}.schema-root{position:relative;width:100%;overflow:hidden;background:#f7f9fb;border-radius:8px}.schema-toolbar{position:absolute;inset:12px 12px auto;z-index:20;display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px 12px;background:#ffffffe6;padding:8px 12px;box-shadow:0 2px 8px #00000014}.toolbar-actions{display:inline-flex;gap:8px;align-items:center}.toolbar-actions button{min-width:32px;height:32px;border-radius:8px;border:1px solid rgba(0,0,0,.12);background:#f7f9fb;font-weight:600;font-size:13px;line-height:1.2;cursor:pointer}.toolbar-selectors{display:inline-flex;gap:10px;align-items:center;justify-content:flex-end}.toolbar-selectors select{height:28px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f9fafb;padding:0 6px;cursor:pointer;font-size:12px;line-height:1.2}@media (max-width: 768px){.schema-toolbar{grid-template-columns:1fr;grid-auto-rows:auto}.toolbar-actions{order:1;justify-content:flex-start}.toolbar-selectors{order:2;justify-content:stretch;flex-wrap:wrap;gap:8px 10px}}@media (max-width: 600px){.toolbar-selectors{flex-direction:column;align-items:stretch;gap:8px}.toolbar-selectors label{display:flex;align-items:center;justify-content:space-between;gap:8px}.toolbar-selectors select{width:100%}}.stage{position:absolute;left:0;top:0;width:12000px;height:6000px;transform-origin:0 0;touch-action:none;-ms-touch-action:none;overscroll-behavior:contain;-webkit-user-select:none;user-select:none}.overlay{position:absolute;inset:0;display:grid;place-items:center;z-index:10;pointer-events:none}.loading-banner,.empty-banner,.error-banner{font-size:13px;line-height:1.2}.loading .loading-banner{width:min(720px,90%);background:#eee;position:relative;overflow:hidden;color:#374151}.loading .msg{position:relative;z-index:1}.loading .shimmer{position:absolute;inset:0;background:linear-gradient(to right,#e0e0e0 8%,#f0f0f0 18%,#e0e0e0 33%);background-size:1000px 100%;animation:shimmer 3s infinite linear;opacity:.9}@keyframes shimmer{0%{background-position:-1000px 0}to{background-position:1000px 0}}.empty .empty-banner{background:#fff;border:1px dashed #cbd5e1;color:#475569}.error .error-banner{background:#fff1f2;border:1px solid #fecdd3;color:#b91c1c}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: SchemaCardComponent, selector: "schema-card", inputs: ["node", "cardTemplate", "settings", "hasChildren", "showCollapseControls", "isCollapsed"], outputs: ["nodeClick", "toggleRequest"] }, { kind: "component", type: SchemaLinksComponent, selector: "schema-links", inputs: ["edges", "settings", "width", "height"], outputs: ["linkClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1612
+ }
1613
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaComponent, decorators: [{
1614
+ type: Component,
1615
+ args: [{ selector: "schema", standalone: true, imports: [CommonModule, NgFor, NgIf, SchemaCardComponent, SchemaLinksComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- URL: projects/schema-ng16/src/lib/components/schema.component.html -->\n<div class=\"schema-root\" #root [style.height.px]=\"viewportHeight()\" [style.minHeight.px]=\"minViewportHeight()\">\n <div class=\"schema-toolbar\" *ngIf=\"showToolbar() && !isLoadingView() && !isErrorView()\">\n <div class=\"toolbar-actions\">\n <button type=\"button\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button type=\"button\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <button type=\"button\" (click)=\"resetView()\" title=\"Centrar\">\u293E</button>\n </div>\n\n <div class=\"toolbar-selectors\">\n <label *ngIf=\"toolbarShowLinkStyle()\">\n Enlaces:\n <select #ls [value]=\"opt_linkStyle()\" (change)=\"setLinkStyle(ls.value)\">\n <option value=\"curve\">Curvo</option>\n <option value=\"orthogonal\">Ortogonal</option>\n <option value=\"line\">Lineal</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutAlign()\">\n Alineaci\u00F3n:\n <select #la [value]=\"opt_layoutAlign()\" (change)=\"setLayoutAlign(la.value)\">\n <option value=\"firstChild\">Superior</option>\n <option value=\"center\">Centrado</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutDirection()\">\n Direcci\u00F3n:\n <select #ld [value]=\"opt_layoutDirection()\" (change)=\"setLayoutDirection(ld.value)\">\n <option value=\"RIGHT\">Derecha</option>\n <option value=\"DOWN\">Abajo</option>\n </select>\n </label>\n </div>\n </div>\n\n <!-- Overlays -->\n <div class=\"overlay loading\" *ngIf=\"isLoadingView()\">\n <div class=\"loading-banner\">\n <div class=\"shimmer\"></div>\n <span class=\"msg\">{{ loadingMessageView() }}</span>\n </div>\n </div>\n\n <div class=\"overlay empty\" *ngIf=\"!isLoadingView() && data == null\">\n <div class=\"empty-banner\">{{ emptyMessageView() }}</div>\n </div>\n\n <div class=\"overlay error\" *ngIf=\"!isLoadingView() && isErrorView()\">\n <div class=\"error-banner\">{{ errorMessageView() }}</div>\n </div>\n\n <!-- Stage -->\n <div\n class=\"stage\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp()\"\n (pointerleave)=\"onPointerLeave()\"\n (dblclick)=\"onDblClick()\"\n [style.transform]=\"transform()\"\n [style.width.px]=\"virtualWidth\"\n [style.height.px]=\"virtualHeight\"\n *ngIf=\"!isLoadingView() && !isErrorView()\"\n >\n <schema-links\n [edges]=\"edges()\"\n [settings]=\"effectiveSettings()\"\n (linkClick)=\"linkClick.emit($event)\"\n [width]=\"virtualWidth\"\n [height]=\"virtualHeight\"\n ></schema-links>\n\n <ng-container *ngFor=\"let n of nodes(); trackBy: trackByNodeId\">\n <schema-card\n [node]=\"n\"\n [settings]=\"effectiveSettings()\"\n [cardTemplate]=\"cardTemplate()\"\n [hasChildren]=\"hasChildren(n.id)\"\n [showCollapseControls]=\"enableCollapse()\"\n [isCollapsed]=\"isNodeCollapsed(n.id)\"\n (toggleRequest)=\"onCardToggle($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n ></schema-card>\n </ng-container>\n </div>\n</div>\n", styles: [".toolbar-actions button{border:0;background:transparent;padding:0;margin:0;font:inherit;color:inherit}.toolbar-selectors select{border:0;background:transparent;appearance:none;font:inherit;color:inherit}.schema-toolbar{border-radius:10px;border:1px solid rgba(0,0,0,.06);box-shadow:0 2px 8px #00000014}.loading-banner,.empty-banner,.error-banner{pointer-events:auto;border-radius:10px;padding:16px 20px;box-shadow:0 6px 20px #00000014;font-weight:600}.schema-root{position:relative;width:100%;overflow:hidden;background:#f7f9fb;border-radius:8px}.schema-toolbar{position:absolute;inset:12px 12px auto;z-index:20;display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px 12px;background:#ffffffe6;padding:8px 12px;box-shadow:0 2px 8px #00000014}.toolbar-actions{display:inline-flex;gap:8px;align-items:center}.toolbar-actions button{min-width:32px;height:32px;border-radius:8px;border:1px solid rgba(0,0,0,.12);background:#f7f9fb;font-weight:600;font-size:13px;line-height:1.2;cursor:pointer}.toolbar-selectors{display:inline-flex;gap:10px;align-items:center;justify-content:flex-end}.toolbar-selectors select{height:28px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f9fafb;padding:0 6px;cursor:pointer;font-size:12px;line-height:1.2}@media (max-width: 768px){.schema-toolbar{grid-template-columns:1fr;grid-auto-rows:auto}.toolbar-actions{order:1;justify-content:flex-start}.toolbar-selectors{order:2;justify-content:stretch;flex-wrap:wrap;gap:8px 10px}}@media (max-width: 600px){.toolbar-selectors{flex-direction:column;align-items:stretch;gap:8px}.toolbar-selectors label{display:flex;align-items:center;justify-content:space-between;gap:8px}.toolbar-selectors select{width:100%}}.stage{position:absolute;left:0;top:0;width:12000px;height:6000px;transform-origin:0 0;touch-action:none;-ms-touch-action:none;overscroll-behavior:contain;-webkit-user-select:none;user-select:none}.overlay{position:absolute;inset:0;display:grid;place-items:center;z-index:10;pointer-events:none}.loading-banner,.empty-banner,.error-banner{font-size:13px;line-height:1.2}.loading .loading-banner{width:min(720px,90%);background:#eee;position:relative;overflow:hidden;color:#374151}.loading .msg{position:relative;z-index:1}.loading .shimmer{position:absolute;inset:0;background:linear-gradient(to right,#e0e0e0 8%,#f0f0f0 18%,#e0e0e0 33%);background-size:1000px 100%;animation:shimmer 3s infinite linear;opacity:.9}@keyframes shimmer{0%{background-position:-1000px 0}to{background-position:1000px 0}}.empty .empty-banner{background:#fff;border:1px dashed #cbd5e1;color:#475569}.error .error-banner{background:#fff1f2;border:1px solid #fecdd3;color:#b91c1c}\n"] }]
1616
+ }], ctorParameters: () => [{ type: JsonAdapterService }, { type: SchemaLayoutService }], propDecorators: { dataInput: [{
1617
+ type: Input,
1618
+ args: ["data"]
1619
+ }], settingsInput: [{
1620
+ type: Input,
1621
+ args: ["settings"]
1622
+ }], cardTemplateInput: [{
1623
+ type: Input,
1624
+ args: ["cardTemplate"]
1625
+ }], isLoadingInput: [{
1626
+ type: Input,
1627
+ args: ["isLoading"]
1628
+ }], isErrorInput: [{
1629
+ type: Input,
1630
+ args: ["isError"]
1631
+ }], emptyMessageInput: [{
1632
+ type: Input,
1633
+ args: ["emptyMessage"]
1634
+ }], loadingMessageInput: [{
1635
+ type: Input,
1636
+ args: ["loadingMessage"]
1637
+ }], errorMessageInput: [{
1638
+ type: Input,
1639
+ args: ["errorMessage"]
1640
+ }], nodeClick: [{
1641
+ type: Output
1642
+ }], linkClick: [{
1643
+ type: Output
1644
+ }], rootRef: [{
1645
+ type: ViewChild,
1646
+ args: ["root", { static: true }]
1647
+ }] } });
1648
+
1649
+ // URL: projects/schema-ng16/src/public-api.ts
1650
+
1651
+ /**
1652
+ * Generated bundle index. Do not edit.
1653
+ */
1654
+
1655
+ export { DEFAULT_SETTINGS, JsonAdapterService, SchemaCardComponent, SchemaComponent, SchemaLayoutService, SchemaLinksComponent };
1656
+ //# sourceMappingURL=json-schema-ng16.mjs.map