@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,111 @@
1
+ <div class="ng-select-container">
2
+
3
+ <label
4
+ class="input-label"
5
+ (click)="openFromLabel($event)">
6
+ {{ label }}
7
+ @if(required){
8
+ <span class="input-label-required">*</span>
9
+ }
10
+ </label>
11
+
12
+ <div
13
+ #controlWrapper
14
+ class="ng-select-control"
15
+ [class.focused]="isOpen()"
16
+ [class.disabled]="disabled()"
17
+ (mousedown)="toggleDropdown($event)"
18
+ >
19
+ <!-- Icon (Always visible if set) -->
20
+ @if(iconSrc){
21
+ <img [src]="iconSrc" [alt]="iconAlt" class="shrink-0" />
22
+ }
23
+ <div class="ng-value-container">
24
+ @if (selectedOptions().length === 0)
25
+ {
26
+ <div class="ng-placeholder">{{ placeholder() }}</div>
27
+ }
28
+ @if
29
+ (multiple() && selectedOptions().length > 0) {
30
+ <div class="ng-value-chips">
31
+ @for (opt of selectedOptions().slice(0, maxLabels()); track $index) {
32
+ <div class="ng-value-chip">
33
+ <span class="ng-value-label">{{ resolveLabel(opt) }}</span>
34
+ <span class="ng-value-icon" (mousedown)="removeOption(opt, $event)">×</span>
35
+ </div>
36
+ }
37
+ @if (selectedOptions().length > maxLabels()) {
38
+ <div class="ng-value-chip remaining-count"><span class="ng-value-label">+{{ selectedOptions().length - maxLabels() }} more</span></div>
39
+ }
40
+ </div>
41
+ }
42
+ @if (!multiple() && selectedOptions().length > 0) {
43
+ <div class="ng-value-label-single">{{ resolveLabel(selectedOptions()[0]) }}</div>
44
+ }
45
+ </div>
46
+ <div class="ng-actions">
47
+ @if (clearable() && selectedOptions().length > 0 && !disabled()) {
48
+ <span class="ng-clear-wrapper" (mousedown)="handleClear($event)" title="Clear">
49
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
50
+ </span>
51
+ }
52
+ <span class="ng-arrow-wrapper" [class.open]="isOpen()">
53
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
54
+ </span>
55
+ </div>
56
+ </div>
57
+
58
+ @if (isOpen()) {
59
+ <div
60
+ #dropdownPanel
61
+ class="custom-ng-dropdown-panel"
62
+ [attr.data-position]="dropdownPosition()"
63
+ (scroll)="onScroll($event)"
64
+
65
+ [style.position]="appendToBody() ? 'fixed' : 'absolute'"
66
+ [style.top]="getTop()"
67
+ [style.bottom]="getBottom()"
68
+ [style.left]="appendToBody() ? dropdownStyle().left : null"
69
+ [style.width]="appendToBody() ? dropdownStyle().width : '100%'"
70
+ >
71
+
72
+
73
+ @if (searchable()) {
74
+ <div class="ng-dropdown-search">
75
+ <div class="ng-search-wrapper">
76
+ <svg class="text-[#BBBDC5] mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
77
+ <input #searchInput type="text" class="ng-search-input" [value]="searchTerm()" [placeholder]="'Search...'" (input)="onSearchInput($event)" (keydown)="onKeyDown($event)" (click)="$event.stopPropagation()">
78
+ </div>
79
+ </div>
80
+ }
81
+ @if (multiple() && filteredItems().length > 0) {
82
+ <div class="ng-option select-all-option" (mousedown)="toggleSelectAll($event)">
83
+ <div class="mr-2 flex items-center justify-center w-4 h-4 border border-gray-300 rounded bg-white" [class.bg-blue-600]="isAllSelected()" [class.border-blue-600]="isAllSelected()">
84
+ @if(isAllSelected()){ <svg class="text-white w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg> }
85
+ </div>
86
+ <span class="font-semibold">Select All</span>
87
+ </div>
88
+ }
89
+ <div #optionsListContainer class="ng-options-list">
90
+ @if (loading()) { <div class="ng-option-disabled">{{ loadingText() }}</div> }
91
+ @else {
92
+ @for (item of filteredItems(); track $index) {
93
+ <div #optionsRef class="ng-option" [class.selected]="isItemSelected(item)" [class.marked]="$index === markedIndex()" (click)="handleSelection(item, $event)" (click)="markedIndex.set($index)">
94
+ @if (multiple()) {
95
+ <div class="mr-2 flex items-center justify-center w-4 h-4 border border-gray-300 rounded bg-white" [class.bg-blue-600]="isItemSelected(item)" [class.border-blue-600]="isItemSelected(item)">
96
+ @if(isItemSelected(item)){ <svg class="text-white w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg> }
97
+ </div>
98
+ }
99
+ <span class="flex-1">{{ resolveLabel(item) }}</span>
100
+ @if (!multiple() && isItemSelected(item)) {
101
+ <svg class="text-[#141414]" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
102
+ }
103
+ </div>
104
+ }
105
+ @if (filteredItems().length === 0) { <div class="ng-option-disabled">{{ notFoundText() }}</div> }
106
+ }
107
+ </div>
108
+
109
+ </div>
110
+ }
111
+ </div>
@@ -0,0 +1,401 @@
1
+ import {
2
+ Component,
3
+ input,
4
+ output,
5
+ signal,
6
+ model,
7
+ forwardRef,
8
+ ElementRef,
9
+ HostListener,
10
+ inject,
11
+ computed,
12
+ effect,
13
+ ViewChild,
14
+ ViewChildren,
15
+ QueryList, Input
16
+ } from '@angular/core';
17
+ import { CommonModule } from '@angular/common';
18
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
19
+
20
+ @Component({
21
+ selector: 'bk-select',
22
+ standalone: true,
23
+ imports: [CommonModule, FormsModule],
24
+ templateUrl: './select.html',
25
+ styleUrls: ['./select.css'],
26
+ providers: [
27
+ {
28
+ provide: NG_VALUE_ACCESSOR,
29
+ useExisting: forwardRef(() => BkSelect),
30
+ multi: true
31
+ }
32
+ ]
33
+ })
34
+ export class BkSelect implements ControlValueAccessor {
35
+ // --- Inputs ---
36
+ items = input<any[]>([]);
37
+ bindLabel = input<string>('label');
38
+ bindValue = input<string>('');
39
+ placeholder = input<string>('');
40
+ notFoundText = input<string>('No items found');
41
+ loadingText = input<string>('Loading...');
42
+ clearAllText = input<string>('Clear all');
43
+ // iconSrc = input<string>('Clear all');
44
+ @Input() iconAlt: string = 'icon';
45
+ @Input() label: string = 'Label';
46
+ @Input() required: Boolean = false;
47
+
48
+
49
+ @Input() iconSrc?: string; // optional icon
50
+
51
+ // Config
52
+ multiple = input<boolean>(false);
53
+ maxLabels = input<number>(2);
54
+ searchable = input<boolean>(true);
55
+ clearable = input<boolean>(true);
56
+ readonly = input<boolean>(false);
57
+ disabled = model<boolean>(false);
58
+ loading = input<boolean>(false);
59
+ closeOnSelect = input<boolean>(true);
60
+ dropdownPosition = input<'bottom' | 'top'>('bottom');
61
+
62
+
63
+ // 1. NEW INPUT: Toggle append-to-body behavior
64
+ appendToBody = input<boolean>(false);
65
+
66
+ // --- Outputs ---
67
+ open = output<void>();
68
+ close = output<void>();
69
+ focus = output<void>();
70
+ blur = output<void>();
71
+ search = output<{ term: string, items: any[] }>();
72
+ clear = output<void>();
73
+ change = output<any>();
74
+ scrollToEnd = output<void>();
75
+
76
+ // --- Refs ---
77
+ @ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
78
+ @ViewChild('optionsListContainer') optionsListContainer!: ElementRef<HTMLDivElement>;
79
+ @ViewChildren('optionsRef') optionsRef!: QueryList<ElementRef>;
80
+ @ViewChild('controlWrapper') controlWrapper!: ElementRef<HTMLDivElement>;
81
+
82
+ // --- State ---
83
+ private _value: any = null;
84
+
85
+ isOpen = signal(false);
86
+ selectedOptions = signal<any[]>([]);
87
+ searchTerm = signal<string>('');
88
+ markedIndex = signal(-1);
89
+
90
+ dropdownStyle = signal<{
91
+ top?: string;
92
+ bottom?: string;
93
+ left: string;
94
+ width: string;
95
+ }>({
96
+ left: '0px',
97
+ width: 'auto'
98
+ });
99
+
100
+
101
+ filteredItems = computed(() => {
102
+ const term = this.searchTerm().toLowerCase();
103
+ const list = this.items();
104
+ if (!term || !this.searchable()) return list;
105
+ return list.filter(item => {
106
+ const label = this.resolveLabel(item).toLowerCase();
107
+ return label.includes(term);
108
+ });
109
+ });
110
+
111
+ isAllSelected = computed(() => {
112
+ const filtered = this.filteredItems();
113
+ const current = this.selectedOptions();
114
+ if (!filtered.length) return false;
115
+ return filtered.every(item => this.isItemSelected(item));
116
+ });
117
+
118
+ constructor() {
119
+ effect(() => {
120
+ const currentItems = this.items();
121
+ if (currentItems.length > 0 && this._value !== null) {
122
+ this.resolveSelectedOptions(this._value);
123
+ }
124
+ });
125
+ }
126
+
127
+ // --- Helpers ---
128
+ resolveLabel(item: any): string {
129
+ if (!item) return '';
130
+ const labelProp = this.bindLabel();
131
+ return labelProp && typeof item === 'object' ? item[labelProp] : String(item);
132
+ }
133
+
134
+ resolveValue(item: any): any {
135
+ const valueProp = this.bindValue();
136
+ return valueProp && typeof item === 'object' ? item[valueProp] : item;
137
+ }
138
+
139
+ isItemSelected(item: any): boolean {
140
+ const current = this.selectedOptions();
141
+ const compareFn = this.compareWith();
142
+ const itemVal = this.resolveValue(item);
143
+ return current.some(selected => compareFn(itemVal, this.resolveValue(selected)));
144
+ }
145
+
146
+ compareWith = input<(a: any, b: any) => boolean>((a, b) => a === b);
147
+
148
+ // --- Actions ---
149
+
150
+ toggleDropdown(event: Event | null) {
151
+ if (event) { event.preventDefault(); event.stopPropagation(); }
152
+ if (this.disabled() || this.readonly()) return;
153
+ this.isOpen() ? this.closeDropdown() : this.openDropdown();
154
+ }
155
+
156
+ openDropdown() {
157
+ if (this.isOpen()) return;
158
+
159
+ // Only calculate position if we are appending to body
160
+ if (this.appendToBody()) {
161
+ this.updatePosition();
162
+ }
163
+
164
+ this.isOpen.set(true);
165
+ // this.markedIndex.set(0);
166
+ this.open.emit();
167
+ this.focus.emit();
168
+ setTimeout(() => this.searchInput?.nativeElement.focus());
169
+ }
170
+
171
+ closeDropdown() {
172
+ if (!this.isOpen()) return;
173
+ this.isOpen.set(false);
174
+ this.searchTerm.set('');
175
+ this.onTouched();
176
+ this.close.emit();
177
+ this.blur.emit();
178
+ }
179
+
180
+ getTop(): string | null {
181
+ if (this.appendToBody()) {
182
+ return this.dropdownStyle().top ?? null;
183
+ }
184
+
185
+ // NOT appendToBody
186
+ return this.dropdownPosition() === 'bottom' ? '105%' : null;
187
+ }
188
+
189
+ getBottom(): string | null {
190
+ if (this.appendToBody()) {
191
+ return this.dropdownStyle().bottom ?? null;
192
+ }
193
+
194
+ // NOT appendToBody
195
+ return this.dropdownPosition() === 'top' ? '105%' : null;
196
+ }
197
+
198
+
199
+ updatePosition() {
200
+ const rect = this.controlWrapper.nativeElement.getBoundingClientRect();
201
+
202
+ if (this.dropdownPosition() === 'bottom') {
203
+ this.dropdownStyle.set({
204
+ top: `${rect.bottom + 4}px`,
205
+ bottom: undefined,
206
+ left: `${rect.left}px`,
207
+ width: `${rect.width}px`
208
+ });
209
+ } else {
210
+ this.dropdownStyle.set({
211
+ top: undefined,
212
+ bottom: `${window.innerHeight - rect.top + 4}px`,
213
+ left: `${rect.left}px`,
214
+ width: `${rect.width}px`
215
+ });
216
+ }
217
+ }
218
+
219
+
220
+
221
+ // 2. FIXED: Removed ['$event'] argument
222
+ @HostListener('window:scroll')
223
+ @HostListener('window:resize')
224
+ onWindowEvents() {
225
+ // Only close on scroll if we are in "fixed" mode (append to body)
226
+ if (this.isOpen() && this.appendToBody()) {
227
+ this.closeDropdown();
228
+ }
229
+ }
230
+
231
+ // ... (toggleSelectAll, handleSelection, removeOption, handleClear logic same as before) ...
232
+
233
+ toggleSelectAll(event: Event) {
234
+ event.stopPropagation();
235
+ event.preventDefault();
236
+ const allSelected = this.isAllSelected();
237
+ const filtered = this.filteredItems();
238
+ let newSelection = [...this.selectedOptions()];
239
+ const compareFn = this.compareWith();
240
+ if (allSelected) {
241
+ newSelection = newSelection.filter(sel => {
242
+ const selVal = this.resolveValue(sel);
243
+ return !filtered.some(fItem => compareFn(this.resolveValue(fItem), selVal));
244
+ });
245
+ } else {
246
+ filtered.forEach(item => {
247
+ if (!this.isItemSelected(item)) newSelection.push(item);
248
+ });
249
+ }
250
+ this.updateModel(newSelection);
251
+ }
252
+
253
+ handleSelection(item: any, event?: Event) {
254
+ if (event) event.stopPropagation();
255
+ if (this.multiple()) {
256
+ const isSelected = this.isItemSelected(item);
257
+ let newSelection = [...this.selectedOptions()];
258
+ const compareFn = this.compareWith();
259
+ if (isSelected) {
260
+ const itemVal = this.resolveValue(item);
261
+ newSelection = newSelection.filter(sel => !compareFn(this.resolveValue(sel), itemVal));
262
+ } else {
263
+ newSelection.push(item);
264
+ }
265
+ this.updateModel(newSelection);
266
+ if (this.closeOnSelect()) this.closeDropdown();
267
+ } else {
268
+ this.updateModel([item]);
269
+ if (this.closeOnSelect()) this.closeDropdown();
270
+ }
271
+ }
272
+
273
+ removeOption(item: any, event: Event) {
274
+ event.stopPropagation();
275
+ const newSelection = this.selectedOptions().filter(i => i !== item);
276
+ this.updateModel(newSelection);
277
+ }
278
+
279
+ handleClear(event: Event) {
280
+ event.stopPropagation();
281
+ this.updateModel([]);
282
+ this.clear.emit();
283
+ }
284
+
285
+ private updateModel(items: any[]) {
286
+ this.selectedOptions.set(items);
287
+ if (this.multiple()) {
288
+ const values = items.map(i => this.resolveValue(i));
289
+ this._value = values;
290
+ this.onChange(values);
291
+ this.change.emit(values);
292
+ } else {
293
+ const item = items[0] || null;
294
+ const value = item ? this.resolveValue(item) : null;
295
+ this._value = value;
296
+ this.onChange(value);
297
+ this.change.emit(value);
298
+ }
299
+ }
300
+
301
+ onSearchInput(event: Event) {
302
+ const val = (event.target as HTMLInputElement).value;
303
+ this.searchTerm.set(val);
304
+ this.markedIndex.set(0);
305
+ this.search.emit({ term: val, items: this.filteredItems() });
306
+ }
307
+
308
+ onKeyDown(event: KeyboardEvent) {
309
+ if (!this.isOpen()) return;
310
+ const list = this.filteredItems();
311
+ const current = this.markedIndex();
312
+ switch (event.key) {
313
+ case 'ArrowDown':
314
+ event.preventDefault();
315
+ if (current < list.length - 1) {
316
+ this.markedIndex.set(current + 1);
317
+ this.scrollToMarked();
318
+ }
319
+ break;
320
+ case 'ArrowUp':
321
+ event.preventDefault();
322
+ if (current > 0) {
323
+ this.markedIndex.set(current - 1);
324
+ this.scrollToMarked();
325
+ }
326
+ break;
327
+ case 'Enter':
328
+ event.preventDefault();
329
+ if (current >= 0 && list[current]) this.handleSelection(list[current]);
330
+ break;
331
+ case 'Escape':
332
+ event.preventDefault();
333
+ this.closeDropdown();
334
+ break;
335
+ }
336
+ }
337
+
338
+ onScroll(event: Event) {
339
+ const target = event.target as HTMLElement;
340
+ if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
341
+ this.scrollToEnd.emit();
342
+ }
343
+ }
344
+
345
+ scrollToMarked() {
346
+ setTimeout(() => {
347
+ const container = this.optionsListContainer?.nativeElement;
348
+ const options = this.optionsRef?.toArray();
349
+ const index = this.markedIndex();
350
+ if (container && options && options[index]) {
351
+ const el = options[index].nativeElement;
352
+ if (el.offsetTop < container.scrollTop) {
353
+ container.scrollTop = el.offsetTop;
354
+ } else if ((el.offsetTop + el.clientHeight) > (container.scrollTop + container.clientHeight)) {
355
+ container.scrollTop = (el.offsetTop + el.clientHeight) - container.clientHeight;
356
+ }
357
+ }
358
+ });
359
+ }
360
+
361
+ onChange: any = () => {};
362
+ onTouched: any = () => {};
363
+ writeValue(value: any): void { this._value = value; this.resolveSelectedOptions(value); }
364
+ registerOnChange(fn: any) { this.onChange = fn; }
365
+ registerOnTouched(fn: any) { this.onTouched = fn; }
366
+ setDisabledState(d: boolean) { this.disabled.set(d); }
367
+
368
+ private resolveSelectedOptions(val: any) {
369
+ const list = this.items();
370
+ if (!list.length) return;
371
+ if (val === null || val === undefined) {
372
+ this.selectedOptions.set([]);
373
+ return;
374
+ }
375
+ const valArray = Array.isArray(val) ? val : [val];
376
+ const bindVal = this.bindValue();
377
+ const compare = this.compareWith();
378
+ const matchedItems = list.filter(item => {
379
+ const itemVal = bindVal ? item[bindVal] : item;
380
+ return valArray.some(v => compare(v, itemVal));
381
+ });
382
+ this.selectedOptions.set(matchedItems);
383
+ }
384
+
385
+ private el = inject(ElementRef);
386
+ @HostListener('document:click', ['$event'])
387
+ onClickOutside(e: Event) {
388
+ if (!this.el.nativeElement.contains(e.target)) this.closeDropdown();
389
+ }
390
+
391
+ openFromLabel(event: MouseEvent) {
392
+ event.preventDefault();
393
+ event.stopPropagation();
394
+
395
+ if (this.disabled() || this.readonly()) return;
396
+
397
+ this.controlWrapper.nativeElement.focus();
398
+ this.openDropdown();
399
+ }
400
+
401
+ }
@@ -0,0 +1,5 @@
1
+ @if (show) {
2
+ <span [class]="classes" role="status" aria-label="loading">
3
+ <span class="sr-only">Loading...</span>
4
+ </span>
5
+ }
@@ -0,0 +1,22 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ // Mapped names to your 5 sizes
5
+ export type SpinnerSize = 'xsm' | 'sm' | 'md' | 'lg' | 'xl';
6
+
7
+ @Component({
8
+ selector: 'bk-spinner',
9
+ standalone: true,
10
+ imports: [CommonModule],
11
+ templateUrl: './spinner.html',
12
+ styleUrl: './spinner.css',
13
+ })
14
+ export class BkSpinner {
15
+ @Input() size: SpinnerSize = 'md';
16
+ @Input() show = true;
17
+ @Input() color = 'text-blue-600'; // default
18
+
19
+ get classes(): string {
20
+ return `spinner ${this.size} ${this.color}`;
21
+ }
22
+ }
@@ -0,0 +1,28 @@
1
+ <div class="tabs-container">
2
+ <ul class="tabs-list" role="tablist">
3
+ @for (tab of list; track tab.id; let i = $index) {
4
+ <li class="tabs-item" role="presentation">
5
+ <button
6
+ type="button"
7
+ [id]="'tab-' + tab.id"
8
+ [attr.aria-selected]="isActive(tab.id)"
9
+ [attr.aria-controls]="'panel-' + tab.id"
10
+ [disabled]="tab.disabled"
11
+ [class.tabs-button--active]="isActive(tab.id)"
12
+ [class.tabs-button--disabled]="disabled || tab.disabled"
13
+ [class.tabs-button--no-icon]="!getTabIcon(tab)"
14
+ class="tabs-button"
15
+ (click)="setActiveTab(tab)"
16
+ role="tab">
17
+ @if (getTabIcon(tab)) {
18
+ <img
19
+ [src]="getTabIcon(tab)"
20
+ [alt]="tab.iconAlt || tab.label"
21
+ class="tabs-icon">
22
+ }
23
+ <span class="tabs-label">{{ tab.label }}</span>
24
+ </button>
25
+ </li>
26
+ }
27
+ </ul>
28
+ </div>
@@ -0,0 +1,48 @@
1
+ import { Component, Input, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ export interface TabItem {
5
+ id: string;
6
+ label: string;
7
+ icon?: string; // Icon for inactive state
8
+ iconActive?: string; // Icon for active state (optional, falls back to icon if not provided)
9
+ iconAlt?: string;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ @Component({
14
+ selector: 'bk-tabs',
15
+ standalone: true,
16
+ imports: [CommonModule],
17
+ templateUrl: './tabs.html',
18
+ styleUrl: './tabs.css',
19
+ })
20
+ export class BkTabs {
21
+ @Input() list: TabItem[] = [];
22
+ @Input() activeTabId: string = '';
23
+ @Input() disabled: boolean = false;
24
+
25
+ @Output() change = new EventEmitter<TabItem>();
26
+
27
+ // Set active tab and emit change event
28
+ setActiveTab(tab:TabItem): void {
29
+ debugger
30
+ if (tab?.disabled || this.disabled) return;
31
+ this.activeTabId = tab.id;
32
+ this.change.emit(tab);
33
+
34
+ }
35
+
36
+ // Check if a tab is active
37
+ isActive(tabId: string): boolean {
38
+ return this.activeTabId === tabId;
39
+ }
40
+
41
+ // Get the appropriate icon for a tab based on its active state
42
+ getTabIcon(tab: TabItem): string | undefined {
43
+ if (this.isActive(tab.id) && tab.iconActive) {
44
+ return tab.iconActive;
45
+ }
46
+ return tab.icon;
47
+ }
48
+ }
@@ -0,0 +1,80 @@
1
+ <div class="flex flex-col gap-1.5 w-full">
2
+
3
+ @if (label) {
4
+ <label
5
+ class="text-sm font-medium text-[#141414] block" [for]="id">
6
+ {{ label }}
7
+ @if (required) {
8
+ <span class="text-[#E7000B] ml-0.5">*</span>
9
+ }
10
+ </label>
11
+ }
12
+
13
+ <div class="relative">
14
+ <textarea
15
+ [id]="id"
16
+ [name]="name"
17
+ [disabled]="disabled"
18
+ [tabindex]="tabIndex"
19
+ [readOnly]="readOnly"
20
+ [attr.maxlength]="maxlength"
21
+ [attr.minlength]="minlength"
22
+ [autocomplete]="autoComplete"
23
+ [autocapitalize]="autoCapitalize"
24
+ [inputMode]="inputMode"
25
+ [value]="value"
26
+ (input)="handleInput($event)"
27
+ (change)="handleChange($event)"
28
+ (blur)="handleBlur($event)"
29
+ (focus)="handleFocus($event)"
30
+ [placeholder]="placeholder"
31
+
32
+ [autocomplete]="autoComplete"
33
+
34
+ rows="{{rows}}"
35
+ class="
36
+ w-full
37
+ px-3 py-2.5
38
+ text-sm
39
+ border border-[#E3E3E7] rounded-[4px]
40
+ outline-none
41
+ transition-colors duration-200
42
+ bg-white resize-y
43
+ placeholder:text-[#6B7080]
44
+ "
45
+ [ngClass]="{
46
+ 'border-[#FA727A] text-[#141414]': hasError && !disabled,
47
+
48
+ 'focus:border-[#6B7080] text-[#141414]': !hasError && !disabled,
49
+
50
+ 'bg-[#F4F4F6] text-[#A1A3AE] border-[#E3E3E7] cursor-not-allowed': disabled
51
+ }"
52
+ ></textarea>
53
+ </div>
54
+
55
+ <div class="flex justify-between items-start font-normal text-sm">
56
+
57
+ <div class="flex-1">
58
+ @if (hasError) {
59
+ <span class="text-[#F34050]">
60
+ {{ errorMessage }}
61
+ </span>
62
+ } @else if (hint) {
63
+ <span class="text-[#868997]">
64
+ {{ hint }}
65
+ </span>
66
+ }
67
+ </div>
68
+
69
+ @if (maxlength) {
70
+ <div
71
+ class="text-[#868997] tabular-nums flex-shrink-0"
72
+ >
73
+ {{ value.length }}/{{ maxlength }}
74
+ </div>
75
+ }
76
+
77
+
78
+ </div>
79
+
80
+ </div>