Răsfoiți Sursa

chore(admin-ui): Add more spartan components

David Höck 11 luni în urmă
părinte
comite
97648b9ac9
22 a modificat fișierele cu 578 adăugiri și 0 ștergeri
  1. 37 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/index.ts
  2. 20 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-close.directive.ts
  3. 52 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-content.component.ts
  4. 17 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-description.directive.ts
  5. 20 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-footer.component.ts
  6. 20 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-header.component.ts
  7. 23 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-overlay.directive.ts
  8. 17 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-title.directive.ts
  9. 28 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts
  10. 35 0
      packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog.service.ts
  11. 10 0
      packages/admin-ui/src/lib/ui/ui-scrollarea-helm/src/index.ts
  12. 20 0
      packages/admin-ui/src/lib/ui/ui-scrollarea-helm/src/lib/hlm-scroll-area.directive.ts
  13. 38 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/index.ts
  14. 26 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-content.directive.ts
  15. 17 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-group.directive.ts
  16. 26 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-label.directive.ts
  17. 41 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-option.component.ts
  18. 18 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-scroll-down.component.ts
  19. 18 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-scroll-up.component.ts
  20. 60 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-trigger.component.ts
  21. 20 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-value.directive.ts
  22. 15 0
      packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select.directive.ts

+ 37 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/index.ts

@@ -0,0 +1,37 @@
+import { NgModule } from '@angular/core';
+
+import { HlmDialogCloseDirective } from './lib/hlm-dialog-close.directive';
+import { HlmDialogContentComponent } from './lib/hlm-dialog-content.component';
+import { HlmDialogDescriptionDirective } from './lib/hlm-dialog-description.directive';
+import { HlmDialogFooterComponent } from './lib/hlm-dialog-footer.component';
+import { HlmDialogHeaderComponent } from './lib/hlm-dialog-header.component';
+import { HlmDialogOverlayDirective } from './lib/hlm-dialog-overlay.directive';
+import { HlmDialogTitleDirective } from './lib/hlm-dialog-title.directive';
+import { HlmDialogComponent } from './lib/hlm-dialog.component';
+
+export * from './lib/hlm-dialog-close.directive';
+export * from './lib/hlm-dialog-content.component';
+export * from './lib/hlm-dialog-description.directive';
+export * from './lib/hlm-dialog-footer.component';
+export * from './lib/hlm-dialog-header.component';
+export * from './lib/hlm-dialog-overlay.directive';
+export * from './lib/hlm-dialog-title.directive';
+export * from './lib/hlm-dialog.component';
+export * from './lib/hlm-dialog.service';
+
+export const HlmDialogImports = [
+	HlmDialogComponent,
+	HlmDialogCloseDirective,
+	HlmDialogContentComponent,
+	HlmDialogDescriptionDirective,
+	HlmDialogFooterComponent,
+	HlmDialogHeaderComponent,
+	HlmDialogOverlayDirective,
+	HlmDialogTitleDirective,
+] as const;
+
+@NgModule({
+	imports: [...HlmDialogImports],
+	exports: [...HlmDialogImports],
+})
+export class HlmDialogModule {}

+ 20 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-close.directive.ts

@@ -0,0 +1,20 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: '[hlmDialogClose],[brnDialogClose][hlm]',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmDialogCloseDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected _computedClass = computed(() =>
+		hlm(
+			'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
+			this.userClass(),
+		),
+	);
+}

+ 52 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-content.component.ts

