@life-cockpit/angular-ui-kit 2.11.0 → 2.12.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.
@@ -140,6 +140,7 @@ const SizeInteractiveLgPadding = "0.625rem 1.25rem";
140
140
  const SizeInteractiveLgFontSize = "1.125rem";
141
141
  const SizeMinTouchWidth = "2.75rem"; // 44px minimum
142
142
  const SizeMinTouchHeight = "2.75rem"; // 44px minimum
143
+ const SizeContentMaxWidth = "1536px"; // Canonical page content cap — matches lc-container size="xxl" and Tailwind's 2xl breakpoint
143
144
 
144
145
  /**
145
146
  * Theme Types
@@ -4805,11 +4806,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
4805
4806
  * Container component for responsive max-width content layout.
4806
4807
  *
4807
4808
  * Features:
4808
- * - Max-width presets (sm, md, lg, xl, full)
4809
+ * - Max-width presets (sm, md, lg, xl, xxl, full)
4809
4810
  * - Optional horizontal padding removal
4810
4811
  * - Optional vertical padding
4811
4812
  * - Centered content alignment
4812
4813
  *
4814
+ * `xxl` caps at the `--lc-content-max-width` token (1536px) — the widest step
4815
+ * before `full`, for data-dense pages that `xl` squeezes but `full` stretches
4816
+ * across ultrawide displays.
4817
+ *
4813
4818
  * @example
4814
4819
  * ```html
4815
4820
  * <lc-container size="lg">Content here</lc-container>
@@ -4826,15 +4831,18 @@ class ContainerComponent {
4826
4831
  const classes = ['mx-auto'];
4827
4832
  // Semantic size class
4828
4833
  classes.push(`container-${this.size()}`);
4829
- // Size/Max-width
4830
- if (this.size() !== 'full') {
4831
- const sizeMap = {
4832
- sm: 'max-w-screen-sm',
4833
- md: 'max-w-screen-md',
4834
- lg: 'max-w-screen-lg',
4835
- xl: 'max-w-screen-xl',
4836
- };
4837
- classes.push(sizeMap[this.size()]);
4834
+ // Size/Max-width — sm…xl map onto Tailwind's screen scale. `xxl` is capped
4835
+ // in SCSS from the --lc-content-max-width token so apps can retune both the
4836
+ // container and their own shell from one place; `full` stays uncapped.
4837
+ const sizeMap = {
4838
+ sm: 'max-w-screen-sm',
4839
+ md: 'max-w-screen-md',
4840
+ lg: 'max-w-screen-lg',
4841
+ xl: 'max-w-screen-xl',
4842
+ };
4843
+ const maxWidthClass = sizeMap[this.size()];
4844
+ if (maxWidthClass) {
4845
+ classes.push(maxWidthClass);
4838
4846
  }
4839
4847
  // Padding
4840
4848
  if (!this.noPadding()) {
@@ -4846,11 +4854,11 @@ class ContainerComponent {
4846
4854
  return classes.join(' ');
4847
4855
  }, ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
4848
4856
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: ContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4849
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.10", type: ContainerComponent, isStandalone: true, selector: "lc-container", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, noPadding: { classPropertyName: "noPadding", publicName: "noPadding", isSignal: true, isRequired: false, transformFunction: null }, paddingY: { classPropertyName: "paddingY", publicName: "paddingY", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<ng-content></ng-content>\n", styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4857
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.10", type: ContainerComponent, isStandalone: true, selector: "lc-container", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, noPadding: { classPropertyName: "noPadding", publicName: "noPadding", isSignal: true, isRequired: false, transformFunction: null }, paddingY: { classPropertyName: "paddingY", publicName: "paddingY", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<ng-content></ng-content>\n", styles: [":host{display:block}:host(.container-xxl){max-width:var(--lc-content-max-width, 1536px)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4850
4858
  }
4851
4859
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: ContainerComponent, decorators: [{
4852
4860
  type: Component,
4853
- args: [{ selector: 'lc-container', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>\n", styles: [":host{display:block}\n"] }]
4861
+ args: [{ selector: 'lc-container', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>\n", styles: [":host{display:block}:host(.container-xxl){max-width:var(--lc-content-max-width, 1536px)}\n"] }]
4854
4862
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], noPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "noPadding", required: false }] }], paddingY: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingY", required: false }] }], hostClasses: [{
4855
4863
  type: HostBinding,
4856
4864
  args: ['class']
@@ -4948,13 +4956,13 @@ class PageHeaderComponent {
4948
4956
  .filter(Boolean)
4949
4957
  .join(' '), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
4950
4958
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: PageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4951
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: PageHeaderComponent, isStandalone: true, selector: "lc-page-header", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconVariant: { classPropertyName: "iconVariant", publicName: "iconVariant", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, showDivider: { classPropertyName: "showDivider", publicName: "showDivider", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, noPaddingX: { classPropertyName: "noPaddingX", publicName: "noPaddingX", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: "@if (true) {\n <div class=\"lc-page-header__breadcrumbs\">\n <ng-content select=\"[slot='breadcrumbs']\" />\n </div>\n\n <div class=\"lc-page-header__row\">\n @if (icon()) {\n <div\n class=\"lc-page-header__icon lc-page-header__icon--{{ iconVariant() }} lc-page-header__icon--size-{{ iconSize() }}\"\n aria-hidden=\"true\"\n >\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize() === 'sm' ? 'xs' : 'sm'\" [decorative]=\"true\" />\n </div>\n }\n <div class=\"lc-page-header__titles\">\n @if (title()) {\n @switch (level()) {\n @case (1) {\n <h1 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h1>\n }\n @case (2) {\n <h2 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h2>\n }\n @case (3) {\n <h3 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h3>\n }\n }\n }\n @if (subtitle()) {\n <p class=\"lc-page-header__subtitle\">{{ subtitle() }}</p>\n }\n </div>\n\n <div class=\"lc-page-header__actions\">\n <ng-content select=\"[slot='actions']\" />\n </div>\n </div>\n\n <div class=\"lc-page-header__meta\">\n <ng-content select=\"[slot='meta']\" />\n </div>\n\n <div class=\"lc-page-header__body\">\n <ng-content />\n </div>\n}\n", styles: [".lc-page-header{display:flex;flex-direction:column;gap:var(--lc-density-gap-md, 1rem);padding-block:var(--lc-density-padding-md, 1.5rem);padding-inline:var(--lc-density-padding-md, 1rem);box-sizing:border-box;width:100%;min-width:0}@media(min-width:640px){.lc-page-header{padding-inline:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){.lc-page-header{padding-inline:var(--lc-density-padding-xl, 2rem)}}.lc-page-header--flush-x{padding-inline:0}.lc-page-header__breadcrumbs,.lc-page-header__body{display:flex;flex-direction:column;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__meta{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__breadcrumbs:empty,.lc-page-header__meta:empty,.lc-page-header__body:empty,.lc-page-header__actions:empty{display:none}.lc-page-header__row{display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:var(--lc-density-gap-md, 1rem);flex-wrap:wrap;min-width:0}.lc-page-header__titles{display:flex;flex-direction:column;gap:var(--lc-density-gap-xs, .25rem);min-width:0;flex:1 1 auto}.lc-page-header__icon{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;border-radius:var(--border-radius-md)}.lc-page-header__icon--size-sm{width:2rem;height:2rem;border-radius:var(--border-radius-sm)}.lc-page-header__icon--brand{background-image:var(--gradient-brand-tile);color:var(--color-on-primary, #fff);box-shadow:var(--shadow-brand-glow)}.lc-page-header__icon--subtle{background-color:#20849724;color:var(--color-primary-500);border:1px solid var(--color-border-strong)}.lc-page-header__title{margin:0;font-family:var(--font-family-sans, inherit);font-weight:var(--font-weight-bold, 700);color:var(--color-text-primary, var(--color-neutral-900, #111));line-height:var(--line-height-tight, 1.2);display:inline-flex;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0;word-break:break-word}.lc-page-header__subtitle{margin:0;font-family:var(--font-family-sans, inherit);color:var(--color-text-secondary, var(--color-neutral-600, #666));line-height:var(--line-height-normal, 1.5)}.lc-page-header__title-suffix{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem)}.lc-page-header__title-suffix:empty{display:none}.lc-page-header__badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:var(--border-radius-full, 999px);background:var(--color-primary-100, #e0f2fe);color:var(--color-primary-700, #0369a1);font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);line-height:1.4;text-transform:uppercase;letter-spacing:.04em}.lc-page-header__actions{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);flex:0 0 auto;justify-content:flex-end}.lc-page-header--size-compact{--lc-density-gap-md: .5rem;--lc-density-gap-sm: .375rem;--lc-density-padding-md: .75rem}.lc-page-header--size-compact .lc-page-header__title{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--size-compact .lc-page-header__subtitle{font-size:var(--font-size-sm, .875rem)}.lc-page-header--size-default .lc-page-header__title{font-size:var(--font-size-2xl, 1.5rem)}.lc-page-header--size-default .lc-page-header__subtitle{font-size:var(--font-size-base, 1rem)}.lc-page-header--size-comfortable{--lc-density-gap-md: 1.5rem;--lc-density-padding-md: 2.5rem}.lc-page-header--size-comfortable .lc-page-header__title{font-size:var(--font-size-3xl, 1.875rem)}.lc-page-header--size-comfortable .lc-page-header__subtitle{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--divided{border-bottom:1px solid var(--color-border, var(--color-neutral-200, #e5e5e5))}\n"], dependencies: [{ kind: "component", type: IconComponent, selector: "lc-icon", inputs: ["name", "variant", "size", "color", "ariaLabel", "decorative", "strict"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4959
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: PageHeaderComponent, isStandalone: true, selector: "lc-page-header", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconVariant: { classPropertyName: "iconVariant", publicName: "iconVariant", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, showDivider: { classPropertyName: "showDivider", publicName: "showDivider", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, noPaddingX: { classPropertyName: "noPaddingX", publicName: "noPaddingX", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: "@if (true) {\n <div class=\"lc-page-header__breadcrumbs\">\n <ng-content select=\"[slot='breadcrumbs']\" />\n </div>\n\n <div class=\"lc-page-header__row\">\n @if (icon()) {\n <div\n class=\"lc-page-header__icon lc-page-header__icon--{{ iconVariant() }} lc-page-header__icon--size-{{ iconSize() }}\"\n aria-hidden=\"true\"\n >\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize() === 'sm' ? 'xs' : 'sm'\" [decorative]=\"true\" />\n </div>\n }\n <div class=\"lc-page-header__titles\">\n <div class=\"lc-page-header__title-row\">\n @if (title()) {\n @switch (level()) {\n @case (1) {\n <h1 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h1>\n }\n @case (2) {\n <h2 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h2>\n }\n @case (3) {\n <h3 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h3>\n }\n }\n }\n\n <div class=\"lc-page-header__actions\">\n <ng-content select=\"[slot='actions']\" />\n </div>\n </div>\n\n @if (subtitle()) {\n <p class=\"lc-page-header__subtitle\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n\n <div class=\"lc-page-header__meta\">\n <ng-content select=\"[slot='meta']\" />\n </div>\n\n <div class=\"lc-page-header__body\">\n <ng-content />\n </div>\n}\n", styles: [".lc-page-header{display:flex;flex-direction:column;gap:var(--lc-density-gap-md, 1rem);padding-block:var(--lc-density-padding-md, 1.5rem);padding-inline:var(--lc-density-padding-md, 1rem);box-sizing:border-box;width:100%;min-width:0}@media(min-width:640px){.lc-page-header{padding-inline:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){.lc-page-header{padding-inline:var(--lc-density-padding-xl, 2rem)}}.lc-page-header--flush-x{padding-inline:0}.lc-page-header__breadcrumbs,.lc-page-header__body{display:flex;flex-direction:column;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__meta{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__breadcrumbs:empty,.lc-page-header__meta:empty,.lc-page-header__body:empty,.lc-page-header__actions:empty{display:none}.lc-page-header__row{display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:var(--lc-density-gap-md, 1rem);flex-wrap:wrap;min-width:0}.lc-page-header__titles{display:flex;flex-direction:column;gap:var(--lc-density-gap-xs, .25rem);min-width:0;flex:1 1 auto}.lc-page-header__title-row{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:var(--lc-density-gap-md, 1rem);min-width:0}.lc-page-header__icon{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;border-radius:var(--border-radius-md)}.lc-page-header__icon--size-sm{width:2rem;height:2rem;border-radius:var(--border-radius-sm)}.lc-page-header__icon--brand{background-image:var(--gradient-brand-tile);color:var(--color-on-primary, #fff);box-shadow:var(--shadow-brand-glow)}.lc-page-header__icon--subtle{background-color:#20849724;color:var(--color-primary-500);border:1px solid var(--color-border-strong)}.lc-page-header__title{margin:0;font-family:var(--font-family-sans, inherit);font-weight:var(--font-weight-bold, 700);color:var(--color-text-primary, var(--color-neutral-900, #111));line-height:var(--line-height-tight, 1.2);display:inline-flex;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0;word-break:break-word}.lc-page-header__subtitle{margin:0;font-family:var(--font-family-sans, inherit);color:var(--color-text-secondary, var(--color-neutral-600, #666));line-height:var(--line-height-normal, 1.5)}.lc-page-header__title-suffix{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem)}.lc-page-header__title-suffix:empty{display:none}.lc-page-header__badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:var(--border-radius-full, 999px);background:var(--color-primary-100, #e0f2fe);color:var(--color-primary-700, #0369a1);font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);line-height:1.4;text-transform:uppercase;letter-spacing:.04em}.lc-page-header__actions{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);flex:0 0 auto;justify-content:flex-end;margin-inline-start:auto}.lc-page-header--size-compact{--lc-density-gap-md: .5rem;--lc-density-gap-sm: .375rem;--lc-density-padding-md: .75rem}.lc-page-header--size-compact .lc-page-header__title{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--size-compact .lc-page-header__subtitle{font-size:var(--font-size-sm, .875rem)}.lc-page-header--size-default .lc-page-header__title{font-size:var(--font-size-2xl, 1.5rem)}.lc-page-header--size-default .lc-page-header__subtitle{font-size:var(--font-size-base, 1rem)}.lc-page-header--size-comfortable{--lc-density-gap-md: 1.5rem;--lc-density-padding-md: 2.5rem}.lc-page-header--size-comfortable .lc-page-header__title{font-size:var(--font-size-3xl, 1.875rem)}.lc-page-header--size-comfortable .lc-page-header__subtitle{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--divided{border-bottom:1px solid var(--color-border, var(--color-neutral-200, #e5e5e5))}\n"], dependencies: [{ kind: "component", type: IconComponent, selector: "lc-icon", inputs: ["name", "variant", "size", "color", "ariaLabel", "decorative", "strict"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4952
4960
  }
4953
4961
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: PageHeaderComponent, decorators: [{
4954
4962
  type: Component,
4955
4963
  args: [{ selector: 'lc-page-header', standalone: true, imports: [IconComponent], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
4956
4964
  '[class]': 'hostClasses()',
4957
- }, template: "@if (true) {\n <div class=\"lc-page-header__breadcrumbs\">\n <ng-content select=\"[slot='breadcrumbs']\" />\n </div>\n\n <div class=\"lc-page-header__row\">\n @if (icon()) {\n <div\n class=\"lc-page-header__icon lc-page-header__icon--{{ iconVariant() }} lc-page-header__icon--size-{{ iconSize() }}\"\n aria-hidden=\"true\"\n >\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize() === 'sm' ? 'xs' : 'sm'\" [decorative]=\"true\" />\n </div>\n }\n <div class=\"lc-page-header__titles\">\n @if (title()) {\n @switch (level()) {\n @case (1) {\n <h1 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h1>\n }\n @case (2) {\n <h2 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h2>\n }\n @case (3) {\n <h3 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h3>\n }\n }\n }\n @if (subtitle()) {\n <p class=\"lc-page-header__subtitle\">{{ subtitle() }}</p>\n }\n </div>\n\n <div class=\"lc-page-header__actions\">\n <ng-content select=\"[slot='actions']\" />\n </div>\n </div>\n\n <div class=\"lc-page-header__meta\">\n <ng-content select=\"[slot='meta']\" />\n </div>\n\n <div class=\"lc-page-header__body\">\n <ng-content />\n </div>\n}\n", styles: [".lc-page-header{display:flex;flex-direction:column;gap:var(--lc-density-gap-md, 1rem);padding-block:var(--lc-density-padding-md, 1.5rem);padding-inline:var(--lc-density-padding-md, 1rem);box-sizing:border-box;width:100%;min-width:0}@media(min-width:640px){.lc-page-header{padding-inline:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){.lc-page-header{padding-inline:var(--lc-density-padding-xl, 2rem)}}.lc-page-header--flush-x{padding-inline:0}.lc-page-header__breadcrumbs,.lc-page-header__body{display:flex;flex-direction:column;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__meta{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__breadcrumbs:empty,.lc-page-header__meta:empty,.lc-page-header__body:empty,.lc-page-header__actions:empty{display:none}.lc-page-header__row{display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:var(--lc-density-gap-md, 1rem);flex-wrap:wrap;min-width:0}.lc-page-header__titles{display:flex;flex-direction:column;gap:var(--lc-density-gap-xs, .25rem);min-width:0;flex:1 1 auto}.lc-page-header__icon{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;border-radius:var(--border-radius-md)}.lc-page-header__icon--size-sm{width:2rem;height:2rem;border-radius:var(--border-radius-sm)}.lc-page-header__icon--brand{background-image:var(--gradient-brand-tile);color:var(--color-on-primary, #fff);box-shadow:var(--shadow-brand-glow)}.lc-page-header__icon--subtle{background-color:#20849724;color:var(--color-primary-500);border:1px solid var(--color-border-strong)}.lc-page-header__title{margin:0;font-family:var(--font-family-sans, inherit);font-weight:var(--font-weight-bold, 700);color:var(--color-text-primary, var(--color-neutral-900, #111));line-height:var(--line-height-tight, 1.2);display:inline-flex;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0;word-break:break-word}.lc-page-header__subtitle{margin:0;font-family:var(--font-family-sans, inherit);color:var(--color-text-secondary, var(--color-neutral-600, #666));line-height:var(--line-height-normal, 1.5)}.lc-page-header__title-suffix{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem)}.lc-page-header__title-suffix:empty{display:none}.lc-page-header__badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:var(--border-radius-full, 999px);background:var(--color-primary-100, #e0f2fe);color:var(--color-primary-700, #0369a1);font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);line-height:1.4;text-transform:uppercase;letter-spacing:.04em}.lc-page-header__actions{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);flex:0 0 auto;justify-content:flex-end}.lc-page-header--size-compact{--lc-density-gap-md: .5rem;--lc-density-gap-sm: .375rem;--lc-density-padding-md: .75rem}.lc-page-header--size-compact .lc-page-header__title{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--size-compact .lc-page-header__subtitle{font-size:var(--font-size-sm, .875rem)}.lc-page-header--size-default .lc-page-header__title{font-size:var(--font-size-2xl, 1.5rem)}.lc-page-header--size-default .lc-page-header__subtitle{font-size:var(--font-size-base, 1rem)}.lc-page-header--size-comfortable{--lc-density-gap-md: 1.5rem;--lc-density-padding-md: 2.5rem}.lc-page-header--size-comfortable .lc-page-header__title{font-size:var(--font-size-3xl, 1.875rem)}.lc-page-header--size-comfortable .lc-page-header__subtitle{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--divided{border-bottom:1px solid var(--color-border, var(--color-neutral-200, #e5e5e5))}\n"] }]
4965
+ }, template: "@if (true) {\n <div class=\"lc-page-header__breadcrumbs\">\n <ng-content select=\"[slot='breadcrumbs']\" />\n </div>\n\n <div class=\"lc-page-header__row\">\n @if (icon()) {\n <div\n class=\"lc-page-header__icon lc-page-header__icon--{{ iconVariant() }} lc-page-header__icon--size-{{ iconSize() }}\"\n aria-hidden=\"true\"\n >\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize() === 'sm' ? 'xs' : 'sm'\" [decorative]=\"true\" />\n </div>\n }\n <div class=\"lc-page-header__titles\">\n <div class=\"lc-page-header__title-row\">\n @if (title()) {\n @switch (level()) {\n @case (1) {\n <h1 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h1>\n }\n @case (2) {\n <h2 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h2>\n }\n @case (3) {\n <h3 class=\"lc-page-header__title\">\n {{ title() }}\n @if (badge()) {\n <span class=\"lc-page-header__badge\">{{ badge() }}</span>\n }\n <span class=\"lc-page-header__title-suffix\">\n <ng-content select=\"[slot='title-suffix']\" />\n </span>\n </h3>\n }\n }\n }\n\n <div class=\"lc-page-header__actions\">\n <ng-content select=\"[slot='actions']\" />\n </div>\n </div>\n\n @if (subtitle()) {\n <p class=\"lc-page-header__subtitle\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n\n <div class=\"lc-page-header__meta\">\n <ng-content select=\"[slot='meta']\" />\n </div>\n\n <div class=\"lc-page-header__body\">\n <ng-content />\n </div>\n}\n", styles: [".lc-page-header{display:flex;flex-direction:column;gap:var(--lc-density-gap-md, 1rem);padding-block:var(--lc-density-padding-md, 1.5rem);padding-inline:var(--lc-density-padding-md, 1rem);box-sizing:border-box;width:100%;min-width:0}@media(min-width:640px){.lc-page-header{padding-inline:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){.lc-page-header{padding-inline:var(--lc-density-padding-xl, 2rem)}}.lc-page-header--flush-x{padding-inline:0}.lc-page-header__breadcrumbs,.lc-page-header__body{display:flex;flex-direction:column;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__meta{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0}.lc-page-header__breadcrumbs:empty,.lc-page-header__meta:empty,.lc-page-header__body:empty,.lc-page-header__actions:empty{display:none}.lc-page-header__row{display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:var(--lc-density-gap-md, 1rem);flex-wrap:wrap;min-width:0}.lc-page-header__titles{display:flex;flex-direction:column;gap:var(--lc-density-gap-xs, .25rem);min-width:0;flex:1 1 auto}.lc-page-header__title-row{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:var(--lc-density-gap-md, 1rem);min-width:0}.lc-page-header__icon{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;border-radius:var(--border-radius-md)}.lc-page-header__icon--size-sm{width:2rem;height:2rem;border-radius:var(--border-radius-sm)}.lc-page-header__icon--brand{background-image:var(--gradient-brand-tile);color:var(--color-on-primary, #fff);box-shadow:var(--shadow-brand-glow)}.lc-page-header__icon--subtle{background-color:#20849724;color:var(--color-primary-500);border:1px solid var(--color-border-strong)}.lc-page-header__title{margin:0;font-family:var(--font-family-sans, inherit);font-weight:var(--font-weight-bold, 700);color:var(--color-text-primary, var(--color-neutral-900, #111));line-height:var(--line-height-tight, 1.2);display:inline-flex;align-items:center;gap:var(--lc-density-gap-sm, .5rem);min-width:0;word-break:break-word}.lc-page-header__subtitle{margin:0;font-family:var(--font-family-sans, inherit);color:var(--color-text-secondary, var(--color-neutral-600, #666));line-height:var(--line-height-normal, 1.5)}.lc-page-header__title-suffix{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem)}.lc-page-header__title-suffix:empty{display:none}.lc-page-header__badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:var(--border-radius-full, 999px);background:var(--color-primary-100, #e0f2fe);color:var(--color-primary-700, #0369a1);font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);line-height:1.4;text-transform:uppercase;letter-spacing:.04em}.lc-page-header__actions{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--lc-density-gap-sm, .5rem);flex:0 0 auto;justify-content:flex-end;margin-inline-start:auto}.lc-page-header--size-compact{--lc-density-gap-md: .5rem;--lc-density-gap-sm: .375rem;--lc-density-padding-md: .75rem}.lc-page-header--size-compact .lc-page-header__title{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--size-compact .lc-page-header__subtitle{font-size:var(--font-size-sm, .875rem)}.lc-page-header--size-default .lc-page-header__title{font-size:var(--font-size-2xl, 1.5rem)}.lc-page-header--size-default .lc-page-header__subtitle{font-size:var(--font-size-base, 1rem)}.lc-page-header--size-comfortable{--lc-density-gap-md: 1.5rem;--lc-density-padding-md: 2.5rem}.lc-page-header--size-comfortable .lc-page-header__title{font-size:var(--font-size-3xl, 1.875rem)}.lc-page-header--size-comfortable .lc-page-header__subtitle{font-size:var(--font-size-lg, 1.125rem)}.lc-page-header--divided{border-bottom:1px solid var(--color-border, var(--color-neutral-200, #e5e5e5))}\n"] }]
4958
4966
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconVariant", required: false }] }], iconSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconSize", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], level: [{ type: i0.Input, args: [{ isSignal: true, alias: "level", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], showDivider: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDivider", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], noPaddingX: [{ type: i0.Input, args: [{ isSignal: true, alias: "noPaddingX", required: false }] }] } });
4959
4967
 
4960
4968
  /**
@@ -5014,6 +5022,19 @@ class PageLayoutComponent {
5014
5022
  scrollBody = input(true, ...(ngDevMode ? [{ debugName: "scrollBody" }] : /* istanbul ignore next */ []));
