diff --git a/frontend/src/components/BaseDialog.vue b/frontend/src/components/BaseDialog.vue
index 67539462..229552ea 100644
--- a/frontend/src/components/BaseDialog.vue
+++ b/frontend/src/components/BaseDialog.vue
@@ -5,6 +5,7 @@
@click.self="handleOverlayClick"
>
@@ -116,6 +126,12 @@ export default {
type: Boolean,
default: true
},
+
+ /** Nicht-modale Dialoge: Ecke zum Vergrößern/Verkleinern */
+ resizable: {
+ type: Boolean,
+ default: true
+ },
// Schließen bei Overlay-Klick (nur bei modalen Dialogen)
closeOnOverlay: {
@@ -129,7 +145,11 @@ export default {
localPosition: { ...this.position },
isDragging: false,
dragStartX: 0,
- dragStartY: 0
+ dragStartY: 0,
+ localWidth: null,
+ localHeight: null,
+ isResizing: false,
+ resizeStart: null,
};
},
@@ -158,6 +178,14 @@ export default {
if (!this.isModal) {
style.left = `${this.localPosition.x}px`;
style.top = `${this.localPosition.y}px`;
+ if (this.localWidth != null) {
+ style.width = `${this.localWidth}px`;
+ style.maxWidth = 'none';
+ }
+ if (this.localHeight != null) {
+ style.height = `${this.localHeight}px`;
+ style.maxHeight = 'none';
+ }
}
return style;
@@ -188,7 +216,11 @@ export default {
startDrag(event) {
if (!this.isDraggable) return;
-
+ if (event.button !== 0) return;
+ if (event.target.closest('button, a, input, select, textarea, [contenteditable="true"]')) {
+ return;
+ }
+
this.isDragging = true;
this.dragStartX = event.clientX - this.localPosition.x;
this.dragStartY = event.clientY - this.localPosition.y;
@@ -201,9 +233,20 @@ export default {
onDrag(event) {
if (!this.isDragging) return;
-
- this.localPosition.x = event.clientX - this.dragStartX;
- this.localPosition.y = event.clientY - this.dragStartY;
+
+ let nx = event.clientX - this.dragStartX;
+ let ny = event.clientY - this.dragStartY;
+ const el = this.$refs.dialogContainer;
+ if (el && typeof window !== 'undefined') {
+ const w = el.offsetWidth;
+ const h = el.offsetHeight;
+ const pad = 8;
+ nx = Math.min(Math.max(pad, nx), window.innerWidth - w - pad);
+ ny = Math.min(Math.max(pad, ny), window.innerHeight - h - pad);
+ }
+
+ this.localPosition.x = nx;
+ this.localPosition.y = ny;
this.$emit('update:position', { ...this.localPosition });
},
@@ -212,7 +255,48 @@ export default {
this.isDragging = false;
document.removeEventListener('mousemove', this.onDrag);
document.removeEventListener('mouseup', this.stopDrag);
- }
+ },
+
+ startResize(event) {
+ if (this.isModal || !this.resizable) return;
+ if (event.button !== 0) return;
+ const el = this.$refs.dialogContainer;
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ this.isResizing = true;
+ this.resizeStart = {
+ mouseX: event.clientX,
+ mouseY: event.clientY,
+ width: rect.width,
+ height: rect.height,
+ };
+ document.addEventListener('mousemove', this.onResizeMove);
+ document.addEventListener('mouseup', this.stopResize);
+ event.preventDefault();
+ },
+
+ onResizeMove(event) {
+ if (!this.isResizing || !this.resizeStart || typeof window === 'undefined') return;
+ const dx = event.clientX - this.resizeStart.mouseX;
+ const dy = event.clientY - this.resizeStart.mouseY;
+ let w = this.resizeStart.width + dx;
+ let h = this.resizeStart.height + dy;
+ const minW = 260;
+ const minH = 160;
+ const maxW = window.innerWidth - this.localPosition.x - 8;
+ const maxH = window.innerHeight - this.localPosition.y - 8;
+ w = Math.min(Math.max(minW, w), maxW);
+ h = Math.min(Math.max(minH, h), maxH);
+ this.localWidth = w;
+ this.localHeight = h;
+ },
+
+ stopResize() {
+ this.isResizing = false;
+ this.resizeStart = null;
+ document.removeEventListener('mousemove', this.onResizeMove);
+ document.removeEventListener('mouseup', this.stopResize);
+ },
},
watch: {
@@ -228,6 +312,9 @@ export default {
if (this.isDragging) {
this.stopDrag();
}
+ if (this.isResizing) {
+ this.stopResize();
+ }
}
};
@@ -265,6 +352,7 @@ export default {
/* Dialog Container */
.dialog-container {
+ position: relative;
background: white;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
@@ -283,6 +371,8 @@ export default {
/* Non-Modal Dialog Container */
.non-modal-dialog .dialog-container {
position: absolute;
+ min-width: 260px;
+ min-height: 160px;
}
/* Größen */
@@ -321,7 +411,34 @@ export default {
}
.dialog-header.draggable {
- cursor: move;
+ cursor: grab;
+}
+
+.dialog-header.draggable:active {
+ cursor: grabbing;
+}
+
+.dialog-resize-handle {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 16px;
+ height: 16px;
+ cursor: nwse-resize;
+ background: linear-gradient(
+ 135deg,
+ transparent 50%,
+ rgba(0, 0, 0, 0.12) 50%
+ );
+ border-bottom-right-radius: 8px;
+}
+
+.dialog-resize-handle:hover {
+ background: linear-gradient(
+ 135deg,
+ transparent 45%,
+ rgba(47, 122, 95, 0.35) 45%
+ );
}
.dialog-title {
diff --git a/frontend/src/components/DialogManager.vue b/frontend/src/components/DialogManager.vue
index 3cc3bc18..e6edad35 100644
--- a/frontend/src/components/DialogManager.vue
+++ b/frontend/src/components/DialogManager.vue
@@ -21,11 +21,7 @@
v-for="dialog in activeDialogs"
:key="dialog.id"
class="dialog-window"
- :style="{
- left: dialog.position.x + 'px',
- top: dialog.position.y + 'px',
- zIndex: dialog.zIndex
- }"
+ :style="dialogWindowStyle(dialog)"
@mousedown="bringToFront(dialog.id)"
>
-
closeDialog(dialog.id)" />
+
@@ -69,6 +70,9 @@ const InternalTournamentStats = defineAsyncComponent(() =>
import('./tournament/InternalTournamentStats.vue'),
);
+const MIN_DIALOG_W = 280;
+const MIN_DIALOG_H = 200;
+
export default {
components: {
MatchReportDialog,
@@ -84,11 +88,42 @@ export default {
InternalTournamentStats,
},
name: 'DialogManager',
+ data() {
+ return {
+ dragSession: null,
+ resizeSession: null,
+ };
+ },
computed: {
...mapGetters(['dialogs', 'minimizedDialogs', 'activeDialogs'])
},
methods: {
- ...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront']),
+ ...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront', 'updateDialogBounds']),
+
+ dialogWindowStyle(dialog) {
+ const s = {
+ left: `${dialog.position.x}px`,
+ top: `${dialog.position.y}px`,
+ zIndex: dialog.zIndex,
+ };
+ if (dialog.width != null && Number.isFinite(Number(dialog.width))) {
+ s.width = `${Number(dialog.width)}px`;
+ }
+ if (dialog.height != null && Number.isFinite(Number(dialog.height))) {
+ s.height = `${Number(dialog.height)}px`;
+ }
+ return s;
+ },
+
+ approxDialogWidth(dialog) {
+ if (dialog.width != null && Number.isFinite(Number(dialog.width))) return Number(dialog.width);
+ return Math.round(window.innerWidth * 0.9);
+ },
+
+ approxDialogHeight(dialog) {
+ if (dialog.height != null && Number.isFinite(Number(dialog.height))) return Number(dialog.height);
+ return Math.round(window.innerHeight * 0.9);
+ },
getDialogComponent(componentName) {
const components = {
@@ -113,9 +148,91 @@ export default {
},
startDrag(dialogId, event) {
- // Drag & Drop deaktiviert, da Dialog jetzt fest positioniert ist
- // Dialog wird immer zentriert angezeigt
- this.bringToFront(dialogId);
+ if (event.button !== 0) return;
+ if (event.target.closest('button, a, input, select, textarea, [contenteditable="true"]')) {
+ return;
+ }
+ const dialog = this.dialogs.find((d) => d.id === dialogId);
+ if (!dialog) return;
+ this.bringDialogToFront(dialogId);
+ this.dragSession = {
+ dialogId,
+ startX: event.clientX,
+ startY: event.clientY,
+ origX: dialog.position.x,
+ origY: dialog.position.y,
+ };
+ document.addEventListener('mousemove', this.onDragMove);
+ document.addEventListener('mouseup', this.onDragEnd);
+ event.preventDefault();
+ },
+
+ onDragMove(event) {
+ if (!this.dragSession) return;
+ const { dialogId, startX, startY, origX, origY } = this.dragSession;
+ const dialog = this.dialogs.find((d) => d.id === dialogId);
+ if (!dialog) return;
+ let nx = origX + event.clientX - startX;
+ let ny = origY + event.clientY - startY;
+ const w = this.approxDialogWidth(dialog);
+ const h = this.approxDialogHeight(dialog);
+ const pad = 8;
+ nx = Math.min(Math.max(pad, nx), window.innerWidth - w - pad);
+ ny = Math.min(Math.max(pad, ny), window.innerHeight - h - pad);
+ this.updateDialogBounds({ dialogId, position: { x: nx, y: ny } });
+ },
+
+ onDragEnd() {
+ this.dragSession = null;
+ document.removeEventListener('mousemove', this.onDragMove);
+ document.removeEventListener('mouseup', this.onDragEnd);
+ },
+
+ startResize(dialogId, event) {
+ if (event.button !== 0) return;
+ this.bringDialogToFront(dialogId);
+ const dialog = this.dialogs.find((d) => d.id === dialogId);
+ if (!dialog) return;
+ const el = event.currentTarget.closest('.dialog-window');
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ this.resizeSession = {
+ dialogId,
+ startX: event.clientX,
+ startY: event.clientY,
+ origW: rect.width,
+ origH: rect.height,
+ };
+ document.addEventListener('mousemove', this.onResizeMove);
+ document.addEventListener('mouseup', this.onResizeEnd);
+ event.preventDefault();
+ },
+
+ onResizeMove(event) {
+ if (!this.resizeSession) return;
+ const { dialogId, startX, startY, origW, origH } = this.resizeSession;
+ const dialog = this.dialogs.find((d) => d.id === dialogId);
+ if (!dialog) return;
+ let w = origW + event.clientX - startX;
+ let h = origH + event.clientY - startY;
+ const maxW = window.innerWidth - dialog.position.x - 8;
+ const maxH = window.innerHeight - dialog.position.y - 8;
+ w = Math.min(Math.max(MIN_DIALOG_W, w), maxW);
+ h = Math.min(Math.max(MIN_DIALOG_H, h), maxH);
+ this.updateDialogBounds({ dialogId, width: w, height: h });
+ let nx = dialog.position.x;
+ let ny = dialog.position.y;
+ if (nx + w > window.innerWidth - 8) nx = Math.max(8, window.innerWidth - w - 8);
+ if (ny + h > window.innerHeight - 8) ny = Math.max(8, window.innerHeight - h - 8);
+ if (nx !== dialog.position.x || ny !== dialog.position.y) {
+ this.updateDialogBounds({ dialogId, position: { x: nx, y: ny } });
+ }
+ },
+
+ onResizeEnd() {
+ this.resizeSession = null;
+ document.removeEventListener('mousemove', this.onResizeMove);
+ document.removeEventListener('mouseup', this.onResizeEnd);
},
handleHeaderAction(action) {
@@ -187,8 +304,9 @@ export default {
},
beforeUnmount() {
- // Event-Listener entfernen
window.removeEventListener('message', this.handlePostMessage);
+ if (this.dragSession) this.onDragEnd();
+ if (this.resizeSession) this.onResizeEnd();
}
};
@@ -206,8 +324,11 @@ export default {
.dialog-window {
position: absolute;
- width: 90vw;
- height: 90vh;
+ box-sizing: border-box;
+ min-width: 280px;
+ min-height: 200px;
+ width: min(90vw, calc(100vw - 32px));
+ height: min(90vh, calc(100vh - 32px));
background: white;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
@@ -215,8 +336,6 @@ export default {
display: flex;
flex-direction: column;
overflow: hidden;
- left: 5vw !important;
- top: 5vh !important;
}
.dialog-header {
@@ -226,8 +345,13 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
- cursor: move;
+ cursor: grab;
user-select: none;
+ flex-shrink: 0;
+}
+
+.dialog-header:active {
+ cursor: grabbing;
}
.dialog-title {
@@ -276,6 +400,31 @@ export default {
flex: 1;
padding: 0;
overflow-y: auto;
+ min-height: 0;
+}
+
+.dialog-resize-handle {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 16px;
+ height: 16px;
+ cursor: nwse-resize;
+ background: linear-gradient(
+ 135deg,
+ transparent 50%,
+ rgba(0, 0, 0, 0.1) 50%
+ );
+ border-bottom-right-radius: 8px;
+ z-index: 2;
+}
+
+.dialog-resize-handle:hover {
+ background: linear-gradient(
+ 135deg,
+ transparent 45%,
+ rgba(47, 122, 95, 0.35) 45%
+ );
}
.minimized-dialogs {
@@ -321,10 +470,8 @@ export default {
/* Responsive Design */
@media (max-width: 768px) {
.dialog-window {
- width: 95vw;
- height: 95vh;
- left: 2.5vw !important;
- top: 2.5vh !important;
+ max-width: calc(100vw - 16px);
+ max-height: calc(100vh - 16px);
}
.minimized-dialogs {
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index b1acb459..fb038b62 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -2078,7 +2078,8 @@
},
"baseDialog": {
"minimize": "Minimieren",
- "close": "Schließen"
+ "close": "Schließen",
+ "resize": "Größe ändern"
},
"dialogManager": {
"noMinimizedDialogs": "Keine minimierten Dialoge",
diff --git a/frontend/src/store.js b/frontend/src/store.js
index 2ac71405..a65cc371 100644
--- a/frontend/src/store.js
+++ b/frontend/src/store.js
@@ -109,15 +109,39 @@ const store = createStore({
// Dialog-Mutations
openDialog(state, dialog) {
const id = ++state.dialogCounter;
+ let position = { x: 48, y: 48 };
+ if (
+ dialog.position &&
+ Number.isFinite(dialog.position.x) &&
+ Number.isFinite(dialog.position.y)
+ ) {
+ position = { x: dialog.position.x, y: dialog.position.y };
+ } else if (typeof window !== 'undefined') {
+ position = {
+ x: Math.max(16, Math.round(window.innerWidth * 0.05)),
+ y: Math.max(16, Math.round(window.innerHeight * 0.05)),
+ };
+ }
const newDialog = {
id,
...dialog,
isMinimized: false,
zIndex: 1000 + state.dialogs.length,
- position: { x: 0, y: 0 } // Position wird durch CSS festgelegt
+ position,
+ width: dialog.width != null ? dialog.width : null,
+ height: dialog.height != null ? dialog.height : null,
};
state.dialogs.push(newDialog);
},
+ updateDialogBounds(state, { dialogId, position, width, height }) {
+ const d = state.dialogs.find((x) => x.id === dialogId);
+ if (!d) return;
+ if (position && Number.isFinite(position.x) && Number.isFinite(position.y)) {
+ d.position = { x: position.x, y: position.y };
+ }
+ if (width !== undefined) d.width = width;
+ if (height !== undefined) d.height = height;
+ },
closeDialog(state, dialogId) {
state.dialogs = state.dialogs.filter(dialog => dialog.id !== dialogId);
},
@@ -219,7 +243,10 @@ const store = createStore({
},
bringDialogToFront({ commit }, dialogId) {
commit('bringDialogToFront', dialogId);
- }
+ },
+ updateDialogBounds({ commit }, payload) {
+ commit('updateDialogBounds', payload);
+ },
},
getters: {
isAuthenticated: state => !!state.token,