@@ -0,0 +1,52 @@
+import { NgComponentOutlet } from '@angular/common';
+import { ChangeDetectionStrategy, Component, ViewEncapsulation, computed, inject, input } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideX } from '@ng-icons/lucide';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnDialogCloseDirective, BrnDialogRef, injectBrnDialogContext } from '@spartan-ng/brain/dialog';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+import type { ClassValue } from 'clsx';
+import { HlmDialogCloseDirective } from './hlm-dialog-close.directive';
+
+@Component({
+	selector: 'hlm-dialog-content',
+	standalone: true,
+	imports: [NgComponentOutlet, BrnDialogCloseDirective, HlmDialogCloseDirective, NgIcon, HlmIconDirective],
+	providers: [provideIcons({ lucideX })],
+	host: {
+		'[class]': '_computedClass()',
+		'[attr.data-state]': 'state()',
+	},
+	template: `
+		@if (component) {
+			<ng-container [ngComponentOutlet]="component" />
+		} @else {
+			<ng-content />
+		}
+
+		<button brnDialogClose hlm>
+			<span class="sr-only">Close</span>
+			<ng-icon hlm size="sm" name="lucideX" />
+		</button>
+	`,
+	changeDetection: ChangeDetectionStrategy.OnPush,
+	encapsulation: ViewEncapsulation.None,
+})
+export class HlmDialogContentComponent {
+	private readonly _dialogRef = inject(BrnDialogRef);
+	private readonly _dialogContext = injectBrnDialogContext({ optional: true });
+
+	public readonly state = computed(() => this._dialogRef?.state() ?? 'closed');
+
+	public readonly component = this._dialogContext?.$component;
+	private readonly _dynamicComponentClass = this._dialogContext?.$dynamicComponentClass;
+
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() =>
+		hlm(
+			'border-border grid w-full max-w-lg relative gap-4 border bg-background p-6 shadow-lg [animation-duration:200] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[2%]  data-[state=open]:slide-in-from-top-[2%] sm:rounded-lg md:w-full',
+			this.userClass(),
+			this._dynamicComponentClass,
+		),
+	);
+}

+ 17 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-description.directive.ts

@@ -0,0 +1,17 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnDialogDescriptionDirective } from '@spartan-ng/brain/dialog';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: '[hlmDialogDescription]',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+	hostDirectives: [BrnDialogDescriptionDirective],
+})
+export class HlmDialogDescriptionDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected _computedClass = computed(() => hlm('text-sm text-muted-foreground', this.userClass()));
+}

+ 20 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-footer.component.ts

