@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 +10 -0
- package/package.json +1 -1
- package/schematics/ui/files/alert/alert.stories.ts +12 -22
- package/schematics/ui/files/alert/alert.ts +1 -1
- package/schematics/ui/files/icon/icon.spec.ts +2 -1
- package/schematics/ui/files/icon/icon.stories.ts +16 -5
- package/schematics/ui/files/icon/icon.ts +51 -43
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.
|
|
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
|
|
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,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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',
|
|
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:
|
|
14
|
-
|
|
21
|
+
control: {
|
|
22
|
+
type: 'select',
|
|
23
|
+
labels: iconLabels,
|
|
24
|
+
},
|
|
25
|
+
options: iconKeys,
|
|
26
|
+
mapping: iconMapping,
|
|
15
27
|
table: {
|
|
16
|
-
type: { summary: '
|
|
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
|
|
1
|
+
import { booleanAttribute, ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
|
|
2
|
+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
17
|
-
* The size, stroke width, and color are configurable
|
|
18
|
-
|
|
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
|
-
*
|
|
15
|
+
* ```html
|
|
16
|
+
* <zen-icon [icon]="SvgObject" />
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Tested with `@hugeicons/core-free-icons`.
|
|
22
20
|
*
|
|
23
|
-
* @
|
|
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
|
-
/**
|
|
52
|
-
readonly icon = input.required(
|
|
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
|
|
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
|
-
|
|
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
|
}
|