css aufgeräumt

This commit is contained in:
2026-02-12 08:35:53 +01:00
parent fb4ee93b17
commit b469a7ab33
5 changed files with 507 additions and 265 deletions

View File

@@ -0,0 +1,301 @@
.device-type-edit {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.device-type-edit .edit-form {
background: white;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.device-type-edit .edit-form fieldset {
margin: 20px 0;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.device-type-edit .edit-form legend {
padding: 0 10px;
font-weight: bold;
font-size: 1.1em;
}
.device-type-edit .form-group {
margin: 15px 0;
}
.device-type-edit .form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.device-type-edit .form-group input[type="text"],
.device-type-edit .form-group input[type="file"],
.device-type-edit .form-group select,
.device-type-edit .form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: inherit;
}
.device-type-edit .form-group textarea {
resize: vertical;
}
.device-type-edit .form-group small {
display: block;
margin-top: 5px;
color: #666;
}
.device-type-edit .required {
color: red;
}
.device-type-edit .form-file-preview {
margin-top: 10px;
}
.device-type-edit .device-type-current-image {
max-width: 300px;
border: 1px solid #ddd;
padding: 10px;
display: block;
}
.device-type-edit .port-definition-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.device-type-edit .port-definition-table th,
.device-type-edit .port-definition-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
vertical-align: middle;
}
.device-type-edit .port-definition-table th {
background: #f5f5f5;
}
.device-type-edit .port-definition-table input[type="text"],
.device-type-edit .port-definition-table select {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
.device-type-edit .form-actions {
display: flex;
gap: 10px;
margin-top: 30px;
}
.device-type-edit .shape-editor {
display: grid;
grid-template-columns: 180px 1fr 320px;
gap: 16px;
margin-top: 16px;
}
.device-type-edit .shape-meta-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.device-type-edit .shape-meta-column {
display: flex;
flex-direction: column;
gap: 4px;
}
.device-type-edit .shape-meta-column label {
font-weight: 600;
font-size: 0.9em;
}
.device-type-edit .shape-meta-column input,
.device-type-edit .shape-meta-column select {
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
font-size: 0.95em;
}
.device-type-edit .port-actions {
margin-top: 15px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.device-type-edit .shape-editor-canvas {
border: 1px solid #ddd;
border-radius: 6px;
background: white;
padding: 12px;
}
.device-type-edit .shape-editor-canvas svg {
width: 100%;
min-height: 320px;
display: block;
font-family: inherit;
cursor: crosshair;
}
.device-type-edit .shape-toolbox,
.device-type-edit .shape-overlay {
border: 1px solid #ddd;
border-radius: 6px;
padding: 12px;
background: #fff;
}
.device-type-edit .shape-toolbox h4,
.device-type-edit .shape-overlay h4 {
margin: 0 0 8px;
}
.device-type-edit .shape-tool-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.device-type-edit .shape-tool {
text-align: left;
border: 1px solid #bbb;
background: #f7f7f7;
color: #222;
padding: 10px;
border-radius: 4px;
cursor: grab;
}
.device-type-edit .shape-tool:active {
cursor: grabbing;
}
.device-type-edit .shape-tool.is-active {
background: #007bff;
color: #fff;
border-color: #0056b3;
}
.device-type-edit .shape-editor-canvas.drag-over {
outline: 2px dashed #007bff;
outline-offset: 2px;
}
.device-type-edit .shape-object {
cursor: move;
}
.device-type-edit #shape-canvas.shape-tool-active {
cursor: crosshair;
}
.device-type-edit .shape-object.is-selected {
filter: drop-shadow(0 0 5px rgba(0, 123, 255, 0.7));
}
.device-type-edit .shape-object.is-port {
stroke-dasharray: 4 2;
}
.device-type-edit .shape-overlay-form .shape-control-grid {
display: grid;
grid-template-columns: repeat(2, minmax(120px, 1fr));
gap: 8px;
}
.device-type-edit .shape-overlay-form label {
font-size: 0.82rem;
display: flex;
flex-direction: column;
gap: 4px;
}
.device-type-edit .shape-overlay-form input[type="number"],
.device-type-edit .shape-overlay-form input[type="text"],
.device-type-edit .shape-overlay-form input[type="color"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
.device-type-edit .shape-port-settings {
margin-top: 10px;
}
.device-type-edit .inline-checkbox {
flex-direction: row !important;
align-items: center;
gap: 6px !important;
}
.device-type-edit .shape-overlay-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.device-type-edit .shape-overlay-empty {
margin: 6px 0 0;
color: #666;
font-size: 0.9rem;
}
.device-type-edit .hint {
font-size: 0.8rem;
color: #666;
margin: 8px 0 0;
}
.device-type-edit .button {
padding: 10px 15px;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.95em;
}
.device-type-edit .button-primary {
background: #28a745;
}
.device-type-edit .button-danger {
background: #dc3545;
}
.device-type-edit .button:hover {
opacity: 0.8;
}
@media (max-width: 1100px) {
.device-type-edit .shape-editor {
grid-template-columns: 1fr;
}
}

View File

@@ -1,6 +1,12 @@
(() => { (() => {
const SVG_NS = 'http://www.w3.org/2000/svg'; const SVG_NS = 'http://www.w3.org/2000/svg';
const MIN_DRAW_SIZE = 4; const MIN_DRAW_SIZE = 4;
const MIN_CANVAS_WIDTH = 200;
const MIN_CANVAS_HEIGHT = 120;
const FORM_FACTOR_PRESETS = {
'19': { width: 760, height: 80 },
'10': { width: 420, height: 80 }
};
function initEditor() { function initEditor() {
const editor = document.getElementById('device-type-shape-editor'); const editor = document.getElementById('device-type-shape-editor');
@@ -31,6 +37,13 @@
deleteButton: document.getElementById('shape-delete') deleteButton: document.getElementById('shape-delete')
}; };
const metaInputs = {
formFactor: document.getElementById('shape-meta-form-factor'),
rackHeight: document.getElementById('shape-meta-rack-height'),
canvasWidth: document.getElementById('shape-meta-canvas-width'),
canvasHeight: document.getElementById('shape-meta-canvas-height')
};
const fieldVisibility = { const fieldVisibility = {
width: editor.querySelector('[data-field="width"]'), width: editor.querySelector('[data-field="width"]'),
height: editor.querySelector('[data-field="height"]'), height: editor.querySelector('[data-field="height"]'),
@@ -53,11 +66,20 @@
offsetY: 0 offsetY: 0
}; };
let selectedShapeId = null; let selectedShapeId = null;
let shapes = normalizeShapeList(readJson(hiddenInput.value)); let shapes = [];
let meta = getDefaultMeta();
const definition = readDefinition(hiddenInput.value);
shapes = normalizeShapeList(definition.shapes);
meta = definition.meta;
bindToolbarEvents(editor); bindToolbarEvents(editor);
bindCanvasPointerEvents(svg); bindCanvasPointerEvents(svg);
bindOverlayEvents(overlay); bindOverlayEvents(overlay);
bindMetaEvents();
applyFormFactorPreset();
applyMetaToInputs();
persist();
render(); render();
function bindToolbarEvents(root) { function bindToolbarEvents(root) {
@@ -218,6 +240,63 @@
}); });
} }
function bindMetaEvents() {
Object.values(metaInputs).forEach((input) => {
if (!input) {
return;
}
const eventName = input.tagName === 'SELECT' ? 'change' : 'input';
input.addEventListener(eventName, handleMetaInputChange);
});
}
function handleMetaInputChange(event) {
const targetId = event?.target?.id;
applyMetaFromInputs();
applyFormFactorPreset(targetId);
applyMetaToInputs();
persist();
render();
}
function applyMetaFromInputs() {
if (!metaInputs.formFactor) {
return;
}
meta.formFactor = normalizeFormFactor(metaInputs.formFactor.value);
meta.heightHe = Math.max(1, toNumberOrDefault(metaInputs.rackHeight.value, meta.heightHe));
meta.canvasWidth = Math.max(MIN_CANVAS_WIDTH, toNumberOrDefault(metaInputs.canvasWidth.value, meta.canvasWidth));
meta.canvasHeight = Math.max(MIN_CANVAS_HEIGHT, toNumberOrDefault(metaInputs.canvasHeight.value, meta.canvasHeight));
}
function applyMetaToInputs() {
if (!metaInputs.formFactor) {
return;
}
metaInputs.formFactor.value = meta.formFactor;
metaInputs.rackHeight.value = meta.heightHe;
metaInputs.canvasWidth.value = meta.canvasWidth;
metaInputs.canvasHeight.value = meta.canvasHeight;
}
function applyFormFactorPreset(triggerId) {
if (!['shape-meta-form-factor', 'shape-meta-rack-height'].includes(triggerId) && triggerId !== undefined) {
return;
}
if (meta.heightHe !== 1) {
return;
}
const preset = FORM_FACTOR_PRESETS[meta.formFactor];
if (!preset) {
return;
}
meta.canvasWidth = Math.max(MIN_CANVAS_WIDTH, preset.width);
meta.canvasHeight = Math.max(MIN_CANVAS_HEIGHT, preset.height);
}
function applyOverlayToSelectedShape() { function applyOverlayToSelectedShape() {
const shape = findShape(selectedShapeId); const shape = findShape(selectedShapeId);
if (!shape) { if (!shape) {
@@ -248,6 +327,7 @@
function render() { function render() {
renderToolState(); renderToolState();
updateCanvasDimensions();
renderCanvas(); renderCanvas();
renderOverlay(); renderOverlay();
} }
@@ -261,7 +341,8 @@
} }
function renderCanvas() { function renderCanvas() {
svg.querySelectorAll('.shape-object').forEach((el) => el.remove()); svg.querySelectorAll('.shape-object, .shape-auto-frame').forEach((el) => el.remove());
renderAutoFrame();
shapes.forEach((shape) => { shapes.forEach((shape) => {
const element = createSvgShapeElement(shape); const element = createSvgShapeElement(shape);
@@ -280,6 +361,39 @@
}); });
} }
function renderAutoFrame() {
if (!['19', '10'].includes(meta.formFactor)) {
return;
}
const padding = 4;
const frameWidth = Math.max(0, meta.canvasWidth - padding * 2);
const frameHeight = Math.max(0, meta.canvasHeight - padding * 2);
if (frameWidth <= 0 || frameHeight <= 0) {
return;
}
const frame = document.createElementNS(SVG_NS, 'rect');
frame.setAttribute('x', String(padding));
frame.setAttribute('y', String(padding));
frame.setAttribute('width', String(frameWidth));
frame.setAttribute('height', String(frameHeight));
frame.setAttribute('fill', '#ffffff');
frame.setAttribute('stroke', '#000000');
frame.setAttribute('stroke-width', '1');
frame.setAttribute('class', 'shape-auto-frame');
frame.setAttribute('pointer-events', 'none');
svg.appendChild(frame);
}
function updateCanvasDimensions() {
const width = Math.max(MIN_CANVAS_WIDTH, meta.canvasWidth);
const height = Math.max(MIN_CANVAS_HEIGHT, meta.canvasHeight);
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.style.height = `${height}px`;
svg.style.minHeight = `${height}px`;
}
function renderOverlay() { function renderOverlay() {
const selected = findShape(selectedShapeId); const selected = findShape(selectedShapeId);
const hasSelection = !!selected; const hasSelection = !!selected;
@@ -319,7 +433,16 @@
} }
function persist() { function persist() {
hiddenInput.value = JSON.stringify(shapes); const payload = {
shapes,
meta: {
formFactor: meta.formFactor,
heightHe: meta.heightHe,
canvasWidth: meta.canvasWidth,
canvasHeight: meta.canvasHeight
}
};
hiddenInput.value = JSON.stringify(payload);
} }
function findShape(id) { function findShape(id) {
@@ -542,6 +665,56 @@
} }
} }
function readDefinition(raw) {
const parsed = readJson(raw);
if (Array.isArray(parsed)) {
return {
shapes: parsed,
meta: getDefaultMeta()
};
}
if (parsed && typeof parsed === 'object') {
return {
shapes: Array.isArray(parsed.shapes) ? parsed.shapes : [],
meta: normalizeMeta(parsed.meta ?? parsed)
};
}
return {
shapes: [],
meta: getDefaultMeta()
};
}
function normalizeMeta(source) {
const base = getDefaultMeta();
if (!source || typeof source !== 'object') {
return base;
}
return {
formFactor: normalizeFormFactor(source.formFactor ?? source.rackFormFactor ?? base.formFactor),
heightHe: Math.max(1, toNumberOrDefault(source.heightHe ?? source.rackHeight ?? base.heightHe, base.heightHe)),
canvasWidth: Math.max(MIN_CANVAS_WIDTH, toNumberOrDefault(source.canvasWidth ?? source.width ?? base.canvasWidth, base.canvasWidth)),
canvasHeight: Math.max(MIN_CANVAS_HEIGHT, toNumberOrDefault(source.canvasHeight ?? source.height ?? base.canvasHeight, base.canvasHeight))
};
}
function getDefaultMeta() {
return {
formFactor: 'other',
heightHe: 1,
canvasWidth: 800,
canvasHeight: 360
};
}
function normalizeFormFactor(value) {
const normalized = String(value || '').trim();
return ['10', '19'].includes(normalized) ? normalized : 'other';
}
function normalizeColor(value, fallback) { function normalizeColor(value, fallback) {
const v = String(value || '').trim(); const v = String(value || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(v)) { if (/^#[0-9a-fA-F]{6}$/.test(v)) {

View File

@@ -120,10 +120,11 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
</div> </div>
<?php if ($isEdit && $deviceType['image_path']): ?> <?php if ($isEdit && $deviceType['image_path']): ?>
<div class="form-group"> <div class="form-group form-file-preview">
<label>Aktuelles Bild:</label> <label>Aktuelles Bild:</label>
<img src="<?php echo htmlspecialchars($deviceType['image_path']); ?>" <img class="device-type-current-image"
alt="Gerätetyp-Bild" style="max-width: 300px; border: 1px solid #ddd; padding: 10px;"> src="<?php echo htmlspecialchars($deviceType['image_path']); ?>"
alt="Gerätetyp-Bild">
</div> </div>
<?php endif; ?> <?php endif; ?>
</fieldset> </fieldset>
@@ -131,6 +132,29 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
<fieldset> <fieldset>
<legend>Gerätedesign (SVG-Editor)</legend> <legend>Gerätedesign (SVG-Editor)</legend>
<div class="shape-meta-controls">
<div class="shape-meta-column">
<label for="shape-meta-form-factor">Formfaktor</label>
<select id="shape-meta-form-factor">
<option value="other">Andere/Breiter</option>
<option value="10">10 Zoll Front</option>
<option value="19">19 Zoll Front</option>
</select>
</div>
<div class="shape-meta-column">
<label for="shape-meta-rack-height">Höhe (HE)</label>
<input type="number" id="shape-meta-rack-height" min="1" step="1" value="1">
</div>
<div class="shape-meta-column">
<label for="shape-meta-canvas-width">Zeichenfläche Breite (px)</label>
<input type="number" id="shape-meta-canvas-width" min="200" step="10" value="800">
</div>
<div class="shape-meta-column">
<label for="shape-meta-canvas-height">Zeichenfläche Höhe (px)</label>
<input type="number" id="shape-meta-canvas-height" min="120" step="10" value="360">
</div>
</div>
<input type="hidden" name="shape_definition" id="shape-definition" value="<?php echo htmlspecialchars($shapeDefinition); ?>"> <input type="hidden" name="shape_definition" id="shape-definition" value="<?php echo htmlspecialchars($shapeDefinition); ?>">
<div class="shape-editor" id="device-type-shape-editor"> <div class="shape-editor" id="device-type-shape-editor">
@@ -276,7 +300,7 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
</tbody> </tbody>
</table> </table>
<div style="margin-top: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;"> <div class="port-actions">
<button type="button" class="button" id="add-port-row">+ Port hinzufügen</button> <button type="button" class="button" id="add-port-row">+ Port hinzufügen</button>
<small>Ports können nach dem ersten Speichern jederzeit einzeln bearbeitet und gelöscht werden.</small> <small>Ports können nach dem ersten Speichern jederzeit einzeln bearbeitet und gelöscht werden.</small>
</div> </div>
@@ -297,263 +321,6 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
</form> </form>
</div> </div>
<style>
.device-type-edit {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.edit-form {
background: white;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.edit-form fieldset {
margin: 20px 0;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.edit-form legend {
padding: 0 10px;
font-weight: bold;
font-size: 1.1em;
}
.form-group {
margin: 15px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group input[type="file"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
}
.form-group small {
display: block;
margin-top: 5px;
color: #666;
}
.required {
color: red;
}
.port-definition-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.port-definition-table th,
.port-definition-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
vertical-align: middle;
}
.port-definition-table th {
background: #f5f5f5;
}
.port-definition-table input[type="text"],
.port-definition-table select {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 30px;
}
.shape-editor {
display: grid;
grid-template-columns: 180px 1fr 320px;
gap: 16px;
margin-top: 16px;
}
.shape-editor-canvas {
border: 1px solid #ddd;
border-radius: 6px;
background: white;
padding: 12px;
}
.shape-editor-canvas svg {
width: 100%;
min-height: 320px;
display: block;
font-family: inherit;
cursor: crosshair;
}
.shape-toolbox,
.shape-overlay {
border: 1px solid #ddd;
border-radius: 6px;
padding: 12px;
background: #fff;
}
.shape-toolbox h4,
.shape-overlay h4 {
margin: 0 0 8px;
}
.shape-tool-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.shape-tool {
text-align: left;
border: 1px solid #bbb;
background: #f7f7f7;
color: #222;
padding: 10px;
border-radius: 4px;
cursor: grab;
}
.shape-tool:active {
cursor: grabbing;
}
.shape-tool.is-active {
background: #007bff;
color: #fff;
border-color: #0056b3;
}
.shape-editor-canvas.drag-over {
outline: 2px dashed #007bff;
outline-offset: 2px;
}
.shape-object {
cursor: move;
}
#shape-canvas.shape-tool-active {
cursor: crosshair;
}
.shape-object.is-selected {
filter: drop-shadow(0 0 5px rgba(0, 123, 255, 0.7));
}
.shape-object.is-port {
stroke-dasharray: 4 2;
}
.shape-overlay-form .shape-control-grid {
display: grid;
grid-template-columns: repeat(2, minmax(120px, 1fr));
gap: 8px;
}
.shape-overlay-form label {
font-size: 0.82rem;
display: flex;
flex-direction: column;
gap: 4px;
}
.shape-overlay-form input[type="number"],
.shape-overlay-form input[type="text"],
.shape-overlay-form input[type="color"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
.shape-port-settings {
margin-top: 10px;
}
.inline-checkbox {
flex-direction: row !important;
align-items: center;
gap: 6px !important;
}
.shape-overlay-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.shape-overlay-empty {
margin: 6px 0 0;
color: #666;
font-size: 0.9rem;
}
.hint {
font-size: 0.8rem;
color: #666;
margin: 8px 0 0;
}
@media (max-width: 1100px) {
.shape-editor {
grid-template-columns: 1fr;
}
}
.button {
padding: 10px 15px;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.95em;
}
.button-primary {
background: #28a745;
}
.button-danger {
background: #dc3545;
}
.button:hover {
opacity: 0.8;
}
</style>
<script> <script>
function addPortRow() { function addPortRow() {

View File

@@ -14,6 +14,7 @@
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="/assets/css/app.css"> <link rel="stylesheet" href="/assets/css/app.css">
<link rel="stylesheet" href="/assets/css/device-type-edit.css">
<!-- JS --> <!-- JS -->
<script src="/assets/js/app.js" defer></script> <script src="/assets/js/app.js" defer></script>

View File

@@ -120,7 +120,7 @@ Definiert eine Gerätevorlage.
- Grundlage für alle grafischen Ansichten - Grundlage für alle grafischen Ansichten
**Technische Attribute** **Technische Attribute**
- `shape_definition`: JSON-Array mit einfachen Formen (rect/circle/text) für die integrierte Zeichenfläche. - `shape_definition`: JSON-Objekt mit `meta` und `shapes`. `meta` enthält den Formfaktor (`'10'`, `'19'`, `'other'`), die Rack-Höhe in HE (`heightHe`) sowie die Zeichenflächen-Abmessungen (`canvasWidth`, `canvasHeight`). Bei 1HE-Geräten mit 10" oder 19" Frontpanel wird der Zeichenbereich automatisch auf die dort üblichen Breiten/Höhen (420×80px bzw. 760×80px) skaliert und zusätzlich von einem weißen Rahmen mit schwarzer Linie umgeben, damit die Darstellung den realen Formfaktor besser widerspiegelt. Der `shapes`-Array enthält wie bisher die einzelnen Formen (`rect`, `circle`, `text`), die im Editor platziert wurden.
--- ---