@foliokit/cms-admin-ui 0.0.0 → 0.1.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.
Files changed (50) hide show
  1. package/esm2022/index.js +14 -0
  2. package/esm2022/index.js.map +1 -1
  3. package/esm2022/lib/cms-admin-ui/cms-admin-ui.js +3 -3
  4. package/esm2022/lib/page-editor/about-editor-form.component.js +84 -0
  5. package/esm2022/lib/page-editor/about-editor-form.component.js.map +1 -0
  6. package/esm2022/lib/page-editor/links-editor-form.component.js +474 -0
  7. package/esm2022/lib/page-editor/links-editor-form.component.js.map +1 -0
  8. package/esm2022/lib/page-editor/page-editor-hero-image.component.js +216 -0
  9. package/esm2022/lib/page-editor/page-editor-hero-image.component.js.map +1 -0
  10. package/esm2022/lib/page-editor/page-editor.store.js +198 -0
  11. package/esm2022/lib/page-editor/page-editor.store.js.map +1 -0
  12. package/esm2022/lib/post-editor/post-editor-cover-image.component.js +251 -0
  13. package/esm2022/lib/post-editor/post-editor-cover-image.component.js.map +1 -0
  14. package/esm2022/lib/post-editor/post-editor-embedded-media-item.component.js +99 -0
  15. package/esm2022/lib/post-editor/post-editor-embedded-media-item.component.js.map +1 -0
  16. package/esm2022/lib/post-editor/post-editor-embedded-media.component.js +173 -0
  17. package/esm2022/lib/post-editor/post-editor-embedded-media.component.js.map +1 -0
  18. package/esm2022/lib/post-editor/post-editor-media-tab.component.js +23 -0
  19. package/esm2022/lib/post-editor/post-editor-media-tab.component.js.map +1 -0
  20. package/esm2022/lib/post-editor/post-editor.store.js +189 -0
  21. package/esm2022/lib/post-editor/post-editor.store.js.map +1 -0
  22. package/esm2022/lib/posts-list/posts-board.component.js +66 -0
  23. package/esm2022/lib/posts-list/posts-board.component.js.map +1 -0
  24. package/esm2022/lib/posts-list/posts-draft-column.component.js +71 -0
  25. package/esm2022/lib/posts-list/posts-draft-column.component.js.map +1 -0
  26. package/esm2022/lib/posts-list/posts-list.component.js +79 -0
  27. package/esm2022/lib/posts-list/posts-list.component.js.map +1 -0
  28. package/esm2022/lib/posts-list/posts-list.store.js +43 -0
  29. package/esm2022/lib/posts-list/posts-list.store.js.map +1 -0
  30. package/esm2022/lib/posts-list/posts-published-column.component.js +129 -0
  31. package/esm2022/lib/posts-list/posts-published-column.component.js.map +1 -0
  32. package/esm2022/lib/posts-list/posts-queue-column.component.js +112 -0
  33. package/esm2022/lib/posts-list/posts-queue-column.component.js.map +1 -0
  34. package/index.d.ts +14 -0
  35. package/lib/page-editor/about-editor-form.component.d.ts +32 -0
  36. package/lib/page-editor/links-editor-form.component.d.ts +52 -0
  37. package/lib/page-editor/page-editor-hero-image.component.d.ts +51 -0
  38. package/lib/page-editor/page-editor.store.d.ts +37 -0
  39. package/lib/post-editor/post-editor-cover-image.component.d.ts +50 -0
  40. package/lib/post-editor/post-editor-embedded-media-item.component.d.ts +37 -0
  41. package/lib/post-editor/post-editor-embedded-media.component.d.ts +43 -0
  42. package/lib/post-editor/post-editor-media-tab.component.d.ts +5 -0
  43. package/lib/post-editor/post-editor.store.d.ts +37 -0
  44. package/lib/posts-list/posts-board.component.d.ts +24 -0
  45. package/lib/posts-list/posts-draft-column.component.d.ts +8 -0
  46. package/lib/posts-list/posts-list.component.d.ts +28 -0
  47. package/lib/posts-list/posts-list.store.d.ts +24 -0
  48. package/lib/posts-list/posts-published-column.component.d.ts +10 -0
  49. package/lib/posts-list/posts-queue-column.component.d.ts +14 -0
  50. package/package.json +1 -1
