@lifecorner/angular-ui-kit 3.32.0 → 3.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/fesm2022/lifecorner-angular-ui-kit-bottom-nav.mjs +2 -2
  2. package/fesm2022/lifecorner-angular-ui-kit-bottom-nav.mjs.map +1 -1
  3. package/fesm2022/lifecorner-angular-ui-kit-chat.mjs +21 -5
  4. package/fesm2022/lifecorner-angular-ui-kit-chat.mjs.map +1 -1
  5. package/fesm2022/lifecorner-angular-ui-kit-combobox.mjs +65 -19
  6. package/fesm2022/lifecorner-angular-ui-kit-combobox.mjs.map +1 -1
  7. package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs +7 -6
  8. package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs.map +1 -1
  9. package/fesm2022/lifecorner-angular-ui-kit-header.mjs +21 -4
  10. package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
  11. package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs +21 -1
  12. package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs.map +1 -1
  13. package/fesm2022/lifecorner-angular-ui-kit-list.mjs +2 -2
  14. package/fesm2022/lifecorner-angular-ui-kit-list.mjs.map +1 -1
  15. package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs +2 -2
  16. package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs.map +1 -1
  17. package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs +21 -4
  18. package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs.map +1 -1
  19. package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs +2 -2
  20. package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs.map +1 -1
  21. package/fesm2022/lifecorner-angular-ui-kit-select.mjs +5 -4
  22. package/fesm2022/lifecorner-angular-ui-kit-select.mjs.map +1 -1
  23. package/fesm2022/lifecorner-angular-ui-kit-sidenav.mjs +4 -4
  24. package/fesm2022/lifecorner-angular-ui-kit-sidenav.mjs.map +1 -1
  25. package/fesm2022/lifecorner-angular-ui-kit-table.mjs +2 -2
  26. package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
  27. package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs +2 -2
  28. package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs.map +1 -1
  29. package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +2 -2
  30. package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
  31. package/fesm2022/lifecorner-angular-ui-kit-toast.mjs +9 -4
  32. package/fesm2022/lifecorner-angular-ui-kit-toast.mjs.map +1 -1
  33. package/fesm2022/lifecorner-angular-ui-kit-tooltip.mjs +7 -0
  34. package/fesm2022/lifecorner-angular-ui-kit-tooltip.mjs.map +1 -1
  35. package/package.json +1 -1
  36. package/src/styles/theme-contrast.spec.ts +14 -6
  37. package/types/lifecorner-angular-ui-kit-bottom-nav.d.ts +3 -2
  38. package/types/lifecorner-angular-ui-kit-chat.d.ts +14 -1
  39. package/types/lifecorner-angular-ui-kit-combobox.d.ts +18 -1
  40. package/types/lifecorner-angular-ui-kit-datepicker.d.ts +4 -1
  41. package/types/lifecorner-angular-ui-kit-header.d.ts +26 -0
  42. package/types/lifecorner-angular-ui-kit-internal-a11y.d.ts +12 -1
  43. package/types/lifecorner-angular-ui-kit-list.d.ts +6 -1
  44. package/types/lifecorner-angular-ui-kit-next-step-card.d.ts +15 -0
  45. package/types/lifecorner-angular-ui-kit-select.d.ts +7 -0
  46. package/types/lifecorner-angular-ui-kit-sidenav.d.ts +3 -1
  47. package/types/lifecorner-angular-ui-kit-toast.d.ts +17 -3
  48. package/types/lifecorner-angular-ui-kit-tooltip.d.ts +7 -2
