WIP
This commit is contained in:
parent
62e663d053
commit
d448ca6516
|
|
@ -2,4 +2,8 @@
|
|||
.vscode/
|
||||
|
||||
.env
|
||||
.env.prod
|
||||
.env.stage
|
||||
.DS_Store
|
||||
|
||||
node_modules/
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
- name: Nodes initialisieren und härten
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- role: common
|
||||
tags: common
|
||||
- role: ssh_hardening
|
||||
tags: ssh
|
||||
- role: ufw_firewall
|
||||
tags: firewall
|
||||
- role: fail2ban
|
||||
tags: fail2ban
|
||||
handlers:
|
||||
- name: restart sshd
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
- name: restart fail2ban
|
||||
ansible.builtin.service:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
- name: Setup Ceph Cluster and CephFS
|
||||
hosts: all
|
||||
become: true
|
||||
roles:
|
||||
- role: ceph_setup
|
||||
|
||||
- name: Docker Swarm initialisieren
|
||||
hosts: all
|
||||
become: true
|
||||
roles:
|
||||
- role: docker_swarm
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
- name: Infrastruktur Dienste bereitstellen
|
||||
hosts: all
|
||||
gather_facts: true
|
||||
roles:
|
||||
# - traefik
|
||||
# - portainer
|
||||
# - hyperdx
|
||||
- kestra
|
||||
# - authentik
|
||||
# # - dockge
|
||||
# - signoz
|
||||
# - signoz-infra
|
||||
# - leantime
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[client.admin]
|
||||
key = AQDfUDBpnhHKNBAAwIRER17JF8flD9GSeNtFlg==
|
||||
caps mds = "allow *"
|
||||
caps mgr = "allow *"
|
||||
caps mon = "allow *"
|
||||
caps osd = "allow *"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# minimal ceph.conf for f103f9ee-d058-11f0-aee8-920006d45637
|
||||
[global]
|
||||
fsid = f103f9ee-d058-11f0-aee8-920006d45637
|
||||
mon_host = [v2:10.0.1.2:3300/0,v1:10.0.1.2:6789/0]
|
||||
|
|
@ -45,4 +45,4 @@
|
|||
# - signoz-infra
|
||||
# - leantime
|
||||
- kestra
|
||||
- hyperdx
|
||||
# - hyperdx
|
||||
|
|
@ -28,6 +28,18 @@
|
|||
run_once: true
|
||||
delegate_to: "{{ groups['managers'][0] }}"
|
||||
|
||||
- name: HYPERDX | shared-observability Netzwerk erstellen
|
||||
community.docker.docker_network:
|
||||
name: shared-observability
|
||||
driver: overlay
|
||||
state: present
|
||||
attachable: yes
|
||||
ipam_config:
|
||||
- subnet: '172.16.201.0/24'
|
||||
gateway: '172.16.201.1'
|
||||
run_once: true
|
||||
delegate_to: "{{ groups['managers'][0] }}"
|
||||
|
||||
- name: HYPERDX | Generate Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
dist
|
||||
.git
|
||||
.local
|
||||
.vscode
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
.angular
|
||||
.vscode
|
||||
dist
|
||||
node_modules
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,18 +1,37 @@
|
|||
FROM node:20.19.1-alpine AS builder
|
||||
FROM node:20.19.1-slim AS builder
|
||||
|
||||
WORKDIR /app/builder
|
||||
|
||||
COPY . .
|
||||
COPY package*.json .
|
||||
COPY packages/shared-dtos/package*.json ./packages/shared-dtos/
|
||||
# COPY data-hub/data-connector/package*.json ./data-hub/data-connector/
|
||||
COPY app-hub/dashboard/package*.json ./app-hub/dashboard/
|
||||
|
||||
RUN npm install \
|
||||
&& npm run build
|
||||
ENV LIBC=glibc
|
||||
ENV npm_config_arch=x64
|
||||
ENV npm_config_platform=linux
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN npm ci --verbose
|
||||
|
||||
COPY packages/shared-dtos ./packages/shared-dtos/
|
||||
COPY app-hub/dashboard ./app-hub/dashboard/
|
||||
|
||||
WORKDIR /app/builder/app-hub/dashboard
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.19.2
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=builder /app/builder/dist/dashboard/browser ./
|
||||
COPY --from=builder /app/builder/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=builder /app/builder/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY --from=builder /app/builder/app-hub/dashboard/dist/dashboard/browser ./
|
||||
COPY --from=builder /app/builder/app-hub/dashboard/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=builder /app/builder/app-hub/dashboard/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -23,6 +23,7 @@
|
|||
"@angular/platform-browser-dynamic": "^19.2.6",
|
||||
"@angular/router": "^19.2.6",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@avicenna/shared-dtos": "^1.0.0",
|
||||
"@azure/msal-angular": "^3.0.22",
|
||||
"@azure/msal-browser": "^3.19.1",
|
||||
"@hyperdx/browser": "^0.22.0",
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
"@opentelemetry/sdk-trace-web": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@spartan-ng/brain": "^0.0.1-alpha.561",
|
||||
"@tanstack/angular-query-experimental": "^5.90.20",
|
||||
"apollo-angular": "^10.0.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
"echarts": "^5.5.1",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"ngx-scrollbar": "^19.1.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tslib": "^2.3.0",
|
||||
|
|
@ -76,6 +79,7 @@
|
|||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.8.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.5.2"
|
||||
|
|
|
|||
|
|
@ -9,12 +9,38 @@ import { provideRouter, withComponentInputBinding } from '@angular/router';
|
|||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { provideHotToastConfig } from '@ngxpert/hot-toast';
|
||||
import {
|
||||
provideTanStackQuery,
|
||||
QueryClient,
|
||||
} from '@tanstack/angular-query-experimental';
|
||||
|
||||
import { withDevtools } from '@tanstack/angular-query-experimental/devtools';
|
||||
import { environment } from '../environments/environment';
|
||||
import { routes } from './app.routes';
|
||||
import { MsAuthenticationModule } from './core/components/ms-authentication/ms-authentication.module';
|
||||
import { GlobalErrorHandler } from './core/telemetry/global-error-handler';
|
||||
import { graphqlProvider } from './graphql.provider';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Daten gelten für 2 Minute als "frisch" (kein Refetch)
|
||||
staleTime: 2000 * 60 * 1,
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient;
|
||||
}
|
||||
}
|
||||
|
||||
// This code is for all users
|
||||
window.__TANSTACK_QUERY_CLIENT__ = queryClient;
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
{
|
||||
|
|
@ -32,6 +58,7 @@ export const appConfig: ApplicationConfig = {
|
|||
postLogoutRedirectUri: environment.msAuth.postLogoutRedirectUri,
|
||||
})
|
||||
),
|
||||
provideTanStackQuery(queryClient, withDevtools()),
|
||||
provideHttpClient(),
|
||||
graphqlProvider,
|
||||
provideHotToastConfig(),
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const routes: Routes = [
|
|||
{
|
||||
path: 'tickets',
|
||||
loadChildren: () =>
|
||||
import('./pages/ticket-system/ticket-system.module').then(
|
||||
import('./pages/tickets/ticket-system.module').then(
|
||||
(m) => m.TicketSystemModule
|
||||
),
|
||||
canActivate: [MsalGuard],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { HlmAlertDialog } from './lib/hlm-alert-dialog';
|
||||
import { HlmAlertDialogActionButton } from './lib/hlm-alert-dialog-action-button';
|
||||
import { HlmAlertDialogCancelButton } from './lib/hlm-alert-dialog-cancel-button';
|
||||
import { HlmAlertDialogContent } from './lib/hlm-alert-dialog-content';
|
||||
import { HlmAlertDialogDescription } from './lib/hlm-alert-dialog-description';
|
||||
import { HlmAlertDialogFooter } from './lib/hlm-alert-dialog-footer';
|
||||
import { HlmAlertDialogHeader } from './lib/hlm-alert-dialog-header';
|
||||
import { HlmAlertDialogOverlay } from './lib/hlm-alert-dialog-overlay';
|
||||
import { HlmAlertDialogTitle } from './lib/hlm-alert-dialog-title';
|
||||
|
||||
export * from './lib/hlm-alert-dialog';
|
||||
export * from './lib/hlm-alert-dialog-action-button';
|
||||
export * from './lib/hlm-alert-dialog-cancel-button';
|
||||
export * from './lib/hlm-alert-dialog-content';
|
||||
export * from './lib/hlm-alert-dialog-description';
|
||||
export * from './lib/hlm-alert-dialog-footer';
|
||||
export * from './lib/hlm-alert-dialog-header';
|
||||
export * from './lib/hlm-alert-dialog-overlay';
|
||||
export * from './lib/hlm-alert-dialog-title';
|
||||
|
||||
export const HlmAlertDialogImports = [
|
||||
HlmAlertDialogContent,
|
||||
HlmAlertDialogDescription,
|
||||
HlmAlertDialogFooter,
|
||||
HlmAlertDialogHeader,
|
||||
HlmAlertDialogOverlay,
|
||||
HlmAlertDialogTitle,
|
||||
HlmAlertDialogActionButton,
|
||||
HlmAlertDialogCancelButton,
|
||||
HlmAlertDialog,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Directive } from '@angular/core';
|
||||
import { HlmButton } from '@spartan-ng/helm/button';
|
||||
|
||||
@Directive({
|
||||
selector: 'button[hlmAlertDialogAction]',
|
||||
hostDirectives: [{ directive: HlmButton, inputs: ['variant', 'size'] }],
|
||||
})
|
||||
export class HlmAlertDialogActionButton {}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Directive } from '@angular/core';
|
||||
import { HlmButton, provideBrnButtonConfig } from '@spartan-ng/helm/button';
|
||||
|
||||
@Directive({
|
||||
selector: 'button[hlmAlertDialogCancel]',
|
||||
providers: [provideBrnButtonConfig({ variant: 'outline' })],
|
||||
hostDirectives: [{ directive: HlmButton, inputs: ['variant', 'size'] }],
|
||||
})
|
||||
export class HlmAlertDialogCancelButton {}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
|
||||
import { injectExposesStateProvider } from '@spartan-ng/brain/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-alert-dialog-content',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
'[attr.data-state]': 'state()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmAlertDialogContent {
|
||||
private readonly _stateProvider = injectExposesStateProvider({ optional: true, host: true });
|
||||
public readonly state = this._stateProvider?.state ?? signal('closed');
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'bg-background 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 relative z-50 mx-auto grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border p-6 shadow-lg data-[state=closed]:duration-200 data-[state=open]:duration-200 sm:mx-0 sm:max-w-lg',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { BrnAlertDialogDescription } from '@spartan-ng/brain/alert-dialog';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmAlertDialogDescription]',
|
||||
hostDirectives: [BrnAlertDialogDescription],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmAlertDialogDescription {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('text-muted-foreground text-sm', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-alert-dialog-footer',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmAlertDialogFooter {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-alert-dialog-header',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmAlertDialogHeader {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('flex flex-col gap-2 text-center sm:text-left', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Directive, computed, effect, input, untracked } from '@angular/core';
|
||||
import { injectCustomClassSettable } from '@spartan-ng/brain/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmAlertDialogOverlay],brn-alert-dialog-overlay[hlm]',
|
||||
})
|
||||
export class HlmAlertDialogOverlay {
|
||||
private readonly _classSettable = injectCustomClassSettable({ optional: true, host: true });
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/50',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const classValue = this._computedClass();
|
||||
untracked(() => this._classSettable?.setClassToCustomElement(classValue));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { BrnAlertDialogTitle } from '@spartan-ng/brain/alert-dialog';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmAlertDialogTitle]',
|
||||
hostDirectives: [BrnAlertDialogTitle],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmAlertDialogTitle {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('text-lg font-semibold', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
|
||||
import {
|
||||
BRN_ALERT_DIALOG_DEFAULT_OPTIONS,
|
||||
BrnAlertDialog,
|
||||
BrnAlertDialogOverlay,
|
||||
} from '@spartan-ng/brain/alert-dialog';
|
||||
import { BrnDialog, provideBrnDialogDefaultOptions } from '@spartan-ng/brain/dialog';
|
||||
import { HlmAlertDialogOverlay } from './hlm-alert-dialog-overlay';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-alert-dialog',
|
||||
exportAs: 'hlmAlertDialog',
|
||||
imports: [BrnAlertDialogOverlay, HlmAlertDialogOverlay],
|
||||
providers: [
|
||||
{
|
||||
provide: BrnDialog,
|
||||
useExisting: forwardRef(() => HlmAlertDialog),
|
||||
},
|
||||
provideBrnDialogDefaultOptions({
|
||||
...BRN_ALERT_DIALOG_DEFAULT_OPTIONS,
|
||||
}),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<brn-alert-dialog-overlay hlm />
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmAlertDialog extends BrnAlertDialog {}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { HlmAutocomplete } from './lib/hlm-autocomplete';
|
||||
import { HlmAutocompleteEmpty } from './lib/hlm-autocomplete-empty';
|
||||
import { HlmAutocompleteGroup } from './lib/hlm-autocomplete-group';
|
||||
import { HlmAutocompleteItem } from './lib/hlm-autocomplete-item';
|
||||
import { HlmAutocompleteList } from './lib/hlm-autocomplete-list';
|
||||
import { HlmAutocompleteTrigger } from './lib/hlm-autocomplete-trigger';
|
||||
|
||||
export * from './lib/hlm-autocomplete';
|
||||
export * from './lib/hlm-autocomplete-empty';
|
||||
export * from './lib/hlm-autocomplete-group';
|
||||
export * from './lib/hlm-autocomplete-item';
|
||||
export * from './lib/hlm-autocomplete-list';
|
||||
export * from './lib/hlm-autocomplete-trigger';
|
||||
export * from './lib/hlm-autocomplete.token';
|
||||
|
||||
export const HlmAutocompleteImports = [
|
||||
HlmAutocomplete,
|
||||
HlmAutocompleteEmpty,
|
||||
HlmAutocompleteGroup,
|
||||
HlmAutocompleteItem,
|
||||
HlmAutocompleteList,
|
||||
HlmAutocompleteTrigger,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmAutocompleteEmpty]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmAutocompleteEmpty {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('py-6 text-center text-sm', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnAutocompleteGroup } from '@spartan-ng/brain/autocomplete';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-autocomplete-group',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnAutocompleteGroup,
|
||||
inputs: ['id'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '<ng-content />',
|
||||
})
|
||||
export class HlmAutocompleteGroup {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('text-foreground block overflow-hidden p-1', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnAutocompleteItem } from '@spartan-ng/brain/autocomplete';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'button[hlm-autocomplete-item]',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnAutocompleteItem,
|
||||
inputs: ['value', 'disabled', 'id'],
|
||||
outputs: ['selected'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmAutocompleteItem {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'data-[selected]:bg-accent data-[selected=true]:text-accent-foreground [&>ng-icon]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[hidden]:hidden [&>ng-icon]:pointer-events-none [&>ng-icon]:shrink-0 [&>ng-icon]:text-base',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnAutocompleteList } from '@spartan-ng/brain/autocomplete';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-autocomplete-list',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnAutocompleteList,
|
||||
inputs: ['id'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '<ng-content />',
|
||||
})
|
||||
export class HlmAutocompleteList {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('block max-h-60 overflow-x-hidden overflow-y-auto', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { booleanAttribute, Directive, ElementRef, inject, input } from '@angular/core';
|
||||
import { BrnDialog } from '@spartan-ng/brain/dialog';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmAutocompleteTrigger]',
|
||||
host: {
|
||||
'(click)': 'open()',
|
||||
},
|
||||
})
|
||||
export class HlmAutocompleteTrigger {
|
||||
private readonly _host = inject(ElementRef, { host: true });
|
||||
|
||||
private readonly _brnDialog = inject(BrnDialog, { optional: true });
|
||||
|
||||
/** Whether the trigger is disabled. */
|
||||
public readonly disabledTrigger = input<boolean, BooleanInput>(false, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
if (!this._brnDialog) return;
|
||||
|
||||
this._brnDialog.mutableAttachTo.set(this._host.nativeElement);
|
||||
}
|
||||
|
||||
open() {
|
||||
if (this.disabledTrigger()) return;
|
||||
|
||||
this._brnDialog?.open();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { inject, InjectionToken, type ValueProvider } from '@angular/core';
|
||||
|
||||
export type TransformValueToString<T> = (option: T) => string;
|
||||
|
||||
export interface HlmAutocompleteConfig<T, V = T> {
|
||||
transformValueToSearch: TransformValueToString<T>;
|
||||
transformOptionToString: TransformValueToString<T>;
|
||||
transformOptionToValue: ((option: T) => V) | undefined;
|
||||
requireSelection: boolean;
|
||||
showClearBtn: boolean;
|
||||
debounceTime: number;
|
||||
}
|
||||
|
||||
function getDefaultConfig<T, V = T>(): HlmAutocompleteConfig<T, V> {
|
||||
return {
|
||||
transformValueToSearch: (option: T) => (typeof option === 'string' ? option : String(option)),
|
||||
transformOptionToString: (option: T) => (typeof option === 'string' ? option : String(option)),
|
||||
transformOptionToValue: undefined,
|
||||
requireSelection: false,
|
||||
showClearBtn: false,
|
||||
debounceTime: 150,
|
||||
};
|
||||
}
|
||||
|
||||
const HlmAutocompleteConfigToken = new InjectionToken<HlmAutocompleteConfig<unknown, unknown>>('HlmAutocompleteConfig');
|
||||
|
||||
export function provideHlmAutocompleteConfig<T, V = T>(config: Partial<HlmAutocompleteConfig<T, V>>): ValueProvider {
|
||||
return { provide: HlmAutocompleteConfigToken, useValue: { ...getDefaultConfig(), ...config } };
|
||||
}
|
||||
|
||||
export function injectHlmAutocompleteConfig<T, V = T>(): HlmAutocompleteConfig<T, V> {
|
||||
return (
|
||||
(inject(HlmAutocompleteConfigToken, { optional: true }) as HlmAutocompleteConfig<T, V> | null) ?? getDefaultConfig()
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
import type { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
booleanAttribute,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
model,
|
||||
output,
|
||||
type TemplateRef,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideChevronDown,
|
||||
lucideCircleX,
|
||||
lucideSearch,
|
||||
} from '@ng-icons/lucide';
|
||||
import {
|
||||
BrnAutocomplete,
|
||||
BrnAutocompleteEmpty,
|
||||
BrnAutocompleteImports,
|
||||
} from '@spartan-ng/brain/autocomplete';
|
||||
import { debouncedSignal } from '@spartan-ng/brain/core';
|
||||
import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
|
||||
import { BrnPopoverImports } from '@spartan-ng/brain/popover';
|
||||
import { HlmIconImports } from '@spartan-ng/helm/icon';
|
||||
import { HlmPopoverImports } from '@spartan-ng/helm/popover';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { HlmAutocompleteEmpty } from './hlm-autocomplete-empty';
|
||||
import { HlmAutocompleteGroup } from './hlm-autocomplete-group';
|
||||
import { HlmAutocompleteItem } from './hlm-autocomplete-item';
|
||||
import { HlmAutocompleteList } from './hlm-autocomplete-list';
|
||||
|
||||
import { HlmInputGroupImports } from '@spartan-ng/helm/input-group';
|
||||
import { HlmAutocompleteTrigger } from './hlm-autocomplete-trigger';
|
||||
import { injectHlmAutocompleteConfig } from './hlm-autocomplete.token';
|
||||
|
||||
export const HLM_AUTOCOMPLETE_VALUE_ACCESSOR = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => HlmAutocomplete),
|
||||
multi: true,
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-autocomplete',
|
||||
imports: [
|
||||
NgTemplateOutlet,
|
||||
BrnAutocomplete,
|
||||
BrnAutocompleteEmpty,
|
||||
HlmAutocompleteEmpty,
|
||||
HlmAutocompleteGroup,
|
||||
HlmAutocompleteItem,
|
||||
HlmAutocompleteList,
|
||||
HlmAutocompleteTrigger,
|
||||
BrnPopoverImports,
|
||||
HlmPopoverImports,
|
||||
HlmIconImports,
|
||||
HlmInputGroupImports,
|
||||
BrnAutocompleteImports,
|
||||
],
|
||||
providers: [
|
||||
HLM_AUTOCOMPLETE_VALUE_ACCESSOR,
|
||||
provideIcons({ lucideSearch, lucideChevronDown, lucideCircleX }),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
@let transformer = transformOptionToValue();
|
||||
<brn-popover
|
||||
#popover
|
||||
align="start"
|
||||
autoFocus="first-heading"
|
||||
sideOffset="5"
|
||||
closeDelay="100"
|
||||
(closed)="_closed()"
|
||||
[closeOnOutsidePointerEvents]="true"
|
||||
>
|
||||
<div brnAutocomplete (selectionCleared)="_selectionCleared()">
|
||||
<div
|
||||
hlmInputGroup
|
||||
hlmAutocompleteTrigger
|
||||
[class]="_computedAutocompleteSearchClass()"
|
||||
[disabledTrigger]="!_search() || _disabled()"
|
||||
>
|
||||
<input
|
||||
#input
|
||||
brnAutocompleteSearchInput
|
||||
hlmInputGroupInput
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
[id]="inputId()"
|
||||
[class]="_computedAutocompleteInputClass()"
|
||||
[placeholder]="searchPlaceholderText()"
|
||||
[disabled]="_disabled()"
|
||||
[value]="search()"
|
||||
(input)="_searchChanged($event)"
|
||||
data-bwignore="true"
|
||||
data-1p-ignore="true"
|
||||
data-lpignore="true"
|
||||
/>
|
||||
<div hlmInputGroupAddon>
|
||||
<ng-icon name="lucideSearch" />
|
||||
</div>
|
||||
<div hlmInputGroupAddon align="inline-end">
|
||||
@if (showClearBtn() && value() !== undefined) {
|
||||
<button
|
||||
hlmInputGroupButton
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
aria-label="clear"
|
||||
[disabled]="_disabled()"
|
||||
(click)="_selectionCleared()"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ng-icon name="lucideCircleX" />
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
hlmInputGroupButton
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
[attr.aria-label]="ariaLabelToggleButton()"
|
||||
[disabled]="_disabled()"
|
||||
(click)="_toggleOptions()"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ng-icon name="lucideChevronDown" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*brnPopoverContent="let ctx"
|
||||
hlmPopoverContent
|
||||
class="p-0"
|
||||
[style.width.px]="_elementRef.nativeElement.offsetWidth"
|
||||
>
|
||||
<hlm-autocomplete-list
|
||||
[class]="_computedAutocompleteListClass()"
|
||||
[class.hidden]="filteredOptions().length === 0"
|
||||
>
|
||||
<hlm-autocomplete-group>
|
||||
@for (option of filteredOptions(); track option) {
|
||||
<button
|
||||
hlm-autocomplete-item
|
||||
[class]="_computedAutocompleteItemClass()"
|
||||
[value]="transformer ? transformer(option) : option"
|
||||
(selected)="_optionSelected(option)"
|
||||
>
|
||||
@if (optionTemplate(); as optionTemplate) {
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
optionTemplate;
|
||||
context: { $implicit: option }
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
{{ transformOptionToString()(option) }}
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</hlm-autocomplete-group>
|
||||
</hlm-autocomplete-list>
|
||||
|
||||
<div
|
||||
*brnAutocompleteEmpty
|
||||
hlmAutocompleteEmpty
|
||||
[class]="_computedAutocompleteEmptyClass()"
|
||||
>
|
||||
@if (loading()) {
|
||||
<ng-content select="[loading]">{{ loadingText() }}</ng-content>
|
||||
} @else {
|
||||
<ng-content select="[empty]">{{ emptyText() }}</ng-content>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</brn-popover>
|
||||
`,
|
||||
})
|
||||
export class HlmAutocomplete<T, V = T> implements ControlValueAccessor {
|
||||
private static _id = 0;
|
||||
private readonly _config = injectHlmAutocompleteConfig<T, V>();
|
||||
|
||||
private readonly _brnAutocomplete = viewChild.required(BrnAutocomplete);
|
||||
|
||||
private readonly _inputRef = viewChild.required('input', {
|
||||
read: ElementRef,
|
||||
});
|
||||
|
||||
protected readonly _elementRef = inject(ElementRef<HTMLElement>);
|
||||
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('block w-full', this.userClass())
|
||||
);
|
||||
|
||||
/** Custom class for the autocomplete search container. */
|
||||
public readonly autocompleteSearchClass = input<ClassValue>('');
|
||||
protected readonly _computedAutocompleteSearchClass = computed(() =>
|
||||
hlm('', this.autocompleteSearchClass())
|
||||
);
|
||||
|
||||
/** Custom class for the autocomplete input. */
|
||||
public readonly autocompleteInputClass = input<ClassValue>('');
|
||||
protected readonly _computedAutocompleteInputClass = computed(() =>
|
||||
hlm('', this.autocompleteInputClass())
|
||||
);
|
||||
|
||||
/** Custom class for the autocomplete list. */
|
||||
public readonly autocompleteListClass = input<ClassValue>('');
|
||||
protected readonly _computedAutocompleteListClass = computed(() =>
|
||||
hlm('', this.autocompleteListClass())
|
||||
);
|
||||
|
||||
/** Custom class for each autocomplete item. */
|
||||
public readonly autocompleteItemClass = input<ClassValue>('');
|
||||
protected readonly _computedAutocompleteItemClass = computed(() =>
|
||||
hlm('', this.autocompleteItemClass())
|
||||
);
|
||||
|
||||
/** Custom class for the empty and loading state container. */
|
||||
public readonly autocompleteEmptyClass = input<ClassValue>('');
|
||||
protected readonly _computedAutocompleteEmptyClass = computed(() =>
|
||||
hlm('', this.autocompleteEmptyClass())
|
||||
);
|
||||
|
||||
/** The list of filtered options to display in the autocomplete. */
|
||||
public readonly filteredOptions = input<T[]>([]);
|
||||
|
||||
/** The selected value. */
|
||||
public readonly value = model<T | V>();
|
||||
|
||||
/** Debounce time in milliseconds for the search input. */
|
||||
public readonly debounceTime = input<number>(this._config.debounceTime);
|
||||
|
||||
/** The search query. */
|
||||
public readonly search = model<string>('');
|
||||
|
||||
/** Debounced search query. */
|
||||
protected readonly _search = debouncedSignal(
|
||||
this.search,
|
||||
this.debounceTime()
|
||||
);
|
||||
|
||||
/** Function to transform an option value to a search string. Defaults to identity function for strings. */
|
||||
public readonly transformValueToSearch = input<(option: T) => string>(
|
||||
this._config.transformValueToSearch
|
||||
);
|
||||
|
||||
/** Whether selection of an option is required. */
|
||||
public readonly requireSelection = input<boolean, BooleanInput>(
|
||||
this._config.requireSelection,
|
||||
{
|
||||
transform: booleanAttribute,
|
||||
}
|
||||
);
|
||||
|
||||
/** Function to transform an option value to a display string. Defaults to identity function for strings. */
|
||||
public readonly transformOptionToString = input<(option: T) => string>(
|
||||
this._config.transformOptionToString
|
||||
);
|
||||
|
||||
/** Function to transform the object to the value. */
|
||||
public readonly transformOptionToValue = input<
|
||||
((option: T) => V) | undefined
|
||||
>(this._config.transformOptionToValue);
|
||||
|
||||
/** Function to display the selected value as a string. */
|
||||
public readonly displayWith = input<((value: V) => string) | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
/** Computed function to get the display value for the selected option. */
|
||||
protected readonly _displaySearchValue = computed(() => {
|
||||
const displayWith = this.displayWith();
|
||||
if (displayWith) {
|
||||
return displayWith;
|
||||
} else {
|
||||
return this.transformValueToSearch();
|
||||
}
|
||||
});
|
||||
|
||||
/** Optional template for rendering each option. */
|
||||
public readonly optionTemplate =
|
||||
input<TemplateRef<HlmAutocompleteOption<T>>>();
|
||||
|
||||
/** Whether the autocomplete is in a loading state. */
|
||||
public readonly loading = input<boolean, BooleanInput>(false, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
|
||||
/** Whether to show the clear button when a option is selected. */
|
||||
public readonly showClearBtn = input<boolean, BooleanInput>(
|
||||
this._config.showClearBtn,
|
||||
{
|
||||
transform: booleanAttribute,
|
||||
}
|
||||
);
|
||||
|
||||
/** Placeholder text for the input field. */
|
||||
public readonly searchPlaceholderText = input('Select an option');
|
||||
|
||||
/** Text to display when loading options. */
|
||||
public readonly loadingText = input('Loading options...');
|
||||
|
||||
/** Text to display when no options are found. */
|
||||
public readonly emptyText = input('No options found');
|
||||
|
||||
/** Aria label for the toggle button. */
|
||||
public readonly ariaLabelToggleButton = input('Toggle options');
|
||||
|
||||
/** The id of the input field. */
|
||||
public readonly inputId = input(
|
||||
`hlm-autocomplete-input-${++HlmAutocomplete._id}`
|
||||
);
|
||||
|
||||
/** Whether the autocomplete is disabled. */
|
||||
public readonly disabled = input<boolean, BooleanInput>(false, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
|
||||
protected readonly _disabled = linkedSignal(() => this.disabled());
|
||||
|
||||
/** Emitted when the selected value changes. */
|
||||
public readonly valueChange = output<T | V | null>();
|
||||
|
||||
/** Emitted when the search query changes. */
|
||||
public readonly searchChange = output<string>();
|
||||
|
||||
protected _onChange?: ChangeFn<T | V | null>;
|
||||
protected _onTouched?: TouchFn;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const search = this._search();
|
||||
this.searchChange.emit(search);
|
||||
});
|
||||
}
|
||||
|
||||
protected _searchChanged(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
this.search.set(value ?? '');
|
||||
|
||||
if (!this._brnAutocomplete().isExpanded() && value.length > 0) {
|
||||
this._brnAutocomplete().open();
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle the options panel */
|
||||
protected _toggleOptions() {
|
||||
if (this._search() || this.filteredOptions().length > 0) {
|
||||
// only toggle if there's a search term or options to show
|
||||
this._brnAutocomplete().toggle();
|
||||
}
|
||||
|
||||
this._inputRef().nativeElement.focus();
|
||||
}
|
||||
|
||||
/** Clear the current selection and search input */
|
||||
protected _selectionCleared() {
|
||||
this.value.set(undefined);
|
||||
this._onChange?.(null);
|
||||
this.valueChange.emit(null);
|
||||
this.search.set('');
|
||||
}
|
||||
|
||||
protected _optionSelected(option: T) {
|
||||
const transformer = this.transformOptionToValue();
|
||||
|
||||
const value = transformer ? transformer(option) : option;
|
||||
|
||||
this.value.set(value);
|
||||
this._onChange?.(value);
|
||||
this.valueChange.emit(value);
|
||||
|
||||
const searchValue = this._displaySearchValue()(value as any);
|
||||
this.search.set(searchValue ?? '');
|
||||
this._brnAutocomplete().close();
|
||||
}
|
||||
|
||||
/** CONTROL VALUE ACCESSOR */
|
||||
public writeValue(value: T | V | null): void {
|
||||
this.value.set(value ? value : undefined);
|
||||
|
||||
const searchValue = value ? this._displaySearchValue()(value as any) : '';
|
||||
this.search.set(searchValue);
|
||||
}
|
||||
|
||||
public registerOnChange(fn: ChangeFn<T | V | null>): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
public registerOnTouched(fn: TouchFn): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
public setDisabledState(isDisabled: boolean): void {
|
||||
this._disabled.set(isDisabled);
|
||||
}
|
||||
|
||||
protected _closed() {
|
||||
if (this.requireSelection()) {
|
||||
const value = this.value();
|
||||
const searchValue = value ? this._displaySearchValue()(value as any) : '';
|
||||
this.search.set(searchValue ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface HlmAutocompleteOption<T> {
|
||||
$implicit: T;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { HlmBadge } from './lib/hlm-badge';
|
||||
|
||||
export * from './lib/hlm-badge';
|
||||
|
||||
export const HlmBadgeImports = [HlmBadge] as const;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&_ng-icon]:pointer-events-none [&_ng-icon]:size-3',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
|
||||
secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
|
||||
destructive:
|
||||
'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 border-transparent text-white',
|
||||
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmBadge]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmBadge {
|
||||
protected readonly _computedClass = computed(() => hlm(badgeVariants({ variant: this.variant() }), this.userClass()));
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
public readonly variant = input<BadgeVariants['variant']>('default');
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { HlmButton } from './lib/hlm-button';
|
||||
|
||||
export * from './lib/hlm-button';
|
||||
export * from './lib/hlm-button.token';
|
||||
|
||||
export const HlmButtonImports = [HlmButton] as const;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { InjectionToken, type ValueProvider, inject } from '@angular/core';
|
||||
import type { ButtonVariants } from './hlm-button';
|
||||
|
||||
export interface BrnButtonConfig {
|
||||
variant: ButtonVariants['variant'];
|
||||
size: ButtonVariants['size'];
|
||||
}
|
||||
|
||||
const defaultConfig: BrnButtonConfig = {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
};
|
||||
|
||||
const BrnButtonConfigToken = new InjectionToken<BrnButtonConfig>('BrnButtonConfig');
|
||||
|
||||
export function provideBrnButtonConfig(config: Partial<BrnButtonConfig>): ValueProvider {
|
||||
return { provide: BrnButtonConfigToken, useValue: { ...defaultConfig, ...config } };
|
||||
}
|
||||
|
||||
export function injectBrnButtonConfig(): BrnButtonConfig {
|
||||
return inject(BrnButtonConfigToken, { optional: true }) ?? defaultConfig;
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { Directive, computed, input, signal } from '@angular/core';
|
||||
import { BrnButton } from '@spartan-ng/brain/button';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { injectBrnButtonConfig } from './hlm-button.token';
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_ng-icon]:pointer-events-none [&_ng-icon]:shrink-0 [&_ng-icon:not([class*='text-'])]:text-base",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
|
||||
outline:
|
||||
'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>ng-icon]:px-3',
|
||||
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>ng-icon]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>ng-icon]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: 'button[hlmBtn], a[hlmBtn]',
|
||||
exportAs: 'hlmBtn',
|
||||
hostDirectives: [{ directive: BrnButton, inputs: ['disabled'] }],
|
||||
host: {
|
||||
'data-slot': 'button',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmButton {
|
||||
private readonly _config = injectBrnButtonConfig();
|
||||
|
||||
private readonly _additionalClasses = signal<ClassValue>('');
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(buttonVariants({ variant: this.variant(), size: this.size() }), this.userClass(), this._additionalClasses()),
|
||||
);
|
||||
|
||||
public readonly variant = input<ButtonVariants['variant']>(this._config.variant);
|
||||
|
||||
public readonly size = input<ButtonVariants['size']>(this._config.size);
|
||||
|
||||
setClass(classes: string): void {
|
||||
this._additionalClasses.set(classes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { HlmCard } from './lib/hlm-card';
|
||||
import { HlmCardAction } from './lib/hlm-card-action';
|
||||
import { HlmCardContent } from './lib/hlm-card-content';
|
||||
import { HlmCardDescription } from './lib/hlm-card-description';
|
||||
import { HlmCardFooter } from './lib/hlm-card-footer';
|
||||
import { HlmCardHeader } from './lib/hlm-card-header';
|
||||
import { HlmCardTitle } from './lib/hlm-card-title';
|
||||
|
||||
export * from './lib/hlm-card';
|
||||
export * from './lib/hlm-card-action';
|
||||
export * from './lib/hlm-card-content';
|
||||
export * from './lib/hlm-card-description';
|
||||
export * from './lib/hlm-card-footer';
|
||||
export * from './lib/hlm-card-header';
|
||||
export * from './lib/hlm-card-title';
|
||||
|
||||
export const HlmCardImports = [
|
||||
HlmCard,
|
||||
HlmCardHeader,
|
||||
HlmCardFooter,
|
||||
HlmCardTitle,
|
||||
HlmCardDescription,
|
||||
HlmCardContent,
|
||||
HlmCardAction,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCardAction]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCardAction {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('col-start-2 row-span-2 row-start-1 self-start justify-self-end', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCardContent]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCardContent {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('px-6', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCardDescription]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCardDescription {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('text-muted-foreground text-sm', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCardFooter]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCardFooter {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('flex items-center px-6 [.border-t]:pt-6', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCardHeader]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCardHeader {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCardTitle]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCardTitle {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('leading-none font-semibold', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
export const cardVariants = cva('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', {
|
||||
variants: {},
|
||||
defaultVariants: {},
|
||||
});
|
||||
export type CardVariants = VariantProps<typeof cardVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCard]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCard {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm(cardVariants(), this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { HlmCommand } from './lib/hlm-command';
|
||||
import { HlmCommandDialog } from './lib/hlm-command-dialog';
|
||||
import { HlmCommandDialogCloseButton } from './lib/hlm-command-dialog-close-button';
|
||||
import { HlmCommandEmpty } from './lib/hlm-command-empty';
|
||||
import { HlmCommandGroup } from './lib/hlm-command-group';
|
||||
import { HlmCommandGroupLabel } from './lib/hlm-command-group-label';
|
||||
import { HlmCommandIcon } from './lib/hlm-command-icon';
|
||||
import { HlmCommandItem } from './lib/hlm-command-item';
|
||||
import { HlmCommandList } from './lib/hlm-command-list';
|
||||
import { HlmCommandSearch } from './lib/hlm-command-search';
|
||||
import { HlmCommandSearchInput } from './lib/hlm-command-search-input';
|
||||
import { HlmCommandSeparator } from './lib/hlm-command-separator';
|
||||
import { HlmCommandShortcut } from './lib/hlm-command-shortcut';
|
||||
|
||||
export * from './lib/hlm-command';
|
||||
export * from './lib/hlm-command-dialog';
|
||||
export * from './lib/hlm-command-dialog-close-button';
|
||||
export * from './lib/hlm-command-empty';
|
||||
export * from './lib/hlm-command-group';
|
||||
export * from './lib/hlm-command-group-label';
|
||||
export * from './lib/hlm-command-icon';
|
||||
export * from './lib/hlm-command-item';
|
||||
export * from './lib/hlm-command-list';
|
||||
export * from './lib/hlm-command-search';
|
||||
export * from './lib/hlm-command-search-input';
|
||||
export * from './lib/hlm-command-separator';
|
||||
export * from './lib/hlm-command-shortcut';
|
||||
|
||||
export const HlmCommandImports = [
|
||||
HlmCommand,
|
||||
HlmCommandItem,
|
||||
HlmCommandSeparator,
|
||||
HlmCommandGroup,
|
||||
HlmCommandList,
|
||||
HlmCommandShortcut,
|
||||
HlmCommandIcon,
|
||||
HlmCommandDialogCloseButton,
|
||||
HlmCommandDialog,
|
||||
HlmCommandSearchInput,
|
||||
HlmCommandSearch,
|
||||
HlmCommandGroupLabel,
|
||||
HlmCommandEmpty,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { BrnDialogClose } from '@spartan-ng/brain/dialog';
|
||||
import { HlmButton, provideBrnButtonConfig } from '@spartan-ng/helm/button';
|
||||
import { provideHlmIconConfig } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCommandDialogCloseBtn]',
|
||||
providers: [provideBrnButtonConfig({ variant: 'ghost' }), provideHlmIconConfig({ size: 'xs' })],
|
||||
hostDirectives: [HlmButton, BrnDialogClose],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCommandDialogCloseButton {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground ring-offset-background absolute top-3 right-3 inline-flex !h-5 h-10 !w-5 items-center justify-center rounded-md !p-1 px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { Directive, ElementRef, Renderer2, computed, contentChild, effect, inject, input, signal } from '@angular/core';
|
||||
import { BrnCommandSearchInputToken } from '@spartan-ng/brain/command';
|
||||
import { injectExposesStateProvider } from '@spartan-ng/brain/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCommandDialog]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCommandDialog {
|
||||
private readonly _stateProvider = injectExposesStateProvider({ host: true });
|
||||
public readonly state = this._stateProvider.state ?? signal('closed').asReadonly();
|
||||
private readonly _renderer = inject(Renderer2);
|
||||
private readonly _element = inject(ElementRef);
|
||||
|
||||
/** Access the search field */
|
||||
private readonly _searchInput = contentChild(BrnCommandSearchInputToken, { read: ElementRef });
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'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%]',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this._renderer.setAttribute(this._element.nativeElement, 'data-state', this.state());
|
||||
|
||||
const searchInput = this._searchInput();
|
||||
|
||||
if (this.state() === 'open' && searchInput) {
|
||||
searchInput.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCommandEmpty]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCommandEmpty {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('py-6 text-center text-sm', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command-group-label',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
role: 'presentation',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '<ng-content />',
|
||||
})
|
||||
export class HlmCommandGroupLabel {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('text-muted-foreground px-2 py-1.5 text-xs font-medium', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnCommandGroup } from '@spartan-ng/brain/command';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command-group',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnCommandGroup,
|
||||
inputs: ['id'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '<ng-content />',
|
||||
})
|
||||
export class HlmCommandGroup {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('text-foreground block overflow-hidden p-1 data-[hidden]:hidden', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { computed, Directive, input } from '@angular/core';
|
||||
import { provideHlmIconConfig } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmCommandIcon]',
|
||||
providers: [provideHlmIconConfig({ size: 'sm' })],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmCommandIcon {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('text-muted-foreground pointer-events-none shrink-0', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnCommandItem } from '@spartan-ng/brain/command';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'button[hlm-command-item]',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnCommandItem,
|
||||
inputs: ['value', 'disabled', 'id'],
|
||||
outputs: ['selected'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmCommandItem {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'data-[selected]:bg-accent data-[selected=true]:text-accent-foreground [&>ng-icon]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[hidden]:hidden [&>ng-icon]:pointer-events-none [&>ng-icon]:shrink-0 [&>ng-icon]:text-base',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnCommandList } from '@spartan-ng/brain/command';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command-list',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnCommandList,
|
||||
inputs: ['id'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '<ng-content />',
|
||||
})
|
||||
export class HlmCommandList {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('max-h-[300px] overflow-x-hidden overflow-y-auto', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnCommandSearchInput } from '@spartan-ng/brain/command';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'input[hlm-command-search-input]',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [{ directive: BrnCommandSearchInput, inputs: ['value'] }],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '',
|
||||
})
|
||||
export class HlmCommandSearchInput {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { provideHlmIconConfig } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command-search',
|
||||
providers: [provideHlmIconConfig({ size: 'sm' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmCommandSearch {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('flex h-9 items-center gap-2 border-b px-3 [&>_ng-icon]:flex-none [&>_ng-icon]:opacity-50', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command-separator',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
role: 'separator',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '',
|
||||
})
|
||||
export class HlmCommandSeparator {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<string>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() => hlm('bg-border -mx-1 block h-px', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command-shortcut',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: '<ng-content />',
|
||||
})
|
||||
export class HlmCommandShortcut {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('text-muted-foreground ml-auto text-xs tracking-widest', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { BrnCommand } from '@spartan-ng/brain/command';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-command',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: BrnCommand,
|
||||
inputs: ['id', 'filter'],
|
||||
outputs: ['valueChange'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmCommand {
|
||||
/** The user defined class */
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
/** The styles to apply */
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { HlmDialog } from './lib/hlm-dialog';
|
||||
import { HlmDialogClose } from './lib/hlm-dialog-close';
|
||||
import { HlmDialogContent } from './lib/hlm-dialog-content';
|
||||
import { HlmDialogDescription } from './lib/hlm-dialog-description';
|
||||
import { HlmDialogFooter } from './lib/hlm-dialog-footer';
|
||||
import { HlmDialogHeader } from './lib/hlm-dialog-header';
|
||||
import { HlmDialogOverlay } from './lib/hlm-dialog-overlay';
|
||||
import { HlmDialogTitle } from './lib/hlm-dialog-title';
|
||||
|
||||
export * from './lib/hlm-dialog';
|
||||
export * from './lib/hlm-dialog-close';
|
||||
export * from './lib/hlm-dialog-content';
|
||||
export * from './lib/hlm-dialog-description';
|
||||
export * from './lib/hlm-dialog-footer';
|
||||
export * from './lib/hlm-dialog-header';
|
||||
export * from './lib/hlm-dialog-overlay';
|
||||
export * from './lib/hlm-dialog-title';
|
||||
export * from './lib/hlm-dialog.service';
|
||||
|
||||
export const HlmDialogImports = [
|
||||
HlmDialog,
|
||||
HlmDialogClose,
|
||||
HlmDialogContent,
|
||||
HlmDialogDescription,
|
||||
HlmDialogFooter,
|
||||
HlmDialogHeader,
|
||||
HlmDialogOverlay,
|
||||
HlmDialogTitle,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmDialogClose],[brnDialogClose][hlm]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmDialogClose {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 flex items-center justify-center rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none [&_ng-icon]:shrink-0',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { NgComponentOutlet } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucideX } from '@ng-icons/lucide';
|
||||
import { BrnDialogClose, BrnDialogRef, injectBrnDialogContext } from '@spartan-ng/brain/dialog';
|
||||
import { HlmIcon } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { HlmDialogClose } from './hlm-dialog-close';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-dialog-content',
|
||||
imports: [NgComponentOutlet, BrnDialogClose, HlmDialogClose, NgIcon, HlmIcon],
|
||||
providers: [provideIcons({ lucideX })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
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>
|
||||
`,
|
||||
})
|
||||
export class HlmDialogContent {
|
||||
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(
|
||||
'bg-background 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 relative z-50 mx-auto grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border p-6 shadow-lg data-[state=closed]:duration-200 data-[state=open]:duration-200 sm:mx-0 sm:max-w-lg',
|
||||
this.userClass(),
|
||||
this._dynamicComponentClass,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { BrnDialogDescription } from '@spartan-ng/brain/dialog';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmDialogDescription]',
|
||||
hostDirectives: [BrnDialogDescription],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmDialogDescription {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('text-muted-foreground text-sm', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-dialog-footer',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmDialogFooter {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-dialog-header',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmDialogHeader {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('flex flex-col gap-2 text-center sm:text-left', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Directive, computed, effect, input, untracked } from '@angular/core';
|
||||
import { injectCustomClassSettable } from '@spartan-ng/brain/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
export const hlmDialogOverlayClass =
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/50';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmDialogOverlay],brn-dialog-overlay[hlm]',
|
||||
})
|
||||
export class HlmDialogOverlay {
|
||||
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(() => {
|
||||
const newClass = this._computedClass();
|
||||
untracked(() => this._classSettable?.setClassToCustomElement(newClass));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { BrnDialogTitle } from '@spartan-ng/brain/dialog';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmDialogTitle]',
|
||||
hostDirectives: [BrnDialogTitle],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmDialogTitle {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('text-lg leading-none font-semibold', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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 { HlmDialogContent } from './hlm-dialog-content';
|
||||
import { hlmDialogOverlayClass } from './hlm-dialog-overlay';
|
||||
|
||||
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,
|
||||
|
||||
...(options ?? {}),
|
||||
backdropClass: cssClassesToArray(`${hlmDialogOverlayClass} ${options?.backdropClass ?? ''}`),
|
||||
context: {
|
||||
...(options?.context && typeof options.context === 'object' ? options.context : {}),
|
||||
$component: component,
|
||||
$dynamicComponentClass: options?.contentClass,
|
||||
},
|
||||
};
|
||||
|
||||
return this._brnDialogService.open(HlmDialogContent, undefined, mergedOptions.context, mergedOptions);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
|
||||
import { BrnDialog, BrnDialogOverlay, provideBrnDialogDefaultOptions } from '@spartan-ng/brain/dialog';
|
||||
import { HlmDialogOverlay } from './hlm-dialog-overlay';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-dialog',
|
||||
exportAs: 'hlmDialog',
|
||||
imports: [BrnDialogOverlay, HlmDialogOverlay],
|
||||
providers: [
|
||||
{
|
||||
provide: BrnDialog,
|
||||
useExisting: forwardRef(() => HlmDialog),
|
||||
},
|
||||
provideBrnDialogDefaultOptions({
|
||||
// add custom options here
|
||||
}),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<brn-dialog-overlay hlm />
|
||||
<ng-content />
|
||||
`,
|
||||
})
|
||||
export class HlmDialog extends BrnDialog {}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { HlmError } from './lib/hlm-error';
|
||||
import { HlmFormField } from './lib/hlm-form-field';
|
||||
import { HlmHint } from './lib/hlm-hint';
|
||||
|
||||
export * from './lib/hlm-error';
|
||||
export * from './lib/hlm-form-field';
|
||||
export * from './lib/hlm-hint';
|
||||
|
||||
export const HlmFormFieldImports = [HlmFormField, HlmError, HlmHint] as const;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { computed, Directive, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||
selector: 'hlm-error',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmError {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('text-destructive block text-sm font-medium', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
contentChild,
|
||||
contentChildren,
|
||||
effect,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { BrnFormFieldControl } from '@spartan-ng/brain/form-field';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { HlmError } from './hlm-error';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-form-field',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
template: `
|
||||
<ng-content />
|
||||
|
||||
@switch (_hasDisplayedMessage()) {
|
||||
@case ('error') {
|
||||
<ng-content select="hlm-error" />
|
||||
}
|
||||
@default {
|
||||
<ng-content select="hlm-hint" />
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class HlmFormField {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('block space-y-2', this.userClass()));
|
||||
public readonly control = contentChild(BrnFormFieldControl);
|
||||
|
||||
public readonly errorChildren = contentChildren(HlmError);
|
||||
|
||||
protected readonly _hasDisplayedMessage = computed<'error' | 'hint'>(() =>
|
||||
this.errorChildren() && this.errorChildren().length > 0 && this.control()?.errorState() ? 'error' : 'hint',
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (!this.control()) {
|
||||
throw new Error('hlm-form-field must contain a BrnFormFieldControl.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { computed, Directive, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||
selector: 'hlm-hint',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmHint {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm('text-muted-foreground block text-sm', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { NgIcon } from '@ng-icons/core';
|
||||
import { HlmIcon } from './lib/hlm-icon';
|
||||
|
||||
export * from './lib/hlm-icon';
|
||||
export * from './lib/hlm-icon.token';
|
||||
|
||||
export const HlmIconImports = [HlmIcon, NgIcon] as const;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { InjectionToken, type ValueProvider, inject } from '@angular/core';
|
||||
import type { IconSize } from './hlm-icon';
|
||||
|
||||
export interface HlmIconConfig {
|
||||
size: IconSize;
|
||||
}
|
||||
|
||||
const defaultConfig: HlmIconConfig = {
|
||||
size: 'base',
|
||||
};
|
||||
|
||||
const HlmIconConfigToken = new InjectionToken<HlmIconConfig>('HlmIconConfig');
|
||||
|
||||
export function provideHlmIconConfig(config: Partial<HlmIconConfig>): ValueProvider {
|
||||
return { provide: HlmIconConfigToken, useValue: { ...defaultConfig, ...config } };
|
||||
}
|
||||
|
||||
export function injectHlmIconConfig(): HlmIconConfig {
|
||||
return inject(HlmIconConfigToken, { optional: true }) ?? defaultConfig;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { injectHlmIconConfig } from './hlm-icon.token';
|
||||
|
||||
export type IconSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | 'none' | (Record<never, never> & string);
|
||||
|
||||
@Directive({
|
||||
selector: 'ng-icon[hlmIcon], ng-icon[hlm]',
|
||||
host: {
|
||||
'[style.--ng-icon__size]': '_computedSize()',
|
||||
},
|
||||
})
|
||||
export class HlmIcon {
|
||||
private readonly _config = injectHlmIconConfig();
|
||||
public readonly size = input<IconSize>(this._config.size);
|
||||
|
||||
protected readonly _computedSize = computed(() => {
|
||||
const size = this.size();
|
||||
|
||||
switch (size) {
|
||||
case 'xs':
|
||||
return '12px';
|
||||
case 'sm':
|
||||
return '16px';
|
||||
case 'base':
|
||||
return '24px';
|
||||
case 'lg':
|
||||
return '32px';
|
||||
case 'xl':
|
||||
return '48px';
|
||||
default: {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { HlmInputGroup } from './lib/hlm-input-group';
|
||||
import { HlmInputGroupAddon } from './lib/hlm-input-group-addon';
|
||||
import { HlmInputGroupButton } from './lib/hlm-input-group-button';
|
||||
import { HlmInputGroupInput } from './lib/hlm-input-group-input';
|
||||
import { HlmInputGroupText } from './lib/hlm-input-group-text';
|
||||
import { HlmInputGroupTextarea } from './lib/hlm-input-group-textarea';
|
||||
|
||||
export * from './lib/hlm-input-group';
|
||||
export * from './lib/hlm-input-group-addon';
|
||||
export * from './lib/hlm-input-group-button';
|
||||
export * from './lib/hlm-input-group-input';
|
||||
export * from './lib/hlm-input-group-text';
|
||||
export * from './lib/hlm-input-group-textarea';
|
||||
|
||||
export const HlmInputGroupImports = [
|
||||
HlmInputGroup,
|
||||
HlmInputGroupAddon,
|
||||
HlmInputGroupButton,
|
||||
HlmInputGroupInput,
|
||||
HlmInputGroupText,
|
||||
HlmInputGroupTextarea,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { computed, Directive, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>ng-icon:not([class*='text-'])]:text-base",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
|
||||
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
|
||||
'block-start':
|
||||
'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',
|
||||
'block-end': 'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: 'inline-start',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type InputGroupAddonVariants = VariantProps<typeof inputGroupAddonVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: 'hlm-input-group-addon,[hlmInputGroupAddon]',
|
||||
host: {
|
||||
role: 'group',
|
||||
'data-slot': 'input-group-addon',
|
||||
'[attr.data-align]': 'align()',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmInputGroupAddon {
|
||||
public readonly align = input<InputGroupAddonVariants['align']>('inline-start');
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(inputGroupAddonVariants({ align: this.align() }), this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { computed, Directive, effect, inject, input } from '@angular/core';
|
||||
import { HlmButton, provideBrnButtonConfig } from '@spartan-ng/helm/button';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
const inputGroupAddonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>ng-icon]:px-2 [&>ng-icon:not([class*='text-'])]:text-sm",
|
||||
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>ng-icon]:px-2.5',
|
||||
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>ng-icon]:p-0',
|
||||
'icon-sm': 'size-8 p-0 has-[>ng-icon]:p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'xs',
|
||||
},
|
||||
});
|
||||
|
||||
type InputGroupAddonVariants = VariantProps<typeof inputGroupAddonVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: 'button[hlmInputGroupButton]',
|
||||
providers: [
|
||||
provideBrnButtonConfig({
|
||||
variant: 'ghost',
|
||||
}),
|
||||
],
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: HlmButton,
|
||||
inputs: ['variant'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[attr.data-size]': 'size()',
|
||||
'[type]': 'type()',
|
||||
},
|
||||
})
|
||||
export class HlmInputGroupButton {
|
||||
private readonly _hlmButton = inject(HlmButton);
|
||||
public readonly size = input<InputGroupAddonVariants['size']>('xs');
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
public readonly type = input<'button' | 'submit' | 'reset'>('button');
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(inputGroupAddonVariants({ size: this.size() }), this.userClass()),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this._hlmButton.setClass(this._computedClass());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { computed, Directive, effect, inject, input } from '@angular/core';
|
||||
import { HlmInput } from '@spartan-ng/helm/input';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: 'input[hlmInputGroupInput]',
|
||||
hostDirectives: [HlmInput],
|
||||
host: {
|
||||
'data-slot': 'input-group-control',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmInputGroupInput {
|
||||
private readonly _hlmInput = inject(HlmInput);
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this._hlmInput.setClass(this._computedClass());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { computed, Directive, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: 'span[hlmInputGroupText]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmInputGroupText {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
`text-muted-foreground flex items-center gap-2 text-sm [&_ng-icon]:pointer-events-none [&_ng-icon:not([class*='text-'])]:text-base`,
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { computed, Directive, effect, inject, input } from '@angular/core';
|
||||
import { HlmTextarea } from '@spartan-ng/helm/textarea';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: 'textarea[hlmInputGroupTextarea]',
|
||||
hostDirectives: [HlmTextarea],
|
||||
host: {
|
||||
'data-slot': 'input-group-control',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmInputGroupTextarea {
|
||||
private readonly _hlmInput = inject(HlmTextarea);
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this._hlmInput.setClass(this._computedClass());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { computed, Directive, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmInputGroup],hlm-input-group',
|
||||
host: {
|
||||
'data-slot': 'input-group',
|
||||
role: 'group',
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmInputGroup {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
|
||||
'h-9 min-w-0 has-[>textarea]:h-auto',
|
||||
// Variants based on alignment.
|
||||
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
||||
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||
// Focus state.
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
||||
// Error state.
|
||||
'has-[>.ng-invalid.ng-touched]:ring-destructive/20 has-[>.ng-invalid.ng-touched]:border-destructive dark:has-[>.ng-invalid.ng-touched]:ring-destructive/40',
|
||||
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { HlmNumberedPagination } from './lib/hlm-numbered-pagination';
|
||||
import { HlmNumberedPaginationQueryParams } from './lib/hlm-numbered-pagination-query-params';
|
||||
import { HlmPagination } from './lib/hlm-pagination';
|
||||
import { HlmPaginationContent } from './lib/hlm-pagination-content';
|
||||
import { HlmPaginationEllipsis } from './lib/hlm-pagination-ellipsis';
|
||||
import { HlmPaginationItem } from './lib/hlm-pagination-item';
|
||||
import { HlmPaginationLink } from './lib/hlm-pagination-link';
|
||||
import { HlmPaginationNext } from './lib/hlm-pagination-next';
|
||||
import { HlmPaginationPrevious } from './lib/hlm-pagination-previous';
|
||||
|
||||
export * from './lib/hlm-numbered-pagination';
|
||||
export * from './lib/hlm-numbered-pagination-query-params';
|
||||
export * from './lib/hlm-pagination';
|
||||
export * from './lib/hlm-pagination-content';
|
||||
export * from './lib/hlm-pagination-ellipsis';
|
||||
export * from './lib/hlm-pagination-item';
|
||||
export * from './lib/hlm-pagination-link';
|
||||
export * from './lib/hlm-pagination-next';
|
||||
export * from './lib/hlm-pagination-previous';
|
||||
|
||||
export const HlmPaginationImports = [
|
||||
HlmPagination,
|
||||
HlmPaginationContent,
|
||||
HlmPaginationItem,
|
||||
HlmPaginationLink,
|
||||
HlmPaginationPrevious,
|
||||
HlmPaginationNext,
|
||||
HlmPaginationEllipsis,
|
||||
HlmNumberedPagination,
|
||||
HlmNumberedPaginationQueryParams,
|
||||
] as const;
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
import type { BooleanInput, NumberInput } from '@angular/cdk/coercion';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
booleanAttribute,
|
||||
computed,
|
||||
input,
|
||||
model,
|
||||
numberAttribute,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrnSelectImports } from '@spartan-ng/brain/select';
|
||||
import { HlmSelectImports } from '@spartan-ng/helm/select';
|
||||
import { createPageArray, outOfBoundCorrection } from './hlm-numbered-pagination';
|
||||
import { HlmPagination } from './hlm-pagination';
|
||||
import { HlmPaginationContent } from './hlm-pagination-content';
|
||||
import { HlmPaginationEllipsis } from './hlm-pagination-ellipsis';
|
||||
import { HlmPaginationItem } from './hlm-pagination-item';
|
||||
import { HlmPaginationLink } from './hlm-pagination-link';
|
||||
import { HlmPaginationNext } from './hlm-pagination-next';
|
||||
import { HlmPaginationPrevious } from './hlm-pagination-previous';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-numbered-pagination-query-params',
|
||||
imports: [
|
||||
FormsModule,
|
||||
HlmPagination,
|
||||
HlmPaginationContent,
|
||||
HlmPaginationItem,
|
||||
HlmPaginationPrevious,
|
||||
HlmPaginationNext,
|
||||
HlmPaginationLink,
|
||||
HlmPaginationEllipsis,
|
||||
BrnSelectImports,
|
||||
HlmSelectImports,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="flex items-center justify-between gap-2 px-4 py-2">
|
||||
<div class="flex items-center gap-1 text-sm text-nowrap text-gray-600">
|
||||
<b>{{ totalItems() }}</b>
|
||||
total items |
|
||||
<b>{{ _lastPageNumber() }}</b>
|
||||
pages
|
||||
</div>
|
||||
|
||||
<nav hlmPagination>
|
||||
<ul hlmPaginationContent>
|
||||
@if (showEdges() && !_isFirstPageActive()) {
|
||||
<li hlmPaginationItem>
|
||||
<hlm-pagination-previous
|
||||
[link]="link()"
|
||||
[queryParams]="{ page: currentPage() - 1 }"
|
||||
queryParamsHandling="merge"
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (page of _pages(); track page) {
|
||||
<li hlmPaginationItem>
|
||||
@if (page === '...') {
|
||||
<hlm-pagination-ellipsis />
|
||||
} @else {
|
||||
<a
|
||||
hlmPaginationLink
|
||||
[link]="currentPage() !== page ? link() : undefined"
|
||||
[queryParams]="{ page }"
|
||||
queryParamsHandling="merge"
|
||||
[isActive]="currentPage() === page"
|
||||
>
|
||||
{{ page }}
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (showEdges() && !_isLastPageActive()) {
|
||||
<li hlmPaginationItem>
|
||||
<hlm-pagination-next
|
||||
[link]="link()"
|
||||
[queryParams]="{ page: currentPage() + 1 }"
|
||||
queryParamsHandling="merge"
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Show Page Size selector -->
|
||||
<brn-select [(ngModel)]="itemsPerPage" class="ml-auto" placeholder="Page size">
|
||||
<hlm-select-trigger class="w-fit">
|
||||
<hlm-select-value />
|
||||
</hlm-select-trigger>
|
||||
<hlm-select-content>
|
||||
@for (pageSize of _pageSizesWithCurrent(); track pageSize) {
|
||||
<hlm-option [value]="pageSize">{{ pageSize }} / page</hlm-option>
|
||||
}
|
||||
</hlm-select-content>
|
||||
</brn-select>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class HlmNumberedPaginationQueryParams {
|
||||
/**
|
||||
* The current (active) page.
|
||||
*/
|
||||
public readonly currentPage = model.required<number>();
|
||||
|
||||
/**
|
||||
* The number of items per paginated page.
|
||||
*/
|
||||
public readonly itemsPerPage = model.required<number>();
|
||||
|
||||
/**
|
||||
* The total number of items in the collection. Only useful when
|
||||
* doing server-side paging, where the collection size is limited
|
||||
* to a single page returned by the server API.
|
||||
*/
|
||||
public readonly totalItems = input.required<number, NumberInput>({
|
||||
transform: numberAttribute,
|
||||
});
|
||||
|
||||
/**
|
||||
* The URL path to use for the pagination links.
|
||||
* Defaults to '.' (current path).
|
||||
*/
|
||||
public readonly link = input<string>('.');
|
||||
|
||||
/**
|
||||
* The number of page links to show.
|
||||
*/
|
||||
public readonly maxSize = input<number, NumberInput>(7, {
|
||||
transform: numberAttribute,
|
||||
});
|
||||
|
||||
/**
|
||||
* Show the first and last page buttons.
|
||||
*/
|
||||
public readonly showEdges = input<boolean, BooleanInput>(true, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
|
||||
/**
|
||||
* The page sizes to show.
|
||||
* Defaults to [10, 20, 50, 100]
|
||||
*/
|
||||
public readonly pageSizes = input<number[]>([10, 20, 50, 100]);
|
||||
|
||||
protected readonly _pageSizesWithCurrent = computed(() => {
|
||||
const pageSizes = this.pageSizes();
|
||||
return pageSizes.includes(this.itemsPerPage())
|
||||
? pageSizes // if current page size is included, return the same array
|
||||
: [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
|
||||
});
|
||||
|
||||
protected readonly _isFirstPageActive = computed(() => this.currentPage() === 1);
|
||||
protected readonly _isLastPageActive = computed(() => this.currentPage() === this._lastPageNumber());
|
||||
|
||||
protected readonly _lastPageNumber = computed(() => {
|
||||
if (this.totalItems() < 1) {
|
||||
// when there are 0 or fewer (an error case) items, there are no "pages" as such,
|
||||
// but it makes sense to consider a single, empty page as the last page.
|
||||
return 1;
|
||||
}
|
||||
return Math.ceil(this.totalItems() / this.itemsPerPage());
|
||||
});
|
||||
|
||||
protected readonly _pages = computed(() => {
|
||||
const correctedCurrentPage = outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage());
|
||||
|
||||
if (correctedCurrentPage !== this.currentPage()) {
|
||||
// update the current page
|
||||
untracked(() => this.currentPage.set(correctedCurrentPage));
|
||||
}
|
||||
|
||||
return createPageArray(correctedCurrentPage, this.itemsPerPage(), this.totalItems(), this.maxSize());
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
import type { BooleanInput, NumberInput } from '@angular/cdk/coercion';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
booleanAttribute,
|
||||
computed,
|
||||
input,
|
||||
model,
|
||||
numberAttribute,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrnSelectImports } from '@spartan-ng/brain/select';
|
||||
import { HlmSelectImports } from '@spartan-ng/helm/select';
|
||||
import { HlmPagination } from './hlm-pagination';
|
||||
import { HlmPaginationContent } from './hlm-pagination-content';
|
||||
import { HlmPaginationEllipsis } from './hlm-pagination-ellipsis';
|
||||
import { HlmPaginationItem } from './hlm-pagination-item';
|
||||
import { HlmPaginationLink } from './hlm-pagination-link';
|
||||
import { HlmPaginationNext } from './hlm-pagination-next';
|
||||
import { HlmPaginationPrevious } from './hlm-pagination-previous';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-numbered-pagination',
|
||||
imports: [
|
||||
FormsModule,
|
||||
HlmPagination,
|
||||
HlmPaginationContent,
|
||||
HlmPaginationItem,
|
||||
HlmPaginationPrevious,
|
||||
HlmPaginationNext,
|
||||
HlmPaginationLink,
|
||||
HlmPaginationEllipsis,
|
||||
BrnSelectImports,
|
||||
HlmSelectImports,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="flex items-center justify-between gap-2 px-4 py-2">
|
||||
<div class="flex items-center gap-1 text-sm text-nowrap text-gray-600">
|
||||
<b>{{ totalItems() }}</b>
|
||||
total items |
|
||||
<b>{{ _lastPageNumber() }}</b>
|
||||
pages
|
||||
</div>
|
||||
|
||||
<nav hlmPagination>
|
||||
<ul hlmPaginationContent>
|
||||
@if (showEdges() && !_isFirstPageActive()) {
|
||||
<li hlmPaginationItem (click)="goToPrevious()">
|
||||
<hlm-pagination-previous />
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (page of _pages(); track page) {
|
||||
<li hlmPaginationItem>
|
||||
@if (page === '...') {
|
||||
<hlm-pagination-ellipsis />
|
||||
} @else {
|
||||
<a hlmPaginationLink [isActive]="currentPage() === page" (click)="currentPage.set(page)">
|
||||
{{ page }}
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (showEdges() && !_isLastPageActive()) {
|
||||
<li hlmPaginationItem (click)="goToNext()">
|
||||
<hlm-pagination-next />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Show Page Size selector -->
|
||||
<brn-select [(ngModel)]="itemsPerPage" class="ml-auto" placeholder="Page size">
|
||||
<hlm-select-trigger class="w-fit">
|
||||
<hlm-select-value />
|
||||
</hlm-select-trigger>
|
||||
<hlm-select-content>
|
||||
@for (pageSize of _pageSizesWithCurrent(); track pageSize) {
|
||||
<hlm-option [value]="pageSize">{{ pageSize }} / page</hlm-option>
|
||||
}
|
||||
</hlm-select-content>
|
||||
</brn-select>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class HlmNumberedPagination {
|
||||
/**
|
||||
* The current (active) page.
|
||||
*/
|
||||
public readonly currentPage = model.required<number>();
|
||||
|
||||
/**
|
||||
* The number of items per paginated page.
|
||||
*/
|
||||
public readonly itemsPerPage = model.required<number>();
|
||||
|
||||
/**
|
||||
* The total number of items in the collection. Only useful when
|
||||
* doing server-side paging, where the collection size is limited
|
||||
* to a single page returned by the server API.
|
||||
*/
|
||||
public readonly totalItems = input.required<number, NumberInput>({
|
||||
transform: numberAttribute,
|
||||
});
|
||||
|
||||
/**
|
||||
* The number of page links to show.
|
||||
*/
|
||||
public readonly maxSize = input<number, NumberInput>(7, {
|
||||
transform: numberAttribute,
|
||||
});
|
||||
|
||||
/**
|
||||
* Show the first and last page buttons.
|
||||
*/
|
||||
public readonly showEdges = input<boolean, BooleanInput>(true, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
|
||||
/**
|
||||
* The page sizes to show.
|
||||
* Defaults to [10, 20, 50, 100]
|
||||
*/
|
||||
public readonly pageSizes = input<number[]>([10, 20, 50, 100]);
|
||||
|
||||
protected readonly _pageSizesWithCurrent = computed(() => {
|
||||
const pageSizes = this.pageSizes();
|
||||
return pageSizes.includes(this.itemsPerPage())
|
||||
? pageSizes // if current page size is included, return the same array
|
||||
: [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
|
||||
});
|
||||
|
||||
protected readonly _isFirstPageActive = computed(() => this.currentPage() === 1);
|
||||
protected readonly _isLastPageActive = computed(() => this.currentPage() === this._lastPageNumber());
|
||||
|
||||
protected readonly _lastPageNumber = computed(() => {
|
||||
if (this.totalItems() < 1) {
|
||||
// when there are 0 or fewer (an error case) items, there are no "pages" as such,
|
||||
// but it makes sense to consider a single, empty page as the last page.
|
||||
return 1;
|
||||
}
|
||||
return Math.ceil(this.totalItems() / this.itemsPerPage());
|
||||
});
|
||||
|
||||
protected readonly _pages = computed(() => {
|
||||
const correctedCurrentPage = outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage());
|
||||
|
||||
if (correctedCurrentPage !== this.currentPage()) {
|
||||
// update the current page
|
||||
untracked(() => this.currentPage.set(correctedCurrentPage));
|
||||
}
|
||||
|
||||
return createPageArray(correctedCurrentPage, this.itemsPerPage(), this.totalItems(), this.maxSize());
|
||||
});
|
||||
|
||||
protected goToPrevious(): void {
|
||||
this.currentPage.set(this.currentPage() - 1);
|
||||
}
|
||||
|
||||
protected goToNext(): void {
|
||||
this.currentPage.set(this.currentPage() + 1);
|
||||
}
|
||||
|
||||
protected goToFirst(): void {
|
||||
this.currentPage.set(1);
|
||||
}
|
||||
|
||||
protected goToLast(): void {
|
||||
this.currentPage.set(this._lastPageNumber());
|
||||
}
|
||||
}
|
||||
|
||||
type Page = number | '...';
|
||||
|
||||
/**
|
||||
* Checks that the instance.currentPage property is within bounds for the current page range.
|
||||
* If not, return a correct value for currentPage, or the current value if OK.
|
||||
*
|
||||
* Copied from 'ngx-pagination' package
|
||||
*/
|
||||
export function outOfBoundCorrection(totalItems: number, itemsPerPage: number, currentPage: number): number {
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
if (totalPages < currentPage && 0 < totalPages) {
|
||||
return totalPages;
|
||||
}
|
||||
|
||||
if (currentPage < 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Page objects to use in the pagination controls.
|
||||
*
|
||||
* Copied from 'ngx-pagination' package
|
||||
*/
|
||||
export function createPageArray(
|
||||
currentPage: number,
|
||||
itemsPerPage: number,
|
||||
totalItems: number,
|
||||
paginationRange: number,
|
||||
): Page[] {
|
||||
// paginationRange could be a string if passed from attribute, so cast to number.
|
||||
paginationRange = +paginationRange;
|
||||
const pages: Page[] = [];
|
||||
|
||||
// Return 1 as default page number
|
||||
// Make sense to show 1 instead of empty when there are no items
|
||||
const totalPages = Math.max(Math.ceil(totalItems / itemsPerPage), 1);
|
||||
const halfWay = Math.ceil(paginationRange / 2);
|
||||
|
||||
const isStart = currentPage <= halfWay;
|
||||
const isEnd = totalPages - halfWay < currentPage;
|
||||
const isMiddle = !isStart && !isEnd;
|
||||
|
||||
const ellipsesNeeded = paginationRange < totalPages;
|
||||
let i = 1;
|
||||
|
||||
while (i <= totalPages && i <= paginationRange) {
|
||||
let label: number | '...';
|
||||
const pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
|
||||
const openingEllipsesNeeded = i === 2 && (isMiddle || isEnd);
|
||||
const closingEllipsesNeeded = i === paginationRange - 1 && (isMiddle || isStart);
|
||||
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
|
||||
label = '...';
|
||||
} else {
|
||||
label = pageNumber;
|
||||
}
|
||||
pages.push(label);
|
||||
i++;
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the position in the sequence of pagination links [i],
|
||||
* figure out what page number corresponds to that position.
|
||||
*
|
||||
* Copied from 'ngx-pagination' package
|
||||
*/
|
||||
function calculatePageNumber(i: number, currentPage: number, paginationRange: number, totalPages: number) {
|
||||
const halfWay = Math.ceil(paginationRange / 2);
|
||||
if (i === paginationRange) {
|
||||
return totalPages;
|
||||
}
|
||||
|
||||
if (i === 1) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (paginationRange < totalPages) {
|
||||
if (totalPages - halfWay < currentPage) {
|
||||
return totalPages - paginationRange + i;
|
||||
}
|
||||
if (halfWay < currentPage) {
|
||||
return currentPage - halfWay + i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
export const paginationContentVariants = cva('flex flex-row items-center gap-1', {
|
||||
variants: {},
|
||||
defaultVariants: {},
|
||||
});
|
||||
export type PaginationContentVariants = VariantProps<typeof paginationContentVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmPaginationContent]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmPaginationContent {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm(paginationContentVariants(), this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucideEllipsis } from '@ng-icons/lucide';
|
||||
import { HlmIcon } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-pagination-ellipsis',
|
||||
imports: [NgIcon, HlmIcon],
|
||||
providers: [provideIcons({ lucideEllipsis })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<span [class]="_computedClass()">
|
||||
<ng-icon hlm size="sm" name="lucideEllipsis" />
|
||||
<span class="sr-only">{{ srOnlyText() }}</span>
|
||||
</span>
|
||||
`,
|
||||
})
|
||||
export class HlmPaginationEllipsis {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
/** Screen reader only text for the ellipsis */
|
||||
public readonly srOnlyText = input<string>('More pages');
|
||||
|
||||
protected readonly _computedClass = computed(() => hlm('flex size-9 items-center justify-center', this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
export const paginationItemVariants = cva('', {
|
||||
variants: {},
|
||||
defaultVariants: {},
|
||||
});
|
||||
|
||||
export type PaginationItemVariants = VariantProps<typeof paginationItemVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmPaginationItem]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmPaginationItem {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() => hlm(paginationItemVariants(), this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import type { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { Directive, booleanAttribute, computed, input } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { type ButtonVariants, buttonVariants } from '@spartan-ng/helm/button';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
export const paginationLinkVariants = cva('', {
|
||||
variants: {},
|
||||
defaultVariants: {},
|
||||
});
|
||||
export type PaginationLinkVariants = VariantProps<typeof paginationLinkVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmPaginationLink]',
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: RouterLink,
|
||||
inputs: [
|
||||
'target',
|
||||
'queryParams',
|
||||
'fragment',
|
||||
'queryParamsHandling',
|
||||
'state',
|
||||
'info',
|
||||
'relativeTo',
|
||||
'preserveFragment',
|
||||
'skipLocationChange',
|
||||
'replaceUrl',
|
||||
'routerLink: link',
|
||||
],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
'[attr.aria-current]': 'isActive() ? "page" : null',
|
||||
},
|
||||
})
|
||||
export class HlmPaginationLink {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
/** Whether the link is active (i.e., the current page). */
|
||||
public readonly isActive = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
|
||||
/** The size of the button. */
|
||||
public readonly size = input<ButtonVariants['size']>('icon');
|
||||
/** The link to navigate to the page. */
|
||||
public readonly link = input<RouterLink['routerLink']>();
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
paginationLinkVariants(),
|
||||
this.link() === undefined ? 'cursor-pointer' : '',
|
||||
buttonVariants({
|
||||
variant: this.isActive() ? 'outline' : 'ghost',
|
||||
size: this.size(),
|
||||
}),
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import type { RouterLink } from '@angular/router';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucideChevronRight } from '@ng-icons/lucide';
|
||||
import type { ButtonVariants } from '@spartan-ng/helm/button';
|
||||
import { HlmIcon } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { HlmPaginationLink } from './hlm-pagination-link';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-pagination-next',
|
||||
imports: [HlmPaginationLink, NgIcon, HlmIcon],
|
||||
providers: [provideIcons({ lucideChevronRight })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<a
|
||||
[class]="_computedClass()"
|
||||
hlmPaginationLink
|
||||
[link]="link()"
|
||||
[queryParams]="queryParams()"
|
||||
[queryParamsHandling]="queryParamsHandling()"
|
||||
[size]="_size()"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
>
|
||||
<span [class]="_labelClass()">{{ text() }}</span>
|
||||
<ng-icon hlm size="sm" name="lucideChevronRight" />
|
||||
</a>
|
||||
`,
|
||||
})
|
||||
export class HlmPaginationNext {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
/** The link to navigate to the next page. */
|
||||
public readonly link = input<RouterLink['routerLink']>();
|
||||
/** The query parameters to pass to the next page. */
|
||||
public readonly queryParams = input<RouterLink['queryParams']>();
|
||||
/** How to handle query parameters when navigating to the next page. */
|
||||
public readonly queryParamsHandling = input<RouterLink['queryParamsHandling']>();
|
||||
|
||||
/** The aria-label for the next page link. */
|
||||
public readonly ariaLabel = input<string>('Go to next page', { alias: 'aria-label' });
|
||||
/** The text to display for the next page link. */
|
||||
public readonly text = input<string>('Next');
|
||||
/** Whether the button should only display the icon. */
|
||||
public readonly iconOnly = input<boolean, BooleanInput>(false, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
protected readonly _labelClass = computed(() => (this.iconOnly() ? 'sr-only' : 'hidden sm:block'));
|
||||
|
||||
protected readonly _size = computed<ButtonVariants['size']>(() => (this.iconOnly() ? 'icon' : 'default'));
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('gap-1', !this.iconOnly() ? 'sm:pr-2.5' : '', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||
import type { RouterLink } from '@angular/router';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucideChevronLeft } from '@ng-icons/lucide';
|
||||
import type { ButtonVariants } from '@spartan-ng/helm/button';
|
||||
import { HlmIcon } from '@spartan-ng/helm/icon';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { HlmPaginationLink } from './hlm-pagination-link';
|
||||
|
||||
@Component({
|
||||
selector: 'hlm-pagination-previous',
|
||||
imports: [HlmPaginationLink, NgIcon, HlmIcon],
|
||||
providers: [provideIcons({ lucideChevronLeft })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<a
|
||||
[class]="_computedClass()"
|
||||
hlmPaginationLink
|
||||
[link]="link()"
|
||||
[queryParams]="queryParams()"
|
||||
[queryParamsHandling]="queryParamsHandling()"
|
||||
[size]="_size()"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
>
|
||||
<ng-icon hlm size="sm" name="lucideChevronLeft" />
|
||||
<span [class]="_labelClass()">{{ text() }}</span>
|
||||
</a>
|
||||
`,
|
||||
})
|
||||
export class HlmPaginationPrevious {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
/** The link to navigate to the previous page. */
|
||||
public readonly link = input<RouterLink['routerLink']>();
|
||||
/** The query parameters to pass to the previous page. */
|
||||
public readonly queryParams = input<RouterLink['queryParams']>();
|
||||
/** How to handle query parameters when navigating to the previous page. */
|
||||
public readonly queryParamsHandling = input<RouterLink['queryParamsHandling']>();
|
||||
|
||||
/** The aria-label for the previous page link. */
|
||||
public readonly ariaLabel = input<string>('Go to previous page', { alias: 'aria-label' });
|
||||
/** The text to display for the previous page link. */
|
||||
public readonly text = input<string>('Previous');
|
||||
/** Whether the button should only display the icon. */
|
||||
public readonly iconOnly = input<boolean, BooleanInput>(false, {
|
||||
transform: booleanAttribute,
|
||||
});
|
||||
protected readonly _labelClass = computed(() => (this.iconOnly() ? 'sr-only' : 'hidden sm:block'));
|
||||
|
||||
protected readonly _size = computed<ButtonVariants['size']>(() => (this.iconOnly() ? 'icon' : 'default'));
|
||||
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm('gap-1', !this.iconOnly() ? 'sm:pl-2.5' : '', this.userClass()),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
export const paginationVariants = cva('mx-auto flex w-full justify-center', {
|
||||
variants: {},
|
||||
defaultVariants: {},
|
||||
});
|
||||
export type PaginationVariants = VariantProps<typeof paginationVariants>;
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmPagination]',
|
||||
host: {
|
||||
role: 'navigation',
|
||||
'[class]': '_computedClass()',
|
||||
'[attr.aria-label]': 'ariaLabel()',
|
||||
},
|
||||
})
|
||||
export class HlmPagination {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
|
||||
/** The aria-label for the pagination component. */
|
||||
public readonly ariaLabel = input<string>('pagination', { alias: 'aria-label' });
|
||||
|
||||
protected readonly _computedClass = computed(() => hlm(paginationVariants(), this.userClass()));
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { HlmPopoverClose } from './lib/hlm-popover-close';
|
||||
import { HlmPopoverContent } from './lib/hlm-popover-content';
|
||||
|
||||
export * from './lib/hlm-popover-close';
|
||||
export * from './lib/hlm-popover-content';
|
||||
|
||||
export const HlmPopoverImports = [HlmPopoverContent, HlmPopoverClose] as const;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmPopoverClose],[brnPopoverClose][hlm]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmPopoverClose {
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Directive, ElementRef, Renderer2, computed, effect, inject, input, signal } from '@angular/core';
|
||||
import { injectExposesStateProvider } from '@spartan-ng/brain/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: '[hlmPopoverContent],[brnPopoverContent][hlm]',
|
||||
host: {
|
||||
'[class]': '_computedClass()',
|
||||
},
|
||||
})
|
||||
export class HlmPopoverContent {
|
||||
private readonly _stateProvider = injectExposesStateProvider({ host: true });
|
||||
public state = this._stateProvider.state ?? signal('closed');
|
||||
private readonly _renderer = inject(Renderer2);
|
||||
private readonly _element = inject(ElementRef);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this._renderer.setAttribute(this._element.nativeElement, 'data-state', this.state());
|
||||
});
|
||||
}
|
||||
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
protected readonly _computedClass = computed(() =>
|
||||
hlm(
|
||||
'border-border bg-popover text-popover-foreground 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 relative w-72 rounded-md border p-4 shadow-md outline-none',
|
||||
this.userClass(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { HlmScrollArea } from './lib/hlm-scroll-area';
|
||||
|
||||
export * from './lib/hlm-scroll-area';
|
||||
|
||||
export const HlmScrollAreaImports = [HlmScrollArea] as const;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Directive, computed, input } from '@angular/core';
|
||||
import { hlm } from '@spartan-ng/helm/utils';
|
||||
import type { ClassValue } from 'clsx';
|
||||
|
||||
@Directive({
|
||||
selector: 'ng-scrollbar[hlm]',
|
||||
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 HlmScrollArea {
|
||||
protected readonly _computedClass = computed(() => hlm('block', this.userClass()));
|
||||
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { HlmSelect } from './lib/hlm-select';
|
||||
import { HlmSelectContent } from './lib/hlm-select-content';
|
||||
import { HlmSelectGroup } from './lib/hlm-select-group';
|
||||
import { HlmSelectLabel } from './lib/hlm-select-label';
|
||||
import { HlmSelectOption } from './lib/hlm-select-option';
|
||||
import { HlmSelectScrollDown } from './lib/hlm-select-scroll-down';
|
||||
import { HlmSelectScrollUp } from './lib/hlm-select-scroll-up';
|
||||
import { HlmSelectTrigger } from './lib/hlm-select-trigger';
|
||||
import { HlmSelectValue } from './lib/hlm-select-value';
|
||||
|
||||
export * from './lib/hlm-select';
|
||||
export * from './lib/hlm-select-content';
|
||||
export * from './lib/hlm-select-group';
|
||||
export * from './lib/hlm-select-label';
|
||||
export * from './lib/hlm-select-option';
|
||||
export * from './lib/hlm-select-scroll-down';
|
||||
export * from './lib/hlm-select-scroll-up';
|
||||
export * from './lib/hlm-select-trigger';
|
||||
export * from './lib/hlm-select-value';
|
||||
|
||||
export const HlmSelectImports = [
|
||||
HlmSelectContent,
|
||||
HlmSelectTrigger,
|
||||
HlmSelectOption,
|
||||
HlmSelectValue,
|
||||
HlmSelect,
|
||||
HlmSelectScrollUp,
|
||||
HlmSelectScrollDown,
|
||||
HlmSelectLabel,
|
||||
HlmSelectGroup,
|
||||
] as const;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue