Skip to content

Commit f6a6551

Browse files
committed
feat: add spartan dropdown menu
1 parent 08a75e4 commit f6a6551

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1175
-46
lines changed

libs/ui/ui-icon-helm/.eslintrc.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"extends": [
3+
"../../../.eslintrc.base.json"
4+
],
5+
"ignorePatterns": [
6+
"!**/*"
7+
],
8+
"overrides": [
9+
{
10+
"files": [
11+
"*.ts"
12+
],
13+
"extends": [
14+
"plugin:@nx/angular",
15+
"plugin:@angular-eslint/template/process-inline-templates"
16+
],
17+
"rules": {
18+
"@angular-eslint/directive-selector": [
19+
"error",
20+
{
21+
"type": "attribute",
22+
"prefix": "hlm",
23+
"style": "camelCase"
24+
}
25+
],
26+
"@angular-eslint/component-selector": [
27+
"error",
28+
{
29+
"type": "element",
30+
"prefix": "hlm",
31+
"style": "kebab-case"
32+
}
33+
]
34+
}
35+
},
36+
{
37+
"files": [
38+
"*.html"
39+
],
40+
"extends": [
41+
"plugin:@nx/angular-template"
42+
],
43+
"rules": {}
44+
},
45+
{
46+
"files": [
47+
"*.json"
48+
],
49+
"parser": "jsonc-eslint-parser",
50+
"rules": {
51+
"@nx/dependency-checks": "error"
52+
}
53+
}
54+
]
55+
}

libs/ui/ui-icon-helm/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# ui-icon-helm
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
6+
## Running unit tests
7+
8+
Run `nx test ui-icon-helm` to execute the unit tests.
9+

libs/ui/ui-icon-helm/jest.config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'ui-icon-helm',
4+
preset: '../../../jest.preset.js',
5+
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
6+
coverageDirectory: '../../../coverage/libs/ui/ui-icon-helm',
7+
transform: {
8+
'^.+\\.(ts|mjs|js|html)$': [
9+
'jest-preset-angular',
10+
{
11+
tsconfig: '<rootDir>/tsconfig.spec.json',
12+
stringifyContentPathRegex: '\\.(html|svg)$',
13+
},
14+
],
15+
},
16+
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
17+
snapshotSerializers: [
18+
'jest-preset-angular/build/serializers/no-ng-attributes',
19+
'jest-preset-angular/build/serializers/ng-snapshot',
20+
'jest-preset-angular/build/serializers/html-comment',
21+
]
22+
};

libs/ui/ui-icon-helm/ng-package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
3+
"dest": "../../../dist/libs/ui/ui-icon-helm",
4+
"lib": {
5+
"entryFile": "src/index.ts"
6+
}
7+
}

libs/ui/ui-icon-helm/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@spartan-ng/ui-icon-helm",
3+
"version": "0.0.1",
4+
"peerDependencies": {
5+
"@angular/common": "^18.0.0",
6+
"@angular/core": "^18.0.0"
7+
},
8+
"sideEffects": false
9+
}

libs/ui/ui-icon-helm/project.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "ui-icon-helm",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "libs/ui/ui-icon-helm/src",
5+
"prefix": "hlm",
6+
"projectType": "library",
7+
"tags": [],
8+
"targets": {
9+
"build": {
10+
"executor": "@nx/angular:ng-packagr-lite",
11+
"outputs": [
12+
"{workspaceRoot}/dist/{projectRoot}"
13+
],
14+
"options": {
15+
"project": "libs/ui/ui-icon-helm/ng-package.json"
16+
},
17+
"configurations": {
18+
"production": {
19+
"tsConfig": "libs/ui/ui-icon-helm/tsconfig.lib.prod.json"
20+
},
21+
"development": {
22+
"tsConfig": "libs/ui/ui-icon-helm/tsconfig.lib.json"
23+
}
24+
},
25+
"defaultConfiguration": "production"
26+
},
27+
"test": {
28+
"executor": "@nx/jest:jest",
29+
"outputs": [
30+
"{workspaceRoot}/coverage/{projectRoot}"
31+
],
32+
"options": {
33+
"jestConfig": "libs/ui/ui-icon-helm/jest.config.ts"
34+
}
35+
},
36+
"lint": {
37+
"executor": "@nx/eslint:lint"
38+
}
39+
}
40+
}

