@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.
- package/fesm2022/life-cockpit-angular-ui-kit.mjs +516 -100
- package/fesm2022/life-cockpit-angular-ui-kit.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/_theme-dark.scss +5 -0
- package/src/styles/_theme-light.scss +8 -0
- package/src/styles/tokens/_all.scss +1 -0
- package/src/styles/tokens/_size.scss +1 -0
- package/types/life-cockpit-angular-ui-kit.d.ts +118 -11
|
@@ -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
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
13100
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
13383
|
+
const relation = resolveRelation(dep.relation);
|
|
13196
13384
|
const style = RELATION_STYLES[relation];
|
|
13197
|
-
|
|
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
|
|
13221
|
-
|
|
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-
|
|
13230
|
-
active: { bg: 'var(--color-primary-50)', border: 'var(--color-primary-400)', text: 'var(--color-primary
|
|
13231
|
-
success: { bg: 'var(--color-success-50, #f0fdf4)', border: 'var(--color-success-default)', text: 'var(--color-
|
|
13232
|
-
warning: { bg: 'var(--color-warning-50, #fffbeb)', border: 'var(--color-warning-default)', text: 'var(--color-
|
|
13233
|
-
error: { bg: 'var(--color-error-50, #fef2f2)', border: 'var(--color-error-default)', text: 'var(--color-
|
|
13234
|
-
muted: { bg: 'var(--color-neutral-100)', border: 'var(--color-neutral-200)', text: 'var(--color-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
13644
|
+
const seen = new Map();
|
|
13322
13645
|
for (const edge of this.layout().edges) {
|
|
13323
|
-
if (edge.
|
|
13324
|
-
|
|
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(
|
|
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 !!
|
|
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
|
-
|
|
13341
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
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
|