@@ -0,0 +1,20 @@
+import { Component, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Component({
+	selector: 'hlm-dialog-footer',
+	standalone: true,
+	template: `
+		<ng-content />
+	`,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmDialogFooterComponent {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected _computedClass = computed(() =>
+		hlm('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', this.userClass()),
+	);
+}

+ 20 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-header.component.ts

@@ -0,0 +1,20 @@
+import { Component, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Component({
+	selector: 'hlm-dialog-header',
+	standalone: true,
+	template: `
+		<ng-content />
+	`,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmDialogHeaderComponent {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected _computedClass = computed(() =>
+		hlm('flex flex-col space-y-1.5 text-center sm:text-left', this.userClass()),
+	);
+}

+ 23 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-overlay.directive.ts

@@ -0,0 +1,23 @@
+import { Directive, computed, effect, input } from '@angular/core';
+import { hlm, injectCustomClassSettable } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+export const hlmDialogOverlayClass =
+	'bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0';
+
+@Directive({
+	selector: '[hlmDialogOverlay],brn-dialog-overlay[hlm]',
+	standalone: true,
+})
+export class HlmDialogOverlayDirective {
+	private readonly _classSettable = injectCustomClassSettable({ optional: true, host: true });
+
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() => hlm(hlmDialogOverlayClass, this.userClass()));
+
+	constructor() {
+		effect(() => {
+			this._classSettable?.setClassToCustomElement(this._computedClass());
+		});
+	}
+}

+ 17 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog-title.directive.ts

@@ -0,0 +1,17 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnDialogTitleDirective } from '@spartan-ng/brain/dialog';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: '[hlmDialogTitle]',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+	hostDirectives: [BrnDialogTitleDirective],
+})
+export class HlmDialogTitleDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected _computedClass = computed(() => hlm('text-lg font-semibold leading-none tracking-tight', this.userClass()));
+}

+ 28 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts

@@ -0,0 +1,28 @@
+import { ChangeDetectionStrategy, Component, ViewEncapsulation, forwardRef } from '@angular/core';
+import { BrnDialogComponent, BrnDialogOverlayComponent } from '@spartan-ng/brain/dialog';
+import { HlmDialogOverlayDirective } from './hlm-dialog-overlay.directive';
+
+@Component({
+	selector: 'hlm-dialog',
+	standalone: true,
+	imports: [BrnDialogOverlayComponent, HlmDialogOverlayDirective],
+	providers: [
+		{
+			provide: BrnDialogComponent,
+			useExisting: forwardRef(() => HlmDialogComponent),
+		},
+	],
+	template: `
+		<brn-dialog-overlay hlm />
+		<ng-content />
+	`,
+	changeDetection: ChangeDetectionStrategy.OnPush,
+	encapsulation: ViewEncapsulation.None,
+	exportAs: 'hlmDialog',
+})
+export class HlmDialogComponent extends BrnDialogComponent {
+	constructor() {
+		super();
+		this.closeDelay = 100;
+	}
+}

+ 35 - 0
packages/admin-ui/src/lib/ui/ui-dialog-helm/src/lib/hlm-dialog.service.ts

@@ -0,0 +1,35 @@
+import type { ComponentType } from '@angular/cdk/portal';
+import { Injectable, type TemplateRef, inject } from '@angular/core';
+import {
+	type BrnDialogOptions,
+	BrnDialogService,
+	DEFAULT_BRN_DIALOG_OPTIONS,
+	cssClassesToArray,
+} from '@spartan-ng/brain/dialog';
+import { HlmDialogContentComponent } from './hlm-dialog-content.component';
+import { hlmDialogOverlayClass } from './hlm-dialog-overlay.directive';
+
+export type HlmDialogOptions<DialogContext = unknown> = BrnDialogOptions & {
+	contentClass?: string;
+	context?: DialogContext;
+};
+
+@Injectable({
+	providedIn: 'root',
+})
+export class HlmDialogService {
+	private readonly _brnDialogService = inject(BrnDialogService);
+
+	public open(component: ComponentType<unknown> | TemplateRef<unknown>, options?: Partial<HlmDialogOptions>) {
+		const mergedOptions = {
+			...DEFAULT_BRN_DIALOG_OPTIONS,
+			closeDelay: 100,
+
+			...(options ?? {}),
+			backdropClass: cssClassesToArray(`${hlmDialogOverlayClass} ${options?.backdropClass ?? ''}`),
+			context: { ...(options?.context ?? {}), $component: component, $dynamicComponentClass: options?.contentClass },
+		};
+
+		return this._brnDialogService.open(HlmDialogContentComponent, undefined, mergedOptions.context, mergedOptions);
+	}
+}

+ 10 - 0
packages/admin-ui/src/lib/ui/ui-scrollarea-helm/src/index.ts

@@ -0,0 +1,10 @@
+import { NgModule } from '@angular/core';
+import { HlmScrollAreaDirective } from './lib/hlm-scroll-area.directive';
+
+export * from './lib/hlm-scroll-area.directive';
+
+@NgModule({
+	imports: [HlmScrollAreaDirective],
+	exports: [HlmScrollAreaDirective],
+})
+export class HlmScrollAreaModule {}

+ 20 - 0
packages/admin-ui/src/lib/ui/ui-scrollarea-helm/src/lib/hlm-scroll-area.directive.ts

@@ -0,0 +1,20 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: 'ng-scrollbar[hlm]',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+		'[style.--scrollbar-border-radius.px]': '100',
+		'[style.--scrollbar-offset]': '3',
+		'[style.--scrollbar-thumb-color]': '"hsl(var(--border))"',
+		'[style.--scrollbar-thumb-hover-color]': '"hsl(var(--border))"',
+		'[style.--scrollbar-thickness]': '7',
+	},
+})
+export class HlmScrollAreaDirective {
+	protected readonly _computedClass = computed(() => hlm('block', this.userClass()));
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+}

+ 38 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/index.ts

@@ -0,0 +1,38 @@
+import { NgModule } from '@angular/core';
+import { HlmSelectContentDirective } from './lib/hlm-select-content.directive';
+import { HlmSelectGroupDirective } from './lib/hlm-select-group.directive';
+import { HlmSelectLabelDirective } from './lib/hlm-select-label.directive';
+import { HlmSelectOptionComponent } from './lib/hlm-select-option.component';
+import { HlmSelectScrollDownComponent } from './lib/hlm-select-scroll-down.component';
+import { HlmSelectScrollUpComponent } from './lib/hlm-select-scroll-up.component';
+import { HlmSelectTriggerComponent } from './lib/hlm-select-trigger.component';
+import { HlmSelectValueDirective } from './lib/hlm-select-value.directive';
+import { HlmSelectDirective } from './lib/hlm-select.directive';
+
+export * from './lib/hlm-select-content.directive';
+export * from './lib/hlm-select-group.directive';
+export * from './lib/hlm-select-label.directive';
+export * from './lib/hlm-select-option.component';
+export * from './lib/hlm-select-scroll-down.component';
+export * from './lib/hlm-select-scroll-up.component';
+export * from './lib/hlm-select-trigger.component';
+export * from './lib/hlm-select-value.directive';
+export * from './lib/hlm-select.directive';
+
+export const HlmSelectImports = [
+	HlmSelectContentDirective,
+	HlmSelectTriggerComponent,
+	HlmSelectOptionComponent,
+	HlmSelectValueDirective,
+	HlmSelectDirective,
+	HlmSelectScrollUpComponent,
+	HlmSelectScrollDownComponent,
+	HlmSelectLabelDirective,
+	HlmSelectGroupDirective,
+] as const;
+
+@NgModule({
+	imports: [...HlmSelectImports],
+	exports: [...HlmSelectImports],
+})
+export class HlmSelectModule {}

