@brickclay-org/ui 0.0.39 → 0.0.40

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 (56) hide show
  1. package/ASSETS_SETUP.md +59 -0
  2. package/ng-package.json +29 -0
  3. package/package.json +15 -26
  4. package/src/lib/assets/icons.ts +8 -0
  5. package/src/lib/badge/badge.html +24 -0
  6. package/src/lib/badge/badge.ts +42 -0
  7. package/src/lib/brickclay-lib.spec.ts +23 -0
  8. package/src/lib/brickclay-lib.ts +15 -0
  9. package/src/lib/button-group/button-group.html +12 -0
  10. package/src/lib/button-group/button-group.ts +73 -0
  11. package/src/lib/calender/calendar.module.ts +35 -0
  12. package/src/lib/calender/components/custom-calendar/custom-calendar.component.css +698 -0
  13. package/src/lib/calender/components/custom-calendar/custom-calendar.component.html +230 -0
  14. package/src/lib/calender/components/custom-calendar/custom-calendar.component.spec.ts +23 -0
  15. package/src/lib/calender/components/custom-calendar/custom-calendar.component.ts +1554 -0
  16. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.css +373 -0
  17. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.html +210 -0
  18. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.ts +361 -0
  19. package/src/lib/calender/components/time-picker/time-picker.component.css +174 -0
  20. package/src/lib/calender/components/time-picker/time-picker.component.html +60 -0
  21. package/src/lib/calender/components/time-picker/time-picker.component.ts +283 -0
  22. package/src/lib/calender/services/calendar-manager.service.ts +45 -0
  23. package/src/lib/checkbox/checkbox.html +42 -0
  24. package/src/lib/checkbox/checkbox.ts +67 -0
  25. package/src/lib/chips/chips.html +74 -0
  26. package/src/lib/chips/chips.ts +222 -0
  27. package/src/lib/grid/components/grid/grid.html +97 -0
  28. package/src/lib/grid/components/grid/grid.ts +139 -0
  29. package/src/lib/grid/models/grid.model.ts +20 -0
  30. package/src/lib/input/input.html +127 -0
  31. package/src/lib/input/input.ts +394 -0
  32. package/src/lib/pill/pill.html +24 -0
  33. package/src/lib/pill/pill.ts +39 -0
  34. package/src/lib/radio/radio.html +58 -0
  35. package/src/lib/radio/radio.ts +72 -0
  36. package/src/lib/select/select.html +111 -0
  37. package/src/lib/select/select.ts +401 -0
  38. package/src/lib/spinner/spinner.html +5 -0
  39. package/src/lib/spinner/spinner.ts +22 -0
  40. package/src/lib/tabs/tabs.html +28 -0
  41. package/src/lib/tabs/tabs.ts +48 -0
  42. package/src/lib/textarea/textarea.html +80 -0
  43. package/src/lib/textarea/textarea.ts +172 -0
  44. package/src/lib/toggle/toggle.html +24 -0
  45. package/src/lib/toggle/toggle.ts +62 -0
  46. package/src/lib/ui-button/ui-button.html +25 -0
  47. package/src/lib/ui-button/ui-button.ts +55 -0
  48. package/src/lib/ui-icon-button/ui-icon-button.html +7 -0
  49. package/src/lib/ui-icon-button/ui-icon-button.ts +38 -0
  50. package/src/public-api.ts +43 -0
  51. package/tsconfig.lib.json +19 -0
  52. package/tsconfig.lib.prod.json +11 -0
  53. package/tsconfig.spec.json +15 -0
  54. package/fesm2022/brickclay-org-ui.mjs +0 -4035
  55. package/fesm2022/brickclay-org-ui.mjs.map +0 -1
  56. package/index.d.ts +0 -857
