@sonny-ui/core 0.1.0-alpha.14 → 0.1.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/fesm2022/sonny-ui-core.mjs +2257 -68
  2. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/src/lib/calendar/calendar.component.spec.ts +87 -0
  5. package/src/lib/calendar/calendar.component.ts +184 -61
  6. package/src/lib/calendar/calendar.types.ts +24 -0
  7. package/src/lib/calendar/index.ts +6 -0
  8. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  9. package/src/lib/color-picker/color-picker.component.ts +537 -0
  10. package/src/lib/color-picker/color-picker.types.ts +24 -0
  11. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  12. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  13. package/src/lib/color-picker/index.ts +20 -0
  14. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  15. package/src/lib/command-palette/command-palette.component.ts +195 -0
  16. package/src/lib/command-palette/command-palette.service.ts +36 -0
  17. package/src/lib/command-palette/command-palette.types.ts +23 -0
  18. package/src/lib/command-palette/index.ts +7 -0
  19. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  20. package/src/lib/date-picker/date-picker.component.ts +220 -0
  21. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  22. package/src/lib/date-picker/index.ts +2 -0
  23. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  24. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  25. package/src/lib/date-range-picker/index.ts +1 -0
  26. package/src/lib/otp-input/index.ts +2 -0
  27. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  28. package/src/lib/otp-input/otp-input.component.ts +275 -0
  29. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  30. package/types/sonny-ui-core.d.ts +331 -7