+ 26 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-content.directive.ts

@@ -0,0 +1,26 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm, injectExposedSideProvider, injectExposesStateProvider } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: '[hlmSelectContent], hlm-select-content',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+		'[attr.data-state]': '_stateProvider?.state() ?? "open"',
+		'[attr.data-side]': '_sideProvider?.side() ?? "bottom"',
+	},
+})
+export class HlmSelectContentDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	public readonly stickyLabels = input<boolean>(false);
+	protected readonly _stateProvider = injectExposesStateProvider({ optional: true });
+	protected readonly _sideProvider = injectExposedSideProvider({ optional: true });
+
+	protected readonly _computedClass = computed(() =>
+		hlm(
+			'w-full relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md p-1 data-[side=bottom]:top-[2px] data-[side=top]:bottom-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
+			this.userClass(),
+		),
+	);
+}

+ 17 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-group.directive.ts

@@ -0,0 +1,17 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnSelectGroupDirective } from '@spartan-ng/brain/select';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: '[hlmSelectGroup], hlm-select-group',
+	hostDirectives: [BrnSelectGroupDirective],
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmSelectGroupDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() => hlm(this.userClass()));
+}

+ 26 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-label.directive.ts

@@ -0,0 +1,26 @@
+import { Directive, computed, inject, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnSelectLabelDirective } from '@spartan-ng/brain/select';
+import type { ClassValue } from 'clsx';
+import { HlmSelectContentDirective } from './hlm-select-content.directive';
+
+@Directive({
+	selector: '[hlmSelectLabel], hlm-select-label',
+	hostDirectives: [BrnSelectLabelDirective],
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmSelectLabelDirective {
+	private readonly _selectContent = inject(HlmSelectContentDirective);
+	private readonly _stickyLabels = computed(() => this._selectContent.stickyLabels());
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected _computedClass = computed(() =>
+		hlm(
+			'pl-8 pr-2 text-sm font-semibold rtl:pl-2 rtl:pr-8',
+			this._stickyLabels() ? 'sticky top-0 bg-popover block z-[2]' : '',
+			this.userClass(),
+		),
+	);
+}

+ 41 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-option.component.ts

@@ -0,0 +1,41 @@
+import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideCheck } from '@ng-icons/lucide';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnSelectOptionDirective } from '@spartan-ng/brain/select';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+import type { ClassValue } from 'clsx';
+
+@Component({
+	selector: 'hlm-option',
+	standalone: true,
+	changeDetection: ChangeDetectionStrategy.OnPush,
+	hostDirectives: [{ directive: BrnSelectOptionDirective, inputs: ['disabled', 'value'] }],
+	providers: [provideIcons({ lucideCheck })],
+	host: {
+		'[class]': '_computedClass()',
+	},
+	template: `
+		<ng-content />
+		<span
+			[attr.dir]="_brnSelectOption.dir()"
+			class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center rtl:left-auto rtl:right-2"
+			[attr.data-state]="this._brnSelectOption.checkedState()"
+		>
+			@if (this._brnSelectOption.selected()) {
+				<ng-icon hlm aria-hidden="true" name="lucideCheck" />
+			}
+		</span>
+	`,
+	imports: [NgIcon, HlmIconDirective],
+})
+export class HlmSelectOptionComponent {
+	protected readonly _brnSelectOption = inject(BrnSelectOptionDirective, { host: true });
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() =>
+		hlm(
+			'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2  rtl:flex-reverse rtl:pr-8 rtl:pl-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
+			this.userClass(),
+		),
+	);
+}

+ 18 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-scroll-down.component.ts

