@kaufmann-lab/ui 0.1.16 → 0.1.17

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.
@@ -6,6 +6,7 @@ import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
6
6
  import { Overlay, OverlayContainer } from '@angular/cdk/overlay';
7
7
  import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
8
8
  import { Subscription } from 'rxjs';
9
+ import { DomSanitizer } from '@angular/platform-browser';
9
10
  import { FocusTrapFactory } from '@angular/cdk/a11y';
10
11
  import mapboxgl from 'mapbox-gl';
11
12
 
@@ -2596,10 +2597,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
2596
2597
  args: [{ selector: 'kf-content-state', imports: [EmptyState, Skeleton, Spinner], template: "@switch (state()) {\n @case ('ready') {\n <ng-content />\n }\n @case ('loading') {\n <div class=\"kf-content-state__loading\" aria-busy=\"true\">\n @if (load() === 'spinner') {\n <kf-spinner [label]=\"loadingLabel()\" />\n } @else {\n <kf-skeleton />\n <kf-skeleton width=\"80%\" />\n <kf-skeleton width=\"60%\" />\n }\n </div>\n }\n @default {\n <kf-empty-state [tone]=\"state() === 'error' ? 'error' : 'neutral'\">\n @if (title()) {\n <p class=\"kf-content-state__title\">{{ title() }}</p>\n }\n @if (subtitle()) {\n <p class=\"kf-content-state__subtitle\">{{ subtitle() }}</p>\n }\n <div class=\"kf-content-state__actions\"><ng-content select=\"[actions]\" /></div>\n </kf-empty-state>\n }\n}\n", styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-content-state__loading{display:flex;flex-direction:column;gap:var(--kf-space-sm)}.kf-content-state__title{margin:0;font-family:var(--kf-font-title);font-weight:700;color:var(--kf-color-ink)}.kf-content-state__subtitle{margin:0;color:var(--kf-color-text-muted)}.kf-content-state__actions:empty{display:none}.kf-content-state__actions{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--kf-space-sm);margin-top:var(--kf-space-sm)}\n"] }]
2597
2598
  }], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], load: [{ type: i0.Input, args: [{ isSignal: true, alias: "load", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], loadingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingLabel", required: false }] }] } });
2598
2599
 
2600
+ const RAW_BASE64_MIME = {
2601
+ image: 'image/jpeg',
2602
+ video: 'video/mp4',
2603
+ pdf: 'application/pdf',
2604
+ };
2605
+ /** Solo contenido ya en memoria: un `<iframe>` con una URL remota arbitraria podría cargar cualquier página. */
2606
+ const LOCAL_PDF_SRC = /^(blob:|data:application\/pdf[;,])/i;
2599
2607
  let contentViewerId = 0;
2600
2608
  /**
2601
- * Visor a pantalla completa para ampliar una pieza de contenido — hoy
2602
- * imagen, con `video` ya soportado para cuando haya contenido de ese tipo.
2609
+ * Visor a pantalla completa para ampliar una pieza de contenido: imagen,
2610
+ * video o PDF (`type="pdf"`, renderizado por el visor nativo del navegador).
2603
2611
  * Mismo mecanismo que `kf-modal` (el `<dialog>` nativo resuelve backdrop,
2604
2612
  * atrapado de foco, top-layer y `Escape`), pero sin panel/título/footer:
2605
2613
  * el contenido ocupa toda la pantalla sobre un backdrop oscuro.
@@ -2607,10 +2615,17 @@ let contentViewerId = 0;
2607
2615
  * `src` acepta lo que venga del backend sin que el consumidor tenga que
2608
2616
  * normalizarlo primero: una URL (`http(s)://`), un data URI ya armado
2609
2617
  * (`data:...`), o base64 crudo (se le antepone el prefijo `data:` según
2610
- * `type`, asumiendo JPEG para imagen y MP4 para video).
2618
+ * `type`, asumiendo JPEG para imagen, MP4 para video y PDF para pdf). Un PDF
2619
+ * solo acepta contenido local (`blob:`, data URI o base64), nunca una URL remota.
2620
+ *
2621
+ * Lo proyectado con el atributo `actions` (p. ej. un botón de descarga) va en
2622
+ * la barra superior, junto al título.
2611
2623
  *
2612
2624
  * <kf-content-viewer [(open)]="viewerOpen" [src]="viewerSrc()" alt="Foto del vehículo" />
2613
2625
  * <kf-content-viewer [(open)]="viewerOpen" type="video" [src]="viewerSrc()" title="Inspección" />
2626
+ * <kf-content-viewer [(open)]="viewerOpen" type="pdf" [src]="blobUrl()" title="Contrato">
2627
+ * <kf-button actions variant="surface" icon="download" (click)="save()">Descargar</kf-button>
2628
+ * </kf-content-viewer>
2614
2629
  */
2615
2630
  class ContentViewer {
2616
2631
  type = input('image', /* @ts-ignore */
@@ -2638,13 +2653,22 @@ class ContentViewer {
2638
2653
  return null;
2639
2654
  if (/^(https?:|data:|blob:)/i.test(raw))
2640
2655
  return raw;
2641
- const mime = this.type() === 'video' ? 'video/mp4' : 'image/jpeg';
2642
- return `data:${mime};base64,${raw}`;
2656
+ return `data:${RAW_BASE64_MIME[this.type()]};base64,${raw}`;
2643
2657
  }, /* @ts-ignore */
2644
2658
  ...(ngDevMode ? [{ debugName: "resolvedSrc" }] : /* istanbul ignore next */ []));
2659
+ pdfSrc = computed(() => {
2660
+ const src = this.resolvedSrc();
2661
+ if (this.type() !== 'pdf' || !src || !LOCAL_PDF_SRC.test(src))
2662
+ return null;
2663
+ return this.sanitizer.bypassSecurityTrustResourceUrl(src);
2664
+ }, /* @ts-ignore */
2665
+ ...(ngDevMode ? [{ debugName: "pdfSrc" }] : /* istanbul ignore next */ []));
2666
+ fallbackLabel = computed(() => ({ image: 'Visor de imagen', video: 'Visor de video', pdf: 'Visor de documento' })[this.type()], /* @ts-ignore */
2667
+ ...(ngDevMode ? [{ debugName: "fallbackLabel" }] : /* istanbul ignore next */ []));
2645
2668
  dialog = viewChild('dialog', /* @ts-ignore */
2646
2669
  ...(ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []));
2647
2670
  document = inject(DOCUMENT);
2671
+ sanitizer = inject(DomSanitizer);
2648
2672
  constructor() {
2649
2673
  afterRenderEffect(() => {
2650
2674
  const el = this.dialog()?.nativeElement;
@@ -2693,11 +2717,11 @@ class ContentViewer {
2693
2717
  }
2694
2718
  }
2695
2719
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ContentViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
2696
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: ContentViewer, isStandalone: true, selector: "kf-content-viewer", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "click": "onHostClick($event)" } }, viewQueries: [{ propertyName: "dialog", first: true, predicate: ["dialog"], descendants: true, isSignal: true }], ngImport: i0, template: "<dialog\n #dialog\n class=\"kf-content-viewer\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : type() === 'video' ? 'Visor de video' : 'Visor de imagen'\"\n (cancel)=\"onCancel($event)\"\n (close)=\"open.set(false)\"\n>\n @if (open()) {\n <div class=\"kf-content-viewer__panel\">\n @if (dismissible()) {\n <button\n type=\"button\"\n class=\"kf-content-viewer__close\"\n aria-label=\"Cerrar visor\"\n (click)=\"open.set(false)\"\n >\n <kf-icon name=\"x\" size=\"md\" />\n </button>\n }\n @if (title()) {\n <span class=\"kf-content-viewer__title\" [id]=\"titleId\">{{ title() }}</span>\n }\n <div class=\"kf-content-viewer__stage\">\n @if (resolvedSrc(); as src) {\n @if (type() === 'video') {\n <video class=\"kf-content-viewer__media\" [src]=\"src\" controls autoplay></video>\n } @else {\n <img class=\"kf-content-viewer__media\" [src]=\"src\" [alt]=\"alt()\" />\n }\n }\n </div>\n </div>\n }\n</dialog>\n", styles: [".kf-content-viewer{width:100vw;height:100vh;max-width:100vw;max-height:100vh;margin:0;padding:0;border:none;background:transparent;overflow:visible;font-family:var(--kf-font-body)}.kf-content-viewer::backdrop{background:#0a0c10eb}.kf-content-viewer__panel{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--kf-space-md);width:100%;height:100%;padding:var(--kf-space-xl);box-sizing:border-box}.kf-content-viewer__close{position:absolute;top:var(--kf-space-lg);right:var(--kf-space-lg);display:inline-flex;padding:var(--kf-space-xs);border:none;border-radius:var(--kf-radius-pill);background:#ffffff1f;color:var(--kf-color-white);cursor:pointer}.kf-content-viewer__close:hover{background:#ffffff38}.kf-content-viewer__close:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-content-viewer__title{font-size:.9rem;font-weight:600;color:var(--kf-color-white);text-align:center}.kf-content-viewer__stage{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;width:100%}.kf-content-viewer__media{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--kf-radius-sm)}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }] });
2720
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: ContentViewer, isStandalone: true, selector: "kf-content-viewer", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "click": "onHostClick($event)" } }, viewQueries: [{ propertyName: "dialog", first: true, predicate: ["dialog"], descendants: true, isSignal: true }], ngImport: i0, template: "<dialog\n #dialog\n class=\"kf-content-viewer\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : fallbackLabel()\"\n (cancel)=\"onCancel($event)\"\n (close)=\"open.set(false)\"\n>\n @if (open()) {\n <div class=\"kf-content-viewer__panel\">\n @if (dismissible()) {\n <button\n type=\"button\"\n class=\"kf-content-viewer__close\"\n aria-label=\"Cerrar visor\"\n (click)=\"open.set(false)\"\n >\n <kf-icon name=\"x\" size=\"md\" />\n </button>\n }\n <div class=\"kf-content-viewer__bar\">\n @if (title()) {\n <span class=\"kf-content-viewer__title\" [id]=\"titleId\">{{ title() }}</span>\n }\n <div class=\"kf-content-viewer__actions\"><ng-content select=\"[actions]\" /></div>\n </div>\n <div class=\"kf-content-viewer__stage\">\n @if (type() === 'pdf') {\n @if (pdfSrc(); as src) {\n <iframe\n class=\"kf-content-viewer__document\"\n [src]=\"src\"\n [title]=\"title() || fallbackLabel()\"\n ></iframe>\n }\n } @else if (resolvedSrc(); as src) {\n @if (type() === 'video') {\n <video class=\"kf-content-viewer__media\" [src]=\"src\" controls autoplay></video>\n } @else {\n <img class=\"kf-content-viewer__media\" [src]=\"src\" [alt]=\"alt()\" />\n }\n }\n </div>\n </div>\n }\n</dialog>\n", styles: [".kf-content-viewer{width:100vw;height:100vh;max-width:100vw;max-height:100vh;margin:0;padding:0;border:none;background:transparent;overflow:visible;font-family:var(--kf-font-body)}.kf-content-viewer::backdrop{background:#0a0c10eb}.kf-content-viewer__panel{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--kf-space-md);width:100%;height:100%;padding:var(--kf-space-xl);box-sizing:border-box}.kf-content-viewer__close{position:absolute;top:var(--kf-space-lg);right:var(--kf-space-lg);display:inline-flex;padding:var(--kf-space-xs);border:none;border-radius:var(--kf-radius-pill);background:#ffffff1f;color:var(--kf-color-white);cursor:pointer}.kf-content-viewer__close:hover{background:#ffffff38}.kf-content-viewer__close:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-content-viewer__bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--kf-space-md);max-width:calc(100% - 2 * var(--kf-space-xl))}.kf-content-viewer__actions{display:contents}.kf-content-viewer__title{font-size:.9rem;font-weight:600;color:var(--kf-color-white);text-align:center}.kf-content-viewer__stage{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;width:100%}.kf-content-viewer__document{width:min(100%,1100px);height:100%;border:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-white)}.kf-content-viewer__media{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--kf-radius-sm)}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }] });
2697
2721
  }
2698
2722
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ContentViewer, decorators: [{
2699
2723
  type: Component,
2700
- args: [{ selector: 'kf-content-viewer', imports: [Icon], template: "<dialog\n #dialog\n class=\"kf-content-viewer\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : type() === 'video' ? 'Visor de video' : 'Visor de imagen'\"\n (cancel)=\"onCancel($event)\"\n (close)=\"open.set(false)\"\n>\n @if (open()) {\n <div class=\"kf-content-viewer__panel\">\n @if (dismissible()) {\n <button\n type=\"button\"\n class=\"kf-content-viewer__close\"\n aria-label=\"Cerrar visor\"\n (click)=\"open.set(false)\"\n >\n <kf-icon name=\"x\" size=\"md\" />\n </button>\n }\n @if (title()) {\n <span class=\"kf-content-viewer__title\" [id]=\"titleId\">{{ title() }}</span>\n }\n <div class=\"kf-content-viewer__stage\">\n @if (resolvedSrc(); as src) {\n @if (type() === 'video') {\n <video class=\"kf-content-viewer__media\" [src]=\"src\" controls autoplay></video>\n } @else {\n <img class=\"kf-content-viewer__media\" [src]=\"src\" [alt]=\"alt()\" />\n }\n }\n </div>\n </div>\n }\n</dialog>\n", styles: [".kf-content-viewer{width:100vw;height:100vh;max-width:100vw;max-height:100vh;margin:0;padding:0;border:none;background:transparent;overflow:visible;font-family:var(--kf-font-body)}.kf-content-viewer::backdrop{background:#0a0c10eb}.kf-content-viewer__panel{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--kf-space-md);width:100%;height:100%;padding:var(--kf-space-xl);box-sizing:border-box}.kf-content-viewer__close{position:absolute;top:var(--kf-space-lg);right:var(--kf-space-lg);display:inline-flex;padding:var(--kf-space-xs);border:none;border-radius:var(--kf-radius-pill);background:#ffffff1f;color:var(--kf-color-white);cursor:pointer}.kf-content-viewer__close:hover{background:#ffffff38}.kf-content-viewer__close:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-content-viewer__title{font-size:.9rem;font-weight:600;color:var(--kf-color-white);text-align:center}.kf-content-viewer__stage{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;width:100%}.kf-content-viewer__media{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--kf-radius-sm)}\n"] }]
2724
+ args: [{ selector: 'kf-content-viewer', imports: [Icon], template: "<dialog\n #dialog\n class=\"kf-content-viewer\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : fallbackLabel()\"\n (cancel)=\"onCancel($event)\"\n (close)=\"open.set(false)\"\n>\n @if (open()) {\n <div class=\"kf-content-viewer__panel\">\n @if (dismissible()) {\n <button\n type=\"button\"\n class=\"kf-content-viewer__close\"\n aria-label=\"Cerrar visor\"\n (click)=\"open.set(false)\"\n >\n <kf-icon name=\"x\" size=\"md\" />\n </button>\n }\n <div class=\"kf-content-viewer__bar\">\n @if (title()) {\n <span class=\"kf-content-viewer__title\" [id]=\"titleId\">{{ title() }}</span>\n }\n <div class=\"kf-content-viewer__actions\"><ng-content select=\"[actions]\" /></div>\n </div>\n <div class=\"kf-content-viewer__stage\">\n @if (type() === 'pdf') {\n @if (pdfSrc(); as src) {\n <iframe\n class=\"kf-content-viewer__document\"\n [src]=\"src\"\n [title]=\"title() || fallbackLabel()\"\n ></iframe>\n }\n } @else if (resolvedSrc(); as src) {\n @if (type() === 'video') {\n <video class=\"kf-content-viewer__media\" [src]=\"src\" controls autoplay></video>\n } @else {\n <img class=\"kf-content-viewer__media\" [src]=\"src\" [alt]=\"alt()\" />\n }\n }\n </div>\n </div>\n }\n</dialog>\n", styles: [".kf-content-viewer{width:100vw;height:100vh;max-width:100vw;max-height:100vh;margin:0;padding:0;border:none;background:transparent;overflow:visible;font-family:var(--kf-font-body)}.kf-content-viewer::backdrop{background:#0a0c10eb}.kf-content-viewer__panel{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--kf-space-md);width:100%;height:100%;padding:var(--kf-space-xl);box-sizing:border-box}.kf-content-viewer__close{position:absolute;top:var(--kf-space-lg);right:var(--kf-space-lg);display:inline-flex;padding:var(--kf-space-xs);border:none;border-radius:var(--kf-radius-pill);background:#ffffff1f;color:var(--kf-color-white);cursor:pointer}.kf-content-viewer__close:hover{background:#ffffff38}.kf-content-viewer__close:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-content-viewer__bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--kf-space-md);max-width:calc(100% - 2 * var(--kf-space-xl))}.kf-content-viewer__actions{display:contents}.kf-content-viewer__title{font-size:.9rem;font-weight:600;color:var(--kf-color-white);text-align:center}.kf-content-viewer__stage{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;width:100%}.kf-content-viewer__document{width:min(100%,1100px);height:100%;border:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-white)}.kf-content-viewer__media{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--kf-radius-sm)}\n"] }]
2701
2725
  }], ctorParameters: () => [], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], dialog: [{ type: i0.ViewChild, args: ['dialog', { isSignal: true }] }], onHostClick: [{
2702
2726
  type: HostListener,
2703
2727
  args: ['click', ['$event']]