5015
5023
  /** Add density-aware padding to the body. Off by default so embedded components keep their own padding. */
5016
5024
  padded = input(false, ...(ngDevMode ? [{ debugName: "padded" }] : /* istanbul ignore next */ []));
5025
+ /**
5026
+ * How wide content may grow on very large monitors.
5027
+ * - `'capped'` (default): header, body and footer are capped at
5028
+ * `--lc-content-max-width` and centred, so content doesn't stretch edge to
5029
+ * edge on wide/ultrawide displays. All three regions share the cap, so the
5030
+ * page title stays aligned with the body beneath it.
5031
+ * - `'full'`: no cap — content spans the whole shell. Use for full-bleed
5032
+ * bodies (maps, boards, canvases) that should use every pixel.
5033
+ *
5034
+ * Override the width itself — globally or per subtree — via the
5035
+ * `--lc-content-max-width` custom property.
5036
+ */
5037
+ contentWidth = input('capped', ...(ngDevMode ? [{ debugName: "contentWidth" }] : /* istanbul ignore next */ []));
5017
5038
  get hostClasses() {
5018
5039
  return this.classes();
5019
5040
  }
@@ -5024,15 +5045,17 @@ class PageLayoutComponent {
5024
5045
  classes.push('scroll-body');
5025
5046
  if (this.padded())
5026
5047
  classes.push('padded');
5048
+ if (this.contentWidth() === 'capped')
5049
+ classes.push('content-capped');
5027
5050
  return classes.join(' ');
5028
5051
  }, ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
