@life-cockpit/angular-ui-kit 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/life-cockpit-angular-ui-kit.mjs +549 -102
- 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 +130 -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
|
}] } });
|
|
@@ -12040,8 +12063,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
|
|
|
12040
12063
|
/**
|
|
12041
12064
|
* Chat component for conversational user interfaces.
|
|
12042
12065
|
*
|
|
12066
|
+
* Compact, document-style layout: agent/system turns flow as full-width text on
|
|
12067
|
+
* a left timeline rail, while user turns render as right-aligned, accent-tinted
|
|
12068
|
+
* bubbles (no name label) with the timestamp on a muted line beneath. Spacing is
|
|
12069
|
+
* a single space-efficient scale (tunable via the `--lc-chat-*` custom
|
|
12070
|
+
* properties); there is no separate roomy variant.
|
|
12071
|
+
*
|
|
12043
12072
|
* Features:
|
|
12044
12073
|
* - User, agent, and system message roles
|
|
12074
|
+
* - Right-aligned user bubbles with an optional avatar: `msg.avatar` image, or a
|
|
12075
|
+
* monogram built from `msg.name` initials (gated by `showAvatars`)
|
|
12076
|
+
* - Agent/system turns on a left rail with a status dot, avatar or semantic icon
|
|
12045
12077
|
* - Streaming cursor indicator for AI responses
|
|
12046
12078
|
* - Typing indicator with animated dots
|
|
12047
12079
|
* - Auto-scroll to latest message
|
|
@@ -12264,6 +12296,31 @@ class ChatComponent {
|
|
|
12264
12296
|
return '';
|
|
12265
12297
|
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
12266
12298
|
}
|
|
12299
|
+
/**
|
|
12300
|
+
* Up to two uppercase initials from a name (first + last word), used for the
|
|
12301
|
+
* user avatar monogram when no `avatar` image is provided. Returns `''` for an
|
|
12302
|
+
* empty/whitespace name so the caller can skip rendering.
|
|
12303
|
+
*/
|
|
12304
|
+
initials(name) {
|
|
12305
|
+
if (!name)
|
|
12306
|
+
return '';
|
|
12307
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
12308
|
+
if (parts.length === 0)
|
|
12309
|
+
return '';
|
|
12310
|
+
const first = parts[0][0] ?? '';
|
|
12311
|
+
const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';
|
|
12312
|
+
return (first + last).toUpperCase();
|
|
12313
|
+
}
|
|
12314
|
+
/**
|
|
12315
|
+
* Whether to show an avatar on a user turn: only when avatars are enabled and
|
|
12316
|
+
* the message carries either an image or a name we can build a monogram from.
|
|
12317
|
+
* Keeps the labelless bubble clean when there's nothing to show.
|
|
12318
|
+
*/
|
|
12319
|
+
showUserAvatar(msg) {
|
|
12320
|
+
return (msg.role === 'user' &&
|
|
12321
|
+
this.showAvatars() &&
|
|
12322
|
+
!!(msg.avatar || this.initials(msg.name)));
|
|
12323
|
+
}
|
|
12267
12324
|
shouldRenderMarkdown(role) {
|
|
12268
12325
|
const mode = this.renderMarkdown();
|
|
12269
12326
|
if (mode === false)
|
|
@@ -12336,11 +12393,11 @@ class ChatComponent {
|
|
|
12336
12393
|
}
|
|
12337
12394
|
}
|
|
12338
12395
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: ChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12339
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: ChatComponent, isStandalone: true, selector: "lc-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, isStreaming: { classPropertyName: "isStreaming", publicName: "isStreaming", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, messageAnchor: { classPropertyName: "messageAnchor", publicName: "messageAnchor", isSignal: true, isRequired: false, transformFunction: null }, contentWidth: { classPropertyName: "contentWidth", publicName: "contentWidth", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showAvatars: { classPropertyName: "showAvatars", publicName: "showAvatars", isSignal: true, isRequired: false, transformFunction: null }, showTimestamps: { classPropertyName: "showTimestamps", publicName: "showTimestamps", isSignal: true, isRequired: false, transformFunction: null }, renderMarkdown: { classPropertyName: "renderMarkdown", publicName: "renderMarkdown", isSignal: true, isRequired: false, transformFunction: null }, allowFileUpload: { classPropertyName: "allowFileUpload", publicName: "allowFileUpload", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messageSend: "messageSend", fileAttach: "fileAttach" }, queries: [{ propertyName: "messageTemplate", first: true, predicate: ["messageTemplate"], descendants: true }], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "composerInput", first: true, predicate: ["composerInput"], descendants: true }], ngImport: i0, template: "<div\n class=\"lc-chat\"\n [class.lc-chat--borderless]=\"!bordered()\"\n [class.lc-chat--width-narrow]=\"contentWidth() === 'narrow'\"\n [class.lc-chat--width-full]=\"contentWidth() === 'full'\"\n>\n @if (showHeader()) {\n <div class=\"lc-chat__header\">\n <span class=\"lc-chat__title\">{{ title() }}</span>\n @if (isStreaming()) {\n <span class=\"lc-chat__streaming-badge\">Streaming\u2026</span>\n }\n </div>\n }\n\n <div\n class=\"lc-chat__messages\"\n [class.lc-chat__messages--anchor-bottom]=\"messageAnchor() === 'bottom'\"\n #scrollContainer\n >\n <div class=\"lc-chat__thread\">\n @for (msg of formattedMessages(); track msg.id) {\n <div\n class=\"lc-chat__message\"\n [class.lc-chat__message--user]=\"msg.role === 'user'\"\n [class.lc-chat__message--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__message--system]=\"msg.role === 'system'\"\n [class.lc-chat__message--streaming]=\"msg.streaming\"\n [attr.role]=\"msg.status === 'error' ? 'alert' : null\"\n [attr.aria-live]=\"statusAriaLive(msg.status)\"\n >\n @if (msg.role !== 'user' && (showAvatars() || isSemanticStatus(msg.status))) {\n <div class=\"lc-chat__marker\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__status-icon\">\n <lc-icon [name]=\"statusIcon(msg.status)\" size=\"xs\" [color]=\"statusColor(msg.status)\" [decorative]=\"true\" />\n </span>\n } @else if (msg.avatar) {\n <img [src]=\"msg.avatar\" [alt]=\"msg.name || msg.role\" class=\"lc-chat__avatar-img\" />\n } @else {\n <span\n class=\"lc-chat__dot\"\n [class.lc-chat__dot--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__dot--system]=\"msg.role === 'system'\"\n ></span>\n }\n </div>\n }\n\n <div class=\"lc-chat__bubble\" [class.lc-chat__bubble--streaming]=\"msg.streaming\">\n @if (msg.role === 'user') {\n <div class=\"lc-chat__meta\">\n <span class=\"lc-chat__name\">{{ msg.name || 'Du' }}</span>\n @if (showTimestamps() && msg.timestamp) {\n <span class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</span>\n }\n </div>\n } @else if (msg.name) {\n <div class=\"lc-chat__name\">{{ msg.name }}</div>\n }\n <div class=\"lc-chat__content\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__sr-only\">{{ statusLabel(msg.status) }} </span>\n }\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate; context: { $implicit: msg }\" />\n } @else if (shouldRenderMarkdown(msg.role)) {\n <lc-markdown [content]=\"msg.content\" variant=\"chat\" />\n } @else {\n {{ msg.content }}\n }\n <span class=\"lc-chat__cursor\" [class.lc-chat__cursor--visible]=\"msg.streaming\"></span>\n </div>\n @if (msg.attachments?.length) {\n <div class=\"lc-chat__attachments\">\n @for (att of msg.attachments; track att.id) {\n @if (isImage(att) && att.url) {\n <a class=\"lc-chat__attachment lc-chat__attachment--image\" [href]=\"att.url\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"att.url\" [alt]=\"att.name\" />\n </a>\n } @else {\n <a\n class=\"lc-chat__attachment lc-chat__attachment--file\"\n [href]=\"att.url || null\"\n [attr.download]=\"att.url ? att.name : null\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span class=\"lc-chat__attachment-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__attachment-size\">{{ formatBytes(att.size) }}</span>\n }\n </a>\n }\n }\n </div>\n }\n @if (showTimestamps() && msg.timestamp && msg.role !== 'user') {\n <div class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n </div>\n }\n\n @if (isStreaming() && !(formattedMessages().length && formattedMessages()[formattedMessages().length - 1].streaming)) {\n <div class=\"lc-chat__message lc-chat__message--agent\">\n @if (showAvatars()) {\n <div class=\"lc-chat__marker\">\n <span class=\"lc-chat__dot lc-chat__dot--agent\"></span>\n </div>\n }\n <div class=\"lc-chat__bubble\">\n <div class=\"lc-chat__typing\">\n <span></span><span></span><span></span>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"lc-chat__input-area\">\n <div class=\"lc-chat__composer\">\n @if (allowFileUpload() && pendingAttachments().length) {\n <div class=\"lc-chat__pending-attachments\">\n @for (att of pendingAttachments(); track att.id) {\n <div class=\"lc-chat__pending-attachment\" [class.lc-chat__pending-attachment--image]=\"isImage(att)\">\n @if (isImage(att) && att.url) {\n <img class=\"lc-chat__pending-thumb\" [src]=\"att.url\" [alt]=\"att.name\" />\n } @else {\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n }\n <span class=\"lc-chat__pending-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__pending-size\">{{ formatBytes(att.size) }}</span>\n }\n <button\n type=\"button\"\n class=\"lc-chat__pending-remove\"\n (click)=\"removeAttachment(att.id)\"\n [attr.aria-label]=\"'Entfernen: ' + att.name\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n }\n\n <textarea\n #composerInput\n class=\"lc-chat__input\"\n [placeholder]=\"placeholder()\"\n [value]=\"inputValue()\"\n [disabled]=\"disabled()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n rows=\"1\"\n ></textarea>\n\n <div class=\"lc-chat__composer-actions\">\n @if (allowFileUpload()) {\n <input\n #fileInput\n type=\"file\"\n class=\"lc-chat__file-input\"\n [accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n (change)=\"onFilesSelected($event)\"\n />\n <button\n type=\"button\"\n class=\"lc-chat__attach-btn\"\n [disabled]=\"disabled()\"\n (click)=\"openFilePicker()\"\n aria-label=\"Datei anh\u00E4ngen\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M14.5 9.5l-5 5a3 3 0 0 1-4.243-4.243l6.364-6.364a2 2 0 0 1 2.829 2.829l-6.364 6.364a1 1 0 1 1-1.415-1.415L11.5 6.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n }\n <button\n class=\"lc-chat__send-btn\"\n [disabled]=\"(!inputValue().trim() && !pendingAttachments().length) || disabled()\"\n (click)=\"send()\"\n aria-label=\"Send\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M3 10l7-7m0 0l7 7m-7-7v14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(90 10 10)\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n</div>\n", styles: [".lc-chat{--lc-chat-content-width: 46rem;display:flex;flex-direction:column;height:100%;min-height:20rem;border:1px solid var(--color-border);border-radius:var(--border-radius-lg);overflow:hidden;background-color:var(--color-surface);color:var(--color-text-primary);font-family:var(--font-family-sans, sans-serif)}.lc-chat--borderless{border:none;border-radius:0}.lc-chat--width-full{--lc-chat-content-width: 100%}.lc-chat__header{display:flex;align-items:center;justify-content:space-between;padding:var(--spacing-3) var(--spacing-4);border-bottom:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-primary)}.lc-chat__streaming-badge{font-size:var(--font-size-xs, .6875rem);padding:.125rem .5rem;background:color-mix(in srgb,var(--color-primary-500) 15%,transparent);color:var(--color-primary-500);border-radius:var(--border-radius-full, 1rem);font-weight:var(--font-weight-medium, 500);animation:pulse-badge 1.5s infinite}@keyframes pulse-badge{0%,to{opacity:1}50%{opacity:.6}}.lc-chat__messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;padding-block:var(--spacing-6, 1.5rem);padding-inline:var(--spacing-4, 1rem)}.lc-chat__thread{position:relative;width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--spacing-5, 1.25rem)}.lc-chat__messages--anchor-bottom .lc-chat__thread>.lc-chat__message:first-child{margin-top:auto}.lc-chat__message{position:relative;display:flex;align-items:stretch;gap:var(--spacing-3, .75rem);align-self:stretch;max-width:100%}.lc-chat__marker{position:relative;z-index:1;flex-shrink:0;width:.875rem;display:flex;flex-direction:column;align-items:center}.lc-chat__marker:before{content:\"\";position:absolute;left:50%;top:.55rem;bottom:calc(-1 * var(--spacing-5, 1.25rem));width:1.5px;transform:translate(-50%);background:var(--color-divider);z-index:-1}.lc-chat__message:last-child .lc-chat__marker:before{display:none}.lc-chat__dot{position:relative;z-index:1;width:.625rem;height:.625rem;margin-top:.25rem;border-radius:50%;background:var(--color-text-tertiary);box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__dot--agent{background:var(--color-primary-500)}.lc-chat__dot--system{background:var(--color-text-tertiary);opacity:.6}.lc-chat__message--streaming .lc-chat__dot--agent{animation:dot-pulse 1.4s ease-out infinite}@keyframes dot-pulse{0%{box-shadow:0 0 0 3px var(--color-surface),0 0 0 3px color-mix(in srgb,var(--color-primary-500) 45%,transparent)}70%,to{box-shadow:0 0 0 3px var(--color-surface),0 0 0 9px color-mix(in srgb,var(--color-primary-500) 0%,transparent)}}.lc-chat__avatar-img{width:1.5rem;height:1.5rem;border-radius:50%;object-fit:cover;box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__status-icon{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;margin-top:.0625rem;border-radius:50%;background:var(--color-surface);box-shadow:0 0 0 2px var(--color-surface)}.lc-chat__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.lc-chat__bubble{flex:1;min-width:0;font-size:var(--font-size-base, .9375rem);line-height:var(--line-height-relaxed, 1.6);word-break:break-word;color:var(--color-text-primary)}.lc-chat__message--user .lc-chat__bubble{position:relative;z-index:1;padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem);border:1px solid var(--color-border);border-radius:var(--border-radius-lg);background:var(--color-surface-2)}.lc-chat__message--agent .lc-chat__bubble{padding:0;background:transparent}.lc-chat__message--system .lc-chat__bubble{padding:0;background:transparent;color:var(--color-text-tertiary);font-size:var(--font-size-sm, .8125rem)}.lc-chat__meta{display:flex;align-items:baseline;justify-content:space-between;gap:var(--spacing-2, .5rem);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__meta .lc-chat__name{margin-bottom:0}.lc-chat__meta .lc-chat__time{margin-top:0}.lc-chat__name{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-secondary);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__content{white-space:pre-wrap}.lc-chat__cursor{display:none}.lc-chat__cursor--visible{display:inline-block;width:2px;height:1em;background:currentColor;margin-left:1px;vertical-align:text-bottom;animation:blink-cursor .8s step-end infinite}@keyframes blink-cursor{0%,to{opacity:1}50%{opacity:0}}.lc-chat__time{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:var(--spacing-1, .25rem);flex-shrink:0}.lc-chat__typing{display:flex;gap:.25rem;padding:.25rem 0}.lc-chat__typing span{width:.375rem;height:.375rem;border-radius:50%;background:var(--color-text-tertiary);animation:typing-bounce 1.4s ease-in-out infinite}.lc-chat__typing span:nth-child(2){animation-delay:.2s}.lc-chat__typing span:nth-child(3){animation-delay:.4s}@keyframes typing-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-.375rem)}}.lc-chat__input-area{padding:var(--spacing-4, .75rem);border-top:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__composer{width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;display:flex;flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--spacing-3, .75rem);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--border-radius-xl);transition:border-color .15s,box-shadow .15s}.lc-chat__composer:focus-within{border-color:var(--color-primary-500);box-shadow:var(--elevation-1)}.lc-chat__input{display:block;width:100%;box-sizing:border-box;resize:none;border:none;background:transparent;padding:var(--spacing-1, .25rem) var(--spacing-2, .5rem);font-size:var(--font-size-base, .875rem);font-family:inherit;line-height:var(--line-height-normal, 1.4);color:var(--color-text-primary);outline:none;height:auto;max-height:12rem;overflow-y:auto}.lc-chat__input:disabled{opacity:.5;cursor:not-allowed}.lc-chat__input::placeholder{color:var(--color-text-tertiary)}.lc-chat__composer-actions{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.lc-chat__send-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;margin-left:auto;border:none;border-radius:var(--border-radius-md);background:var(--color-primary-fill);color:var(--color-on-primary);cursor:pointer;flex-shrink:0;transition:background .15s,opacity .15s}.lc-chat__send-btn:hover:not(:disabled){background:var(--color-primary-fill-hover)}.lc-chat__send-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__file-input{display:none}.lc-chat__attach-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:none;border-radius:var(--border-radius-md);background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0;transition:background .15s,color .15s}.lc-chat__attach-btn:hover:not(:disabled){background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attach-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.375rem}.lc-chat__pending-attachment{display:inline-flex;align-items:center;gap:.375rem;padding:.25rem .375rem .25rem .5rem;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--border-radius-md, .5rem);font-size:var(--font-size-xs, .75rem);color:var(--color-text-primary);max-width:16rem}.lc-chat__pending-thumb{width:1.75rem;height:1.75rem;object-fit:cover;border-radius:.25rem;flex-shrink:0}.lc-chat__pending-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:9rem}.lc-chat__pending-size{color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}.lc-chat__pending-remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:none;border-radius:50%;background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attachments{display:flex;flex-wrap:wrap;gap:.375rem;margin-top:.5rem}.lc-chat__attachment{display:inline-flex;align-items:center;gap:.375rem;text-decoration:none;color:inherit;font-size:var(--font-size-xs, .75rem)}.lc-chat__attachment--file{padding:.25rem .5rem;background:color-mix(in srgb,currentColor 8%,transparent);border-radius:var(--border-radius-md, .375rem);max-width:16rem}.lc-chat__attachment--file:hover{background:color-mix(in srgb,currentColor 16%,transparent)}.lc-chat__attachment--image img{max-width:12rem;max-height:8rem;border-radius:var(--border-radius-md, .375rem);object-fit:cover;display:block}.lc-chat__attachment-icon{flex-shrink:0}.lc-chat__attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-chat__attachment-size{opacity:.7;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MarkdownComponent, selector: "lc-markdown", inputs: ["src", "content", "variant", "linkTarget", "sanitize", "syntaxHighlight", "showHeadingAnchors", "baseUrl", "highlightChanges", "previousContent", "changeHighlightFadeMs", "scrollToFirstChange"], outputs: ["linkClick", "rendered", "changesHighlighted"] }, { kind: "component", type: IconComponent, selector: "lc-icon", inputs: ["name", "variant", "size", "color", "ariaLabel", "decorative", "strict"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12396
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: ChatComponent, isStandalone: true, selector: "lc-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, isStreaming: { classPropertyName: "isStreaming", publicName: "isStreaming", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, messageAnchor: { classPropertyName: "messageAnchor", publicName: "messageAnchor", isSignal: true, isRequired: false, transformFunction: null }, contentWidth: { classPropertyName: "contentWidth", publicName: "contentWidth", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showAvatars: { classPropertyName: "showAvatars", publicName: "showAvatars", isSignal: true, isRequired: false, transformFunction: null }, showTimestamps: { classPropertyName: "showTimestamps", publicName: "showTimestamps", isSignal: true, isRequired: false, transformFunction: null }, renderMarkdown: { classPropertyName: "renderMarkdown", publicName: "renderMarkdown", isSignal: true, isRequired: false, transformFunction: null }, allowFileUpload: { classPropertyName: "allowFileUpload", publicName: "allowFileUpload", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messageSend: "messageSend", fileAttach: "fileAttach" }, queries: [{ propertyName: "messageTemplate", first: true, predicate: ["messageTemplate"], descendants: true }], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "composerInput", first: true, predicate: ["composerInput"], descendants: true }], ngImport: i0, template: "<div\n class=\"lc-chat\"\n [class.lc-chat--borderless]=\"!bordered()\"\n [class.lc-chat--width-narrow]=\"contentWidth() === 'narrow'\"\n [class.lc-chat--width-full]=\"contentWidth() === 'full'\"\n>\n @if (showHeader()) {\n <div class=\"lc-chat__header\">\n <span class=\"lc-chat__title\">{{ title() }}</span>\n @if (isStreaming()) {\n <span class=\"lc-chat__streaming-badge\">Streaming\u2026</span>\n }\n </div>\n }\n\n <div\n class=\"lc-chat__messages\"\n [class.lc-chat__messages--anchor-bottom]=\"messageAnchor() === 'bottom'\"\n #scrollContainer\n >\n <div class=\"lc-chat__thread\">\n @for (msg of formattedMessages(); track msg.id) {\n <div\n class=\"lc-chat__message\"\n [class.lc-chat__message--user]=\"msg.role === 'user'\"\n [class.lc-chat__message--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__message--system]=\"msg.role === 'system'\"\n [class.lc-chat__message--avatar]=\"showUserAvatar(msg)\"\n [class.lc-chat__message--streaming]=\"msg.streaming\"\n [attr.role]=\"msg.status === 'error' ? 'alert' : null\"\n [attr.aria-live]=\"statusAriaLive(msg.status)\"\n >\n @if ((msg.role !== 'user' && (showAvatars() || isSemanticStatus(msg.status))) || showUserAvatar(msg)) {\n <div class=\"lc-chat__marker\" [class.lc-chat__marker--user]=\"msg.role === 'user'\">\n @if (isSemanticStatus(msg.status) && msg.role !== 'user') {\n <span class=\"lc-chat__status-icon\">\n <lc-icon [name]=\"statusIcon(msg.status)\" size=\"xs\" [color]=\"statusColor(msg.status)\" [decorative]=\"true\" />\n </span>\n } @else if (msg.avatar) {\n <img [src]=\"msg.avatar\" [alt]=\"msg.name || msg.role\" class=\"lc-chat__avatar-img\" />\n } @else if (msg.role === 'user') {\n <span class=\"lc-chat__monogram\" [attr.aria-label]=\"msg.name\">{{ initials(msg.name) }}</span>\n } @else {\n <span\n class=\"lc-chat__dot\"\n [class.lc-chat__dot--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__dot--system]=\"msg.role === 'system'\"\n ></span>\n }\n </div>\n }\n\n <div class=\"lc-chat__bubble\" [class.lc-chat__bubble--streaming]=\"msg.streaming\">\n @if (msg.name && msg.role !== 'user') {\n @if (showTimestamps() && msg.timestamp) {\n <div class=\"lc-chat__meta lc-chat__meta--agent\">\n <span class=\"lc-chat__name\">{{ msg.name }}</span>\n <span class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</span>\n </div>\n } @else {\n <div class=\"lc-chat__name\">{{ msg.name }}</div>\n }\n }\n <div class=\"lc-chat__content\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__sr-only\">{{ statusLabel(msg.status) }} </span>\n }\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate; context: { $implicit: msg }\" />\n } @else if (shouldRenderMarkdown(msg.role)) {\n <lc-markdown [content]=\"msg.content\" variant=\"chat\" />\n } @else {\n {{ msg.content }}\n }\n <span class=\"lc-chat__cursor\" [class.lc-chat__cursor--visible]=\"msg.streaming\"></span>\n </div>\n @if (msg.attachments?.length) {\n <div class=\"lc-chat__attachments\">\n @for (att of msg.attachments; track att.id) {\n @if (isImage(att) && att.url) {\n <a class=\"lc-chat__attachment lc-chat__attachment--image\" [href]=\"att.url\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"att.url\" [alt]=\"att.name\" />\n </a>\n } @else {\n <a\n class=\"lc-chat__attachment lc-chat__attachment--file\"\n [href]=\"att.url || null\"\n [attr.download]=\"att.url ? att.name : null\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span class=\"lc-chat__attachment-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__attachment-size\">{{ formatBytes(att.size) }}</span>\n }\n </a>\n }\n }\n </div>\n }\n @if (showTimestamps() && msg.timestamp && msg.role !== 'user' && !msg.name) {\n <div class=\"lc-chat__time lc-chat__time--tight\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n @if (showTimestamps() && msg.timestamp && msg.role === 'user') {\n <div class=\"lc-chat__time lc-chat__time--user\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n }\n\n @if (isStreaming() && !(formattedMessages().length && formattedMessages()[formattedMessages().length - 1].streaming)) {\n <div class=\"lc-chat__message lc-chat__message--agent\">\n @if (showAvatars()) {\n <div class=\"lc-chat__marker\">\n <span class=\"lc-chat__dot lc-chat__dot--agent\"></span>\n </div>\n }\n <div class=\"lc-chat__bubble\">\n <div class=\"lc-chat__typing\">\n <span></span><span></span><span></span>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"lc-chat__input-area\">\n <div class=\"lc-chat__composer\">\n @if (allowFileUpload() && pendingAttachments().length) {\n <div class=\"lc-chat__pending-attachments\">\n @for (att of pendingAttachments(); track att.id) {\n <div class=\"lc-chat__pending-attachment\" [class.lc-chat__pending-attachment--image]=\"isImage(att)\">\n @if (isImage(att) && att.url) {\n <img class=\"lc-chat__pending-thumb\" [src]=\"att.url\" [alt]=\"att.name\" />\n } @else {\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n }\n <span class=\"lc-chat__pending-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__pending-size\">{{ formatBytes(att.size) }}</span>\n }\n <button\n type=\"button\"\n class=\"lc-chat__pending-remove\"\n (click)=\"removeAttachment(att.id)\"\n [attr.aria-label]=\"'Entfernen: ' + att.name\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n }\n\n <textarea\n #composerInput\n class=\"lc-chat__input\"\n [placeholder]=\"placeholder()\"\n [value]=\"inputValue()\"\n [disabled]=\"disabled()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n rows=\"1\"\n ></textarea>\n\n <div class=\"lc-chat__composer-actions\">\n @if (allowFileUpload()) {\n <input\n #fileInput\n type=\"file\"\n class=\"lc-chat__file-input\"\n [accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n (change)=\"onFilesSelected($event)\"\n />\n <button\n type=\"button\"\n class=\"lc-chat__attach-btn\"\n [disabled]=\"disabled()\"\n (click)=\"openFilePicker()\"\n aria-label=\"Datei anh\u00E4ngen\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M14.5 9.5l-5 5a3 3 0 0 1-4.243-4.243l6.364-6.364a2 2 0 0 1 2.829 2.829l-6.364 6.364a1 1 0 1 1-1.415-1.415L11.5 6.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n }\n <button\n class=\"lc-chat__send-btn\"\n [disabled]=\"(!inputValue().trim() && !pendingAttachments().length) || disabled()\"\n (click)=\"send()\"\n aria-label=\"Send\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M3 10l7-7m0 0l7 7m-7-7v14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(90 10 10)\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n</div>\n", styles: [".lc-chat{--lc-chat-content-width: 46rem;--lc-chat-thread-gap: var(--spacing-3, .75rem);--lc-chat-messages-pad-block: var(--spacing-4, 1rem);--lc-chat-messages-pad-inline: var(--spacing-3, .75rem);--lc-chat-bubble-font: var(--font-size-sm, .875rem);--lc-chat-bubble-line: var(--line-height-normal, 1.5);--lc-chat-bubble-pad-block: var(--spacing-2, .5rem);--lc-chat-bubble-pad-inline: var(--spacing-3, .75rem);--lc-chat-header-pad-block: var(--spacing-2, .5rem);--lc-chat-input-area-pad: var(--spacing-3, .75rem);--lc-chat-composer-pad: var(--spacing-2, .5rem);display:flex;flex-direction:column;height:100%;min-height:20rem;border:1px solid var(--color-border);border-radius:var(--border-radius-lg);overflow:hidden;background-color:var(--color-surface);color:var(--color-text-primary);font-family:var(--font-family-sans, sans-serif)}.lc-chat--borderless{border:none;border-radius:0}.lc-chat--width-full{--lc-chat-content-width: 100%}.lc-chat__header{display:flex;align-items:center;justify-content:space-between;padding:var(--lc-chat-header-pad-block) var(--spacing-4);border-bottom:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-primary)}.lc-chat__streaming-badge{font-size:var(--font-size-xs, .6875rem);padding:.125rem .5rem;background:color-mix(in srgb,var(--color-primary-500) 15%,transparent);color:var(--color-primary-500);border-radius:var(--border-radius-full, 1rem);font-weight:var(--font-weight-medium, 500);animation:pulse-badge 1.5s infinite}@keyframes pulse-badge{0%,to{opacity:1}50%{opacity:.6}}.lc-chat__messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;padding-block:var(--lc-chat-messages-pad-block);padding-inline:var(--lc-chat-messages-pad-inline)}.lc-chat__thread{position:relative;width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--lc-chat-thread-gap)}.lc-chat__messages--anchor-bottom .lc-chat__thread>.lc-chat__message:first-child{margin-top:auto}.lc-chat__message{position:relative;display:flex;align-items:stretch;gap:var(--spacing-3, .75rem);align-self:stretch;max-width:100%}.lc-chat__message--user{flex-direction:column;align-items:flex-end;gap:var(--spacing-1, .25rem)}.lc-chat__message--user.lc-chat__message--avatar{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:\"bubble avatar\" \"time avatar\";column-gap:var(--spacing-2, .5rem);align-items:start}.lc-chat__message--user.lc-chat__message--avatar>.lc-chat__bubble{grid-area:bubble;justify-self:end}.lc-chat__message--user.lc-chat__message--avatar>.lc-chat__time--user{grid-area:time;justify-self:end}.lc-chat__message--user.lc-chat__message--avatar>.lc-chat__marker--user{grid-area:avatar}.lc-chat__marker{position:relative;z-index:1;flex-shrink:0;width:.875rem;display:flex;flex-direction:column;align-items:center}.lc-chat__marker:before{content:\"\";position:absolute;left:50%;top:.55rem;bottom:calc(-1 * var(--lc-chat-thread-gap));width:1.5px;transform:translate(-50%);background:var(--color-divider);z-index:-1}.lc-chat__message:last-child .lc-chat__marker:before{display:none}.lc-chat__dot{position:relative;z-index:1;width:.625rem;height:.625rem;margin-top:.25rem;border-radius:50%;background:var(--color-text-tertiary);box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__dot--agent{background:var(--color-primary-500)}.lc-chat__dot--system{background:var(--color-text-tertiary);opacity:.6}.lc-chat__message--streaming .lc-chat__dot--agent{animation:dot-pulse 1.4s ease-out infinite}@keyframes dot-pulse{0%{box-shadow:0 0 0 3px var(--color-surface),0 0 0 3px color-mix(in srgb,var(--color-primary-500) 45%,transparent)}70%,to{box-shadow:0 0 0 3px var(--color-surface),0 0 0 9px color-mix(in srgb,var(--color-primary-500) 0%,transparent)}}.lc-chat__avatar-img{width:1.5rem;height:1.5rem;border-radius:50%;object-fit:cover;box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__marker--user{width:auto}.lc-chat__marker--user:before{display:none}.lc-chat__monogram{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:color-mix(in srgb,var(--color-primary-500) 18%,var(--color-surface-2));color:var(--color-primary-500);font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);line-height:1;box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__status-icon{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;margin-top:.0625rem;border-radius:50%;background:var(--color-surface);box-shadow:0 0 0 2px var(--color-surface)}.lc-chat__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.lc-chat__bubble{flex:1;min-width:0;font-size:var(--lc-chat-bubble-font);line-height:var(--lc-chat-bubble-line);word-break:break-word;color:var(--color-text-primary)}.lc-chat__message--user .lc-chat__bubble{position:relative;z-index:1;flex:0 1 auto;max-width:min(85%,34rem);padding:var(--lc-chat-bubble-pad-block) var(--lc-chat-bubble-pad-inline);border:1px solid color-mix(in srgb,var(--color-primary-500) 22%,transparent);border-radius:var(--border-radius-lg);background:color-mix(in srgb,var(--color-primary-500) 10%,var(--color-surface-2))}.lc-chat__message--agent .lc-chat__bubble{padding:0;background:transparent}.lc-chat__message--system .lc-chat__bubble{padding:0;background:transparent;color:var(--color-text-tertiary);font-size:var(--font-size-sm, .8125rem)}.lc-chat__meta{display:flex;align-items:baseline;justify-content:space-between;gap:var(--spacing-2, .5rem);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__meta .lc-chat__name{margin-bottom:0}.lc-chat__meta .lc-chat__time{margin-top:0}.lc-chat__name{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-secondary);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__content{white-space:pre-wrap}.lc-chat__cursor{display:none}.lc-chat__cursor--visible{display:inline-block;width:2px;height:1em;background:currentColor;margin-left:1px;vertical-align:text-bottom;animation:blink-cursor .8s step-end infinite}@keyframes blink-cursor{0%,to{opacity:1}50%{opacity:0}}.lc-chat__time{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:var(--spacing-1, .25rem);flex-shrink:0}.lc-chat__time--tight{margin-top:0}.lc-chat__time--user{margin-top:0;padding-inline:var(--spacing-1, .25rem)}.lc-chat__typing{display:flex;gap:.25rem;padding:.25rem 0}.lc-chat__typing span{width:.375rem;height:.375rem;border-radius:50%;background:var(--color-text-tertiary);animation:typing-bounce 1.4s ease-in-out infinite}.lc-chat__typing span:nth-child(2){animation-delay:.2s}.lc-chat__typing span:nth-child(3){animation-delay:.4s}@keyframes typing-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-.375rem)}}.lc-chat__input-area{padding:var(--lc-chat-input-area-pad);border-top:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__composer{width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;display:flex;flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--lc-chat-composer-pad);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--border-radius-xl);transition:border-color .15s,box-shadow .15s}.lc-chat__composer:focus-within{border-color:var(--color-primary-500);box-shadow:var(--elevation-1)}.lc-chat__input{display:block;width:100%;box-sizing:border-box;resize:none;border:none;background:transparent;padding:var(--spacing-1, .25rem) var(--spacing-2, .5rem);font-size:var(--font-size-base, .875rem);font-family:inherit;line-height:var(--line-height-normal, 1.4);color:var(--color-text-primary);outline:none;height:auto;max-height:12rem;overflow-y:auto}.lc-chat__input:disabled{opacity:.5;cursor:not-allowed}.lc-chat__input::placeholder{color:var(--color-text-tertiary)}.lc-chat__composer-actions{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.lc-chat__send-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;margin-left:auto;border:none;border-radius:var(--border-radius-md);background:var(--color-primary-fill);color:var(--color-on-primary);cursor:pointer;flex-shrink:0;transition:background .15s,opacity .15s}.lc-chat__send-btn:hover:not(:disabled){background:var(--color-primary-fill-hover)}.lc-chat__send-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__file-input{display:none}.lc-chat__attach-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:none;border-radius:var(--border-radius-md);background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0;transition:background .15s,color .15s}.lc-chat__attach-btn:hover:not(:disabled){background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attach-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.375rem}.lc-chat__pending-attachment{display:inline-flex;align-items:center;gap:.375rem;padding:.25rem .375rem .25rem .5rem;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--border-radius-md, .5rem);font-size:var(--font-size-xs, .75rem);color:var(--color-text-primary);max-width:16rem}.lc-chat__pending-thumb{width:1.75rem;height:1.75rem;object-fit:cover;border-radius:.25rem;flex-shrink:0}.lc-chat__pending-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:9rem}.lc-chat__pending-size{color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}.lc-chat__pending-remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:none;border-radius:50%;background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attachments{display:flex;flex-wrap:wrap;gap:.375rem;margin-top:.5rem}.lc-chat__attachment{display:inline-flex;align-items:center;gap:.375rem;text-decoration:none;color:inherit;font-size:var(--font-size-xs, .75rem)}.lc-chat__attachment--file{padding:.25rem .5rem;background:color-mix(in srgb,currentColor 8%,transparent);border-radius:var(--border-radius-md, .375rem);max-width:16rem}.lc-chat__attachment--file:hover{background:color-mix(in srgb,currentColor 16%,transparent)}.lc-chat__attachment--image img{max-width:12rem;max-height:8rem;border-radius:var(--border-radius-md, .375rem);object-fit:cover;display:block}.lc-chat__attachment-icon{flex-shrink:0}.lc-chat__attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-chat__attachment-size{opacity:.7;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MarkdownComponent, selector: "lc-markdown", inputs: ["src", "content", "variant", "linkTarget", "sanitize", "syntaxHighlight", "showHeadingAnchors", "baseUrl", "highlightChanges", "previousContent", "changeHighlightFadeMs", "scrollToFirstChange"], outputs: ["linkClick", "rendered", "changesHighlighted"] }, { kind: "component", type: IconComponent, selector: "lc-icon", inputs: ["name", "variant", "size", "color", "ariaLabel", "decorative", "strict"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12340
12397
|
}
|
|
12341
12398
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: ChatComponent, decorators: [{
|
|
12342
12399
|
type: Component,
|
|
12343
|
-
args: [{ selector: 'lc-chat', standalone: true, imports: [NgTemplateOutlet, MarkdownComponent, IconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"lc-chat\"\n [class.lc-chat--borderless]=\"!bordered()\"\n [class.lc-chat--width-narrow]=\"contentWidth() === 'narrow'\"\n [class.lc-chat--width-full]=\"contentWidth() === 'full'\"\n>\n @if (showHeader()) {\n <div class=\"lc-chat__header\">\n <span class=\"lc-chat__title\">{{ title() }}</span>\n @if (isStreaming()) {\n <span class=\"lc-chat__streaming-badge\">Streaming\u2026</span>\n }\n </div>\n }\n\n <div\n class=\"lc-chat__messages\"\n [class.lc-chat__messages--anchor-bottom]=\"messageAnchor() === 'bottom'\"\n #scrollContainer\n >\n <div class=\"lc-chat__thread\">\n @for (msg of formattedMessages(); track msg.id) {\n <div\n class=\"lc-chat__message\"\n [class.lc-chat__message--user]=\"msg.role === 'user'\"\n [class.lc-chat__message--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__message--system]=\"msg.role === 'system'\"\n [class.lc-chat__message--streaming]=\"msg.streaming\"\n [attr.role]=\"msg.status === 'error' ? 'alert' : null\"\n [attr.aria-live]=\"statusAriaLive(msg.status)\"\n >\n @if (msg.role !== 'user' && (showAvatars() || isSemanticStatus(msg.status))) {\n <div class=\"lc-chat__marker\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__status-icon\">\n <lc-icon [name]=\"statusIcon(msg.status)\" size=\"xs\" [color]=\"statusColor(msg.status)\" [decorative]=\"true\" />\n </span>\n } @else if (msg.avatar) {\n <img [src]=\"msg.avatar\" [alt]=\"msg.name || msg.role\" class=\"lc-chat__avatar-img\" />\n } @else {\n <span\n class=\"lc-chat__dot\"\n [class.lc-chat__dot--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__dot--system]=\"msg.role === 'system'\"\n ></span>\n }\n </div>\n }\n\n <div class=\"lc-chat__bubble\" [class.lc-chat__bubble--streaming]=\"msg.streaming\">\n @if (msg.role === 'user') {\n <div class=\"lc-chat__meta\">\n <span class=\"lc-chat__name\">{{ msg.name || 'Du' }}</span>\n @if (showTimestamps() && msg.timestamp) {\n <span class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</span>\n }\n </div>\n } @else if (msg.name) {\n <div class=\"lc-chat__name\">{{ msg.name }}</div>\n }\n <div class=\"lc-chat__content\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__sr-only\">{{ statusLabel(msg.status) }} </span>\n }\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate; context: { $implicit: msg }\" />\n } @else if (shouldRenderMarkdown(msg.role)) {\n <lc-markdown [content]=\"msg.content\" variant=\"chat\" />\n } @else {\n {{ msg.content }}\n }\n <span class=\"lc-chat__cursor\" [class.lc-chat__cursor--visible]=\"msg.streaming\"></span>\n </div>\n @if (msg.attachments?.length) {\n <div class=\"lc-chat__attachments\">\n @for (att of msg.attachments; track att.id) {\n @if (isImage(att) && att.url) {\n <a class=\"lc-chat__attachment lc-chat__attachment--image\" [href]=\"att.url\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"att.url\" [alt]=\"att.name\" />\n </a>\n } @else {\n <a\n class=\"lc-chat__attachment lc-chat__attachment--file\"\n [href]=\"att.url || null\"\n [attr.download]=\"att.url ? att.name : null\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span class=\"lc-chat__attachment-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__attachment-size\">{{ formatBytes(att.size) }}</span>\n }\n </a>\n }\n }\n </div>\n }\n @if (showTimestamps() && msg.timestamp && msg.role !== 'user') {\n <div class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n </div>\n }\n\n @if (isStreaming() && !(formattedMessages().length && formattedMessages()[formattedMessages().length - 1].streaming)) {\n <div class=\"lc-chat__message lc-chat__message--agent\">\n @if (showAvatars()) {\n <div class=\"lc-chat__marker\">\n <span class=\"lc-chat__dot lc-chat__dot--agent\"></span>\n </div>\n }\n <div class=\"lc-chat__bubble\">\n <div class=\"lc-chat__typing\">\n <span></span><span></span><span></span>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"lc-chat__input-area\">\n <div class=\"lc-chat__composer\">\n @if (allowFileUpload() && pendingAttachments().length) {\n <div class=\"lc-chat__pending-attachments\">\n @for (att of pendingAttachments(); track att.id) {\n <div class=\"lc-chat__pending-attachment\" [class.lc-chat__pending-attachment--image]=\"isImage(att)\">\n @if (isImage(att) && att.url) {\n <img class=\"lc-chat__pending-thumb\" [src]=\"att.url\" [alt]=\"att.name\" />\n } @else {\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n }\n <span class=\"lc-chat__pending-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__pending-size\">{{ formatBytes(att.size) }}</span>\n }\n <button\n type=\"button\"\n class=\"lc-chat__pending-remove\"\n (click)=\"removeAttachment(att.id)\"\n [attr.aria-label]=\"'Entfernen: ' + att.name\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n }\n\n <textarea\n #composerInput\n class=\"lc-chat__input\"\n [placeholder]=\"placeholder()\"\n [value]=\"inputValue()\"\n [disabled]=\"disabled()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n rows=\"1\"\n ></textarea>\n\n <div class=\"lc-chat__composer-actions\">\n @if (allowFileUpload()) {\n <input\n #fileInput\n type=\"file\"\n class=\"lc-chat__file-input\"\n [accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n (change)=\"onFilesSelected($event)\"\n />\n <button\n type=\"button\"\n class=\"lc-chat__attach-btn\"\n [disabled]=\"disabled()\"\n (click)=\"openFilePicker()\"\n aria-label=\"Datei anh\u00E4ngen\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M14.5 9.5l-5 5a3 3 0 0 1-4.243-4.243l6.364-6.364a2 2 0 0 1 2.829 2.829l-6.364 6.364a1 1 0 1 1-1.415-1.415L11.5 6.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n }\n <button\n class=\"lc-chat__send-btn\"\n [disabled]=\"(!inputValue().trim() && !pendingAttachments().length) || disabled()\"\n (click)=\"send()\"\n aria-label=\"Send\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M3 10l7-7m0 0l7 7m-7-7v14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(90 10 10)\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n</div>\n", styles: [".lc-chat{--lc-chat-content-width: 46rem;display:flex;flex-direction:column;height:100%;min-height:20rem;border:1px solid var(--color-border);border-radius:var(--border-radius-lg);overflow:hidden;background-color:var(--color-surface);color:var(--color-text-primary);font-family:var(--font-family-sans, sans-serif)}.lc-chat--borderless{border:none;border-radius:0}.lc-chat--width-full{--lc-chat-content-width: 100%}.lc-chat__header{display:flex;align-items:center;justify-content:space-between;padding:var(--spacing-3) var(--spacing-4);border-bottom:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-primary)}.lc-chat__streaming-badge{font-size:var(--font-size-xs, .6875rem);padding:.125rem .5rem;background:color-mix(in srgb,var(--color-primary-500) 15%,transparent);color:var(--color-primary-500);border-radius:var(--border-radius-full, 1rem);font-weight:var(--font-weight-medium, 500);animation:pulse-badge 1.5s infinite}@keyframes pulse-badge{0%,to{opacity:1}50%{opacity:.6}}.lc-chat__messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;padding-block:var(--spacing-6, 1.5rem);padding-inline:var(--spacing-4, 1rem)}.lc-chat__thread{position:relative;width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--spacing-5, 1.25rem)}.lc-chat__messages--anchor-bottom .lc-chat__thread>.lc-chat__message:first-child{margin-top:auto}.lc-chat__message{position:relative;display:flex;align-items:stretch;gap:var(--spacing-3, .75rem);align-self:stretch;max-width:100%}.lc-chat__marker{position:relative;z-index:1;flex-shrink:0;width:.875rem;display:flex;flex-direction:column;align-items:center}.lc-chat__marker:before{content:\"\";position:absolute;left:50%;top:.55rem;bottom:calc(-1 * var(--spacing-5, 1.25rem));width:1.5px;transform:translate(-50%);background:var(--color-divider);z-index:-1}.lc-chat__message:last-child .lc-chat__marker:before{display:none}.lc-chat__dot{position:relative;z-index:1;width:.625rem;height:.625rem;margin-top:.25rem;border-radius:50%;background:var(--color-text-tertiary);box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__dot--agent{background:var(--color-primary-500)}.lc-chat__dot--system{background:var(--color-text-tertiary);opacity:.6}.lc-chat__message--streaming .lc-chat__dot--agent{animation:dot-pulse 1.4s ease-out infinite}@keyframes dot-pulse{0%{box-shadow:0 0 0 3px var(--color-surface),0 0 0 3px color-mix(in srgb,var(--color-primary-500) 45%,transparent)}70%,to{box-shadow:0 0 0 3px var(--color-surface),0 0 0 9px color-mix(in srgb,var(--color-primary-500) 0%,transparent)}}.lc-chat__avatar-img{width:1.5rem;height:1.5rem;border-radius:50%;object-fit:cover;box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__status-icon{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;margin-top:.0625rem;border-radius:50%;background:var(--color-surface);box-shadow:0 0 0 2px var(--color-surface)}.lc-chat__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.lc-chat__bubble{flex:1;min-width:0;font-size:var(--font-size-base, .9375rem);line-height:var(--line-height-relaxed, 1.6);word-break:break-word;color:var(--color-text-primary)}.lc-chat__message--user .lc-chat__bubble{position:relative;z-index:1;padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem);border:1px solid var(--color-border);border-radius:var(--border-radius-lg);background:var(--color-surface-2)}.lc-chat__message--agent .lc-chat__bubble{padding:0;background:transparent}.lc-chat__message--system .lc-chat__bubble{padding:0;background:transparent;color:var(--color-text-tertiary);font-size:var(--font-size-sm, .8125rem)}.lc-chat__meta{display:flex;align-items:baseline;justify-content:space-between;gap:var(--spacing-2, .5rem);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__meta .lc-chat__name{margin-bottom:0}.lc-chat__meta .lc-chat__time{margin-top:0}.lc-chat__name{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-secondary);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__content{white-space:pre-wrap}.lc-chat__cursor{display:none}.lc-chat__cursor--visible{display:inline-block;width:2px;height:1em;background:currentColor;margin-left:1px;vertical-align:text-bottom;animation:blink-cursor .8s step-end infinite}@keyframes blink-cursor{0%,to{opacity:1}50%{opacity:0}}.lc-chat__time{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:var(--spacing-1, .25rem);flex-shrink:0}.lc-chat__typing{display:flex;gap:.25rem;padding:.25rem 0}.lc-chat__typing span{width:.375rem;height:.375rem;border-radius:50%;background:var(--color-text-tertiary);animation:typing-bounce 1.4s ease-in-out infinite}.lc-chat__typing span:nth-child(2){animation-delay:.2s}.lc-chat__typing span:nth-child(3){animation-delay:.4s}@keyframes typing-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-.375rem)}}.lc-chat__input-area{padding:var(--spacing-4, .75rem);border-top:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__composer{width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;display:flex;flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--spacing-3, .75rem);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--border-radius-xl);transition:border-color .15s,box-shadow .15s}.lc-chat__composer:focus-within{border-color:var(--color-primary-500);box-shadow:var(--elevation-1)}.lc-chat__input{display:block;width:100%;box-sizing:border-box;resize:none;border:none;background:transparent;padding:var(--spacing-1, .25rem) var(--spacing-2, .5rem);font-size:var(--font-size-base, .875rem);font-family:inherit;line-height:var(--line-height-normal, 1.4);color:var(--color-text-primary);outline:none;height:auto;max-height:12rem;overflow-y:auto}.lc-chat__input:disabled{opacity:.5;cursor:not-allowed}.lc-chat__input::placeholder{color:var(--color-text-tertiary)}.lc-chat__composer-actions{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.lc-chat__send-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;margin-left:auto;border:none;border-radius:var(--border-radius-md);background:var(--color-primary-fill);color:var(--color-on-primary);cursor:pointer;flex-shrink:0;transition:background .15s,opacity .15s}.lc-chat__send-btn:hover:not(:disabled){background:var(--color-primary-fill-hover)}.lc-chat__send-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__file-input{display:none}.lc-chat__attach-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:none;border-radius:var(--border-radius-md);background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0;transition:background .15s,color .15s}.lc-chat__attach-btn:hover:not(:disabled){background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attach-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.375rem}.lc-chat__pending-attachment{display:inline-flex;align-items:center;gap:.375rem;padding:.25rem .375rem .25rem .5rem;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--border-radius-md, .5rem);font-size:var(--font-size-xs, .75rem);color:var(--color-text-primary);max-width:16rem}.lc-chat__pending-thumb{width:1.75rem;height:1.75rem;object-fit:cover;border-radius:.25rem;flex-shrink:0}.lc-chat__pending-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:9rem}.lc-chat__pending-size{color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}.lc-chat__pending-remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:none;border-radius:50%;background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attachments{display:flex;flex-wrap:wrap;gap:.375rem;margin-top:.5rem}.lc-chat__attachment{display:inline-flex;align-items:center;gap:.375rem;text-decoration:none;color:inherit;font-size:var(--font-size-xs, .75rem)}.lc-chat__attachment--file{padding:.25rem .5rem;background:color-mix(in srgb,currentColor 8%,transparent);border-radius:var(--border-radius-md, .375rem);max-width:16rem}.lc-chat__attachment--file:hover{background:color-mix(in srgb,currentColor 16%,transparent)}.lc-chat__attachment--image img{max-width:12rem;max-height:8rem;border-radius:var(--border-radius-md, .375rem);object-fit:cover;display:block}.lc-chat__attachment-icon{flex-shrink:0}.lc-chat__attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-chat__attachment-size{opacity:.7;font-variant-numeric:tabular-nums}\n"] }]
|
|
12400
|
+
args: [{ selector: 'lc-chat', standalone: true, imports: [NgTemplateOutlet, MarkdownComponent, IconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"lc-chat\"\n [class.lc-chat--borderless]=\"!bordered()\"\n [class.lc-chat--width-narrow]=\"contentWidth() === 'narrow'\"\n [class.lc-chat--width-full]=\"contentWidth() === 'full'\"\n>\n @if (showHeader()) {\n <div class=\"lc-chat__header\">\n <span class=\"lc-chat__title\">{{ title() }}</span>\n @if (isStreaming()) {\n <span class=\"lc-chat__streaming-badge\">Streaming\u2026</span>\n }\n </div>\n }\n\n <div\n class=\"lc-chat__messages\"\n [class.lc-chat__messages--anchor-bottom]=\"messageAnchor() === 'bottom'\"\n #scrollContainer\n >\n <div class=\"lc-chat__thread\">\n @for (msg of formattedMessages(); track msg.id) {\n <div\n class=\"lc-chat__message\"\n [class.lc-chat__message--user]=\"msg.role === 'user'\"\n [class.lc-chat__message--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__message--system]=\"msg.role === 'system'\"\n [class.lc-chat__message--avatar]=\"showUserAvatar(msg)\"\n [class.lc-chat__message--streaming]=\"msg.streaming\"\n [attr.role]=\"msg.status === 'error' ? 'alert' : null\"\n [attr.aria-live]=\"statusAriaLive(msg.status)\"\n >\n @if ((msg.role !== 'user' && (showAvatars() || isSemanticStatus(msg.status))) || showUserAvatar(msg)) {\n <div class=\"lc-chat__marker\" [class.lc-chat__marker--user]=\"msg.role === 'user'\">\n @if (isSemanticStatus(msg.status) && msg.role !== 'user') {\n <span class=\"lc-chat__status-icon\">\n <lc-icon [name]=\"statusIcon(msg.status)\" size=\"xs\" [color]=\"statusColor(msg.status)\" [decorative]=\"true\" />\n </span>\n } @else if (msg.avatar) {\n <img [src]=\"msg.avatar\" [alt]=\"msg.name || msg.role\" class=\"lc-chat__avatar-img\" />\n } @else if (msg.role === 'user') {\n <span class=\"lc-chat__monogram\" [attr.aria-label]=\"msg.name\">{{ initials(msg.name) }}</span>\n } @else {\n <span\n class=\"lc-chat__dot\"\n [class.lc-chat__dot--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__dot--system]=\"msg.role === 'system'\"\n ></span>\n }\n </div>\n }\n\n <div class=\"lc-chat__bubble\" [class.lc-chat__bubble--streaming]=\"msg.streaming\">\n @if (msg.name && msg.role !== 'user') {\n @if (showTimestamps() && msg.timestamp) {\n <div class=\"lc-chat__meta lc-chat__meta--agent\">\n <span class=\"lc-chat__name\">{{ msg.name }}</span>\n <span class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</span>\n </div>\n } @else {\n <div class=\"lc-chat__name\">{{ msg.name }}</div>\n }\n }\n <div class=\"lc-chat__content\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__sr-only\">{{ statusLabel(msg.status) }} </span>\n }\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate; context: { $implicit: msg }\" />\n } @else if (shouldRenderMarkdown(msg.role)) {\n <lc-markdown [content]=\"msg.content\" variant=\"chat\" />\n } @else {\n {{ msg.content }}\n }\n <span class=\"lc-chat__cursor\" [class.lc-chat__cursor--visible]=\"msg.streaming\"></span>\n </div>\n @if (msg.attachments?.length) {\n <div class=\"lc-chat__attachments\">\n @for (att of msg.attachments; track att.id) {\n @if (isImage(att) && att.url) {\n <a class=\"lc-chat__attachment lc-chat__attachment--image\" [href]=\"att.url\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"att.url\" [alt]=\"att.name\" />\n </a>\n } @else {\n <a\n class=\"lc-chat__attachment lc-chat__attachment--file\"\n [href]=\"att.url || null\"\n [attr.download]=\"att.url ? att.name : null\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span class=\"lc-chat__attachment-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__attachment-size\">{{ formatBytes(att.size) }}</span>\n }\n </a>\n }\n }\n </div>\n }\n @if (showTimestamps() && msg.timestamp && msg.role !== 'user' && !msg.name) {\n <div class=\"lc-chat__time lc-chat__time--tight\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n @if (showTimestamps() && msg.timestamp && msg.role === 'user') {\n <div class=\"lc-chat__time lc-chat__time--user\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n }\n\n @if (isStreaming() && !(formattedMessages().length && formattedMessages()[formattedMessages().length - 1].streaming)) {\n <div class=\"lc-chat__message lc-chat__message--agent\">\n @if (showAvatars()) {\n <div class=\"lc-chat__marker\">\n <span class=\"lc-chat__dot lc-chat__dot--agent\"></span>\n </div>\n }\n <div class=\"lc-chat__bubble\">\n <div class=\"lc-chat__typing\">\n <span></span><span></span><span></span>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"lc-chat__input-area\">\n <div class=\"lc-chat__composer\">\n @if (allowFileUpload() && pendingAttachments().length) {\n <div class=\"lc-chat__pending-attachments\">\n @for (att of pendingAttachments(); track att.id) {\n <div class=\"lc-chat__pending-attachment\" [class.lc-chat__pending-attachment--image]=\"isImage(att)\">\n @if (isImage(att) && att.url) {\n <img class=\"lc-chat__pending-thumb\" [src]=\"att.url\" [alt]=\"att.name\" />\n } @else {\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n }\n <span class=\"lc-chat__pending-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__pending-size\">{{ formatBytes(att.size) }}</span>\n }\n <button\n type=\"button\"\n class=\"lc-chat__pending-remove\"\n (click)=\"removeAttachment(att.id)\"\n [attr.aria-label]=\"'Entfernen: ' + att.name\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n }\n\n <textarea\n #composerInput\n class=\"lc-chat__input\"\n [placeholder]=\"placeholder()\"\n [value]=\"inputValue()\"\n [disabled]=\"disabled()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n rows=\"1\"\n ></textarea>\n\n <div class=\"lc-chat__composer-actions\">\n @if (allowFileUpload()) {\n <input\n #fileInput\n type=\"file\"\n class=\"lc-chat__file-input\"\n [accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n (change)=\"onFilesSelected($event)\"\n />\n <button\n type=\"button\"\n class=\"lc-chat__attach-btn\"\n [disabled]=\"disabled()\"\n (click)=\"openFilePicker()\"\n aria-label=\"Datei anh\u00E4ngen\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M14.5 9.5l-5 5a3 3 0 0 1-4.243-4.243l6.364-6.364a2 2 0 0 1 2.829 2.829l-6.364 6.364a1 1 0 1 1-1.415-1.415L11.5 6.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n }\n <button\n class=\"lc-chat__send-btn\"\n [disabled]=\"(!inputValue().trim() && !pendingAttachments().length) || disabled()\"\n (click)=\"send()\"\n aria-label=\"Send\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M3 10l7-7m0 0l7 7m-7-7v14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(90 10 10)\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n</div>\n", styles: [".lc-chat{--lc-chat-content-width: 46rem;--lc-chat-thread-gap: var(--spacing-3, .75rem);--lc-chat-messages-pad-block: var(--spacing-4, 1rem);--lc-chat-messages-pad-inline: var(--spacing-3, .75rem);--lc-chat-bubble-font: var(--font-size-sm, .875rem);--lc-chat-bubble-line: var(--line-height-normal, 1.5);--lc-chat-bubble-pad-block: var(--spacing-2, .5rem);--lc-chat-bubble-pad-inline: var(--spacing-3, .75rem);--lc-chat-header-pad-block: var(--spacing-2, .5rem);--lc-chat-input-area-pad: var(--spacing-3, .75rem);--lc-chat-composer-pad: var(--spacing-2, .5rem);display:flex;flex-direction:column;height:100%;min-height:20rem;border:1px solid var(--color-border);border-radius:var(--border-radius-lg);overflow:hidden;background-color:var(--color-surface);color:var(--color-text-primary);font-family:var(--font-family-sans, sans-serif)}.lc-chat--borderless{border:none;border-radius:0}.lc-chat--width-full{--lc-chat-content-width: 100%}.lc-chat__header{display:flex;align-items:center;justify-content:space-between;padding:var(--lc-chat-header-pad-block) var(--spacing-4);border-bottom:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-primary)}.lc-chat__streaming-badge{font-size:var(--font-size-xs, .6875rem);padding:.125rem .5rem;background:color-mix(in srgb,var(--color-primary-500) 15%,transparent);color:var(--color-primary-500);border-radius:var(--border-radius-full, 1rem);font-weight:var(--font-weight-medium, 500);animation:pulse-badge 1.5s infinite}@keyframes pulse-badge{0%,to{opacity:1}50%{opacity:.6}}.lc-chat__messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;padding-block:var(--lc-chat-messages-pad-block);padding-inline:var(--lc-chat-messages-pad-inline)}.lc-chat__thread{position:relative;width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--lc-chat-thread-gap)}.lc-chat__messages--anchor-bottom .lc-chat__thread>.lc-chat__message:first-child{margin-top:auto}.lc-chat__message{position:relative;display:flex;align-items:stretch;gap:var(--spacing-3, .75rem);align-self:stretch;max-width:100%}.lc-chat__message--user{flex-direction:column;align-items:flex-end;gap:var(--spacing-1, .25rem)}.lc-chat__message--user.lc-chat__message--avatar{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:\"bubble avatar\" \"time avatar\";column-gap:var(--spacing-2, .5rem);align-items:start}.lc-chat__message--user.lc-chat__message--avatar>.lc-chat__bubble{grid-area:bubble;justify-self:end}.lc-chat__message--user.lc-chat__message--avatar>.lc-chat__time--user{grid-area:time;justify-self:end}.lc-chat__message--user.lc-chat__message--avatar>.lc-chat__marker--user{grid-area:avatar}.lc-chat__marker{position:relative;z-index:1;flex-shrink:0;width:.875rem;display:flex;flex-direction:column;align-items:center}.lc-chat__marker:before{content:\"\";position:absolute;left:50%;top:.55rem;bottom:calc(-1 * var(--lc-chat-thread-gap));width:1.5px;transform:translate(-50%);background:var(--color-divider);z-index:-1}.lc-chat__message:last-child .lc-chat__marker:before{display:none}.lc-chat__dot{position:relative;z-index:1;width:.625rem;height:.625rem;margin-top:.25rem;border-radius:50%;background:var(--color-text-tertiary);box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__dot--agent{background:var(--color-primary-500)}.lc-chat__dot--system{background:var(--color-text-tertiary);opacity:.6}.lc-chat__message--streaming .lc-chat__dot--agent{animation:dot-pulse 1.4s ease-out infinite}@keyframes dot-pulse{0%{box-shadow:0 0 0 3px var(--color-surface),0 0 0 3px color-mix(in srgb,var(--color-primary-500) 45%,transparent)}70%,to{box-shadow:0 0 0 3px var(--color-surface),0 0 0 9px color-mix(in srgb,var(--color-primary-500) 0%,transparent)}}.lc-chat__avatar-img{width:1.5rem;height:1.5rem;border-radius:50%;object-fit:cover;box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__marker--user{width:auto}.lc-chat__marker--user:before{display:none}.lc-chat__monogram{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:color-mix(in srgb,var(--color-primary-500) 18%,var(--color-surface-2));color:var(--color-primary-500);font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);line-height:1;box-shadow:0 0 0 3px var(--color-surface)}.lc-chat__status-icon{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;margin-top:.0625rem;border-radius:50%;background:var(--color-surface);box-shadow:0 0 0 2px var(--color-surface)}.lc-chat__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.lc-chat__bubble{flex:1;min-width:0;font-size:var(--lc-chat-bubble-font);line-height:var(--lc-chat-bubble-line);word-break:break-word;color:var(--color-text-primary)}.lc-chat__message--user .lc-chat__bubble{position:relative;z-index:1;flex:0 1 auto;max-width:min(85%,34rem);padding:var(--lc-chat-bubble-pad-block) var(--lc-chat-bubble-pad-inline);border:1px solid color-mix(in srgb,var(--color-primary-500) 22%,transparent);border-radius:var(--border-radius-lg);background:color-mix(in srgb,var(--color-primary-500) 10%,var(--color-surface-2))}.lc-chat__message--agent .lc-chat__bubble{padding:0;background:transparent}.lc-chat__message--system .lc-chat__bubble{padding:0;background:transparent;color:var(--color-text-tertiary);font-size:var(--font-size-sm, .8125rem)}.lc-chat__meta{display:flex;align-items:baseline;justify-content:space-between;gap:var(--spacing-2, .5rem);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__meta .lc-chat__name{margin-bottom:0}.lc-chat__meta .lc-chat__time{margin-top:0}.lc-chat__name{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-text-secondary);margin-bottom:var(--spacing-1, .25rem)}.lc-chat__content{white-space:pre-wrap}.lc-chat__cursor{display:none}.lc-chat__cursor--visible{display:inline-block;width:2px;height:1em;background:currentColor;margin-left:1px;vertical-align:text-bottom;animation:blink-cursor .8s step-end infinite}@keyframes blink-cursor{0%,to{opacity:1}50%{opacity:0}}.lc-chat__time{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:var(--spacing-1, .25rem);flex-shrink:0}.lc-chat__time--tight{margin-top:0}.lc-chat__time--user{margin-top:0;padding-inline:var(--spacing-1, .25rem)}.lc-chat__typing{display:flex;gap:.25rem;padding:.25rem 0}.lc-chat__typing span{width:.375rem;height:.375rem;border-radius:50%;background:var(--color-text-tertiary);animation:typing-bounce 1.4s ease-in-out infinite}.lc-chat__typing span:nth-child(2){animation-delay:.2s}.lc-chat__typing span:nth-child(3){animation-delay:.4s}@keyframes typing-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-.375rem)}}.lc-chat__input-area{padding:var(--lc-chat-input-area-pad);border-top:1px solid var(--color-divider);background:var(--color-surface)}.lc-chat__composer{width:100%;max-width:var(--lc-chat-content-width, 46rem);margin-inline:auto;display:flex;flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--lc-chat-composer-pad);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--border-radius-xl);transition:border-color .15s,box-shadow .15s}.lc-chat__composer:focus-within{border-color:var(--color-primary-500);box-shadow:var(--elevation-1)}.lc-chat__input{display:block;width:100%;box-sizing:border-box;resize:none;border:none;background:transparent;padding:var(--spacing-1, .25rem) var(--spacing-2, .5rem);font-size:var(--font-size-base, .875rem);font-family:inherit;line-height:var(--line-height-normal, 1.4);color:var(--color-text-primary);outline:none;height:auto;max-height:12rem;overflow-y:auto}.lc-chat__input:disabled{opacity:.5;cursor:not-allowed}.lc-chat__input::placeholder{color:var(--color-text-tertiary)}.lc-chat__composer-actions{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.lc-chat__send-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;margin-left:auto;border:none;border-radius:var(--border-radius-md);background:var(--color-primary-fill);color:var(--color-on-primary);cursor:pointer;flex-shrink:0;transition:background .15s,opacity .15s}.lc-chat__send-btn:hover:not(:disabled){background:var(--color-primary-fill-hover)}.lc-chat__send-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__file-input{display:none}.lc-chat__attach-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:none;border-radius:var(--border-radius-md);background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0;transition:background .15s,color .15s}.lc-chat__attach-btn:hover:not(:disabled){background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attach-btn:disabled{opacity:.4;cursor:not-allowed}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.375rem}.lc-chat__pending-attachment{display:inline-flex;align-items:center;gap:.375rem;padding:.25rem .375rem .25rem .5rem;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--border-radius-md, .5rem);font-size:var(--font-size-xs, .75rem);color:var(--color-text-primary);max-width:16rem}.lc-chat__pending-thumb{width:1.75rem;height:1.75rem;object-fit:cover;border-radius:.25rem;flex-shrink:0}.lc-chat__pending-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:9rem}.lc-chat__pending-size{color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}.lc-chat__pending-remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:none;border-radius:50%;background:transparent;color:var(--color-text-tertiary);cursor:pointer;flex-shrink:0}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__attachments{display:flex;flex-wrap:wrap;gap:.375rem;margin-top:.5rem}.lc-chat__attachment{display:inline-flex;align-items:center;gap:.375rem;text-decoration:none;color:inherit;font-size:var(--font-size-xs, .75rem)}.lc-chat__attachment--file{padding:.25rem .5rem;background:color-mix(in srgb,currentColor 8%,transparent);border-radius:var(--border-radius-md, .375rem);max-width:16rem}.lc-chat__attachment--file:hover{background:color-mix(in srgb,currentColor 16%,transparent)}.lc-chat__attachment--image img{max-width:12rem;max-height:8rem;border-radius:var(--border-radius-md, .375rem);object-fit:cover;display:block}.lc-chat__attachment-icon{flex-shrink:0}.lc-chat__attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-chat__attachment-size{opacity:.7;font-variant-numeric:tabular-nums}\n"] }]
|
|
12344
12401
|
}], propDecorators: { messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], isStreaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "isStreaming", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], messageAnchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageAnchor", required: false }] }], contentWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentWidth", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showAvatars: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAvatars", required: false }] }], showTimestamps: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTimestamps", required: false }] }], renderMarkdown: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderMarkdown", required: false }] }], allowFileUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowFileUpload", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], messageSend: [{ type: i0.Output, args: ["messageSend"] }], fileAttach: [{ type: i0.Output, args: ["fileAttach"] }], messageTemplate: [{
|
|
12345
12402
|
type: ContentChild,
|
|
12346
12403
|
args: ['messageTemplate']
|
|
@@ -13053,6 +13110,12 @@ const NODE_WIDTH = 160;
|
|
|
13053
13110
|
const NODE_HEIGHT = 40;
|
|
13054
13111
|
const H_GAP = 80;
|
|
13055
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;
|
|
13056
13119
|
const RELATION_STYLES = {
|
|
13057
13120
|
depends: { color: 'var(--color-neutral-400)', dashed: false, label: 'depends on' },
|
|
13058
13121
|
blocks: { color: 'var(--color-error-default)', dashed: false, label: 'blocks' },
|
|
@@ -13062,8 +13125,26 @@ const RELATION_STYLES = {
|
|
|
13062
13125
|
implements: { color: 'var(--color-success-default)', dashed: true, label: 'implements' },
|
|
13063
13126
|
uses: { color: 'var(--color-neutral-300)', dashed: true, label: 'uses' },
|
|
13064
13127
|
};
|
|
13065
|
-
|
|
13066
|
-
|
|
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
|
+
}
|
|
13067
13148
|
const nodePrimary = dir === 'horizontal' ? NODE_WIDTH : NODE_HEIGHT;
|
|
13068
13149
|
const nodeSecondary = dir === 'horizontal' ? NODE_HEIGHT : NODE_WIDTH;
|
|
13069
13150
|
if (children.length === 0) {
|
|
@@ -13080,17 +13161,20 @@ function measureTree(node, depth, dir) {
|
|
|
13080
13161
|
depth,
|
|
13081
13162
|
};
|
|
13082
13163
|
}
|
|
13083
|
-
function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes) {
|
|
13164
|
+
function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes, nodeMap) {
|
|
13084
13165
|
const nodeY = y + measure.secondary / 2 - NODE_HEIGHT / 2;
|
|
13085
13166
|
const orig = allNodes.get(measure.node.id);
|
|
13086
|
-
|
|
13167
|
+
const laid = {
|
|
13087
13168
|
id: measure.node.id, label: measure.node.label, description: measure.node.description,
|
|
13088
13169
|
icon: measure.node.icon, status: measure.node.status || 'default',
|
|
13170
|
+
type: measure.node.type, moreCount: measure.node.moreCount,
|
|
13089
13171
|
x, y: nodeY, width: NODE_WIDTH, height: NODE_HEIGHT, depth: measure.depth, parentId,
|
|
13090
13172
|
incomingCount: orig?.dependsOn?.length ?? 0, outgoingCount: orig?.children?.length ?? 0,
|
|
13091
|
-
}
|
|
13173
|
+
};
|
|
13174
|
+
nodes.push(laid);
|
|
13175
|
+
nodeMap.set(laid.id, laid);
|
|
13092
13176
|
if (parentId) {
|
|
13093
|
-
const parent =
|
|
13177
|
+
const parent = nodeMap.get(parentId);
|
|
13094
13178
|
const sx = parent.x + parent.width;
|
|
13095
13179
|
const sy = parent.y + parent.height / 2;
|
|
13096
13180
|
const tx = x;
|
|
@@ -13101,25 +13185,29 @@ function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes) {
|
|
|
13101
13185
|
path: `M ${sx} ${sy} C ${mx} ${sy}, ${mx} ${ty}, ${tx} ${ty}`,
|
|
13102
13186
|
labelX: mx, labelY: (sy + ty) / 2,
|
|
13103
13187
|
color: 'var(--color-neutral-300)', dashed: false, isCrossRef: false,
|
|
13188
|
+
marker: 'depends',
|
|
13104
13189
|
});
|
|
13105
13190
|
}
|
|
13106
13191
|
let childY = y;
|
|
13107
13192
|
for (const child of measure.children) {
|
|
13108
|
-
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);
|
|
13109
13194
|
childY += child.secondary + V_GAP;
|
|
13110
13195
|
}
|
|
13111
13196
|
}
|
|
13112
|
-
function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes) {
|
|
13197
|
+
function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes, nodeMap) {
|
|
13113
13198
|
const nodeX = x + measure.secondary / 2 - NODE_WIDTH / 2;
|
|
13114
13199
|
const orig = allNodes.get(measure.node.id);
|
|
13115
|
-
|
|
13200
|
+
const laid = {
|
|
13116
13201
|
id: measure.node.id, label: measure.node.label, description: measure.node.description,
|
|
13117
13202
|
icon: measure.node.icon, status: measure.node.status || 'default',
|
|
13203
|
+
type: measure.node.type, moreCount: measure.node.moreCount,
|
|
13118
13204
|
x: nodeX, y, width: NODE_WIDTH, height: NODE_HEIGHT, depth: measure.depth, parentId,
|
|
13119
13205
|
incomingCount: orig?.dependsOn?.length ?? 0, outgoingCount: orig?.children?.length ?? 0,
|
|
13120
|
-
}
|
|
13206
|
+
};
|
|
13207
|
+
nodes.push(laid);
|
|
13208
|
+
nodeMap.set(laid.id, laid);
|
|
13121
13209
|
if (parentId) {
|
|
13122
|
-
const parent =
|
|
13210
|
+
const parent = nodeMap.get(parentId);
|
|
13123
13211
|
const sx = parent.x + parent.width / 2;
|
|
13124
13212
|
const sy = parent.y + parent.height;
|
|
13125
13213
|
const tx = nodeX + NODE_WIDTH / 2;
|
|
@@ -13130,24 +13218,155 @@ function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes) {
|
|
|
13130
13218
|
path: `M ${sx} ${sy} C ${sx} ${my}, ${tx} ${my}, ${tx} ${ty}`,
|
|
13131
13219
|
labelX: (sx + tx) / 2, labelY: my,
|
|
13132
13220
|
color: 'var(--color-neutral-300)', dashed: false, isCrossRef: false,
|
|
13221
|
+
marker: 'depends',
|
|
13133
13222
|
});
|
|
13134
13223
|
}
|
|
13135
13224
|
let childX = x;
|
|
13136
13225
|
for (const child of measure.children) {
|
|
13137
|
-
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);
|
|
13138
13227
|
childX += child.secondary + V_GAP;
|
|
13139
13228
|
}
|
|
13140
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.
|
|
13141
13233
|
function collectAllNodes(node, map) {
|
|
13234
|
+
if (map.has(node.id))
|
|
13235
|
+
return;
|
|
13142
13236
|
map.set(node.id, node);
|
|
13143
13237
|
for (const child of node.children || [])
|
|
13144
13238
|
collectAllNodes(child, map);
|
|
13145
13239
|
}
|
|
13146
|
-
|
|
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
|
+
return { path: cubicPath(points), labelX: mx, labelY: (p0.y + p3.y) / 2 - 8, points };
|
|
13277
|
+
}
|
|
13278
|
+
const p0 = { x: source.x + source.width / 2, y: source.y + source.height };
|
|
13279
|
+
const p3 = { x: target.x + target.width / 2, y: target.y };
|
|
13280
|
+
const my = (p0.y + p3.y) / 2;
|
|
13281
|
+
const points = [p0, { x: p0.x, y: my }, { x: p3.x, y: my }, p3];
|
|
13282
|
+
return { path: cubicPath(points), labelX: (p0.x + p3.x) / 2, labelY: my - 8, points };
|
|
13283
|
+
}
|
|
13284
|
+
/**
|
|
13285
|
+
* Leaves from one side of both boxes and bows clear of everything in `obstacles`,
|
|
13286
|
+
* so the edge travels through empty space instead of disappearing behind the nodes
|
|
13287
|
+
* between its endpoints.
|
|
13288
|
+
*
|
|
13289
|
+
* The side matters: bowing *below* clears an obstacle sitting beside the endpoints,
|
|
13290
|
+
* but drives straight through one stacked underneath them. Callers try several.
|
|
13291
|
+
*/
|
|
13292
|
+
function bowedGeometry(source, target, obstacles, side) {
|
|
13293
|
+
const all = [source, target, ...obstacles];
|
|
13294
|
+
switch (side) {
|
|
13295
|
+
case 'below': {
|
|
13296
|
+
const dip = Math.max(...all.map(n => n.y + n.height)) + BACK_EDGE_BOW;
|
|
13297
|
+
const p0 = { x: source.x + source.width / 2, y: source.y + source.height };
|
|
13298
|
+
const p3 = { x: target.x + target.width / 2, y: target.y + target.height };
|
|
13299
|
+
const points = [p0, { x: p0.x, y: dip }, { x: p3.x, y: dip }, p3];
|
|
13300
|
+
return { path: cubicPath(points), labelX: (p0.x + p3.x) / 2, labelY: dip - 4, points };
|
|
13301
|
+
}
|
|
13302
|
+
case 'above': {
|
|
13303
|
+
const rise = Math.min(...all.map(n => n.y)) - BACK_EDGE_BOW;
|
|
13304
|
+
const p0 = { x: source.x + source.width / 2, y: source.y };
|
|
13305
|
+
const p3 = { x: target.x + target.width / 2, y: target.y };
|
|
13306
|
+
const points = [p0, { x: p0.x, y: rise }, { x: p3.x, y: rise }, p3];
|
|
13307
|
+
return { path: cubicPath(points), labelX: (p0.x + p3.x) / 2, labelY: rise + 12, points };
|
|
13308
|
+
}
|
|
13309
|
+
case 'right': {
|
|
13310
|
+
const bow = Math.max(...all.map(n => n.x + n.width)) + BACK_EDGE_BOW;
|
|
13311
|
+
const p0 = { x: source.x + source.width, y: source.y + source.height / 2 };
|
|
13312
|
+
const p3 = { x: target.x + target.width, y: target.y + target.height / 2 };
|
|
13313
|
+
const points = [p0, { x: bow, y: p0.y }, { x: bow, y: p3.y }, p3];
|
|
13314
|
+
return { path: cubicPath(points), labelX: bow - 4, labelY: (p0.y + p3.y) / 2, points };
|
|
13315
|
+
}
|
|
13316
|
+
case 'left': {
|
|
13317
|
+
const bow = Math.min(...all.map(n => n.x)) - BACK_EDGE_BOW;
|
|
13318
|
+
const p0 = { x: source.x, y: source.y + source.height / 2 };
|
|
13319
|
+
const p3 = { x: target.x, y: target.y + target.height / 2 };
|
|
13320
|
+
const points = [p0, { x: bow, y: p0.y }, { x: bow, y: p3.y }, p3];
|
|
13321
|
+
return { path: cubicPath(points), labelX: bow + 4, labelY: (p0.y + p3.y) / 2, points };
|
|
13322
|
+
}
|
|
13323
|
+
}
|
|
13324
|
+
}
|
|
13325
|
+
// Try the side that suits the layout flow first: in a left-to-right tree the free
|
|
13326
|
+
// space is above and below the rows; in a top-to-bottom one it's left and right.
|
|
13327
|
+
const bowSides = (dir) => dir === 'horizontal' ? ['below', 'above', 'right', 'left'] : ['right', 'left', 'below', 'above'];
|
|
13328
|
+
/**
|
|
13329
|
+
* Cross-references connect arbitrary pairs, so unlike parent→child edges (which
|
|
13330
|
+
* always run down the gutter between two levels) they can cut straight across
|
|
13331
|
+
* unrelated nodes. Pick the cleanest of a handful of candidate routes.
|
|
13332
|
+
*
|
|
13333
|
+
* This is a small fixed search, not obstacle-avoiding routing: it clears the common
|
|
13334
|
+
* cases cheaply, but a dense enough graph can leave every candidate crossing
|
|
13335
|
+
* something. Then the least-bad one wins, and `direct` wins ties — a pointless
|
|
13336
|
+
* detour reads worse than a short hop behind one box.
|
|
13337
|
+
*/
|
|
13338
|
+
function routeCrossRef(source, target, dir, nodes, allowDirect = true) {
|
|
13339
|
+
const exclude = new Set([source.id, target.id]);
|
|
13340
|
+
const direct = directGeometry(source, target, dir);
|
|
13341
|
+
const obstacles = crossedNodes(direct.points, nodes, exclude);
|
|
13342
|
+
if (allowDirect && !obstacles.length)
|
|
13343
|
+
return direct;
|
|
13344
|
+
let best = allowDirect ? direct : null;
|
|
13345
|
+
let bestCount = allowDirect ? obstacles.length : Infinity;
|
|
13346
|
+
for (const side of bowSides(dir)) {
|
|
13347
|
+
const candidate = bowedGeometry(source, target, obstacles, side);
|
|
13348
|
+
const count = crossedNodes(candidate.points, nodes, exclude).length;
|
|
13349
|
+
if (count < bestCount) {
|
|
13350
|
+
best = candidate;
|
|
13351
|
+
bestCount = count;
|
|
13352
|
+
if (count === 0)
|
|
13353
|
+
break;
|
|
13354
|
+
}
|
|
13355
|
+
}
|
|
13356
|
+
return best ?? direct;
|
|
13357
|
+
}
|
|
13358
|
+
/**
|
|
13359
|
+
* Falls back to `depends` for anything not in the style table. Callers may pass a
|
|
13360
|
+
* relation that only carries a free-form `relationLabel`, and untyped callers can
|
|
13361
|
+
* pass an unknown string — neither may end up with an undefined style or an
|
|
13362
|
+
* arrow marker id that doesn't exist in the template's `<defs>`.
|
|
13363
|
+
*/
|
|
13364
|
+
function resolveRelation(relation) {
|
|
13365
|
+
return relation && RELATION_STYLES[relation] ? relation : 'depends';
|
|
13366
|
+
}
|
|
13367
|
+
function createCrossRefEdges(nodeMap, allOriginal, dir, backEdges) {
|
|
13147
13368
|
const edges = [];
|
|
13148
|
-
const
|
|
13149
|
-
for (const n of nodes)
|
|
13150
|
-
nodeMap.set(n.id, n);
|
|
13369
|
+
const nodes = [...nodeMap.values()];
|
|
13151
13370
|
for (const [, orig] of allOriginal) {
|
|
13152
13371
|
if (!orig.dependsOn?.length)
|
|
13153
13372
|
continue;
|
|
@@ -13158,47 +13377,87 @@ function createCrossRefEdges(nodes, allOriginal, dir) {
|
|
|
13158
13377
|
const source = nodeMap.get(dep.id);
|
|
13159
13378
|
if (!source)
|
|
13160
13379
|
continue;
|
|
13161
|
-
const relation = dep.relation
|
|
13380
|
+
const relation = resolveRelation(dep.relation);
|
|
13162
13381
|
const style = RELATION_STYLES[relation];
|
|
13163
|
-
|
|
13164
|
-
if (dir === 'horizontal') {
|
|
13165
|
-
const sx = source.x + source.width;
|
|
13166
|
-
const sy = source.y + source.height / 2;
|
|
13167
|
-
const tx = target.x;
|
|
13168
|
-
const ty = target.y + target.height / 2;
|
|
13169
|
-
const mx = (sx + tx) / 2;
|
|
13170
|
-
path = `M ${sx} ${sy} C ${mx} ${sy}, ${mx} ${ty}, ${tx} ${ty}`;
|
|
13171
|
-
labelX = mx;
|
|
13172
|
-
labelY = (sy + ty) / 2 - 8;
|
|
13173
|
-
}
|
|
13174
|
-
else {
|
|
13175
|
-
const sx = source.x + source.width / 2;
|
|
13176
|
-
const sy = source.y + source.height;
|
|
13177
|
-
const tx = target.x + target.width / 2;
|
|
13178
|
-
const ty = target.y;
|
|
13179
|
-
const my = (sy + ty) / 2;
|
|
13180
|
-
path = `M ${sx} ${sy} C ${sx} ${my}, ${tx} ${my}, ${tx} ${ty}`;
|
|
13181
|
-
labelX = (sx + tx) / 2;
|
|
13182
|
-
labelY = my - 8;
|
|
13183
|
-
}
|
|
13382
|
+
const geo = routeCrossRef(source, target, dir, nodes);
|
|
13184
13383
|
edges.push({
|
|
13185
13384
|
id: `${dep.id}⇢${orig.id}`, sourceId: dep.id, targetId: orig.id,
|
|
13186
|
-
path, relation
|
|
13187
|
-
|
|
13385
|
+
path: geo.path, relation: dep.relation,
|
|
13386
|
+
label: dep.relationLabel ?? style.label,
|
|
13387
|
+
labelX: geo.labelX, labelY: geo.labelY,
|
|
13388
|
+
color: style.color, dashed: style.dashed, isCrossRef: true, marker: relation,
|
|
13188
13389
|
});
|
|
13189
13390
|
}
|
|
13190
13391
|
}
|
|
13392
|
+
// Links the tree layout couldn't follow (cycle, or a node reached from a second
|
|
13393
|
+
// parent). The relationship is real, so draw it as a cross-reference rather than
|
|
13394
|
+
// dropping it silently. These always bow: they point against the layout flow by
|
|
13395
|
+
// definition, so the direct route would double back across their own row.
|
|
13396
|
+
for (const back of backEdges) {
|
|
13397
|
+
const source = nodeMap.get(back.from);
|
|
13398
|
+
const target = nodeMap.get(back.to);
|
|
13399
|
+
if (!source || !target)
|
|
13400
|
+
continue;
|
|
13401
|
+
const style = RELATION_STYLES.depends;
|
|
13402
|
+
const geo = routeCrossRef(source, target, dir, nodes, /* allowDirect */ false);
|
|
13403
|
+
edges.push({
|
|
13404
|
+
id: `${back.from}↺${back.to}`, sourceId: back.from, targetId: back.to,
|
|
13405
|
+
path: geo.path, labelX: geo.labelX, labelY: geo.labelY,
|
|
13406
|
+
color: style.color, dashed: true, isCrossRef: true, marker: 'depends',
|
|
13407
|
+
});
|
|
13408
|
+
}
|
|
13191
13409
|
return edges;
|
|
13192
13410
|
}
|
|
13193
13411
|
// ── Status colors ────────────────────────────────────────────────────────────
|
|
13412
|
+
// Labels read from the theme's semantic ink, not from a `--color-<status>-700`.
|
|
13413
|
+
// Those resolve to dark ink in both themes (error-700 is #6b0909), which on the
|
|
13414
|
+
// dark theme's translucent tint over #14222e is dark-on-dark. The status is
|
|
13415
|
+
// carried by the border and tint; the label just has to be readable, and
|
|
13416
|
+
// --color-text-* is the one pair that flips with the theme.
|
|
13194
13417
|
const STATUS_COLORS = {
|
|
13195
|
-
default: { bg: 'var(--color-neutral-50)', border: 'var(--color-neutral-300)', text: 'var(--color-
|
|
13196
|
-
active: { bg: 'var(--color-primary-50)', border: 'var(--color-primary-400)', text: 'var(--color-primary
|
|
13197
|
-
success: { bg: 'var(--color-success-50, #f0fdf4)', border: 'var(--color-success-default)', text: 'var(--color-
|
|
13198
|
-
warning: { bg: 'var(--color-warning-50, #fffbeb)', border: 'var(--color-warning-default)', text: 'var(--color-
|
|
13199
|
-
error: { bg: 'var(--color-error-50, #fef2f2)', border: 'var(--color-error-default)', text: 'var(--color-
|
|
13200
|
-
muted: { bg: 'var(--color-neutral-100)', border: 'var(--color-neutral-200)', text: 'var(--color-
|
|
13418
|
+
default: { bg: 'var(--color-neutral-50)', border: 'var(--color-neutral-300)', text: 'var(--color-text-primary)' },
|
|
13419
|
+
active: { bg: 'var(--color-primary-50)', border: 'var(--color-primary-400)', text: 'var(--color-text-primary)' },
|
|
13420
|
+
success: { bg: 'var(--color-success-50, #f0fdf4)', border: 'var(--color-success-default)', text: 'var(--color-text-primary)' },
|
|
13421
|
+
warning: { bg: 'var(--color-warning-50, #fffbeb)', border: 'var(--color-warning-default)', text: 'var(--color-text-primary)' },
|
|
13422
|
+
error: { bg: 'var(--color-error-50, #fef2f2)', border: 'var(--color-error-default)', text: 'var(--color-text-primary)' },
|
|
13423
|
+
muted: { bg: 'var(--color-neutral-100)', border: 'var(--color-neutral-200)', text: 'var(--color-text-tertiary)' },
|
|
13201
13424
|
};
|
|
13425
|
+
/** Parses `#rgb`, `#rrggbb`, `rgb()` and `rgba()`. Anything else → null. */
|
|
13426
|
+
function parseRgb(color) {
|
|
13427
|
+
const hex = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
13428
|
+
if (hex) {
|
|
13429
|
+
const h = hex[1];
|
|
13430
|
+
const full = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
|
|
13431
|
+
return [0, 2, 4].map(i => parseInt(full.slice(i, i + 2), 16));
|
|
13432
|
+
}
|
|
13433
|
+
const rgb = color.match(/^rgba?\(([^)]+)\)/i);
|
|
13434
|
+
if (rgb) {
|
|
13435
|
+
const p = rgb[1].split(',').map(s => parseFloat(s));
|
|
13436
|
+
if (p.length >= 3 && p.every(n => !Number.isNaN(n)))
|
|
13437
|
+
return [p[0], p[1], p[2]];
|
|
13438
|
+
}
|
|
13439
|
+
return null;
|
|
13440
|
+
}
|
|
13441
|
+
/**
|
|
13442
|
+
* Ink that stays readable on an arbitrary caller-supplied `typeColors` value.
|
|
13443
|
+
*
|
|
13444
|
+
* No theme token can do this job: a type colour is chosen by the consumer, so it
|
|
13445
|
+
* may be light while the theme is dark (or the reverse). `--color-neutral-900`
|
|
13446
|
+
* looks like "dark ink" but the dark theme inverts the neutral scale and resolves
|
|
13447
|
+
* it to #f9fafb — white label on a pale tile. Derive the ink from the colour's own
|
|
13448
|
+
* luminance instead, and fall back to the theme when it can't be parsed.
|
|
13449
|
+
*/
|
|
13450
|
+
function readableInk(color) {
|
|
13451
|
+
const rgb = parseRgb(color);
|
|
13452
|
+
if (!rgb)
|
|
13453
|
+
return 'var(--color-text-primary)';
|
|
13454
|
+
const [r, g, b] = rgb.map(c => {
|
|
13455
|
+
const s = c / 255;
|
|
13456
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
13457
|
+
});
|
|
13458
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
13459
|
+
return luminance > 0.4 ? '#111827' : '#f9fafb';
|
|
13460
|
+
}
|
|
13202
13461
|
/**
|
|
13203
13462
|
* Dependency viewer component for visualizing hierarchical and cross-cutting relationships.
|
|
13204
13463
|
*
|
|
@@ -13217,10 +13476,34 @@ const STATUS_COLORS = {
|
|
|
13217
13476
|
* - SVG arrowhead markers on cross-reference edges
|
|
13218
13477
|
* - Dark/light theme support
|
|
13219
13478
|
*
|
|
13479
|
+
* ## Feeding it a graph
|
|
13480
|
+
*
|
|
13481
|
+
* `root` is a *tree*, but the input is treated as a graph: a link back to a node
|
|
13482
|
+
* that already has a place in the layout — a cycle, or a node reached from a
|
|
13483
|
+
* second parent — is drawn as a cross-reference arrow instead of being followed.
|
|
13484
|
+
* Every node is laid out exactly once, and no input can make the layout recurse
|
|
13485
|
+
* forever.
|
|
13486
|
+
*
|
|
13487
|
+
* For incremental exploration, hand in a wider `root` per step and let
|
|
13488
|
+
* `nodeExpand` drive the loading. Pan, zoom, collapse state and selection all
|
|
13489
|
+
* survive a `root` swap, and `anchorNodeId` (defaulting to the selected node)
|
|
13490
|
+
* keeps the viewport pinned while the graph grows around it.
|
|
13491
|
+
*
|
|
13220
13492
|
* @example
|
|
13221
13493
|
* ```html
|
|
13222
13494
|
* <lc-dependency-viewer [root]="specTree" direction="horizontal" />
|
|
13223
13495
|
* ```
|
|
13496
|
+
*
|
|
13497
|
+
* @example Incremental graph exploration
|
|
13498
|
+
* ```html
|
|
13499
|
+
* <lc-dependency-viewer
|
|
13500
|
+
* [root]="graph()"
|
|
13501
|
+
* [anchorNodeId]="anchor()"
|
|
13502
|
+
* [typeColors]="{ Alpha: '#c1e3e9' }"
|
|
13503
|
+
* (nodeSelect)="showDetails($event)"
|
|
13504
|
+
* (nodeExpand)="loadNeighbours($event)"
|
|
13505
|
+
* />
|
|
13506
|
+
* ```
|
|
13224
13507
|
*/
|
|
13225
13508
|
class DependencyViewerComponent {
|
|
13226
13509
|
root = input.required(...(ngDevMode ? [{ debugName: "root" }] : /* istanbul ignore next */ []));
|
|
@@ -13229,6 +13512,22 @@ class DependencyViewerComponent {
|
|
|
13229
13512
|
showToolbar = input(true, ...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
|
|
13230
13513
|
showEdgeLabels = input(true, ...(ngDevMode ? [{ debugName: "showEdgeLabels" }] : /* istanbul ignore next */ []));
|
|
13231
13514
|
edgeWidth = input(1.5, ...(ngDevMode ? [{ debugName: "edgeWidth" }] : /* istanbul ignore next */ []));
|
|
13515
|
+
/**
|
|
13516
|
+
* Node the viewport holds still across `root` updates. Defaults to the node the
|
|
13517
|
+
* user last selected, which keeps click-to-expand steady without any wiring.
|
|
13518
|
+
* Set it explicitly to anchor somewhere else (e.g. a deep-linked node).
|
|
13519
|
+
*/
|
|
13520
|
+
anchorNodeId = input(null, ...(ngDevMode ? [{ debugName: "anchorNodeId" }] : /* istanbul ignore next */ []));
|
|
13521
|
+
/** Fill colour per `DependencyNode.type`, e.g. `{ Class: '#8ea475' }`. */
|
|
13522
|
+
typeColors = input({}, ...(ngDevMode ? [{ debugName: "typeColors" }] : /* istanbul ignore next */ []));
|
|
13523
|
+
/** Relations to hide. Cross-reference edges of these types aren't rendered. */
|
|
13524
|
+
hiddenRelations = input([], ...(ngDevMode ? [{ debugName: "hiddenRelations" }] : /* istanbul ignore next */ []));
|
|
13525
|
+
/** Node types to hide, matched against `DependencyNode.type`. */
|
|
13526
|
+
hiddenTypes = input([], ...(ngDevMode ? [{ debugName: "hiddenTypes" }] : /* istanbul ignore next */ []));
|
|
13527
|
+
/** Fires when a node is selected (not on deselect), with the original node incl. `data`. */
|
|
13528
|
+
nodeSelect = output();
|
|
13529
|
+
/** Fires on double-click — the hook for "expand this node's neighbourhood". */
|
|
13530
|
+
nodeExpand = output();
|
|
13232
13531
|
zoom = signal(100, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
13233
13532
|
panX = signal(40, ...(ngDevMode ? [{ debugName: "panX" }] : /* istanbul ignore next */ []));
|
|
13234
13533
|
panY = signal(40, ...(ngDevMode ? [{ debugName: "panY" }] : /* istanbul ignore next */ []));
|
|
@@ -13237,7 +13536,10 @@ class DependencyViewerComponent {
|
|
|
13237
13536
|
isPanning = false;
|
|
13238
13537
|
lastMouseX = 0;
|
|
13239
13538
|
lastMouseY = 0;
|
|
13240
|
-
|
|
13539
|
+
hostEl = inject((ElementRef));
|
|
13540
|
+
/** Last known layout position of the anchor, to compensate pan after a relayout. */
|
|
13541
|
+
anchorPos = null;
|
|
13542
|
+
effectiveRoot = computed(() => this.pruneCollapsed(this.root(), this.collapsedIds(), new Set()), ...(ngDevMode ? [{ debugName: "effectiveRoot" }] : /* istanbul ignore next */ []));
|
|
13241
13543
|
allOriginalNodes = computed(() => {
|
|
13242
13544
|
const map = new Map();
|
|
13243
13545
|
collectAllNodes(this.root(), map);
|
|
@@ -13246,24 +13548,73 @@ class DependencyViewerComponent {
|
|
|
13246
13548
|
layout = computed(() => {
|
|
13247
13549
|
const root = this.effectiveRoot();
|
|
13248
13550
|
const dir = this.direction();
|
|
13249
|
-
const
|
|
13551
|
+
const hiddenTypes = new Set(this.hiddenTypes());
|
|
13552
|
+
const hiddenRelations = new Set(this.hiddenRelations());
|
|
13553
|
+
const backEdges = [];
|
|
13554
|
+
const measured = measureTree(root, 0, dir, new Set(), backEdges);
|
|
13250
13555
|
const nodes = [];
|
|
13556
|
+
const nodeMap = new Map();
|
|
13251
13557
|
const treeEdges = [];
|
|
13252
13558
|
const allNodes = this.allOriginalNodes();
|
|
13253
13559
|
if (dir === 'horizontal') {
|
|
13254
|
-
layoutTreeH(measured, 0, 0, null, nodes, treeEdges, allNodes);
|
|
13560
|
+
layoutTreeH(measured, 0, 0, null, nodes, treeEdges, allNodes, nodeMap);
|
|
13255
13561
|
}
|
|
13256
13562
|
else {
|
|
13257
|
-
layoutTreeV(measured, 0, 0, null, nodes, treeEdges, allNodes);
|
|
13258
|
-
}
|
|
13259
|
-
const crossEdges = createCrossRefEdges(
|
|
13563
|
+
layoutTreeV(measured, 0, 0, null, nodes, treeEdges, allNodes, nodeMap);
|
|
13564
|
+
}
|
|
13565
|
+
const crossEdges = createCrossRefEdges(nodeMap, allNodes, dir, backEdges);
|
|
13566
|
+
const edges = [...treeEdges, ...crossEdges].filter(e => !(e.relation && hiddenRelations.has(e.relation)));
|
|
13567
|
+
// Type filtering hides nodes and anything still pointing at them. It runs
|
|
13568
|
+
// after layout so the remaining nodes keep the positions they'd have anyway —
|
|
13569
|
+
// toggling a filter must not reshuffle the whole graph.
|
|
13570
|
+
const visibleNodes = hiddenTypes.size
|
|
13571
|
+
? nodes.filter(n => !(n.type && hiddenTypes.has(n.type)))
|
|
13572
|
+
: nodes;
|
|
13573
|
+
const visibleIds = new Set(visibleNodes.map(n => n.id));
|
|
13574
|
+
const visibleEdges = hiddenTypes.size
|
|
13575
|
+
? edges.filter(e => visibleIds.has(e.sourceId) && visibleIds.has(e.targetId))
|
|
13576
|
+
: edges;
|
|
13260
13577
|
return {
|
|
13261
|
-
nodes,
|
|
13262
|
-
edges:
|
|
13578
|
+
nodes: visibleNodes,
|
|
13579
|
+
edges: visibleEdges,
|
|
13580
|
+
nodeMap,
|
|
13263
13581
|
width: dir === 'horizontal' ? measured.primary : measured.secondary,
|
|
13264
13582
|
height: dir === 'horizontal' ? measured.secondary : measured.primary,
|
|
13265
13583
|
};
|
|
13266
13584
|
}, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
13585
|
+
constructor() {
|
|
13586
|
+
// Keep the anchor node pinned to its screen position across relayouts. A new
|
|
13587
|
+
// `root` (an expand step) re-runs the tree layout from scratch, which recentres
|
|
13588
|
+
// subtrees and shifts nodes that didn't change — the camera stays put while the
|
|
13589
|
+
// graph slides underneath it. Compensating the pan by the anchor's own delta is
|
|
13590
|
+
// what makes incremental expansion feel stable.
|
|
13591
|
+
effect(() => {
|
|
13592
|
+
const nodeMap = this.layout().nodeMap;
|
|
13593
|
+
const anchorId = this.anchorNodeId() ?? this.selectedNodeId();
|
|
13594
|
+
if (!anchorId) {
|
|
13595
|
+
this.anchorPos = null;
|
|
13596
|
+
return;
|
|
13597
|
+
}
|
|
13598
|
+
const node = nodeMap.get(anchorId);
|
|
13599
|
+
if (!node) {
|
|
13600
|
+
this.anchorPos = null;
|
|
13601
|
+
return;
|
|
13602
|
+
}
|
|
13603
|
+
const prev = this.anchorPos;
|
|
13604
|
+
if (prev && prev.id === anchorId && (prev.x !== node.x || prev.y !== node.y)) {
|
|
13605
|
+
const dx = node.x - prev.x;
|
|
13606
|
+
const dy = node.y - prev.y;
|
|
13607
|
+
// `transform: translate(pan px) scale(z)` with `transform-origin: 0 0`, so
|
|
13608
|
+
// screen = pan + z * layout — a layout delta moves the node by z * delta.
|
|
13609
|
+
untracked(() => {
|
|
13610
|
+
const z = this.zoom() / 100;
|
|
13611
|
+
this.panX.set(this.panX() - dx * z);
|
|
13612
|
+
this.panY.set(this.panY() - dy * z);
|
|
13613
|
+
});
|
|
13614
|
+
}
|
|
13615
|
+
this.anchorPos = { id: anchorId, x: node.x, y: node.y };
|
|
13616
|
+
});
|
|
13617
|
+
}
|
|
13267
13618
|
svgWidth = computed(() => this.layout().width + 80, ...(ngDevMode ? [{ debugName: "svgWidth" }] : /* istanbul ignore next */ []));
|
|
13268
13619
|
svgHeight = computed(() => this.layout().height + 80, ...(ngDevMode ? [{ debugName: "svgHeight" }] : /* istanbul ignore next */ []));
|
|
13269
13620
|
viewBox = computed(() => `0 0 ${this.svgWidth()} ${this.svgHeight()}`, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
|
|
@@ -13283,19 +13634,33 @@ class DependencyViewerComponent {
|
|
|
13283
13634
|
return [];
|
|
13284
13635
|
return this.allOriginalNodes().get(id)?.dependsOn ?? [];
|
|
13285
13636
|
}, ...(ngDevMode ? [{ debugName: "selectedDependsOn" }] : /* istanbul ignore next */ []));
|
|
13637
|
+
// Grouped by label rather than relation, so free-form `relationLabel`s (CALLS,
|
|
13638
|
+
// HAS_COLUMN, …) each get their own entry instead of collapsing into the handful
|
|
13639
|
+
// of built-in relation types they borrow their colour from.
|
|
13286
13640
|
legendItems = computed(() => {
|
|
13287
|
-
const
|
|
13641
|
+
const seen = new Map();
|
|
13288
13642
|
for (const edge of this.layout().edges) {
|
|
13289
|
-
if (edge.
|
|
13290
|
-
|
|
13643
|
+
if (!edge.isCrossRef || !edge.label || seen.has(edge.label))
|
|
13644
|
+
continue;
|
|
13645
|
+
seen.set(edge.label, { label: edge.label, color: edge.color, dashed: edge.dashed });
|
|
13291
13646
|
}
|
|
13292
|
-
return Array.from(
|
|
13293
|
-
relation: r, label: RELATION_STYLES[r].label,
|
|
13294
|
-
color: RELATION_STYLES[r].color, dashed: RELATION_STYLES[r].dashed,
|
|
13295
|
-
}));
|
|
13647
|
+
return Array.from(seen.values());
|
|
13296
13648
|
}, ...(ngDevMode ? [{ debugName: "legendItems" }] : /* istanbul ignore next */ []));
|
|
13649
|
+
typeLegendItems = computed(() => {
|
|
13650
|
+
const colors = this.typeColors();
|
|
13651
|
+
const seen = new Map();
|
|
13652
|
+
for (const node of this.layout().nodes) {
|
|
13653
|
+
if (!node.type || seen.has(node.type) || !colors[node.type])
|
|
13654
|
+
continue;
|
|
13655
|
+
seen.set(node.type, { type: node.type, color: colors[node.type] });
|
|
13656
|
+
}
|
|
13657
|
+
return Array.from(seen.values());
|
|
13658
|
+
}, ...(ngDevMode ? [{ debugName: "typeLegendItems" }] : /* istanbul ignore next */ []));
|
|
13659
|
+
// O(1) against the collected map. Deliberately keyed off the *original* tree, so
|
|
13660
|
+
// a collapsed node keeps its "+" toggle even though it has no children in the
|
|
13661
|
+
// pruned tree that gets laid out.
|
|
13297
13662
|
hasChildren(nodeId) {
|
|
13298
|
-
return !!
|
|
13663
|
+
return !!this.allOriginalNodes().get(nodeId)?.children?.length;
|
|
13299
13664
|
}
|
|
13300
13665
|
isCollapsed(nodeId) {
|
|
13301
13666
|
return this.collapsedIds().has(nodeId);
|
|
@@ -13303,24 +13668,37 @@ class DependencyViewerComponent {
|
|
|
13303
13668
|
getRelationLabel(relation) {
|
|
13304
13669
|
return RELATION_STYLES[relation]?.label ?? relation;
|
|
13305
13670
|
}
|
|
13306
|
-
|
|
13307
|
-
|
|
13671
|
+
// `visited` keeps a cyclic graph from recursing forever here too — this runs
|
|
13672
|
+
// before the layout's own guard sees the tree.
|
|
13673
|
+
pruneCollapsed(node, collapsed, visited) {
|
|
13674
|
+
if (visited.has(node.id) || collapsed.has(node.id) || !node.children?.length) {
|
|
13308
13675
|
return { ...node, children: [] };
|
|
13309
|
-
return { ...node, children: node.children.map(c => this.pruneCollapsed(c, collapsed)) };
|
|
13310
|
-
}
|
|
13311
|
-
findOriginalNode(node, id) {
|
|
13312
|
-
if (node.id === id)
|
|
13313
|
-
return node;
|
|
13314
|
-
for (const child of node.children || []) {
|
|
13315
|
-
const found = this.findOriginalNode(child, id);
|
|
13316
|
-
if (found)
|
|
13317
|
-
return found;
|
|
13318
13676
|
}
|
|
13319
|
-
|
|
13677
|
+
visited.add(node.id);
|
|
13678
|
+
return {
|
|
13679
|
+
...node,
|
|
13680
|
+
children: node.children.map(c => this.pruneCollapsed(c, collapsed, visited)),
|
|
13681
|
+
};
|
|
13320
13682
|
}
|
|
13321
13683
|
selectNode(id, event) {
|
|
13322
13684
|
event.stopPropagation();
|
|
13323
|
-
this.selectedNodeId
|
|
13685
|
+
const next = this.selectedNodeId() === id ? null : id;
|
|
13686
|
+
this.selectedNodeId.set(next);
|
|
13687
|
+
if (!next)
|
|
13688
|
+
return;
|
|
13689
|
+
const original = this.allOriginalNodes().get(id);
|
|
13690
|
+
if (original)
|
|
13691
|
+
this.nodeSelect.emit(original);
|
|
13692
|
+
}
|
|
13693
|
+
expandNode(id, event) {
|
|
13694
|
+
event.stopPropagation();
|
|
13695
|
+
// A double-click also delivers two clicks, which would toggle the selection
|
|
13696
|
+
// straight back off. Re-assert it so the expanded node stays selected — and
|
|
13697
|
+
// stays the anchor the viewport holds onto.
|
|
13698
|
+
this.selectedNodeId.set(id);
|
|
13699
|
+
const original = this.allOriginalNodes().get(id);
|
|
13700
|
+
if (original)
|
|
13701
|
+
this.nodeExpand.emit(original);
|
|
13324
13702
|
}
|
|
13325
13703
|
toggleCollapse(id, event) {
|
|
13326
13704
|
event.stopPropagation();
|
|
@@ -13333,7 +13711,63 @@ class DependencyViewerComponent {
|
|
|
13333
13711
|
deselectNode() { this.selectedNodeId.set(null); }
|
|
13334
13712
|
zoomIn() { this.zoom.update(z => Math.min(z + 25, 300)); }
|
|
13335
13713
|
zoomOut() { this.zoom.update(z => Math.max(z - 25, 25)); }
|
|
13336
|
-
resetZoom() { this.
|
|
13714
|
+
resetZoom() { this.resetView(); }
|
|
13715
|
+
// ── Imperative API ─────────────────────────────────────────────────────────
|
|
13716
|
+
// For deep links (?anchor=<key> → centre on that node) and programmatic
|
|
13717
|
+
// expand/collapse. Mirrors lc-tree-view's expandAll()/collapseAll() surface.
|
|
13718
|
+
/** Centres the viewport on a node. No-op for an id that isn't laid out. */
|
|
13719
|
+
focusNode(id) {
|
|
13720
|
+
const node = this.layout().nodeMap.get(id);
|
|
13721
|
+
if (!node)
|
|
13722
|
+
return;
|
|
13723
|
+
const z = this.zoom() / 100;
|
|
13724
|
+
const el = this.hostEl.nativeElement.querySelector('.dep-viewer__canvas');
|
|
13725
|
+
const viewW = el?.clientWidth ?? 0;
|
|
13726
|
+
const viewH = el?.clientHeight ?? 0;
|
|
13727
|
+
// Invert screen = pan + z * layout for the node's centre.
|
|
13728
|
+
this.panX.set(viewW / 2 - (node.x + node.width / 2) * z);
|
|
13729
|
+
this.panY.set(viewH / 2 - (node.y + node.height / 2) * z);
|
|
13730
|
+
this.anchorPos = { id, x: node.x, y: node.y };
|
|
13731
|
+
}
|
|
13732
|
+
/** Restores the initial zoom and pan. */
|
|
13733
|
+
resetView() {
|
|
13734
|
+
this.zoom.set(100);
|
|
13735
|
+
this.panX.set(40);
|
|
13736
|
+
this.panY.set(40);
|
|
13737
|
+
}
|
|
13738
|
+
/** Expands a collapsed node. */
|
|
13739
|
+
expand(id) {
|
|
13740
|
+
this.collapsedIds.update(set => {
|
|
13741
|
+
if (!set.has(id))
|
|
13742
|
+
return set;
|
|
13743
|
+
const next = new Set(set);
|
|
13744
|
+
next.delete(id);
|
|
13745
|
+
return next;
|
|
13746
|
+
});
|
|
13747
|
+
}
|
|
13748
|
+
/** Collapses a node's sub-tree. */
|
|
13749
|
+
collapse(id) {
|
|
13750
|
+
this.collapsedIds.update(set => {
|
|
13751
|
+
if (set.has(id))
|
|
13752
|
+
return set;
|
|
13753
|
+
const next = new Set(set);
|
|
13754
|
+
next.add(id);
|
|
13755
|
+
return next;
|
|
13756
|
+
});
|
|
13757
|
+
}
|
|
13758
|
+
/** Expands every collapsed node. */
|
|
13759
|
+
expandAll() {
|
|
13760
|
+
this.collapsedIds.set(new Set());
|
|
13761
|
+
}
|
|
13762
|
+
/** Collapses every node that has children. */
|
|
13763
|
+
collapseAll() {
|
|
13764
|
+
const ids = new Set();
|
|
13765
|
+
for (const [id, node] of this.allOriginalNodes()) {
|
|
13766
|
+
if (node.children?.length)
|
|
13767
|
+
ids.add(id);
|
|
13768
|
+
}
|
|
13769
|
+
this.collapsedIds.set(ids);
|
|
13770
|
+
}
|
|
13337
13771
|
onMouseDown(event) {
|
|
13338
13772
|
if (event.button !== 0)
|
|
13339
13773
|
return;
|
|
@@ -13354,16 +13788,29 @@ class DependencyViewerComponent {
|
|
|
13354
13788
|
event.preventDefault();
|
|
13355
13789
|
this.zoom.update(z => event.deltaY < 0 ? Math.min(z + 10, 300) : Math.max(z - 10, 25));
|
|
13356
13790
|
}
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13791
|
+
// `type` is the category axis (Class/Table/Feature/…), `status` the semantic one
|
|
13792
|
+
// (error/warning/…). A resolved type colour wins for the fill; everything else
|
|
13793
|
+
// falls back to the status palette, so consumers that only use `status` are
|
|
13794
|
+
// unaffected.
|
|
13795
|
+
getNodeBg(node) {
|
|
13796
|
+
const typeColor = node.type ? this.typeColors()[node.type] : undefined;
|
|
13797
|
+
return typeColor ?? STATUS_COLORS[node.status].bg;
|
|
13798
|
+
}
|
|
13799
|
+
getNodeBorder(node) {
|
|
13800
|
+
const typeColor = node.type ? this.typeColors()[node.type] : undefined;
|
|
13801
|
+
return typeColor ?? STATUS_COLORS[node.status].border;
|
|
13802
|
+
}
|
|
13803
|
+
getNodeText(node) {
|
|
13804
|
+
const typeColor = node.type ? this.typeColors()[node.type] : undefined;
|
|
13805
|
+
return typeColor ? readableInk(typeColor) : STATUS_COLORS[node.status].text;
|
|
13806
|
+
}
|
|
13360
13807
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13361
|
-
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 });
|
|
13808
|
+
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 -->\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 @if (showEdgeLabels() && 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 <!-- 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 </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}.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 });
|
|
13362
13809
|
}
|
|
13363
13810
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, decorators: [{
|
|
13364
13811
|
type: Component,
|
|
13365
|
-
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"] }]
|
|
13366
|
-
}], 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: [{
|
|
13812
|
+
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.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n @if (showEdgeLabels() && 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 <!-- 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 </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}.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"] }]
|
|
13813
|
+
}], 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: [{
|
|
13367
13814
|
type: HostListener,
|
|
13368
13815
|
args: ['document:mousemove', ['$event']]
|
|
13369
13816
|
}], onMouseUp: [{
|
|
@@ -15972,5 +16419,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
|
|
|
15972
16419
|
* Generated bundle index. Do not edit.
|
|
15973
16420
|
*/
|
|
15974
16421
|
|
|
15975
|
-
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 };
|
|
16422
|
+
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 };
|
|
15976
16423
|
//# sourceMappingURL=life-cockpit-angular-ui-kit.mjs.map
|