@@ -69,14 +103,24 @@ import { ImageUploadService } from '../../../services/image-upload.service';
}
@if (showHandles()) {
-
-
-
-
-
-
-
-
+
+
+
+
+
+
}
@if (props.caption) {
@@ -127,27 +171,22 @@ import { ImageUploadService } from '../../../services/image-upload.service';
.resize-handle {
position: absolute;
- background: #ffffff;
- border: 2px solid #9ca3af; /* gray-400 */
- border-radius: 50%;
+ background: rgba(0,0,0,0.78);
+ color: #e5e7eb;
+ border: 1px solid rgba(255,255,255,0.25);
+ border-radius: 9999px;
cursor: pointer;
z-index: 10;
- transition: transform 0.2s;
+ transition: transform 0.2s, background 0.2s;
}
.resize-handle:hover {
transform: scale(1.2);
}
- .resize-handle.corner {
- width: 12px;
- height: 12px;
- }
-
- .resize-handle.edge {
- width: 10px;
- height: 10px;
- }
+ .resize-handle.corner { width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; }
+ .resize-handle.edge { width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; }
+ .rh-ico { width: 12px; height: 12px; opacity: 0.9; }
.resize-handle.top-left {
top: -6px;
@@ -243,6 +282,8 @@ import { ImageUploadService } from '../../../services/image-upload.service';
background: #ffffff;
}
.qa-btn:hover { background: #f3f4f6; }
+ .qa-icon-btn { width: 28px; height: 28px; display:flex; align-items:center; justify-content:center; border-radius: 6px; border: 1px solid #e5e7eb; background: #ffffff; }
+ .qa-icon-btn:hover { background: #f3f4f6; }
`]
})
export class ImageBlockComponent {
@@ -394,11 +435,25 @@ export class ImageBlockComponent {
return (this.props.aspectRatio || 'free') === ratio;
}
onAspect(ratio: string) {
- this.update.emit({ ...this.props, aspectRatio: ratio });
+ const patch: any = { ...this.props, aspectRatio: ratio };
+ if (ratio && ratio !== 'free') {
+ patch.height = undefined;
+ }
+ this.update.emit(patch);
}
onCrop() {
alert('Crop coming soon!');
}
+ setAlignment(a: 'left' | 'center' | 'right' | 'full') {
+ const patch: any = { ...this.props, alignment: a };
+ if (a === 'full') { patch.width = undefined; patch.height = undefined; }
+ this.update.emit(patch);
+ this.showQuick.set(false);
+ }
+ defaultSize() {
+ this.update.emit({ ...this.props, width: undefined, height: undefined, aspectRatio: 'free' } as any);
+ this.showQuick.set(false);
+ }
openSettings(ev: MouseEvent) {
ev.stopPropagation();
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
@@ -432,25 +487,26 @@ export class ImageBlockComponent {
let newWidth = this.startWidth;
let newHeight = this.startHeight;
-
- // Calculer les nouvelles dimensions selon la direction
- if (this.resizeDirection.includes('e')) newWidth = this.startWidth + deltaX;
- if (this.resizeDirection.includes('w')) newWidth = this.startWidth - deltaX;
- if (this.resizeDirection.includes('s')) newHeight = this.startHeight + deltaY;
- if (this.resizeDirection.includes('n')) newHeight = this.startHeight - deltaY;
-
- // Limites min/max
- newWidth = Math.max(100, Math.min(1200, newWidth));
- newHeight = Math.max(100, Math.min(1200, newHeight));
-
- // Si aspect ratio défini, maintenir la proportion
- if (this.props.aspectRatio && this.props.aspectRatio !== 'free') {
- const ratio = this.getAspectRatioValue();
- if (ratio) {
- newHeight = newWidth / ratio;
- }
+ const dir = this.resizeDirection;
+ const isCorner = dir === 'nw' || dir === 'ne' || dir === 'sw' || dir === 'se';
+ const hasFixedRatio = !!(this.props.aspectRatio && this.props.aspectRatio !== 'free');
+
+ // Compute tentative size per axis
+ if (dir.includes('e')) newWidth = this.startWidth + deltaX;
+ if (dir.includes('w')) newWidth = this.startWidth - deltaX;
+ if (dir.includes('s')) newHeight = this.startHeight + deltaY;
+ if (dir.includes('n')) newHeight = this.startHeight - deltaY;
+
+ // Clamp
+ newWidth = Math.max(100, Math.min(1600, newWidth));
+ newHeight = Math.max(100, Math.min(1600, newHeight));
+
+ // Maintain aspect ratio for corner handles: if no fixed ratio, keep initial ratio
+ if (isCorner) {
+ const ratio = hasFixedRatio ? (this.getAspectRatioValue() || (this.startWidth / this.startHeight)) : (this.startWidth / this.startHeight);
+ if (ratio > 0) newHeight = Math.round(newWidth / ratio);
}
-
+
this.update.emit({
...this.props,
width: Math.round(newWidth),
diff --git a/src/app/editor/components/image/image-caption-modal.component.ts b/src/app/editor/components/image/image-caption-modal.component.ts
new file mode 100644
index 0000000..e8f4b28
--- /dev/null
+++ b/src/app/editor/components/image/image-caption-modal.component.ts
@@ -0,0 +1,38 @@
+import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'app-image-caption-modal',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
{{ title || 'Image caption' }}
+
+
+
+
+
+
+
+
+ `,
+})
+export class ImageCaptionModalComponent {
+ @Input() caption: string = '';
+ @Input() title: string = '';
+ @Output() save = new EventEmitter
();
+ @Output() cancel = new EventEmitter();
+ isDark(): boolean { try { return document.documentElement.classList.contains('dark'); } catch { return false; } }
+ onBackdrop(e: MouseEvent) { if (e.target === e.currentTarget) this.cancel.emit(); }
+}
diff --git a/src/app/editor/components/image/image-info-modal.component.ts b/src/app/editor/components/image/image-info-modal.component.ts
new file mode 100644
index 0000000..cc7b8ff
--- /dev/null
+++ b/src/app/editor/components/image/image-info-modal.component.ts
@@ -0,0 +1,92 @@
+import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'app-image-info-modal',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+
+
🖼️
+
+
Image info
+
{{ src }}
+
+
+
+
+
Displayed size: {{ width || 'auto' }} × {{ height || 'auto' }} px
+
Natural size: {{ natW || '—' }} × {{ natH || '—' }} px
+
Aspect: {{ aspect || 'free' }}
+
Alignment: {{ alignment || 'center' }}
+
Rotation: {{ rotation || 0 }}°
+
Type: {{ typeHint }}
+
+
+
+
+
+
+
+
+ `,
+})
+export class ImageInfoModalComponent {
+ @Input() src: string = '';
+ @Input() width: number | undefined;
+ @Input() height: number | undefined;
+ @Input() aspect: string | undefined;
+ @Input() alignment: string | undefined;
+ @Input() rotation: number | undefined;
+ @Output() close = new EventEmitter();
+
+ natW: number | null = null;
+ natH: number | null = null;
+
+ ngOnInit() {
+ if (this.src) {
+ const img = new Image();
+ img.onload = () => {
+ this.natW = img.naturalWidth;
+ this.natH = img.naturalHeight;
+ };
+ img.src = this.src;
+ }
+ }
+
+ get typeHint(): string {
+ const s = (this.src || '').toLowerCase();
+ if (s.endsWith('.png')) return 'PNG';
+ if (s.endsWith('.jpg') || s.endsWith('.jpeg')) return 'JPEG';
+ if (s.endsWith('.gif')) return 'GIF';
+ if (s.endsWith('.webp')) return 'WEBP';
+ return 'Image';
+ }
+
+ isDark(): boolean { try { return document.documentElement.classList.contains('dark'); } catch { return false; } }
+ onBackdrop(e: MouseEvent) { if (e.target === e.currentTarget) this.close.emit(); }
+ openInNewTab() { if (this.src) window.open(this.src, '_blank', 'noopener'); }
+ download() {
+ if (!this.src) return;
+ const a = document.createElement('a');
+ a.href = this.src;
+ a.download = this.src.split('/').pop() || 'image';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+}
diff --git a/vault/.obsidian/workspace.json b/vault/.obsidian/workspace.json
index eefa591..cc7cd3f 100644
--- a/vault/.obsidian/workspace.json
+++ b/vault/.obsidian/workspace.json
@@ -181,6 +181,8 @@
},
"active": "aaf62e01f34df49b",
"lastOpenFiles": [
+ "attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png",
+ "attachments/nimbus/2025/1111",
"attachments/nimbus/2025/1110/img-bridger-tower-vejbbvtdgcm-unsplash-jpg-20251110-175132-92f6k8.png",
"attachments/nimbus/2025/1110/img-image_1-png-20251110-154537-gaoaou.png",
"attachments/nimbus/2025/1110/img-logo_obsiviewer-png-20251110-151755-r8r5qq.png",
@@ -218,7 +220,6 @@
"big/note_497.md.bak",
"big/note_498.md.bak",
"big/note_495.md.bak",
- "big/note_496.md.bak",
"mixe/Dessin-02.png",
"Dessin-02.png",
"mixe/Claude_ObsiViewer_V1.png",
@@ -226,7 +227,6 @@
"Drawing-20251028-1452.png",
"dessin.svg",
"dessin.png",
- "dessin_05.svg",
"Untitled.canvas"
]
}
\ No newline at end of file
diff --git a/vault/attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png b/vault/attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png
new file mode 100644
index 0000000..87c8951
Binary files /dev/null and b/vault/attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png differ