@memberjunction/ng-artifacts 5.37.0 → 5.38.0
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.
- package/README.md +31 -0
- package/dist/__tests__/image-artifact-viewer.test.d.ts +2 -0
- package/dist/__tests__/image-artifact-viewer.test.d.ts.map +1 -0
- package/dist/__tests__/image-artifact-viewer.test.js +62 -0
- package/dist/__tests__/image-artifact-viewer.test.js.map +1 -0
- package/dist/lib/__tests__/interactive-form-apply.service.test.d.ts +2 -0
- package/dist/lib/__tests__/interactive-form-apply.service.test.d.ts.map +1 -0
- package/dist/lib/__tests__/interactive-form-apply.service.test.js +206 -0
- package/dist/lib/__tests__/interactive-form-apply.service.test.js.map +1 -0
- package/dist/lib/artifacts.module.d.ts +14 -12
- package/dist/lib/artifacts.module.d.ts.map +1 -1
- package/dist/lib/artifacts.module.js +14 -2
- package/dist/lib/artifacts.module.js.map +1 -1
- package/dist/lib/components/artifact-type-plugin-viewer.component.d.ts +11 -1
- package/dist/lib/components/artifact-type-plugin-viewer.component.d.ts.map +1 -1
- package/dist/lib/components/artifact-type-plugin-viewer.component.js +18 -1
- package/dist/lib/components/artifact-type-plugin-viewer.component.js.map +1 -1
- package/dist/lib/components/artifact-viewer-panel.component.d.ts +10 -1
- package/dist/lib/components/artifact-viewer-panel.component.d.ts.map +1 -1
- package/dist/lib/components/artifact-viewer-panel.component.js +11 -3
- package/dist/lib/components/artifact-viewer-panel.component.js.map +1 -1
- package/dist/lib/components/plugins/component-artifact-viewer.component.d.ts +60 -2
- package/dist/lib/components/plugins/component-artifact-viewer.component.d.ts.map +1 -1
- package/dist/lib/components/plugins/component-artifact-viewer.component.js +373 -24
- package/dist/lib/components/plugins/component-artifact-viewer.component.js.map +1 -1
- package/dist/lib/components/plugins/data-artifact-viewer.component.d.ts.map +1 -1
- package/dist/lib/components/plugins/data-artifact-viewer.component.js +0 -1
- package/dist/lib/components/plugins/data-artifact-viewer.component.js.map +1 -1
- package/dist/lib/components/plugins/image-artifact-viewer.component.d.ts +129 -0
- package/dist/lib/components/plugins/image-artifact-viewer.component.d.ts.map +1 -0
- package/dist/lib/components/plugins/image-artifact-viewer.component.js +413 -0
- package/dist/lib/components/plugins/image-artifact-viewer.component.js.map +1 -0
- package/dist/lib/components/plugins/save-query-dialog.component.d.ts.map +1 -1
- package/dist/lib/components/plugins/save-query-dialog.component.js +0 -2
- package/dist/lib/components/plugins/save-query-dialog.component.js.map +1 -1
- package/dist/lib/services/interactive-form-apply.service.d.ts +46 -0
- package/dist/lib/services/interactive-form-apply.service.d.ts.map +1 -0
- package/dist/lib/services/interactive-form-apply.service.js +219 -0
- package/dist/lib/services/interactive-form-apply.service.js.map +1 -0
- package/dist/public-api.d.ts +2 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +2 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +18 -16
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core';
|
|
2
|
+
import { DataSnapshot } from '@memberjunction/core';
|
|
3
|
+
import { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';
|
|
4
|
+
import { ArtifactFileService } from '../../services/artifact-file.service';
|
|
5
|
+
import * as i0 from "@angular/core";
|
|
6
|
+
/**
|
|
7
|
+
* Lookup table for synthesizing a download filename when the artifact version
|
|
8
|
+
* has no `FileName` set. Covers the MIME types declared by the seed `Image`
|
|
9
|
+
* artifact type (`image/*`); anything not listed falls back to `img`.
|
|
10
|
+
*
|
|
11
|
+
* Exported so the unit test can assert the mapping without instantiating an
|
|
12
|
+
* Angular component (the package's vitest config uses the `node` environment).
|
|
13
|
+
*/
|
|
14
|
+
export declare const IMAGE_MIME_EXTENSION_MAP: Readonly<Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Pure helper that synthesizes a safe download filename from an artifact's
|
|
17
|
+
* name + MIME type. Pulled out as a top-level function (not a class method) so
|
|
18
|
+
* the vitest `node`-environment unit test can exercise it directly.
|
|
19
|
+
*
|
|
20
|
+
* - Strips characters illegal on common filesystems (anything not
|
|
21
|
+
* `[A-Za-z0-9._-]`) and replaces them with underscores.
|
|
22
|
+
* - Picks the extension from {@link IMAGE_MIME_EXTENSION_MAP}, defaulting to
|
|
23
|
+
* `img` if the MIME type is missing or unrecognized.
|
|
24
|
+
* - Falls back to the literal `image` when no name is supplied.
|
|
25
|
+
*
|
|
26
|
+
* @param name Artifact name (e.g. `"Generated image 1"`). Optional.
|
|
27
|
+
* @param mime MIME type from the artifact version (e.g. `"image/jpeg"`). Optional.
|
|
28
|
+
* @returns Sanitized filename with extension (e.g. `"Generated_image_1.jpg"`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildImageDownloadFileName(name: string | null | undefined, mime: string | null | undefined): string;
|
|
31
|
+
/**
|
|
32
|
+
* Viewer plugin for raster image artifact versions (`image/png`, `image/jpeg`,
|
|
33
|
+
* `image/webp`, etc.). Vector SVG is handled by `SvgArtifactViewerPlugin`.
|
|
34
|
+
*
|
|
35
|
+
* Storage modes both supported (same contract as `PdfArtifactViewerPlugin`):
|
|
36
|
+
* - `ContentMode === 'Text'` — `artifactVersion.Content` is a `data:image/<mime>;base64,…` URI.
|
|
37
|
+
* We bind it directly to `<img [src]>`; the browser decodes inline.
|
|
38
|
+
* - `ContentMode === 'File'` — bytes live in MJStorage. We fetch a
|
|
39
|
+
* pre-authenticated URL via `ArtifactFileService.getDownloadUrl()` and
|
|
40
|
+
* bind that to `<img [src]>`.
|
|
41
|
+
*
|
|
42
|
+
* UX highlights:
|
|
43
|
+
* - Click the image to toggle **fit** (default — `object-fit: contain` inside
|
|
44
|
+
* the panel) ↔ **actual** size (1:1 pixels, scrolls within the panel body).
|
|
45
|
+
* - Toolbar provides Download (reuses base class `triggerBrowserDownload`) and
|
|
46
|
+
* Print (opens in a new tab so the browser's native print dialog handles it).
|
|
47
|
+
* - Loading and error states match the PDF plugin's vocabulary, so behavior
|
|
48
|
+
* across all file-style viewers is consistent.
|
|
49
|
+
* - Image dimensions are surfaced as a small chip once the browser has decoded
|
|
50
|
+
* the image (`naturalWidth × naturalHeight`).
|
|
51
|
+
*
|
|
52
|
+
* Design tokens are used for every color so the plugin adapts to light / dark
|
|
53
|
+
* themes — see root `CLAUDE.md` "Design Token System".
|
|
54
|
+
*
|
|
55
|
+
* @see PdfArtifactViewerComponent — the dual-storage reference implementation.
|
|
56
|
+
* @see plans/artifact-attachment-unification.md — the broader artifact-as-canonical-storage design.
|
|
57
|
+
* @see docs/chat-ui-image-display-bugs.md — context for why this plugin exists (Bug B).
|
|
58
|
+
*/
|
|
59
|
+
export declare class ImageArtifactViewerComponent extends BaseArtifactViewerPluginComponent implements OnInit, OnDestroy {
|
|
60
|
+
private fileService;
|
|
61
|
+
private cdr;
|
|
62
|
+
/** Current resolved URL to bind to the `<img src>` — either a data URI (inline) or a pre-auth URL (file). */
|
|
63
|
+
imageUrl: string;
|
|
64
|
+
/** True from `ngOnInit` until the `<img>` `(load)` event fires (or an error happens). */
|
|
65
|
+
isLoading: boolean;
|
|
66
|
+
/** Set while the download is in flight, to drive the toolbar spinner. */
|
|
67
|
+
isDownloading: boolean;
|
|
68
|
+
/** User-facing error message; non-empty hides the image and shows the error block. */
|
|
69
|
+
errorMessage: string;
|
|
70
|
+
/** Click-to-toggle display mode. `'fit'` = contained in the panel, `'actual'` = 1:1 with scroll. */
|
|
71
|
+
displayMode: 'fit' | 'actual';
|
|
72
|
+
/** Native image dimensions, populated by the `(load)` event. Null until then. */
|
|
73
|
+
naturalWidth: number | null;
|
|
74
|
+
naturalHeight: number | null;
|
|
75
|
+
/**
|
|
76
|
+
* Object URL we lazily build from inline data so Download / Print have a real
|
|
77
|
+
* URL to anchor on. Tracked here so `ngOnDestroy` can release it via
|
|
78
|
+
* `URL.revokeObjectURL`. Null until the user clicks Download or Print.
|
|
79
|
+
*/
|
|
80
|
+
private inlineObjectUrl;
|
|
81
|
+
constructor(fileService: ArtifactFileService, cdr: ChangeDetectorRef);
|
|
82
|
+
/** Opt in to the wrapper's "Display" tab — without this the panel only shows Details/JSON. */
|
|
83
|
+
get hasDisplayContent(): boolean;
|
|
84
|
+
/** Filename surfaced by the toolbar in the center of the bar. */
|
|
85
|
+
get toolbarFileName(): string;
|
|
86
|
+
/** Accessible alt text — prefers a descriptive name over a raw filename. */
|
|
87
|
+
get imageAltText(): string;
|
|
88
|
+
ngOnInit(): Promise<void>;
|
|
89
|
+
ngOnDestroy(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the image URL based on the artifact version's `ContentMode`.
|
|
92
|
+
* Sets `imageUrl` (which the template binds to `<img src>`) or `errorMessage`.
|
|
93
|
+
* Loading state remains `true` until the browser fires `(load)` on the `<img>`.
|
|
94
|
+
*/
|
|
95
|
+
private resolveImageUrl;
|
|
96
|
+
onImageLoad(event: Event): void;
|
|
97
|
+
onImageError(): void;
|
|
98
|
+
private showError;
|
|
99
|
+
toggleDisplayMode(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Download the image to disk. Uses the inherited `triggerBrowserDownload`
|
|
102
|
+
* helper so the fetch → blob → object-URL → anchor pattern is shared across
|
|
103
|
+
* every file-backed viewer (PDF, XLSX, DOCX, and now Image).
|
|
104
|
+
*/
|
|
105
|
+
onDownload(): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Open the image in a new browser tab so the user can use the browser's
|
|
108
|
+
* native print dialog. We deliberately don't manage a print pipeline
|
|
109
|
+
* ourselves — same approach as `PdfArtifactViewerComponent.onPrint()`.
|
|
110
|
+
*/
|
|
111
|
+
onPrint(): void;
|
|
112
|
+
/**
|
|
113
|
+
* Returns a URL suitable for `<a download>` / `window.open` use.
|
|
114
|
+
* - File mode: `imageUrl` is already a pre-auth blob URL — use it directly.
|
|
115
|
+
* - Inline mode: `imageUrl` is a data URI; we lazily turn it into a blob URL
|
|
116
|
+
* (cached on the instance) for nicer download UX. Some browsers truncate
|
|
117
|
+
* long data URIs in the downloads tray; blob URLs preserve the filename.
|
|
118
|
+
*/
|
|
119
|
+
private ensureDownloadableUrl;
|
|
120
|
+
/**
|
|
121
|
+
* Required by the base class. Returns a structured summary of the artifact
|
|
122
|
+
* in its current display state — used by snapshot-consuming features
|
|
123
|
+
* (e.g. agent context capture, share / export flows).
|
|
124
|
+
*/
|
|
125
|
+
GetCurrentStateSnapshot(): DataSnapshot | null;
|
|
126
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ImageArtifactViewerComponent, never>;
|
|
127
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<ImageArtifactViewerComponent, "mj-image-artifact-viewer", never, {}, {}, never, never, false, never>;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=image-artifact-viewer.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-artifact-viewer.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/plugins/image-artifact-viewer.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAa,SAAS,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,iCAAiC,EAAE,MAAM,mCAAmC,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWpE,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAWnH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBA0Ja,4BAA6B,SAAQ,iCAAkC,YAAW,MAAM,EAAE,SAAS;IA4BxG,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,GAAG;IA5Bf,6GAA6G;IACtG,QAAQ,SAAM;IAErB,yFAAyF;IAClF,SAAS,UAAQ;IAExB,yEAAyE;IAClE,aAAa,UAAS;IAE7B,sFAAsF;IAC/E,YAAY,SAAM;IAEzB,oGAAoG;IAC7F,WAAW,EAAE,KAAK,GAAG,QAAQ,CAAS;IAE7C,iFAAiF;IAC1E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE3C;;;;OAIG;IACH,OAAO,CAAC,eAAe,CAAuB;gBAGlC,WAAW,EAAE,mBAAmB,EAChC,GAAG,EAAE,iBAAiB;IAKlC,8FAA8F;IAC9F,IAAoB,iBAAiB,IAAI,OAAO,CAE/C;IAED,iEAAiE;IACjE,IAAW,eAAe,IAAI,MAAM,CAEnC;IAED,4EAA4E;IAC5E,IAAW,YAAY,IAAI,MAAM,CAEhC;IAEK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,WAAW,IAAI,IAAI;IAWnB;;;;OAIG;YACW,eAAe;IAqCtB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAQ/B,YAAY,IAAI,IAAI;IAI3B,OAAO,CAAC,SAAS;IAQV,iBAAiB,IAAI,IAAI;IAShC;;;;OAIG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBxC;;;;OAIG;IACI,OAAO,IAAI,IAAI;IAQtB;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAc7B;;;;OAIG;IACa,uBAAuB,IAAI,YAAY,GAAG,IAAI;yCAtMrD,4BAA4B;2CAA5B,4BAA4B;CA4NxC"}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Component } from '@angular/core';
|
|
8
|
+
import { DataSnapshot } from '@memberjunction/core';
|
|
9
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
10
|
+
import { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';
|
|
11
|
+
import * as i0 from "@angular/core";
|
|
12
|
+
import * as i1 from "../../services/artifact-file.service";
|
|
13
|
+
import * as i2 from "@angular/common";
|
|
14
|
+
import * as i3 from "../file-artifact-toolbar.component";
|
|
15
|
+
function ImageArtifactViewerComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
16
|
+
i0.ɵɵelementStart(0, "div", 3);
|
|
17
|
+
i0.ɵɵelement(1, "i", 5);
|
|
18
|
+
i0.ɵɵelementStart(2, "span");
|
|
19
|
+
i0.ɵɵtext(3);
|
|
20
|
+
i0.ɵɵelementEnd()();
|
|
21
|
+
} if (rf & 2) {
|
|
22
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
23
|
+
i0.ɵɵadvance(3);
|
|
24
|
+
i0.ɵɵtextInterpolate(ctx_r0.errorMessage);
|
|
25
|
+
} }
|
|
26
|
+
function ImageArtifactViewerComponent_Conditional_4_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
27
|
+
i0.ɵɵelementStart(0, "div", 6);
|
|
28
|
+
i0.ɵɵelement(1, "i", 8);
|
|
29
|
+
i0.ɵɵelementStart(2, "span");
|
|
30
|
+
i0.ɵɵtext(3, "Loading image\u2026");
|
|
31
|
+
i0.ɵɵelementEnd()();
|
|
32
|
+
} }
|
|
33
|
+
function ImageArtifactViewerComponent_Conditional_4_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
34
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
35
|
+
i0.ɵɵelementStart(0, "img", 9);
|
|
36
|
+
i0.ɵɵlistener("load", function ImageArtifactViewerComponent_Conditional_4_Conditional_1_Template_img_load_0_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.onImageLoad($event)); })("error", function ImageArtifactViewerComponent_Conditional_4_Conditional_1_Template_img_error_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.onImageError()); })("click", function ImageArtifactViewerComponent_Conditional_4_Conditional_1_Template_img_click_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.toggleDisplayMode()); });
|
|
37
|
+
i0.ɵɵelementEnd();
|
|
38
|
+
} if (rf & 2) {
|
|
39
|
+
const ctx_r0 = i0.ɵɵnextContext(2);
|
|
40
|
+
i0.ɵɵclassProp("image-viewer__img--actual", ctx_r0.displayMode === "actual")("image-viewer__img--hidden", ctx_r0.isLoading);
|
|
41
|
+
i0.ɵɵproperty("src", ctx_r0.imageUrl, i0.ɵɵsanitizeUrl)("alt", ctx_r0.imageAltText)("title", ctx_r0.displayMode === "fit" ? "Click to view at actual size" : "Click to fit to window");
|
|
42
|
+
} }
|
|
43
|
+
function ImageArtifactViewerComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
44
|
+
i0.ɵɵconditionalCreate(0, ImageArtifactViewerComponent_Conditional_4_Conditional_0_Template, 4, 0, "div", 6);
|
|
45
|
+
i0.ɵɵconditionalCreate(1, ImageArtifactViewerComponent_Conditional_4_Conditional_1_Template, 1, 7, "img", 7);
|
|
46
|
+
} if (rf & 2) {
|
|
47
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
48
|
+
i0.ɵɵconditional(ctx_r0.isLoading ? 0 : -1);
|
|
49
|
+
i0.ɵɵadvance();
|
|
50
|
+
i0.ɵɵconditional(ctx_r0.imageUrl ? 1 : -1);
|
|
51
|
+
} }
|
|
52
|
+
function ImageArtifactViewerComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
53
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
54
|
+
i0.ɵɵtext(1);
|
|
55
|
+
i0.ɵɵelementEnd();
|
|
56
|
+
} if (rf & 2) {
|
|
57
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
58
|
+
i0.ɵɵadvance();
|
|
59
|
+
i0.ɵɵtextInterpolate2("", ctx_r0.naturalWidth, " \u00D7 ", ctx_r0.naturalHeight);
|
|
60
|
+
} }
|
|
61
|
+
/**
|
|
62
|
+
* Lookup table for synthesizing a download filename when the artifact version
|
|
63
|
+
* has no `FileName` set. Covers the MIME types declared by the seed `Image`
|
|
64
|
+
* artifact type (`image/*`); anything not listed falls back to `img`.
|
|
65
|
+
*
|
|
66
|
+
* Exported so the unit test can assert the mapping without instantiating an
|
|
67
|
+
* Angular component (the package's vitest config uses the `node` environment).
|
|
68
|
+
*/
|
|
69
|
+
export const IMAGE_MIME_EXTENSION_MAP = Object.freeze({
|
|
70
|
+
'image/png': 'png',
|
|
71
|
+
'image/jpeg': 'jpg',
|
|
72
|
+
'image/jpg': 'jpg',
|
|
73
|
+
'image/gif': 'gif',
|
|
74
|
+
'image/webp': 'webp',
|
|
75
|
+
'image/bmp': 'bmp',
|
|
76
|
+
'image/tiff': 'tif',
|
|
77
|
+
'image/avif': 'avif',
|
|
78
|
+
'image/heic': 'heic',
|
|
79
|
+
'image/heif': 'heif',
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Pure helper that synthesizes a safe download filename from an artifact's
|
|
83
|
+
* name + MIME type. Pulled out as a top-level function (not a class method) so
|
|
84
|
+
* the vitest `node`-environment unit test can exercise it directly.
|
|
85
|
+
*
|
|
86
|
+
* - Strips characters illegal on common filesystems (anything not
|
|
87
|
+
* `[A-Za-z0-9._-]`) and replaces them with underscores.
|
|
88
|
+
* - Picks the extension from {@link IMAGE_MIME_EXTENSION_MAP}, defaulting to
|
|
89
|
+
* `img` if the MIME type is missing or unrecognized.
|
|
90
|
+
* - Falls back to the literal `image` when no name is supplied.
|
|
91
|
+
*
|
|
92
|
+
* @param name Artifact name (e.g. `"Generated image 1"`). Optional.
|
|
93
|
+
* @param mime MIME type from the artifact version (e.g. `"image/jpeg"`). Optional.
|
|
94
|
+
* @returns Sanitized filename with extension (e.g. `"Generated_image_1.jpg"`).
|
|
95
|
+
*/
|
|
96
|
+
export function buildImageDownloadFileName(name, mime) {
|
|
97
|
+
// `??` falls back on null/undefined but NOT empty / whitespace-only names —
|
|
98
|
+
// those should also use the default, so trim first and check for length.
|
|
99
|
+
const trimmed = name?.trim();
|
|
100
|
+
const baseName = trimmed && trimmed.length > 0 ? trimmed : 'image';
|
|
101
|
+
const safeBase = baseName.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
102
|
+
const normalizedMime = (mime ?? '').toLowerCase();
|
|
103
|
+
const extension = IMAGE_MIME_EXTENSION_MAP[normalizedMime] ?? 'img';
|
|
104
|
+
// If the name already ends with the right extension, don't append it again
|
|
105
|
+
// (e.g. metadata round-tripped through a file upload).
|
|
106
|
+
return safeBase.toLowerCase().endsWith(`.${extension}`) ? safeBase : `${safeBase}.${extension}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Viewer plugin for raster image artifact versions (`image/png`, `image/jpeg`,
|
|
110
|
+
* `image/webp`, etc.). Vector SVG is handled by `SvgArtifactViewerPlugin`.
|
|
111
|
+
*
|
|
112
|
+
* Storage modes both supported (same contract as `PdfArtifactViewerPlugin`):
|
|
113
|
+
* - `ContentMode === 'Text'` — `artifactVersion.Content` is a `data:image/<mime>;base64,…` URI.
|
|
114
|
+
* We bind it directly to `<img [src]>`; the browser decodes inline.
|
|
115
|
+
* - `ContentMode === 'File'` — bytes live in MJStorage. We fetch a
|
|
116
|
+
* pre-authenticated URL via `ArtifactFileService.getDownloadUrl()` and
|
|
117
|
+
* bind that to `<img [src]>`.
|
|
118
|
+
*
|
|
119
|
+
* UX highlights:
|
|
120
|
+
* - Click the image to toggle **fit** (default — `object-fit: contain` inside
|
|
121
|
+
* the panel) ↔ **actual** size (1:1 pixels, scrolls within the panel body).
|
|
122
|
+
* - Toolbar provides Download (reuses base class `triggerBrowserDownload`) and
|
|
123
|
+
* Print (opens in a new tab so the browser's native print dialog handles it).
|
|
124
|
+
* - Loading and error states match the PDF plugin's vocabulary, so behavior
|
|
125
|
+
* across all file-style viewers is consistent.
|
|
126
|
+
* - Image dimensions are surfaced as a small chip once the browser has decoded
|
|
127
|
+
* the image (`naturalWidth × naturalHeight`).
|
|
128
|
+
*
|
|
129
|
+
* Design tokens are used for every color so the plugin adapts to light / dark
|
|
130
|
+
* themes — see root `CLAUDE.md` "Design Token System".
|
|
131
|
+
*
|
|
132
|
+
* @see PdfArtifactViewerComponent — the dual-storage reference implementation.
|
|
133
|
+
* @see plans/artifact-attachment-unification.md — the broader artifact-as-canonical-storage design.
|
|
134
|
+
* @see docs/chat-ui-image-display-bugs.md — context for why this plugin exists (Bug B).
|
|
135
|
+
*/
|
|
136
|
+
let ImageArtifactViewerComponent = class ImageArtifactViewerComponent extends BaseArtifactViewerPluginComponent {
|
|
137
|
+
fileService;
|
|
138
|
+
cdr;
|
|
139
|
+
/** Current resolved URL to bind to the `<img src>` — either a data URI (inline) or a pre-auth URL (file). */
|
|
140
|
+
imageUrl = '';
|
|
141
|
+
/** True from `ngOnInit` until the `<img>` `(load)` event fires (or an error happens). */
|
|
142
|
+
isLoading = true;
|
|
143
|
+
/** Set while the download is in flight, to drive the toolbar spinner. */
|
|
144
|
+
isDownloading = false;
|
|
145
|
+
/** User-facing error message; non-empty hides the image and shows the error block. */
|
|
146
|
+
errorMessage = '';
|
|
147
|
+
/** Click-to-toggle display mode. `'fit'` = contained in the panel, `'actual'` = 1:1 with scroll. */
|
|
148
|
+
displayMode = 'fit';
|
|
149
|
+
/** Native image dimensions, populated by the `(load)` event. Null until then. */
|
|
150
|
+
naturalWidth = null;
|
|
151
|
+
naturalHeight = null;
|
|
152
|
+
/**
|
|
153
|
+
* Object URL we lazily build from inline data so Download / Print have a real
|
|
154
|
+
* URL to anchor on. Tracked here so `ngOnDestroy` can release it via
|
|
155
|
+
* `URL.revokeObjectURL`. Null until the user clicks Download or Print.
|
|
156
|
+
*/
|
|
157
|
+
inlineObjectUrl = null;
|
|
158
|
+
constructor(fileService, cdr) {
|
|
159
|
+
super();
|
|
160
|
+
this.fileService = fileService;
|
|
161
|
+
this.cdr = cdr;
|
|
162
|
+
}
|
|
163
|
+
/** Opt in to the wrapper's "Display" tab — without this the panel only shows Details/JSON. */
|
|
164
|
+
get hasDisplayContent() {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/** Filename surfaced by the toolbar in the center of the bar. */
|
|
168
|
+
get toolbarFileName() {
|
|
169
|
+
return this.artifactVersion?.FileName || this.artifactVersion?.Name || 'image';
|
|
170
|
+
}
|
|
171
|
+
/** Accessible alt text — prefers a descriptive name over a raw filename. */
|
|
172
|
+
get imageAltText() {
|
|
173
|
+
return this.artifactVersion?.Name || this.artifactVersion?.FileName || 'Image artifact';
|
|
174
|
+
}
|
|
175
|
+
async ngOnInit() {
|
|
176
|
+
await this.resolveImageUrl();
|
|
177
|
+
}
|
|
178
|
+
ngOnDestroy() {
|
|
179
|
+
// Release any blob URL we built for Download / Print so the browser
|
|
180
|
+
// doesn't hold the raw bytes in memory after the panel closes.
|
|
181
|
+
if (this.inlineObjectUrl) {
|
|
182
|
+
URL.revokeObjectURL(this.inlineObjectUrl);
|
|
183
|
+
this.inlineObjectUrl = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ─── Loading ────────────────────────────────────────────────────────────────
|
|
187
|
+
/**
|
|
188
|
+
* Resolve the image URL based on the artifact version's `ContentMode`.
|
|
189
|
+
* Sets `imageUrl` (which the template binds to `<img src>`) or `errorMessage`.
|
|
190
|
+
* Loading state remains `true` until the browser fires `(load)` on the `<img>`.
|
|
191
|
+
*/
|
|
192
|
+
async resolveImageUrl() {
|
|
193
|
+
if (!this.artifactVersion?.ID) {
|
|
194
|
+
this.showError('No artifact version provided.');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Defensive MIME check — refuse to render non-image artifacts. If we get here
|
|
198
|
+
// it's a metadata bug somewhere (the ArtifactType registry routed something
|
|
199
|
+
// non-image to the image plugin), and silently showing a broken-image icon
|
|
200
|
+
// would mask the real issue.
|
|
201
|
+
const mime = (this.artifactVersion.MimeType ?? '').toLowerCase();
|
|
202
|
+
if (mime && !mime.startsWith('image/')) {
|
|
203
|
+
this.showError(`This viewer cannot display content of type "${this.artifactVersion.MimeType}".`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
if (this.artifactVersion.ContentMode === 'File') {
|
|
208
|
+
// File-backed: fetch a pre-auth URL from MJStorage via the shared service.
|
|
209
|
+
// ArtifactFileService caches the URL so reopening the same artifact is instant.
|
|
210
|
+
this.imageUrl = await this.fileService.getDownloadUrl(this.artifactVersion.ID);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Inline ('Text' mode): Content is a data URI ready to bind directly.
|
|
214
|
+
const content = this.artifactVersion.Content;
|
|
215
|
+
if (!content) {
|
|
216
|
+
this.showError('This artifact has no image content.');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.imageUrl = content;
|
|
220
|
+
}
|
|
221
|
+
this.cdr.markForCheck();
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
225
|
+
this.showError(`Could not load image: ${detail}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
onImageLoad(event) {
|
|
229
|
+
const img = event.target;
|
|
230
|
+
this.naturalWidth = img.naturalWidth || null;
|
|
231
|
+
this.naturalHeight = img.naturalHeight || null;
|
|
232
|
+
this.isLoading = false;
|
|
233
|
+
this.cdr.markForCheck();
|
|
234
|
+
}
|
|
235
|
+
onImageError() {
|
|
236
|
+
this.showError('The image could not be displayed. It may be corrupt or in an unsupported format.');
|
|
237
|
+
}
|
|
238
|
+
showError(message) {
|
|
239
|
+
this.isLoading = false;
|
|
240
|
+
this.errorMessage = message;
|
|
241
|
+
this.cdr.markForCheck();
|
|
242
|
+
}
|
|
243
|
+
// ─── User actions ───────────────────────────────────────────────────────────
|
|
244
|
+
toggleDisplayMode() {
|
|
245
|
+
// Don't toggle while loading or errored — the user has nothing to react to yet.
|
|
246
|
+
if (this.isLoading || this.errorMessage) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.displayMode = this.displayMode === 'fit' ? 'actual' : 'fit';
|
|
250
|
+
this.cdr.markForCheck();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Download the image to disk. Uses the inherited `triggerBrowserDownload`
|
|
254
|
+
* helper so the fetch → blob → object-URL → anchor pattern is shared across
|
|
255
|
+
* every file-backed viewer (PDF, XLSX, DOCX, and now Image).
|
|
256
|
+
*/
|
|
257
|
+
async onDownload() {
|
|
258
|
+
if (this.isDownloading || !this.imageUrl) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.isDownloading = true;
|
|
262
|
+
this.cdr.markForCheck();
|
|
263
|
+
try {
|
|
264
|
+
const sourceUrl = this.ensureDownloadableUrl();
|
|
265
|
+
const fileName = buildImageDownloadFileName(this.artifactVersion?.FileName ?? this.artifactVersion?.Name ?? null, this.artifactVersion?.MimeType ?? null);
|
|
266
|
+
await this.triggerBrowserDownload(sourceUrl, fileName);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.isDownloading = false;
|
|
270
|
+
this.cdr.markForCheck();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Open the image in a new browser tab so the user can use the browser's
|
|
275
|
+
* native print dialog. We deliberately don't manage a print pipeline
|
|
276
|
+
* ourselves — same approach as `PdfArtifactViewerComponent.onPrint()`.
|
|
277
|
+
*/
|
|
278
|
+
onPrint() {
|
|
279
|
+
if (!this.imageUrl) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const sourceUrl = this.ensureDownloadableUrl();
|
|
283
|
+
window.open(sourceUrl, '_blank');
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Returns a URL suitable for `<a download>` / `window.open` use.
|
|
287
|
+
* - File mode: `imageUrl` is already a pre-auth blob URL — use it directly.
|
|
288
|
+
* - Inline mode: `imageUrl` is a data URI; we lazily turn it into a blob URL
|
|
289
|
+
* (cached on the instance) for nicer download UX. Some browsers truncate
|
|
290
|
+
* long data URIs in the downloads tray; blob URLs preserve the filename.
|
|
291
|
+
*/
|
|
292
|
+
ensureDownloadableUrl() {
|
|
293
|
+
if (this.artifactVersion?.ContentMode === 'File') {
|
|
294
|
+
return this.imageUrl;
|
|
295
|
+
}
|
|
296
|
+
if (this.inlineObjectUrl) {
|
|
297
|
+
return this.inlineObjectUrl;
|
|
298
|
+
}
|
|
299
|
+
const mime = this.artifactVersion?.MimeType || 'application/octet-stream';
|
|
300
|
+
this.inlineObjectUrl = this.fileService.dataUrlToObjectUrl(this.imageUrl, mime);
|
|
301
|
+
return this.inlineObjectUrl;
|
|
302
|
+
}
|
|
303
|
+
// ─── Snapshot ───────────────────────────────────────────────────────────────
|
|
304
|
+
/**
|
|
305
|
+
* Required by the base class. Returns a structured summary of the artifact
|
|
306
|
+
* in its current display state — used by snapshot-consuming features
|
|
307
|
+
* (e.g. agent context capture, share / export flows).
|
|
308
|
+
*/
|
|
309
|
+
GetCurrentStateSnapshot() {
|
|
310
|
+
if (!this.artifactVersion) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const snap = new DataSnapshot();
|
|
314
|
+
snap.title = this.getDisplayTitle() ?? undefined;
|
|
315
|
+
const dimensionPart = this.naturalWidth && this.naturalHeight ? ` ${this.naturalWidth} × ${this.naturalHeight}` : '';
|
|
316
|
+
const mimeLabel = this.artifactVersion.MimeType ?? 'image';
|
|
317
|
+
snap.interpretation = `Image (${mimeLabel}${dimensionPart}).`;
|
|
318
|
+
snap.custom = {
|
|
319
|
+
mimeType: this.artifactVersion.MimeType ?? undefined,
|
|
320
|
+
fileName: this.artifactVersion.FileName ?? undefined,
|
|
321
|
+
contentMode: this.artifactVersion.ContentMode,
|
|
322
|
+
width: this.naturalWidth ?? undefined,
|
|
323
|
+
height: this.naturalHeight ?? undefined,
|
|
324
|
+
displayMode: this.displayMode,
|
|
325
|
+
};
|
|
326
|
+
return snap;
|
|
327
|
+
}
|
|
328
|
+
static ɵfac = function ImageArtifactViewerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ImageArtifactViewerComponent)(i0.ɵɵdirectiveInject(i1.ArtifactFileService), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
329
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ImageArtifactViewerComponent, selectors: [["mj-image-artifact-viewer"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 6, vars: 8, consts: [[1, "image-viewer", 3, "ngClass"], [3, "download", "print", "fileName", "isDownloading", "showPrint"], [1, "image-viewer__body"], [1, "image-viewer__state", "image-viewer__state--error"], [1, "image-viewer__dimensions"], [1, "fas", "fa-exclamation-triangle"], [1, "image-viewer__state"], [1, "image-viewer__img", 3, "image-viewer__img--actual", "image-viewer__img--hidden", "src", "alt", "title"], [1, "fas", "fa-spinner", "fa-spin"], [1, "image-viewer__img", 3, "load", "error", "click", "src", "alt", "title"]], template: function ImageArtifactViewerComponent_Template(rf, ctx) { if (rf & 1) {
|
|
330
|
+
i0.ɵɵelementStart(0, "div", 0)(1, "mj-file-artifact-toolbar", 1);
|
|
331
|
+
i0.ɵɵlistener("download", function ImageArtifactViewerComponent_Template_mj_file_artifact_toolbar_download_1_listener() { return ctx.onDownload(); })("print", function ImageArtifactViewerComponent_Template_mj_file_artifact_toolbar_print_1_listener() { return ctx.onPrint(); });
|
|
332
|
+
i0.ɵɵelementEnd();
|
|
333
|
+
i0.ɵɵelementStart(2, "div", 2);
|
|
334
|
+
i0.ɵɵconditionalCreate(3, ImageArtifactViewerComponent_Conditional_3_Template, 4, 1, "div", 3)(4, ImageArtifactViewerComponent_Conditional_4_Template, 2, 2);
|
|
335
|
+
i0.ɵɵelementEnd();
|
|
336
|
+
i0.ɵɵconditionalCreate(5, ImageArtifactViewerComponent_Conditional_5_Template, 2, 2, "div", 4);
|
|
337
|
+
i0.ɵɵelementEnd();
|
|
338
|
+
} if (rf & 2) {
|
|
339
|
+
i0.ɵɵproperty("ngClass", ctx.cssClass);
|
|
340
|
+
i0.ɵɵadvance();
|
|
341
|
+
i0.ɵɵproperty("fileName", ctx.toolbarFileName)("isDownloading", ctx.isDownloading)("showPrint", true);
|
|
342
|
+
i0.ɵɵadvance();
|
|
343
|
+
i0.ɵɵclassProp("image-viewer__body--actual", ctx.displayMode === "actual");
|
|
344
|
+
i0.ɵɵadvance();
|
|
345
|
+
i0.ɵɵconditional(ctx.errorMessage ? 3 : 4);
|
|
346
|
+
i0.ɵɵadvance(2);
|
|
347
|
+
i0.ɵɵconditional(ctx.naturalWidth && ctx.naturalHeight && !ctx.isLoading && !ctx.errorMessage ? 5 : -1);
|
|
348
|
+
} }, dependencies: [i2.NgClass, i3.FileArtifactToolbarComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n }\n\n .image-viewer[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface-sunken);\n position: relative;\n }\n\n .image-viewer__body[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n }\n\n \n\n\n .image-viewer__body--actual[_ngcontent-%COMP%] {\n align-items: flex-start;\n justify-content: flex-start;\n }\n\n .image-viewer__state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .image-viewer__state--error[_ngcontent-%COMP%] {\n color: var(--mj-status-error-text);\n }\n\n .image-viewer__img[_ngcontent-%COMP%] {\n \n\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n object-fit: contain;\n cursor: zoom-in;\n background: var(--mj-bg-surface);\n border-radius: 4px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n transition: opacity 0.18s ease-out;\n }\n\n \n\n\n .image-viewer__img--actual[_ngcontent-%COMP%] {\n max-width: none;\n max-height: none;\n width: auto;\n height: auto;\n cursor: zoom-out;\n }\n\n \n\n\n .image-viewer__img--hidden[_ngcontent-%COMP%] {\n opacity: 0;\n position: absolute;\n pointer-events: none;\n }\n\n .image-viewer__dimensions[_ngcontent-%COMP%] {\n position: absolute;\n bottom: 8px;\n right: 12px;\n padding: 2px 8px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 90%, transparent);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n \n\n }"] });
|
|
349
|
+
};
|
|
350
|
+
ImageArtifactViewerComponent = __decorate([
|
|
351
|
+
RegisterClass(BaseArtifactViewerPluginComponent, 'ImageArtifactViewerPlugin')
|
|
352
|
+
], ImageArtifactViewerComponent);
|
|
353
|
+
export { ImageArtifactViewerComponent };
|
|
354
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ImageArtifactViewerComponent, [{
|
|
355
|
+
type: Component,
|
|
356
|
+
args: [{ standalone: false, selector: 'mj-image-artifact-viewer', template: `
|
|
357
|
+
<div class="image-viewer" [ngClass]="cssClass">
|
|
358
|
+
<mj-file-artifact-toolbar
|
|
359
|
+
[fileName]="toolbarFileName"
|
|
360
|
+
[isDownloading]="isDownloading"
|
|
361
|
+
[showPrint]="true"
|
|
362
|
+
(download)="onDownload()"
|
|
363
|
+
(print)="onPrint()"
|
|
364
|
+
>
|
|
365
|
+
</mj-file-artifact-toolbar>
|
|
366
|
+
|
|
367
|
+
<div
|
|
368
|
+
class="image-viewer__body"
|
|
369
|
+
[class.image-viewer__body--actual]="displayMode === 'actual'"
|
|
370
|
+
>
|
|
371
|
+
@if (errorMessage) {
|
|
372
|
+
<div class="image-viewer__state image-viewer__state--error">
|
|
373
|
+
<i class="fas fa-exclamation-triangle"></i>
|
|
374
|
+
<span>{{ errorMessage }}</span>
|
|
375
|
+
</div>
|
|
376
|
+
} @else {
|
|
377
|
+
@if (isLoading) {
|
|
378
|
+
<div class="image-viewer__state">
|
|
379
|
+
<i class="fas fa-spinner fa-spin"></i>
|
|
380
|
+
<span>Loading image…</span>
|
|
381
|
+
</div>
|
|
382
|
+
}
|
|
383
|
+
@if (imageUrl) {
|
|
384
|
+
<!-- The img is rendered even while loading=true (with opacity 0)
|
|
385
|
+
so the browser starts decoding immediately. The (load) handler
|
|
386
|
+
flips isLoading=false once dimensions are known. -->
|
|
387
|
+
<img
|
|
388
|
+
class="image-viewer__img"
|
|
389
|
+
[class.image-viewer__img--actual]="displayMode === 'actual'"
|
|
390
|
+
[class.image-viewer__img--hidden]="isLoading"
|
|
391
|
+
[src]="imageUrl"
|
|
392
|
+
[alt]="imageAltText"
|
|
393
|
+
[title]="
|
|
394
|
+
displayMode === 'fit'
|
|
395
|
+
? 'Click to view at actual size'
|
|
396
|
+
: 'Click to fit to window'
|
|
397
|
+
"
|
|
398
|
+
(load)="onImageLoad($event)"
|
|
399
|
+
(error)="onImageError()"
|
|
400
|
+
(click)="toggleDisplayMode()"
|
|
401
|
+
/>
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
@if (naturalWidth && naturalHeight && !isLoading && !errorMessage) {
|
|
407
|
+
<div class="image-viewer__dimensions">{{ naturalWidth }} × {{ naturalHeight }}</div>
|
|
408
|
+
}
|
|
409
|
+
</div>
|
|
410
|
+
`, styles: ["\n :host {\n display: block;\n height: 100%;\n }\n\n .image-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface-sunken);\n position: relative;\n }\n\n .image-viewer__body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n }\n\n /* \"Actual size\" mode: allow scrolling from the top-left so large images\n don't get re-centered as the user pans. The browser handles overflow. */\n .image-viewer__body--actual {\n align-items: flex-start;\n justify-content: flex-start;\n }\n\n .image-viewer__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .image-viewer__state--error {\n color: var(--mj-status-error-text);\n }\n\n .image-viewer__img {\n /* Fit mode (default): never exceeds the panel, preserves aspect ratio. */\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n object-fit: contain;\n cursor: zoom-in;\n background: var(--mj-bg-surface);\n border-radius: 4px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n transition: opacity 0.18s ease-out;\n }\n\n /* Actual-size mode: render at the image's intrinsic dimensions so the body\n scroll-area handles anything larger than the panel. */\n .image-viewer__img--actual {\n max-width: none;\n max-height: none;\n width: auto;\n height: auto;\n cursor: zoom-out;\n }\n\n /* Pre-load: keep the <img> in the DOM (so the browser decodes it) but\n invisible and out of layout flow so the spinner sits centered. */\n .image-viewer__img--hidden {\n opacity: 0;\n position: absolute;\n pointer-events: none;\n }\n\n .image-viewer__dimensions {\n position: absolute;\n bottom: 8px;\n right: 12px;\n padding: 2px 8px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 90%, transparent);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n /* Don't intercept clicks meant for the image-toggle. */\n }\n "] }]
|
|
411
|
+
}], () => [{ type: i1.ArtifactFileService }, { type: i0.ChangeDetectorRef }], null); })();
|
|
412
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ImageArtifactViewerComponent, { className: "ImageArtifactViewerComponent", filePath: "src/lib/components/plugins/image-artifact-viewer.component.ts", lineNumber: 238 }); })();
|
|
413
|
+
//# sourceMappingURL=image-artifact-viewer.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-artifact-viewer.component.js","sourceRoot":"","sources":["../../../../src/lib/components/plugins/image-artifact-viewer.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';\nimport { DataSnapshot } from '@memberjunction/core';\nimport { RegisterClass } from '@memberjunction/global';\nimport { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';\nimport { ArtifactFileService } from '../../services/artifact-file.service';\n\n/**\n * Lookup table for synthesizing a download filename when the artifact version\n * has no `FileName` set. Covers the MIME types declared by the seed `Image`\n * artifact type (`image/*`); anything not listed falls back to `img`.\n *\n * Exported so the unit test can assert the mapping without instantiating an\n * Angular component (the package's vitest config uses the `node` environment).\n */\nexport const IMAGE_MIME_EXTENSION_MAP: Readonly<Record<string, string>> = Object.freeze({\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n 'image/bmp': 'bmp',\n 'image/tiff': 'tif',\n 'image/avif': 'avif',\n 'image/heic': 'heic',\n 'image/heif': 'heif',\n});\n\n/**\n * Pure helper that synthesizes a safe download filename from an artifact's\n * name + MIME type. Pulled out as a top-level function (not a class method) so\n * the vitest `node`-environment unit test can exercise it directly.\n *\n * - Strips characters illegal on common filesystems (anything not\n * `[A-Za-z0-9._-]`) and replaces them with underscores.\n * - Picks the extension from {@link IMAGE_MIME_EXTENSION_MAP}, defaulting to\n * `img` if the MIME type is missing or unrecognized.\n * - Falls back to the literal `image` when no name is supplied.\n *\n * @param name Artifact name (e.g. `\"Generated image 1\"`). Optional.\n * @param mime MIME type from the artifact version (e.g. `\"image/jpeg\"`). Optional.\n * @returns Sanitized filename with extension (e.g. `\"Generated_image_1.jpg\"`).\n */\nexport function buildImageDownloadFileName(name: string | null | undefined, mime: string | null | undefined): string {\n // `??` falls back on null/undefined but NOT empty / whitespace-only names —\n // those should also use the default, so trim first and check for length.\n const trimmed = name?.trim();\n const baseName = trimmed && trimmed.length > 0 ? trimmed : 'image';\n const safeBase = baseName.replace(/[^A-Za-z0-9._-]/g, '_');\n const normalizedMime = (mime ?? '').toLowerCase();\n const extension = IMAGE_MIME_EXTENSION_MAP[normalizedMime] ?? 'img';\n // If the name already ends with the right extension, don't append it again\n // (e.g. metadata round-tripped through a file upload).\n return safeBase.toLowerCase().endsWith(`.${extension}`) ? safeBase : `${safeBase}.${extension}`;\n}\n\n/**\n * Viewer plugin for raster image artifact versions (`image/png`, `image/jpeg`,\n * `image/webp`, etc.). Vector SVG is handled by `SvgArtifactViewerPlugin`.\n *\n * Storage modes both supported (same contract as `PdfArtifactViewerPlugin`):\n * - `ContentMode === 'Text'` — `artifactVersion.Content` is a `data:image/<mime>;base64,…` URI.\n * We bind it directly to `<img [src]>`; the browser decodes inline.\n * - `ContentMode === 'File'` — bytes live in MJStorage. We fetch a\n * pre-authenticated URL via `ArtifactFileService.getDownloadUrl()` and\n * bind that to `<img [src]>`.\n *\n * UX highlights:\n * - Click the image to toggle **fit** (default — `object-fit: contain` inside\n * the panel) ↔ **actual** size (1:1 pixels, scrolls within the panel body).\n * - Toolbar provides Download (reuses base class `triggerBrowserDownload`) and\n * Print (opens in a new tab so the browser's native print dialog handles it).\n * - Loading and error states match the PDF plugin's vocabulary, so behavior\n * across all file-style viewers is consistent.\n * - Image dimensions are surfaced as a small chip once the browser has decoded\n * the image (`naturalWidth × naturalHeight`).\n *\n * Design tokens are used for every color so the plugin adapts to light / dark\n * themes — see root `CLAUDE.md` \"Design Token System\".\n *\n * @see PdfArtifactViewerComponent — the dual-storage reference implementation.\n * @see plans/artifact-attachment-unification.md — the broader artifact-as-canonical-storage design.\n * @see docs/chat-ui-image-display-bugs.md — context for why this plugin exists (Bug B).\n */\n@Component({\n standalone: false,\n selector: 'mj-image-artifact-viewer',\n template: `\n <div class=\"image-viewer\" [ngClass]=\"cssClass\">\n <mj-file-artifact-toolbar\n [fileName]=\"toolbarFileName\"\n [isDownloading]=\"isDownloading\"\n [showPrint]=\"true\"\n (download)=\"onDownload()\"\n (print)=\"onPrint()\"\n >\n </mj-file-artifact-toolbar>\n\n <div\n class=\"image-viewer__body\"\n [class.image-viewer__body--actual]=\"displayMode === 'actual'\"\n >\n @if (errorMessage) {\n <div class=\"image-viewer__state image-viewer__state--error\">\n <i class=\"fas fa-exclamation-triangle\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n } @else {\n @if (isLoading) {\n <div class=\"image-viewer__state\">\n <i class=\"fas fa-spinner fa-spin\"></i>\n <span>Loading image…</span>\n </div>\n }\n @if (imageUrl) {\n <!-- The img is rendered even while loading=true (with opacity 0)\n so the browser starts decoding immediately. The (load) handler\n flips isLoading=false once dimensions are known. -->\n <img\n class=\"image-viewer__img\"\n [class.image-viewer__img--actual]=\"displayMode === 'actual'\"\n [class.image-viewer__img--hidden]=\"isLoading\"\n [src]=\"imageUrl\"\n [alt]=\"imageAltText\"\n [title]=\"\n displayMode === 'fit'\n ? 'Click to view at actual size'\n : 'Click to fit to window'\n \"\n (load)=\"onImageLoad($event)\"\n (error)=\"onImageError()\"\n (click)=\"toggleDisplayMode()\"\n />\n }\n }\n </div>\n\n @if (naturalWidth && naturalHeight && !isLoading && !errorMessage) {\n <div class=\"image-viewer__dimensions\">{{ naturalWidth }} × {{ naturalHeight }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n height: 100%;\n }\n\n .image-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface-sunken);\n position: relative;\n }\n\n .image-viewer__body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n }\n\n /* \"Actual size\" mode: allow scrolling from the top-left so large images\n don't get re-centered as the user pans. The browser handles overflow. */\n .image-viewer__body--actual {\n align-items: flex-start;\n justify-content: flex-start;\n }\n\n .image-viewer__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .image-viewer__state--error {\n color: var(--mj-status-error-text);\n }\n\n .image-viewer__img {\n /* Fit mode (default): never exceeds the panel, preserves aspect ratio. */\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n object-fit: contain;\n cursor: zoom-in;\n background: var(--mj-bg-surface);\n border-radius: 4px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n transition: opacity 0.18s ease-out;\n }\n\n /* Actual-size mode: render at the image's intrinsic dimensions so the body\n scroll-area handles anything larger than the panel. */\n .image-viewer__img--actual {\n max-width: none;\n max-height: none;\n width: auto;\n height: auto;\n cursor: zoom-out;\n }\n\n /* Pre-load: keep the <img> in the DOM (so the browser decodes it) but\n invisible and out of layout flow so the spinner sits centered. */\n .image-viewer__img--hidden {\n opacity: 0;\n position: absolute;\n pointer-events: none;\n }\n\n .image-viewer__dimensions {\n position: absolute;\n bottom: 8px;\n right: 12px;\n padding: 2px 8px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 90%, transparent);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n /* Don't intercept clicks meant for the image-toggle. */\n }\n `,\n ],\n})\n@RegisterClass(BaseArtifactViewerPluginComponent, 'ImageArtifactViewerPlugin')\nexport class ImageArtifactViewerComponent extends BaseArtifactViewerPluginComponent implements OnInit, OnDestroy {\n /** Current resolved URL to bind to the `<img src>` — either a data URI (inline) or a pre-auth URL (file). */\n public imageUrl = '';\n\n /** True from `ngOnInit` until the `<img>` `(load)` event fires (or an error happens). */\n public isLoading = true;\n\n /** Set while the download is in flight, to drive the toolbar spinner. */\n public isDownloading = false;\n\n /** User-facing error message; non-empty hides the image and shows the error block. */\n public errorMessage = '';\n\n /** Click-to-toggle display mode. `'fit'` = contained in the panel, `'actual'` = 1:1 with scroll. */\n public displayMode: 'fit' | 'actual' = 'fit';\n\n /** Native image dimensions, populated by the `(load)` event. Null until then. */\n public naturalWidth: number | null = null;\n public naturalHeight: number | null = null;\n\n /**\n * Object URL we lazily build from inline data so Download / Print have a real\n * URL to anchor on. Tracked here so `ngOnDestroy` can release it via\n * `URL.revokeObjectURL`. Null until the user clicks Download or Print.\n */\n private inlineObjectUrl: string | null = null;\n\n constructor(\n private fileService: ArtifactFileService,\n private cdr: ChangeDetectorRef,\n ) {\n super();\n }\n\n /** Opt in to the wrapper's \"Display\" tab — without this the panel only shows Details/JSON. */\n public override get hasDisplayContent(): boolean {\n return true;\n }\n\n /** Filename surfaced by the toolbar in the center of the bar. */\n public get toolbarFileName(): string {\n return this.artifactVersion?.FileName || this.artifactVersion?.Name || 'image';\n }\n\n /** Accessible alt text — prefers a descriptive name over a raw filename. */\n public get imageAltText(): string {\n return this.artifactVersion?.Name || this.artifactVersion?.FileName || 'Image artifact';\n }\n\n async ngOnInit(): Promise<void> {\n await this.resolveImageUrl();\n }\n\n ngOnDestroy(): void {\n // Release any blob URL we built for Download / Print so the browser\n // doesn't hold the raw bytes in memory after the panel closes.\n if (this.inlineObjectUrl) {\n URL.revokeObjectURL(this.inlineObjectUrl);\n this.inlineObjectUrl = null;\n }\n }\n\n // ─── Loading ────────────────────────────────────────────────────────────────\n\n /**\n * Resolve the image URL based on the artifact version's `ContentMode`.\n * Sets `imageUrl` (which the template binds to `<img src>`) or `errorMessage`.\n * Loading state remains `true` until the browser fires `(load)` on the `<img>`.\n */\n private async resolveImageUrl(): Promise<void> {\n if (!this.artifactVersion?.ID) {\n this.showError('No artifact version provided.');\n return;\n }\n\n // Defensive MIME check — refuse to render non-image artifacts. If we get here\n // it's a metadata bug somewhere (the ArtifactType registry routed something\n // non-image to the image plugin), and silently showing a broken-image icon\n // would mask the real issue.\n const mime = (this.artifactVersion.MimeType ?? '').toLowerCase();\n if (mime && !mime.startsWith('image/')) {\n this.showError(`This viewer cannot display content of type \"${this.artifactVersion.MimeType}\".`);\n return;\n }\n\n try {\n if (this.artifactVersion.ContentMode === 'File') {\n // File-backed: fetch a pre-auth URL from MJStorage via the shared service.\n // ArtifactFileService caches the URL so reopening the same artifact is instant.\n this.imageUrl = await this.fileService.getDownloadUrl(this.artifactVersion.ID);\n } else {\n // Inline ('Text' mode): Content is a data URI ready to bind directly.\n const content = this.artifactVersion.Content;\n if (!content) {\n this.showError('This artifact has no image content.');\n return;\n }\n this.imageUrl = content;\n }\n this.cdr.markForCheck();\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n this.showError(`Could not load image: ${detail}`);\n }\n }\n\n public onImageLoad(event: Event): void {\n const img = event.target as HTMLImageElement;\n this.naturalWidth = img.naturalWidth || null;\n this.naturalHeight = img.naturalHeight || null;\n this.isLoading = false;\n this.cdr.markForCheck();\n }\n\n public onImageError(): void {\n this.showError('The image could not be displayed. It may be corrupt or in an unsupported format.');\n }\n\n private showError(message: string): void {\n this.isLoading = false;\n this.errorMessage = message;\n this.cdr.markForCheck();\n }\n\n // ─── User actions ───────────────────────────────────────────────────────────\n\n public toggleDisplayMode(): void {\n // Don't toggle while loading or errored — the user has nothing to react to yet.\n if (this.isLoading || this.errorMessage) {\n return;\n }\n this.displayMode = this.displayMode === 'fit' ? 'actual' : 'fit';\n this.cdr.markForCheck();\n }\n\n /**\n * Download the image to disk. Uses the inherited `triggerBrowserDownload`\n * helper so the fetch → blob → object-URL → anchor pattern is shared across\n * every file-backed viewer (PDF, XLSX, DOCX, and now Image).\n */\n public async onDownload(): Promise<void> {\n if (this.isDownloading || !this.imageUrl) {\n return;\n }\n this.isDownloading = true;\n this.cdr.markForCheck();\n try {\n const sourceUrl = this.ensureDownloadableUrl();\n const fileName = buildImageDownloadFileName(\n this.artifactVersion?.FileName ?? this.artifactVersion?.Name ?? null,\n this.artifactVersion?.MimeType ?? null,\n );\n await this.triggerBrowserDownload(sourceUrl, fileName);\n } finally {\n this.isDownloading = false;\n this.cdr.markForCheck();\n }\n }\n\n /**\n * Open the image in a new browser tab so the user can use the browser's\n * native print dialog. We deliberately don't manage a print pipeline\n * ourselves — same approach as `PdfArtifactViewerComponent.onPrint()`.\n */\n public onPrint(): void {\n if (!this.imageUrl) {\n return;\n }\n const sourceUrl = this.ensureDownloadableUrl();\n window.open(sourceUrl, '_blank');\n }\n\n /**\n * Returns a URL suitable for `<a download>` / `window.open` use.\n * - File mode: `imageUrl` is already a pre-auth blob URL — use it directly.\n * - Inline mode: `imageUrl` is a data URI; we lazily turn it into a blob URL\n * (cached on the instance) for nicer download UX. Some browsers truncate\n * long data URIs in the downloads tray; blob URLs preserve the filename.\n */\n private ensureDownloadableUrl(): string {\n if (this.artifactVersion?.ContentMode === 'File') {\n return this.imageUrl;\n }\n if (this.inlineObjectUrl) {\n return this.inlineObjectUrl;\n }\n const mime = this.artifactVersion?.MimeType || 'application/octet-stream';\n this.inlineObjectUrl = this.fileService.dataUrlToObjectUrl(this.imageUrl, mime);\n return this.inlineObjectUrl;\n }\n\n // ─── Snapshot ───────────────────────────────────────────────────────────────\n\n /**\n * Required by the base class. Returns a structured summary of the artifact\n * in its current display state — used by snapshot-consuming features\n * (e.g. agent context capture, share / export flows).\n */\n public override GetCurrentStateSnapshot(): DataSnapshot | null {\n if (!this.artifactVersion) {\n return null;\n }\n const snap = new DataSnapshot();\n snap.title = this.getDisplayTitle() ?? undefined;\n\n const dimensionPart =\n this.naturalWidth && this.naturalHeight ? ` ${this.naturalWidth} × ${this.naturalHeight}` : '';\n const mimeLabel = this.artifactVersion.MimeType ?? 'image';\n snap.interpretation = `Image (${mimeLabel}${dimensionPart}).`;\n\n snap.custom = {\n mimeType: this.artifactVersion.MimeType ?? undefined,\n fileName: this.artifactVersion.FileName ?? undefined,\n contentMode: this.artifactVersion.ContentMode,\n width: this.naturalWidth ?? undefined,\n height: this.naturalHeight ?? undefined,\n displayMode: this.displayMode,\n };\n return snap;\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-query-dialog.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/plugins/save-query-dialog.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAAsC,iBAAiB,EAAgB,MAAM,eAAe,CAAC;AAC5I,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAY,YAAY,EAA4B,MAAM,sBAAsB,CAAC;AAGxF,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAwca,uBAAwB,SAAQ,oBAAoB;IAuCnD,OAAO,CAAC,GAAG;IAtCd,SAAS,SAAM;IACf,gBAAgB,SAAM;IACtB,GAAG,SAAM;IACT,IAAI,SAAM;IAET,KAAK,gCAAuC;IAC5C,SAAS,qBAA4B;IAEpB,YAAY,EAAG,qBAAqB,CAAC;IAGzD,IAAI,SAAM;IACV,WAAW,SAAM;IACjB,QAAQ,UAAS;IACjB,YAAY,SAAM;IAClB,SAAS,UAAS;IAGzB,OAAO,CAAC,kBAAkB,CAAuB;IAC1C,mBAAmB,EAAE,YAAY,GAAG,IAAI,CAAQ;IAGhD,kBAAkB,UAAS;IAC3B,wBAAwB,UAAS;IACjC,eAAe,SAAM;IACrB,mBAAmB,SAAM;IAChC,OAAO,CAAC,mBAAmB,CAAuB;IAElD,+CAA+C;IACxC,oBAAoB,EAAE,gBAAgB,CAO3C;gBAEkB,GAAG,EAAE,iBAAiB;IAG1C,IAAW,OAAO,IAAI,OAAO,CAE5B;IAED,QAAQ,IAAI,IAAI;IAaT,WAAW,IAAI,IAAI;IAYnB,iBAAiB,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI;IAchE,mBAAmB,IAAI,IAAI;IAQ3B,oBAAoB,IAAI,IAAI;IAQ5B,0BAA0B,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI;IAUnE,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"save-query-dialog.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/plugins/save-query-dialog.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAAsC,iBAAiB,EAAgB,MAAM,eAAe,CAAC;AAC5I,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAY,YAAY,EAA4B,MAAM,sBAAsB,CAAC;AAGxF,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAwca,uBAAwB,SAAQ,oBAAoB;IAuCnD,OAAO,CAAC,GAAG;IAtCd,SAAS,SAAM;IACf,gBAAgB,SAAM;IACtB,GAAG,SAAM;IACT,IAAI,SAAM;IAET,KAAK,gCAAuC;IAC5C,SAAS,qBAA4B;IAEpB,YAAY,EAAG,qBAAqB,CAAC;IAGzD,IAAI,SAAM;IACV,WAAW,SAAM;IACjB,QAAQ,UAAS;IACjB,YAAY,SAAM;IAClB,SAAS,UAAS;IAGzB,OAAO,CAAC,kBAAkB,CAAuB;IAC1C,mBAAmB,EAAE,YAAY,GAAG,IAAI,CAAQ;IAGhD,kBAAkB,UAAS;IAC3B,wBAAwB,UAAS;IACjC,eAAe,SAAM;IACrB,mBAAmB,SAAM;IAChC,OAAO,CAAC,mBAAmB,CAAuB;IAElD,+CAA+C;IACxC,oBAAoB,EAAE,gBAAgB,CAO3C;gBAEkB,GAAG,EAAE,iBAAiB;IAG1C,IAAW,OAAO,IAAI,OAAO,CAE5B;IAED,QAAQ,IAAI,IAAI;IAaT,WAAW,IAAI,IAAI;IAYnB,iBAAiB,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI;IAchE,mBAAmB,IAAI,IAAI;IAQ3B,oBAAoB,IAAI,IAAI;IAQ5B,0BAA0B,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI;IAUnE,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IA0D/B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAuD7B,QAAQ,IAAI,IAAI;IAWvB,OAAO,CAAC,qBAAqB;yCA3OlB,uBAAuB;2CAAvB,uBAAuB;CAkPnC"}
|