@haloduck/ui 2.0.43 → 2.0.45

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.
@@ -2333,6 +2333,167 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2333
2333
  args: ['canvas', { static: true }]
2334
2334
  }] } });
2335
2335
 
2336
+ class TagInputComponent {
2337
+ label;
2338
+ inputEl;
2339
+ placeholder = '';
2340
+ disabled = false;
2341
+ allowDuplicates = false;
2342
+ // Two-way binding support independent of CVA
2343
+ set value(tags) {
2344
+ if (Array.isArray(tags)) {
2345
+ this.tags = tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
2346
+ }
2347
+ else {
2348
+ this.tags = [];
2349
+ }
2350
+ }
2351
+ valueChange = new EventEmitter();
2352
+ tags = [];
2353
+ inputValue = '';
2354
+ onChange = () => { };
2355
+ onTouched = () => { };
2356
+ writeValue(value) {
2357
+ if (Array.isArray(value)) {
2358
+ this.tags = value.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
2359
+ }
2360
+ else {
2361
+ this.tags = [];
2362
+ }
2363
+ }
2364
+ registerOnChange(fn) {
2365
+ this.onChange = fn;
2366
+ }
2367
+ registerOnTouched(fn) {
2368
+ this.onTouched = fn;
2369
+ }
2370
+ setDisabledState(isDisabled) {
2371
+ this.disabled = isDisabled;
2372
+ }
2373
+ ngAfterViewInit() {
2374
+ // hide label if no projected content
2375
+ if (this.label && this.label.nativeElement) {
2376
+ const hasContent = this.label.nativeElement.textContent?.trim();
2377
+ if (!hasContent) {
2378
+ this.label.nativeElement.style.display = 'none';
2379
+ }
2380
+ }
2381
+ }
2382
+ focus() {
2383
+ this.inputEl?.nativeElement?.focus();
2384
+ }
2385
+ onInput(event) {
2386
+ const input = event.target;
2387
+ this.inputValue = input.value;
2388
+ this.onTouched();
2389
+ }
2390
+ onBlur() {
2391
+ if (this.disabled)
2392
+ return;
2393
+ this.inputValue = '';
2394
+ this.onTouched();
2395
+ }
2396
+ onKeydown(event) {
2397
+ if (this.disabled)
2398
+ return;
2399
+ // Commit on comma or Enter
2400
+ if (event.key === ',' || event.key === 'Enter') {
2401
+ event.preventDefault();
2402
+ this.commitCurrentInput();
2403
+ return;
2404
+ }
2405
+ // Backspace behavior
2406
+ if (event.key === 'Backspace') {
2407
+ if (this.inputValue.length > 0) {
2408
+ return; // default backspace in input
2409
+ }
2410
+ if (this.tags.length > 0) {
2411
+ event.preventDefault();
2412
+ this.removeTag(this.tags.length - 1);
2413
+ return;
2414
+ }
2415
+ }
2416
+ }
2417
+ removeTag(index) {
2418
+ if (this.disabled)
2419
+ return;
2420
+ if (index < 0 || index >= this.tags.length)
2421
+ return;
2422
+ this.tags = this.tags.filter((_, i) => i !== index);
2423
+ this.emitChanges();
2424
+ }
2425
+ commitCurrentInput() {
2426
+ const raw = this.inputValue.trim();
2427
+ if (!raw) {
2428
+ this.inputValue = '';
2429
+ return;
2430
+ }
2431
+ // If user pasted multiple comma-separated values, split and add all
2432
+ const parts = raw
2433
+ .split(',')
2434
+ .map((p) => p.trim())
2435
+ .filter((p) => p.length > 0);
2436
+ for (const part of parts) {
2437
+ this.addTag(part);
2438
+ }
2439
+ this.inputValue = '';
2440
+ }
2441
+ addTag(tag) {
2442
+ if (!this.allowDuplicates) {
2443
+ const exists = this.tags.some((t) => t.toLowerCase() === tag.toLowerCase());
2444
+ if (exists) {
2445
+ return;
2446
+ }
2447
+ }
2448
+ this.tags = [...this.tags, tag];
2449
+ this.emitChanges();
2450
+ }
2451
+ emitChanges() {
2452
+ const cleaned = this.tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
2453
+ if (cleaned.length !== this.tags.length || cleaned.some((t, i) => t !== this.tags[i])) {
2454
+ this.tags = cleaned;
2455
+ }
2456
+ this.onChange(this.tags);
2457
+ this.valueChange.emit(this.tags);
2458
+ }
2459
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2460
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: TagInputComponent, isStandalone: true, selector: "haloduck-tag-input", inputs: { placeholder: "placeholder", disabled: "disabled", allowDuplicates: "allowDuplicates", value: "value" }, outputs: { valueChange: "valueChange" }, providers: [
2461
+ {
2462
+ provide: NG_VALUE_ACCESSOR,
2463
+ useExisting: forwardRef(() => TagInputComponent),
2464
+ multi: true,
2465
+ },
2466
+ provideTranslocoScope('haloduck'),
2467
+ ], viewQueries: [{ propertyName: "label", first: true, predicate: ["label"], descendants: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
2468
+ }
2469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, decorators: [{
2470
+ type: Component,
2471
+ args: [{ selector: 'haloduck-tag-input', imports: [CommonModule], providers: [
2472
+ {
2473
+ provide: NG_VALUE_ACCESSOR,
2474
+ useExisting: forwardRef(() => TagInputComponent),
2475
+ multi: true,
2476
+ },
2477
+ provideTranslocoScope('haloduck'),
2478
+ ], template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"] }]
2479
+ }], propDecorators: { label: [{
2480
+ type: ViewChild,
2481
+ args: ['label']
2482
+ }], inputEl: [{
2483
+ type: ViewChild,
2484
+ args: ['inputEl']
2485
+ }], placeholder: [{
2486
+ type: Input
2487
+ }], disabled: [{
2488
+ type: Input
2489
+ }], allowDuplicates: [{
2490
+ type: Input
2491
+ }], value: [{
2492
+ type: Input
2493
+ }], valueChange: [{
2494
+ type: Output
2495
+ }] } });
2496
+
2336
2497
  const ERROR_NOT_ACCEPTABLE_FILE_TYPE = 'NOT_ACCEPTABLE_FILE_TYPE';
2337
2498
  const ERROR_OVER_SIZE = 'OVER_SIZE';
2338
2499
  const ERROR_OVER_COUNT = 'OVER_COUNT';
@@ -2382,6 +2543,12 @@ class FileUploaderComponent {
2382
2543
  this.fileRemoved.emit(removedFile);
2383
2544
  this.onChange(this.files);
2384
2545
  }
2546
+ onFileTagChanged(index, tags) {
2547
+ if (this.files[index]) {
2548
+ this.files[index].tag = tags;
2549
+ this.onChange(this.files);
2550
+ }
2551
+ }
2385
2552
  onDragOver(event) {
2386
2553
  event.preventDefault();
2387
2554
  this.isDragOver = true;
@@ -2429,11 +2596,15 @@ class FileUploaderComponent {
2429
2596
  if (notAcceptedFiles.length > 0) {
2430
2597
  this.error.emit(notAcceptedFiles);
2431
2598
  }
2432
- // Compute preview URLs for the new files
2599
+ // Compute preview URLs for the new files and initialize tags
2433
2600
  filteredFiles.forEach((file) => {
2434
2601
  if (file.type.startsWith('image')) {
2435
2602
  file.previewUrl = URL.createObjectURL(file);
2436
2603
  }
2604
+ // Initialize tag array if not exists
2605
+ if (!file.tag) {
2606
+ file.tag = [];
2607
+ }
2437
2608
  });
2438
2609
  this.files = this.files.concat(filteredFiles);
2439
2610
  this.filesAdded.emit(filteredFiles);
@@ -2468,7 +2639,9 @@ class FileUploaderComponent {
2468
2639
  this.files.forEach((file) => {
2469
2640
  file.isUploading = true;
2470
2641
  file.isUploaded = false;
2471
- uploadTrigger$.push(this.uploadApi(file, this.keyPrefix).pipe(
2642
+ // Create a file with tag information for upload
2643
+ const fileWithTag = Object.assign(file, { tag: file.tag || [] });
2644
+ uploadTrigger$.push(this.uploadApi(fileWithTag, this.keyPrefix).pipe(
2472
2645
  // Handle the upload API response
2473
2646
  tap((key) => {
2474
2647
  file.isUploading = false;
@@ -2498,18 +2671,18 @@ class FileUploaderComponent {
2498
2671
  multi: true,
2499
2672
  },
2500
2673
  provideTranslocoScope('haloduck'),
2501
- ], ngImport: i0, template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex items-center justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </li>\n }\n </ul>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
2674
+ ], ngImport: i0, template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex items-center justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: TagInputComponent, selector: "haloduck-tag-input", inputs: ["placeholder", "disabled", "allowDuplicates", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
2502
2675
  }
2503
2676
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FileUploaderComponent, decorators: [{
2504
2677
  type: Component,
2505
- args: [{ selector: 'haloduck-file-uploader', imports: [CommonModule, TranslocoModule], providers: [
2678
+ args: [{ selector: 'haloduck-file-uploader', imports: [CommonModule, TranslocoModule, TagInputComponent], providers: [
2506
2679
  {
2507
2680
  provide: NG_VALUE_ACCESSOR,
2508
2681
  useExisting: forwardRef(() => FileUploaderComponent),
2509
2682
  multi: true,
2510
2683
  },
2511
2684
  provideTranslocoScope('haloduck'),
2512
- ], template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex items-center justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </li>\n }\n </ul>\n }\n</div>\n" }]
2685
+ ], template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex items-center justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n" }]
2513
2686
  }], propDecorators: { disabled: [{
2514
2687
  type: Input
2515
2688
  }], urlPrefix: [{
@@ -3506,6 +3679,8 @@ class TableComponent {
3506
3679
  return this.getDateTime(value);
3507
3680
  case 'time':
3508
3681
  return this.getTime(value);
3682
+ case 'number':
3683
+ return this.getNumber(value);
3509
3684
  default:
3510
3685
  return of(value);
3511
3686
  }
@@ -3908,167 +4083,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
3908
4083
  args: ['label']
3909
4084
  }] } });
3910
4085
 
3911
- class TagInputComponent {
3912
- label;
3913
- inputEl;
3914
- placeholder = '';
3915
- disabled = false;
3916
- allowDuplicates = false;
3917
- // Two-way binding support independent of CVA
3918
- set value(tags) {
3919
- if (Array.isArray(tags)) {
3920
- this.tags = tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
3921
- }
3922
- else {
3923
- this.tags = [];
3924
- }
3925
- }
3926
- valueChange = new EventEmitter();
3927
- tags = [];
3928
- inputValue = '';
3929
- onChange = () => { };
3930
- onTouched = () => { };
3931
- writeValue(value) {
3932
- if (Array.isArray(value)) {
3933
- this.tags = value.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
3934
- }
3935
- else {
3936
- this.tags = [];
3937
- }
3938
- }
3939
- registerOnChange(fn) {
3940
- this.onChange = fn;
3941
- }
3942
- registerOnTouched(fn) {
3943
- this.onTouched = fn;
3944
- }
3945
- setDisabledState(isDisabled) {
3946
- this.disabled = isDisabled;
3947
- }
3948
- ngAfterViewInit() {
3949
- // hide label if no projected content
3950
- if (this.label && this.label.nativeElement) {
3951
- const hasContent = this.label.nativeElement.textContent?.trim();
3952
- if (!hasContent) {
3953
- this.label.nativeElement.style.display = 'none';
3954
- }
3955
- }
3956
- }
3957
- focus() {
3958
- this.inputEl?.nativeElement?.focus();
3959
- }
3960
- onInput(event) {
3961
- const input = event.target;
3962
- this.inputValue = input.value;
3963
- this.onTouched();
3964
- }
3965
- onBlur() {
3966
- if (this.disabled)
3967
- return;
3968
- this.inputValue = '';
3969
- this.onTouched();
3970
- }
3971
- onKeydown(event) {
3972
- if (this.disabled)
3973
- return;
3974
- // Commit on comma or Enter
3975
- if (event.key === ',' || event.key === 'Enter') {
3976
- event.preventDefault();
3977
- this.commitCurrentInput();
3978
- return;
3979
- }
3980
- // Backspace behavior
3981
- if (event.key === 'Backspace') {
3982
- if (this.inputValue.length > 0) {
3983
- return; // default backspace in input
3984
- }
3985
- if (this.tags.length > 0) {
3986
- event.preventDefault();
3987
- this.removeTag(this.tags.length - 1);
3988
- return;
3989
- }
3990
- }
3991
- }
3992
- removeTag(index) {
3993
- if (this.disabled)
3994
- return;
3995
- if (index < 0 || index >= this.tags.length)
3996
- return;
3997
- this.tags = this.tags.filter((_, i) => i !== index);
3998
- this.emitChanges();
3999
- }
4000
- commitCurrentInput() {
4001
- const raw = this.inputValue.trim();
4002
- if (!raw) {
4003
- this.inputValue = '';
4004
- return;
4005
- }
4006
- // If user pasted multiple comma-separated values, split and add all
4007
- const parts = raw
4008
- .split(',')
4009
- .map((p) => p.trim())
4010
- .filter((p) => p.length > 0);
4011
- for (const part of parts) {
4012
- this.addTag(part);
4013
- }
4014
- this.inputValue = '';
4015
- }
4016
- addTag(tag) {
4017
- if (!this.allowDuplicates) {
4018
- const exists = this.tags.some((t) => t.toLowerCase() === tag.toLowerCase());
4019
- if (exists) {
4020
- return;
4021
- }
4022
- }
4023
- this.tags = [...this.tags, tag];
4024
- this.emitChanges();
4025
- }
4026
- emitChanges() {
4027
- const cleaned = this.tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
4028
- if (cleaned.length !== this.tags.length || cleaned.some((t, i) => t !== this.tags[i])) {
4029
- this.tags = cleaned;
4030
- }
4031
- this.onChange(this.tags);
4032
- this.valueChange.emit(this.tags);
4033
- }
4034
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4035
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: TagInputComponent, isStandalone: true, selector: "haloduck-tag-input", inputs: { placeholder: "placeholder", disabled: "disabled", allowDuplicates: "allowDuplicates", value: "value" }, outputs: { valueChange: "valueChange" }, providers: [
4036
- {
4037
- provide: NG_VALUE_ACCESSOR,
4038
- useExisting: forwardRef(() => TagInputComponent),
4039
- multi: true,
4040
- },
4041
- provideTranslocoScope('haloduck'),
4042
- ], viewQueries: [{ propertyName: "label", first: true, predicate: ["label"], descendants: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
4043
- }
4044
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, decorators: [{
4045
- type: Component,
4046
- args: [{ selector: 'haloduck-tag-input', imports: [CommonModule], providers: [
4047
- {
4048
- provide: NG_VALUE_ACCESSOR,
4049
- useExisting: forwardRef(() => TagInputComponent),
4050
- multi: true,
4051
- },
4052
- provideTranslocoScope('haloduck'),
4053
- ], template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"] }]
4054
- }], propDecorators: { label: [{
4055
- type: ViewChild,
4056
- args: ['label']
4057
- }], inputEl: [{
4058
- type: ViewChild,
4059
- args: ['inputEl']
4060
- }], placeholder: [{
4061
- type: Input
4062
- }], disabled: [{
4063
- type: Input
4064
- }], allowDuplicates: [{
4065
- type: Input
4066
- }], value: [{
4067
- type: Input
4068
- }], valueChange: [{
4069
- type: Output
4070
- }] } });
4071
-
4072
4086
  class TagViewerComponent {
4073
4087
  label;
4074
4088
  tags = [];