5029
5052
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: PageLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5030
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.10", type: PageLayoutComponent, isStandalone: true, selector: "lc-page-layout", inputs: { fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, scrollBody: { classPropertyName: "scrollBody", publicName: "scrollBody", isSignal: true, isRequired: false, transformFunction: null }, padded: { classPropertyName: "padded", publicName: "padded", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"lc-page-layout__header\">\n <ng-content select=\"[layout-header]\"></ng-content>\n</div>\n\n<div class=\"lc-page-layout__body\">\n <ng-content></ng-content>\n</div>\n\n<div class=\"lc-page-layout__footer\">\n <ng-content select=\"[layout-footer]\"></ng-content>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;min-height:0;overflow:hidden;box-sizing:border-box}:host(.fill-screen){height:100dvh}:host(.fill-parent){height:100%}.lc-page-layout__header,.lc-page-layout__footer{flex:0 0 auto;min-width:0}.lc-page-layout__body{flex:1 1 auto;min-height:0;min-width:0}:host(.scroll-body) .lc-page-layout__body{overflow:auto}:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-md, 1rem)}@media(min-width:640px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-xl, 2rem)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5053
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.10", type: PageLayoutComponent, isStandalone: true, selector: "lc-page-layout", inputs: { fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, scrollBody: { classPropertyName: "scrollBody", publicName: "scrollBody", isSignal: true, isRequired: false, transformFunction: null }, padded: { classPropertyName: "padded", publicName: "padded", isSignal: true, isRequired: false, transformFunction: null }, contentWidth: { classPropertyName: "contentWidth", publicName: "contentWidth", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"lc-page-layout__header\">\n <ng-content select=\"[layout-header]\"></ng-content>\n</div>\n\n<div class=\"lc-page-layout__body\">\n <ng-content></ng-content>\n</div>\n\n<div class=\"lc-page-layout__footer\">\n <ng-content select=\"[layout-footer]\"></ng-content>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;min-height:0;overflow:hidden;box-sizing:border-box}:host(.fill-screen){height:100dvh}:host(.fill-parent){height:100%}.lc-page-layout__header,.lc-page-layout__footer{flex:0 0 auto;min-width:0}.lc-page-layout__body{flex:1 1 auto;min-height:0;min-width:0}:host(.scroll-body) .lc-page-layout__body{overflow:auto}:host(.content-capped) .lc-page-layout__header,:host(.content-capped) .lc-page-layout__body,:host(.content-capped) .lc-page-layout__footer{max-width:var(--lc-content-max-width, 1536px);margin-inline:auto;width:100%}:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-md, 1rem)}@media(min-width:640px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-xl, 2rem)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5031
5054
  }
5032
5055
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: PageLayoutComponent, decorators: [{
5033
5056
  type: Component,
5034
- args: [{ selector: 'lc-page-layout', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"lc-page-layout__header\">\n <ng-content select=\"[layout-header]\"></ng-content>\n</div>\n\n<div class=\"lc-page-layout__body\">\n <ng-content></ng-content>\n</div>\n\n<div class=\"lc-page-layout__footer\">\n <ng-content select=\"[layout-footer]\"></ng-content>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;min-height:0;overflow:hidden;box-sizing:border-box}:host(.fill-screen){height:100dvh}:host(.fill-parent){height:100%}.lc-page-layout__header,.lc-page-layout__footer{flex:0 0 auto;min-width:0}.lc-page-layout__body{flex:1 1 auto;min-height:0;min-width:0}:host(.scroll-body) .lc-page-layout__body{overflow:auto}:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-md, 1rem)}@media(min-width:640px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-xl, 2rem)}}\n"] }]
5035
- }], propDecorators: { fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], scrollBody: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollBody", required: false }] }], padded: [{ type: i0.Input, args: [{ isSignal: true, alias: "padded", required: false }] }], hostClasses: [{
5057
+ args: [{ selector: 'lc-page-layout', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"lc-page-layout__header\">\n <ng-content select=\"[layout-header]\"></ng-content>\n</div>\n\n<div class=\"lc-page-layout__body\">\n <ng-content></ng-content>\n</div>\n\n<div class=\"lc-page-layout__footer\">\n <ng-content select=\"[layout-footer]\"></ng-content>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;min-height:0;overflow:hidden;box-sizing:border-box}:host(.fill-screen){height:100dvh}:host(.fill-parent){height:100%}.lc-page-layout__header,.lc-page-layout__footer{flex:0 0 auto;min-width:0}.lc-page-layout__body{flex:1 1 auto;min-height:0;min-width:0}:host(.scroll-body) .lc-page-layout__body{overflow:auto}:host(.content-capped) .lc-page-layout__header,:host(.content-capped) .lc-page-layout__body,:host(.content-capped) .lc-page-layout__footer{max-width:var(--lc-content-max-width, 1536px);margin-inline:auto;width:100%}:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-md, 1rem)}@media(min-width:640px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-lg, 1.5rem)}}@media(min-width:1024px){:host(.padded) .lc-page-layout__body{padding:var(--lc-density-padding-xl, 2rem)}}\n"] }]
5058
+ }], propDecorators: { fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], scrollBody: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollBody", required: false }] }], padded: [{ type: i0.Input, args: [{ isSignal: true, alias: "padded", required: false }] }], contentWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentWidth", required: false }] }], hostClasses: [{
5036
5059
  type: HostBinding,
5037
5060
  args: ['class']
5038
5061
  }] } });
@@ -13087,6 +13110,12 @@ const NODE_WIDTH = 160;
13087
13110
  const NODE_HEIGHT = 40;
13088
13111
  const H_GAP = 80;
13089
13112
  const V_GAP = 24;
13113
+ /** How far a bowed edge clears the row/column it loops across. */
13114
+ const BACK_EDGE_BOW = 36;
13115
+ /** Points sampled along a curve when testing it against the node boxes. */
13116
+ const ROUTE_SAMPLES = 24;
13117
+ /** Ignore grazing the very edge of a box — only a real incursion counts. */
13118
+ const ROUTE_INSET = 3;
13090
13119
  const RELATION_STYLES = {
13091
13120
  depends: { color: 'var(--color-neutral-400)', dashed: false, label: 'depends on' },
13092
13121
  blocks: { color: 'var(--color-error-default)', dashed: false, label: 'blocks' },
@@ -13096,8 +13125,26 @@ const RELATION_STYLES = {
13096
13125
  implements: { color: 'var(--color-success-default)', dashed: true, label: 'implements' },
13097
13126
  uses: { color: 'var(--color-neutral-300)', dashed: true, label: 'uses' },
13098
13127
  };
13099
- function measureTree(node, depth, dir) {
13100
- const children = (node.children || []).map(c => measureTree(c, depth + 1, dir));
13128
+ /**
13129
+ * Measures the tree, treating the input as a *graph*: `visited` guards against a
13130
+ * node being laid out twice.
13131
+ *
13132
+ * Two cases route through it, and both are normal in a graph rather than a strict
13133
+ * tree: a cycle (a → b → a), which would otherwise recurse until the stack blows,
13134
+ * and a node reachable from two parents, which would otherwise be drawn twice at
13135
+ * two positions. Either way the node already has a place in the layout, so the
13136
+ * link is recorded as a back edge and later drawn as a cross-reference arrow.
13137
+ */
13138
+ function measureTree(node, depth, dir, visited, backEdges) {
13139
+ visited.add(node.id);
13140
+ const children = [];
13141
+ for (const child of node.children || []) {
13142
+ if (visited.has(child.id)) {
13143
+ backEdges.push({ from: node.id, to: child.id });
13144
+ continue;
13145
+ }
13146
+ children.push(measureTree(child, depth + 1, dir, visited, backEdges));
13147
+ }
13101
13148
  const nodePrimary = dir === 'horizontal' ? NODE_WIDTH : NODE_HEIGHT;
13102
13149
  const nodeSecondary = dir === 'horizontal' ? NODE_HEIGHT : NODE_WIDTH;
13103
13150
  if (children.length === 0) {
@@ -13114,17 +13161,20 @@ function measureTree(node, depth, dir) {
13114
13161
  depth,
13115
13162
  };
13116
13163
  }
13117
- function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes) {
13164
+ function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes, nodeMap) {
13118
13165
  const nodeY = y + measure.secondary / 2 - NODE_HEIGHT / 2;
13119
13166
  const orig = allNodes.get(measure.node.id);
13120
- nodes.push({
13167
+ const laid = {
13121
13168
  id: measure.node.id, label: measure.node.label, description: measure.node.description,
13122
13169
  icon: measure.node.icon, status: measure.node.status || 'default',
13170
+ type: measure.node.type, moreCount: measure.node.moreCount,
13123
13171
  x, y: nodeY, width: NODE_WIDTH, height: NODE_HEIGHT, depth: measure.depth, parentId,
13124
13172
  incomingCount: orig?.dependsOn?.length ?? 0, outgoingCount: orig?.children?.length ?? 0,
13125
- });
13173
+ };
13174
+ nodes.push(laid);
13175
+ nodeMap.set(laid.id, laid);
13126
13176
  if (parentId) {
13127
- const parent = nodes.find(n => n.id === parentId);
13177
+ const parent = nodeMap.get(parentId);
13128
13178
  const sx = parent.x + parent.width;
13129
13179
  const sy = parent.y + parent.height / 2;
13130
13180
  const tx = x;
@@ -13135,25 +13185,29 @@ function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes) {
13135
13185
  path: `M ${sx} ${sy} C ${mx} ${sy}, ${mx} ${ty}, ${tx} ${ty}`,
13136
13186
  labelX: mx, labelY: (sy + ty) / 2,
13137
13187
  color: 'var(--color-neutral-300)', dashed: false, isCrossRef: false,
13188
+ marker: 'depends',
13138
13189
  });
13139
13190
  }
13140
13191
  let childY = y;
13141
13192
  for (const child of measure.children) {
13142
- layoutTreeH(child, x + NODE_WIDTH + H_GAP, childY, measure.node.id, nodes, edges, allNodes);
13193
+ layoutTreeH(child, x + NODE_WIDTH + H_GAP, childY, measure.node.id, nodes, edges, allNodes, nodeMap);
13143
13194
  childY += child.secondary + V_GAP;
13144
13195
  }
13145
13196
  }
