@life-cockpit/angular-ui-kit 2.13.0 → 2.13.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.
@@ -533,6 +533,27 @@ const ICON_FALLBACK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
533
533
  '<rect x="3.5" y="3.5" width="17" height="17" rx="3" stroke-dasharray="2.5 2.5" />' +
534
534
  '<text x="12" y="16.5" text-anchor="middle" font-size="12" font-weight="700" font-family="system-ui, -apple-system, sans-serif" fill="currentColor" stroke="none">?</text>' +
535
535
  '</svg>';
536
+ /**
537
+ * Icon names already reported to the console. A page with dozens of instances
538
+ * of the same typo must produce exactly one warning, not one per instance —
539
+ * and the warning fires in production too (unlike the old dev-only warning,
540
+ * which left prod incidents invisible in the console).
541
+ */
542
+ const warnedIconNames = new Set();
543
+ function warnOnce(name, message) {
544
+ if (warnedIconNames.has(name))
545
+ return;
546
+ warnedIconNames.add(name);
547
+ console.warn(message);
548
+ }
549
+ /**
550
+ * Clears the warn-once-per-name registry. Only needed by unit tests that
551
+ * assert on console warnings across multiple cases.
552
+ * @internal
553
+ */
554
+ function resetIconWarnings() {
555
+ warnedIconNames.clear();
556
+ }
536
557
  /**
537
558
  * Icon component - Tabler Icons wrapper for displaying SVG icons
538
559
  *
@@ -543,10 +564,15 @@ const ICON_FALLBACK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
543
564
  * - Custom color support (CSS colors, variables)
544
565
  * - Accessibility attributes (ARIA labels, decorative icons)
545
566
  * - Dynamic SVG loading from Tabler Icons
546
- * - Fail-loud on unknown names: a dev-mode warning + a visible placeholder
547
- * (never a silent empty space). See {@link ICON_NAMES} / {@link isValidIconName}
548
- * for the canonical set of valid names, and {@link ICON_ALIASES} for the
549
- * supported Heroicon/Material aliases.
567
+ * - Fail-loud on unknown names: a console warning (once per name, in dev AND
568
+ * prod) + a visible placeholder (never a silent empty space) and **no
569
+ * network request**, because in SPA deployments the server answers missing
570
+ * asset paths with the index.html fallback. See {@link ICON_NAMES} /
571
+ * {@link isValidIconName} for the canonical set of valid names, and
572
+ * {@link ICON_ALIASES} for the supported Heroicon/Material aliases.
573
+ * - Fetched responses are validated (content-type + parsed `<svg>` root)
574
+ * before anything is trusted — a non-SVG response (e.g. the SPA index.html
575
+ * fallback) is dropped and replaced by the placeholder.
550
576
  *
551
577
  * @example
552
578
  * ```html
@@ -679,14 +705,18 @@ class IconComponent {
679
705
  return;
680
706
  }
681
707
  // Fail loud on names outside the canonical set (typos, un-aliased
682
- // Heroicon names, …). We still attempt to load below, so anything that
683
- // *is* servable keeps working this only surfaces the problem instead of
684
- // silently rendering an empty icon.
685
- if (!isValidIconName(rawName) && isDevMode()) {
686
- console.warn(`[lc-icon] Unknown icon "${rawName}". Use a Tabler name or a documented alias (see ICON_NAMES / ICON_ALIASES).`);
687
- if (this.strict()) {
708
+ // Heroicon names, …) in dev AND prod, once per name. Crucially, an
709
+ // unknown name is never fetched: in SPA deployments the server answers
710
+ // missing asset paths with the index.html fallback (status 200), so a
711
+ // fetch for a typo'd name would come back as a full HTML document. The
712
+ // placeholder is rendered instead.
713
+ if (!isValidIconName(rawName)) {
714
+ warnOnce(rawName, `[lc-icon] Unknown icon "${rawName}". Use a Tabler name or a documented alias (see ICON_NAMES / ICON_ALIASES). Rendering a placeholder instead.`);
715
+ if (isDevMode() && this.strict()) {
688
716
  throw new Error(`[lc-icon] Unknown icon "${rawName}" (strict mode). Use a Tabler name or a documented alias.`);
689
717
  }
718
+ this.renderFallback();
719
+ return;
690
720
  }
691
721
  // Resolve aliases (Heroicon/Material -> Tabler)
692
722
  const iconName = ICON_ALIASES[rawName] ?? rawName;
@@ -694,7 +724,12 @@ class IconComponent {
694
724
  const inlineSvg = INLINE_ICON_SVGS[iconName]?.[iconVariant];
695
725
  if (inlineSvg) {
696
726
  const processed = this.processSvgString(inlineSvg);
697
- this.svgContent.set(this.sanitizer.bypassSecurityTrustHtml(processed));
727
+ if (processed !== null) {
728
+ this.setTrustedSvg(processed);
729
+ }
730
+ else {
731
+ this.renderFallback();
732
+ }
698
733
  return;
699
734
  }
700
735
  // Fallback to HTTP loading (may not work in all environments)
@@ -709,7 +744,7 @@ class IconComponent {
709
744
  // otherwise-valid icon stuck on the placeholder. A genuine 404 (missing
710
745
  // asset) is not retried.
711
746
  this.http
712
- .get(path, { responseType: 'text' })
747
+ .get(path, { observe: 'response', responseType: 'text' })
713
748
  .pipe(retry({
714
749
  count: 2,
715
750
  delay: (error, retryCount) => error instanceof HttpErrorResponse && error.status === 404
@@ -717,10 +752,24 @@ class IconComponent {
717
752
  : timer(200 * retryCount),
718
753
  }))
719
754
  .subscribe({
720
- next: (svgText) => {
721
- // Process SVG to add our attributes
722
- const processed = this.processSvgString(svgText);
723
- this.svgContent.set(this.sanitizer.bypassSecurityTrustHtml(processed));
755
+ next: (response) => {
756
+ // Validate before anything touches the DOM: SPA deployments answer
757
+ // missing asset paths with `index.html` and status 200 — that
758
+ // document must be dropped, never injected. A text/html content-type
759
+ // is rejected outright; everything else must parse as a standalone
760
+ // <svg> document (see processSvgString).
761
+ const contentType = response.headers.get('content-type') ?? '';
762
+ const processed = contentType.includes('text/html')
763
+ ? null
764
+ : this.processSvgString(response.body ?? '');
765
+ if (processed === null) {
766
+ warnOnce(rawName, `[lc-icon] "${rawName}": ${path} returned non-SVG content` +
767
+ (contentType ? ` (${contentType})` : '') +
768
+ ' — dropped. In SPA deployments this is typically the index.html fallback for a missing asset.');
769
+ this.renderFallback();
770
+ return;
771
+ }
772
+ this.setTrustedSvg(processed);
724
773
  },
725
774
  error: () => {
726
775
  // Asset missing or unreachable: render a visible placeholder so the
@@ -728,23 +777,53 @@ class IconComponent {
728
777
  // already reported above; a known-but-unloadable name (e.g. offline,
729
778
  // SSR, or unit tests where assets are not served) stays quiet here to
730
779
  // avoid console noise.
731
- const processed = this.processSvgString(ICON_FALLBACK_SVG);
732
- this.svgContent.set(this.sanitizer.bypassSecurityTrustHtml(processed));
780
+ this.renderFallback();
733
781
  },
734
782
  });
735
783
  });
736
784
  }
737
785
  /**
738
- * Process SVG string to add size, color, and accessibility attributes
786
+ * The single funnel to `bypassSecurityTrustHtml`. The bypass is only safe
787
+ * because every caller passes the output of {@link processSvgString}, which
788
+ * guarantees the markup is a parsed and re-serialized standalone `<svg>`
789
+ * document — never raw fetched text. Do NOT add callers that skip that
790
+ * validation, and do NOT "simplify" the gates away: without them, a SPA
791
+ * server's index.html fallback (status 200, text/html) gets injected into
792
+ * the page once per icon instance.
793
+ * @private
794
+ */
795
+ setTrustedSvg(processedSvg) {
796
+ this.svgContent.set(this.sanitizer.bypassSecurityTrustHtml(processedSvg));
797
+ }
798
+ /**
799
+ * Renders the visible "?" placeholder for anything that cannot be resolved
800
+ * to a real icon (unknown name, failed load, non-SVG response).
801
+ * @private
802
+ */
803
+ renderFallback() {
804
+ this.setTrustedSvg(this.processSvgString(ICON_FALLBACK_SVG) ?? ICON_FALLBACK_SVG);
805
+ }
806
+ /**
807
+ * Parse an SVG string and add size, color, and accessibility attributes.
808
+ *
809
+ * Returns `null` when the input is not a standalone SVG document — a parse
810
+ * error, or a non-`<svg>` root such as an HTML page. Callers must treat
811
+ * `null` as "do not render"; the raw input is never passed through.
739
812
  * @private
740
813
  */