@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideChevronDown } from '@ng-icons/lucide';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+
+@Component({
+	selector: 'hlm-select-scroll-down',
+	standalone: true,
+	imports: [NgIcon, HlmIconDirective],
+	providers: [provideIcons({ lucideChevronDown })],
+	host: {
+		class: 'flex cursor-default items-center justify-center py-1',
+	},
+	template: `
+		<ng-icon hlm size="sm" class="ml-2" name="lucideChevronDown" />
+	`,
+})
+export class HlmSelectScrollDownComponent {}

+ 18 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-scroll-up.component.ts

@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideChevronUp } from '@ng-icons/lucide';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+
+@Component({
+	selector: 'hlm-select-scroll-up',
+	standalone: true,
+	imports: [NgIcon, HlmIconDirective],
+	providers: [provideIcons({ lucideChevronUp })],
+	host: {
+		class: 'flex cursor-default items-center justify-center py-1',
+	},
+	template: `
+		<ng-icon hlm size="sm" class="ml-2" name="lucideChevronUp" />
+	`,
+})
+export class HlmSelectScrollUpComponent {}

+ 60 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-trigger.component.ts

@@ -0,0 +1,60 @@
+import { Component, computed, contentChild, inject, input } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideChevronDown } from '@ng-icons/lucide';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnSelectComponent, BrnSelectTriggerDirective } from '@spartan-ng/brain/select';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+import { type VariantProps, cva } from 'class-variance-authority';
+import type { ClassValue } from 'clsx';
+
+export const selectTriggerVariants = cva(
+	'flex items-center justify-between rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
+	{
+		variants: {
+			size: {
+				default: 'h-10 py-2 px-4',
+				sm: 'h-9 px-3',
+				lg: 'h-11 px-8',
+			},
+			error: {
+				auto: '[&.ng-invalid.ng-touched]:text-destructive [&.ng-invalid.ng-touched]:border-destructive [&.ng-invalid.ng-touched]:focus-visible:ring-destructive',
+				true: 'text-destructive border-destructive focus-visible:ring-destructive',
+			},
+		},
+		defaultVariants: {
+			size: 'default',
+			error: 'auto',
+		},
+	},
+);
+type SelectTriggerVariants = VariantProps<typeof selectTriggerVariants>;
+
+@Component({
+	selector: 'hlm-select-trigger',
+	standalone: true,
+	imports: [BrnSelectTriggerDirective, NgIcon, HlmIconDirective],
+	providers: [provideIcons({ lucideChevronDown })],
+
+	template: `
+		<button [class]="_computedClass()" #button hlmInput brnSelectTrigger type="button">
+			<ng-content />
+			@if (icon()) {
+				<ng-content select="hlm-icon" />
+			} @else {
+				<ng-icon hlm size="sm" class="ml-2 flex-none" name="lucideChevronDown" />
+			}
+		</button>
+	`,
+})
+export class HlmSelectTriggerComponent {
+	protected readonly icon = contentChild(HlmIconDirective);
+
+	protected readonly brnSelect = inject(BrnSelectComponent, { optional: true });
+
+	public readonly _size = input<SelectTriggerVariants['size']>('default');
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+
+	protected _computedClass = computed(() =>
+		hlm(selectTriggerVariants({ size: this._size(), error: this.brnSelect?.errorState() }), this.userClass()),
+	);
+}

+ 20 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select-value.directive.ts

@@ -0,0 +1,20 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: 'hlm-select-value,[hlmSelectValue], brn-select-value[hlm]',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmSelectValueDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() =>
+		hlm(
+			'!inline-block ltr:text-left rtl:text-right border-border w-[calc(100%)]] min-w-0 pointer-events-none truncate',
+			this.userClass(),
+		),
+	);
+}

+ 15 - 0
packages/admin-ui/src/lib/ui/ui-select-helm/src/lib/hlm-select.directive.ts

@@ -0,0 +1,15 @@
+import { Directive, computed, input } from '@angular/core';
+import { hlm } from '@spartan-ng/brain/core';
+import type { ClassValue } from 'clsx';
+
+@Directive({
+	selector: 'hlm-select, brn-select [hlm]',
+	standalone: true,
+	host: {
+		'[class]': '_computedClass()',
+	},
+})
+export class HlmSelectDirective {
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() => hlm('space-y-2', this.userClass()));
+}