@@ -180,7 +180,7 @@ var BottomNavComponent = class BottomNavComponent {
180
180
  host: { classAttribute: "lc-bottom-nav-host" },
181
181
  ngImport: i0,
182
182
  template: "<nav [class]=\"navClasses()\" [attr.aria-label]=\"ariaLabel()\">\n <ul class=\"lc-bottom-nav__list\">\n @for (entry of entries(); track entry.item.id) {\n <li class=\"lc-bottom-nav__entry\">\n @if (entry.more && !entry.item.route) {\n <button\n type=\"button\"\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n (click)=\"onMoreButtonClick()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </button>\n } @else {\n <a\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n [routerLink]=\"entry.item.route\"\n [attr.aria-current]=\"entry.current ? 'page' : null\"\n (click)=\"onItemClick(entry)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </a>\n }\n </li>\n }\n </ul>\n</nav>\n\n<ng-template #itemContent let-entry>\n <!-- Icon and badge are decoration: the label and the hidden badge text name the link. -->\n <span class=\"lc-bottom-nav__pill\" aria-hidden=\"true\">\n <lc-icon [name]=\"entry.item.icon\" size=\"md\" [decorative]=\"true\" class=\"lc-bottom-nav__icon\" />\n @if (entry.item.badge; as badge) {\n <span class=\"lc-bottom-nav__badge\">\n <lc-badge [variant]=\"badge.variant || 'default'\" size=\"xs\" [rounded]=\"true\">{{ badge.value }}</lc-badge>\n </span>\n }\n </span>\n <span class=\"lc-bottom-nav__label\">{{ entry.item.label }}</span>\n @if (entry.item.badge) {\n <span class=\"lc-bottom-nav__sr-only\">{{ badgeText(entry.item) }}</span>\n }\n</ng-template>\n",
183
- styles: [":host{display:block}.lc-bottom-nav{--_fg: var(--lc-bottom-nav-fg, var(--color-text-secondary));--_current-bg: var(--lc-bottom-nav-current-bg, var(--color-surface-selected));--_current-fg: var(--lc-bottom-nav-current-fg, var(--color-on-surface-selected, var(--color-link)));--_hover-bg: var(--lc-bottom-nav-hover-bg, var(--color-surface-hover));box-sizing:border-box;background-color:var(--lc-bottom-nav-bg, var(--_bg));color:var(--_fg)}.lc-bottom-nav--docked{--_bg: var(--color-surface);border-top:1px solid var(--lc-bottom-nav-border, var(--color-border));padding-bottom:env(safe-area-inset-bottom,0px);padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-bottom-nav--floating{--_bg: var(--color-floating-bar-surface, var(--color-surface));border:1px solid var(--lc-bottom-nav-border, var(--color-floating-bar-border, var(--color-border)));border-radius:28px;box-shadow:var(--lc-bottom-nav-shadow, var(--shadow-floating-bar, 0 10px 30px -10px rgba(13, 53, 61, .28)))}.lc-bottom-nav--fixed{position:fixed;z-index:var(--lc-bottom-nav-z, 900)}.lc-bottom-nav--fixed.lc-bottom-nav--docked{left:0;right:0;bottom:0}.lc-bottom-nav--fixed.lc-bottom-nav--floating{left:calc(16px + env(safe-area-inset-left,0px));right:calc(16px + env(safe-area-inset-right,0px));bottom:calc(16px + env(safe-area-inset-bottom,0px))}.lc-bottom-nav__list{display:flex;margin:0;padding:4px 8px;list-style:none}.lc-bottom-nav__entry{display:flex;flex:1 1 0;min-width:0}.lc-bottom-nav__item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:4px;min-width:min(64px,100%);min-height:64px;padding-block:6px 2px;padding-inline:0;box-sizing:border-box;border:0;border-radius:20px;background:transparent;color:var(--_fg);font-family:inherit;text-decoration:none;cursor:pointer;-webkit-tap-highlight-color:transparent}.lc-bottom-nav__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}@media(hover:hover){.lc-bottom-nav__item:hover:not(.lc-bottom-nav__item--current) .lc-bottom-nav__pill{background-color:var(--_hover-bg)}}.lc-bottom-nav__pill{position:relative;display:flex;align-items:center;justify-content:center;flex:none;width:56px;max-width:100%;height:32px;border-radius:9999px;background-color:transparent;transition:background-color .2s ease}@media(prefers-reduced-motion:reduce){.lc-bottom-nav__pill{transition:none}}.lc-bottom-nav__icon{display:flex;width:24px;height:24px;color:currentColor}.lc-bottom-nav__label{--_reach: clamp(-4px, 32px - 1.5rem, 8px);max-width:calc(100% + var(--_reach));margin-inline:calc(var(--_reach) / -2);font-size:var(--font-size-xs, .75rem);line-height:1rem;font-weight:var(--font-weight-medium, 500);text-align:center;overflow-wrap:anywhere;hyphens:auto}.lc-bottom-nav__item--current{color:var(--_current-fg)}.lc-bottom-nav__item--current .lc-bottom-nav__pill{background-color:var(--_current-bg)}.lc-bottom-nav__item--current .lc-bottom-nav__label{font-weight:var(--font-weight-bold, 700)}.lc-bottom-nav__item--current .lc-bottom-nav__icon ::ng-deep svg{stroke-width:2.4}.lc-bottom-nav__badge{position:absolute;top:-4px;left:calc(50% + 6px);display:flex;pointer-events:none}.lc-bottom-nav__item--current .lc-bottom-nav__badge ::ng-deep .lc-badge{background-image:linear-gradient(var(--lc-badge-bg),var(--lc-badge-bg));background-color:var(--lc-bottom-nav-bg, var(--_bg))}.lc-bottom-nav__sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media print{.lc-bottom-nav{display:none}}\n"],
183
+ styles: [":host{display:block}.lc-bottom-nav{--_fg: var(--lc-bottom-nav-fg, var(--color-text-secondary));--_current-bg: var(--lc-bottom-nav-current-bg, var(--color-surface-selected));--_current-fg: var(--lc-bottom-nav-current-fg, var(--color-on-surface-selected, var(--color-link)));--_hover-bg: var(--lc-bottom-nav-hover-bg, var(--color-surface-hover));box-sizing:border-box;background-color:var(--lc-bottom-nav-bg, var(--_bg));color:var(--_fg)}.lc-bottom-nav--docked{--_bg: var(--color-surface);border-top:1px solid var(--lc-bottom-nav-border, var(--color-border));padding-bottom:env(safe-area-inset-bottom,0px);padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-bottom-nav--floating{--_bg: var(--color-floating-bar-surface, var(--color-surface));border:1px solid var(--lc-bottom-nav-border, var(--color-floating-bar-border, var(--color-border)));border-radius:28px;box-shadow:var(--lc-bottom-nav-shadow, var(--shadow-floating-bar, 0 10px 30px -10px rgba(13, 53, 61, .28)))}.lc-bottom-nav--fixed{position:fixed;z-index:var(--lc-bottom-nav-z, 900)}.lc-bottom-nav--fixed.lc-bottom-nav--docked{left:0;right:0;bottom:0}.lc-bottom-nav--fixed.lc-bottom-nav--floating{left:calc(16px + env(safe-area-inset-left,0px));right:calc(16px + env(safe-area-inset-right,0px));bottom:calc(16px + env(safe-area-inset-bottom,0px))}.lc-bottom-nav__list{display:flex;margin:0;padding:4px 8px;list-style:none}.lc-bottom-nav__entry{display:flex;flex:1 1 0;min-width:0}.lc-bottom-nav__item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:4px;min-width:min(64px,100%);min-height:64px;padding-block:6px 2px;padding-inline:0;box-sizing:border-box;border:0;border-radius:20px;background:transparent;color:var(--_fg);font-family:inherit;text-decoration:none;cursor:pointer;-webkit-tap-highlight-color:transparent}.lc-bottom-nav__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}@media(hover:hover){.lc-bottom-nav__item:hover:not(.lc-bottom-nav__item--current) .lc-bottom-nav__pill{background-color:var(--_hover-bg)}}.lc-bottom-nav__pill{position:relative;display:flex;align-items:center;justify-content:center;flex:none;width:56px;max-width:100%;height:32px;border-radius:9999px;background-color:transparent;transition:background-color .2s ease}@media(prefers-reduced-motion:reduce){.lc-bottom-nav__pill{transition:none}}.lc-bottom-nav__icon{display:flex;width:24px;height:24px;color:currentColor}.lc-bottom-nav__label{--_reach: clamp(-4px, 32px - 1.5rem, 8px);max-width:calc(100% + var(--_reach));margin-inline:calc(var(--_reach) / -2);font-size:var(--font-size-xs, .75rem);line-height:1rem;font-weight:var(--font-weight-medium, 500);text-align:center;overflow-wrap:anywhere;hyphens:auto}.lc-bottom-nav__item--current{color:var(--_current-fg)}.lc-bottom-nav__item--current .lc-bottom-nav__pill{background-color:var(--_current-bg)}.lc-bottom-nav__item--current .lc-bottom-nav__label{font-weight:var(--font-weight-bold, 700)}.lc-bottom-nav__item--current .lc-bottom-nav__icon ::ng-deep svg{stroke-width:2.4}.lc-bottom-nav__badge{position:absolute;top:-4px;left:calc(50% + 6px);display:flex;pointer-events:none}.lc-bottom-nav__entry:last-child{position:relative}.lc-bottom-nav__entry:last-child .lc-bottom-nav__pill{position:static}.lc-bottom-nav__entry:last-child .lc-bottom-nav__badge{top:2px;right:0;direction:rtl;justify-content:flex-end;justify-content:safe flex-end}.lc-bottom-nav__entry:last-child .lc-bottom-nav__badge>lc-badge{direction:ltr}.lc-bottom-nav__item--current .lc-bottom-nav__badge ::ng-deep .lc-badge{background-image:linear-gradient(var(--lc-badge-bg),var(--lc-badge-bg));background-color:var(--lc-bottom-nav-bg, var(--_bg))}.lc-bottom-nav__sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media print{.lc-bottom-nav{display:none}}\n"],
184
184
  dependencies: [
185
185
  {
186
186
  kind: "directive",
@@ -266,7 +266,7 @@ i0.ɵɵngDeclareClassMetadata({
266
266
  changeDetection: ChangeDetectionStrategy.OnPush,
267
267
  host: { class: "lc-bottom-nav-host" },
268
268
  template: "<nav [class]=\"navClasses()\" [attr.aria-label]=\"ariaLabel()\">\n <ul class=\"lc-bottom-nav__list\">\n @for (entry of entries(); track entry.item.id) {\n <li class=\"lc-bottom-nav__entry\">\n @if (entry.more && !entry.item.route) {\n <button\n type=\"button\"\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n (click)=\"onMoreButtonClick()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </button>\n } @else {\n <a\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n [routerLink]=\"entry.item.route\"\n [attr.aria-current]=\"entry.current ? 'page' : null\"\n (click)=\"onItemClick(entry)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </a>\n }\n </li>\n }\n </ul>\n</nav>\n\n<ng-template #itemContent let-entry>\n <!-- Icon and badge are decoration: the label and the hidden badge text name the link. -->\n <span class=\"lc-bottom-nav__pill\" aria-hidden=\"true\">\n <lc-icon [name]=\"entry.item.icon\" size=\"md\" [decorative]=\"true\" class=\"lc-bottom-nav__icon\" />\n @if (entry.item.badge; as badge) {\n <span class=\"lc-bottom-nav__badge\">\n <lc-badge [variant]=\"badge.variant || 'default'\" size=\"xs\" [rounded]=\"true\">{{ badge.value }}</lc-badge>\n </span>\n }\n </span>\n <span class=\"lc-bottom-nav__label\">{{ entry.item.label }}</span>\n @if (entry.item.badge) {\n <span class=\"lc-bottom-nav__sr-only\">{{ badgeText(entry.item) }}</span>\n }\n</ng-template>\n",
269
- styles: [":host{display:block}.lc-bottom-nav{--_fg: var(--lc-bottom-nav-fg, var(--color-text-secondary));--_current-bg: var(--lc-bottom-nav-current-bg, var(--color-surface-selected));--_current-fg: var(--lc-bottom-nav-current-fg, var(--color-on-surface-selected, var(--color-link)));--_hover-bg: var(--lc-bottom-nav-hover-bg, var(--color-surface-hover));box-sizing:border-box;background-color:var(--lc-bottom-nav-bg, var(--_bg));color:var(--_fg)}.lc-bottom-nav--docked{--_bg: var(--color-surface);border-top:1px solid var(--lc-bottom-nav-border, var(--color-border));padding-bottom:env(safe-area-inset-bottom,0px);padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-bottom-nav--floating{--_bg: var(--color-floating-bar-surface, var(--color-surface));border:1px solid var(--lc-bottom-nav-border, var(--color-floating-bar-border, var(--color-border)));border-radius:28px;box-shadow:var(--lc-bottom-nav-shadow, var(--shadow-floating-bar, 0 10px 30px -10px rgba(13, 53, 61, .28)))}.lc-bottom-nav--fixed{position:fixed;z-index:var(--lc-bottom-nav-z, 900)}.lc-bottom-nav--fixed.lc-bottom-nav--docked{left:0;right:0;bottom:0}.lc-bottom-nav--fixed.lc-bottom-nav--floating{left:calc(16px + env(safe-area-inset-left,0px));right:calc(16px + env(safe-area-inset-right,0px));bottom:calc(16px + env(safe-area-inset-bottom,0px))}.lc-bottom-nav__list{display:flex;margin:0;padding:4px 8px;list-style:none}.lc-bottom-nav__entry{display:flex;flex:1 1 0;min-width:0}.lc-bottom-nav__item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:4px;min-width:min(64px,100%);min-height:64px;padding-block:6px 2px;padding-inline:0;box-sizing:border-box;border:0;border-radius:20px;background:transparent;color:var(--_fg);font-family:inherit;text-decoration:none;cursor:pointer;-webkit-tap-highlight-color:transparent}.lc-bottom-nav__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}@media(hover:hover){.lc-bottom-nav__item:hover:not(.lc-bottom-nav__item--current) .lc-bottom-nav__pill{background-color:var(--_hover-bg)}}.lc-bottom-nav__pill{position:relative;display:flex;align-items:center;justify-content:center;flex:none;width:56px;max-width:100%;height:32px;border-radius:9999px;background-color:transparent;transition:background-color .2s ease}@media(prefers-reduced-motion:reduce){.lc-bottom-nav__pill{transition:none}}.lc-bottom-nav__icon{display:flex;width:24px;height:24px;color:currentColor}.lc-bottom-nav__label{--_reach: clamp(-4px, 32px - 1.5rem, 8px);max-width:calc(100% + var(--_reach));margin-inline:calc(var(--_reach) / -2);font-size:var(--font-size-xs, .75rem);line-height:1rem;font-weight:var(--font-weight-medium, 500);text-align:center;overflow-wrap:anywhere;hyphens:auto}.lc-bottom-nav__item--current{color:var(--_current-fg)}.lc-bottom-nav__item--current .lc-bottom-nav__pill{background-color:var(--_current-bg)}.lc-bottom-nav__item--current .lc-bottom-nav__label{font-weight:var(--font-weight-bold, 700)}.lc-bottom-nav__item--current .lc-bottom-nav__icon ::ng-deep svg{stroke-width:2.4}.lc-bottom-nav__badge{position:absolute;top:-4px;left:calc(50% + 6px);display:flex;pointer-events:none}.lc-bottom-nav__item--current .lc-bottom-nav__badge ::ng-deep .lc-badge{background-image:linear-gradient(var(--lc-badge-bg),var(--lc-badge-bg));background-color:var(--lc-bottom-nav-bg, var(--_bg))}.lc-bottom-nav__sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media print{.lc-bottom-nav{display:none}}\n"]
269
+ styles: [":host{display:block}.lc-bottom-nav{--_fg: var(--lc-bottom-nav-fg, var(--color-text-secondary));--_current-bg: var(--lc-bottom-nav-current-bg, var(--color-surface-selected));--_current-fg: var(--lc-bottom-nav-current-fg, var(--color-on-surface-selected, var(--color-link)));--_hover-bg: var(--lc-bottom-nav-hover-bg, var(--color-surface-hover));box-sizing:border-box;background-color:var(--lc-bottom-nav-bg, var(--_bg));color:var(--_fg)}.lc-bottom-nav--docked{--_bg: var(--color-surface);border-top:1px solid var(--lc-bottom-nav-border, var(--color-border));padding-bottom:env(safe-area-inset-bottom,0px);padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-bottom-nav--floating{--_bg: var(--color-floating-bar-surface, var(--color-surface));border:1px solid var(--lc-bottom-nav-border, var(--color-floating-bar-border, var(--color-border)));border-radius:28px;box-shadow:var(--lc-bottom-nav-shadow, var(--shadow-floating-bar, 0 10px 30px -10px rgba(13, 53, 61, .28)))}.lc-bottom-nav--fixed{position:fixed;z-index:var(--lc-bottom-nav-z, 900)}.lc-bottom-nav--fixed.lc-bottom-nav--docked{left:0;right:0;bottom:0}.lc-bottom-nav--fixed.lc-bottom-nav--floating{left:calc(16px + env(safe-area-inset-left,0px));right:calc(16px + env(safe-area-inset-right,0px));bottom:calc(16px + env(safe-area-inset-bottom,0px))}.lc-bottom-nav__list{display:flex;margin:0;padding:4px 8px;list-style:none}.lc-bottom-nav__entry{display:flex;flex:1 1 0;min-width:0}.lc-bottom-nav__item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:4px;min-width:min(64px,100%);min-height:64px;padding-block:6px 2px;padding-inline:0;box-sizing:border-box;border:0;border-radius:20px;background:transparent;color:var(--_fg);font-family:inherit;text-decoration:none;cursor:pointer;-webkit-tap-highlight-color:transparent}.lc-bottom-nav__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}@media(hover:hover){.lc-bottom-nav__item:hover:not(.lc-bottom-nav__item--current) .lc-bottom-nav__pill{background-color:var(--_hover-bg)}}.lc-bottom-nav__pill{position:relative;display:flex;align-items:center;justify-content:center;flex:none;width:56px;max-width:100%;height:32px;border-radius:9999px;background-color:transparent;transition:background-color .2s ease}@media(prefers-reduced-motion:reduce){.lc-bottom-nav__pill{transition:none}}.lc-bottom-nav__icon{display:flex;width:24px;height:24px;color:currentColor}.lc-bottom-nav__label{--_reach: clamp(-4px, 32px - 1.5rem, 8px);max-width:calc(100% + var(--_reach));margin-inline:calc(var(--_reach) / -2);font-size:var(--font-size-xs, .75rem);line-height:1rem;font-weight:var(--font-weight-medium, 500);text-align:center;overflow-wrap:anywhere;hyphens:auto}.lc-bottom-nav__item--current{color:var(--_current-fg)}.lc-bottom-nav__item--current .lc-bottom-nav__pill{background-color:var(--_current-bg)}.lc-bottom-nav__item--current .lc-bottom-nav__label{font-weight:var(--font-weight-bold, 700)}.lc-bottom-nav__item--current .lc-bottom-nav__icon ::ng-deep svg{stroke-width:2.4}.lc-bottom-nav__badge{position:absolute;top:-4px;left:calc(50% + 6px);display:flex;pointer-events:none}.lc-bottom-nav__entry:last-child{position:relative}.lc-bottom-nav__entry:last-child .lc-bottom-nav__pill{position:static}.lc-bottom-nav__entry:last-child .lc-bottom-nav__badge{top:2px;right:0;direction:rtl;justify-content:flex-end;justify-content:safe flex-end}.lc-bottom-nav__entry:last-child .lc-bottom-nav__badge>lc-badge{direction:ltr}.lc-bottom-nav__item--current .lc-bottom-nav__badge ::ng-deep .lc-badge{background-image:linear-gradient(var(--lc-badge-bg),var(--lc-badge-bg));background-color:var(--lc-bottom-nav-bg, var(--_bg))}.lc-bottom-nav__sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media print{.lc-bottom-nav{display:none}}\n"]
270
270
  }]
271
271
  }],
272
272
  propDecorators: {
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-bottom-nav.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/bottom-nav/bottom-nav.component.ts","../../../../libs/angular-ui-kit/src/lib/bottom-nav/bottom-nav.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { RouterModule } from '@angular/router';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_DOTS } from '@lifecorner/angular-ui-kit/icons';\nimport { BadgeComponent, type BadgeVariant } from '@lifecorner/angular-ui-kit/badge';\n\n/** A count or status on a bottom-nav entry (`BottomNavItem.badge`, `moreBadge`). */\nexport interface BottomNavBadge {\n /** Text or number inside the badge. */\n value: string | number;\n /** Badge colour (`lc-badge` variant). */\n variant?: BadgeVariant;\n}\n\n/**\n * A destination in the bottom navigation bar. A subset of `NavigationItem`, so\n * the side navigation's items can be passed as they are (sections and children\n * are ignored).\n */\nexport interface BottomNavItem {\n /** Unique id. */\n id: string;\n /** Visible label, always shown below the icon. */\n label: string;\n /** Icon name (`lc-icon`), drawn at 24 px. */\n icon: string;\n /** Router path the item links to (`routerLink`). */\n route: string;\n /** Sort order; items without one keep their array order. */\n displayOrder?: number;\n /** Count or status shown on the icon; read out through `badgeAriaLabel`. */\n badge?: BottomNavBadge;\n}\n\n/** Surface of the bar: `docked` spans the bottom edge, `floating` is an inset pill. */\nexport type BottomNavVariant = 'docked' | 'floating';\n\n/** One rendered entry: a destination or the overflow entry (\"More\"). */\ninterface BottomNavEntry {\n item: BottomNavItem;\n current: boolean;\n more: boolean;\n}\n\n/**\n * Bottom navigation bar for phones and tablets: the app's main destinations as\n * links in the thumb zone.\n *\n * - Renders `<nav aria-label><ul><li><a routerLink>` — links, not tabs — with\n * `aria-current=\"page\"` on the current destination.\n * - Items are at least 48 px high and 64 px wide: a 24 px icon over a 12 px\n * label that is always visible and wraps at large text sizes.\n * - The current item sits on a 56 × 32 px pill in `--color-surface-selected`,\n * its icon drawn heavier and its label bold in `--color-on-surface-selected`.\n * - The current item follows `activeRoute` by its first path segment, so a\n * sub-page (`/household/members`) keeps its destination current; `/` only\n * matches itself. Query and fragment are ignored.\n * - More than `maxItems` destinations: the first `maxItems − 1` stay, the\n * rest move behind a \"More\" entry (`moreLabel`, `moreIcon`, `moreRoute`);\n * without `moreRoute` the entry is a button that emits `moreClick`.\n * - `showMore`: \"More\" on request, also when every destination fits (fixed\n * slots); it takes one of the `maxItems` entries.\n * - Tapping the current item emits `reselect` (scroll the page to the top).\n * - `variant=\"floating\"`: an inset pill 16 px from the sides and 16 px above\n * the safe-area inset, radius 28 px, elevated. `docked` (default) spans the\n * bottom edge with a top border and pads the safe-area inset.\n * - Fixed to the viewport by default (`fixed`), below menus and dialogs\n * (`z-index` 900). Reserve space for it at the end of the page (see\n * `lc-app-shell`, which does that for you).\n * - Light and dark from the semantic tokens; no hover-only information;\n * transitions are off under `prefers-reduced-motion`.\n *\n * @example\n * ```html\n * <lc-bottom-nav\n * variant=\"floating\"\n * ariaLabel=\"Hauptnavigation\"\n * [items]=\"[\n * { id: 'today', label: 'Heute', icon: 'sun', route: '/' },\n * { id: 'household', label: 'Haushalt', icon: 'home', route: '/household' }\n * ]\"\n * [activeRoute]=\"router.url\"\n * (reselect)=\"scrollToTop()\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-bottom-nav',\n standalone: true,\n imports: [NgTemplateOutlet, RouterModule, IconComponent, BadgeComponent],\n // The default \"more\" glyph (moreIcon); the items' glyphs are the app's.\n viewProviders: [ɵprovideKitIcons([ICON_DOTS])],\n templateUrl: './bottom-nav.component.html',\n styleUrl: './bottom-nav.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'lc-bottom-nav-host',\n },\n})\nexport class BottomNavComponent {\n /** Destinations, left to right (sorted by `displayOrder` when given). */\n readonly items = input<readonly BottomNavItem[]>([]);\n\n /**\n * The current URL (e.g. `router.url`). The item whose route matches its\n * first path segment is current; `/` matches only itself.\n */\n readonly activeRoute = input('');\n\n /** Accessible name of the `<nav>` landmark. */\n readonly ariaLabel = input('Main navigation');\n\n /** `docked` spans the bottom edge; `floating` is the inset, elevated pill (LK-5). */\n readonly variant = input<BottomNavVariant>('docked');\n\n /**\n * Pin the bar to the bottom of the viewport. Turn off to render it in the\n * document flow (previews, tests, a custom container).\n */\n readonly fixed = input(true);\n\n /**\n * Most entries the bar shows, \"More\" included. With more destinations, or\n * with `showMore`, the first `maxItems − 1` stay and the rest go behind the\n * \"More\" entry.\n *\n * @default 5\n */\n readonly maxItems = input(5);\n\n /**\n * Always end the bar with the \"More\" entry (`moreLabel`, `moreIcon`, and\n * `moreRoute` or `moreClick`), even when every destination fits: for a bar\n * with fixed slots, such as two destinations plus \"More\". \"More\" takes one\n * of the `maxItems` entries, so the first `maxItems − 1` destinations stay\n * and any others move behind it, as on overflow.\n *\n * @default false\n */\n readonly showMore = input(false);\n\n /**\n * Label of the \"More\" entry (on overflow or with `showMore`).\n *\n * @default 'More'\n */\n readonly moreLabel = input('More');\n\n /**\n * Icon of the \"More\" entry (on overflow or with `showMore`).\n *\n * @default 'dots'\n */\n readonly moreIcon = input('dots');\n\n /**\n * Route of the \"More\" entry (on overflow or with `showMore`): a page that\n * lists the remaining destinations. Empty renders the entry as a button that\n * emits `moreClick`.\n *\n * @default ''\n */\n readonly moreRoute = input('');\n\n /**\n * Badge on the \"More\" entry (on overflow or with `showMore`), e.g. the open\n * tasks on the pages behind it: drawn like an item badge and read after the\n * label through `badgeAriaLabel`. The kit doesn't add up the hidden\n * destinations' badges; pass the value to show. It sits on the bar's last\n * entry, so keep it to two characters (`'9+'`). `null` or `undefined`: no\n * badge.\n *\n * @default null\n */\n readonly moreBadge = input<BottomNavBadge | null | undefined>(null);\n\n /**\n * Text read after an item's label, and after the \"More\" label with\n * `moreBadge`, when it has a badge; `{value}` is replaced with the badge\n * value (e.g. \"3 new\").\n *\n * @default '{value} new'\n */\n readonly badgeAriaLabel = input('{value} new');\n\n /** The current item was tapped again (e.g. scroll the page to the top). */\n readonly reselect = output<BottomNavItem>();\n\n /** The \"More\" entry was tapped while it has no `moreRoute`. */\n readonly moreClick = output<void>();\n\n /** Destinations in display order. */\n private readonly sortedItems = computed(() =>\n [...this.items()].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)),\n );\n\n /** Path of `activeRoute` without query and fragment. */\n private readonly activePath = computed(() => this.activeRoute().split(/[?#]/)[0] || '/');\n\n /**\n * Entries as rendered: destinations plus, on overflow or with `showMore`,\n * the \"More\" entry, which then takes one of the `maxItems` entries.\n */\n protected readonly entries = computed<BottomNavEntry[]>(() => {\n const items = this.sortedItems();\n const max = Math.max(1, Math.floor(this.maxItems()));\n if (!this.showMore() && items.length <= max) {\n return items.map((item) => ({ item, current: this.matches(item.route), more: false }));\n }\n const visible = items.slice(0, max - 1);\n const hidden = items.slice(max - 1);\n const moreRoute = this.moreRoute();\n const moreItem: BottomNavItem = {\n id: '__more',\n label: this.moreLabel(),\n icon: this.moreIcon(),\n route: moreRoute,\n badge: this.moreBadge() ?? undefined,\n };\n const moreCurrent =\n (moreRoute !== '' && this.matches(moreRoute)) || hidden.some((item) => this.matches(item.route));\n return [\n ...visible.map((item) => ({ item, current: this.matches(item.route), more: false })),\n { item: moreItem, current: moreCurrent, more: true },\n ];\n });\n\n protected readonly navClasses = computed(() => {\n const classes = ['lc-bottom-nav', `lc-bottom-nav--${this.variant()}`];\n if (this.fixed()) classes.push('lc-bottom-nav--fixed');\n return classes.join(' ');\n });\n\n /** Visually hidden badge text for screen readers. */\n protected badgeText(item: BottomNavItem): string {\n return item.badge ? this.badgeAriaLabel().replace('{value}', String(item.badge.value)) : '';\n }\n\n protected onItemClick(entry: BottomNavEntry): void {\n if (entry.current && !entry.more) {\n this.reselect.emit(entry.item);\n }\n }\n\n protected onMoreButtonClick(): void {\n this.moreClick.emit();\n }\n\n /** First-segment match: `/household/members` keeps `/household` current. */\n private matches(route: string): boolean {\n const path = this.activePath();\n const target = route.split(/[?#]/)[0] || '/';\n if (target === '/') return path === '/';\n const normalized = target.endsWith('/') ? target.slice(0, -1) : target;\n return path === normalized || path.startsWith(`${normalized}/`);\n }\n}\n","<nav [class]=\"navClasses()\" [attr.aria-label]=\"ariaLabel()\">\n <ul class=\"lc-bottom-nav__list\">\n @for (entry of entries(); track entry.item.id) {\n <li class=\"lc-bottom-nav__entry\">\n @if (entry.more && !entry.item.route) {\n <button\n type=\"button\"\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n (click)=\"onMoreButtonClick()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </button>\n } @else {\n <a\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n [routerLink]=\"entry.item.route\"\n [attr.aria-current]=\"entry.current ? 'page' : null\"\n (click)=\"onItemClick(entry)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </a>\n }\n </li>\n }\n </ul>\n</nav>\n\n<ng-template #itemContent let-entry>\n <!-- Icon and badge are decoration: the label and the hidden badge text name the link. -->\n <span class=\"lc-bottom-nav__pill\" aria-hidden=\"true\">\n <lc-icon [name]=\"entry.item.icon\" size=\"md\" [decorative]=\"true\" class=\"lc-bottom-nav__icon\" />\n @if (entry.item.badge; as badge) {\n <span class=\"lc-bottom-nav__badge\">\n <lc-badge [variant]=\"badge.variant || 'default'\" size=\"xs\" [rounded]=\"true\">{{ badge.value }}</lc-badge>\n </span>\n }\n </span>\n <span class=\"lc-bottom-nav__label\">{{ entry.item.label }}</span>\n @if (entry.item.badge) {\n <span class=\"lc-bottom-nav__sr-only\">{{ badgeText(entry.item) }}</span>\n }\n</ng-template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-bottom-nav.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/bottom-nav/bottom-nav.component.ts","../../../../libs/angular-ui-kit/src/lib/bottom-nav/bottom-nav.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { RouterModule } from '@angular/router';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_DOTS } from '@lifecorner/angular-ui-kit/icons';\nimport { BadgeComponent, type BadgeVariant } from '@lifecorner/angular-ui-kit/badge';\n\n/** A count or status on a bottom-nav entry (`BottomNavItem.badge`, `moreBadge`). */\nexport interface BottomNavBadge {\n /** Text or number inside the badge. */\n value: string | number;\n /** Badge colour (`lc-badge` variant). */\n variant?: BadgeVariant;\n}\n\n/**\n * A destination in the bottom navigation bar. A subset of `NavigationItem`, so\n * the side navigation's items can be passed as they are (sections and children\n * are ignored).\n */\nexport interface BottomNavItem {\n /** Unique id. */\n id: string;\n /** Visible label, always shown below the icon. */\n label: string;\n /** Icon name (`lc-icon`), drawn at 24 px. */\n icon: string;\n /** Router path the item links to (`routerLink`). */\n route: string;\n /** Sort order; items without one keep their array order. */\n displayOrder?: number;\n /** Count or status shown on the icon; read out through `badgeAriaLabel`. */\n badge?: BottomNavBadge;\n}\n\n/** Surface of the bar: `docked` spans the bottom edge, `floating` is an inset pill. */\nexport type BottomNavVariant = 'docked' | 'floating';\n\n/** One rendered entry: a destination or the overflow entry (\"More\"). */\ninterface BottomNavEntry {\n item: BottomNavItem;\n current: boolean;\n more: boolean;\n}\n\n/**\n * Bottom navigation bar for phones and tablets: the app's main destinations as\n * links in the thumb zone.\n *\n * - Renders `<nav aria-label><ul><li><a routerLink>` — links, not tabs — with\n * `aria-current=\"page\"` on the current destination.\n * - Items are at least 48 px high and 64 px wide: a 24 px icon over a 12 px\n * label that is always visible and wraps at large text sizes.\n * - The current item sits on a 56 × 32 px pill in `--color-surface-selected`,\n * its icon drawn heavier and its label bold in `--color-on-surface-selected`.\n * - The current item follows `activeRoute` by its first path segment, so a\n * sub-page (`/household/members`) keeps its destination current; `/` only\n * matches itself. Query and fragment are ignored.\n * - More than `maxItems` destinations: the first `maxItems − 1` stay, the\n * rest move behind a \"More\" entry (`moreLabel`, `moreIcon`, `moreRoute`);\n * without `moreRoute` the entry is a button that emits `moreClick`.\n * - `showMore`: \"More\" on request, also when every destination fits (fixed\n * slots); it takes one of the `maxItems` entries.\n * - Tapping the current item emits `reselect` (scroll the page to the top).\n * - `variant=\"floating\"`: an inset pill 16 px from the sides and 16 px above\n * the safe-area inset, radius 28 px, elevated. `docked` (default) spans the\n * bottom edge with a top border and pads the safe-area inset.\n * - Fixed to the viewport by default (`fixed`), below menus and dialogs\n * (`z-index` 900). Reserve space for it at the end of the page (see\n * `lc-app-shell`, which does that for you).\n * - Light and dark from the semantic tokens; no hover-only information;\n * transitions are off under `prefers-reduced-motion`.\n *\n * @example\n * ```html\n * <lc-bottom-nav\n * variant=\"floating\"\n * ariaLabel=\"Hauptnavigation\"\n * [items]=\"[\n * { id: 'today', label: 'Heute', icon: 'sun', route: '/' },\n * { id: 'household', label: 'Haushalt', icon: 'home', route: '/household' }\n * ]\"\n * [activeRoute]=\"router.url\"\n * (reselect)=\"scrollToTop()\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-bottom-nav',\n standalone: true,\n imports: [NgTemplateOutlet, RouterModule, IconComponent, BadgeComponent],\n // The default \"more\" glyph (moreIcon); the items' glyphs are the app's.\n viewProviders: [ɵprovideKitIcons([ICON_DOTS])],\n templateUrl: './bottom-nav.component.html',\n styleUrl: './bottom-nav.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'lc-bottom-nav-host',\n },\n})\nexport class BottomNavComponent {\n /** Destinations, left to right (sorted by `displayOrder` when given). */\n readonly items = input<readonly BottomNavItem[]>([]);\n\n /**\n * The current URL (e.g. `router.url`). The item whose route matches its\n * first path segment is current; `/` matches only itself.\n */\n readonly activeRoute = input('');\n\n /** Accessible name of the `<nav>` landmark. */\n readonly ariaLabel = input('Main navigation');\n\n /** `docked` spans the bottom edge; `floating` is the inset, elevated pill (LK-5). */\n readonly variant = input<BottomNavVariant>('docked');\n\n /**\n * Pin the bar to the bottom of the viewport. Turn off to render it in the\n * document flow (previews, tests, a custom container).\n */\n readonly fixed = input(true);\n\n /**\n * Most entries the bar shows, \"More\" included. With more destinations, or\n * with `showMore`, the first `maxItems − 1` stay and the rest go behind the\n * \"More\" entry.\n *\n * @default 5\n */\n readonly maxItems = input(5);\n\n /**\n * Always end the bar with the \"More\" entry (`moreLabel`, `moreIcon`, and\n * `moreRoute` or `moreClick`), even when every destination fits: for a bar\n * with fixed slots, such as two destinations plus \"More\". \"More\" takes one\n * of the `maxItems` entries, so the first `maxItems − 1` destinations stay\n * and any others move behind it, as on overflow.\n *\n * @default false\n */\n readonly showMore = input(false);\n\n /**\n * Label of the \"More\" entry (on overflow or with `showMore`).\n *\n * @default 'More'\n */\n readonly moreLabel = input('More');\n\n /**\n * Icon of the \"More\" entry (on overflow or with `showMore`).\n *\n * @default 'dots'\n */\n readonly moreIcon = input('dots');\n\n /**\n * Route of the \"More\" entry (on overflow or with `showMore`): a page that\n * lists the remaining destinations. Empty renders the entry as a button that\n * emits `moreClick`.\n *\n * @default ''\n */\n readonly moreRoute = input('');\n\n /**\n * Badge on the \"More\" entry (on overflow or with `showMore`), e.g. the open\n * tasks on the pages behind it: drawn like an item badge and read after the\n * label through `badgeAriaLabel`. The kit doesn't add up the hidden\n * destinations' badges; pass the value to show. It sits on the bar's last\n * entry; values up to three characters (`'99+'`) fit: on the last entry a\n * badge that would pass the entry's edge moves left until it ends there\n * (3.33). `null` or `undefined`: no badge.\n *\n * @default null\n */\n readonly moreBadge = input<BottomNavBadge | null | undefined>(null);\n\n /**\n * Text read after an item's label, and after the \"More\" label with\n * `moreBadge`, when it has a badge; `{value}` is replaced with the badge\n * value (e.g. \"3 new\").\n *\n * @default '{value} new'\n */\n readonly badgeAriaLabel = input('{value} new');\n\n /** The current item was tapped again (e.g. scroll the page to the top). */\n readonly reselect = output<BottomNavItem>();\n\n /** The \"More\" entry was tapped while it has no `moreRoute`. */\n readonly moreClick = output<void>();\n\n /** Destinations in display order. */\n private readonly sortedItems = computed(() =>\n [...this.items()].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)),\n );\n\n /** Path of `activeRoute` without query and fragment. */\n private readonly activePath = computed(() => this.activeRoute().split(/[?#]/)[0] || '/');\n\n /**\n * Entries as rendered: destinations plus, on overflow or with `showMore`,\n * the \"More\" entry, which then takes one of the `maxItems` entries.\n */\n protected readonly entries = computed<BottomNavEntry[]>(() => {\n const items = this.sortedItems();\n const max = Math.max(1, Math.floor(this.maxItems()));\n if (!this.showMore() && items.length <= max) {\n return items.map((item) => ({ item, current: this.matches(item.route), more: false }));\n }\n const visible = items.slice(0, max - 1);\n const hidden = items.slice(max - 1);\n const moreRoute = this.moreRoute();\n const moreItem: BottomNavItem = {\n id: '__more',\n label: this.moreLabel(),\n icon: this.moreIcon(),\n route: moreRoute,\n badge: this.moreBadge() ?? undefined,\n };\n const moreCurrent =\n (moreRoute !== '' && this.matches(moreRoute)) || hidden.some((item) => this.matches(item.route));\n return [\n ...visible.map((item) => ({ item, current: this.matches(item.route), more: false })),\n { item: moreItem, current: moreCurrent, more: true },\n ];\n });\n\n protected readonly navClasses = computed(() => {\n const classes = ['lc-bottom-nav', `lc-bottom-nav--${this.variant()}`];\n if (this.fixed()) classes.push('lc-bottom-nav--fixed');\n return classes.join(' ');\n });\n\n /** Visually hidden badge text for screen readers. */\n protected badgeText(item: BottomNavItem): string {\n return item.badge ? this.badgeAriaLabel().replace('{value}', String(item.badge.value)) : '';\n }\n\n protected onItemClick(entry: BottomNavEntry): void {\n if (entry.current && !entry.more) {\n this.reselect.emit(entry.item);\n }\n }\n\n protected onMoreButtonClick(): void {\n this.moreClick.emit();\n }\n\n /** First-segment match: `/household/members` keeps `/household` current. */\n private matches(route: string): boolean {\n const path = this.activePath();\n const target = route.split(/[?#]/)[0] || '/';\n if (target === '/') return path === '/';\n const normalized = target.endsWith('/') ? target.slice(0, -1) : target;\n return path === normalized || path.startsWith(`${normalized}/`);\n }\n}\n","<nav [class]=\"navClasses()\" [attr.aria-label]=\"ariaLabel()\">\n <ul class=\"lc-bottom-nav__list\">\n @for (entry of entries(); track entry.item.id) {\n <li class=\"lc-bottom-nav__entry\">\n @if (entry.more && !entry.item.route) {\n <button\n type=\"button\"\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n (click)=\"onMoreButtonClick()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </button>\n } @else {\n <a\n class=\"lc-bottom-nav__item\"\n [class.lc-bottom-nav__item--current]=\"entry.current\"\n [routerLink]=\"entry.item.route\"\n [attr.aria-current]=\"entry.current ? 'page' : null\"\n (click)=\"onItemClick(entry)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" [ngTemplateOutletContext]=\"{ $implicit: entry }\" />\n </a>\n }\n </li>\n }\n </ul>\n</nav>\n\n<ng-template #itemContent let-entry>\n <!-- Icon and badge are decoration: the label and the hidden badge text name the link. -->\n <span class=\"lc-bottom-nav__pill\" aria-hidden=\"true\">\n <lc-icon [name]=\"entry.item.icon\" size=\"md\" [decorative]=\"true\" class=\"lc-bottom-nav__icon\" />\n @if (entry.item.badge; as badge) {\n <span class=\"lc-bottom-nav__badge\">\n <lc-badge [variant]=\"badge.variant || 'default'\" size=\"xs\" [rounded]=\"true\">{{ badge.value }}</lc-badge>\n </span>\n }\n </span>\n <span class=\"lc-bottom-nav__label\">{{ entry.item.label }}</span>\n @if (entry.item.badge) {\n <span class=\"lc-bottom-nav__sr-only\">{{ badgeText(entry.item) }}</span>\n }\n</ng-template>\n"],"mappings":";;;;;;;;AAoGA,IAAa,qBAAb,MAAa,mBAAkB;CAEpB,QAAQ,MAAgC,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAM3C,cAAc,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAGvB,YAAY,MAAM,mBAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAGpC,UAAU,MAAwB,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAM3C,QAAQ,MAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CASnB,WAAW,MAAM,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAWnB,WAAW,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAOvB,YAAY,MAAM,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAOzB,WAAW,MAAM,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CASxB,YAAY,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAarB,YAAY,MAAyC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAS1D,iBAAiB,MAAM,eAAc,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,WAAW,OAAM;CAGjB,YAAY,OAAM;CAGV,cAAc,eAC7B,CAAC,GAAG,KAAK,MAAK,CAAE,CAAC,CAAC,MAAM,GAAG,OAAO,EAAE,gBAAgB,MAAM,EAAE,gBAAgB,EAAE,GAC/E,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAGgB,aAAa,eAAe,KAAK,YAAW,CAAE,CAAC,MAAM,MAAM,CAAC,CAAC,MAAM,KAAI,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAMrE,UAAU,eAAgC;EAC3D,MAAM,QAAQ,KAAK,YAAW;EAC9B,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,SAAQ,CAAE,CAAC;EACnD,IAAI,CAAC,KAAK,SAAQ,KAAM,MAAM,UAAU,KACtC,OAAO,MAAM,KAAK,UAAU;GAAE;GAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;GAAG,MAAM;EAAK,EAAG;EAEvF,MAAM,UAAU,MAAM,MAAM,GAAG,MAAM,CAAC;EACtC,MAAM,SAAS,MAAM,MAAM,MAAM,CAAC;EAClC,MAAM,YAAY,KAAK,UAAS;EAChC,MAAM,WAA0B;GAC9B,IAAI;GACJ,OAAO,KAAK,UAAS;GACrB,MAAM,KAAK,SAAQ;GACnB,OAAO;GACP,OAAO,KAAK,UAAS,KAAM;EAC5B;EACD,MAAM,cACH,cAAc,MAAM,KAAK,QAAQ,SAAS,KAAM,OAAO,MAAM,SAAS,KAAK,QAAQ,KAAK,KAAK,CAAC;EACjG,OAAO,CACL,GAAG,QAAQ,KAAK,UAAU;GAAE;GAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;GAAG,MAAM;EAAK,EAAG,GACnF;GAAE,MAAM;GAAU,SAAS;GAAa,MAAM;EAAI,CACnD;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,aAAa,eAAc;EAC5C,MAAM,UAAU,CAAC,iBAAiB,kBAAkB,KAAK,QAAO,GAAI;EACpE,IAAI,KAAK,MAAK,GAAI,QAAQ,KAAK,sBAAsB;EACrD,OAAO,QAAQ,KAAK,GAAG;CACzB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGQ,UAAU,MAAmB;EACrC,OAAO,KAAK,QAAQ,KAAK,eAAc,CAAE,CAAC,QAAQ,WAAW,OAAO,KAAK,MAAM,KAAK,CAAC,IAAI;CAC3F;CAEU,YAAY,OAAqB;EACzC,IAAI,MAAM,WAAW,CAAC,MAAM,MAC1B,KAAK,SAAS,KAAK,MAAM,IAAI;CAEjC;CAEU,oBAAiB;EACzB,KAAK,UAAU,KAAI;CACrB;CAGQ,QAAQ,OAAa;EAC3B,MAAM,OAAO,KAAK,WAAU;EAC5B,MAAM,SAAS,MAAM,MAAM,MAAM,CAAC,CAAC,MAAM;EACzC,IAAI,WAAW,KAAK,OAAO,SAAS;EACpC,MAAM,aAAa,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI;EAChE,OAAO,SAAS,cAAc,KAAK,WAAW,GAAG,WAAU,EAAG;CAChE;CA7JW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAlB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAkB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,aAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,UAAA;GAAA,WAAA;EAAA;EAAA,MAAA,EAAA,gBAAA,qBAAA;EAAA,UAAA;EAAA,UCpG/B;EA4CA,QAAA,CAAA,+0HAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MD8CY;IAAgB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAY;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAa,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAc,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;EAAA,eAExD,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;EAAC,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAQnC,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAkB,YAAA,CAAA;EAb9B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAe,YACb;GAAI,SACP;IAAC;IAAkB;IAAc;IAAe;GAAc;GAAC,eAEzD,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;GAAC,iBAG7B,wBAAwB;GAAM,MACzC,EACJ,OAAO,qBACR;GAAA,UAAA;GAAA,QAAA,CAAA,+0HAAA;EAAA,CAAA"}
@@ -1,7 +1,8 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, ContentChild, DestroyRef, Injector, NgZone, ViewChild, afterNextRender, computed, effect, inject, input, output, signal, viewChild } from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, ContentChild, DestroyRef, ElementRef, Injector, NgZone, ViewChild, afterNextRender, computed, effect, inject, input, output, signal, viewChild } from "@angular/core";
3
3
  import { NgTemplateOutlet } from "@angular/common";
4
4
  import { MarkdownComponent } from "@lifecorner/angular-ui-kit/markdown";
5
+ import { closestLang } from "@lifecorner/angular-ui-kit/internal/a11y";
5
6
  import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/icon";
6
7
  import { ICON_CHECK_CIRCLE, ICON_CIRCLE_X, ICON_EXCLAMATION_TRIANGLE, ICON_INFORMATION_CIRCLE } from "@lifecorner/angular-ui-kit/icons";
7
8
  var ChatComponent = class ChatComponent {
@@ -124,11 +125,26 @@ var ChatComponent = class ChatComponent {
124
125
  return list.filter((a) => a.id !== id);
125
126
  });
126
127
  }