13146
- function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes) {
13197
+ function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes, nodeMap) {
13147
13198
  const nodeX = x + measure.secondary / 2 - NODE_WIDTH / 2;
13148
13199
  const orig = allNodes.get(measure.node.id);
13149
- nodes.push({
13200
+ const laid = {
13150
13201
  id: measure.node.id, label: measure.node.label, description: measure.node.description,
13151
13202
  icon: measure.node.icon, status: measure.node.status || 'default',
13203
+ type: measure.node.type, moreCount: measure.node.moreCount,
13152
13204
  x: nodeX, y, width: NODE_WIDTH, height: NODE_HEIGHT, depth: measure.depth, parentId,
13153
13205
  incomingCount: orig?.dependsOn?.length ?? 0, outgoingCount: orig?.children?.length ?? 0,
13154
- });
13206
+ };
13207
+ nodes.push(laid);
13208
+ nodeMap.set(laid.id, laid);
13155
13209
  if (parentId) {
13156
- const parent = nodes.find(n => n.id === parentId);
13210
+ const parent = nodeMap.get(parentId);
13157
13211
  const sx = parent.x + parent.width / 2;
13158
13212
  const sy = parent.y + parent.height;
13159
13213
  const tx = nodeX + NODE_WIDTH / 2;
@@ -13164,24 +13218,158 @@ function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes) {
13164
13218
  path: `M ${sx} ${sy} C ${sx} ${my}, ${tx} ${my}, ${tx} ${ty}`,
13165
13219
  labelX: (sx + tx) / 2, labelY: my,
13166
13220
  color: 'var(--color-neutral-300)', dashed: false, isCrossRef: false,
13221
+ marker: 'depends',
13167
13222
  });
13168
13223
  }
13169
13224
  let childX = x;
13170
13225
  for (const child of measure.children) {
13171
- layoutTreeV(child, childX, y + NODE_HEIGHT + H_GAP, measure.node.id, nodes, edges, allNodes);
13226
+ layoutTreeV(child, childX, y + NODE_HEIGHT + H_GAP, measure.node.id, nodes, edges, allNodes, nodeMap);
13172
13227
  childX += child.secondary + V_GAP;
13173
13228
  }
13174
13229
  }
13230
+ // The `map.has` check doubles as the cycle guard: revisiting a node means the
13231
+ // graph loops back (or converges from a second parent), and there is nothing
13232
+ // left to collect from it.
13175
13233
  function collectAllNodes(node, map) {
13234
+ if (map.has(node.id))
13235
+ return;
13176
13236
  map.set(node.id, node);
13177
13237
  for (const child of node.children || [])
13178
13238
  collectAllNodes(child, map);
13179
13239
  }
