knxdisplay/web-interface/src/components/SidebarRight.vue
2026-01-30 19:07:52 +01:00

589 lines
34 KiB
Vue

<template>
<aside class="h-full overflow-y-auto p-[18px] flex flex-col gap-4 border-l border-border max-[1100px]:border-l-0 max-[1100px]:border-t">
<section class="bg-gradient-to-b from-white to-[#f6f9fc] border border-border rounded-[14px] p-3.5 shadow-[0_10px_24px_rgba(15,23,42,0.12)]" id="properties">
<div v-if="!w" class="text-muted text-center py-5 text-[13px]">
Kein Widget ausgewaehlt.<br><br>
Waehle ein Widget im Canvas oder im Baum.
</div>
<div v-else>
<!-- Layout -->
<h4 :class="headingClass">Layout</h4>
<div :class="rowClass"><label :class="labelClass">X</label><input :class="inputClass" type="number" v-model.number="w.x"></div>
<div :class="rowClass"><label :class="labelClass">Y</label><input :class="inputClass" type="number" v-model.number="w.y"></div>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" v-model.number="w.w"></div>
<div :class="rowClass"><label :class="labelClass">Hoehe</label><input :class="inputClass" type="number" v-model.number="w.h"></div>
<div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div>
<!-- Content -->
<template v-if="key === 'label' || key === 'button'">
<h4 :class="headingClass">Inhalt</h4>
<div :class="rowClass"><label :class="labelClass">Quelle</label>
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
<optgroup v-for="group in groupedSources(sourceOptions[key])" :key="group.label" :label="group.label">
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
</optgroup>
</select>
</div>
<div v-if="w.textSrc === 0" :class="rowClass">
<label :class="labelClass">Text</label><input :class="inputClass" type="text" v-model="w.text">
</div>
<template v-else>
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="w.text"></div>
<div v-if="w.textSrc < 11" :class="rowClass"><label :class="labelClass">KNX Objekt</label>
<select :class="inputClass" v-model.number="w.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</template>
</template>
<template v-if="key === 'led'">
<h4 :class="headingClass">LED</h4>
<div :class="rowClass"><label :class="labelClass">Modus</label>
<select :class="inputClass" v-model.number="w.textSrc">
<optgroup v-for="group in groupedSources(sourceOptions.led)" :key="group.label" :label="group.label">
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
</optgroup>
</select>
</div>
<div v-if="w.textSrc === 2" :class="rowClass">
<label :class="labelClass">KNX Objekt</label>
<select :class="inputClass" v-model.number="w.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</template>
<!-- Icon Widget -->
<template v-if="key === 'icon'">
<h4 :class="headingClass">Icon</h4>
<div :class="rowClass">
<label :class="labelClass">Icon</label>
<button :class="iconSelectClass" @click="showIconPicker = true">
<span v-if="w.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(w.iconCodepoint) }}</span>
<span v-else>Auswaehlen</span>
</button>
</div>
<div :class="rowClass">
<label :class="labelClass">Groesse</label>
<select :class="inputClass" v-model.number="w.iconSize">
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
</select>
</div>
<div :class="rowClass"><label :class="labelClass">Modus</label>
<select :class="inputClass" v-model.number="w.textSrc">
<optgroup v-for="group in groupedSources(sourceOptions.icon)" :key="group.label" :label="group.label">
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
</optgroup>
</select>
</div>
<div v-if="w.textSrc === 2" :class="rowClass">
<label :class="labelClass">KNX Objekt</label>
<select :class="inputClass" v-model.number="w.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</template>
<template v-if="key === 'tabview'">
<h4 :class="headingClass">Tabs</h4>
<div :class="rowClass"><label :class="labelClass">Position</label>
<select :class="inputClass" v-model.number="w.iconPosition">
<option :value="0">Oben</option>
<option :value="1">Unten</option>
<option :value="2">Links</option>
<option :value="3">Rechts</option>
</select>
</div>
<div :class="rowClass"><label :class="labelClass">Tab Hoehe</label>
<input :class="inputClass" type="number" v-model.number="w.iconSize" min="1" max="20">
<span class="text-[10px] text-muted ml-1">x10px</span>
</div>
</template>
<template v-if="key === 'tabpage'">
<h4 :class="headingClass">Tab Seite</h4>
<div :class="rowClass"><label :class="labelClass">Titel</label><input :class="inputClass" type="text" v-model="w.text"></div>
</template>
<template v-if="key === 'powerflow'">
<h4 :class="headingClass">Power Flow</h4>
<div :class="rowClass"><label :class="labelClass">Titel</label><input :class="inputClass" type="text" v-model="w.text"></div>
<div :class="rowClass">
<label :class="labelClass">Knoten</label>
<span class="text-[12px] text-muted">{{ powerNodeCount }}</span>
<button class="ml-auto border border-border bg-panel-2 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold hover:bg-[#e4ebf2]" @click="addPowerNode">+ Node</button>
</div>
<div :class="rowClass">
<label :class="labelClass">Verbindungen</label>
<button class="border border-border bg-panel-2 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold hover:bg-[#e4ebf2]" @click="togglePowerLinkMode">
{{ isLinkModeActive ? 'Modus: aktiv' : 'Modus: aus' }}
</button>
<button v-if="isLinkModeActive && linkSourceLabel" class="ml-auto border border-border bg-panel-2 px-2.5 py-1.5 rounded-lg text-[11px] hover:bg-[#e4ebf2]" @click="clearPowerLinkSource">Quelle loeschen</button>
</div>
<div class="text-[11px] text-muted mb-2">{{ linkModeHint }}</div>
<div v-if="powerFlowLinkItems.length" class="mt-2">
<div v-for="link in powerFlowLinkItems" :key="link.id" class="border border-border rounded-lg px-2.5 py-2 mb-2 bg-panel-2">
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<span class="px-1.5 py-0.5 rounded-md bg-white border border-border text-text max-w-[90px] truncate">{{ link.fromLabel }}</span>
<span>-&gt;</span>
<span class="px-1.5 py-0.5 rounded-md bg-white border border-border text-text max-w-[90px] truncate">{{ link.toLabel }}</span>
<button class="ml-auto w-6 h-6 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[11px] cursor-pointer hover:bg-[#f2cfcf]" @click="removePowerLink(link.id)">x</button>
</div>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">Linie</label>
<input class="h-[22px] w-[32px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="link.widget.bgColor">
<input class="w-[70px] bg-white border border-border rounded-md px-2 py-1 text-[11px]" type="number" min="1" max="12" v-model.number="link.widget.w">
<span class="text-[10px] text-muted">px</span>
</div>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">Speed</label>
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="link.widget.textSrc">
<optgroup v-for="group in groupedSources(sourceOptions.powerlink)" :key="group.label" :label="group.label">
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
</optgroup>
</select>
</div>
<div v-if="link.widget.textSrc === 0" class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">Wert</label>
<input class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" type="text" inputmode="decimal" v-model="link.widget.text" placeholder="z.B. 60">
</div>
<div v-else class="flex flex-col gap-2 text-[11px] text-muted">
<div class="flex items-center gap-2">
<label class="w-[70px] text-[11px] text-muted">Faktor</label>
<input class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" type="text" inputmode="decimal" v-model="link.widget.text" placeholder="z.B. 0.2">
</div>
<div class="flex items-center gap-2">
<label class="w-[70px] text-[11px] text-muted">KNX</label>
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="link.widget.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</div>
</div>
</div>
<div v-else class="text-[11px] text-muted">Keine Verbindungen.</div>
</template>
<template v-if="key === 'powernode'">
<h4 :class="headingClass">Power Node</h4>
<div :class="rowClass"><label :class="labelClass">Label</label><input :class="inputClass" type="text" v-model="powerNodeLabel"></div>
<div :class="rowClass"><label :class="labelClass">Quelle</label>
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
<optgroup v-for="group in groupedSources(sourceOptions.powernode)" :key="group.label" :label="group.label">
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
</optgroup>
</select>
</div>
<div v-if="w.textSrc === 0" :class="rowClass">
<label :class="labelClass">Wert</label><input :class="inputClass" type="text" v-model="powerNodeValue">
</div>
<template v-else>
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="powerNodeValue"></div>
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
<select :class="inputClass" v-model.number="w.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</template>
</template>
<template v-if="key === 'chart'">
<h4 :class="headingClass">Chart</h4>
<div :class="rowClass"><label :class="labelClass">Titel</label><input :class="inputClass" type="text" v-model="w.text"></div>
<div :class="rowClass"><label :class="labelClass">Zeitraum</label>
<select :class="inputClass" v-model.number="w.chart.period">
<option v-for="opt in chartPeriods" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
</div>
<div :class="rowClass"><label :class="labelClass">Serien</label>
<select :class="inputClass" v-model.number="chartSeriesCount">
<option :value="1">1</option>
<option :value="2">2</option>
<option :value="3">3</option>
</select>
</div>
<div v-for="(series, idx) in chartSeries" :key="idx" class="border border-border rounded-lg px-2.5 py-2 mb-2 bg-panel-2">
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">Serie {{ idx + 1 }}</label>
<input class="h-[22px] w-[32px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="series.color">
</div>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">Quelle</label>
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="series.textSrc">
<optgroup v-for="group in groupedSources(sourceOptions.chart)" :key="group.label" :label="group.label">
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
</optgroup>
</select>
</div>
<div class="flex items-center gap-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">KNX</label>
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="series.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</div>
</template>
<template v-if="key === 'clock'">
<h4 :class="headingClass">Uhr</h4>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Zeiger</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
</template>
<!-- Typography -->
<template v-if="key === 'label'">
<h4 :class="headingClass">Typo</h4>
<div :class="rowClass"><label :class="labelClass">Schriftgr.</label>
<select :class="inputClass" v-model.number="w.fontSize">
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
</select>
</div>
</template>
<template v-if="key === 'powernode'">
<h4 :class="headingClass">Typo</h4>
<div :class="rowClass"><label :class="labelClass">Wert Schriftgr.</label>
<select :class="inputClass" v-model.number="w.fontSize">
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
</select>
</div>
</template>
<!-- Icon for Label/Button -->
<template v-if="key === 'label'">
<h4 :class="headingClass">Icon</h4>
<div :class="rowClass">
<label :class="labelClass">Icon</label>
<button :class="iconSelectClass" @click="showIconPicker = true">
<span v-if="w.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(w.iconCodepoint) }}</span>
<span v-else>Kein Icon</span>
</button>
<button v-if="w.iconCodepoint" :class="iconRemoveClass" @click="w.iconCodepoint = 0">x</button>
</div>
<template v-if="w.iconCodepoint">
<div :class="rowClass">
<label :class="labelClass">Position</label>
<select :class="inputClass" v-model.number="w.iconPosition">
<option :value="0">Links</option>
<option :value="1">Rechts</option>
<option :value="2">Oben</option>
<option :value="3">Unten</option>
</select>
</div>
<div :class="rowClass">
<label :class="labelClass">Icon-Gr.</label>
<select :class="inputClass" v-model.number="w.iconSize">
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
</select>
</div>
<div :class="rowClass">
<label :class="labelClass">Abstand</label>
<input :class="inputClass" type="number" v-model.number="w.iconGap" min="0" max="50">
</div>
</template>
</template>
<template v-if="key === 'powernode'">
<h4 :class="headingClass">Icon</h4>
<div :class="rowClass">
<label :class="labelClass">Icon</label>
<button :class="iconSelectClass" @click="showIconPicker = true">
<span v-if="w.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(w.iconCodepoint) }}</span>
<span v-else>Kein Icon</span>
</button>
<button v-if="w.iconCodepoint" :class="iconRemoveClass" @click="w.iconCodepoint = 0">x</button>
</div>
<template v-if="w.iconCodepoint">
<div :class="rowClass">
<label :class="labelClass">Icon-Gr.</label>
<select :class="inputClass" v-model.number="w.iconSize">
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
</select>
</div>
</template>
</template>
<!-- Style -->
<template v-if="key === 'led'">
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Helligkeit</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
</template>
<template v-else-if="key === 'icon'">
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
</template>
<template v-else-if="key === 'button'">
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
</template>
<template v-else-if="key === 'powerflow'">
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
</template>
<template v-else-if="key === 'powernode'">
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Ringfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Ring Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
</template>
<template v-else>
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
</template>
<!-- Shadow/Glow -->
<template v-if="key !== 'icon'">
<h4 :class="headingClass">{{ key === 'led' || key === 'powernode' ? 'Glow' : 'Schatten' }}</h4>
<div :class="rowClass"><label :class="labelClass">Aktiv</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.shadow.enabled"></div>
<div :class="rowClass" v-if="key !== 'led'"><label :class="labelClass">X</label><input :class="inputClass" type="number" v-model.number="w.shadow.x"></div>
<div :class="rowClass" v-if="key !== 'led'"><label :class="labelClass">Y</label><input :class="inputClass" type="number" v-model.number="w.shadow.y"></div>
<div :class="rowClass"><label :class="labelClass">Blur</label><input :class="inputClass" type="number" v-model.number="w.shadow.blur"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.shadow.color"></div>
</template>
<!-- Button Actions -->
<template v-if="key === 'button'">
<h4 :class="headingClass">Aktion</h4>
<div :class="rowClass"><label :class="labelClass">Typ</label>
<select :class="inputClass" v-model.number="w.action">
<option :value="BUTTON_ACTIONS.KNX">KNX</option>
<option :value="BUTTON_ACTIONS.JUMP">Sprung</option>
<option :value="BUTTON_ACTIONS.BACK">Zurueck</option>
</select>
</div>
<div v-if="w.action === BUTTON_ACTIONS.JUMP" :class="rowClass">
<label :class="labelClass">Ziel</label>
<select :class="inputClass" v-model.number="w.targetScreen">
<option v-for="s in store.config.screens" :key="s.id" :value="s.id">{{ s.name }}</option>
</select>
</div>
<template v-if="w.action === BUTTON_ACTIONS.KNX">
<div :class="rowClass"><label :class="labelClass">Toggle</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.isToggle"></div>
<div :class="rowClass"><label :class="labelClass">KNX Schreib</label>
<select :class="inputClass" v-model.number="w.knxAddrWrite">
<option :value="0">-- Keine --</option>
<option v-for="addr in writeableAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
</template>
</template>
<div class="mt-4">
<button class="border border-red-200 bg-[#f7dede] text-[#b3261e] px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#f2cfcf] active:translate-y-0.5" @click="store.deleteWidget">Widget loeschen</button>
</div>
</div>
</section>
<!-- Icon Picker Modal -->
<IconPicker
v-if="showIconPicker"
v-model="w.iconCodepoint"
@close="showIconPicker = false"
/>
</aside>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useEditorStore } from '../stores/editor';
import { typeKeyFor } from '../utils';
import { sourceOptions, textSources, textSourceGroups, fontSizes, BUTTON_ACTIONS, defaultFormats, WIDGET_TYPES, chartPeriods } from '../constants';
import IconPicker from './IconPicker.vue';
const store = useEditorStore();
const w = computed(() => store.selectedWidget);
const key = computed(() => w.value ? typeKeyFor(w.value.type) : 'label');
const showIconPicker = ref(false);
const powerNodeCount = computed(() => {
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW || !store.activeScreen) return 0;
return store.activeScreen.widgets.filter((child) => child.parentId === w.value.id && child.type === WIDGET_TYPES.POWERNODE).length;
});
const powerFlowLinkItems = computed(() => {
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW || !store.activeScreen) return [];
const nodes = store.activeScreen.widgets.filter((child) => child.parentId === w.value.id && child.type === WIDGET_TYPES.POWERNODE);
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
return store.activeScreen.widgets
.filter((child) => child.parentId === w.value.id && child.type === WIDGET_TYPES.POWERLINK)
.map((link) => {
const fromNode = nodeMap.get(link.x);
const toNode = nodeMap.get(link.y);
return {
id: link.id,
widget: link,
fromLabel: getPowerNodeLabel(fromNode, link.x),
toLabel: getPowerNodeLabel(toNode, link.y)
};
});
});
const isLinkModeActive = computed(() => {
return store.powerLinkMode.active && store.powerLinkMode.powerflowId === w.value?.id;
});
const linkSourceLabel = computed(() => {
if (!isLinkModeActive.value || !store.activeScreen || !store.powerLinkMode.fromNodeId) return '';
const node = store.activeScreen.widgets.find((child) => child.id === store.powerLinkMode.fromNodeId);
return getPowerNodeLabel(node, store.powerLinkMode.fromNodeId);
});
const linkModeHint = computed(() => {
if (!isLinkModeActive.value) return 'Aktiviere den Modus und klicke zwei Knoten, um eine Verbindung zu erstellen.';
if (!linkSourceLabel.value) return 'Klicke den Startknoten.';
return `Quelle: ${linkSourceLabel.value} - jetzt Zielknoten waehlen.`;
});
const powerNodeLabel = computed({
get() {
return splitPowerNodeText(w.value?.text).label;
},
set(value) {
const parts = splitPowerNodeText(w.value?.text);
setPowerNodeText(value, parts.value);
}
});
const powerNodeValue = computed({
get() {
return splitPowerNodeText(w.value?.text).value;
},
set(value) {
const parts = splitPowerNodeText(w.value?.text);
setPowerNodeText(parts.label, value);
}
});
const chartSeries = computed(() => w.value?.chart?.series ?? []);
const chartSeriesCount = computed({
get() {
const count = chartSeries.value.length || 1;
return Math.max(1, Math.min(count, 3));
},
set(value) {
if (!w.value || !w.value.chart) return;
const target = Math.max(1, Math.min(value, 3));
const colors = ['#EF6351', '#7DD3B0', '#5EA2EF'];
if (!Array.isArray(w.value.chart.series)) {
w.value.chart.series = [];
}
while (w.value.chart.series.length < target) {
const idx = w.value.chart.series.length;
w.value.chart.series.push({
knxAddr: 0,
textSrc: 1,
color: colors[idx] || '#EF6351'
});
}
if (w.value.chart.series.length > target) {
w.value.chart.series = w.value.chart.series.slice(0, target);
}
}
});
const writeableAddresses = computed(() => store.knxAddresses.filter(a => a.write));
const rowClass = 'flex items-center gap-2.5 mb-2';
const labelClass = 'w-[90px] text-[12px] text-muted';
const inputClass = 'flex-1 bg-panel-2 border border-border rounded-lg px-2 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent';
const headingClass = 'mt-4 mb-2.5 text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]';
const noteClass = 'text-[11px] text-muted leading-tight';
const iconSelectClass = 'flex-1 bg-panel-2 border border-border rounded-lg px-2.5 py-1.5 text-text text-[12px] flex items-center justify-center gap-1.5 cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]';
const iconRemoveClass = 'w-7 h-7 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[12px] cursor-pointer hover:bg-[#f2cfcf]';
function groupedSources(options) {
const allowed = new Set(options || []);
return textSourceGroups
.map((group) => ({
label: group.label,
values: group.values.filter((value) => allowed.has(value))
}))
.filter((group) => group.values.length > 0);
}
function addPowerNode() {
store.addWidget('powernode');
}
function togglePowerLinkMode() {
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW) return;
const nextState = !(store.powerLinkMode.active && store.powerLinkMode.powerflowId === w.value.id);
store.setPowerLinkMode(nextState, w.value.id);
}
function clearPowerLinkSource() {
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW) return;
store.powerLinkMode.fromNodeId = null;
}
function removePowerLink(linkId) {
store.removePowerLink(linkId);
}
function handleTextSrcChange() {
if (!w.value) return;
const newSrc = w.value.textSrc;
if (w.value.type === WIDGET_TYPES.LABEL && newSrc > 0 && defaultFormats[newSrc]) {
w.value.text = defaultFormats[newSrc];
}
if (w.value.type === WIDGET_TYPES.POWERNODE && newSrc > 0 && defaultFormats[newSrc]) {
const parts = splitPowerNodeText(w.value.text);
setPowerNodeText(parts.label, defaultFormats[newSrc]);
}
}
function splitPowerNodeText(text) {
if (typeof text !== 'string') return { label: '', value: '' };
const parts = text.split('\n');
const label = parts[0] ?? '';
const value = parts.slice(1).join('\n');
return { label, value };
}
function setPowerNodeText(label, value) {
if (!w.value) return;
const labelLine = label ?? '';
const valueLine = value ?? '';
w.value.text = valueLine !== '' || labelLine !== '' ? `${labelLine}${valueLine !== '' ? `\n${valueLine}` : ''}` : '';
}
function getPowerNodeLabel(node, fallbackId) {
if (!node) return `Node ${fallbackId ?? ''}`.trim();
const parts = splitPowerNodeText(node.text);
return parts.label || `Node ${node.id}`;
}
</script>