128
+ host = inject(ElementRef);
127
129
  formatBytes(size) {
128
130
  if (!size && size !== 0) return "";
129
131
  if (size < 1024) return `${size} B`;
130
- if (size < 1048576) return `${(size / 1024).toFixed(1)} KB`;
131
- return `${(size / 1048576).toFixed(1)} MB`;
132
+ const decimal = this.decimalFormat();
133
+ if (size < 1048576) return `${decimal.format(size / 1024)} KB`;
134
+ return `${decimal.format(size / 1048576)} MB`;
135
+ }
136
+ decimalFormat() {
137
+ const options = {
138
+ minimumFractionDigits: 1,
139
+ maximumFractionDigits: 1,
140
+ useGrouping: false
141
+ };
142
+ const lang = closestLang(this.host.nativeElement);
143
+ try {
144
+ return new Intl.NumberFormat(lang || "en", options);
145
+ } catch {
146
+ return new Intl.NumberFormat("en", options);
147
+ }
132
148
  }
133
149
  isImage(att) {
134
150
  return !!att.type?.startsWith("image/");
@@ -470,7 +486,7 @@ var ChatComponent = class ChatComponent {
470
486
  ],
471
487
  ngImport: i0,
472
488
  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\">{{ streamingLabel() }}</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 <!-- Announces the latest completed non-user turn (see `announcement`). -->\n <div class=\"lc-chat__sr-only lc-chat__announcer\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</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]=\"removeAttachmentAriaLabel() + ' ' + 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 <label class=\"lc-chat__sr-only\" [for]=\"inputId\">{{ inputLabel() }}</label>\n <textarea\n #composerInput\n [id]=\"inputId\"\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 [attr.aria-label]=\"attachAriaLabel()\"\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 [attr.aria-label]=\"sendAriaLabel()\"\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",
473
- 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-on-primary-subtle);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{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__send-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__file-input{display:none}.lc-chat__attach-btn{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__attach-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.875rem .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{position:relative;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:before{content:\"\";position:absolute;inset:calc((1.25rem - var(--size-min-touch-height, 2.75rem)) / 2)}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__pending-remove:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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}@media(forced-colors:active){.lc-chat__send-btn:focus-visible,.lc-chat__attach-btn:focus-visible,.lc-chat__pending-remove:focus-visible,.lc-chat__attachment:focus-visible{outline-color:Highlight}}\n"],
489
+ 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-on-primary-subtle);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{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__send-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__file-input{display:none}.lc-chat__attach-btn{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__attach-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.875rem .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;white-space:nowrap;flex-shrink:0}.lc-chat__pending-remove{position:relative;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:before{content:\"\";position:absolute;inset:calc((1.25rem - var(--size-min-touch-height, 2.75rem)) / 2)}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__pending-remove:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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}@media(forced-colors:active){.lc-chat__send-btn:focus-visible,.lc-chat__attach-btn:focus-visible,.lc-chat__pending-remove:focus-visible,.lc-chat__attachment:focus-visible{outline-color:Highlight}}\n"],
474
490
  dependencies: [
475
491
  {
476
492
  kind: "directive",
@@ -559,7 +575,7 @@ i0.ɵɵngDeclareClassMetadata({
559
575
  ])],
560
576
  changeDetection: ChangeDetectionStrategy.OnPush,
561
577
  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\">{{ streamingLabel() }}</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 <!-- Announces the latest completed non-user turn (see `announcement`). -->\n <div class=\"lc-chat__sr-only lc-chat__announcer\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</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]=\"removeAttachmentAriaLabel() + ' ' + 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 <label class=\"lc-chat__sr-only\" [for]=\"inputId\">{{ inputLabel() }}</label>\n <textarea\n #composerInput\n [id]=\"inputId\"\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 [attr.aria-label]=\"attachAriaLabel()\"\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 [attr.aria-label]=\"sendAriaLabel()\"\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",
562
- 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-on-primary-subtle);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{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__send-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__file-input{display:none}.lc-chat__attach-btn{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__attach-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.875rem .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{position:relative;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:before{content:\"\";position:absolute;inset:calc((1.25rem - var(--size-min-touch-height, 2.75rem)) / 2)}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__pending-remove:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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}@media(forced-colors:active){.lc-chat__send-btn:focus-visible,.lc-chat__attach-btn:focus-visible,.lc-chat__pending-remove:focus-visible,.lc-chat__attachment:focus-visible{outline-color:Highlight}}\n"]
578
+ 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-on-primary-subtle);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{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__send-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__file-input{display:none}.lc-chat__attach-btn{position:relative;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:before{content:\"\";position:absolute;inset:calc((2rem - var(--size-min-touch-height, 2.75rem)) / 2)}.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__attach-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-chat__pending-attachments{display:flex;flex-wrap:wrap;gap:.875rem .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;white-space:nowrap;flex-shrink:0}.lc-chat__pending-remove{position:relative;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:before{content:\"\";position:absolute;inset:calc((1.25rem - var(--size-min-touch-height, 2.75rem)) / 2)}.lc-chat__pending-remove:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-chat__pending-remove:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.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}@media(forced-colors:active){.lc-chat__send-btn:focus-visible,.lc-chat__attach-btn:focus-visible,.lc-chat__pending-remove:focus-visible,.lc-chat__attachment:focus-visible{outline-color:Highlight}}\n"]
563
579
  }]
564
580
  }],
565
581
  ctorParameters: () => [],
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-chat.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/chat/chat.component.ts","../../../../libs/angular-ui-kit/src/lib/chat/chat.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Injector,\n NgZone,\n TemplateRef,\n ContentChild,\n ViewChild,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { MarkdownComponent } from '@lifecorner/angular-ui-kit/markdown';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n} from '@lifecorner/angular-ui-kit/icons';\n\nexport type ChatMessageRole = 'user' | 'agent' | 'system';\n\n/**\n * Semantic status of a chat message — orthogonal to `role` (role = *who*\n * speaks; status = *what kind* of message). Reuses the semantic vocabulary of\n * `lc-alert` / `lc-callout` and drives the rail dot/accent colour + a status\n * icon. `'default'` (or omitted) reproduces the role-coloured behaviour exactly.\n */\nexport type ChatMessageStatus = 'default' | 'info' | 'success' | 'warning' | 'error';\n\n/**\n * Controls which messages are rendered as Markdown via `<lc-markdown>`.\n * - `false` (default): plain text rendering (legacy behavior)\n * - `true` / `'all'`: render every role as Markdown\n * - `'agent'`: only agent messages are rendered as Markdown (recommended)\n */\nexport type ChatRenderMarkdown = boolean | 'agent' | 'all';\n\n/**\n * Represents a file attached to a chat message or pending in the input.\n *\n * The `file` reference is only available for attachments produced by the\n * input (i.e. emitted via `messageSend` / `fileAttach`). Attachments\n * coming from the server or message history typically rely on `url` only.\n */\nexport interface ChatAttachment {\n id: string;\n name: string;\n /** MIME type, e.g. `image/png`, `application/pdf`. */\n type?: string;\n /** Size in bytes. */\n size?: number;\n /** Object URL or remote URL to preview / download the file. */\n url?: string;\n /** Raw browser File reference (set for newly uploaded files). */\n file?: File;\n}\n\nexport interface ChatMessage {\n id: string;\n role: ChatMessageRole;\n content: string;\n timestamp?: Date | string;\n avatar?: string;\n name?: string;\n streaming?: boolean;\n /**\n * Semantic status — colours the rail dot/accent and shows a status icon.\n * Independent of `role`: a `role: 'agent'` message can be `status: 'error'`\n * (failed reply) or `status: 'success'` (tool finished). Defaults to\n * `'default'`, which reproduces today's role-coloured behaviour exactly.\n * An `'error'` message never pulses and is announced assertively (`role=\"alert\"`).\n */\n status?: ChatMessageStatus;\n /** Optional file attachments associated with the message. */\n attachments?: ChatAttachment[];\n /** Arbitrary data passed to a custom messageTemplate. */\n data?: Record<string, unknown>;\n}\n\nexport interface ChatSendEvent {\n content: string;\n /** Files attached to the outgoing message, if any. */\n attachments?: ChatAttachment[];\n}\n\nexport interface ChatFileAttachEvent {\n attachments: ChatAttachment[];\n /** Files rejected because of `accept` / `maxFileSize` constraints. */\n rejected: { file: File; reason: 'type' | 'size' }[];\n}\n\n@Component({\n selector: 'lc-chat',\n standalone: true,\n imports: [NgTemplateOutlet, MarkdownComponent, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_INFORMATION_CIRCLE, ICON_CHECK_CIRCLE, ICON_EXCLAMATION_TRIANGLE, ICON_CIRCLE_X])],\n templateUrl: './chat.component.html',\n styleUrls: ['./chat.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Chat component for conversational user interfaces.\n *\n * Compact, document-style layout: agent/system turns flow as full-width text on\n * a left timeline rail, while user turns render as right-aligned, accent-tinted\n * bubbles (no name label) with the timestamp on a muted line beneath. Spacing is\n * a single space-efficient scale (tunable via the `--lc-chat-*` custom\n * properties); there is no separate roomy variant.\n *\n * Features:\n * - User, agent, and system message roles\n * - Right-aligned user bubbles with an optional avatar: `msg.avatar` image, or a\n * monogram built from `msg.name` initials (gated by `showAvatars`)\n * - Agent/system turns on a left rail with a status dot, avatar or semantic icon\n * - Streaming cursor indicator for AI responses\n * - Typing indicator with animated dots\n * - Auto-scroll to the latest message (new turns, streamed tokens, history\n * loads) — only while the reader is at the bottom; scrolling up pauses it\n * - Optional avatars and timestamps\n * - Configurable header with title\n * - Send on Enter with Shift+Enter for newline\n * - Optional file upload (`allowFileUpload`) with `accept`/`maxFileSize`\n * constraints, preview chips, and image/file thumbnails inside messages.\n *\n * @example Basic\n * ```html\n * <lc-chat [messages]=\"messages\" title=\"Support\" (messageSend)=\"onSend($event)\" />\n * ```\n *\n * @example With file upload\n * ```html\n * <lc-chat\n * [messages]=\"messages\"\n * [allowFileUpload]=\"true\"\n * accept=\"image/*,.pdf\"\n * [maxFileSize]=\"5 * 1024 * 1024\"\n * (messageSend)=\"onSend($event)\"\n * (fileAttach)=\"onAttach($event)\" />\n * ```\n */\nexport class ChatComponent {\n private static nextId = 0;\n /** Id of the composer textarea (links the visually-hidden label). */\n protected readonly inputId = `lc-chat-input-${++ChatComponent.nextId}`;\n\n /** Messages to display. */\n messages = input<ChatMessage[]>([]);\n\n /** Placeholder text for the input. */\n placeholder = input<string>('Nachricht eingeben…');\n\n /** Accessible name of the composer textarea (visually hidden label). */\n inputLabel = input<string>('Nachricht');\n\n /** Title displayed in the header. */\n title = input<string>('Chat');\n\n /** Whether the agent is currently streaming / typing. */\n isStreaming = input<boolean>(false);\n\n /** Show the header bar. */\n showHeader = input<boolean>(true);\n\n /**\n * Draw the chat's own border + rounded corners. Defaults to `true` (card\n * style). Set `false` to render flush/edge-to-edge — e.g. when the chat is\n * the full-height body of an `<lc-page-layout>` and should sit directly under\n * a page header without a nested box.\n */\n bordered = input<boolean>(true);\n\n /**\n * Where messages stack when they don't fill the height.\n * - `'top'` (default): messages grow from the top down.\n * - `'bottom'`: messages anchor to the bottom (empty space above), like most\n * messaging apps. Still scrolls to reveal the top once they overflow.\n */\n messageAnchor = input<'top' | 'bottom'>('top');\n\n /**\n * Width of the message thread and composer.\n * - `'full'` (default): messages span the full available width.\n * - `'narrow'`: a centered, readable column (~46rem) like a document-style chat.\n */\n contentWidth = input<'narrow' | 'full'>('full');\n\n /** Disable the input. */\n disabled = input<boolean>(false);\n\n /** Show avatars next to messages. */\n showAvatars = input<boolean>(true);\n\n /** Show timestamps. */\n showTimestamps = input<boolean>(true);\n\n /**\n * Render message content as Markdown using `<lc-markdown variant=\"chat\">`.\n * Defaults to `'agent'` so agent replies get rich formatting while user\n * input stays as plain text. Pass `false` to opt out, or `true`/`'all'`\n * to render every role as Markdown.\n */\n renderMarkdown = input<ChatRenderMarkdown>('agent');\n\n /** Enable the file-upload (paperclip) button in the input area. */\n allowFileUpload = input<boolean>(false);\n\n /**\n * Comma-separated list of accepted file types, forwarded to the native\n * `<input accept>` attribute. Examples: `image/*`, `.pdf,.docx`.\n */\n accept = input<string>('');\n\n /** Allow selecting multiple files at once. Defaults to `true`. */\n multiple = input<boolean>(true);\n\n /** Maximum file size in bytes. `0` disables the check. */\n maxFileSize = input<number>(0);\n\n /** BCP 47 locale of the message times (hour and minute). */\n locale = input('de-DE');\n\n /** Header badge while a message is streaming. */\n streamingLabel = input('Streaming…');\n\n /** Accessible name of the attach button (`allowFileUpload`). */\n attachAriaLabel = input('Datei anhängen');\n\n /** Accessible name of the send button. */\n sendAriaLabel = input('Send');\n\n /** Screen-reader prefix of messages with status `info`. */\n statusInfoLabel = input('Info:');\n\n /** Screen-reader prefix of messages with status `success`. */\n statusSuccessLabel = input('Erfolg:');\n\n /** Screen-reader prefix of messages with status `warning`. */\n statusWarningLabel = input('Warnung:');\n\n /** Screen-reader prefix of messages with status `error`. */\n statusErrorLabel = input('Fehler:');\n\n /** Accessible name prefix of a pending attachment's remove button, followed by the file name. */\n removeAttachmentAriaLabel = input('Entfernen:');\n\n /** Emits when user sends a message. */\n messageSend = output<ChatSendEvent>();\n\n /** Emits whenever files are added to the pending attachments list. */\n fileAttach = output<ChatFileAttachEvent>();\n\n /** Custom template for rendering message content. Context: { $implicit: ChatMessage } */\n @ContentChild('messageTemplate') messageTemplate?: TemplateRef<{ $implicit: ChatMessage }>;\n\n protected readonly inputValue = signal('');\n protected readonly pendingAttachments = signal<ChatAttachment[]>([]);\n private attachmentCounter = 0;\n\n private readonly injector = inject(Injector);\n private readonly destroyRef = inject(DestroyRef);\n private readonly ngZone = inject(NgZone);\n\n /**\n * Whether the reader is (near) the bottom of the thread. Auto-scroll only\n * follows new content while this is true, so scrolling up to re-read is\n * never fought. Plain field: updated from a passive scroll listener outside\n * the zone, read untracked by the follow effect.\n */\n private atBottom = true;\n\n /** Distance from the bottom (px) still counted as \"at the bottom\". */\n private static readonly AT_BOTTOM_THRESHOLD = 32;\n\n private readonly scrollContainer = viewChild.required<ElementRef<HTMLDivElement>>('scrollContainer');\n @ViewChild('fileInput') private fileInput?: ElementRef<HTMLInputElement>;\n @ViewChild('composerInput') private composerInput?: ElementRef<HTMLTextAreaElement>;\n\n /** Upper bound for the auto-growing composer before it starts scrolling (px). */\n private static readonly INPUT_MAX_HEIGHT = 192;\n\n protected readonly formattedMessages = computed(() =>\n this.messages().map(m => ({\n ...m,\n timestamp: m.timestamp instanceof Date ? m.timestamp : m.timestamp ? new Date(m.timestamp) : undefined,\n }))\n );\n\n /**\n * Text for the visually-hidden live region: the most recent *completed*\n * non-user message. Streaming turns are announced once they finish (a\n * token-by-token live region would be unusable); user turns were typed by\n * the reader; semantic-status messages carry their own `aria-live`/`alert`.\n */\n protected readonly announcement = computed(() => {\n const msgs = this.messages();\n for (let i = msgs.length - 1; i >= 0; i--) {\n const m = msgs[i];\n if (m.role === 'user' || m.streaming || this.isSemanticStatus(m.status)) continue;\n return m.name ? `${m.name}: ${m.content}` : m.content;\n }\n return '';\n });\n\n constructor() {\n // Passive scroll tracking outside the zone — no CD per scroll frame.\n afterNextRender(() => {\n const el = this.scrollContainer().nativeElement;\n const onScroll = () => {\n this.atBottom =\n el.scrollHeight - el.scrollTop - el.clientHeight < ChatComponent.AT_BOTTOM_THRESHOLD;\n };\n this.ngZone.runOutsideAngular(() => el.addEventListener('scroll', onScroll, { passive: true }));\n this.destroyRef.onDestroy(() => el.removeEventListener('scroll', onScroll));\n });\n\n // Follow the conversation: a new turn, a streamed token on the last turn\n // (the parent hands in a new `messages` array), a history load or the\n // typing indicator appearing all grow the thread — scroll after render,\n // but only if the reader was already at the bottom.\n effect(() => {\n this.messages();\n this.isStreaming();\n if (!this.atBottom) return;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n });\n }\n\n protected onInput(event: Event): void {\n const el = event.target as HTMLTextAreaElement;\n this.inputValue.set(el.value);\n this.autoGrow(el);\n }\n\n /** Resize the textarea to fit its content so the composer hugs the text. */\n private autoGrow(el: HTMLTextAreaElement): void {\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, ChatComponent.INPUT_MAX_HEIGHT)}px`;\n }\n\n protected onKeydown(event: KeyboardEvent): void {\n // Enter while an IME composition is open commits the composition, not the message.\n if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {\n event.preventDefault();\n this.send();\n }\n }\n\n protected send(): void {\n const content = this.inputValue().trim();\n const attachments = this.pendingAttachments();\n if ((!content && attachments.length === 0) || this.disabled()) return;\n this.messageSend.emit({\n content,\n attachments: attachments.length ? attachments : undefined,\n });\n this.inputValue.set('');\n this.pendingAttachments.set([]);\n // Sending re-engages follow mode: the reader wants to see their own turn\n // and the reply, even if they had scrolled up before.\n this.atBottom = true;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n // Collapse the auto-grown textarea back to a single row.\n const el = this.composerInput?.nativeElement;\n if (el) el.style.height = 'auto';\n }\n\n protected openFilePicker(): void {\n if (this.disabled() || !this.allowFileUpload()) return;\n this.fileInput?.nativeElement.click();\n }\n\n protected onFilesSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n if (!input.files || input.files.length === 0) return;\n this.addFiles(Array.from(input.files));\n // Reset so the same file can be re-selected later.\n input.value = '';\n }\n\n protected removeAttachment(id: string): void {\n this.pendingAttachments.update(list => {\n const removed = list.find(a => a.id === id);\n if (removed?.url && removed.file) {\n URL.revokeObjectURL(removed.url);\n }\n return list.filter(a => a.id !== id);\n });\n }\n\n protected formatBytes(size: number | undefined): string {\n if (!size && size !== 0) return '';\n if (size < 1024) return `${size} B`;\n if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;\n return `${(size / (1024 * 1024)).toFixed(1)} MB`;\n }\n\n protected isImage(att: ChatAttachment): boolean {\n return !!att.type?.startsWith('image/');\n }\n\n private addFiles(files: File[]): void {\n const max = this.maxFileSize();\n const acceptList = this.accept()\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n\n const accepted: ChatAttachment[] = [];\n const rejected: { file: File; reason: 'type' | 'size' }[] = [];\n\n for (const file of files) {\n if (max > 0 && file.size > max) {\n rejected.push({ file, reason: 'size' });\n continue;\n }\n if (acceptList.length > 0 && !this.matchesAccept(file, acceptList)) {\n rejected.push({ file, reason: 'type' });\n continue;\n }\n accepted.push({\n id: `att-${Date.now()}-${this.attachmentCounter++}`,\n name: file.name,\n type: file.type,\n size: file.size,\n url: URL.createObjectURL(file),\n file,\n });\n }\n\n if (accepted.length) {\n this.pendingAttachments.update(list => [...list, ...accepted]);\n }\n if (accepted.length || rejected.length) {\n this.fileAttach.emit({ attachments: accepted, rejected });\n }\n }\n\n private matchesAccept(file: File, acceptList: string[]): boolean {\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return acceptList.some(rule => {\n const r = rule.toLowerCase();\n if (r.startsWith('.')) return name.endsWith(r);\n if (r.endsWith('/*')) return type.startsWith(r.slice(0, -1));\n return type === r;\n });\n }\n\n protected formatTime(date: Date | undefined): string {\n if (!date) return '';\n return date.toLocaleTimeString(this.locale(), { hour: '2-digit', minute: '2-digit' });\n }\n\n /**\n * Up to two uppercase initials from a name (first + last word), used for the\n * user avatar monogram when no `avatar` image is provided. Returns `''` for an\n * empty/whitespace name so the caller can skip rendering.\n */\n protected initials(name: string | undefined): string {\n if (!name) return '';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '';\n const first = parts[0][0] ?? '';\n const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';\n return (first + last).toUpperCase();\n }\n\n /**\n * Whether to show an avatar on a user turn: only when avatars are enabled and\n * the message carries either an image or a name we can build a monogram from.\n * Keeps the labelless bubble clean when there's nothing to show.\n */\n protected showUserAvatar(msg: ChatMessage): boolean {\n return (\n msg.role === 'user' &&\n this.showAvatars() &&\n !!(msg.avatar || this.initials(msg.name))\n );\n }\n\n protected shouldRenderMarkdown(role: ChatMessageRole): boolean {\n const mode = this.renderMarkdown();\n if (mode === false) return false;\n if (mode === true || mode === 'all') return true;\n return role === mode;\n }\n\n /** Whether a message carries a semantic (non-`default`) status. */\n protected isSemanticStatus(status: ChatMessageStatus | undefined): boolean {\n return !!status && status !== 'default';\n }\n\n /** Rail icon for a semantic status — reuses the `lc-alert` icon names. */\n protected statusIcon(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'information-circle';\n case 'success':\n return 'check-circle';\n case 'warning':\n return 'exclamation-triangle';\n case 'error':\n return 'x-circle';\n default:\n return '';\n }\n }\n\n /** Semantic token colour for a status dot/icon. */\n protected statusColor(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'var(--color-info-500)';\n case 'success':\n return 'var(--color-success-500)';\n case 'warning':\n return 'var(--color-warning-500)';\n case 'error':\n return 'var(--color-error-500)';\n default:\n return 'currentColor';\n }\n }\n\n /** Visually-hidden prefix so colour/icon is never the only signal. */\n protected statusLabel(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return this.statusInfoLabel();\n case 'success':\n return this.statusSuccessLabel();\n case 'warning':\n return this.statusWarningLabel();\n case 'error':\n return this.statusErrorLabel();\n default:\n return '';\n }\n }\n\n /** ARIA live priority: assertive for `error`, polite for other statuses. */\n protected statusAriaLive(status: ChatMessageStatus | undefined): 'assertive' | 'polite' | null {\n if (status === 'error') return 'assertive';\n if (this.isSemanticStatus(status)) return 'polite';\n return null;\n }\n\n private scrollToBottom(): void {\n const el = this.scrollContainer().nativeElement;\n el.scrollTop = el.scrollHeight;\n }\n}\n","<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\">{{ streamingLabel() }}</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 <!-- Announces the latest completed non-user turn (see `announcement`). -->\n <div class=\"lc-chat__sr-only lc-chat__announcer\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</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]=\"removeAttachmentAriaLabel() + ' ' + 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 <label class=\"lc-chat__sr-only\" [for]=\"inputId\">{{ inputLabel() }}</label>\n <textarea\n #composerInput\n [id]=\"inputId\"\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 [attr.aria-label]=\"attachAriaLabel()\"\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 [attr.aria-label]=\"sendAriaLabel()\"\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"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-chat.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/chat/chat.component.ts","../../../../libs/angular-ui-kit/src/lib/chat/chat.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Injector,\n NgZone,\n TemplateRef,\n ContentChild,\n ViewChild,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { MarkdownComponent } from '@lifecorner/angular-ui-kit/markdown';\nimport { closestLang } from '@lifecorner/angular-ui-kit/internal/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n} from '@lifecorner/angular-ui-kit/icons';\n\nexport type ChatMessageRole = 'user' | 'agent' | 'system';\n\n/**\n * Semantic status of a chat message — orthogonal to `role` (role = *who*\n * speaks; status = *what kind* of message). Reuses the semantic vocabulary of\n * `lc-alert` / `lc-callout` and drives the rail dot/accent colour + a status\n * icon. `'default'` (or omitted) reproduces the role-coloured behaviour exactly.\n */\nexport type ChatMessageStatus = 'default' | 'info' | 'success' | 'warning' | 'error';\n\n/**\n * Controls which messages are rendered as Markdown via `<lc-markdown>`.\n * - `false` (default): plain text rendering (legacy behavior)\n * - `true` / `'all'`: render every role as Markdown\n * - `'agent'`: only agent messages are rendered as Markdown (recommended)\n */\nexport type ChatRenderMarkdown = boolean | 'agent' | 'all';\n\n/**\n * Represents a file attached to a chat message or pending in the input.\n *\n * The `file` reference is only available for attachments produced by the\n * input (i.e. emitted via `messageSend` / `fileAttach`). Attachments\n * coming from the server or message history typically rely on `url` only.\n */\nexport interface ChatAttachment {\n id: string;\n name: string;\n /** MIME type, e.g. `image/png`, `application/pdf`. */\n type?: string;\n /** Size in bytes. */\n size?: number;\n /** Object URL or remote URL to preview / download the file. */\n url?: string;\n /** Raw browser File reference (set for newly uploaded files). */\n file?: File;\n}\n\nexport interface ChatMessage {\n id: string;\n role: ChatMessageRole;\n content: string;\n timestamp?: Date | string;\n avatar?: string;\n name?: string;\n streaming?: boolean;\n /**\n * Semantic status — colours the rail dot/accent and shows a status icon.\n * Independent of `role`: a `role: 'agent'` message can be `status: 'error'`\n * (failed reply) or `status: 'success'` (tool finished). Defaults to\n * `'default'`, which reproduces today's role-coloured behaviour exactly.\n * An `'error'` message never pulses and is announced assertively (`role=\"alert\"`).\n */\n status?: ChatMessageStatus;\n /** Optional file attachments associated with the message. */\n attachments?: ChatAttachment[];\n /** Arbitrary data passed to a custom messageTemplate. */\n data?: Record<string, unknown>;\n}\n\nexport interface ChatSendEvent {\n content: string;\n /** Files attached to the outgoing message, if any. */\n attachments?: ChatAttachment[];\n}\n\nexport interface ChatFileAttachEvent {\n attachments: ChatAttachment[];\n /** Files rejected because of `accept` / `maxFileSize` constraints. */\n rejected: { file: File; reason: 'type' | 'size' }[];\n}\n\n@Component({\n selector: 'lc-chat',\n standalone: true,\n imports: [NgTemplateOutlet, MarkdownComponent, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_INFORMATION_CIRCLE, ICON_CHECK_CIRCLE, ICON_EXCLAMATION_TRIANGLE, ICON_CIRCLE_X])],\n templateUrl: './chat.component.html',\n styleUrls: ['./chat.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Chat component for conversational user interfaces.\n *\n * Compact, document-style layout: agent/system turns flow as full-width text on\n * a left timeline rail, while user turns render as right-aligned, accent-tinted\n * bubbles (no name label) with the timestamp on a muted line beneath. Spacing is\n * a single space-efficient scale (tunable via the `--lc-chat-*` custom\n * properties); there is no separate roomy variant.\n *\n * Features:\n * - User, agent, and system message roles\n * - Right-aligned user bubbles with an optional avatar: `msg.avatar` image, or a\n * monogram built from `msg.name` initials (gated by `showAvatars`)\n * - Agent/system turns on a left rail with a status dot, avatar or semantic icon\n * - Streaming cursor indicator for AI responses\n * - Typing indicator with animated dots\n * - Auto-scroll to the latest message (new turns, streamed tokens, history\n * loads) — only while the reader is at the bottom; scrolling up pauses it\n * - Optional avatars and timestamps\n * - Configurable header with title\n * - Send on Enter with Shift+Enter for newline\n * - Optional file upload (`allowFileUpload`) with `accept`/`maxFileSize`\n * constraints, preview chips, and image/file thumbnails inside messages.\n *\n * @example Basic\n * ```html\n * <lc-chat [messages]=\"messages\" title=\"Support\" (messageSend)=\"onSend($event)\" />\n * ```\n *\n * @example With file upload\n * ```html\n * <lc-chat\n * [messages]=\"messages\"\n * [allowFileUpload]=\"true\"\n * accept=\"image/*,.pdf\"\n * [maxFileSize]=\"5 * 1024 * 1024\"\n * (messageSend)=\"onSend($event)\"\n * (fileAttach)=\"onAttach($event)\" />\n * ```\n */\nexport class ChatComponent {\n private static nextId = 0;\n /** Id of the composer textarea (links the visually-hidden label). */\n protected readonly inputId = `lc-chat-input-${++ChatComponent.nextId}`;\n\n /** Messages to display. */\n messages = input<ChatMessage[]>([]);\n\n /** Placeholder text for the input. */\n placeholder = input<string>('Nachricht eingeben…');\n\n /** Accessible name of the composer textarea (visually hidden label). */\n inputLabel = input<string>('Nachricht');\n\n /** Title displayed in the header. */\n title = input<string>('Chat');\n\n /** Whether the agent is currently streaming / typing. */\n isStreaming = input<boolean>(false);\n\n /** Show the header bar. */\n showHeader = input<boolean>(true);\n\n /**\n * Draw the chat's own border + rounded corners. Defaults to `true` (card\n * style). Set `false` to render flush/edge-to-edge — e.g. when the chat is\n * the full-height body of an `<lc-page-layout>` and should sit directly under\n * a page header without a nested box.\n */\n bordered = input<boolean>(true);\n\n /**\n * Where messages stack when they don't fill the height.\n * - `'top'` (default): messages grow from the top down.\n * - `'bottom'`: messages anchor to the bottom (empty space above), like most\n * messaging apps. Still scrolls to reveal the top once they overflow.\n */\n messageAnchor = input<'top' | 'bottom'>('top');\n\n /**\n * Width of the message thread and composer.\n * - `'full'` (default): messages span the full available width.\n * - `'narrow'`: a centered, readable column (~46rem) like a document-style chat.\n */\n contentWidth = input<'narrow' | 'full'>('full');\n\n /** Disable the input. */\n disabled = input<boolean>(false);\n\n /** Show avatars next to messages. */\n showAvatars = input<boolean>(true);\n\n /** Show timestamps. */\n showTimestamps = input<boolean>(true);\n\n /**\n * Render message content as Markdown using `<lc-markdown variant=\"chat\">`.\n * Defaults to `'agent'` so agent replies get rich formatting while user\n * input stays as plain text. Pass `false` to opt out, or `true`/`'all'`\n * to render every role as Markdown.\n */\n renderMarkdown = input<ChatRenderMarkdown>('agent');\n\n /** Enable the file-upload (paperclip) button in the input area. */\n allowFileUpload = input<boolean>(false);\n\n /**\n * Comma-separated list of accepted file types, forwarded to the native\n * `<input accept>` attribute. Examples: `image/*`, `.pdf,.docx`.\n */\n accept = input<string>('');\n\n /** Allow selecting multiple files at once. Defaults to `true`. */\n multiple = input<boolean>(true);\n\n /** Maximum file size in bytes. `0` disables the check. */\n maxFileSize = input<number>(0);\n\n /**\n * BCP 47 locale of the message times (hour and minute). Attachment sizes\n * follow the host's language (closest `lang`, 3.33), English without one:\n * `240,0 KB` under `lang=\"de\"`.\n * @default 'de-DE'\n */\n locale = input('de-DE');\n\n /** Header badge while a message is streaming. */\n streamingLabel = input('Streaming…');\n\n /** Accessible name of the attach button (`allowFileUpload`). */\n attachAriaLabel = input('Datei anhängen');\n\n /** Accessible name of the send button. */\n sendAriaLabel = input('Send');\n\n /** Screen-reader prefix of messages with status `info`. */\n statusInfoLabel = input('Info:');\n\n /** Screen-reader prefix of messages with status `success`. */\n statusSuccessLabel = input('Erfolg:');\n\n /** Screen-reader prefix of messages with status `warning`. */\n statusWarningLabel = input('Warnung:');\n\n /** Screen-reader prefix of messages with status `error`. */\n statusErrorLabel = input('Fehler:');\n\n /** Accessible name prefix of a pending attachment's remove button, followed by the file name. */\n removeAttachmentAriaLabel = input('Entfernen:');\n\n /** Emits when user sends a message. */\n messageSend = output<ChatSendEvent>();\n\n /** Emits whenever files are added to the pending attachments list. */\n fileAttach = output<ChatFileAttachEvent>();\n\n /** Custom template for rendering message content. Context: { $implicit: ChatMessage } */\n @ContentChild('messageTemplate') messageTemplate?: TemplateRef<{ $implicit: ChatMessage }>;\n\n protected readonly inputValue = signal('');\n protected readonly pendingAttachments = signal<ChatAttachment[]>([]);\n private attachmentCounter = 0;\n\n private readonly injector = inject(Injector);\n private readonly destroyRef = inject(DestroyRef);\n private readonly ngZone = inject(NgZone);\n\n /**\n * Whether the reader is (near) the bottom of the thread. Auto-scroll only\n * follows new content while this is true, so scrolling up to re-read is\n * never fought. Plain field: updated from a passive scroll listener outside\n * the zone, read untracked by the follow effect.\n */\n private atBottom = true;\n\n /** Distance from the bottom (px) still counted as \"at the bottom\". */\n private static readonly AT_BOTTOM_THRESHOLD = 32;\n\n private readonly scrollContainer = viewChild.required<ElementRef<HTMLDivElement>>('scrollContainer');\n @ViewChild('fileInput') private fileInput?: ElementRef<HTMLInputElement>;\n @ViewChild('composerInput') private composerInput?: ElementRef<HTMLTextAreaElement>;\n\n /** Upper bound for the auto-growing composer before it starts scrolling (px). */\n private static readonly INPUT_MAX_HEIGHT = 192;\n\n protected readonly formattedMessages = computed(() =>\n this.messages().map(m => ({\n ...m,\n timestamp: m.timestamp instanceof Date ? m.timestamp : m.timestamp ? new Date(m.timestamp) : undefined,\n }))\n );\n\n /**\n * Text for the visually-hidden live region: the most recent *completed*\n * non-user message. Streaming turns are announced once they finish (a\n * token-by-token live region would be unusable); user turns were typed by\n * the reader; semantic-status messages carry their own `aria-live`/`alert`.\n */\n protected readonly announcement = computed(() => {\n const msgs = this.messages();\n for (let i = msgs.length - 1; i >= 0; i--) {\n const m = msgs[i];\n if (m.role === 'user' || m.streaming || this.isSemanticStatus(m.status)) continue;\n return m.name ? `${m.name}: ${m.content}` : m.content;\n }\n return '';\n });\n\n constructor() {\n // Passive scroll tracking outside the zone — no CD per scroll frame.\n afterNextRender(() => {\n const el = this.scrollContainer().nativeElement;\n const onScroll = () => {\n this.atBottom =\n el.scrollHeight - el.scrollTop - el.clientHeight < ChatComponent.AT_BOTTOM_THRESHOLD;\n };\n this.ngZone.runOutsideAngular(() => el.addEventListener('scroll', onScroll, { passive: true }));\n this.destroyRef.onDestroy(() => el.removeEventListener('scroll', onScroll));\n });\n\n // Follow the conversation: a new turn, a streamed token on the last turn\n // (the parent hands in a new `messages` array), a history load or the\n // typing indicator appearing all grow the thread — scroll after render,\n // but only if the reader was already at the bottom.\n effect(() => {\n this.messages();\n this.isStreaming();\n if (!this.atBottom) return;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n });\n }\n\n protected onInput(event: Event): void {\n const el = event.target as HTMLTextAreaElement;\n this.inputValue.set(el.value);\n this.autoGrow(el);\n }\n\n /** Resize the textarea to fit its content so the composer hugs the text. */\n private autoGrow(el: HTMLTextAreaElement): void {\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, ChatComponent.INPUT_MAX_HEIGHT)}px`;\n }\n\n protected onKeydown(event: KeyboardEvent): void {\n // Enter while an IME composition is open commits the composition, not the message.\n if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {\n event.preventDefault();\n this.send();\n }\n }\n\n protected send(): void {\n const content = this.inputValue().trim();\n const attachments = this.pendingAttachments();\n if ((!content && attachments.length === 0) || this.disabled()) return;\n this.messageSend.emit({\n content,\n attachments: attachments.length ? attachments : undefined,\n });\n this.inputValue.set('');\n this.pendingAttachments.set([]);\n // Sending re-engages follow mode: the reader wants to see their own turn\n // and the reply, even if they had scrolled up before.\n this.atBottom = true;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n // Collapse the auto-grown textarea back to a single row.\n const el = this.composerInput?.nativeElement;\n if (el) el.style.height = 'auto';\n }\n\n protected openFilePicker(): void {\n if (this.disabled() || !this.allowFileUpload()) return;\n this.fileInput?.nativeElement.click();\n }\n\n protected onFilesSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n if (!input.files || input.files.length === 0) return;\n this.addFiles(Array.from(input.files));\n // Reset so the same file can be re-selected later.\n input.value = '';\n }\n\n protected removeAttachment(id: string): void {\n this.pendingAttachments.update(list => {\n const removed = list.find(a => a.id === id);\n if (removed?.url && removed.file) {\n URL.revokeObjectURL(removed.url);\n }\n return list.filter(a => a.id !== id);\n });\n }\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n\n /**\n * Attachment size with one decimal in the host's language (3.33, request\n * 052, 044-n2): the closest `lang`, English without one (or when it is\n * empty or invalid), so `240.0 KB` by default and `240,0 KB` under\n * `lang=\"de\"`. No digit grouping, as before.\n */\n protected formatBytes(size: number | undefined): string {\n if (!size && size !== 0) return '';\n if (size < 1024) return `${size} B`;\n const decimal = this.decimalFormat();\n if (size < 1024 * 1024) return `${decimal.format(size / 1024)} KB`;\n return `${decimal.format(size / (1024 * 1024))} MB`;\n }\n\n private decimalFormat(): Intl.NumberFormat {\n const options: Intl.NumberFormatOptions = { minimumFractionDigits: 1, maximumFractionDigits: 1, useGrouping: false };\n const lang = closestLang(this.host.nativeElement);\n try {\n return new Intl.NumberFormat(lang || 'en', options);\n } catch {\n return new Intl.NumberFormat('en', options);\n }\n }\n\n protected isImage(att: ChatAttachment): boolean {\n return !!att.type?.startsWith('image/');\n }\n\n private addFiles(files: File[]): void {\n const max = this.maxFileSize();\n const acceptList = this.accept()\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n\n const accepted: ChatAttachment[] = [];\n const rejected: { file: File; reason: 'type' | 'size' }[] = [];\n\n for (const file of files) {\n if (max > 0 && file.size > max) {\n rejected.push({ file, reason: 'size' });\n continue;\n }\n if (acceptList.length > 0 && !this.matchesAccept(file, acceptList)) {\n rejected.push({ file, reason: 'type' });\n continue;\n }\n accepted.push({\n id: `att-${Date.now()}-${this.attachmentCounter++}`,\n name: file.name,\n type: file.type,\n size: file.size,\n url: URL.createObjectURL(file),\n file,\n });\n }\n\n if (accepted.length) {\n this.pendingAttachments.update(list => [...list, ...accepted]);\n }\n if (accepted.length || rejected.length) {\n this.fileAttach.emit({ attachments: accepted, rejected });\n }\n }\n\n private matchesAccept(file: File, acceptList: string[]): boolean {\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return acceptList.some(rule => {\n const r = rule.toLowerCase();\n if (r.startsWith('.')) return name.endsWith(r);\n if (r.endsWith('/*')) return type.startsWith(r.slice(0, -1));\n return type === r;\n });\n }\n\n protected formatTime(date: Date | undefined): string {\n if (!date) return '';\n return date.toLocaleTimeString(this.locale(), { hour: '2-digit', minute: '2-digit' });\n }\n\n /**\n * Up to two uppercase initials from a name (first + last word), used for the\n * user avatar monogram when no `avatar` image is provided. Returns `''` for an\n * empty/whitespace name so the caller can skip rendering.\n */\n protected initials(name: string | undefined): string {\n if (!name) return '';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '';\n const first = parts[0][0] ?? '';\n const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';\n return (first + last).toUpperCase();\n }\n\n /**\n * Whether to show an avatar on a user turn: only when avatars are enabled and\n * the message carries either an image or a name we can build a monogram from.\n * Keeps the labelless bubble clean when there's nothing to show.\n */\n protected showUserAvatar(msg: ChatMessage): boolean {\n return (\n msg.role === 'user' &&\n this.showAvatars() &&\n !!(msg.avatar || this.initials(msg.name))\n );\n }\n\n protected shouldRenderMarkdown(role: ChatMessageRole): boolean {\n const mode = this.renderMarkdown();\n if (mode === false) return false;\n if (mode === true || mode === 'all') return true;\n return role === mode;\n }\n\n /** Whether a message carries a semantic (non-`default`) status. */\n protected isSemanticStatus(status: ChatMessageStatus | undefined): boolean {\n return !!status && status !== 'default';\n }\n\n /** Rail icon for a semantic status — reuses the `lc-alert` icon names. */\n protected statusIcon(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'information-circle';\n case 'success':\n return 'check-circle';\n case 'warning':\n return 'exclamation-triangle';\n case 'error':\n return 'x-circle';\n default:\n return '';\n }\n }\n\n /** Semantic token colour for a status dot/icon. */\n protected statusColor(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'var(--color-info-500)';\n case 'success':\n return 'var(--color-success-500)';\n case 'warning':\n return 'var(--color-warning-500)';\n case 'error':\n return 'var(--color-error-500)';\n default:\n return 'currentColor';\n }\n }\n\n /** Visually-hidden prefix so colour/icon is never the only signal. */\n protected statusLabel(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return this.statusInfoLabel();\n case 'success':\n return this.statusSuccessLabel();\n case 'warning':\n return this.statusWarningLabel();\n case 'error':\n return this.statusErrorLabel();\n default:\n return '';\n }\n }\n\n /** ARIA live priority: assertive for `error`, polite for other statuses. */\n protected statusAriaLive(status: ChatMessageStatus | undefined): 'assertive' | 'polite' | null {\n if (status === 'error') return 'assertive';\n if (this.isSemanticStatus(status)) return 'polite';\n return null;\n }\n\n private scrollToBottom(): void {\n const el = this.scrollContainer().nativeElement;\n el.scrollTop = el.scrollHeight;\n }\n}\n","<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\">{{ streamingLabel() }}</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 <!-- Announces the latest completed non-user turn (see `announcement`). -->\n <div class=\"lc-chat__sr-only lc-chat__announcer\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</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]=\"removeAttachmentAriaLabel() + ' ' + 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 <label class=\"lc-chat__sr-only\" [for]=\"inputId\">{{ inputLabel() }}</label>\n <textarea\n #composerInput\n [id]=\"inputId\"\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 [attr.aria-label]=\"attachAriaLabel()\"\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 [attr.aria-label]=\"sendAriaLabel()\"\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"],"mappings":"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"}