13180
- function createCrossRefEdges(nodes, allOriginal, dir) {
13240
+ const cubicPath = (p) => `M ${p[0].x} ${p[0].y} C ${p[1].x} ${p[1].y}, ${p[2].x} ${p[2].y}, ${p[3].x} ${p[3].y}`;
13241
+ function cubicAt(p, t) {
13242
+ const m = 1 - t;
13243
+ return {
13244
+ x: m * m * m * p[0].x + 3 * m * m * t * p[1].x + 3 * m * t * t * p[2].x + t * t * t * p[3].x,
13245
+ y: m * m * m * p[0].y + 3 * m * m * t * p[1].y + 3 * m * t * t * p[2].y + t * t * t * p[3].y,
13246
+ };
13247
+ }
13248
+ /**
13249
+ * Nodes the curve passes through, ignoring its own endpoints — a path necessarily
13250
+ * touches those, and counting them would make every edge look like a crossing.
13251
+ */
13252
+ function crossedNodes(points, nodes, exclude) {
13253
+ const hit = new Map();
13254
+ for (let i = 0; i <= ROUTE_SAMPLES; i++) {
13255
+ const p = cubicAt(points, i / ROUTE_SAMPLES);
13256
+ for (const n of nodes) {
13257
+ if (exclude.has(n.id) || hit.has(n.id))
13258
+ continue;
13259
+ if (p.x > n.x + ROUTE_INSET &&
13260
+ p.x < n.x + n.width - ROUTE_INSET &&
13261
+ p.y > n.y + ROUTE_INSET &&
13262
+ p.y < n.y + n.height - ROUTE_INSET) {
13263
+ hit.set(n.id, n);
13264
+ }
13265
+ }
13266
+ }
13267
+ return [...hit.values()];
13268
+ }
13269
+ /** The straight-ish curve between two nodes, following the layout flow. */
13270
+ function directGeometry(source, target, dir) {
13271
+ if (dir === 'horizontal') {
13272
+ const p0 = { x: source.x + source.width, y: source.y + source.height / 2 };
13273
+ const p3 = { x: target.x, y: target.y + target.height / 2 };
13274
+ const mx = (p0.x + p3.x) / 2;
13275
+ const points = [p0, { x: mx, y: p0.y }, { x: mx, y: p3.y }, p3];
13276
+ // labelY is the curve midpoint, not a baseline offset: the template centres the
13277
+ // text on it via `dominant-baseline`. Nudging it up instead (as this did) pushes
13278
+ // the label out of a 24px gutter and into the node above it.
13279
+ return { path: cubicPath(points), labelX: mx, labelY: (p0.y + p3.y) / 2, points };
13280
+ }
13281
+ const p0 = { x: source.x + source.width / 2, y: source.y + source.height };
13282
+ const p3 = { x: target.x + target.width / 2, y: target.y };
13283
+ const my = (p0.y + p3.y) / 2;
13284
+ const points = [p0, { x: p0.x, y: my }, { x: p3.x, y: my }, p3];
13285
+ return { path: cubicPath(points), labelX: (p0.x + p3.x) / 2, labelY: my, points };
13286
+ }
13287
+ /**
13288
+ * Leaves from one side of both boxes and bows clear of everything in `obstacles`,
13289
+ * so the edge travels through empty space instead of disappearing behind the nodes
13290
+ * between its endpoints.
13291
+ *
13292
+ * The side matters: bowing *below* clears an obstacle sitting beside the endpoints,
13293
+ * but drives straight through one stacked underneath them. Callers try several.
13294
+ */
13295
+ function bowedGeometry(source, target, obstacles, side) {
13296
+ const all = [source, target, ...obstacles];
13297
+ switch (side) {
13298
+ case 'below': {
13299
+ const dip = Math.max(...all.map(n => n.y + n.height)) + BACK_EDGE_BOW;
13300
+ const p0 = { x: source.x + source.width / 2, y: source.y + source.height };
13301
+ const p3 = { x: target.x + target.width / 2, y: target.y + target.height };
13302
+ const points = [p0, { x: p0.x, y: dip }, { x: p3.x, y: dip }, p3];
13303
+ return { path: cubicPath(points), labelX: (p0.x + p3.x) / 2, labelY: dip - 4, points };
13304
+ }
13305
+ case 'above': {
13306
+ const rise = Math.min(...all.map(n => n.y)) - BACK_EDGE_BOW;
13307
+ const p0 = { x: source.x + source.width / 2, y: source.y };
13308
+ const p3 = { x: target.x + target.width / 2, y: target.y };
13309
+ const points = [p0, { x: p0.x, y: rise }, { x: p3.x, y: rise }, p3];
13310
+ return { path: cubicPath(points), labelX: (p0.x + p3.x) / 2, labelY: rise + 12, points };
13311
+ }
13312
+ case 'right': {
13313
+ const bow = Math.max(...all.map(n => n.x + n.width)) + BACK_EDGE_BOW;
13314
+ const p0 = { x: source.x + source.width, y: source.y + source.height / 2 };
13315
+ const p3 = { x: target.x + target.width, y: target.y + target.height / 2 };
13316
+ const points = [p0, { x: bow, y: p0.y }, { x: bow, y: p3.y }, p3];
13317
+ return { path: cubicPath(points), labelX: bow - 4, labelY: (p0.y + p3.y) / 2, points };
13318
+ }
13319
+ case 'left': {
13320
+ const bow = Math.min(...all.map(n => n.x)) - BACK_EDGE_BOW;
13321
+ const p0 = { x: source.x, y: source.y + source.height / 2 };
13322
+ const p3 = { x: target.x, y: target.y + target.height / 2 };
13323
+ const points = [p0, { x: bow, y: p0.y }, { x: bow, y: p3.y }, p3];
13324
+ return { path: cubicPath(points), labelX: bow + 4, labelY: (p0.y + p3.y) / 2, points };
13325
+ }
13326
+ }
13327
+ }
13328
+ // Try the side that suits the layout flow first: in a left-to-right tree the free
13329
+ // space is above and below the rows; in a top-to-bottom one it's left and right.
13330
+ const bowSides = (dir) => dir === 'horizontal' ? ['below', 'above', 'right', 'left'] : ['right', 'left', 'below', 'above'];
13331
+ /**
13332
+ * Cross-references connect arbitrary pairs, so unlike parent→child edges (which
13333
+ * always run down the gutter between two levels) they can cut straight across
13334
+ * unrelated nodes. Pick the cleanest of a handful of candidate routes.
13335
+ *
13336
+ * This is a small fixed search, not obstacle-avoiding routing: it clears the common
13337
+ * cases cheaply, but a dense enough graph can leave every candidate crossing
13338
+ * something. Then the least-bad one wins, and `direct` wins ties — a pointless
13339
+ * detour reads worse than a short hop behind one box.
13340
+ */
13341
+ function routeCrossRef(source, target, dir, nodes, allowDirect = true) {
13342
+ const exclude = new Set([source.id, target.id]);
13343
+ const direct = directGeometry(source, target, dir);
13344
+ const obstacles = crossedNodes(direct.points, nodes, exclude);
13345
+ if (allowDirect && !obstacles.length)
13346
+ return direct;
13347
+ let best = allowDirect ? direct : null;
13348
+ let bestCount = allowDirect ? obstacles.length : Infinity;
13349
+ for (const side of bowSides(dir)) {
13350
+ const candidate = bowedGeometry(source, target, obstacles, side);
13351
+ const count = crossedNodes(candidate.points, nodes, exclude).length;
13352
+ if (count < bestCount) {
13353
+ best = candidate;
13354
+ bestCount = count;
13355
+ if (count === 0)
13356
+ break;
13357
+ }
13358
+ }
13359
+ return best ?? direct;
13360
+ }
13361
+ /**
13362
+ * Falls back to `depends` for anything not in the style table. Callers may pass a
13363
+ * relation that only carries a free-form `relationLabel`, and untyped callers can
13364
+ * pass an unknown string — neither may end up with an undefined style or an
13365
+ * arrow marker id that doesn't exist in the template's `<defs>`.
13366
+ */
13367
+ function resolveRelation(relation) {
13368
+ return relation && RELATION_STYLES[relation] ? relation : 'depends';
13369
+ }
13370
+ function createCrossRefEdges(nodeMap, allOriginal, dir, backEdges) {
13181
13371
  const edges = [];
13182
- const nodeMap = new Map();
13183
- for (const n of nodes)
13184
- nodeMap.set(n.id, n);
13372
+ const nodes = [...nodeMap.values()];
13185
13373
  for (const [, orig] of allOriginal) {
13186
13374
  if (!orig.dependsOn?.length)
13187
13375
  continue;
@@ -13192,47 +13380,87 @@ function createCrossRefEdges(nodes, allOriginal, dir) {
13192
13380
  const source = nodeMap.get(dep.id);
13193
13381
  if (!source)
13194
13382
  continue;
13195
- const relation = dep.relation || 'depends';
13383
+ const relation = resolveRelation(dep.relation);
13196
13384
  const style = RELATION_STYLES[relation];
13197
- let path, labelX, labelY;
13198
- if (dir === 'horizontal') {
13199
- const sx = source.x + source.width;
13200
- const sy = source.y + source.height / 2;
13201
- const tx = target.x;
13202
- const ty = target.y + target.height / 2;
13203
- const mx = (sx + tx) / 2;
13204
- path = `M ${sx} ${sy} C ${mx} ${sy}, ${mx} ${ty}, ${tx} ${ty}`;
13205
- labelX = mx;
13206
- labelY = (sy + ty) / 2 - 8;
13207
- }
13208
- else {
13209
- const sx = source.x + source.width / 2;
13210
- const sy = source.y + source.height;
13211
- const tx = target.x + target.width / 2;
13212
- const ty = target.y;
13213
- const my = (sy + ty) / 2;
13214
- path = `M ${sx} ${sy} C ${sx} ${my}, ${tx} ${my}, ${tx} ${ty}`;
13215
- labelX = (sx + tx) / 2;
13216
- labelY = my - 8;
13217
- }
13385
+ const geo = routeCrossRef(source, target, dir, nodes);
13218
13386
  edges.push({
13219
13387
  id: `${dep.id}⇢${orig.id}`, sourceId: dep.id, targetId: orig.id,
13220
- path, relation, labelX, labelY,
13221
- color: style.color, dashed: style.dashed, isCrossRef: true,
13388
+ path: geo.path, relation: dep.relation,
13389
+ label: dep.relationLabel ?? style.label,
13390
+ labelX: geo.labelX, labelY: geo.labelY,
13391
+ color: style.color, dashed: style.dashed, isCrossRef: true, marker: relation,
13222
13392
  });
13223
13393
  }
13224
13394
  }
13395
+ // Links the tree layout couldn't follow (cycle, or a node reached from a second
13396
+ // parent). The relationship is real, so draw it as a cross-reference rather than
13397
+ // dropping it silently. These always bow: they point against the layout flow by
13398
+ // definition, so the direct route would double back across their own row.
13399
+ for (const back of backEdges) {
13400
+ const source = nodeMap.get(back.from);
13401
+ const target = nodeMap.get(back.to);
13402
+ if (!source || !target)
13403
+ continue;
13404
+ const style = RELATION_STYLES.depends;
13405
+ const geo = routeCrossRef(source, target, dir, nodes, /* allowDirect */ false);
13406
+ edges.push({
13407
+ id: `${back.from}↺${back.to}`, sourceId: back.from, targetId: back.to,
13408
+ path: geo.path, labelX: geo.labelX, labelY: geo.labelY,
13409
+ color: style.color, dashed: true, isCrossRef: true, marker: 'depends',
13410
+ });
13411
+ }
13225
13412
  return edges;
13226
13413
  }
13227
13414
  // ── Status colors ────────────────────────────────────────────────────────────
13415
+ // Labels read from the theme's semantic ink, not from a `--color-<status>-700`.
13416
+ // Those resolve to dark ink in both themes (error-700 is #6b0909), which on the
13417
+ // dark theme's translucent tint over #14222e is dark-on-dark. The status is
13418
+ // carried by the border and tint; the label just has to be readable, and
13419
+ // --color-text-* is the one pair that flips with the theme.
13228
13420
  const STATUS_COLORS = {
13229
- default: { bg: 'var(--color-neutral-50)', border: 'var(--color-neutral-300)', text: 'var(--color-neutral-900)' },
13230
- active: { bg: 'var(--color-primary-50)', border: 'var(--color-primary-400)', text: 'var(--color-primary-700)' },
13231
- success: { bg: 'var(--color-success-50, #f0fdf4)', border: 'var(--color-success-default)', text: 'var(--color-success-700, #15803d)' },
13232
- warning: { bg: 'var(--color-warning-50, #fffbeb)', border: 'var(--color-warning-default)', text: 'var(--color-warning-700, #a16207)' },
13233
- error: { bg: 'var(--color-error-50, #fef2f2)', border: 'var(--color-error-default)', text: 'var(--color-error-700, #b91c1c)' },
13234
- muted: { bg: 'var(--color-neutral-100)', border: 'var(--color-neutral-200)', text: 'var(--color-neutral-400)' },
13421
+ default: { bg: 'var(--color-neutral-50)', border: 'var(--color-neutral-300)', text: 'var(--color-text-primary)' },
13422
+ active: { bg: 'var(--color-primary-50)', border: 'var(--color-primary-400)', text: 'var(--color-text-primary)' },
13423
+ success: { bg: 'var(--color-success-50, #f0fdf4)', border: 'var(--color-success-default)', text: 'var(--color-text-primary)' },
13424
+ warning: { bg: 'var(--color-warning-50, #fffbeb)', border: 'var(--color-warning-default)', text: 'var(--color-text-primary)' },
13425
+ error: { bg: 'var(--color-error-50, #fef2f2)', border: 'var(--color-error-default)', text: 'var(--color-text-primary)' },
13426
+ muted: { bg: 'var(--color-neutral-100)', border: 'var(--color-neutral-200)', text: 'var(--color-text-tertiary)' },
13235
13427
  };
13428
+ /** Parses `#rgb`, `#rrggbb`, `rgb()` and `rgba()`. Anything else → null. */
13429
+ function parseRgb(color) {
13430
+ const hex = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
13431
+ if (hex) {
13432
+ const h = hex[1];
13433
+ const full = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
13434
+ return [0, 2, 4].map(i => parseInt(full.slice(i, i + 2), 16));
13435
+ }
13436
+ const rgb = color.match(/^rgba?\(([^)]+)\)/i);
13437
+ if (rgb) {
13438
+ const p = rgb[1].split(',').map(s => parseFloat(s));
13439
+ if (p.length >= 3 && p.every(n => !Number.isNaN(n)))
13440
+ return [p[0], p[1], p[2]];
13441
+ }
13442
+ return null;
13443
+ }
13444
+ /**
13445
+ * Ink that stays readable on an arbitrary caller-supplied `typeColors` value.
13446
+ *
13447
+ * No theme token can do this job: a type colour is chosen by the consumer, so it
13448
+ * may be light while the theme is dark (or the reverse). `--color-neutral-900`
13449
+ * looks like "dark ink" but the dark theme inverts the neutral scale and resolves
13450
+ * it to #f9fafb — white label on a pale tile. Derive the ink from the colour's own
13451
+ * luminance instead, and fall back to the theme when it can't be parsed.
13452
+ */
13453
+ function readableInk(color) {
13454
+ const rgb = parseRgb(color);
13455
+ if (!rgb)
13456
+ return 'var(--color-text-primary)';
13457
+ const [r, g, b] = rgb.map(c => {
13458
+ const s = c / 255;
13459
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
13460
+ });
13461
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
13462
+ return luminance > 0.4 ? '#111827' : '#f9fafb';
13463
+ }
13236
13464
  /**
13237
13465
  * Dependency viewer component for visualizing hierarchical and cross-cutting relationships.
13238
13466
  *
@@ -13251,10 +13479,34 @@ const STATUS_COLORS = {
13251
13479
  * - SVG arrowhead markers on cross-reference edges
13252
13480
  * - Dark/light theme support
13253
13481
  *
13482
+ * ## Feeding it a graph
13483
+ *
13484
+ * `root` is a *tree*, but the input is treated as a graph: a link back to a node
13485
+ * that already has a place in the layout — a cycle, or a node reached from a
13486
+ * second parent — is drawn as a cross-reference arrow instead of being followed.
13487
+ * Every node is laid out exactly once, and no input can make the layout recurse
13488
+ * forever.
13489
+ *
13490
+ * For incremental exploration, hand in a wider `root` per step and let
13491
+ * `nodeExpand` drive the loading. Pan, zoom, collapse state and selection all
13492
+ * survive a `root` swap, and `anchorNodeId` (defaulting to the selected node)
13493
+ * keeps the viewport pinned while the graph grows around it.
13494
+ *
13254
13495
  * @example
13255
13496
  * ```html
13256
13497
  * <lc-dependency-viewer [root]="specTree" direction="horizontal" />
13257
13498
  * ```
13499
+ *
13500
+ * @example Incremental graph exploration
13501
+ * ```html
13502
+ * <lc-dependency-viewer
13503
+ * [root]="graph()"
13504
+ * [anchorNodeId]="anchor()"
13505
+ * [typeColors]="{ Alpha: '#c1e3e9' }"
13506
+ * (nodeSelect)="showDetails($event)"
13507
+ * (nodeExpand)="loadNeighbours($event)"
13508
+ * />
13509
+ * ```
13258
13510
  */