@@ -0,0 +1,251 @@
1
+ import { ChangeDetectionStrategy, Component, PLATFORM_ID, ViewChild, inject, signal, } from '@angular/core';
2
+ import { isPlatformBrowser } from '@angular/common';
3
+ import { MatButtonModule } from '@angular/material/button';
4
+ import { MatIconModule } from '@angular/material/icon';
5
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
6
+ import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage';
7
+ import { FIREBASE_STORAGE, PostService } from '@foliokit/cms-core';
8
+ import { PostEditorStore } from './post-editor.store';
9
+ import * as i0 from "@angular/core";
10
+ import * as i1 from "@angular/material/button";
11
+ import * as i2 from "@angular/material/icon";
12
+ import * as i3 from "@angular/material/progress-bar";
13
+ export class PostEditorCoverImageComponent {
14
+ fileInput;
15
+ store = inject(PostEditorStore);
16
+ storage = inject(FIREBASE_STORAGE);
17
+ postService = inject(PostService);
18
+ platformId = inject(PLATFORM_ID);
19
+ isBrowser = isPlatformBrowser(this.platformId);
20
+ uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
21
+ uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : /* istanbul ignore next */ []));
22
+ uploadError = signal(null, ...(ngDevMode ? [{ debugName: "uploadError" }] : /* istanbul ignore next */ []));
23
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : /* istanbul ignore next */ []));
24
+ storagePath = signal(null, ...(ngDevMode ? [{ debugName: "storagePath" }] : /* istanbul ignore next */ []));
25
+ onDragOver(event) {
26
+ event.preventDefault();
27
+ this.isDragOver.set(true);
28
+ }
29
+ onDragLeave() {
30
+ this.isDragOver.set(false);
31
+ }
32
+ onDrop(event) {
33
+ event.preventDefault();
34
+ this.isDragOver.set(false);
35
+ const files = event.dataTransfer?.files;
36
+ if (files?.length) {
37
+ this.upload(files[0]);
38
+ }
39
+ }
40
+ onFileSelected(files) {
41
+ if (!files?.length)
42
+ return;
43
+ this.upload(files[0]);
44
+ // Reset so the same file can be re-selected after a delete
45
+ if (this.fileInput?.nativeElement) {
46
+ this.fileInput.nativeElement.value = '';
47
+ }
48
+ }
49
+ onDeleteCover() {
50
+ if (!window.confirm('Remove cover image?'))
51
+ return;
52
+ const path = this.storagePath();
53
+ if (path) {
54
+ this.postService.deleteStorageFile(path).subscribe({
55
+ next: () => this.clearCover(),
56
+ error: () => this.clearCover(),
57
+ });
58
+ }
59
+ else {
60
+ this.clearCover();
61
+ }
62
+ }
63
+ clearCover() {
64
+ this.store.updateField('thumbnailUrl', '');
65
+ this.store.updateField('thumbnailAlt', '');
66
+ this.storagePath.set(null);
67
+ this.uploadProgress.set(0);
68
+ }
69
+ upload(file) {
70
+ const previousPath = this.storagePath();
71
+ const postId = this.store.post()?.id || this.store.tempPostId();
72
+ const storagePath = `posts/${postId}/cover/${file.name}`;
73
+ if (previousPath) {
74
+ this.postService.deleteStorageFile(previousPath).subscribe();
75
+ }
76
+ const fileRef = ref(this.storage, storagePath);
77
+ this.uploading.set(true);
78
+ this.uploadProgress.set(0);
79
+ this.uploadError.set(null);
80
+ const task = uploadBytesResumable(fileRef, file);
81
+ task.on('state_changed', (snapshot) => {
82
+ this.uploadProgress.set(Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100));
83
+ }, (error) => {
84
+ this.uploading.set(false);
85
+ this.uploadError.set(error.message);
86
+ }, () => {
87
+ getDownloadURL(task.snapshot.ref).then((downloadUrl) => {
88
+ this.store.updateField('thumbnailUrl', downloadUrl);
89
+ this.store.updateField('thumbnailAlt', file.name);
90
+ this.storagePath.set(storagePath);
91
+ this.uploading.set(false);
92
+ });
93
+ });
94
+ }
95
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorCoverImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
96
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: PostEditorCoverImageComponent, isStandalone: true, selector: "folio-post-editor-cover-image", viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: `
97
+ <div class="flex flex-col gap-2">
98
+ <span class="text-sm font-semibold">Cover Image</span>
99
+
100
+ @if (store.post()?.thumbnailUrl; as url) {
101
+ <!-- Filled state -->
102
+ <div class="image-wrapper rounded-lg overflow-hidden">
103
+ <div class="aspect-video w-full relative">
104
+ <img
105
+ [src]="url"
106
+ [alt]="store.post()?.thumbnailAlt ?? ''"
107
+ class="w-full h-full object-cover"
108
+ />
109
+ <!-- Hover overlay -->
110
+ <div
111
+ class="hover-overlay absolute inset-0 flex items-center justify-center gap-3"
112
+ style="background: rgba(0,0,0,0.45)"
113
+ >
114
+ <button
115
+ mat-icon-button
116
+ style="color: white"
117
+ title="Replace image"
118
+ (click)="isBrowser && fileInput.click()"
119
+ >
120
+ <mat-icon>swap_horiz</mat-icon>
121
+ </button>
122
+ <button
123
+ mat-icon-button
124
+ style="color: white"
125
+ title="Delete image"
126
+ (click)="onDeleteCover()"
127
+ >
128
+ <mat-icon>delete</mat-icon>
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ } @else {
134
+ <!-- Empty state -->
135
+ <div
136
+ class="drop-zone w-full flex flex-col items-center justify-center gap-2 py-10 cursor-pointer select-none"
137
+ [class.drag-over]="isDragOver()"
138
+ (click)="isBrowser && fileInput.click()"
139
+ (dragover)="isBrowser && onDragOver($event)"
140
+ (dragleave)="isBrowser && onDragLeave()"
141
+ (drop)="isBrowser && onDrop($event)"
142
+ >
143
+ <mat-icon class="opacity-40" style="font-size: 2.5rem; width: 2.5rem; height: 2.5rem">
144
+ upload_file
145
+ </mat-icon>
146
+ <span class="text-sm opacity-50">Upload cover image</span>
147
+ </div>
148
+ }
149
+
150
+ <!-- Hidden file input -->
151
+ <input
152
+ #fileInput
153
+ type="file"
154
+ accept="image/*"
155
+ class="hidden"
156
+ (change)="onFileSelected($any($event.target).files)"
157
+ />
158
+
159
+ <!-- Progress bar -->
160
+ @if (uploading()) {
161
+ <mat-progress-bar mode="determinate" [value]="uploadProgress()" />
162
+ }
163
+
164
+ <!-- Error message -->
165
+ @if (uploadError()) {
166
+ <p class="text-sm text-red-500">{{ uploadError() }}</p>
167
+ }
168
+ </div>
169
+ `, isInline: true, styles: [":host{display:block}.drop-zone{border:2px dashed color-mix(in srgb,currentColor 25%,transparent);border-radius:8px;transition:border-color .15s,background .15s}.drop-zone.drag-over{border-color:var(--mat-sys-primary);background:color-mix(in srgb,var(--mat-sys-primary) 8%,transparent)}.image-wrapper{position:relative}.image-wrapper:hover .hover-overlay{opacity:1}.hover-overlay{opacity:0;transition:opacity .15s}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i3.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
170
+ }
171
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorCoverImageComponent, decorators: [{
172
+ type: Component,
173
+ args: [{ selector: 'folio-post-editor-cover-image', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatButtonModule, MatIconModule, MatProgressBarModule], template: `
174
+ <div class="flex flex-col gap-2">
175
+ <span class="text-sm font-semibold">Cover Image</span>
176
+
177
+ @if (store.post()?.thumbnailUrl; as url) {
178
+ <!-- Filled state -->
179
+ <div class="image-wrapper rounded-lg overflow-hidden">
180
+ <div class="aspect-video w-full relative">
181
+ <img
182
+ [src]="url"
183
+ [alt]="store.post()?.thumbnailAlt ?? ''"
184
+ class="w-full h-full object-cover"
185
+ />
186
+ <!-- Hover overlay -->
187
+ <div
188
+ class="hover-overlay absolute inset-0 flex items-center justify-center gap-3"
189
+ style="background: rgba(0,0,0,0.45)"
190
+ >
191
+ <button
192
+ mat-icon-button
193
+ style="color: white"
194
+ title="Replace image"
195
+ (click)="isBrowser && fileInput.click()"
196
+ >
197
+ <mat-icon>swap_horiz</mat-icon>
198
+ </button>
199
+ <button
200
+ mat-icon-button
201
+ style="color: white"
202
+ title="Delete image"
203
+ (click)="onDeleteCover()"
204
+ >
205
+ <mat-icon>delete</mat-icon>
206
+ </button>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ } @else {
211
+ <!-- Empty state -->
212
+ <div
213
+ class="drop-zone w-full flex flex-col items-center justify-center gap-2 py-10 cursor-pointer select-none"
214
+ [class.drag-over]="isDragOver()"
215
+ (click)="isBrowser && fileInput.click()"
216
+ (dragover)="isBrowser && onDragOver($event)"
217
+ (dragleave)="isBrowser && onDragLeave()"
218
+ (drop)="isBrowser && onDrop($event)"
219
+ >
220
+ <mat-icon class="opacity-40" style="font-size: 2.5rem; width: 2.5rem; height: 2.5rem">
221
+ upload_file
222
+ </mat-icon>
223
+ <span class="text-sm opacity-50">Upload cover image</span>
224
+ </div>
225
+ }
226
+
227
+ <!-- Hidden file input -->
228
+ <input
229
+ #fileInput
230
+ type="file"
231
+ accept="image/*"
232
+ class="hidden"
233
+ (change)="onFileSelected($any($event.target).files)"
234
+ />
235
+
236
+ <!-- Progress bar -->
237
+ @if (uploading()) {
238
+ <mat-progress-bar mode="determinate" [value]="uploadProgress()" />
239
+ }
240
+
241
+ <!-- Error message -->
242
+ @if (uploadError()) {
243
+ <p class="text-sm text-red-500">{{ uploadError() }}</p>
244
+ }
245
+ </div>
246
+ `, styles: [":host{display:block}.drop-zone{border:2px dashed color-mix(in srgb,currentColor 25%,transparent);border-radius:8px;transition:border-color .15s,background .15s}.drop-zone.drag-over{border-color:var(--mat-sys-primary);background:color-mix(in srgb,var(--mat-sys-primary) 8%,transparent)}.image-wrapper{position:relative}.image-wrapper:hover .hover-overlay{opacity:1}.hover-overlay{opacity:0;transition:opacity .15s}\n"] }]
247
+ }], propDecorators: { fileInput: [{
248
+ type: ViewChild,
249
+ args: ['fileInput']
250
+ }] } });
251
+ //# sourceMappingURL=post-editor-cover-image.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-editor-cover-image.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-admin-ui/src/lib/post-editor/post-editor-cover-image.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n PLATFORM_ID,\n ViewChild,\n inject,\n signal,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage';\nimport { FIREBASE_STORAGE, PostService } from '@foliokit/cms-core';\nimport { PostEditorStore } from './post-editor.store';\n\n@Component({\n selector: 'folio-post-editor-cover-image',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [MatButtonModule, MatIconModule, MatProgressBarModule],\n styles: [\n `\n :host {\n display: block;\n }\n .drop-zone {\n border: 2px dashed color-mix(in srgb, currentColor 25%, transparent);\n border-radius: 8px;\n transition: border-color 0.15s, background 0.15s;\n }\n .drop-zone.drag-over {\n border-color: var(--mat-sys-primary);\n background: color-mix(in srgb, var(--mat-sys-primary) 8%, transparent);\n }\n .image-wrapper {\n position: relative;\n }\n .image-wrapper:hover .hover-overlay {\n opacity: 1;\n }\n .hover-overlay {\n opacity: 0;\n transition: opacity 0.15s;\n }\n `,\n ],\n template: `\n <div class=\"flex flex-col gap-2\">\n <span class=\"text-sm font-semibold\">Cover Image</span>\n\n @if (store.post()?.thumbnailUrl; as url) {\n <!-- Filled state -->\n <div class=\"image-wrapper rounded-lg overflow-hidden\">\n <div class=\"aspect-video w-full relative\">\n <img\n [src]=\"url\"\n [alt]=\"store.post()?.thumbnailAlt ?? ''\"\n class=\"w-full h-full object-cover\"\n />\n <!-- Hover overlay -->\n <div\n class=\"hover-overlay absolute inset-0 flex items-center justify-center gap-3\"\n style=\"background: rgba(0,0,0,0.45)\"\n >\n <button\n mat-icon-button\n style=\"color: white\"\n title=\"Replace image\"\n (click)=\"isBrowser && fileInput.click()\"\n >\n <mat-icon>swap_horiz</mat-icon>\n </button>\n <button\n mat-icon-button\n style=\"color: white\"\n title=\"Delete image\"\n (click)=\"onDeleteCover()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n </div>\n } @else {\n <!-- Empty state -->\n <div\n class=\"drop-zone w-full flex flex-col items-center justify-center gap-2 py-10 cursor-pointer select-none\"\n [class.drag-over]=\"isDragOver()\"\n (click)=\"isBrowser && fileInput.click()\"\n (dragover)=\"isBrowser && onDragOver($event)\"\n (dragleave)=\"isBrowser && onDragLeave()\"\n (drop)=\"isBrowser && onDrop($event)\"\n >\n <mat-icon class=\"opacity-40\" style=\"font-size: 2.5rem; width: 2.5rem; height: 2.5rem\">\n upload_file\n </mat-icon>\n <span class=\"text-sm opacity-50\">Upload cover image</span>\n </div>\n }\n\n <!-- Hidden file input -->\n <input\n #fileInput\n type=\"file\"\n accept=\"image/*\"\n class=\"hidden\"\n (change)=\"onFileSelected($any($event.target).files)\"\n />\n\n <!-- Progress bar -->\n @if (uploading()) {\n <mat-progress-bar mode=\"determinate\" [value]=\"uploadProgress()\" />\n }\n\n <!-- Error message -->\n @if (uploadError()) {\n <p class=\"text-sm text-red-500\">{{ uploadError() }}</p>\n }\n </div>\n `,\n})\nexport class PostEditorCoverImageComponent {\n @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;\n\n readonly store = inject(PostEditorStore);\n private readonly storage = inject(FIREBASE_STORAGE);\n private readonly postService = inject(PostService);\n private readonly platformId = inject(PLATFORM_ID);\n\n readonly isBrowser = isPlatformBrowser(this.platformId);\n\n readonly uploading = signal(false);\n readonly uploadProgress = signal(0);\n readonly uploadError = signal<string | null>(null);\n readonly isDragOver = signal(false);\n readonly storagePath = signal<string | null>(null);\n\n onDragOver(event: DragEvent): void {\n event.preventDefault();\n this.isDragOver.set(true);\n }\n\n onDragLeave(): void {\n this.isDragOver.set(false);\n }\n\n onDrop(event: DragEvent): void {\n event.preventDefault();\n this.isDragOver.set(false);\n const files = event.dataTransfer?.files;\n if (files?.length) {\n this.upload(files[0]);\n }\n }\n\n onFileSelected(files: FileList | null): void {\n if (!files?.length) return;\n this.upload(files[0]);\n // Reset so the same file can be re-selected after a delete\n if (this.fileInput?.nativeElement) {\n this.fileInput.nativeElement.value = '';\n }\n }\n\n onDeleteCover(): void {\n if (!window.confirm('Remove cover image?')) return;\n const path = this.storagePath();\n if (path) {\n this.postService.deleteStorageFile(path).subscribe({\n next: () => this.clearCover(),\n error: () => this.clearCover(),\n });\n } else {\n this.clearCover();\n }\n }\n\n private clearCover(): void {\n this.store.updateField('thumbnailUrl', '');\n this.store.updateField('thumbnailAlt', '');\n this.storagePath.set(null);\n this.uploadProgress.set(0);\n }\n\n private upload(file: File): void {\n const previousPath = this.storagePath();\n const postId = this.store.post()?.id || this.store.tempPostId();\n const storagePath = `posts/${postId}/cover/${file.name}`;\n\n if (previousPath) {\n this.postService.deleteStorageFile(previousPath).subscribe();\n }\n\n const fileRef = ref(this.storage, storagePath);\n\n this.uploading.set(true);\n this.uploadProgress.set(0);\n this.uploadError.set(null);\n\n const task = uploadBytesResumable(fileRef, file);\n\n task.on(\n 'state_changed',\n (snapshot) => {\n this.uploadProgress.set(\n Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100),\n );\n },\n (error) => {\n this.uploading.set(false);\n this.uploadError.set(error.message);\n },\n () => {\n getDownloadURL(task.snapshot.ref).then((downloadUrl) => {\n this.store.updateField('thumbnailUrl', downloadUrl);\n this.store.updateField('thumbnailAlt', file.name);\n this.storagePath.set(storagePath);\n this.uploading.set(false);\n });\n },\n );\n }\n}\n"]}
@@ -0,0 +1,99 @@
1
+ import { ChangeDetectionStrategy, Component, inject, input, } from '@angular/core';
2
+ import { MatButtonModule } from '@angular/material/button';
3
+ import { MatIconModule } from '@angular/material/icon';
4
+ import { MatTooltipModule } from '@angular/material/tooltip';
5
+ import { PostEditorStore } from './post-editor.store';
6
+ import * as i0 from "@angular/core";
7
+ import * as i1 from "@angular/material/button";
8
+ import * as i2 from "@angular/material/icon";
9
+ import * as i3 from "@angular/material/tooltip";
10
+ export class PostEditorEmbeddedMediaItemComponent {
11
+ entry = input.required(...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
12
+ token = input.required(...(ngDevMode ? [{ debugName: "token" }] : /* istanbul ignore next */ []));
13
+ store = inject(PostEditorStore);
14
+ onInsert() {
15
+ this.store.insertMediaAtCursor(this.token());
16
+ }
17
+ onDelete() {
18
+ if (!window.confirm('Remove this image? It will be removed from Storage and any markdown references will break.'))
19
+ return;
20
+ this.store.removeEmbeddedMedia(this.token());
21
+ }
22
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorEmbeddedMediaItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
23
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", type: PostEditorEmbeddedMediaItemComponent, isStandalone: true, selector: "folio-post-editor-embedded-media-item", inputs: { entry: { classPropertyName: "entry", publicName: "entry", isSignal: true, isRequired: true, transformFunction: null }, token: { classPropertyName: "token", publicName: "token", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
24
+ <div class="item-wrapper flex flex-col gap-1">
25
+ <div class="relative aspect-square rounded-md overflow-hidden bg-[var(--mat-sys-surface-container-high)]">
26
+ <img
27
+ [src]="entry().downloadUrl"
28
+ [alt]="entry().alt"
29
+ class="w-full h-full object-cover"
30
+ />
31
+ <!-- Hover overlay -->
32
+ <div
33
+ class="hover-overlay absolute inset-0 flex items-center justify-center gap-2"
34
+ style="background: rgba(0,0,0,0.5)"
35
+ >
36
+ <button
37
+ mat-icon-button
38
+ style="color: white"
39
+ matTooltip="Insert at cursor"
40
+ (click)="onInsert()"
41
+ >
42
+ <mat-icon>add_photo_alternate</mat-icon>
43
+ </button>
44
+ <button
45
+ mat-icon-button
46
+ style="color: white"
47
+ matTooltip="Delete"
48
+ (click)="onDelete()"
49
+ >
50
+ <mat-icon>delete</mat-icon>
51
+ </button>
52
+ </div>
53
+ </div>
54
+ <span class="text-xs opacity-60 truncate" [title]="entry().alt">
55
+ {{ entry().alt }}
56
+ </span>
57
+ </div>
58
+ `, isInline: true, styles: [":host{display:block}.item-wrapper:hover .hover-overlay{opacity:1}.hover-overlay{opacity:0;transition:opacity .15s}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
59
+ }
60
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorEmbeddedMediaItemComponent, decorators: [{
61
+ type: Component,
62
+ args: [{ selector: 'folio-post-editor-embedded-media-item', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
63
+ <div class="item-wrapper flex flex-col gap-1">
64
+ <div class="relative aspect-square rounded-md overflow-hidden bg-[var(--mat-sys-surface-container-high)]">
65
+ <img
66
+ [src]="entry().downloadUrl"
67
+ [alt]="entry().alt"
68
+ class="w-full h-full object-cover"
69
+ />
70
+ <!-- Hover overlay -->
71
+ <div
72
+ class="hover-overlay absolute inset-0 flex items-center justify-center gap-2"
73
+ style="background: rgba(0,0,0,0.5)"
74
+ >
75
+ <button
76
+ mat-icon-button
77
+ style="color: white"
78
+ matTooltip="Insert at cursor"
79
+ (click)="onInsert()"
80
+ >
81
+ <mat-icon>add_photo_alternate</mat-icon>
82
+ </button>
83
+ <button
84
+ mat-icon-button
85
+ style="color: white"
86
+ matTooltip="Delete"
87
+ (click)="onDelete()"
88
+ >
89
+ <mat-icon>delete</mat-icon>
90
+ </button>
91
+ </div>
92
+ </div>
93
+ <span class="text-xs opacity-60 truncate" [title]="entry().alt">
94
+ {{ entry().alt }}
95
+ </span>
96
+ </div>
97
+ `, styles: [":host{display:block}.item-wrapper:hover .hover-overlay{opacity:1}.hover-overlay{opacity:0;transition:opacity .15s}\n"] }]
98
+ }], propDecorators: { entry: [{ type: i0.Input, args: [{ isSignal: true, alias: "entry", required: true }] }], token: [{ type: i0.Input, args: [{ isSignal: true, alias: "token", required: true }] }] } });
99
+ //# sourceMappingURL=post-editor-embedded-media-item.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-editor-embedded-media-item.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-admin-ui/src/lib/post-editor/post-editor-embedded-media-item.component.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,SAAS,EACT,MAAM,EACN,KAAK,GACN,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;;;;AA0DtD,MAAM,OAAO,oCAAoC;IACtC,KAAK,GAAG,KAAK,CAAC,QAAQ,2EAAsB,CAAC;IAC7C,KAAK,GAAG,KAAK,CAAC,QAAQ,2EAAU,CAAC;IAEjC,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;IAEzC,QAAQ;QACN,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,4FAA4F,CAAC;YAAE,OAAO;QAC1H,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,CAAC;uGAbU,oCAAoC;2FAApC,oCAAoC,2VArCrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCT,6LAlDS,eAAe,qNAAE,aAAa,mLAAE,gBAAgB;;2FAoD/C,oCAAoC;kBAxDhD,SAAS;+BACE,uCAAuC,cACrC,IAAI,mBACC,uBAAuB,CAAC,MAAM,WACtC,CAAC,eAAe,EAAE,aAAa,EAAE,gBAAgB,CAAC,YAejD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCT","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n inject,\n input,\n} from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { EmbeddedMediaEntry } from '@foliokit/cms-core';\nimport { PostEditorStore } from './post-editor.store';\n\n@Component({\n selector: 'folio-post-editor-embedded-media-item',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [MatButtonModule, MatIconModule, MatTooltipModule],\n styles: [\n `\n :host {\n display: block;\n }\n .item-wrapper:hover .hover-overlay {\n opacity: 1;\n }\n .hover-overlay {\n opacity: 0;\n transition: opacity 0.15s;\n }\n `,\n ],\n template: `\n <div class=\"item-wrapper flex flex-col gap-1\">\n <div class=\"relative aspect-square rounded-md overflow-hidden bg-[var(--mat-sys-surface-container-high)]\">\n <img\n [src]=\"entry().downloadUrl\"\n [alt]=\"entry().alt\"\n class=\"w-full h-full object-cover\"\n />\n <!-- Hover overlay -->\n <div\n class=\"hover-overlay absolute inset-0 flex items-center justify-center gap-2\"\n style=\"background: rgba(0,0,0,0.5)\"\n >\n <button\n mat-icon-button\n style=\"color: white\"\n matTooltip=\"Insert at cursor\"\n (click)=\"onInsert()\"\n >\n <mat-icon>add_photo_alternate</mat-icon>\n </button>\n <button\n mat-icon-button\n style=\"color: white\"\n matTooltip=\"Delete\"\n (click)=\"onDelete()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n <span class=\"text-xs opacity-60 truncate\" [title]=\"entry().alt\">\n {{ entry().alt }}\n </span>\n </div>\n `,\n})\nexport class PostEditorEmbeddedMediaItemComponent {\n readonly entry = input.required<EmbeddedMediaEntry>();\n readonly token = input.required<string>();\n\n readonly store = inject(PostEditorStore);\n\n onInsert(): void {\n this.store.insertMediaAtCursor(this.token());\n }\n\n onDelete(): void {\n if (!window.confirm('Remove this image? It will be removed from Storage and any markdown references will break.')) return;\n this.store.removeEmbeddedMedia(this.token());\n }\n}\n"]}
@@ -0,0 +1,173 @@
1
+ import { ChangeDetectionStrategy, Component, PLATFORM_ID, ViewChild, computed, inject, signal, } from '@angular/core';
2
+ import { isPlatformBrowser } from '@angular/common';
3
+ import { MatButtonModule } from '@angular/material/button';
4
+ import { MatIconModule } from '@angular/material/icon';
5
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
6
+ import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage';
7
+ import { FIREBASE_STORAGE } from '@foliokit/cms-core';
8
+ import { PostEditorStore } from './post-editor.store';
9
+ import { PostEditorEmbeddedMediaItemComponent } from './post-editor-embedded-media-item.component';
10
+ import * as i0 from "@angular/core";
11
+ import * as i1 from "@angular/material/button";
12
+ import * as i2 from "@angular/material/icon";
13
+ import * as i3 from "@angular/material/progress-bar";
14
+ export class PostEditorEmbeddedMediaComponent {
15
+ fileInput;
16
+ store = inject(PostEditorStore);
17
+ storage = inject(FIREBASE_STORAGE);
18
+ platformId = inject(PLATFORM_ID);
19
+ isBrowser = isPlatformBrowser(this.platformId);
20
+ uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
21
+ uploadError = signal(null, ...(ngDevMode ? [{ debugName: "uploadError" }] : /* istanbul ignore next */ []));
22
+ entries = computed(() => {
23
+ const media = this.store.post()?.embeddedMedia ?? {};
24
+ return Object.values(media);
25
+ }, ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
26
+ onFileSelected(files) {
27
+ if (!files?.length)
28
+ return;
29
+ this.upload(files[0]);
30
+ if (this.fileInput?.nativeElement) {
31
+ this.fileInput.nativeElement.value = '';
32
+ }
33
+ }
34
+ upload(file) {
35
+ const postId = this.store.post()?.id || this.store.tempPostId();
36
+ const storagePath = `posts/${postId}/media/${file.name}`;
37
+ const fileRef = ref(this.storage, storagePath);
38
+ const token = crypto.randomUUID();
39
+ this.uploading.set(true);
40
+ this.uploadError.set(null);
41
+ const task = uploadBytesResumable(fileRef, file);
42
+ task.on('state_changed', null, (error) => {
43
+ this.uploading.set(false);
44
+ this.uploadError.set(error.message);
45
+ }, () => {
46
+ getDownloadURL(task.snapshot.ref).then((downloadUrl) => {
47
+ const entry = {
48
+ token,
49
+ storagePath,
50
+ downloadUrl,
51
+ alt: file.name,
52
+ mimeType: file.type,
53
+ };
54
+ const currentMedia = this.store.post()?.embeddedMedia ?? {};
55
+ this.store.updateField('embeddedMedia', {
56
+ ...currentMedia,
57
+ [token]: entry,
58
+ });
59
+ this.uploading.set(false);
60
+ });
61
+ });
62
+ }
63
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorEmbeddedMediaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
64
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: PostEditorEmbeddedMediaComponent, isStandalone: true, selector: "folio-post-editor-embedded-media", viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: `
65
+ <div class="flex flex-col gap-3">
66
+ <!-- Header -->
67
+ <div class="flex items-center justify-between">
68
+ <span class="text-sm font-semibold">Embedded Media</span>
69
+ <button
70
+ mat-stroked-button
71
+ [disabled]="uploading()"
72
+ (click)="isBrowser && fileInput.click()"
73
+ >
74
+ <mat-icon>upload</mat-icon>
75
+ Upload Image
76
+ </button>
77
+ </div>
78
+
79
+ <!-- Hidden file input -->
80
+ <input
81
+ #fileInput
82
+ type="file"
83
+ accept="image/*"
84
+ class="hidden"
85
+ (change)="onFileSelected($any($event.target).files)"
86
+ />
87
+
88
+ <!-- Upload progress -->
89
+ @if (uploading()) {
90
+ <mat-progress-bar mode="indeterminate" />
91
+ }
92
+
93
+ <!-- Upload error -->
94
+ @if (uploadError()) {
95
+ <p class="text-sm text-red-500">{{ uploadError() }}</p>
96
+ }
97
+
98
+ <!-- Media grid -->
99
+ @if (entries().length > 0) {
100
+ <div class="grid grid-cols-2 gap-3">
101
+ @for (item of entries(); track item.token) {
102
+ <folio-post-editor-embedded-media-item
103
+ [entry]="item"
104
+ [token]="item.token"
105
+ />
106
+ }
107
+ </div>
108
+ } @else {
109
+ <div class="flex items-center justify-center py-8">
110
+ <span class="text-sm opacity-40">No embedded media yet</span>
111
+ </div>
112
+ }
113
+ </div>
114
+ `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i3.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: PostEditorEmbeddedMediaItemComponent, selector: "folio-post-editor-embedded-media-item", inputs: ["entry", "token"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
115
+ }
116
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorEmbeddedMediaComponent, decorators: [{
117
+ type: Component,
118
+ args: [{ selector: 'folio-post-editor-embedded-media', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatButtonModule, MatIconModule, MatProgressBarModule, PostEditorEmbeddedMediaItemComponent], template: `
119
+ <div class="flex flex-col gap-3">
120
+ <!-- Header -->
121
+ <div class="flex items-center justify-between">
122
+ <span class="text-sm font-semibold">Embedded Media</span>
123
+ <button
124
+ mat-stroked-button
125
+ [disabled]="uploading()"
126
+ (click)="isBrowser && fileInput.click()"
127
+ >
128
+ <mat-icon>upload</mat-icon>
129
+ Upload Image
130
+ </button>
131
+ </div>
132
+
133
+ <!-- Hidden file input -->
134
+ <input
135
+ #fileInput
136
+ type="file"
137
+ accept="image/*"
138
+ class="hidden"
139
+ (change)="onFileSelected($any($event.target).files)"
140
+ />
141
+
142
+ <!-- Upload progress -->
143
+ @if (uploading()) {
144
+ <mat-progress-bar mode="indeterminate" />
145
+ }
146
+
147
+ <!-- Upload error -->
148
+ @if (uploadError()) {
149
+ <p class="text-sm text-red-500">{{ uploadError() }}</p>
150
+ }
151
+
152
+ <!-- Media grid -->
153
+ @if (entries().length > 0) {
154
+ <div class="grid grid-cols-2 gap-3">
155
+ @for (item of entries(); track item.token) {
156
+ <folio-post-editor-embedded-media-item
157
+ [entry]="item"
158
+ [token]="item.token"
159
+ />
160
+ }
161
+ </div>
162
+ } @else {
163
+ <div class="flex items-center justify-center py-8">
164
+ <span class="text-sm opacity-40">No embedded media yet</span>
165
+ </div>
166
+ }
167
+ </div>
168
+ `, styles: [":host{display:block}\n"] }]
169
+ }], propDecorators: { fileInput: [{
170
+ type: ViewChild,
171
+ args: ['fileInput']
172
+ }] } });
173
+ //# sourceMappingURL=post-editor-embedded-media.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-editor-embedded-media.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-admin-ui/src/lib/post-editor/post-editor-embedded-media.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n PLATFORM_ID,\n ViewChild,\n computed,\n inject,\n signal,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage';\nimport { EmbeddedMediaEntry, FIREBASE_STORAGE } from '@foliokit/cms-core';\nimport { PostEditorStore } from './post-editor.store';\nimport { PostEditorEmbeddedMediaItemComponent } from './post-editor-embedded-media-item.component';\n\n@Component({\n selector: 'folio-post-editor-embedded-media',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [MatButtonModule, MatIconModule, MatProgressBarModule, PostEditorEmbeddedMediaItemComponent],\n styles: [\n `\n :host {\n display: block;\n }\n `,\n ],\n template: `\n <div class=\"flex flex-col gap-3\">\n <!-- Header -->\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-semibold\">Embedded Media</span>\n <button\n mat-stroked-button\n [disabled]=\"uploading()\"\n (click)=\"isBrowser && fileInput.click()\"\n >\n <mat-icon>upload</mat-icon>\n Upload Image\n </button>\n </div>\n\n <!-- Hidden file input -->\n <input\n #fileInput\n type=\"file\"\n accept=\"image/*\"\n class=\"hidden\"\n (change)=\"onFileSelected($any($event.target).files)\"\n />\n\n <!-- Upload progress -->\n @if (uploading()) {\n <mat-progress-bar mode=\"indeterminate\" />\n }\n\n <!-- Upload error -->\n @if (uploadError()) {\n <p class=\"text-sm text-red-500\">{{ uploadError() }}</p>\n }\n\n <!-- Media grid -->\n @if (entries().length > 0) {\n <div class=\"grid grid-cols-2 gap-3\">\n @for (item of entries(); track item.token) {\n <folio-post-editor-embedded-media-item\n [entry]=\"item\"\n [token]=\"item.token\"\n />\n }\n </div>\n } @else {\n <div class=\"flex items-center justify-center py-8\">\n <span class=\"text-sm opacity-40\">No embedded media yet</span>\n </div>\n }\n </div>\n `,\n})\nexport class PostEditorEmbeddedMediaComponent {\n @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;\n\n readonly store = inject(PostEditorStore);\n private readonly storage = inject(FIREBASE_STORAGE);\n private readonly platformId = inject(PLATFORM_ID);\n\n readonly isBrowser = isPlatformBrowser(this.platformId);\n readonly uploading = signal(false);\n readonly uploadError = signal<string | null>(null);\n\n readonly entries = computed<EmbeddedMediaEntry[]>(() => {\n const media = this.store.post()?.embeddedMedia ?? {};\n return Object.values(media) as EmbeddedMediaEntry[];\n });\n\n onFileSelected(files: FileList | null): void {\n if (!files?.length) return;\n this.upload(files[0]);\n if (this.fileInput?.nativeElement) {\n this.fileInput.nativeElement.value = '';\n }\n }\n\n private upload(file: File): void {\n const postId = this.store.post()?.id || this.store.tempPostId();\n const storagePath = `posts/${postId}/media/${file.name}`;\n const fileRef = ref(this.storage, storagePath);\n const token = crypto.randomUUID();\n\n this.uploading.set(true);\n this.uploadError.set(null);\n\n const task = uploadBytesResumable(fileRef, file);\n\n task.on(\n 'state_changed',\n null,\n (error) => {\n this.uploading.set(false);\n this.uploadError.set(error.message);\n },\n () => {\n getDownloadURL(task.snapshot.ref).then((downloadUrl) => {\n const entry: EmbeddedMediaEntry = {\n token,\n storagePath,\n downloadUrl,\n alt: file.name,\n mimeType: file.type,\n };\n const currentMedia = this.store.post()?.embeddedMedia ?? {};\n this.store.updateField('embeddedMedia', {\n ...currentMedia,\n [token]: entry,\n });\n this.uploading.set(false);\n });\n },\n );\n }\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { PostEditorCoverImageComponent } from './post-editor-cover-image.component';
3
+ import { PostEditorEmbeddedMediaComponent } from './post-editor-embedded-media.component';
4
+ import * as i0 from "@angular/core";
5
+ export class PostEditorMediaTabComponent {
6
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorMediaTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.4", type: PostEditorMediaTabComponent, isStandalone: true, selector: "folio-post-editor-media-tab", ngImport: i0, template: `
8
+ <div class="flex flex-col gap-6 p-4">
9
+ <folio-post-editor-cover-image />
10
+ <folio-post-editor-embedded-media />
11
+ </div>
12
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}\n"], dependencies: [{ kind: "component", type: PostEditorCoverImageComponent, selector: "folio-post-editor-cover-image" }, { kind: "component", type: PostEditorEmbeddedMediaComponent, selector: "folio-post-editor-embedded-media" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13
+ }
14
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PostEditorMediaTabComponent, decorators: [{
15
+ type: Component,
16
+ args: [{ selector: 'folio-post-editor-media-tab', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PostEditorCoverImageComponent, PostEditorEmbeddedMediaComponent], template: `
17
+ <div class="flex flex-col gap-6 p-4">
18
+ <folio-post-editor-cover-image />
19
+ <folio-post-editor-embedded-media />
20
+ </div>
21
+ `, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}\n"] }]
22
+ }] });
23
+ //# sourceMappingURL=post-editor-media-tab.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-editor-media-tab.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-admin-ui/src/lib/post-editor/post-editor-media-tab.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;;AAyB1F,MAAM,OAAO,2BAA2B;uGAA3B,2BAA2B;2FAA3B,2BAA2B,uFAP5B;;;;;GAKT,yJAjBS,6BAA6B,0EAAE,gCAAgC;;2FAmB9D,2BAA2B;kBAvBvC,SAAS;+BACE,6BAA6B,cAC3B,IAAI,mBACC,uBAAuB,CAAC,MAAM,WACtC,CAAC,6BAA6B,EAAE,gCAAgC,CAAC,YAYhE;;;;;GAKT","sourcesContent":["import { ChangeDetectionStrategy, Component } from '@angular/core';\nimport { PostEditorCoverImageComponent } from './post-editor-cover-image.component';\nimport { PostEditorEmbeddedMediaComponent } from './post-editor-embedded-media.component';\n\n@Component({\n selector: 'folio-post-editor-media-tab',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [PostEditorCoverImageComponent, PostEditorEmbeddedMediaComponent],\n styles: [\n `\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n `,\n ],\n template: `\n <div class=\"flex flex-col gap-6 p-4\">\n <folio-post-editor-cover-image />\n <folio-post-editor-embedded-media />\n </div>\n `,\n})\nexport class PostEditorMediaTabComponent {}\n"]}