741
814
  processSvgString(svgText) {
742
815
  // Parse as DOM to manipulate
743
816
  const parser = new DOMParser();
744
817
  const doc = parser.parseFromString(svgText, 'image/svg+xml');
745
- const svg = doc.querySelector('svg');
746
- if (!svg)
747
- return svgText;
818
+ const svg = doc.documentElement;
819
+ // Reject anything that is not a clean, standalone <svg> document. On
820
+ // parse errors DOMParser yields a <parsererror> root (Firefox) or embeds
821
+ // a <parsererror> element in partially-parsed output (Chromium) — both
822
+ // must never reach the DOM.
823
+ if (svg.nodeName.toLowerCase() !== 'svg' ||
824
+ doc.getElementsByTagName('parsererror').length > 0) {
825
+ return null;
826
+ }
748
827
  // Set size
749
828
  const size = this.sizeInPixels();
750
829
  svg.setAttribute('width', size);
@@ -16730,5 +16809,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
16730
16809
  * Generated bundle index. Do not edit.
16731
16810
  */
16732
16811
 
16733
- export { AccordionComponent, AccordionContentDirective, AccordionGroupComponent, AccordionHeaderDirective, AlertComponent, AnimationDurationFast, AnimationEasingEaseIn, AnimationEasingEaseInOut, AnimationEasingEaseOut, AreaChartComponent, AvatarComponent, AvatarGroupComponent, BadgeComponent, BarChartComponent, BorderRadius2xl, BorderRadiusFull, BorderRadiusLg, BorderRadiusMd, BorderRadiusNone, BorderRadiusSm, BorderRadiusXl, BreadcrumbsComponent, ButtonComponent, CalendarComponent, CalloutComponent, CardComponent, ChatComponent, CheckboxComponent, ChipComponent, CodeBlockComponent, ColorAccentOrange, ColorAccentPurple, ColorAccentRed, ColorAccentRust, ColorAccentViolet, ColorBackgroundDark, ColorErrorDark, ColorErrorDefault, ColorErrorLight, ColorInfoDark, ColorInfoDefault, ColorInfoLight, ColorNeutral100, ColorNeutral200, ColorNeutral300, ColorNeutral400, ColorNeutral50, ColorNeutral500, ColorNeutral600, ColorNeutral700, ColorNeutral800, ColorNeutral900, ColorPickerComponent, ColorPrimary100, ColorPrimary200, ColorPrimary300, ColorPrimary400, ColorPrimary50, ColorPrimary500, ColorPrimary600, ColorPrimary700, ColorPrimary800, ColorPrimary900, ColorSecondary100, ColorSecondary200, ColorSecondary300, ColorSecondary400, ColorSecondary50, ColorSecondary500, ColorSecondary600, ColorSecondary700, ColorSecondary800, ColorSecondary900, ColorSidebarDark, ColorSuccessDark, ColorSuccessDefault, ColorSuccessLight, ColorSurfaceDarkBase, ColorSurfaceDarkRaised, ColorSurfaceDarkSunken, ColorTextDarkPrimary, ColorTextDarkSecondary, ColorTextDarkTertiary, ColorWarningDark, ColorWarningDefault, ColorWarningLight, ComboboxComponent, ConfirmDialogComponent, ConfirmService, ContainerComponent, DateRangePickerComponent, DatepickerComponent, DependencyViewerComponent, DescriptionListComponent, DiffViewerComponent, DividerComponent, DocumentViewerComponent, DonutChartComponent, DrawerComponent, Elevation1, Elevation2, Elevation3, Elevation4, EmailInputComponent, EmptyStateComponent, ErrorDisplayComponent, FILE_FALLBACK_ICON, FOLDER_ICON, FOLDER_OPEN_ICON, FieldGroupComponent, FileUploadComponent, FilterBarComponent, FooterComponent, FunnelChartComponent, GalleryComponent, GanttChartComponent, GaugeComponent, HeaderComponent, HeatmapComponent, HeroComponent, ICON_ALIASES, ICON_NAMES, IconComponent, InputComponent, KanbanBoardComponent, LC_LOGO_BASE_PATH, LineChartComponent, ListComponent, ListItemTemplateDirective, LogViewerComponent, LogoComponent, MarkdownComponent, MenuComponent, ModalComponent, NotificationCenterComponent, NumberInputComponent, PageHeaderComponent, PageLayoutComponent, PaginationComponent, PasswordInputComponent, PieChartComponent, PipelineComponent, PopoverComponent, ProgressBarComponent, ProgressRingComponent, RadarChartComponent, RadioComponent, RatingComponent, RichTextEditorComponent, ScatterPlotComponent, SearchInputComponent, SectionComponent, SelectComponent, SidenavComponent, SizeContentMaxWidth, SizeInteractiveLgFontSize, SizeInteractiveLgHeight, SizeInteractiveLgPadding, SizeInteractiveMdFontSize, SizeInteractiveMdHeight, SizeInteractiveMdPadding, SizeInteractiveSmFontSize, SizeInteractiveSmHeight, SizeInteractiveSmPadding, SizeInteractiveXsFontSize, SizeInteractiveXsHeight, SizeInteractiveXsPadding, SizeMinTouchHeight, SizeMinTouchWidth, SkeletonComponent, SliderComponent, SpacerComponent, Spacing0, Spacing05, Spacing1, Spacing10, Spacing11, Spacing12, Spacing14, Spacing15, Spacing16, Spacing2, Spacing25, Spacing3, Spacing35, Spacing4, Spacing5, Spacing6, Spacing7, Spacing8, Spacing9, SparklineComponent, SpinnerComponent, SplitPaneComponent, StackComponent, StackedBarChartComponent, StageListComponent, StatTrendComponent, StepperComponent, SwitchComponent, TabComponent, TableCellDirective, TableComponent, TabsComponent, TagInputComponent, TextareaComponent, ThemeService, TimelineComponent, ToastComponent, ToastService, ToggleGroupComponent, ToolbarComponent, TooltipContentComponent, TooltipDirective, TreeViewComponent, TypographyComponent, TypographyFontFamilyBase, TypographyFontFamilyMono, TypographyFontSize2xl, TypographyFontSize3xl, TypographyFontSize4xl, TypographyFontSize5xl, TypographyFontSize6xl, TypographyFontSizeBase, TypographyFontSizeLg, TypographyFontSizeSm, TypographyFontSizeXl, TypographyFontSizeXs, TypographyFontWeightBold, TypographyFontWeightMedium, TypographyFontWeightNormal, TypographyFontWeightSemibold, TypographyLineHeightNormal, TypographyLineHeightRelaxed, TypographyLineHeightTight, VerificationCodeInputComponent, WaterfallChartComponent, isValidIconName, resolveFileIcon };
16812
+ export { AccordionComponent, AccordionContentDirective, AccordionGroupComponent, AccordionHeaderDirective, AlertComponent, AnimationDurationFast, AnimationEasingEaseIn, AnimationEasingEaseInOut, AnimationEasingEaseOut, AreaChartComponent, AvatarComponent, AvatarGroupComponent, BadgeComponent, BarChartComponent, BorderRadius2xl, BorderRadiusFull, BorderRadiusLg, BorderRadiusMd, BorderRadiusNone, BorderRadiusSm, BorderRadiusXl, BreadcrumbsComponent, ButtonComponent, CalendarComponent, CalloutComponent, CardComponent, ChatComponent, CheckboxComponent, ChipComponent, CodeBlockComponent, ColorAccentOrange, ColorAccentPurple, ColorAccentRed, ColorAccentRust, ColorAccentViolet, ColorBackgroundDark, ColorErrorDark, ColorErrorDefault, ColorErrorLight, ColorInfoDark, ColorInfoDefault, ColorInfoLight, ColorNeutral100, ColorNeutral200, ColorNeutral300, ColorNeutral400, ColorNeutral50, ColorNeutral500, ColorNeutral600, ColorNeutral700, ColorNeutral800, ColorNeutral900, ColorPickerComponent, ColorPrimary100, ColorPrimary200, ColorPrimary300, ColorPrimary400, ColorPrimary50, ColorPrimary500, ColorPrimary600, ColorPrimary700, ColorPrimary800, ColorPrimary900, ColorSecondary100, ColorSecondary200, ColorSecondary300, ColorSecondary400, ColorSecondary50, ColorSecondary500, ColorSecondary600, ColorSecondary700, ColorSecondary800, ColorSecondary900, ColorSidebarDark, ColorSuccessDark, ColorSuccessDefault, ColorSuccessLight, ColorSurfaceDarkBase, ColorSurfaceDarkRaised, ColorSurfaceDarkSunken, ColorTextDarkPrimary, ColorTextDarkSecondary, ColorTextDarkTertiary, ColorWarningDark, ColorWarningDefault, ColorWarningLight, ComboboxComponent, ConfirmDialogComponent, ConfirmService, ContainerComponent, DateRangePickerComponent, DatepickerComponent, DependencyViewerComponent, DescriptionListComponent, DiffViewerComponent, DividerComponent, DocumentViewerComponent, DonutChartComponent, DrawerComponent, Elevation1, Elevation2, Elevation3, Elevation4, EmailInputComponent, EmptyStateComponent, ErrorDisplayComponent, FILE_FALLBACK_ICON, FOLDER_ICON, FOLDER_OPEN_ICON, FieldGroupComponent, FileUploadComponent, FilterBarComponent, FooterComponent, FunnelChartComponent, GalleryComponent, GanttChartComponent, GaugeComponent, HeaderComponent, HeatmapComponent, HeroComponent, ICON_ALIASES, ICON_NAMES, IconComponent, InputComponent, KanbanBoardComponent, LC_LOGO_BASE_PATH, LineChartComponent, ListComponent, ListItemTemplateDirective, LogViewerComponent, LogoComponent, MarkdownComponent, MenuComponent, ModalComponent, NotificationCenterComponent, NumberInputComponent, PageHeaderComponent, PageLayoutComponent, PaginationComponent, PasswordInputComponent, PieChartComponent, PipelineComponent, PopoverComponent, ProgressBarComponent, ProgressRingComponent, RadarChartComponent, RadioComponent, RatingComponent, RichTextEditorComponent, ScatterPlotComponent, SearchInputComponent, SectionComponent, SelectComponent, SidenavComponent, SizeContentMaxWidth, SizeInteractiveLgFontSize, SizeInteractiveLgHeight, SizeInteractiveLgPadding, SizeInteractiveMdFontSize, SizeInteractiveMdHeight, SizeInteractiveMdPadding, SizeInteractiveSmFontSize, SizeInteractiveSmHeight, SizeInteractiveSmPadding, SizeInteractiveXsFontSize, SizeInteractiveXsHeight, SizeInteractiveXsPadding, SizeMinTouchHeight, SizeMinTouchWidth, SkeletonComponent, SliderComponent, SpacerComponent, Spacing0, Spacing05, Spacing1, Spacing10, Spacing11, Spacing12, Spacing14, Spacing15, Spacing16, Spacing2, Spacing25, Spacing3, Spacing35, Spacing4, Spacing5, Spacing6, Spacing7, Spacing8, Spacing9, SparklineComponent, SpinnerComponent, SplitPaneComponent, StackComponent, StackedBarChartComponent, StageListComponent, StatTrendComponent, StepperComponent, SwitchComponent, TabComponent, TableCellDirective, TableComponent, TabsComponent, TagInputComponent, TextareaComponent, ThemeService, TimelineComponent, ToastComponent, ToastService, ToggleGroupComponent, ToolbarComponent, TooltipContentComponent, TooltipDirective, TreeViewComponent, TypographyComponent, TypographyFontFamilyBase, TypographyFontFamilyMono, TypographyFontSize2xl, TypographyFontSize3xl, TypographyFontSize4xl, TypographyFontSize5xl, TypographyFontSize6xl, TypographyFontSizeBase, TypographyFontSizeLg, TypographyFontSizeSm, TypographyFontSizeXl, TypographyFontSizeXs, TypographyFontWeightBold, TypographyFontWeightMedium, TypographyFontWeightNormal, TypographyFontWeightSemibold, TypographyLineHeightNormal, TypographyLineHeightRelaxed, TypographyLineHeightTight, VerificationCodeInputComponent, WaterfallChartComponent, isValidIconName, resetIconWarnings, resolveFileIcon };
16734
16813
  //# sourceMappingURL=life-cockpit-angular-ui-kit.mjs.map