13259
13511
  class DependencyViewerComponent {
13260
13512
  root = input.required(...(ngDevMode ? [{ debugName: "root" }] : /* istanbul ignore next */ []));
@@ -13263,6 +13515,22 @@ class DependencyViewerComponent {
13263
13515
  showToolbar = input(true, ...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
13264
13516
  showEdgeLabels = input(true, ...(ngDevMode ? [{ debugName: "showEdgeLabels" }] : /* istanbul ignore next */ []));
13265
13517
  edgeWidth = input(1.5, ...(ngDevMode ? [{ debugName: "edgeWidth" }] : /* istanbul ignore next */ []));
13518
+ /**
13519
+ * Node the viewport holds still across `root` updates. Defaults to the node the
13520
+ * user last selected, which keeps click-to-expand steady without any wiring.
13521
+ * Set it explicitly to anchor somewhere else (e.g. a deep-linked node).
13522
+ */
13523
+ anchorNodeId = input(null, ...(ngDevMode ? [{ debugName: "anchorNodeId" }] : /* istanbul ignore next */ []));
13524
+ /** Fill colour per `DependencyNode.type`, e.g. `{ Class: '#8ea475' }`. */
13525
+ typeColors = input({}, ...(ngDevMode ? [{ debugName: "typeColors" }] : /* istanbul ignore next */ []));
13526
+ /** Relations to hide. Cross-reference edges of these types aren't rendered. */
13527
+ hiddenRelations = input([], ...(ngDevMode ? [{ debugName: "hiddenRelations" }] : /* istanbul ignore next */ []));
13528
+ /** Node types to hide, matched against `DependencyNode.type`. */
13529
+ hiddenTypes = input([], ...(ngDevMode ? [{ debugName: "hiddenTypes" }] : /* istanbul ignore next */ []));
13530
+ /** Fires when a node is selected (not on deselect), with the original node incl. `data`. */
13531
+ nodeSelect = output();
13532
+ /** Fires on double-click — the hook for "expand this node's neighbourhood". */
13533
+ nodeExpand = output();
13266
13534
  zoom = signal(100, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
13267
13535
  panX = signal(40, ...(ngDevMode ? [{ debugName: "panX" }] : /* istanbul ignore next */ []));
13268
13536
  panY = signal(40, ...(ngDevMode ? [{ debugName: "panY" }] : /* istanbul ignore next */ []));
@@ -13271,7 +13539,10 @@ class DependencyViewerComponent {
13271
13539
  isPanning = false;
13272
13540
  lastMouseX = 0;
13273
13541
  lastMouseY = 0;
13274
- effectiveRoot = computed(() => this.pruneCollapsed(this.root(), this.collapsedIds()), ...(ngDevMode ? [{ debugName: "effectiveRoot" }] : /* istanbul ignore next */ []));
13542
+ hostEl = inject((ElementRef));
13543
+ /** Last known layout position of the anchor, to compensate pan after a relayout. */
13544
+ anchorPos = null;
13545
+ effectiveRoot = computed(() => this.pruneCollapsed(this.root(), this.collapsedIds(), new Set()), ...(ngDevMode ? [{ debugName: "effectiveRoot" }] : /* istanbul ignore next */ []));
13275
13546
  allOriginalNodes = computed(() => {
13276
13547
  const map = new Map();
13277
13548
  collectAllNodes(this.root(), map);
@@ -13280,24 +13551,73 @@ class DependencyViewerComponent {
13280
13551
  layout = computed(() => {
13281
13552
  const root = this.effectiveRoot();
13282
13553
  const dir = this.direction();
13283
- const measured = measureTree(root, 0, dir);
13554
+ const hiddenTypes = new Set(this.hiddenTypes());
13555
+ const hiddenRelations = new Set(this.hiddenRelations());
13556
+ const backEdges = [];
13557
+ const measured = measureTree(root, 0, dir, new Set(), backEdges);
13284
13558
  const nodes = [];
13559
+ const nodeMap = new Map();
13285
13560
  const treeEdges = [];
13286
13561
  const allNodes = this.allOriginalNodes();
13287
13562
  if (dir === 'horizontal') {
13288
- layoutTreeH(measured, 0, 0, null, nodes, treeEdges, allNodes);
13563
+ layoutTreeH(measured, 0, 0, null, nodes, treeEdges, allNodes, nodeMap);
13289
13564
  }
13290
13565
  else {
13291
- layoutTreeV(measured, 0, 0, null, nodes, treeEdges, allNodes);
13292
- }
13293
- const crossEdges = createCrossRefEdges(nodes, allNodes, dir);
13566
+ layoutTreeV(measured, 0, 0, null, nodes, treeEdges, allNodes, nodeMap);
13567
+ }
13568
+ const crossEdges = createCrossRefEdges(nodeMap, allNodes, dir, backEdges);
13569
+ const edges = [...treeEdges, ...crossEdges].filter(e => !(e.relation && hiddenRelations.has(e.relation)));
13570
+ // Type filtering hides nodes and anything still pointing at them. It runs
13571
+ // after layout so the remaining nodes keep the positions they'd have anyway —
13572
+ // toggling a filter must not reshuffle the whole graph.
13573
+ const visibleNodes = hiddenTypes.size
13574
+ ? nodes.filter(n => !(n.type && hiddenTypes.has(n.type)))
13575
+ : nodes;
13576
+ const visibleIds = new Set(visibleNodes.map(n => n.id));
13577
+ const visibleEdges = hiddenTypes.size
13578
+ ? edges.filter(e => visibleIds.has(e.sourceId) && visibleIds.has(e.targetId))
13579
+ : edges;
13294
13580
  return {
13295
- nodes,
13296
- edges: [...treeEdges, ...crossEdges],
13581
+ nodes: visibleNodes,
13582
+ edges: visibleEdges,
13583
+ nodeMap,
13297
13584
  width: dir === 'horizontal' ? measured.primary : measured.secondary,
13298
13585
  height: dir === 'horizontal' ? measured.secondary : measured.primary,
13299
13586
  };
13300
13587
  }, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
13588
+ constructor() {
13589
+ // Keep the anchor node pinned to its screen position across relayouts. A new
13590
+ // `root` (an expand step) re-runs the tree layout from scratch, which recentres
13591
+ // subtrees and shifts nodes that didn't change — the camera stays put while the
13592
+ // graph slides underneath it. Compensating the pan by the anchor's own delta is
13593
+ // what makes incremental expansion feel stable.
13594
+ effect(() => {
13595
+ const nodeMap = this.layout().nodeMap;
13596
+ const anchorId = this.anchorNodeId() ?? this.selectedNodeId();
13597
+ if (!anchorId) {
13598
+ this.anchorPos = null;
13599
+ return;
13600
+ }
13601
+ const node = nodeMap.get(anchorId);
13602
+ if (!node) {
13603
+ this.anchorPos = null;
13604
+ return;
13605
+ }
13606
+ const prev = this.anchorPos;
13607
+ if (prev && prev.id === anchorId && (prev.x !== node.x || prev.y !== node.y)) {
13608
+ const dx = node.x - prev.x;
13609
+ const dy = node.y - prev.y;
13610
+ // `transform: translate(pan px) scale(z)` with `transform-origin: 0 0`, so
13611
+ // screen = pan + z * layout — a layout delta moves the node by z * delta.
13612
+ untracked(() => {
13613
+ const z = this.zoom() / 100;
13614
+ this.panX.set(this.panX() - dx * z);
13615
+ this.panY.set(this.panY() - dy * z);
13616
+ });
13617
+ }
13618
+ this.anchorPos = { id: anchorId, x: node.x, y: node.y };
13619
+ });
13620
+ }
13301
13621
  svgWidth = computed(() => this.layout().width + 80, ...(ngDevMode ? [{ debugName: "svgWidth" }] : /* istanbul ignore next */ []));
13302
13622
  svgHeight = computed(() => this.layout().height + 80, ...(ngDevMode ? [{ debugName: "svgHeight" }] : /* istanbul ignore next */ []));
13303
13623
  viewBox = computed(() => `0 0 ${this.svgWidth()} ${this.svgHeight()}`, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
@@ -13317,19 +13637,33 @@ class DependencyViewerComponent {
13317
13637
  return [];
13318
13638
  return this.allOriginalNodes().get(id)?.dependsOn ?? [];
13319
13639
  }, ...(ngDevMode ? [{ debugName: "selectedDependsOn" }] : /* istanbul ignore next */ []));
13640
+ // Grouped by label rather than relation, so free-form `relationLabel`s (CALLS,
13641
+ // HAS_COLUMN, …) each get their own entry instead of collapsing into the handful
13642
+ // of built-in relation types they borrow their colour from.
13320
13643
  legendItems = computed(() => {
13321
- const used = new Set();
13644
+ const seen = new Map();
13322
13645
  for (const edge of this.layout().edges) {
13323
- if (edge.relation)
13324
- used.add(edge.relation);
13646
+ if (!edge.isCrossRef || !edge.label || seen.has(edge.label))
13647
+ continue;
13648
+ seen.set(edge.label, { label: edge.label, color: edge.color, dashed: edge.dashed });
13325
13649
  }
13326
- return Array.from(used).map(r => ({
13327
- relation: r, label: RELATION_STYLES[r].label,
13328
- color: RELATION_STYLES[r].color, dashed: RELATION_STYLES[r].dashed,
13329
- }));
13650
+ return Array.from(seen.values());
13330
13651
  }, ...(ngDevMode ? [{ debugName: "legendItems" }] : /* istanbul ignore next */ []));
13652
+ typeLegendItems = computed(() => {
13653
+ const colors = this.typeColors();
13654
+ const seen = new Map();
13655
+ for (const node of this.layout().nodes) {
13656
+ if (!node.type || seen.has(node.type) || !colors[node.type])
13657
+ continue;
13658
+ seen.set(node.type, { type: node.type, color: colors[node.type] });
13659
+ }
13660
+ return Array.from(seen.values());
13661
+ }, ...(ngDevMode ? [{ debugName: "typeLegendItems" }] : /* istanbul ignore next */ []));
13662
+ // O(1) against the collected map. Deliberately keyed off the *original* tree, so
13663
+ // a collapsed node keeps its "+" toggle even though it has no children in the
13664
+ // pruned tree that gets laid out.
13331
13665
  hasChildren(nodeId) {
13332
- return !!(this.findOriginalNode(this.root(), nodeId)?.children?.length);
13666
+ return !!this.allOriginalNodes().get(nodeId)?.children?.length;
13333
13667
  }
13334
13668
  isCollapsed(nodeId) {
13335
13669
  return this.collapsedIds().has(nodeId);
@@ -13337,24 +13671,37 @@ class DependencyViewerComponent {
13337
13671
  getRelationLabel(relation) {
13338
13672
  return RELATION_STYLES[relation]?.label ?? relation;
13339
13673
  }
13340
- pruneCollapsed(node, collapsed) {
13341
- if (collapsed.has(node.id) || !node.children?.length)
13674
+ // `visited` keeps a cyclic graph from recursing forever here too — this runs
13675
+ // before the layout's own guard sees the tree.
13676
+ pruneCollapsed(node, collapsed, visited) {
13677
+ if (visited.has(node.id) || collapsed.has(node.id) || !node.children?.length) {
13342
13678
  return { ...node, children: [] };
13343
- return { ...node, children: node.children.map(c => this.pruneCollapsed(c, collapsed)) };
13344
- }
13345
- findOriginalNode(node, id) {
13346
- if (node.id === id)
13347
- return node;
13348
- for (const child of node.children || []) {
13349
- const found = this.findOriginalNode(child, id);
13350
- if (found)
13351
- return found;
13352
13679
  }
13353
- return null;
13680
+ visited.add(node.id);
13681
+ return {
13682
+ ...node,
13683
+ children: node.children.map(c => this.pruneCollapsed(c, collapsed, visited)),
13684
+ };
13354
13685
  }
13355
13686
  selectNode(id, event) {
13356
13687
  event.stopPropagation();
13357
- this.selectedNodeId.set(this.selectedNodeId() === id ? null : id);
13688
+ const next = this.selectedNodeId() === id ? null : id;
13689
+ this.selectedNodeId.set(next);
13690
+ if (!next)
13691
+ return;
13692
+ const original = this.allOriginalNodes().get(id);
13693
+ if (original)
13694
+ this.nodeSelect.emit(original);
13695
+ }
13696
+ expandNode(id, event) {
13697
+ event.stopPropagation();
13698
+ // A double-click also delivers two clicks, which would toggle the selection
13699
+ // straight back off. Re-assert it so the expanded node stays selected — and
13700
+ // stays the anchor the viewport holds onto.
13701
+ this.selectedNodeId.set(id);
13702
+ const original = this.allOriginalNodes().get(id);
13703
+ if (original)
13704
+ this.nodeExpand.emit(original);
13358
13705
  }
13359
13706
  toggleCollapse(id, event) {
13360
13707
  event.stopPropagation();
@@ -13367,7 +13714,63 @@ class DependencyViewerComponent {
13367
13714
  deselectNode() { this.selectedNodeId.set(null); }
13368
13715
  zoomIn() { this.zoom.update(z => Math.min(z + 25, 300)); }
13369
13716
  zoomOut() { this.zoom.update(z => Math.max(z - 25, 25)); }
13370
- resetZoom() { this.zoom.set(100); this.panX.set(40); this.panY.set(40); }
13717
+ resetZoom() { this.resetView(); }
13718
+ // ── Imperative API ─────────────────────────────────────────────────────────
13719
+ // For deep links (?anchor=<key> → centre on that node) and programmatic
13720
+ // expand/collapse. Mirrors lc-tree-view's expandAll()/collapseAll() surface.
13721
+ /** Centres the viewport on a node. No-op for an id that isn't laid out. */
13722
+ focusNode(id) {
13723
+ const node = this.layout().nodeMap.get(id);
13724
+ if (!node)
13725
+ return;
13726
+ const z = this.zoom() / 100;
13727
+ const el = this.hostEl.nativeElement.querySelector('.dep-viewer__canvas');
13728
+ const viewW = el?.clientWidth ?? 0;
13729
+ const viewH = el?.clientHeight ?? 0;
13730
+ // Invert screen = pan + z * layout for the node's centre.
13731
+ this.panX.set(viewW / 2 - (node.x + node.width / 2) * z);
13732
+ this.panY.set(viewH / 2 - (node.y + node.height / 2) * z);
13733
+ this.anchorPos = { id, x: node.x, y: node.y };
13734
+ }
13735
+ /** Restores the initial zoom and pan. */
13736
+ resetView() {
13737
+ this.zoom.set(100);
13738
+ this.panX.set(40);
13739
+ this.panY.set(40);
13740
+ }
13741
+ /** Expands a collapsed node. */
13742
+ expand(id) {
13743
+ this.collapsedIds.update(set => {
13744
+ if (!set.has(id))
13745
+ return set;
13746
+ const next = new Set(set);
13747
+ next.delete(id);
13748
+ return next;
13749
+ });
13750
+ }
13751
+ /** Collapses a node's sub-tree. */
13752
+ collapse(id) {
13753
+ this.collapsedIds.update(set => {
13754
+ if (set.has(id))
13755
+ return set;
13756
+ const next = new Set(set);
13757
+ next.add(id);
13758
+ return next;
13759
+ });
13760
+ }
13761
+ /** Expands every collapsed node. */
13762
+ expandAll() {
13763
+ this.collapsedIds.set(new Set());
13764
+ }
13765
+ /** Collapses every node that has children. */
13766
+ collapseAll() {
13767
+ const ids = new Set();
13768
+ for (const [id, node] of this.allOriginalNodes()) {
13769
+ if (node.children?.length)
13770
+ ids.add(id);
13771
+ }
13772
+ this.collapsedIds.set(ids);
13773
+ }
13371
13774
  onMouseDown(event) {
13372
13775
  if (event.button !== 0)
13373
13776
  return;
@@ -13388,16 +13791,29 @@ class DependencyViewerComponent {
13388
13791
  event.preventDefault();
13389
13792
  this.zoom.update(z => event.deltaY < 0 ? Math.min(z + 10, 300) : Math.max(z - 10, 25));
13390
13793
  }
13391
- getNodeBg(status) { return STATUS_COLORS[status].bg; }
13392
- getNodeBorder(status) { return STATUS_COLORS[status].border; }
13393
- getNodeText(status) { return STATUS_COLORS[status].text; }
13794
+ // `type` is the category axis (Class/Table/Feature/…), `status` the semantic one
13795
+ // (error/warning/…). A resolved type colour wins for the fill; everything else
13796
+ // falls back to the status palette, so consumers that only use `status` are
13797
+ // unaffected.
13798
+ getNodeBg(node) {
13799
+ const typeColor = node.type ? this.typeColors()[node.type] : undefined;
13800
+ return typeColor ?? STATUS_COLORS[node.status].bg;
13801
+ }
13802
+ getNodeBorder(node) {
13803
+ const typeColor = node.type ? this.typeColors()[node.type] : undefined;
13804
+ return typeColor ?? STATUS_COLORS[node.status].border;
13805
+ }
13806
+ getNodeText(node) {
13807
+ const typeColor = node.type ? this.typeColors()[node.type] : undefined;
13808
+ return typeColor ? readableInk(typeColor) : STATUS_COLORS[node.status].text;
13809
+ }
13394
13810
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13395
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: DependencyViewerComponent, isStandalone: true, selector: "lc-dependency-viewer", inputs: { root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: true, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showEdgeLabels: { classPropertyName: "showEdgeLabels", publicName: "showEdgeLabels", isSignal: true, isRequired: false, transformFunction: null }, edgeWidth: { classPropertyName: "edgeWidth", publicName: "edgeWidth", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()" }, properties: { "class": "\"dep-viewer\"", "style.height": "height()" } }, ngImport: i0, template: "<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoom() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n class=\"dep-viewer__canvas\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + (edge.relation || 'default') + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n @if (showEdgeLabels() && edge.isCrossRef && edge.relation) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ getRelationLabel(edge.relation) }}</text>\n }\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n >\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node.status)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node.status)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node.status)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.relation) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{ getRelationLabel(dep.relation || 'depends') }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;position:relative;overflow:hidden;border:1px solid var(--color-neutral-200);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-neutral-200);background:var(--color-neutral-50);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-neutral-300);border-radius:4px;background:var(--color-surface);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-neutral-100)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;height:100%}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-neutral-200)}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-neutral-600);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-neutral-200);background:var(--color-neutral-50)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface);border:1px solid var(--color-neutral-200);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-neutral-300)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-neutral-100)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-neutral-100);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13811
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: DependencyViewerComponent, isStandalone: true, selector: "lc-dependency-viewer", inputs: { root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: true, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showEdgeLabels: { classPropertyName: "showEdgeLabels", publicName: "showEdgeLabels", isSignal: true, isRequired: false, transformFunction: null }, edgeWidth: { classPropertyName: "edgeWidth", publicName: "edgeWidth", isSignal: true, isRequired: false, transformFunction: null }, anchorNodeId: { classPropertyName: "anchorNodeId", publicName: "anchorNodeId", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, hiddenRelations: { classPropertyName: "hiddenRelations", publicName: "hiddenRelations", isSignal: true, isRequired: false, transformFunction: null }, hiddenTypes: { classPropertyName: "hiddenTypes", publicName: "hiddenTypes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeSelect: "nodeSelect", nodeExpand: "nodeExpand" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()" }, properties: { "class": "\"dep-viewer\"", "style.height": "height()" } }, ngImport: i0, template: "<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoom() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n class=\"dep-viewer__canvas\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges. Their labels are deliberately NOT drawn here \u2014 see below. -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + edge.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n (dblclick)=\"expandNode(node.id, $event)\"\n >\n <!-- Opaque backdrop. The status fills are translucent tints (in the dark\n theme --color-success-50 & co. resolve to rgba(\u2026, 0.18)), so without\n something solid beneath them the edges routed under a node show\n straight through it. This keeps the tint compositing against the\n viewer's own surface instead of against whatever passes behind. -->\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n class=\"dep-viewer__node-backdrop\"\n />\n <rect\n class=\"dep-viewer__node-fill\"\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- \"+N\" marker: this node has more neighbours than are shown -->\n @if (node.moreCount) {\n <g class=\"dep-viewer__more\">\n <rect\n [attr.x]=\"node.x + node.width - 26\"\n [attr.y]=\"node.y - 8\"\n width=\"32\" height=\"16\" rx=\"8\" ry=\"8\"\n />\n <text\n [attr.x]=\"node.x + node.width - 10\"\n [attr.y]=\"node.y + 3\"\n text-anchor=\"middle\"\n >+{{ node.moreCount }}</text>\n </g>\n }\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n\n <!-- Edge labels last: SVG paints in document order, so anything drawn before\n the nodes is covered by them. A label sits at the midpoint of its edge,\n which frequently lands on a node \u2014 it has to come after. The halo (see\n the stylesheet) keeps it legible over whatever it lands on. -->\n @if (showEdgeLabels()) {\n @for (edge of layout().edges; track edge.id) {\n @if (edge.isCrossRef && edge.label) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ edge.label }}</text>\n }\n }\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length || typeLegendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.label) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n @for (item of typeLegendItems(); track item.type) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"10\" height=\"10\">\n <rect width=\"10\" height=\"10\" rx=\"2\" ry=\"2\" [attr.fill]=\"item.color\" />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.type }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n @if (node.type) {\n <span class=\"dep-viewer__detail-type\">{{ node.type }}</span>\n }\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{\n dep.relationLabel || getRelationLabel(dep.relation || 'depends')\n }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;position:relative;overflow:hidden;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface-2);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-border);border-radius:4px;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-surface-hover)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;height:100%}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none;dominant-baseline:central;paint-order:stroke;stroke:var(--color-surface);stroke-width:3px;stroke-linejoin:round}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-backdrop{fill:var(--color-surface)}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-surface-hover)}.dep-viewer__more{pointer-events:none}.dep-viewer__more rect{fill:var(--color-surface-2);stroke:var(--color-border-strong);stroke-width:1}.dep-viewer__more text{font-size:9px;font-weight:600;fill:var(--color-text-secondary);-webkit-user-select:none;user-select:none}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-text-secondary);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-border);background:var(--color-surface-2)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-type{margin-left:auto;padding:1px 6px;border-radius:3px;background:var(--color-surface-hover);color:var(--color-text-secondary);font-size:10px;font-weight:500}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-text-disabled)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-border)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-surface-hover);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13396
13812
  }
13397
13813
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, decorators: [{
13398
13814
  type: Component,
13399
- args: [{ selector: 'lc-dependency-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': '"dep-viewer"', '[style.height]': 'height()' }, template: "<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoom() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n class=\"dep-viewer__canvas\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + (edge.relation || 'default') + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n @if (showEdgeLabels() && edge.isCrossRef && edge.relation) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ getRelationLabel(edge.relation) }}</text>\n }\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n >\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node.status)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node.status)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node.status)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.relation) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{ getRelationLabel(dep.relation || 'depends') }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;position:relative;overflow:hidden;border:1px solid var(--color-neutral-200);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-neutral-200);background:var(--color-neutral-50);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-neutral-300);border-radius:4px;background:var(--color-surface);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-neutral-100)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;height:100%}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-neutral-200)}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-neutral-600);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-neutral-200);background:var(--color-neutral-50)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface);border:1px solid var(--color-neutral-200);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-neutral-300)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-neutral-100)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-neutral-100);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"] }]
13400
- }], propDecorators: { root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: true }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showEdgeLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdgeLabels", required: false }] }], edgeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "edgeWidth", required: false }] }], onMouseMove: [{
13815
+ args: [{ selector: 'lc-dependency-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': '"dep-viewer"', '[style.height]': 'height()' }, template: "<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoom() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n class=\"dep-viewer__canvas\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges. Their labels are deliberately NOT drawn here \u2014 see below. -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + edge.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n (dblclick)=\"expandNode(node.id, $event)\"\n >\n <!-- Opaque backdrop. The status fills are translucent tints (in the dark\n theme --color-success-50 & co. resolve to rgba(\u2026, 0.18)), so without\n something solid beneath them the edges routed under a node show\n straight through it. This keeps the tint compositing against the\n viewer's own surface instead of against whatever passes behind. -->\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n class=\"dep-viewer__node-backdrop\"\n />\n <rect\n class=\"dep-viewer__node-fill\"\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- \"+N\" marker: this node has more neighbours than are shown -->\n @if (node.moreCount) {\n <g class=\"dep-viewer__more\">\n <rect\n [attr.x]=\"node.x + node.width - 26\"\n [attr.y]=\"node.y - 8\"\n width=\"32\" height=\"16\" rx=\"8\" ry=\"8\"\n />\n <text\n [attr.x]=\"node.x + node.width - 10\"\n [attr.y]=\"node.y + 3\"\n text-anchor=\"middle\"\n >+{{ node.moreCount }}</text>\n </g>\n }\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n\n <!-- Edge labels last: SVG paints in document order, so anything drawn before\n the nodes is covered by them. A label sits at the midpoint of its edge,\n which frequently lands on a node \u2014 it has to come after. The halo (see\n the stylesheet) keeps it legible over whatever it lands on. -->\n @if (showEdgeLabels()) {\n @for (edge of layout().edges; track edge.id) {\n @if (edge.isCrossRef && edge.label) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ edge.label }}</text>\n }\n }\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length || typeLegendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.label) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n @for (item of typeLegendItems(); track item.type) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"10\" height=\"10\">\n <rect width=\"10\" height=\"10\" rx=\"2\" ry=\"2\" [attr.fill]=\"item.color\" />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.type }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n @if (node.type) {\n <span class=\"dep-viewer__detail-type\">{{ node.type }}</span>\n }\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{\n dep.relationLabel || getRelationLabel(dep.relation || 'depends')\n }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;position:relative;overflow:hidden;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface-2);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-border);border-radius:4px;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-surface-hover)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;height:100%}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none;dominant-baseline:central;paint-order:stroke;stroke:var(--color-surface);stroke-width:3px;stroke-linejoin:round}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-backdrop{fill:var(--color-surface)}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-surface-hover)}.dep-viewer__more{pointer-events:none}.dep-viewer__more rect{fill:var(--color-surface-2);stroke:var(--color-border-strong);stroke-width:1}.dep-viewer__more text{font-size:9px;font-weight:600;fill:var(--color-text-secondary);-webkit-user-select:none;user-select:none}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-text-secondary);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-border);background:var(--color-surface-2)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-type{margin-left:auto;padding:1px 6px;border-radius:3px;background:var(--color-surface-hover);color:var(--color-text-secondary);font-size:10px;font-weight:500}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-text-disabled)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-border)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-surface-hover);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"] }]
13816
+ }], ctorParameters: () => [], propDecorators: { root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: true }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showEdgeLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdgeLabels", required: false }] }], edgeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "edgeWidth", required: false }] }], anchorNodeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorNodeId", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], hiddenRelations: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenRelations", required: false }] }], hiddenTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenTypes", required: false }] }], nodeSelect: [{ type: i0.Output, args: ["nodeSelect"] }], nodeExpand: [{ type: i0.Output, args: ["nodeExpand"] }], onMouseMove: [{
13401
13817
  type: HostListener,
13402
13818
  args: ['document:mousemove', ['$event']]
13403
13819
  }], onMouseUp: [{
@@ -16006,5 +16422,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
16006
16422
  * Generated bundle index. Do not edit.
16007
16423
  */
16008
16424
 
16009
- export { AccordionComponent, AccordionContentDirective, AccordionGroupComponent, AccordionHeaderDirective, AlertComponent, AnimationDurationFast, AnimationEasingEaseIn, AnimationEasingEaseInOut, AnimationEasingEaseOut, AreaChartComponent, AvatarComponent, AvatarGroupComponent, BadgeComponent, BarChartComponent, BorderRadius2xl, BorderRadiusFull, BorderRadiusLg, BorderRadiusMd, BorderRadiusNone, BorderRadiusSm, BorderRadiusXl, BreadcrumbsComponent, ButtonComponent, CalendarComponent, CalloutComponent, CardComponent, ChatComponent, CheckboxComponent, ChipComponent, CodeBlockComponent, ColorAccentOrange, ColorAccentPurple, ColorAccentRed, ColorAccentRust, ColorAccentViolet, ColorBackgroundDark, ColorErrorDark, ColorErrorDefault, ColorErrorLight, ColorInfoDark, ColorInfoDefault, ColorInfoLight, ColorNeutral100, ColorNeutral200, ColorNeutral300, ColorNeutral400, ColorNeutral50, ColorNeutral500, ColorNeutral600, ColorNeutral700, ColorNeutral800, ColorNeutral900, ColorPickerComponent, ColorPrimary100, ColorPrimary200, ColorPrimary300, ColorPrimary400, ColorPrimary50, ColorPrimary500, ColorPrimary600, ColorPrimary700, ColorPrimary800, ColorPrimary900, ColorSecondary100, ColorSecondary200, ColorSecondary300, ColorSecondary400, ColorSecondary50, ColorSecondary500, ColorSecondary600, ColorSecondary700, ColorSecondary800, ColorSecondary900, ColorSidebarDark, ColorSuccessDark, ColorSuccessDefault, ColorSuccessLight, ColorSurfaceDarkBase, ColorSurfaceDarkRaised, ColorSurfaceDarkSunken, ColorTextDarkPrimary, ColorTextDarkSecondary, ColorTextDarkTertiary, ColorWarningDark, ColorWarningDefault, ColorWarningLight, ComboboxComponent, ConfirmDialogComponent, ConfirmService, ContainerComponent, DateRangePickerComponent, DatepickerComponent, DependencyViewerComponent, DescriptionListComponent, DiffViewerComponent, DividerComponent, DocumentViewerComponent, DonutChartComponent, DrawerComponent, Elevation1, Elevation2, Elevation3, Elevation4, EmailInputComponent, EmptyStateComponent, ErrorDisplayComponent, FILE_FALLBACK_ICON, FOLDER_ICON, FOLDER_OPEN_ICON, FieldGroupComponent, FileUploadComponent, FilterBarComponent, FooterComponent, FunnelChartComponent, GalleryComponent, GanttChartComponent, GaugeComponent, HeaderComponent, HeatmapComponent, HeroComponent, ICON_ALIASES, ICON_NAMES, IconComponent, InputComponent, KanbanBoardComponent, LC_LOGO_BASE_PATH, LineChartComponent, ListComponent, ListItemTemplateDirective, LogViewerComponent, LogoComponent, MarkdownComponent, MenuComponent, ModalComponent, NotificationCenterComponent, NumberInputComponent, PageHeaderComponent, PageLayoutComponent, PaginationComponent, PasswordInputComponent, PieChartComponent, PipelineComponent, PopoverComponent, ProgressBarComponent, ProgressRingComponent, RadarChartComponent, RadioComponent, RatingComponent, RichTextEditorComponent, ScatterPlotComponent, SearchInputComponent, SectionComponent, SelectComponent, SidenavComponent, SizeInteractiveLgFontSize, SizeInteractiveLgHeight, SizeInteractiveLgPadding, SizeInteractiveMdFontSize, SizeInteractiveMdHeight, SizeInteractiveMdPadding, SizeInteractiveSmFontSize, SizeInteractiveSmHeight, SizeInteractiveSmPadding, SizeInteractiveXsFontSize, SizeInteractiveXsHeight, SizeInteractiveXsPadding, SizeMinTouchHeight, SizeMinTouchWidth, SkeletonComponent, SliderComponent, SpacerComponent, Spacing0, Spacing05, Spacing1, Spacing10, Spacing11, Spacing12, Spacing14, Spacing15, Spacing16, Spacing2, Spacing25, Spacing3, Spacing35, Spacing4, Spacing5, Spacing6, Spacing7, Spacing8, Spacing9, SparklineComponent, SpinnerComponent, StackComponent, StackedBarChartComponent, StageListComponent, StatTrendComponent, StepperComponent, SwitchComponent, TabComponent, TableCellDirective, TableComponent, TabsComponent, TagInputComponent, TextareaComponent, ThemeService, TimelineComponent, ToastComponent, ToastService, ToggleGroupComponent, ToolbarComponent, TooltipContentComponent, TooltipDirective, TreeViewComponent, TypographyComponent, TypographyFontFamilyBase, TypographyFontFamilyMono, TypographyFontSize2xl, TypographyFontSize3xl, TypographyFontSize4xl, TypographyFontSize5xl, TypographyFontSize6xl, TypographyFontSizeBase, TypographyFontSizeLg, TypographyFontSizeSm, TypographyFontSizeXl, TypographyFontSizeXs, TypographyFontWeightBold, TypographyFontWeightMedium, TypographyFontWeightNormal, TypographyFontWeightSemibold, TypographyLineHeightNormal, TypographyLineHeightRelaxed, TypographyLineHeightTight, VerificationCodeInputComponent, WaterfallChartComponent, isValidIconName, resolveFileIcon };
16425
+ export { AccordionComponent, AccordionContentDirective, AccordionGroupComponent, AccordionHeaderDirective, AlertComponent, AnimationDurationFast, AnimationEasingEaseIn, AnimationEasingEaseInOut, AnimationEasingEaseOut, AreaChartComponent, AvatarComponent, AvatarGroupComponent, BadgeComponent, BarChartComponent, BorderRadius2xl, BorderRadiusFull, BorderRadiusLg, BorderRadiusMd, BorderRadiusNone, BorderRadiusSm, BorderRadiusXl, BreadcrumbsComponent, ButtonComponent, CalendarComponent, CalloutComponent, CardComponent, ChatComponent, CheckboxComponent, ChipComponent, CodeBlockComponent, ColorAccentOrange, ColorAccentPurple, ColorAccentRed, ColorAccentRust, ColorAccentViolet, ColorBackgroundDark, ColorErrorDark, ColorErrorDefault, ColorErrorLight, ColorInfoDark, ColorInfoDefault, ColorInfoLight, ColorNeutral100, ColorNeutral200, ColorNeutral300, ColorNeutral400, ColorNeutral50, ColorNeutral500, ColorNeutral600, ColorNeutral700, ColorNeutral800, ColorNeutral900, ColorPickerComponent, ColorPrimary100, ColorPrimary200, ColorPrimary300, ColorPrimary400, ColorPrimary50, ColorPrimary500, ColorPrimary600, ColorPrimary700, ColorPrimary800, ColorPrimary900, ColorSecondary100, ColorSecondary200, ColorSecondary300, ColorSecondary400, ColorSecondary50, ColorSecondary500, ColorSecondary600, ColorSecondary700, ColorSecondary800, ColorSecondary900, ColorSidebarDark, ColorSuccessDark, ColorSuccessDefault, ColorSuccessLight, ColorSurfaceDarkBase, ColorSurfaceDarkRaised, ColorSurfaceDarkSunken, ColorTextDarkPrimary, ColorTextDarkSecondary, ColorTextDarkTertiary, ColorWarningDark, ColorWarningDefault, ColorWarningLight, ComboboxComponent, ConfirmDialogComponent, ConfirmService, ContainerComponent, DateRangePickerComponent, DatepickerComponent, DependencyViewerComponent, DescriptionListComponent, DiffViewerComponent, DividerComponent, DocumentViewerComponent, DonutChartComponent, DrawerComponent, Elevation1, Elevation2, Elevation3, Elevation4, EmailInputComponent, EmptyStateComponent, ErrorDisplayComponent, FILE_FALLBACK_ICON, FOLDER_ICON, FOLDER_OPEN_ICON, FieldGroupComponent, FileUploadComponent, FilterBarComponent, FooterComponent, FunnelChartComponent, GalleryComponent, GanttChartComponent, GaugeComponent, HeaderComponent, HeatmapComponent, HeroComponent, ICON_ALIASES, ICON_NAMES, IconComponent, InputComponent, KanbanBoardComponent, LC_LOGO_BASE_PATH, LineChartComponent, ListComponent, ListItemTemplateDirective, LogViewerComponent, LogoComponent, MarkdownComponent, MenuComponent, ModalComponent, NotificationCenterComponent, NumberInputComponent, PageHeaderComponent, PageLayoutComponent, PaginationComponent, PasswordInputComponent, PieChartComponent, PipelineComponent, PopoverComponent, ProgressBarComponent, ProgressRingComponent, RadarChartComponent, RadioComponent, RatingComponent, RichTextEditorComponent, ScatterPlotComponent, SearchInputComponent, SectionComponent, SelectComponent, SidenavComponent, SizeContentMaxWidth, SizeInteractiveLgFontSize, SizeInteractiveLgHeight, SizeInteractiveLgPadding, SizeInteractiveMdFontSize, SizeInteractiveMdHeight, SizeInteractiveMdPadding, SizeInteractiveSmFontSize, SizeInteractiveSmHeight, SizeInteractiveSmPadding, SizeInteractiveXsFontSize, SizeInteractiveXsHeight, SizeInteractiveXsPadding, SizeMinTouchHeight, SizeMinTouchWidth, SkeletonComponent, SliderComponent, SpacerComponent, Spacing0, Spacing05, Spacing1, Spacing10, Spacing11, Spacing12, Spacing14, Spacing15, Spacing16, Spacing2, Spacing25, Spacing3, Spacing35, Spacing4, Spacing5, Spacing6, Spacing7, Spacing8, Spacing9, SparklineComponent, SpinnerComponent, StackComponent, StackedBarChartComponent, StageListComponent, StatTrendComponent, StepperComponent, SwitchComponent, TabComponent, TableCellDirective, TableComponent, TabsComponent, TagInputComponent, TextareaComponent, ThemeService, TimelineComponent, ToastComponent, ToastService, ToggleGroupComponent, ToolbarComponent, TooltipContentComponent, TooltipDirective, TreeViewComponent, TypographyComponent, TypographyFontFamilyBase, TypographyFontFamilyMono, TypographyFontSize2xl, TypographyFontSize3xl, TypographyFontSize4xl, TypographyFontSize5xl, TypographyFontSize6xl, TypographyFontSizeBase, TypographyFontSizeLg, TypographyFontSizeSm, TypographyFontSizeXl, TypographyFontSizeXs, TypographyFontWeightBold, TypographyFontWeightMedium, TypographyFontWeightNormal, TypographyFontWeightSemibold, TypographyLineHeightNormal, TypographyLineHeightRelaxed, TypographyLineHeightTight, VerificationCodeInputComponent, WaterfallChartComponent, isValidIconName, resolveFileIcon };
16010
16426
  //# sourceMappingURL=life-cockpit-angular-ui-kit.mjs.map