libs/ui/ui-icon-helm/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NgModule } from '@angular/core';
2+
import { provideIcons as provideIconsImport } from '@ng-icons/core';
3+
import { HlmIconComponent } from './lib/hlm-icon.component';
4+
5+
export * from './lib/hlm-icon.component';
6+
7+
export const provideIcons = provideIconsImport;
8+
9+
@NgModule({
10+
imports: [HlmIconComponent],
11+
exports: [HlmIconComponent],
12+
})
13+
export class HlmIconModule {}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2+
import { By } from '@angular/platform-browser';
3+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
4+
import { lucideCheck } from '@ng-icons/lucide';
5+
import { type RenderResult, render } from '@testing-library/angular';
6+
import { HlmIconComponent } from './hlm-icon.component';
7+
8+
@Component({
9+
selector: 'hlm-mock',
10+
standalone: true,
11+
changeDetection: ChangeDetectionStrategy.OnPush,
12+
imports: [HlmIconComponent],
13+
providers: [provideIcons({ lucideCheck })],
14+
template: `
15+
<hlm-icon class="test" ngIconClass="test2" name="lucideCheck" [size]="size" color="red" strokeWidth="2" />
16+
`,
17+
})
18+
class HlmMockComponent {
19+
@Input() size = 'base';
20+
}
21+
22+
describe('HlmIconComponent', () => {
23+
let r: RenderResult<HlmMockComponent>;
24+
25+
beforeEach(async () => {
26+
r = await render(HlmMockComponent);
27+
});
28+
29+
it('should create', () => {
30+
expect(r).toBeTruthy();
31+
});
32+
33+
it('should render the icon', () => {
34+
expect(r.container.querySelector('svg')).toBeTruthy();
35+
});
36+
37+
it('should pass the size, color and strokeWidth props and the classes to the ng-icon component', () => {
38+
const debugEl = r.fixture.debugElement.query(By.directive(NgIconComponent));
39+
const component = debugEl.componentInstance as NgIconComponent;
40+
expect(component.color).toBe('red');
41+
expect(component.strokeWidth).toBe('2');
42+
expect(component.size).toBe('100%');
43+
expect(debugEl.nativeElement.classList).toContain('test2');
44+
});
45+
46+
it('should add the appropriate size variant class', () => {
47+
expect(r.container.querySelector('hlm-icon')?.classList).toContain('h-6');
48+
expect(r.container.querySelector('hlm-icon')?.classList).toContain('w-6');
49+
});
50+
51+
it('should compose the user classes', () => {
52+
expect(r.container.querySelector('hlm-icon')?.classList).toContain('inline-flex');
53+
expect(r.container.querySelector('hlm-icon')?.classList).toContain('test');
54+
});
55+
56+
it('should forward the size property if the size is not a pre-defined size', async () => {
57+
await r.rerender({ componentInputs: { size: '2rem' } });
58+
r.fixture.detectChanges();
59+
const debugEl = r.fixture.debugElement.query(By.directive(NgIconComponent));
60+
expect(debugEl.componentInstance.size).toBe('2rem');
61+
expect(r.container.querySelector('hlm-icon')?.classList).not.toContain('h-6');
62+
expect(r.container.querySelector('hlm-icon')?.classList).not.toContain('w-6');
63+
});
64+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { isPlatformBrowser } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
ElementRef,
6+
Input,
7+
type OnDestroy,
8+
PLATFORM_ID,
9+
ViewEncapsulation,
10+
computed,
11+
inject,
12+
signal,
13+
} from '@angular/core';
14+
import { type IconName, NgIconComponent } from '@ng-icons/core';
15+
import { hlm } from '@spartan-ng/ui-core';
16+
import { cva } from 'class-variance-authority';
17+
import type { ClassValue } from 'clsx';
18+
19+
const DEFINED_SIZES = ['xs', 'sm', 'base', 'lg', 'xl', 'none'] as const;
20+
21+
type DefinedSizes = (typeof DEFINED_SIZES)[number];
22+
23+
export const iconVariants = cva('inline-flex', {
24+
variants: {
25+
variant: {
26+
xs: 'h-3 w-3',
27+
sm: 'h-4 w-4',
28+
base: 'h-6 w-6',
29+
lg: 'h-8 w-8',
30+
xl: 'h-12 w-12',
31+
none: '',
32+
} satisfies Record<DefinedSizes, string>,
33+
},
34+
defaultVariants: {
35+
variant: 'base',
36+
},
37+
});
38+
39+
// eslint-disable-next-line @typescript-eslint/ban-types
40+
export type IconSize = DefinedSizes | (Record<never, never> & string);
41+
42+
const isDefinedSize = (size: IconSize): size is DefinedSizes => {
43+
return DEFINED_SIZES.includes(size as DefinedSizes);
44+
};
45+
46+
const TAILWIND_H_W_PATTERN = /\b(h-\d+|w-\d+)\b/g;
47+
48+
@Component({
49+
selector: 'hlm-icon',
50+
standalone: true,
51+
imports: [NgIconComponent],
52+
encapsulation: ViewEncapsulation.None,
53+
changeDetection: ChangeDetectionStrategy.OnPush,
54+
template: `
55+
<ng-icon
56+
[class]="ngIconCls()"
57+
[size]="ngIconSize()"
58+
[name]="_name()"
59+
[color]="_color()"
60+
[strokeWidth]="_strokeWidth()"
61+
/>
62+
`,
63+
host: {
64+
'[class]': '_computedClass()',
65+
},
66+
})
67+
export class HlmIconComponent implements OnDestroy {
68+
private readonly _host = inject(ElementRef);
69+
private readonly _platformId = inject(PLATFORM_ID);
70+
71+
private _mutObs?: MutationObserver;
72+
73+
private readonly _hostClasses = signal<string>('');
74+
75+
protected readonly _name = signal<IconName | string>('');
76+
protected readonly _size = signal<IconSize>('base');
77+
protected readonly _color = signal<string | undefined>(undefined);
78+
protected readonly _strokeWidth = signal<string | number | undefined>(undefined);
79+
protected readonly userCls = signal<ClassValue>('');
80+
protected readonly ngIconSize = computed(() => (isDefinedSize(this._size()) ? '100%' : (this._size() as string)));
81+
protected readonly ngIconCls = signal<ClassValue>('');
82+
83+
protected readonly _computedClass = computed(() => {
84+
const size: IconSize = this._size();
85+
const hostClasses = this._hostClasses();
86+
const userCls = this.userCls();
87+
const variant = isDefinedSize(size) ? size : 'none';
88+
const classes = variant === 'none' && size === 'none' ? hostClasses : hostClasses.replace(TAILWIND_H_W_PATTERN, '');
89+
return hlm(iconVariants({ variant }), userCls, classes);
90+
});
91+
92+
constructor() {
93+
if (isPlatformBrowser(this._platformId)) {
94+
this._mutObs = new MutationObserver((mutations: MutationRecord[]) => {
95+
mutations.forEach((mutation: MutationRecord) => {
96+
if (mutation.attributeName !== 'class') return;
97+
this._hostClasses.set((mutation.target as Node & { className?: string })?.className ?? '');
98+
});
99+
});
100+
this._mutObs.observe(this._host.nativeElement, {
101+
attributes: true,
102+
});
103+
}
104+
}
105+
106+
ngOnDestroy() {
107+
this._mutObs?.disconnect();
108+
this._mutObs = undefined;
109+
}
110+
111+
@Input()
112+
set name(value: IconName | string) {
113+
this._name.set(value);
114+
}
115+
116+
@Input()
117+
set size(value: IconSize) {
118+
this._size.set(value);
119+
}
120+
121+
@Input()
122+
set color(value: string | undefined) {
123+
this._color.set(value);
124+
}
125+
126+
@Input()
127+
set strokeWidth(value: string | number | undefined) {
128+
this._strokeWidth.set(value);
129+
}
130+
131+
@Input()
132+
set ngIconClass(cls: ClassValue) {
133+
this.ngIconCls.set(cls);
134+
}
135+
136+
@Input()
137+
set class(cls: ClassValue) {
138+
this.userCls.set(cls);
139+
}
140+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'jest-preset-angular/setup-jest';

libs/ui/ui-icon-helm/tsconfig.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"useDefineForClassFields": false
5+
},
6+
"files": [],
7+
"include": [],
8+
"references": [
9+
{
10+
"path": "./tsconfig.lib.json"
11+
},
12+
{
13+
"path": "./tsconfig.spec.json"
14+
}
15+
],
16+
"extends": "../../../tsconfig.base.json"
17+
}

0 commit comments

Comments
 (0)