feat(CourtDrawing): enhance CourtDrawingDialog and CourtDrawingTool components

- Updated CourtDrawingDialog to improve layout and responsiveness, including a new drawing dialog body structure and styling adjustments.
- Enhanced CourtDrawingTool with a comprehensive exercise selection workflow, allowing users to configure exercises with detailed options for start positions, strokes, and targets.
- Introduced new UI elements for better user interaction and streamlined the process of adding additional strokes.
- Improved overall styling for a more cohesive and user-friendly experience.
This commit is contained in:
Torsten Schulz (local)
2026-03-18 21:50:31 +01:00
parent 76cc8d9c30
commit 269f648ad7
2 changed files with 624 additions and 311 deletions

View File

@@ -4,40 +4,46 @@
@update:model-value="$emit('update:modelValue', $event)"
:title="$t('courtDrawing.title')"
size="large"
width="92vw"
:max-width="1500"
:close-on-overlay="false"
@close="handleClose"
>
<CourtDrawingTool
ref="drawingTool"
:drawing-data="initialDrawingData"
:allow-image-upload="false"
@update-drawing-data="handleDrawingDataUpdate"
@update-fields="handleFieldsUpdate"
/>
<div v-if="showDiaryFields" class="diary-fields">
<label v-if="showDurationFields" class="diary-field">
<span>{{ $t('courtDrawing.durationText') }}</span>
<input
type="text"
v-model="diaryFields.durationText"
:placeholder="$t('courtDrawing.durationTextPlaceholder')"
@input="calculateDurationFromText"
<div class="drawing-dialog-body">
<div class="drawing-tool-shell">
<CourtDrawingTool
ref="drawingTool"
:drawing-data="initialDrawingData"
:allow-image-upload="false"
@update-drawing-data="handleDrawingDataUpdate"
@update-fields="handleFieldsUpdate"
/>
</label>
<label v-if="showDurationFields" class="diary-field">
<span>{{ $t('courtDrawing.durationMinutes') }}</span>
<input type="number" min="0" :value="diaryFields.duration" readonly />
</label>
<label v-if="showGroupSelect" class="diary-field">
<span>{{ $t('courtDrawing.group') }}</span>
<select v-model="diaryFields.groupId">
<option value="">{{ $t('courtDrawing.selectGroup') }}</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}
</option>
</select>
</label>
</div>
<div v-if="showDiaryFields" class="diary-fields">
<label v-if="showDurationFields" class="diary-field">
<span>{{ $t('courtDrawing.durationText') }}</span>
<input
type="text"
v-model="diaryFields.durationText"
:placeholder="$t('courtDrawing.durationTextPlaceholder')"
@input="calculateDurationFromText"
/>
</label>
<label v-if="showDurationFields" class="diary-field">
<span>{{ $t('courtDrawing.durationMinutes') }}</span>
<input type="number" min="0" :value="diaryFields.duration" readonly />
</label>
<label v-if="showGroupSelect" class="diary-field">
<span>{{ $t('courtDrawing.group') }}</span>
<select v-model="diaryFields.groupId">
<option value="">{{ $t('courtDrawing.selectGroup') }}</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}
</option>
</select>
</label>
</div>
</div>
<template #footer>
@@ -266,6 +272,23 @@ export default {
</script>
<style scoped>
:deep(.dialog-body) {
overflow: hidden;
}
.drawing-dialog-body {
display: flex;
flex-direction: column;
gap: 0.75rem;
height: min(78vh, calc(100vh - 170px));
min-height: 0;
}
.drawing-tool-shell {
flex: 1;
min-height: 0;
}
.btn-primary,
.btn-secondary {
padding: 0.5rem 1.5rem;

View File

@@ -1,12 +1,207 @@
<template>
<div class="court-drawing-tool">
<div class="tool-header">
<h4>{{ $t('courtDrawingTool.title') }}</h4>
<div class="tool-layout">
<div class="exercise-selection">
<h5>{{ $t('courtDrawingTool.configureExercise') }}</h5>
<div class="workflow-grid">
<section class="workflow-card">
<button type="button" class="workflow-toggle" @click="toggleStartSection">
<div>
<div class="workflow-step">1. Startposition</div>
<p class="workflow-copy workflow-copy-compact">Aus welcher Aufschlagposition startet die Übung?</p>
</div>
<span class="workflow-toggle-icon">{{ startSectionExpanded ? '' : '+' }}</span>
</button>
<div v-if="startSectionExpanded" class="workflow-section-body">
<div class="choice-row">
<button
v-for="start in startPositionOptions"
:key="start.value"
type="button"
:class="['choice-chip', { 'choice-chip-active': selectedStartPosition === start.value }]"
@click="selectStartPosition(start.value)"
>
<strong>{{ start.short }}</strong>
<span>{{ start.label }}</span>
</button>
</div>
</div>
</section>
<section class="workflow-card">
<button type="button" class="workflow-toggle" @click="toggleFirstStrokeSection">
<div>
<div class="workflow-step">2. 1. Schlag</div>
<p class="workflow-copy workflow-copy-compact">Schlagseite und Rotation für den ersten Ball festlegen.</p>
</div>
<span class="workflow-toggle-icon">{{ firstStrokeSectionExpanded ? '' : '+' }}</span>
</button>
<div v-if="firstStrokeSectionExpanded" class="workflow-section-body">
<div class="workflow-block">
<span class="group-label">{{ $t('courtDrawingTool.service') }}</span>
<div class="choice-row">
<button
type="button"
:class="['choice-chip', { 'choice-chip-active': strokeType === 'VH' }]"
@click="strokeType = 'VH'"
>
<strong>VH</strong>
<span>{{ $t('courtDrawingTool.forehand') }}</span>
</button>
<button
type="button"
:class="['choice-chip', { 'choice-chip-active': strokeType === 'RH' }]"
@click="strokeType = 'RH'"
>
<strong>RH</strong>
<span>{{ $t('courtDrawingTool.backhand') }}</span>
</button>
</div>
</div>
<div class="workflow-block">
<span class="group-label">{{ $t('courtDrawingTool.spin') }}</span>
<div class="choice-row choice-row-dense">
<button
v-for="option in mainSpinOptions"
:key="option.value"
type="button"
:class="['choice-chip', 'choice-chip-compact', { 'choice-chip-active': spinType === option.value }]"
@click="spinType = option.value"
>
<strong>{{ option.short }}</strong>
<span>{{ option.label }}</span>
</button>
</div>
</div>
</div>
</section>
<section class="workflow-card">
<button type="button" class="workflow-toggle" @click="toggleTargetSection">
<div>
<div class="workflow-step">3. Ziel</div>
<p class="workflow-copy workflow-copy-compact">Zielzone für den ersten Schlag auswählen.</p>
</div>
<span class="workflow-toggle-icon">{{ targetSectionExpanded ? '' : '+' }}</span>
</button>
<div v-if="targetSectionExpanded" class="workflow-section-body">
<div class="workflow-block">
<span class="group-label">{{ $t('courtDrawingTool.targetPosition') }}</span>
<div class="target-grid target-grid-large">
<button
v-for="n in mainTargetPositions"
:key="`main-target-${n}`"
type="button"
:class="['grid-btn', { 'is-active': targetPosition === String(n) }]"
@click="selectMainTargetPosition(n)"
:disabled="!spinType"
>
{{ n }}
</button>
</div>
</div>
</div>
</section>
<section class="workflow-card workflow-card-wide" v-if="strokeType && spinType && targetPosition">
<div class="workflow-step">4. Folgeschläge</div>
<p class="workflow-copy">Optional weitere Bälle als Liste aufbauen.</p>
<div v-if="additionalStrokes.length" class="stroke-sequence-list">
<div
v-for="(stroke, index) in additionalStrokes"
:key="`${stroke.counter}-${index}`"
class="stroke-sequence-item"
>
<div class="stroke-sequence-index">{{ index + 2 }}</div>
<div class="stroke-sequence-copy">{{ formatAdditionalStroke(stroke) }}</div>
<button type="button" class="stroke-sequence-remove" @click="removeAdditionalStroke(index)">Entfernen</button>
</div>
</div>
<div v-else class="stroke-sequence-empty">Noch keine Folgeschläge angelegt.</div>
<div class="next-stroke-selection">
<div class="workflow-block">
<span class="group-label">Seite</span>
<div class="choice-row">
<button
type="button"
:class="['choice-chip', { 'choice-chip-active': nextStrokeSide === 'VH' }]"
@click="nextStrokeSide = 'VH'"
>
<strong>VH</strong>
<span>{{ $t('courtDrawingTool.forehand') }}</span>
</button>
<button
type="button"
:class="['choice-chip', { 'choice-chip-active': nextStrokeSide === 'RH' }]"
@click="nextStrokeSide = 'RH'"
>
<strong>RH</strong>
<span>{{ $t('courtDrawingTool.backhand') }}</span>
</button>
</div>
</div>
<div class="workflow-block">
<span class="group-label">Schlagart</span>
<div class="choice-row choice-row-dense">
<button
v-for="option in additionalStrokeTypeOptions"
:key="option.value"
type="button"
:class="['choice-chip', 'choice-chip-compact', { 'choice-chip-active': nextStrokeType === option.value }]"
@click="nextStrokeType = option.value"
:title="option.label"
>
<strong>{{ option.short }}</strong>
<span>{{ option.label }}</span>
</button>
</div>
</div>
<div class="workflow-block">
<span class="group-label">Zielposition</span>
<div class="target-grid target-grid-large">
<button
v-for="n in additionalTargetPositions"
:key="`next-target-${n}`"
type="button"
:class="['grid-btn', { 'is-active': nextStrokeTargetPosition === String(n) }]"
@click="selectAdditionalTargetPosition(n)"
:disabled="additionalStrokes.length >= 4"
>
{{ n }}
</button>
</div>
</div>
<button
type="button"
class="btn-primary add-stroke-button"
@click="addNextStroke"
:disabled="additionalStrokes.length >= 4 || !nextStrokeTargetPosition"
>
Schlag hinzufügen
</button>
</div>
</section>
</div>
<div class="exercise-info" v-if="strokeType && spinType">
<p><strong>Kürzel:</strong> {{ getFullCode() }}</p>
<p><strong>Titel:</strong> {{ getFullTitle() }}</p>
</div>
</div>
<div class="canvas-container">
<canvas
ref="drawingCanvas"
<aside class="preview-panel">
<div class="preview-panel-header">
<div class="workflow-step">Vorschau</div>
<p class="workflow-copy">Die Grafik erscheint, sobald der erste Schlag vollständig gesetzt ist.</p>
</div>
<div v-if="isPrimaryStrokeReady" class="canvas-container">
<canvas
ref="drawingCanvas"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@@ -14,224 +209,13 @@
:width="config.canvas.width"
:height="config.canvas.height"
></canvas>
</div>
<div v-else class="canvas-placeholder">
<strong>1. Schlag vervollständigen</strong>
<span>Wähle Startposition, Schlagseite, Rotation und Ziel. Danach erscheint die grafische Darstellung.</span>
</div>
</aside>
</div>
<!-- Startposition und Schlagart Auswahl -->
<div class="exercise-selection" v-if="selectedStartPosition">
<h5>{{ $t('courtDrawingTool.configureExercise') }}</h5>
<div class="selection-group">
<!-- Schlagart Auswahl -->
<div class="stroke-selection">
<div>
<span class="group-label">{{ $t('courtDrawingTool.service') }}</span>
<div class="stroke-buttons">
<button
type="button"
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'VH', 'btn-secondary': strokeType !== 'VH' }]"
@click="strokeType = 'VH'"
:title="$t('courtDrawingTool.forehand')"
>
VH
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'RH', 'btn-secondary': strokeType !== 'RH' }]"
@click="strokeType = 'RH'"
:title="$t('courtDrawingTool.backhand')"
>
RH
</button>
</div>
</div>
<!-- Schnittoption Auswahl -->
<div class="spin-selection">
<span class="group-label">{{ $t('courtDrawingTool.spin') }}</span>
<div class="spin-buttons">
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Unterschnitt', 'btn-secondary': spinType !== 'Unterschnitt' }]"
@click="spinType = 'Unterschnitt'"
:title="$t('courtDrawingTool.underspin')"
>
US
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Überschnitt', 'btn-secondary': spinType !== 'Überschnitt' }]"
@click="spinType = 'Überschnitt'"
:title="$t('courtDrawingTool.topspin')"
>
OS
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitschnitt', 'btn-secondary': spinType !== 'Seitschnitt' }]"
@click="spinType = 'Seitschnitt'"
:title="$t('courtDrawingTool.sidespin')"
>
SR
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitunterschnitt', 'btn-secondary': spinType !== 'Seitunterschnitt' }]"
@click="spinType = 'Seitunterschnitt'"
:title="$t('courtDrawingTool.sideUnderspin')"
>
SU
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Gegenläufer', 'btn-secondary': spinType !== 'Gegenläufer' }]"
@click="spinType = 'Gegenläufer'"
:title="$t('courtDrawingTool.counterSpin')"
>
GL
</button>
</div>
</div>
<!-- Zielposition für Hauptschlag: explizite Auswahl 19 als Buttons -->
<div class="target-selection" v-if="spinType">
<span class="group-label">{{ $t('courtDrawingTool.targetPosition') }}</span>
<div class="target-grid">
<button
v-for="n in mainTargetPositions"
:key="`main-target-${n}`"
type="button"
:class="['grid-btn', { 'is-active': targetPosition === String(n) }]"
@click="targetPosition = String(n)"
>
{{ n }}
</button>
</div>
</div>
</div>
<!-- Zusätzliche Schläge hinzufügen -->
<div class="additional-strokes" v-if="strokeType && spinType && targetPosition">
<div class="next-stroke-selection">
<!-- Schlagart für zusätzlichen Schlag -->
<div class="next-stroke-type">
<span class="group-label">Seite:</span>
<button
type="button"
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'VH', 'btn-secondary': nextStrokeSide !== 'VH' }]"
@click="nextStrokeSide = 'VH'"
:title="$t('courtDrawingTool.forehand')"
>
VH
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'RH', 'btn-secondary': nextStrokeSide !== 'RH' }]"
@click="nextStrokeSide = 'RH'"
:title="$t('courtDrawingTool.backhand')"
>
RH
</button>
</div>
<div class="next-stroke-buttons">
<span class="group-label">Schlagart:</span>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'US', 'btn-secondary': nextStrokeType !== 'US' }]"
@click="nextStrokeType = 'US'"
title="Schupf"
>
US
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'OS', 'btn-secondary': nextStrokeType !== 'OS' }]"
@click="nextStrokeType = 'OS'"
title="Konter"
>
OS
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'TS', 'btn-secondary': nextStrokeType !== 'TS' }]"
@click="nextStrokeType = 'TS'"
title="Topspin"
>
TS
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'F', 'btn-secondary': nextStrokeType !== 'F' }]"
@click="nextStrokeType = 'F'"
title="Flip"
>
F
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'B', 'btn-secondary': nextStrokeType !== 'B' }]"
@click="nextStrokeType = 'B'"
title="Block"
>
B
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'SCH', 'btn-secondary': nextStrokeType !== 'SCH' }]"
@click="nextStrokeType = 'SCH'"
title="Schuss"
>
SCH
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'SAB', 'btn-secondary': nextStrokeType !== 'SAB' }]"
@click="nextStrokeType = 'SAB'"
title="Schnittabwehr"
>
SAB
</button>
<button
type="button"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'BAB', 'btn-secondary': nextStrokeType !== 'BAB' }]"
@click="nextStrokeType = 'BAB'"
title="Ballonabwehr"
>
BAB
</button>
</div>
<!-- Zielposition für Zusatzschlag: explizite Auswahl 19 als Buttons -->
<div class="next-target-selection">
<span>Zielposition:</span>
<div class="target-grid">
<button
v-for="n in additionalTargetPositions"
:key="`next-target-${n}`"
type="button"
:class="['grid-btn', { 'is-active': nextStrokeTargetPosition === String(n) }]"
@click="nextStrokeTargetPosition = String(n)"
>
{{ n }}
</button>
</div>
</div>
<button
type="button"
class="btn-primary btn-small add-stroke-button"
@click="addNextStroke"
:disabled="additionalStrokes.length >= 4"
>
Schlag hinzufügen
</button>
</div>
</div>
<div class="exercise-info" v-if="strokeType && spinType">
<p><strong>Kürzel:</strong> {{ getFullCode() }}</p>
<p><strong>Titel:</strong> {{ getFullTitle() }}</p>
</div>
</div>
</div>
<!-- Buttons entfernt: automatische Speicherung aktiv -->
</div>
@@ -394,10 +378,44 @@ export default {
additionalStrokes: [],
nextStrokeType: 'US',
nextStrokeSide: 'VH',
nextStrokeTargetPosition: ''
nextStrokeTargetPosition: '',
startSectionExpanded: true,
firstStrokeSectionExpanded: true,
targetSectionExpanded: true
}
},
computed: {
isPrimaryStrokeReady() {
return Boolean(this.selectedStartPosition && this.strokeType && this.spinType && this.targetPosition);
},
startPositionOptions() {
return [
{ value: 'AS1', short: 'AS1', label: 'links' },
{ value: 'AS2', short: 'AS2', label: 'mitte' },
{ value: 'AS3', short: 'AS3', label: 'rechts' }
];
},
mainSpinOptions() {
return [
{ value: 'Unterschnitt', short: 'US', label: this.$t('courtDrawingTool.underspin') },
{ value: 'Überschnitt', short: 'OS', label: this.$t('courtDrawingTool.topspin') },
{ value: 'Seitschnitt', short: 'SR', label: this.$t('courtDrawingTool.sidespin') },
{ value: 'Seitunterschnitt', short: 'SU', label: this.$t('courtDrawingTool.sideUnderspin') },
{ value: 'Gegenläufer', short: 'GL', label: this.$t('courtDrawingTool.counterSpin') }
];
},
additionalStrokeTypeOptions() {
return [
{ value: 'US', short: 'US', label: 'Schupf' },
{ value: 'OS', short: 'OS', label: 'Konter' },
{ value: 'TS', short: 'TS', label: 'Topspin' },
{ value: 'F', short: 'F', label: 'Flip' },
{ value: 'B', short: 'B', label: 'Block' },
{ value: 'SCH', short: 'SCH', label: 'Schuss' },
{ value: 'SAB', short: 'SAB', label: 'Schnittabwehr' },
{ value: 'BAB', short: 'BAB', label: 'Ballonabwehr' }
];
},
// Reihenfolge der Positionen für Hauptschlag basierend auf Richtung
mainTargetPositions() {
const isLeftToRight = this.isMainStrokeLeftToRight();
@@ -433,6 +451,17 @@ export default {
this.emitDrawingData();
this.updateTextFields();
},
isPrimaryStrokeReady(newVal) {
if (newVal) {
this.startSectionExpanded = false;
this.firstStrokeSectionExpanded = false;
this.targetSectionExpanded = false;
this.$nextTick(() => {
this.initCanvas();
this.drawCourt(true);
});
}
},
selectedCirclePosition() {
// bei Wechsel der Kreisposition: Daten + Strings aktualisieren
this.emitDrawingData();
@@ -473,6 +502,68 @@ export default {
});
},
methods: {
toggleStartSection() {
this.startSectionExpanded = !this.startSectionExpanded;
},
toggleFirstStrokeSection() {
this.firstStrokeSectionExpanded = !this.firstStrokeSectionExpanded;
},
toggleTargetSection() {
this.targetSectionExpanded = !this.targetSectionExpanded;
},
selectStartPosition(position) {
this.selectedStartPosition = position;
this.selectedCirclePosition = position === 'AS1' ? 'top' : position === 'AS3' ? 'bottom' : 'middle';
this.drawCourt();
this.emitDrawingData();
this.updateTextFields();
},
selectMainTargetPosition(position) {
this.targetPosition = String(position);
if (this.isPrimaryStrokeReady) {
this.$nextTick(() => {
this.initCanvas();
this.drawCourt(true);
});
}
this.emitDrawingData();
this.updateTextFields();
},
selectAdditionalTargetPosition(position) {
this.nextStrokeTargetPosition = String(position);
this.emitDrawingData();
},
formatStrokeSide(side) {
return side === 'VH' ? this.$t('courtDrawingTool.forehand') : this.$t('courtDrawingTool.backhand');
},
formatAdditionalStrokeType(type) {
const option = this.additionalStrokeTypeOptions.find(entry => entry.value === type);
return option ? option.label : type;
},
formatTargetPosition(position) {
const labels = {
'1': 'Vorhand lang',
'2': 'Mitte lang',
'3': 'Rückhand lang',
'4': 'Vorhand halblang',
'5': 'Mitte halblang',
'6': 'Rückhand halblang',
'7': 'Vorhand kurz',
'8': 'Mitte kurz',
'9': 'Rückhand kurz'
};
return labels[String(position)] || String(position);
},
formatAdditionalStroke(stroke) {
return `${this.formatStrokeSide(stroke.side)} ${this.formatAdditionalStrokeType(stroke.type)} nach ${this.formatTargetPosition(stroke.targetPosition)}`;
},
removeAdditionalStroke(index) {
this.additionalStrokes.splice(index, 1);
this.nextStrokeTargetPosition = '';
this.updateTextFields();
this.drawCourt();
this.emitDrawingData();
},
initCanvas() {
this.canvas = this.$refs.drawingCanvas;
if (this.canvas) {
@@ -1642,12 +1733,15 @@ export default {
<style scoped>
.court-drawing-tool {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 1rem;
background: var(--surface-color, #ffffff);
margin: 1rem 0;
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.06);
min-height: 0;
height: 100%;
}
.tool-header {
@@ -1662,21 +1756,75 @@ export default {
color: var(--text-color);
}
.tool-layout {
display: grid;
grid-template-columns: minmax(360px, 430px) minmax(0, 1fr);
gap: 1rem;
align-items: stretch;
min-height: 0;
flex: 1;
overflow: hidden;
}
.tool-controls {
display: flex;
gap: 0.5rem;
}
.preview-panel {
display: flex;
flex-direction: column;
gap: 0.85rem;
min-height: 0;
align-self: stretch;
}
.exercise-selection {
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
max-height: 100%;
padding-right: 0.25rem;
padding: 1rem;
background: var(--background-soft);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.preview-panel-header {
padding: 0.9rem;
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--surface-color, #ffffff);
}
.canvas-container {
text-align: center;
margin: 1rem 0;
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--background-soft);
padding: 0.75rem;
}
.canvas-placeholder {
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 1rem;
border: 1px dashed var(--border-color);
border-radius: 12px;
background: var(--background-soft);
color: var(--text-muted);
}
.canvas-placeholder strong {
color: var(--text-color);
}
canvas {
max-width: 100%;
height: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: crosshair;
@@ -1784,81 +1932,141 @@ input[type="range"] {
width: 100px;
}
.exercise-selection {
margin-top: 1rem;
padding: 1rem;
background: var(--background-soft);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.exercise-selection h5 {
margin: 0 0 1rem 0;
color: var(--text-color);
}
.selection-group {
.workflow-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.9rem;
}
.workflow-card {
padding: 0.9rem;
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--surface-color, #ffffff);
}
.workflow-card-wide {
grid-column: 1 / -1;
}
.workflow-step {
margin-bottom: 0.35rem;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--primary-color);
}
.workflow-toggle {
width: 100%;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
padding: 0;
border: none;
background: transparent;
text-align: left;
cursor: pointer;
}
.workflow-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 999px;
background: rgba(29, 90, 61, 0.1);
color: var(--primary-color);
font-size: 1rem;
font-weight: 700;
flex-shrink: 0;
}
.workflow-copy {
margin: 0 0 0.85rem 0;
color: var(--text-muted);
font-size: 0.9rem;
}
.workflow-copy-compact {
margin-bottom: 0;
}
.workflow-section-body {
margin-top: 0.85rem;
}
.workflow-block + .workflow-block {
margin-top: 0.9rem;
}
.choice-row {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
}
.choice-row-dense {
gap: 0.45rem;
}
.choice-chip {
display: inline-flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
justify-content: center;
min-width: 90px;
gap: 0.15rem;
padding: 0.55rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--surface-color, #ffffff);
color: var(--text-color);
cursor: pointer;
transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease;
}
.stroke-selection {
margin-top: 0;
display: flex;
flex-direction: row;
gap: 0.5rem;
vertical-align: middle;
.choice-chip:hover {
transform: translateY(-1px);
border-color: var(--primary-soft);
}
.stroke-selection label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
.choice-chip strong {
font-size: 0.82rem;
line-height: 1;
}
.choice-chip span {
font-size: 0.78rem;
color: var(--text-muted);
}
.stroke-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: nowrap;
.choice-chip-active {
background: linear-gradient(135deg, rgba(29, 90, 61, 0.1), rgba(29, 90, 61, 0.16));
border-color: var(--primary-color);
}
.spin-selection {
margin-top: 0;
.choice-chip-active span {
color: var(--text-color);
}
.spin-selection label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-muted);
}
.spin-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: nowrap;
}
.additional-strokes {
margin-top: 1rem;
.choice-chip-compact {
min-width: 84px;
}
.next-stroke-selection {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.next-stroke-type {
gap: 0.5rem;
flex-wrap: nowrap;
}
.next-stroke-buttons {
gap: 0.5rem;
flex-wrap: nowrap;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.9rem;
align-items: end;
}
.exercise-info {
@@ -1866,6 +2074,7 @@ input[type="range"] {
padding: 0.75rem;
border-radius: 10px;
border: 1px solid var(--border-color);
margin-top: 1rem;
}
.exercise-info p {
@@ -1881,6 +2090,12 @@ input[type="range"] {
gap: 0;
}
.target-grid-large {
grid-template-columns: repeat(3, 44px);
grid-auto-rows: 44px;
gap: 0.35rem;
}
.grid-btn {
display: inline-flex;
align-items: center;
@@ -1900,12 +2115,70 @@ input[type="range"] {
border: 1px solid rgba(29, 90, 61, 0.14);
}
.target-grid-large .grid-btn {
width: 44px;
height: 44px;
border-radius: 10px;
font-size: 0.92rem;
}
.grid-btn.is-active {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
font-weight: 700; /* fett */
}
.stroke-sequence-list {
display: flex;
flex-direction: column;
gap: 0.55rem;
margin-bottom: 0.9rem;
}
.stroke-sequence-item {
display: grid;
grid-template-columns: 32px 1fr auto;
gap: 0.75rem;
align-items: center;
padding: 0.6rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--background-soft);
}
.stroke-sequence-index {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 999px;
background: rgba(29, 90, 61, 0.1);
color: var(--primary-color);
font-weight: 700;
}
.stroke-sequence-copy {
color: var(--text-color);
font-size: 0.92rem;
}
.stroke-sequence-remove {
border: 1px solid rgba(200, 74, 56, 0.22);
background: rgba(200, 74, 56, 0.08);
color: #8b3327;
border-radius: 8px;
padding: 0.45rem 0.65rem;
cursor: pointer;
}
.stroke-sequence-empty {
margin-bottom: 0.9rem;
padding: 0.75rem;
border-radius: 10px;
background: rgba(29, 90, 61, 0.06);
color: var(--text-muted);
}
.btn-stroke.btn-primary {
color: COURT_TOOL_PALETTE.accentText !important;
font-weight: 700;
@@ -1917,6 +2190,23 @@ input[type="range"] {
}
.add-stroke-button {
align-self: flex-end;
align-self: start;
min-height: 44px;
}
@media (max-width: 720px) {
.tool-layout,
.workflow-grid,
.next-stroke-selection {
grid-template-columns: 1fr;
}
.preview-panel {
position: static;
}
.choice-chip {
min-width: calc(50% - 0.4rem);
}
}
</style>