@@ -0,0 +1,275 @@
1
+ import {
2
+ afterNextRender,
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ effect,
7
+ ElementRef,
8
+ forwardRef,
9
+ input,
10
+ linkedSignal,
11
+ model,
12
+ output,
13
+ signal,
14
+ untracked,
15
+ viewChildren,
16
+ } from '@angular/core';
17
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
18
+ import { cn } from '../core/utils/cn';
19
+ import { otpCellVariants, type OtpInputSize, type OtpInputType } from './otp-input.variants';
20
+
21
+ @Component({
22
+ selector: 'sny-otp-input',
23
+ standalone: true,
24
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
+ providers: [
26
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyOtpInputComponent), multi: true },
27
+ ],
28
+ template: `
29
+ <div
30
+ role="group"
31
+ [attr.aria-label]="'OTP input, ' + length() + ' digits'"
32
+ class="flex items-center gap-2"
33
+ >
34
+ @for (digit of digits(); track $index; let i = $index) {
35
+ @if (separator() !== null && i === separator() && i > 0) {
36
+ <span class="text-muted-foreground text-lg select-none" aria-hidden="true">—</span>
37
+ }
38
+ <input
39
+ #inputEl
40
+ [type]="mask() ? 'password' : 'text'"
41
+ [inputMode]="type() === 'number' ? 'numeric' : 'text'"
42
+ [attr.pattern]="type() === 'number' ? '[0-9]' : '[a-zA-Z0-9]'"
43
+ maxlength="1"
44
+ autocomplete="one-time-code"
45
+ [value]="digit"
46
+ [placeholder]="placeholder()"
47
+ [disabled]="isDisabled()"
48
+ [class]="cellClass(i)"
49
+ [attr.aria-label]="'Digit ' + (i + 1) + ' of ' + length()"
50
+ (input)="onInput($event, i)"
51
+ (keydown)="onKeydown($event, i)"
52
+ (paste)="onPaste($event, i)"
53
+ (focus)="focusedIndex.set(i)"
54
+ (blur)="onBlur()"
55
+ />
56
+ }
57
+ </div>
58
+ `,
59
+ })
60
+ export class SnyOtpInputComponent implements ControlValueAccessor {
61
+ // Public API
62
+ readonly length = input(6);
63
+ readonly type = input<OtpInputType>('number');
64
+ readonly size = input<OtpInputSize>('md');
65
+ readonly disabled = input(false);
66
+ readonly mask = input(false);
67
+ readonly autoFocus = input(true);
68
+ readonly placeholder = input('');
69
+ readonly separator = input<number | null>(null);
70
+ readonly status = input<'idle' | 'loading' | 'success' | 'error'>('idle');
71
+ readonly value = model('');
72
+
73
+ readonly completed = output<string>();
74
+
75
+ // Internal state
76
+ readonly digits = linkedSignal<string[]>(() => Array(this.length()).fill(''));
77
+ readonly focusedIndex = signal(-1);
78
+ readonly inputRefs = viewChildren<ElementRef<HTMLInputElement>>('inputEl');
79
+
80
+ private readonly _disabledByCva = signal(false);
81
+ readonly isDisabled = computed(() => this.disabled() || this._disabledByCva() || this.status() === 'loading');
82
+
83
+ private _onChange: (value: string) => void = () => {};
84
+ private _onTouched: () => void = () => {};
85
+
86
+ // Computed
87
+ readonly fullValue = computed(() => this.digits().join(''));
88
+ readonly isComplete = computed(() => {
89
+ const d = this.digits();
90
+ return d.length === this.length() && d.every((c) => c !== '');
91
+ });
92
+
93
+ constructor() {
94
+ // Sync value → digits when value changes externally (e.g. reset)
95
+ effect(() => {
96
+ const val = this.value();
97
+ untracked(() => {
98
+ const chars = val.split('').slice(0, this.length());
99
+ const padded = [...chars, ...Array(this.length() - chars.length).fill('')];
100
+ const current = this.digits();
101
+ if (padded.join('') !== current.join('')) {
102
+ this.digits.set(padded);
103
+ }
104
+ });
105
+ });
106
+
107
+ afterNextRender(() => {
108
+ if (this.autoFocus()) {
109
+ this.focusInput(0);
110
+ }
111
+ });
112
+ }
113
+
114
+ // CVA
115
+ writeValue(val: string): void {
116
+ const str = val ?? '';
117
+ this.value.set(str);
118
+ const chars = str.split('').slice(0, this.length());
119
+ const padded = [...chars, ...Array(this.length() - chars.length).fill('')];
120
+ this.digits.set(padded);
121
+ }
122
+
123
+ registerOnChange(fn: (value: string) => void): void {
124
+ this._onChange = fn;
125
+ }
126
+
127
+ registerOnTouched(fn: () => void): void {
128
+ this._onTouched = fn;
129
+ }
130
+
131
+ setDisabledState(isDisabled: boolean): void {
132
+ this._disabledByCva.set(isDisabled);
133
+ }
134
+
135
+ // Cell class
136
+ cellClass(index: number): string {
137
+ const isFocused = this.focusedIndex() === index;
138
+ const hasValue = this.digits()[index] !== '';
139
+ const st = this.status();
140
+ return cn(
141
+ otpCellVariants({ size: this.size() }),
142
+ st === 'idle' && isFocused && 'border-primary ring-2 ring-ring',
143
+ st === 'idle' && hasValue && !isFocused && 'border-primary/50',
144
+ st === 'loading' && 'border-muted-foreground/30 opacity-70',
145
+ st === 'success' && 'border-green-500 bg-green-500/5',
146
+ st === 'error' && 'border-destructive bg-destructive/5 animate-shake',
147
+ );
148
+ }
149
+
150
+ // Input handler
151
+ onInput(event: Event, index: number): void {
152
+ const input = event.target as HTMLInputElement;
153
+ const char = input.value.slice(-1);
154
+
155
+ if (!this.isValidChar(char)) {
156
+ input.value = this.digits()[index];
157
+ return;
158
+ }
159
+
160
+ this.setDigit(index, char);
161
+
162
+ if (index < this.length() - 1) {
163
+ this.focusInput(index + 1);
164
+ }
165
+
166
+ this.emitValue();
167
+ }
168
+
169
+ // Keyboard handler
170
+ onKeydown(event: KeyboardEvent, index: number): void {
171
+ switch (event.key) {
172
+ case 'Backspace':
173
+ event.preventDefault();
174
+ if (this.digits()[index] !== '') {
175
+ this.setDigit(index, '');
176
+ this.emitValue();
177
+ } else if (index > 0) {
178
+ this.setDigit(index - 1, '');
179
+ this.focusInput(index - 1);
180
+ this.emitValue();
181
+ }
182
+ break;
183
+
184
+ case 'Delete':
185
+ event.preventDefault();
186
+ this.setDigit(index, '');
187
+ this.emitValue();
188
+ break;
189
+
190
+ case 'ArrowLeft':
191
+ event.preventDefault();
192
+ if (index > 0) this.focusInput(index - 1);
193
+ break;
194
+
195
+ case 'ArrowRight':
196
+ event.preventDefault();
197
+ if (index < this.length() - 1) this.focusInput(index + 1);
198
+ break;
199
+
200
+ case 'Home':
201
+ event.preventDefault();
202
+ this.focusInput(0);
203
+ break;
204
+
205
+ case 'End':
206
+ event.preventDefault();
207
+ this.focusInput(this.length() - 1);
208
+ break;
209
+ }
210
+ }
211
+
212
+ // Paste handler
213
+ onPaste(event: ClipboardEvent, index: number): void {
214
+ event.preventDefault();
215
+ const text = event.clipboardData?.getData('text') ?? '';
216
+ const chars = text.split('').filter((c) => this.isValidChar(c));
217
+
218
+ if (chars.length === 0) return;
219
+
220
+ const newDigits = [...this.digits()];
221
+ let lastFilledIndex = index;
222
+
223
+ for (let i = 0; i < chars.length && index + i < this.length(); i++) {
224
+ newDigits[index + i] = chars[i];
225
+ lastFilledIndex = index + i;
226
+ }
227
+
228
+ this.digits.set(newDigits);
229
+
230
+ const nextIndex = Math.min(lastFilledIndex + 1, this.length() - 1);
231
+ this.focusInput(nextIndex);
232
+ this.emitValue();
233
+ }
234
+
235
+ // Blur
236
+ onBlur(): void {
237
+ this.focusedIndex.set(-1);
238
+ this._onTouched();
239
+ }
240
+
241
+ // Helpers
242
+ private setDigit(index: number, char: string): void {
243
+ this.digits.update((d) => {
244
+ const next = [...d];
245
+ next[index] = char;
246
+ return next;
247
+ });
248
+ }
249
+
250
+ private emitValue(): void {
251
+ const val = this.fullValue();
252
+ this.value.set(val);
253
+ this._onChange(val);
254
+
255
+ if (this.isComplete()) {
256
+ this.completed.emit(val);
257
+ }
258
+ }
259
+
260
+ private focusInput(index: number): void {
261
+ const refs = this.inputRefs();
262
+ if (refs[index]) {
263
+ const el = refs[index].nativeElement;
264
+ el.focus();
265
+ el.select();
266
+ }
267
+ }
268
+
269
+ private isValidChar(char: string): boolean {
270
+ if (!char || char.length !== 1) return false;
271
+ if (this.type() === 'number') return /^[0-9]$/.test(char);
272
+ return /^[a-zA-Z0-9]$/.test(char);
273
+ }
274
+
275
+ }
@@ -0,0 +1,18 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const otpCellVariants = cva(
4
+ 'text-center font-mono font-semibold border border-border bg-background rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-9 w-9 text-sm',
9
+ md: 'h-11 w-11 text-lg',
10
+ lg: 'h-14 w-14 text-2xl',
11
+ },
12
+ },
13
+ defaultVariants: { size: 'md' },
14
+ }
15
+ );
16
+
17
+ export type OtpInputSize = 'sm' | 'md' | 'lg';
18
+ export type OtpInputType = 'number' | 'alphanumeric';