@@ -0,0 +1,283 @@
1
+ import { BrickclayIcons } from '../../../assets/icons';
2
+ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, QueryList, ViewChildren, ElementRef, HostListener, SimpleChanges } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+
5
+ @Component({
6
+ selector: 'bk-time-picker',
7
+ standalone: true,
8
+ imports: [CommonModule],
9
+ templateUrl: './time-picker.component.html',
10
+ styleUrls: ['./time-picker.component.css']
11
+ })
12
+ export class BkTimePicker implements OnInit, OnChanges, AfterViewInit {
13
+ @Input() value: string = '1:00 AM'; // Time in format "H:MM AM/PM"
14
+ @Input() label: string = 'Time';
15
+ @Input() placeholder: string = 'Select time';
16
+ @Input() position: 'left' | 'right' = 'left';
17
+ @Input() pickerId: string = ''; // Unique ID for this picker
18
+ @Input() closePicker: number = 0; // Close counter from parent (triggers close when changed)
19
+ @Input() timeFormat: 12 | 24 = 12; // Visual mode: 12h or 24h
20
+ @Input() showSeconds = false; // Whether to show/edit seconds
21
+ @Output() timeChange = new EventEmitter<string>();
22
+ @Output() pickerOpened = new EventEmitter<string>(); // Notify parent when opened
23
+ @Output() pickerClosed = new EventEmitter<string>(); // Notify parent when closed
24
+
25
+ @ViewChildren('timeScroll') timeScrollElements!: QueryList<ElementRef>;
26
+
27
+ showPicker = false;
28
+ currentHour = 1;
29
+ currentMinute = 0;
30
+ currentAMPM = 'AM';
31
+ currentSecond = 0;
32
+
33
+ brickclayIcons=BrickclayIcons;
34
+
35
+ ngOnInit() {
36
+ this.parseTimeValue();
37
+ }
38
+
39
+ ngAfterViewInit() {
40
+ if (this.showPicker) {
41
+ setTimeout(() => {
42
+ this.scrollToSelectedTimes();
43
+ }, 100);
44
+ }
45
+ }
46
+
47
+ parseTimeValue() {
48
+ const parsed = this.parseTimeStringToComponents(this.value);
49
+ this.currentHour = parsed.hour;
50
+ this.currentMinute = parsed.minute;
51
+ this.currentSecond = parsed.second;
52
+ this.currentAMPM = parsed.ampm;
53
+ }
54
+
55
+ getHours(): number[] {
56
+ // 12-hour: 1-12, 24-hour: 0-23
57
+ if (this.timeFormat === 24) {
58
+ return Array.from({ length: 24 }, (_, i) => i);
59
+ }
60
+ return Array.from({ length: 12 }, (_, i) => i + 1);
61
+ }
62
+
63
+ getMinutes(): number[] {
64
+ return Array.from({ length: 60 }, (_, i) => i);
65
+ }
66
+
67
+ getSeconds(): number[] {
68
+ return Array.from({ length: 60 }, (_, i) => i);
69
+ }
70
+
71
+ getAMPMOptions(): string[] {
72
+ return ['AM', 'PM'];
73
+ }
74
+
75
+ parseTimeStringToComponents(timeStr: string): { hour: number; minute: number; second: number; ampm: string } {
76
+ // Supports:
77
+ // - "H:MM AM/PM"
78
+ // - "H:MM:SS AM/PM"
79
+ // - "HH:MM" (24h)
80
+ // - "HH:MM:SS" (24h)
81
+ if (!timeStr) {
82
+ return {
83
+ hour: this.timeFormat === 24 ? 0 : 12,
84
+ minute: 0,
85
+ second: 0,
86
+ ampm: this.timeFormat === 24 ? '' : 'AM'
87
+ };
88
+ }
89
+
90
+ const parts = timeStr.trim().split(' ');
91
+ const timePart = parts[0] || (this.timeFormat === 24 ? '00:00' : '12:00');
92
+ let ampm = (parts[1] || '').toUpperCase();
93
+
94
+ const [hoursStr, minutesStr, secondsStr] = timePart.split(':');
95
+ let hour = parseInt(hoursStr || (this.timeFormat === 24 ? '0' : '12'), 10);
96
+ const minute = parseInt(minutesStr || '0', 10);
97
+ const second = parseInt(secondsStr || '0', 10);
98
+
99
+ if (this.timeFormat === 24) {
100
+ // In 24-hour mode we ignore AM/PM and keep hour as 0-23
101
+ return {
102
+ hour: isNaN(hour) ? 0 : Math.min(Math.max(hour, 0), 23),
103
+ minute: isNaN(minute) ? 0 : Math.min(Math.max(minute, 0), 59),
104
+ second: isNaN(second) ? 0 : Math.min(Math.max(second, 0), 59),
105
+ ampm: ''
106
+ };
107
+ }
108
+
109
+ // 12-hour mode: normalize AM/PM and convert 24h inputs if needed
110
+ let ampmValue = ampm === 'PM' || ampm === 'AM' ? ampm : '';
111
+ if (!ampmValue) {
112
+ // No AM/PM provided -> interpret as 24-hour and convert to 12-hour with AM/PM
113
+ if (hour >= 12) {
114
+ ampmValue = 'PM';
115
+ if (hour > 12) hour -= 12;
116
+ } else {
117
+ ampmValue = 'AM';
118
+ if (hour === 0) hour = 12;
119
+ }
120
+ }
121
+
122
+ // Clamp to 1-12 range
123
+ if (hour < 1) hour = 1;
124
+ if (hour > 12) hour = 12;
125
+
126
+ return {
127
+ hour,
128
+ minute: isNaN(minute) ? 0 : Math.min(Math.max(minute, 0), 59),
129
+ second: isNaN(second) ? 0 : Math.min(Math.max(second, 0), 59),
130
+ ampm: ampmValue
131
+ };
132
+ }
133
+
134
+ formatTimeFromComponents(hour: number, minute: number, second: number, ampm: string): string {
135
+ const hStr = hour.toString().padStart(2, '0');
136
+ const minuteStr = minute.toString().padStart(2, '0');
137
+ const secondStr = second.toString().padStart(2, '0');
138
+
139
+ if (this.timeFormat === 24) {
140
+ // "HH:mm" or "HH:mm:ss"
141
+ return this.showSeconds
142
+ ? `${hStr}:${minuteStr}:${secondStr}`
143
+ : `${hStr}:${minuteStr}`;
144
+ }
145
+
146
+ // 12-hour: "H:MM" or "H:MM:SS" with AM/PM
147
+ const displayHour = hour; // already 1-12
148
+ return this.showSeconds
149
+ ? `${displayHour}:${minuteStr}:${secondStr} ${ampm}`
150
+ : `${displayHour}:${minuteStr} ${ampm}`;
151
+ }
152
+
153
+ togglePicker() {
154
+ if (!this.showPicker) {
155
+ this.showPicker = true;
156
+ this.parseTimeValue();
157
+ this.pickerOpened.emit(this.pickerId);
158
+ setTimeout(() => {
159
+ this.scrollToSelectedTimes();
160
+ }, 100);
161
+ } else {
162
+ this.showPicker = false;
163
+ this.pickerClosed.emit(this.pickerId);
164
+ }
165
+ }
166
+
167
+ onHourChange(hour: number) {
168
+ this.currentHour = hour;
169
+ this.updateTime();
170
+ setTimeout(() => {
171
+ this.scrollToSelectedTimes();
172
+ }, 50);
173
+ }
174
+
175
+ onMinuteChange(minute: number) {
176
+ this.currentMinute = minute;
177
+ this.updateTime();
178
+ setTimeout(() => {
179
+ this.scrollToSelectedTimes();
180
+ }, 50);
181
+ }
182
+
183
+ onSecondChange(second: number) {
184
+ this.currentSecond = second;
185
+ this.updateTime();
186
+ setTimeout(() => {
187
+ this.scrollToSelectedTimes();
188
+ }, 50);
189
+ }
190
+
191
+ onAMPMChange(ampm: string) {
192
+ this.currentAMPM = ampm;
193
+ this.updateTime();
194
+ setTimeout(() => {
195
+ this.scrollToSelectedTimes();
196
+ }, 50);
197
+ }
198
+
199
+ updateTime() {
200
+ const newTime = this.formatTimeFromComponents(
201
+ this.currentHour,
202
+ this.currentMinute,
203
+ this.currentSecond,
204
+ this.currentAMPM
205
+ );
206
+ this.value = newTime;
207
+ this.timeChange.emit(newTime);
208
+ }
209
+
210
+ scrollToSelectedTimes() {
211
+ this.timeScrollElements.forEach((elementRef) => {
212
+ const element = elementRef.nativeElement;
213
+ const selectedItem = element.querySelector('.time-item.selected');
214
+ if (selectedItem) {
215
+ const scrollTop = selectedItem.offsetTop - element.offsetHeight / 40 + selectedItem.offsetHeight / 40;
216
+ element.scrollTop = scrollTop;
217
+ }
218
+ });
219
+ }
220
+
221
+ @HostListener('document:click', ['$event'])
222
+ onDocumentClick(event: MouseEvent) {
223
+ const target = event.target as HTMLElement;
224
+ if (!target.closest('.time-picker-wrapper') && this.showPicker) {
225
+ this.showPicker = false;
226
+ this.pickerClosed.emit(this.pickerId);
227
+ }
228
+ }
229
+
230
+ private previousCloseCounter: number = 0;
231
+
232
+ ngOnChanges(changes: SimpleChanges) {
233
+ if (changes['value'] && changes['value'].currentValue) {
234
+ this.parseTimeValue();
235
+ }
236
+ if (changes['closePicker'] && this.showPicker) {
237
+ const newCounter = changes['closePicker'].currentValue;
238
+ // If counter increased, close the picker
239
+ if (newCounter > this.previousCloseCounter) {
240
+ this.showPicker = false;
241
+ this.pickerClosed.emit(this.pickerId);
242
+ this.previousCloseCounter = newCounter;
243
+ }
244
+ }
245
+ }
246
+
247
+ // Basic keyboard support on the input (combobox behavior)
248
+ onInputKeydown(event: KeyboardEvent) {
249
+ const key = event.key;
250
+
251
+ if (key === 'Enter' || key === ' ') {
252
+ event.preventDefault();
253
+ this.togglePicker();
254
+ return;
255
+ }
256
+
257
+ if (key === 'Escape' && this.showPicker) {
258
+ this.showPicker = false;
259
+ this.pickerClosed.emit(this.pickerId);
260
+ return;
261
+ }
262
+
263
+ // Simple hour increment/decrement when closed
264
+ if (!this.showPicker && (key === 'ArrowUp' || key === 'ArrowDown')) {
265
+ event.preventDefault();
266
+ if (this.timeFormat === 24) {
267
+ if (key === 'ArrowUp') {
268
+ this.currentHour = (this.currentHour + 1) % 24;
269
+ } else {
270
+ this.currentHour = this.currentHour <= 0 ? 23 : this.currentHour - 1;
271
+ }
272
+ } else {
273
+ if (key === 'ArrowUp') {
274
+ this.currentHour = this.currentHour >= 12 ? 1 : this.currentHour + 1;
275
+ } else {
276
+ this.currentHour = this.currentHour <= 1 ? 12 : this.currentHour - 1;
277
+ }
278
+ }
279
+ this.updateTime();
280
+ }
281
+ }
282
+ }
283
+
@@ -0,0 +1,45 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+
4
+ @Injectable({
5
+ providedIn: 'root'
6
+ })
7
+ export class CalendarManagerService {
8
+ private calendarInstances: Set<() => void> = new Set();
9
+ private closeAllSubject = new Subject<void>();
10
+
11
+ closeAll$ = this.closeAllSubject.asObservable();
12
+
13
+ /**
14
+ * Register a calendar instance with its close function
15
+ */
16
+ register(closeFn: () => void): () => void {
17
+ this.calendarInstances.add(closeFn);
18
+
19
+ // Return unregister function
20
+ return () => {
21
+ this.calendarInstances.delete(closeFn);
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Close all calendars except the one being opened
27
+ */
28
+ closeAllExcept(exceptCloseFn: () => void): void {
29
+ this.calendarInstances.forEach(closeFn => {
30
+ if (closeFn !== exceptCloseFn) {
31
+ closeFn();
32
+ }
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Close all calendars
38
+ */
39
+ closeAll(): void {
40
+ this.closeAllSubject.next();
41
+ this.calendarInstances.forEach(closeFn => closeFn());
42
+ }
43
+ }
44
+
45
+
@@ -0,0 +1,42 @@
1
+ <div
2
+ class="inline-flex items-center gap-2 cursor-pointer group outline-none"
3
+ (click)="toggle()"
4
+ (keydown.enter)="toggle()"
5
+ (keydown.space)="$event.preventDefault(); toggle()"
6
+ tabindex="0"
7
+ [attr.aria-disabled]="disabled">
8
+ <div
9
+ class="relative flex items-center justify-center border-2 transition-all duration-200 ease-in-out rounded group-focus-visible:ring-2 group-focus-visible:ring-blue-600 group-focus-visible:ring-offset-2 checkbox"
10
+ [ngClass]="[
11
+ checkboxClass,
12
+ isChecked && !disabled ? 'bg-black border-black' : '',
13
+ !isChecked && !disabled ? 'bg-white border-gray-300 group-hover:border-gray-400' : '',
14
+ disabled && isChecked ? 'bg-gray-300 border-gray-300' : '',
15
+ disabled && !isChecked ? 'bg-gray-100 border-gray-200' : '',
16
+ disabled ? 'cursor-not-allowed' : ''
17
+ ]"
18
+ >
19
+ <svg
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ stroke-width="3.5"
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ class="text-white pointer-events-none transition-opacity duration-200"
28
+ [class.opacity-0]="!isChecked"
29
+ [class.opacity-100]="isChecked"
30
+ >
31
+ <polyline points="20 6 9 17 4 12"></polyline>
32
+ </svg>
33
+ </div>
34
+ @if(label){
35
+ <span
36
+ [ngClass]="disabled ? 'text-gray-400' : ''"
37
+ class="font-medium text-xs text-[#1B223A] select-none {{labelClass}}"
38
+ >
39
+ {{ label }}
40
+ </span>
41
+ }
42
+ </div>
@@ -0,0 +1,67 @@
1
+ import { Component, Input, Output, EventEmitter, forwardRef, ViewEncapsulation } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
4
+
5
+ @Component({
6
+ selector: 'bk-checkbox',
7
+ standalone: true,
8
+ imports: [CommonModule],
9
+ encapsulation: ViewEncapsulation.None,
10
+ templateUrl: './checkbox.html',
11
+ styleUrls: ['./checkbox.css'],
12
+ providers: [
13
+ {
14
+ provide: NG_VALUE_ACCESSOR,
15
+ useExisting: forwardRef(() => BkCheckbox),
16
+ multi: true
17
+ }
18
+ ]
19
+ })
20
+ export class BkCheckbox implements ControlValueAccessor {
21
+
22
+ @Input() checkboxClass: string = '';
23
+ @Input() label: string = '';
24
+ @Input() labelClass: string = '';
25
+ @Input() disabled: boolean = false;
26
+
27
+ @Output() change = new EventEmitter<boolean>();
28
+
29
+ // This is the value bound via ngModel
30
+ isChecked: boolean = false;
31
+
32
+ // ControlValueAccessor callbacks
33
+ private onChange = (_: any) => {};
34
+ private onTouched = () => {};
35
+
36
+ // Toggle function for click / keyboard
37
+ toggle() {
38
+ if (this.disabled) return;
39
+
40
+ this.isChecked = !this.isChecked;
41
+
42
+ // Update ngModel value
43
+ this.onChange(this.isChecked);
44
+ this.onTouched();
45
+
46
+ // Emit the change event
47
+ this.change.emit(this.isChecked);
48
+ }
49
+
50
+ /** ------------------ ControlValueAccessor methods ------------------ */
51
+
52
+ writeValue(value: boolean): void {
53
+ this.isChecked = value ?? false; // handle null/undefined safely
54
+ }
55
+
56
+ registerOnChange(fn: any): void {
57
+ this.onChange = fn;
58
+ }
59
+
60
+ registerOnTouched(fn: any): void {
61
+ this.onTouched = fn;
62
+ }
63
+
64
+ setDisabledState(isDisabled: boolean): void {
65
+ this.disabled = isDisabled;
66
+ }
67
+ }
@@ -0,0 +1,74 @@
1
+ <div class="input-badge-container">
2
+ @if (label) {
3
+ <label
4
+ [for]="id"
5
+ class="input-badge-label">
6
+ {{ label }}
7
+ @if (required) {
8
+ <span class="input-badge-label-required">*</span>
9
+ }
10
+ </label>
11
+ }
12
+
13
+ <div class="input-badge-wrapper">
14
+ <div
15
+ #fieldWrapper
16
+ class="input-badge-field-wrapper"
17
+ [ngClass]="{
18
+ 'input-badge-field-wrapper--default': inputState === 'default',
19
+ 'input-badge-field-wrapper--focused': inputState === 'focused',
20
+ 'input-badge-field-wrapper--filled': inputState === 'filled',
21
+ 'input-badge-field-wrapper--error': inputState === 'error',
22
+ 'input-badge-field-wrapper--disabled': inputState === 'disabled',
23
+ 'input-badge-field-wrapper--scrollable': needsScroll
24
+ }"
25
+ (click)="focusInput()"
26
+ >
27
+ <!-- Badges -->
28
+ @for (badge of badges; track badge; let i = $index) {
29
+ <div class="input-badge-item">
30
+ <span class="input-badge-item-text">{{ badge }}</span>
31
+ @if (!disabled) {
32
+ <button type="button" (click)="removeBadge(i); $event.stopPropagation()"class="input-badge-item-close" >
33
+ <img src="../../../../assets/images/icons/global/badge-close.svg" alt="Remove" class="input-badge-item-close-icon"/>
34
+ </button>
35
+ }
36
+ </div>
37
+ }
38
+
39
+ <!-- Input Field -->
40
+ <input
41
+ #badgeInput
42
+ type="text"
43
+ [id]="id"
44
+ [name]="name"
45
+ [value]="inputValue"
46
+ (keydown)="onKeyDown($event)"
47
+ (input)="handleInput($event)"
48
+ (focus)="handleFocus($event)"
49
+ (blur)="handleBlur($event)"
50
+ [placeholder]="badges.length === 0 ? placeholder : ''"
51
+ [disabled]="disabled"
52
+ [readOnly]="readOnly"
53
+ class="input-badge-input"
54
+ [ngClass]="{
55
+ 'input-badge-input--default': inputState === 'default',
56
+ 'input-badge-input--focused': inputState === 'focused',
57
+ 'input-badge-input--filled': inputState === 'filled',
58
+ 'input-badge-input--error': inputState === 'error',
59
+ 'input-badge-input--disabled': inputState === 'disabled'
60
+ }"
61
+ />
62
+ </div>
63
+ </div>
64
+
65
+ <div class="input-badge-footer">
66
+ <div class="input-badge-footer-content">
67
+ @if (hasError) {
68
+ <span class="input-badge-error"> {{ errorMessage }}</span>
69
+ } @else if (hint) {
70
+ <span class="input-badge-hint"> {{ hint }}</span>
71
+ }
72
+ </div>
73
+ </div>
74
+ </div>