Skip to content

Commit 8415a96

Browse files
committed
Add more componenets
1 parent 0910e95 commit 8415a96

File tree

6 files changed

+347
-6
lines changed

6 files changed

+347
-6
lines changed

src/app/features/spaces/translations/translations.component.html

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@
1818
<mat-icon>account_tree</mat-icon>
1919
</mat-button-toggle>
2020
</mat-button-toggle-group>
21-
@if ('TRANSLATION_CREATE' | canUserPerform | async) {
22-
<button mat-flat-button (click)="openAddDialog()">
23-
<mat-icon>add</mat-icon>
24-
Add Translation
25-
</button>
26-
}
2721
@if ('TRANSLATION_CREATE' | canUserPerform | async) {
2822
<button z-button zType="destructive" (click)="openAddDialog()">
2923
<mat-icon>add</mat-icon>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { ClassValue } from 'clsx';
2+
3+
import { ChangeDetectionStrategy, Component, computed, forwardRef, input, output, signal, ViewEncapsulation } from '@angular/core';
4+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
5+
6+
import { mergeClasses } from '@shared/utils/merge-classes';
7+
import { toggleGroupVariants, toggleGroupItemVariants } from './toggle-group.variants';
8+
9+
export interface ZardToggleGroupItem {
10+
value: string;
11+
label?: string;
12+
icon?: string;
13+
disabled?: boolean;
14+
ariaLabel?: string;
15+
}
16+
17+
type OnTouchedType = () => void;
18+
type OnChangeType = (value: string | string[]) => void;
19+
20+
@Component({
21+
selector: 'z-toggle-group',
22+
exportAs: 'zToggleGroup',
23+
standalone: true,
24+
changeDetection: ChangeDetectionStrategy.OnPush,
25+
encapsulation: ViewEncapsulation.None,
26+
template: `
27+
<div [class]="classes()" role="group" [attr.data-orientation]="'horizontal'">
28+
@for (item of items(); track item.value; let i = $index) {
29+
<button
30+
type="button"
31+
[attr.aria-pressed]="isItemPressed(item.value)"
32+
[attr.data-state]="isItemPressed(item.value) ? 'on' : 'off'"
33+
[attr.aria-label]="item.ariaLabel"
34+
[class]="getItemClasses(i, items().length)"
35+
[disabled]="disabled() || item.disabled"
36+
(click)="toggleItem(item)"
37+
>
38+
@if (item.icon) {
39+
<span [class]="item.icon + ' w-4 h-4 shrink-0'"></span>
40+
}
41+
@if (item.label) {
42+
<span>{{ item.label }}</span>
43+
} @else if (!item.icon) {
44+
<span>{{ item.value }}</span>
45+
}
46+
</button>
47+
}
48+
</div>
49+
`,
50+
providers: [
51+
{
52+
provide: NG_VALUE_ACCESSOR,
53+
useExisting: forwardRef(() => ZardToggleGroupComponent),
54+
multi: true,
55+
},
56+
],
57+
})
58+
export class ZardToggleGroupComponent implements ControlValueAccessor {
59+
readonly zMode = input<'single' | 'multiple'>('multiple');
60+
readonly zType = input<'default' | 'outline'>('default');
61+
readonly zSize = input<'sm' | 'md' | 'lg'>('md');
62+
readonly value = input<string | string[]>();
63+
readonly defaultValue = input<string | string[]>();
64+
readonly disabled = input<boolean>(false);
65+
readonly class = input<ClassValue>('');
66+
readonly items = input<ZardToggleGroupItem[]>([]);
67+
68+
readonly valueChange = output<string | string[]>();
69+
70+
private internalValue = signal<string | string[] | undefined>(undefined);
71+
72+
protected readonly classes = computed(() =>
73+
mergeClasses(
74+
toggleGroupVariants({
75+
zType: this.zType(),
76+
zSize: this.zSize(),
77+
}),
78+
this.class(),
79+
),
80+
);
81+
82+
protected readonly currentValue = computed(() => {
83+
const internal = this.internalValue();
84+
const input = this.value();
85+
const defaultVal = this.defaultValue();
86+
87+
if (internal !== undefined) return internal;
88+
if (input !== undefined) return input;
89+
if (defaultVal !== undefined) return defaultVal;
90+
91+
return this.zMode() === 'single' ? '' : [];
92+
});
93+
94+
protected getItemClasses(index: number, total: number): string {
95+
const baseClasses = toggleGroupItemVariants({
96+
zType: this.zType(),
97+
zSize: this.zSize(),
98+
});
99+
100+
const positionClasses = [];
101+
102+
// Add rounded corners for first and last items
103+
if (index === 0) {
104+
positionClasses.push('first:rounded-l-md');
105+
}
106+
if (index === total - 1) {
107+
positionClasses.push('last:rounded-r-md');
108+
}
109+
110+
// Handle borders for outline variant
111+
if (this.zType() === 'outline') {
112+
if (index === 0) {
113+
// First item gets full border
114+
positionClasses.push('border-l');
115+
} else {
116+
// Other items don't get left border (connects to previous)
117+
positionClasses.push('border-l-0');
118+
}
119+
}
120+
121+
// Focus z-index
122+
positionClasses.push('focus:z-10', 'focus-visible:z-10');
123+
124+
return mergeClasses(baseClasses, ...positionClasses);
125+
}
126+
127+
protected isItemPressed(itemValue: string): boolean {
128+
const current = this.currentValue();
129+
if (this.zMode() === 'single') {
130+
return current === itemValue;
131+
}
132+
return Array.isArray(current) && current.includes(itemValue);
133+
}
134+
135+
// eslint-disable-next-line @typescript-eslint/no-empty-function
136+
private onTouched: OnTouchedType = () => {};
137+
// eslint-disable-next-line @typescript-eslint/no-empty-function
138+
private onChangeFn: OnChangeType = () => {};
139+
140+
toggleItem(item: ZardToggleGroupItem) {
141+
if (this.disabled() || item.disabled) return;
142+
143+
const currentValue = this.currentValue();
144+
let newValue: string | string[];
145+
146+
if (this.zMode() === 'single') {
147+
newValue = currentValue === item.value ? '' : item.value;
148+
} else {
149+
const currentArray = Array.isArray(currentValue) ? currentValue : [];
150+
if (currentArray.includes(item.value)) {
151+
newValue = currentArray.filter(v => v !== item.value);
152+
} else {
153+
newValue = [...currentArray, item.value];
154+
}
155+
}
156+
157+
this.internalValue.set(newValue);
158+
this.valueChange.emit(newValue);
159+
this.onChangeFn(newValue);
160+
this.onTouched();
161+
}
162+
163+
writeValue(value: string | string[]): void {
164+
if (value !== undefined) {
165+
this.internalValue.set(value);
166+
}
167+
}
168+
169+
registerOnChange(fn: OnChangeType): void {
170+
this.onChangeFn = fn;
171+
}
172+
173+
registerOnTouched(fn: OnTouchedType): void {
174+
this.onTouched = fn;
175+
}
176+
177+
setDisabledState(_isDisabled: boolean): void {
178+
// Note: disabled state is handled through the disabled input
179+
// This method is required by ControlValueAccessor interface
180+
}
181+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { cva, VariantProps } from 'class-variance-authority';
2+
3+
export const toggleGroupVariants = cva('flex w-fit items-center rounded-md', {
4+
variants: {
5+
zType: {
6+
default: '',
7+
outline: 'shadow-sm',
8+
},
9+
zSize: {
10+
sm: '',
11+
md: '',
12+
lg: '',
13+
},
14+
},
15+
defaultVariants: {
16+
zType: 'default',
17+
zSize: 'md',
18+
},
19+
});
20+
21+
export const toggleGroupItemVariants = cva(
22+
'inline-flex items-center justify-center whitespace-nowrap rounded-none gap-2 text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
23+
{
24+
variants: {
25+
zType: {
26+
default: 'bg-transparent',
27+
outline: 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
28+
},
29+
zSize: {
30+
sm: 'h-8 px-2.5 text-xs',
31+
md: 'h-9 px-3 text-sm',
32+
lg: 'h-10 px-4 text-sm',
33+
},
34+
},
35+
defaultVariants: {
36+
zType: 'default',
37+
zSize: 'md',
38+
},
39+
},
40+
);
41+
42+
export type ZardToggleGroupVariants = VariantProps<typeof toggleGroupVariants>;
43+
export type ZardToggleGroupItemVariants = VariantProps<typeof toggleGroupItemVariants>;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ChangeDetectionStrategy, Component, forwardRef, HostListener, ViewEncapsulation, signal, computed, input, output, linkedSignal } from '@angular/core';
2+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3+
import { ClassValue } from 'clsx';
4+
5+
import { toggleVariants, ZardToggleVariants } from './toggle.variants';
6+
import { mergeClasses, transform } from '@shared/utils/merge-classes';
7+
8+
type OnTouchedType = () => void;
9+
type OnChangeType = (value: boolean) => void;
10+
11+
@Component({
12+
selector: 'z-toggle',
13+
exportAs: 'zToggle',
14+
changeDetection: ChangeDetectionStrategy.OnPush,
15+
encapsulation: ViewEncapsulation.None,
16+
template: `
17+
<button
18+
type="button"
19+
[attr.aria-label]="zAriaLabel()"
20+
[attr.aria-pressed]="value()"
21+
[attr.data-state]="value() ? 'on' : 'off'"
22+
[class]="classes()"
23+
[disabled]="disabled()"
24+
(click)="toggle()"
25+
>
26+
<ng-content></ng-content>
27+
</button>
28+
`,
29+
providers: [
30+
{
31+
provide: NG_VALUE_ACCESSOR,
32+
useExisting: forwardRef(() => ZardToggleComponent),
33+
multi: true,
34+
},
35+
],
36+
})
37+
export class ZardToggleComponent implements ControlValueAccessor {
38+
readonly zValue = input<boolean | undefined>();
39+
readonly zDefault = input<boolean>(false);
40+
readonly zDisabled = input(false, { alias: 'disabled', transform });
41+
readonly zType = input<ZardToggleVariants['zType']>('default');
42+
readonly zSize = input<ZardToggleVariants['zSize']>('md');
43+
readonly zAriaLabel = input<string>('', { alias: 'aria-label' });
44+
readonly class = input<ClassValue>('');
45+
46+
readonly onClick = output<void>();
47+
readonly onHover = output<void>();
48+
readonly onChange = output<boolean>();
49+
50+
private isUsingNgModel = signal(false);
51+
52+
protected readonly value = linkedSignal(() => this.zValue() || this.zDefault());
53+
54+
protected readonly disabled = linkedSignal(() => this.zDisabled());
55+
56+
protected readonly classes = computed(() => mergeClasses(toggleVariants({ zSize: this.zSize(), zType: this.zType() }), this.class()));
57+
58+
// eslint-disable-next-line @typescript-eslint/no-empty-function
59+
private onTouched: OnTouchedType = () => {};
60+
// eslint-disable-next-line @typescript-eslint/no-empty-function
61+
private onChangeFn: OnChangeType = () => {};
62+
63+
@HostListener('mouseenter')
64+
handleHover() {
65+
this.onHover.emit();
66+
}
67+
68+
toggle() {
69+
if (this.disabled()) return;
70+
71+
const next = !this.value();
72+
73+
if (this.zValue() === undefined) {
74+
this.value.set(next);
75+
}
76+
77+
this.onClick.emit();
78+
this.onChange.emit(next);
79+
this.onChangeFn(next);
80+
this.onTouched();
81+
}
82+
83+
writeValue(val: boolean): void {
84+
this.value.set(val ?? this.zDefault());
85+
}
86+
87+
registerOnChange(fn: any): void {
88+
this.onChangeFn = fn;
89+
this.isUsingNgModel.set(true);
90+
}
91+
92+
registerOnTouched(fn: any): void {
93+
this.onTouched = fn;
94+
}
95+
96+
setDisabledState(isDisabled: boolean): void {
97+
this.disabled.set(isDisabled);
98+
}
99+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { cva, VariantProps } from 'class-variance-authority';
2+
3+
export const toggleVariants = cva(
4+
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
5+
{
6+
variants: {
7+
zType: {
8+
default: 'bg-transparent',
9+
outline: 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
10+
},
11+
zSize: {
12+
sm: 'h-8 px-2',
13+
md: 'h-9 px-3',
14+
lg: 'h-10 px-3',
15+
},
16+
},
17+
defaultVariants: {
18+
zType: 'default',
19+
zSize: 'md',
20+
},
21+
},
22+
);
23+
export type ZardToggleVariants = VariantProps<typeof toggleVariants>;

src/styles.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import 'tailwindcss';
2+
@import 'lucide-static/font/lucide.css';
23
@plugin "tailwindcss-animate";
34
@custom-variant dark (&:where(.dark, .dark *));
45
@source './**/*.{html,scss,ts}';

0 commit comments

Comments
 (0)