@haloduck/ui 2.0.44 → 2.0.46

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 flex-col sm:flex-row items-stretch sm:items-center justify-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\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 <div class=\"flex-1\">\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 </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 </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 flex-col sm:flex-row items-stretch sm:items-center justify-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\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 <div class=\"flex-1\">\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 </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 </div>\n </li>\n }\n </ul>\n }\n</div>\n" }]
2513
2686
  }], propDecorators: { disabled: [{
2514
2687
  type: Input
2515
2688
  }], urlPrefix: [{
@@ -3910,167 +4083,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
3910
4083
  args: ['label']
3911
4084
  }] } });
3912
4085
 
3913
- class TagInputComponent {
3914
- label;
3915
- inputEl;
3916
- placeholder = '';
3917
- disabled = false;
3918
- allowDuplicates = false;
3919
- // Two-way binding support independent of CVA
3920
- set value(tags) {
3921
- if (Array.isArray(tags)) {
3922
- this.tags = tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
3923
- }
3924
- else {
3925
- this.tags = [];
3926
- }
3927
- }
3928
- valueChange = new EventEmitter();
3929
- tags = [];
3930
- inputValue = '';
3931
- onChange = () => { };
3932
- onTouched = () => { };
3933
- writeValue(value) {
3934
- if (Array.isArray(value)) {
3935
- this.tags = value.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
3936
- }
3937
- else {
3938
- this.tags = [];
3939
- }
3940
- }
3941
- registerOnChange(fn) {
3942
- this.onChange = fn;
3943
- }
3944
- registerOnTouched(fn) {
3945
- this.onTouched = fn;
3946
- }
3947
- setDisabledState(isDisabled) {
3948
- this.disabled = isDisabled;
3949
- }
3950
- ngAfterViewInit() {
3951
- // hide label if no projected content
3952
- if (this.label && this.label.nativeElement) {
3953
- const hasContent = this.label.nativeElement.textContent?.trim();
3954
- if (!hasContent) {
3955
- this.label.nativeElement.style.display = 'none';
3956
- }
3957
- }
3958
- }
3959
- focus() {
3960
- this.inputEl?.nativeElement?.focus();
3961
- }
3962
- onInput(event) {
3963
- const input = event.target;
3964
- this.inputValue = input.value;
3965
- this.onTouched();
3966
- }
3967
- onBlur() {
3968
- if (this.disabled)
3969
- return;
3970
- this.inputValue = '';
3971
- this.onTouched();
3972
- }
3973
- onKeydown(event) {
3974
- if (this.disabled)
3975
- return;
3976
- // Commit on comma or Enter
3977
- if (event.key === ',' || event.key === 'Enter') {
3978
- event.preventDefault();
3979
- this.commitCurrentInput();
3980
- return;
3981
- }
3982
- // Backspace behavior
3983
- if (event.key === 'Backspace') {
3984
- if (this.inputValue.length > 0) {
3985
- return; // default backspace in input
3986
- }
3987
- if (this.tags.length > 0) {
3988
- event.preventDefault();
3989
- this.removeTag(this.tags.length - 1);
3990
- return;
3991
- }
3992
- }
3993
- }
3994
- removeTag(index) {
3995
- if (this.disabled)
3996
- return;
3997
- if (index < 0 || index >= this.tags.length)
3998
- return;
3999
- this.tags = this.tags.filter((_, i) => i !== index);
4000
- this.emitChanges();
4001
- }
4002
- commitCurrentInput() {
4003
- const raw = this.inputValue.trim();
4004
- if (!raw) {
4005
- this.inputValue = '';
4006
- return;
4007
- }
4008
- // If user pasted multiple comma-separated values, split and add all
4009
- const parts = raw
4010
- .split(',')
4011
- .map((p) => p.trim())
4012
- .filter((p) => p.length > 0);
4013
- for (const part of parts) {
4014
- this.addTag(part);
4015
- }
4016
- this.inputValue = '';
4017
- }
4018
- addTag(tag) {
4019
- if (!this.allowDuplicates) {
4020
- const exists = this.tags.some((t) => t.toLowerCase() === tag.toLowerCase());
4021
- if (exists) {
4022
- return;
4023
- }
4024
- }
4025
- this.tags = [...this.tags, tag];
4026
- this.emitChanges();
4027
- }
4028
- emitChanges() {
4029
- const cleaned = this.tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
4030
- if (cleaned.length !== this.tags.length || cleaned.some((t, i) => t !== this.tags[i])) {
4031
- this.tags = cleaned;
4032
- }
4033
- this.onChange(this.tags);
4034
- this.valueChange.emit(this.tags);
4035
- }
4036
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4037
- 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: [
4038
- {
4039
- provide: NG_VALUE_ACCESSOR,
4040
- useExisting: forwardRef(() => TagInputComponent),
4041
- multi: true,
4042
- },
4043
- provideTranslocoScope('haloduck'),
4044
- ], 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 }] });
4045
- }
4046
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, decorators: [{
4047
- type: Component,
4048
- args: [{ selector: 'haloduck-tag-input', imports: [CommonModule], providers: [
4049
- {
4050
- provide: NG_VALUE_ACCESSOR,
4051
- useExisting: forwardRef(() => TagInputComponent),
4052
- multi: true,
4053
- },
4054
- provideTranslocoScope('haloduck'),
4055
- ], 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"] }]
4056
- }], propDecorators: { label: [{
4057
- type: ViewChild,
4058
- args: ['label']
4059
- }], inputEl: [{
4060
- type: ViewChild,
4061
- args: ['inputEl']
4062
- }], placeholder: [{
4063
- type: Input
4064
- }], disabled: [{
4065
- type: Input
4066
- }], allowDuplicates: [{
4067
- type: Input
4068
- }], value: [{
4069
- type: Input
4070
- }], valueChange: [{
4071
- type: Output
4072
- }] } });
4073
-
4074
4086
  class TagViewerComponent {
4075
4087
  label;
4076
4088
  tags = [];