@ng-zen/cli 21.1.0 β†’ 21.1.1-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [21.1.1-next.1](https://github.com/kstepien3/ng-zen/compare/v21.1.0...v21.1.1-next.1) (2026-02-23)
2
+
3
+ ### πŸ› Bug Fixes
4
+
5
+ * **icon:** support all SVG shapes and normalize attribute naming ([#380](https://github.com/kstepien3/ng-zen/issues/380)) ([56cf31f](https://github.com/kstepien3/ng-zen/commit/56cf31ffbd59ca427ddb2f284f264a110d63ad4e))
6
+
7
+ ### πŸ“š Documentation
8
+
9
+ * **alert:** update example ([#382](https://github.com/kstepien3/ng-zen/issues/382)) ([3d29acb](https://github.com/kstepien3/ng-zen/commit/3d29acbd575d2a3eb3e5028f1abb9d0118918cc2))
10
+
1
11
  ## [21.1.0](https://github.com/kstepien3/ng-zen/compare/v21.0.0...v21.1.0) (2026-02-17)
2
12
 
3
13
  ### πŸš€ New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-zen/cli",
3
- "version": "21.1.0",
3
+ "version": "21.1.1-next.1",
4
4
  "description": "Angular UI components generator – Zen UI Kit CLI for schematics-based creation of customizable components like avatar, button, checkbox, divider, form-control, icon, input, skeleton, switch, textarea with Storybook demos.",
5
5
  "license": "BSD-2-Clause",
6
6
  "private": false,
@@ -1,4 +1,4 @@
1
- import * as icons from '@hugeicons/core-free-icons';
1
+ import { Notification02Icon } from '@hugeicons/core-free-icons';
2
2
  import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
3
3
 
4
4
  import { ZenIcon } from '../icon';
@@ -6,7 +6,6 @@ import { ZenAlert } from './alert';
6
6
 
7
7
  interface StoryParams {
8
8
  content: string;
9
- icon: string;
10
9
  title: string;
11
10
  }
12
11
 
@@ -17,7 +16,6 @@ export default {
17
16
  component: ZenAlert,
18
17
  decorators: [moduleMetadata({ imports: [ZenIcon] })],
19
18
  args: {
20
- icon: 'Notification02Icon',
21
19
  title: 'Alert Title',
22
20
  content: 'This is an alert message',
23
21
  },
@@ -31,16 +29,6 @@ export default {
31
29
  },
32
30
  },
33
31
  },
34
- icon: {
35
- control: 'select',
36
- options: Object.keys(icons),
37
- table: {
38
- category: 'story parameters',
39
- type: {
40
- summary: 'string',
41
- },
42
- },
43
- },
44
32
  title: {
45
33
  control: 'text',
46
34
  table: {
@@ -51,15 +39,17 @@ export default {
51
39
  },
52
40
  },
53
41
  },
54
- render: ({ content, icon, title }) => ({
55
- props: {},
56
- template: `
57
- <zen-alert>
58
- ${icon ? '<zen-icon alert-icon icon="' + icon + '" />' : ''}
59
- <h3 alert-title>${title}</h3>
60
- ${content}
61
- </zen-alert>`,
62
- }),
42
+ render: ({ content, title }) => {
43
+ return {
44
+ props: { Notification02Icon },
45
+ template: `
46
+ <zen-alert>
47
+ <zen-icon alert-icon [icon]="Notification02Icon" />
48
+ <h3 alert-title>${title}</h3>
49
+ ${content}
50
+ </zen-alert>`,
51
+ };
52
+ },
63
53
  } satisfies Meta<Options>;
64
54
 
65
55
  type Story = StoryObj<Options>;
@@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
9
9
  *
10
10
  * ```html
11
11
  * <zen-alert>
12
- * <zen-icon alert-icon icon="Notification02Icon" />
12
+ * <zen-icon alert-icon [icon]="Notification02Icon" />
13
13
  * <h3 alert-title>Title</h3>
14
14
  * content
15
15
  * </zen-alert>
@@ -1,5 +1,6 @@
1
1
  import { provideZonelessChangeDetection } from '@angular/core';
2
2
  import { TestBed } from '@angular/core/testing';
3
+ import { HomeIcon } from '@hugeicons/core-free-icons';
3
4
  import { beforeEach, describe, expect, it } from 'vitest';
4
5
 
5
6
  import { ZenIcon } from './icon';
@@ -15,7 +16,7 @@ describe('ZenIcon', () => {
15
16
  it('should create', () => {
16
17
  const fixture = TestBed.createComponent(ZenIcon);
17
18
  const component = fixture.componentInstance;
18
- fixture.componentRef.setInput('icon', 'Tree02Icon');
19
+ fixture.componentRef.setInput('icon', HomeIcon);
19
20
  expect(component).toBeTruthy();
20
21
  });
21
22
  });
@@ -4,19 +4,30 @@ import { Meta, StoryObj } from '@storybook/angular';
4
4
  import { ZenIcon } from './icon';
5
5
 
6
6
  type Options = ZenIcon;
7
+ const iconKeys = Object.keys(icons) as (keyof typeof icons)[];
8
+ const iconMapping = icons as Record<string, unknown>;
9
+ const iconLabels = Object.fromEntries(iconKeys.map(k => [k as string, String(k).replace(/Icon$/, '')])) as Record<
10
+ string,
11
+ string
12
+ >;
13
+
14
+ console.log(iconKeys);
7
15
 
8
16
  export default {
9
17
  title: 'Ui/Icon',
10
18
  component: ZenIcon,
11
19
  argTypes: {
12
20
  icon: {
13
- control: 'select',
14
- options: Object.keys(icons),
21
+ control: {
22
+ type: 'select',
23
+ labels: iconLabels,
24
+ },
25
+ options: iconKeys,
26
+ mapping: iconMapping,
15
27
  table: {
16
- type: { summary: 'string' },
28
+ type: { summary: 'IconSvgObject' },
17
29
  category: 'inputs',
18
30
  subcategory: 'required',
19
- defaultValue: { summary: '' },
20
31
  },
21
32
  },
22
33
  size: {
@@ -40,7 +51,7 @@ export default {
40
51
  size: 24,
41
52
  strokeWidth: 1.5,
42
53
  absoluteStrokeWidth: false,
43
- icon: 'Tree02Icon',
54
+ icon: 'Tree02Icon' as never,
44
55
  },
45
56
  } satisfies Meta<Options>;
46
57
 
@@ -1,55 +1,50 @@
1
- import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
- import * as icons from '@hugeicons/core-free-icons';
1
+ import { booleanAttribute, ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
2
+ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
3
3
 
4
- type Icon = keyof typeof icons;
5
- interface Path {
6
- d: string | number;
7
- fill: string | number;
8
- opacity: string | number;
9
- fillRule: string | number;
10
- strokeWidth?: number;
11
- }
4
+ /** SVG icon data structure from HugeIcons library - an array of [tagName, attributes] tuples. */
5
+ export type IconSvgObject = readonly (readonly [string, Record<string, string | number>])[];
12
6
 
13
7
  /**
14
8
  * A reusable Angular component for rendering icons from the Hugeicons library.
15
9
  *
16
- * This component renders an SVG icon from the `@hugeicons/core-free-icons` collection by name.
17
- * The size, stroke width, and color are configurable through inputs. It dynamically generates the SVG
18
- * and its paths, providing a flexible and efficient way to use Hugeicons in an Angular application.
10
+ * This component renders an SVG icon by accepting an imported SVG object from Ui Libraries
11
+ * The size, stroke width, and color are configurable.
12
+
19
13
  *
20
14
  * @example
21
- * <zen-icon icon="Tree02Icon" />
15
+ * ```html
16
+ * <zen-icon [icon]="SvgObject" />
17
+ * ```
18
+ *
19
+ * Tested with `@hugeicons/core-free-icons`.
22
20
  *
23
- * @see [Hugeicons](https://hugeicons.com)
21
+ * @author Konrad StΔ™pieΕ„
22
+ * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
23
+ * @see [GitHub](https://github.com/kstepien3/ng-zen)
24
24
  */
25
25
  @Component({
26
26
  selector: 'zen-icon',
27
27
  template: `
28
28
  <svg
29
- [attr.color]="color()"
30
29
  [attr.height]="size()"
30
+ [attr.stroke-width]="calculatedStrokeWidth()"
31
31
  [attr.viewBox]="'0 0 ' + BASE_SIZE + ' ' + BASE_SIZE"
32
32
  [attr.width]="size()"
33
+ [innerHTML]="safeSvgContent()"
34
+ [style.color]="color()"
33
35
  fill="none"
36
+ stroke="currentColor"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
34
39
  xmlns="http://www.w3.org/2000/svg"
35
- >
36
- @for (path of paths(); track path) {
37
- <path
38
- [attr.d]="path.d"
39
- [attr.fill-rule]="path.fillRule"
40
- [attr.fill]="path.fill"
41
- [attr.opacity]="path.opacity"
42
- [attr.stroke-width]="path.strokeWidth"
43
- stroke="currentColor"
44
- />
45
- }
46
- </svg>
40
+ ></svg>
47
41
  `,
42
+ styles: [':host { display: inline-flex; }'],
48
43
  changeDetection: ChangeDetectionStrategy.OnPush,
49
44
  })
50
45
  export class ZenIcon {
51
- /** Icon file names from HugeIcons */
52
- readonly icon = input.required({ transform: (icon: Icon) => icons[icon] });
46
+ /** SVG object imported from HugeIcons library. */
47
+ readonly icon = input.required<IconSvgObject>();
53
48
  /** Size of the icon in pixels. */
54
49
  readonly size = input<number>(24);
55
50
  /** Determines if stroke width scales with icon size. */
@@ -60,22 +55,35 @@ export class ZenIcon {
60
55
  readonly color = input<string>('currentColor');
61
56
 
62
57
  protected readonly BASE_SIZE = 24;
63
- protected readonly paths = computed<Path[]>(() => this.updatePaths());
64
- private readonly calculatedStrokeWidth = computed(() => this.calculateStrokeWidth());
65
58
 
66
- private updatePaths(): Path[] {
67
- return this.icon().map(([, attrs]) => ({
68
- d: attrs['d'],
69
- fill: attrs['fill'] ?? 'none',
70
- opacity: attrs['opacity'],
71
- fillRule: attrs['fillRule'],
72
- strokeWidth: this.calculatedStrokeWidth(),
73
- }));
74
- }
59
+ private readonly sanitizer = inject(DomSanitizer);
75
60
 
76
- private calculateStrokeWidth(): number {
61
+ protected readonly calculatedStrokeWidth = computed(() => {
77
62
  if (!this.absoluteStrokeWidth()) return this.strokeWidth();
78
-
79
63
  return (this.strokeWidth() * this.BASE_SIZE) / this.size();
64
+ });
65
+
66
+ /** Converts icon data into a sanitized HTML string of SVG elements. */
67
+ protected readonly safeSvgContent = computed<SafeHtml>(() => {
68
+ const htmlString = this.icon()
69
+ .map(([tag, attrs]) => this.buildTag(tag, attrs))
70
+ .join('');
71
+
72
+ return this.sanitizer.bypassSecurityTrustHtml(htmlString);
73
+ });
74
+
75
+ /** Generates a single SVG element string (e.g., <path ...>) with its mapped attributes. */
76
+ private buildTag(tag: string, attrs: Record<string, string | number>): string {
77
+ const attributes = Object.entries(attrs)
78
+ .filter(([key]) => !['strokeWidth', 'stroke-width'].includes(key))
79
+ .map(([key, value]) => `${this.toKebabCase(key)}="${value}"`)
80
+ .join(' ');
81
+
82
+ return `<${tag} ${attributes} stroke-width="${this.calculatedStrokeWidth()}"></${tag}>`;
83
+ }
84
+
85
+ /** Formats attribute names from camelCase (used in JS library) to a kebab-case for SVG compatibility. */
86
+ private toKebabCase(str: string): string {
87
+ return str.replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
80
88
  }
81
89
  }