This commit is contained in:
Thomas Peterson 2026-02-04 21:11:04 +01:00
parent 21126bb3a8
commit 2ea18624fc
49 changed files with 448 additions and 46 deletions

View File

@ -12,6 +12,7 @@ idf_component_register(SRCS "HistoryStore.cpp" "KnxWorker.cpp" "Nvs.cpp" "main.c
"widgets/PowerLinkWidget.cpp"
"widgets/ChartWidget.cpp"
"widgets/ClockWidget.cpp"
"widgets/RectangleWidget.cpp"
"widgets/RoomCardWidgetBase.cpp"
"widgets/RoomCardBubbleWidget.cpp"
"widgets/RoomCardTileWidget.cpp"

View File

@ -26,6 +26,9 @@ void WidgetConfig::serialize(uint8_t* buf) const {
buf[pos++] = bgColor.r; buf[pos++] = bgColor.g; buf[pos++] = bgColor.b;
buf[pos++] = bgOpacity;
buf[pos++] = borderRadius;
buf[pos++] = borderWidth;
buf[pos++] = borderColor.r; buf[pos++] = borderColor.g; buf[pos++] = borderColor.b;
buf[pos++] = borderOpacity;
buf[pos++] = shadow.offsetX;
buf[pos++] = shadow.offsetY;
@ -162,6 +165,9 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
bgColor.r = buf[pos++]; bgColor.g = buf[pos++]; bgColor.b = buf[pos++];
bgOpacity = buf[pos++];
borderRadius = buf[pos++];
borderWidth = buf[pos++];
borderColor.r = buf[pos++]; borderColor.g = buf[pos++]; borderColor.b = buf[pos++];
borderOpacity = buf[pos++];
shadow.offsetX = static_cast<int8_t>(buf[pos++]);
shadow.offsetY = static_cast<int8_t>(buf[pos++]);
@ -325,6 +331,9 @@ WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const c
cfg.bgColor = {0, 0, 0};
cfg.bgOpacity = 0;
cfg.borderRadius = 0;
cfg.borderWidth = 0;
cfg.borderColor = {255, 255, 255};
cfg.borderOpacity = 0;
cfg.shadow.enabled = false;
// Icon defaults
cfg.iconCodepoint = 0;
@ -375,6 +384,9 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y,
cfg.bgColor = {33, 150, 243}; // Blue
cfg.bgOpacity = 255;
cfg.borderRadius = 8;
cfg.borderWidth = 0;
cfg.borderColor = {255, 255, 255};
cfg.borderOpacity = 0;
cfg.shadow.enabled = true;
cfg.shadow.offsetX = 2;
cfg.shadow.offsetY = 2;

View File

@ -29,6 +29,7 @@ enum class WidgetType : uint8_t {
CHART = 9,
CLOCK = 10,
ROOMCARD = 11,
RECTANGLE = 12,
};
enum class IconPosition : uint8_t {
@ -225,6 +226,9 @@ struct WidgetConfig {
Color bgColor;
uint8_t bgOpacity; // 0-255
uint8_t borderRadius;
uint8_t borderWidth; // 0-32
Color borderColor;
uint8_t borderOpacity; // 0-255
// Shadow
ShadowConfig shadow;
@ -280,8 +284,8 @@ struct WidgetConfig {
TextLineConfig textLines[MAX_TEXTLINES];
// Serialization size (fixed for NVS storage)
// 197 + 4 (iconPositionX/Y) + 1 (subButtonCount) + 1 (subButtonSize) + 1 (subButtonDistance) + 1 (subButtonOpacity) + 1 (cardStyle) + 120 (6 subButtons * 20) = 326
static constexpr size_t SERIALIZED_SIZE = 326;
// 326 + 5 (borderWidth + borderColor + borderOpacity) = 331
static constexpr size_t SERIALIZED_SIZE = 331;
void serialize(uint8_t* buf) const;
void deserialize(const uint8_t* buf);

View File

@ -1522,6 +1522,12 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
cJSON_AddNumberToObject(widget, "bgOpacity", w.bgOpacity);
cJSON_AddNumberToObject(widget, "radius", w.borderRadius);
cJSON_AddNumberToObject(widget, "borderWidth", w.borderWidth);
cJSON_AddNumberToObject(widget, "borderOpacity", w.borderOpacity);
char borderColorStr[8];
snprintf(borderColorStr, sizeof(borderColorStr), "#%02X%02X%02X",
w.borderColor.r, w.borderColor.g, w.borderColor.b);
cJSON_AddStringToObject(widget, "borderColor", borderColorStr);
cJSON* shadow = cJSON_AddObjectToObject(widget, "shadow");
cJSON_AddBoolToObject(shadow, "enabled", w.shadow.enabled);
@ -1801,6 +1807,17 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
cJSON* radius = cJSON_GetObjectItem(widget, "radius");
if (cJSON_IsNumber(radius)) w.borderRadius = radius->valueint;
cJSON* borderWidth = cJSON_GetObjectItem(widget, "borderWidth");
if (cJSON_IsNumber(borderWidth)) w.borderWidth = borderWidth->valueint;
cJSON* borderOpacity = cJSON_GetObjectItem(widget, "borderOpacity");
if (cJSON_IsNumber(borderOpacity)) w.borderOpacity = borderOpacity->valueint;
cJSON* borderColor = cJSON_GetObjectItem(widget, "borderColor");
if (cJSON_IsString(borderColor)) {
w.borderColor = Color::fromHex(parseHexColor(borderColor->valuestring));
}
cJSON* shadow = cJSON_GetObjectItem(widget, "shadow");
if (cJSON_IsObject(shadow)) {
cJSON* enabled = cJSON_GetObjectItem(shadow, "enabled");
@ -2300,4 +2317,4 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
cJSON_Delete(root);
ESP_LOGI(TAG, "Parsed %d screens from JSON", config_->screenCount);
return true;
}
}

View File

@ -80,7 +80,6 @@ void ChartWidget::applyStyle() {
if (!obj_) return;
Widget::applyStyle();
lv_obj_set_style_border_width(obj_, 0, 0);
lv_obj_set_style_pad_all(obj_, 0, 0);
if (chart_) {

View File

@ -67,9 +67,17 @@ void ClockWidget::applyStyle() {
lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0);
}
lv_obj_set_style_border_width(obj_, 2, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 2, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
lv_obj_set_style_border_opa(obj_, LV_OPA_COVER, 0);
}
lv_color_t handColor = lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b);
@ -159,4 +167,4 @@ void ClockWidget::updateHands(const struct tm& t) {
void ClockWidget::onKnxTime(const struct tm& value, TextSource source) {
updateHands(value);
}
}

View File

@ -83,6 +83,17 @@ void IconWidget::applyStyle() {
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);
}
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 0, 0);
}
applyShadowStyle();
// Apply icon style
if (iconLabel_ != nullptr) {
lv_obj_set_style_text_color(iconLabel_, lv_color_make(

View File

@ -202,6 +202,15 @@ void LabelWidget::applyStyle() {
if (config_.borderRadius > 0) {
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);
}
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 0, 0);
}
applyShadowStyle();
}
// Apply text style

View File

@ -25,6 +25,15 @@ void LedWidget::applyStyle() {
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b));
lv_led_set_brightness(obj_, config_.bgOpacity);
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 0, 0);
}
// Shadow
applyShadowStyle();
}

View File

@ -41,10 +41,17 @@ void PowerFlowWidget::applyStyle() {
lv_obj_set_style_bg_opa(obj_, LV_OPA_TRANSP, 0);
}
lv_obj_set_style_border_width(obj_, 1, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
lv_obj_set_style_border_opa(obj_, LV_OPA_20, 0);
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 1, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
lv_obj_set_style_border_opa(obj_, LV_OPA_20, 0);
}
if (config_.borderRadius > 0) {
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);

View File

@ -206,10 +206,17 @@ void PowerNodeWidget::applyStyle() {
if (ring < 2) ring = 2;
if (ring > 12) ring = 12;
lv_obj_set_style_border_width(obj_, ring, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.bgOpacity, 0);
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, ring, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.bgOpacity, 0);
}
applyShadowStyle();

View File

@ -0,0 +1,26 @@
#include "RectangleWidget.hpp"
RectangleWidget::RectangleWidget(const WidgetConfig& config)
: Widget(config)
{
}
lv_obj_t* RectangleWidget::create(lv_obj_t* parent) {
obj_ = lv_obj_create(parent);
if (obj_ == nullptr) return nullptr;
lv_obj_remove_style_all(obj_);
lv_obj_set_pos(obj_, config_.x, config_.y);
lv_obj_set_size(obj_,
config_.width > 0 ? config_.width : 180,
config_.height > 0 ? config_.height : 120);
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
return obj_;
}
void RectangleWidget::applyStyle() {
if (obj_ == nullptr) return;
applyCommonStyle();
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "Widget.hpp"
class RectangleWidget : public Widget {
public:
explicit RectangleWidget(const WidgetConfig& config);
lv_obj_t* create(lv_obj_t* parent) override;
void applyStyle() override;
};

View File

@ -115,6 +115,14 @@ void RoomCardBubbleWidget::applyStyle() {
lv_obj_set_style_shadow_opa(bubble_, 255, 0);
}
if (bubble_ && config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(bubble_, config_.borderWidth, 0);
lv_obj_set_style_border_color(bubble_, lv_color_hex(config_.borderColor.toLvColor()), 0);
lv_obj_set_style_border_opa(bubble_, config_.borderOpacity, 0);
} else if (bubble_) {
lv_obj_set_style_border_width(bubble_, 0, 0);
}
// Font sizes
const lv_font_t* iconFont = Fonts::iconFont(config_.iconSize);
const lv_font_t* textFont = Fonts::bySizeIndex(config_.fontSize);

View File

@ -191,11 +191,17 @@ void RoomCardTileWidget::applyStyle() {
lv_color_t textColor = lv_color_hex(config_.textColor.toLvColor());
// Apply border if shadow is enabled (used as accent border)
if (config_.shadow.enabled) {
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_hex(config_.borderColor.toLvColor()), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else if (config_.shadow.enabled) {
// Backward-compatible accent border behavior
lv_obj_set_style_border_width(obj_, 3, 0);
lv_obj_set_style_border_color(obj_, lv_color_hex(config_.shadow.color.toLvColor()), 0);
lv_obj_set_style_border_opa(obj_, 255, 0);
} else {
lv_obj_set_style_border_width(obj_, 0, 0);
}
// Room name - large font

View File

@ -39,5 +39,22 @@ void TabPageWidget::applyStyle() {
lv_obj_set_style_bg_color(obj_, lv_color_make(
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0);
} else {
lv_obj_set_style_bg_opa(obj_, LV_OPA_TRANSP, 0);
}
if (config_.borderRadius > 0) {
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);
}
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 0, 0);
}
applyShadowStyle();
}

View File

@ -183,6 +183,16 @@ void Widget::applyCommonStyle() {
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);
}
// Border
if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make(
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else {
lv_obj_set_style_border_width(obj_, 0, 0);
}
// Shadow
applyShadowStyle();
}

View File

@ -12,6 +12,7 @@
#include "ClockWidget.hpp"
#include "RoomCardBubbleWidget.hpp"
#include "RoomCardTileWidget.hpp"
#include "RectangleWidget.hpp"
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
if (!config.visible) return nullptr;
@ -45,6 +46,8 @@ std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
return std::make_unique<RoomCardTileWidget>(config);
}
return std::make_unique<RoomCardBubbleWidget>(config);
case WidgetType::RECTANGLE:
return std::make_unique<RectangleWidget>(config);
default:
return nullptr;
}

View File

@ -42,6 +42,10 @@
<span class="text-[13px] font-semibold">Chart</span>
<span class="text-[11px] text-muted mt-0.5 block">Verlauf</span>
</button>
<button class="bg-panel-2 border border-border rounded-xl p-2.5 text-left transition hover:-translate-y-0.5 hover:border-accent" @click="store.addWidget('rectangle')">
<span class="text-[13px] font-semibold">Rechteck</span>
<span class="text-[11px] text-muted mt-0.5 block">Form</span>
</button>
<button class="bg-panel-2 border border-border rounded-xl p-2.5 text-left transition hover:-translate-y-0.5 hover:border-accent" @click="store.addWidget('roomcard')">
<span class="text-[13px] font-semibold">Room Card</span>
<span class="text-[11px] text-muted mt-0.5 block">Raum</span>

View File

@ -60,6 +60,7 @@ import PowerNodeSettings from './widgets/settings/PowerNodeSettings.vue';
import ChartSettings from './widgets/settings/ChartSettings.vue';
import ClockSettings from './widgets/settings/ClockSettings.vue';
import RoomCardSettings from './widgets/settings/RoomCardSettings.vue';
import RectangleSettings from './widgets/settings/RectangleSettings.vue';
const store = useEditorStore();
const w = computed(() => store.selectedWidget);
@ -82,7 +83,8 @@ const componentMap = {
[WIDGET_TYPES.POWERNODE]: markRaw(PowerNodeSettings),
[WIDGET_TYPES.CHART]: markRaw(ChartSettings),
[WIDGET_TYPES.CLOCK]: markRaw(ClockSettings),
[WIDGET_TYPES.ROOMCARD]: markRaw(RoomCardSettings)
[WIDGET_TYPES.ROOMCARD]: markRaw(RoomCardSettings),
[WIDGET_TYPES.RECTANGLE]: markRaw(RectangleSettings)
};
const settingsComponent = computed(() => {

View File

@ -26,6 +26,7 @@ import PowerNodeElement from './widgets/elements/PowerNodeElement.vue';
import ChartElement from './widgets/elements/ChartElement.vue';
import ClockElement from './widgets/elements/ClockElement.vue';
import RoomCardElement from './widgets/elements/RoomCardElement.vue';
import RectangleElement from './widgets/elements/RectangleElement.vue';
const props = defineProps({
widget: { type: Object, required: true },
@ -47,7 +48,8 @@ const componentMap = {
[WIDGET_TYPES.POWERNODE]: markRaw(PowerNodeElement),
[WIDGET_TYPES.CHART]: markRaw(ChartElement),
[WIDGET_TYPES.CLOCK]: markRaw(ClockElement),
[WIDGET_TYPES.ROOMCARD]: markRaw(RoomCardElement)
[WIDGET_TYPES.ROOMCARD]: markRaw(RoomCardElement),
[WIDGET_TYPES.RECTANGLE]: markRaw(RectangleElement)
};
const widgetComponent = computed(() => {

View File

@ -58,7 +58,7 @@
import { computed, defineAsyncComponent } from 'vue';
import { useEditorStore } from '../../../stores/editor';
import { fontSizes, ICON_POSITIONS } from '../../../constants';
import { getBaseStyle, justifyForAlign, getShadowStyle } from '../shared/utils';
import { getBaseStyle, justifyForAlign, getShadowStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
// Lazy import to avoid circular dependency
const WidgetElement = defineAsyncComponent(() => import('../../WidgetElement.vue'));
@ -118,13 +118,15 @@ const computedStyle = computed(() => {
const style = getBaseStyle(w, s);
style.background = w.bgColor;
const alpha = clamp((w.bgOpacity ?? 255) / 255, 0, 1);
style.background = hexToRgba(w.bgColor, alpha);
style.borderRadius = `${w.radius * s}px`;
style.display = 'flex';
style.alignItems = 'center';
style.justifyContent = justifyForAlign(textAlign.value);
style.fontWeight = '600';
Object.assign(style, getBorderStyle(w, s));
Object.assign(style, getShadowStyle(w, s));
return style;

View File

@ -35,7 +35,7 @@
<script setup>
import { computed } from 'vue';
import { getBaseStyle, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, clamp, hexToRgba, getBorderStyle } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -60,6 +60,8 @@ const computedStyle = computed(() => {
style.borderRadius = `${w.radius * s}px`;
}
Object.assign(style, getBorderStyle(w, s));
style.padding = `${12 * s}px`;
return style;

View File

@ -7,7 +7,7 @@
@touchstart.stop="$emit('drag-start', { id: widget.id, event: $event })"
@click.stop="$emit('select')"
>
<div class="relative w-full h-full rounded-full border-2 box-border flex items-center justify-center overflow-hidden" :style="{ borderColor: widget.textColor }">
<div class="relative w-full h-full rounded-full box-border flex items-center justify-center overflow-hidden">
<!-- Center Dot -->
<div class="absolute w-2 h-2 rounded-full z-10" :style="{ backgroundColor: widget.textColor }"></div>
<!-- Hour Hand -->
@ -33,7 +33,7 @@
<script setup>
import { computed } from 'vue';
import { getBaseStyle, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, clamp, hexToRgba, getBorderStyle } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -60,6 +60,12 @@ const computedStyle = computed(() => {
style.borderRadius = '50%';
}
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
Object.assign(style, getBorderStyle(w, s));
} else {
style.border = `${2 * s}px solid ${w.textColor}`;
}
return style;
});
</script>

View File

@ -27,7 +27,7 @@
<script setup>
import { computed } from 'vue';
import { fontSizes } from '../../../constants';
import { getBaseStyle, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, clamp, hexToRgba, getBorderStyle } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -70,6 +70,8 @@ const computedStyle = computed(() => {
style.borderRadius = `${w.radius * s}px`;
}
Object.assign(style, getBorderStyle(w, s));
return style;
});
</script>

View File

@ -45,7 +45,7 @@
<script setup>
import { computed } from 'vue';
import { fontSizes, ICON_POSITIONS } from '../../../constants';
import { getBaseStyle, justifyForAlign, textAlignCss, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, justifyForAlign, textAlignCss, clamp, hexToRgba, getBorderStyle } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -99,6 +99,8 @@ const computedStyle = computed(() => {
style.background = hexToRgba(w.bgColor, alpha);
}
Object.assign(style, getBorderStyle(w, s));
style.display = 'flex';
style.alignItems = 'center';

View File

@ -22,7 +22,7 @@
<script setup>
import { computed } from 'vue';
import { getBaseStyle, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, clamp, hexToRgba, getBorderStyle } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -58,6 +58,8 @@ const computedStyle = computed(() => {
style.boxShadow = 'inset 0 0 0 1px rgba(255,255,255,0.12)';
}
Object.assign(style, getBorderStyle(w, s));
return style;
});
</script>

View File

@ -67,7 +67,7 @@
import { computed, defineAsyncComponent } from 'vue';
import { useEditorStore } from '../../../stores/editor';
import { WIDGET_TYPES } from '../../../constants';
import { getBaseStyle, getShadowStyle, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, getShadowStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
const WidgetElement = defineAsyncComponent(() => import('../../WidgetElement.vue'));
@ -192,7 +192,11 @@ const computedStyle = computed(() => {
const style = getBaseStyle(w, s);
style.borderRadius = `${w.radius * s}px`;
style.overflow = 'hidden';
style.border = `1px solid ${hexToRgba('#94a3b8', 0.35)}`;
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
Object.assign(style, getBorderStyle(w, s));
} else {
style.border = `1px solid ${hexToRgba('#94a3b8', 0.35)}`;
}
Object.assign(style, getShadowStyle(w, s));

View File

@ -32,7 +32,7 @@
import { computed } from 'vue';
import { useEditorStore } from '../../../stores/editor';
import { fontSizes } from '../../../constants';
import { getBaseStyle, clamp, hexToRgba, splitPowerNodeText } from '../shared/utils';
import { getBaseStyle, getBorderStyle, clamp, hexToRgba, splitPowerNodeText } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -97,7 +97,11 @@ const computedStyle = computed(() => {
style.justifyContent = 'center';
style.borderRadius = '999px';
style.background = hexToRgba('#ffffff', 0.96);
style.border = `${ring}px solid ${hexToRgba(w.bgColor, ringAlpha)}`;
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
Object.assign(style, getBorderStyle(w, s));
} else {
style.border = `${ring}px solid ${hexToRgba(w.bgColor, ringAlpha)}`;
}
style.textAlign = 'center';
if (w.shadow && w.shadow.enabled) {

View File

@ -0,0 +1,54 @@
<template>
<div
class="z-[1] select-none touch-none"
:class="selected ? 'outline outline-2 outline-accent outline-offset-2' : ''"
:style="computedStyle"
@mousedown.stop="$emit('drag-start', { id: widget.id, event: $event })"
@touchstart.stop="$emit('drag-start', { id: widget.id, event: $event })"
@click.stop="$emit('select')"
>
<div
v-if="selected"
class="absolute -right-1.5 -bottom-1.5 w-3.5 h-3.5 rounded-[4px] bg-accent border-[2px] border-[#1b1308] shadow-[0_4px_12px_rgba(0,0,0,0.35)] cursor-se-resize z-10"
data-resize-handle
@mousedown.stop="$emit('resize-start', { id: widget.id, event: $event })"
@touchstart.stop="$emit('resize-start', { id: widget.id, event: $event })"
>
<span class="absolute right-[2px] bottom-[2px] w-[6px] h-[6px] border-r-2 border-b-2 border-[#1b1308a6] rounded-[2px]"></span>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { getBaseStyle, getShadowStyle, clamp, hexToRgba } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
scale: { type: Number, default: 1 },
selected: { type: Boolean, default: false }
});
defineEmits(['select', 'drag-start', 'resize-start']);
const computedStyle = computed(() => {
const w = props.widget;
const s = props.scale;
const style = getBaseStyle(w, s);
const bgAlpha = clamp((w.bgOpacity ?? 255) / 255, 0, 1);
style.background = hexToRgba(w.bgColor || '#000000', bgAlpha);
style.borderRadius = `${(w.radius || 0) * s}px`;
const borderWidth = Math.max(0, (w.borderWidth || 0) * s);
const borderAlpha = clamp((w.borderOpacity ?? 0) / 255, 0, 1);
if (borderWidth > 0 && borderAlpha > 0) {
style.border = `${borderWidth}px solid ${hexToRgba(w.borderColor || '#ffffff', borderAlpha)}`;
} else {
style.border = 'none';
}
Object.assign(style, getShadowStyle(w, s));
return style;
});
</script>

View File

@ -78,7 +78,7 @@
<script setup>
import { computed } from 'vue';
import { fontSizes, iconFontSizes } from '../../../constants';
import { getBaseStyle, clamp, hexToRgba } from '../shared/utils';
import { getBaseStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
const props = defineProps({
widget: { type: Object, required: true },
@ -135,14 +135,10 @@ function getTextLineIconSize(line) {
const roomCardTileContainerStyle = computed(() => {
const alpha = (props.widget.bgOpacity ?? 255) / 255;
const style = {
return {
backgroundColor: hexToRgba(props.widget.bgColor || '#333333', alpha),
borderRadius: (props.widget.radius || 16) + 'px',
};
if (props.widget.shadow?.enabled) {
style.border = `3px solid ${props.widget.shadow.color || '#ff6b6b'}`;
}
return style;
});
const decorIconStyle = computed(() => {
@ -194,6 +190,9 @@ const roomCardBubbleStyle = computed(() => {
top: `${top}px`,
backgroundColor: hexToRgba(props.widget.bgColor, alpha),
color: props.widget.textColor,
border: ((props.widget.borderWidth || 0) > 0 && (props.widget.borderOpacity ?? 0) > 0)
? `${(props.widget.borderWidth || 0) * s}px solid ${hexToRgba(props.widget.borderColor || '#ffffff', clamp((props.widget.borderOpacity ?? 0) / 255, 0, 1))}`
: 'none',
boxShadow: props.widget.shadow?.enabled
? `${(props.widget.shadow.x || 0) * s}px ${(props.widget.shadow.y || 0) * s}px ${(props.widget.shadow.blur || 0) * s}px ${hexToRgba(props.widget.shadow.color || '#000000', 0.3)}`
: '0 4px 12px rgba(0,0,0,0.15)'
@ -275,7 +274,9 @@ const computedStyle = computed(() => {
style.background = hexToRgba(w.bgColor || '#333333', alpha);
style.borderRadius = `${(w.radius || 16) * s}px`;
style.overflow = 'hidden';
if (w.shadow && w.shadow.enabled) {
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
Object.assign(style, getBorderStyle(w, s));
} else if (w.shadow && w.shadow.enabled) {
style.border = `3px solid ${w.shadow.color || '#ff6b6b'}`;
}
} else {

View File

@ -23,6 +23,7 @@
import { computed, defineAsyncComponent } from 'vue';
import { useEditorStore } from '../../../stores/editor';
import { fontSizes } from '../../../constants';
import { clamp, hexToRgba, getBorderStyle } from '../shared/utils';
const WidgetElement = defineAsyncComponent(() => import('../../WidgetElement.vue'));
@ -45,7 +46,7 @@ const computedStyle = computed(() => {
const w = props.widget;
const s = props.scale;
return {
const style = {
position: 'relative',
width: '100%',
height: '100%',
@ -57,5 +58,15 @@ const computedStyle = computed(() => {
userSelect: 'none',
touchAction: 'none'
};
if ((w.bgOpacity ?? 0) > 0) {
style.background = hexToRgba(w.bgColor, clamp((w.bgOpacity ?? 255) / 255, 0, 1));
}
if ((w.radius || 0) > 0) {
style.borderRadius = `${(w.radius || 0) * s}px`;
}
Object.assign(style, getBorderStyle(w, s));
return style;
});
</script>

View File

@ -49,7 +49,7 @@
<script setup>
import { computed, ref } from 'vue';
import { useEditorStore } from '../../../stores/editor';
import { getBaseStyle } from '../shared/utils';
import { getBaseStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
import TabPageElement from './TabPageElement.vue';
const props = defineProps({
@ -92,8 +92,9 @@ const computedStyle = computed(() => {
const s = props.scale;
const style = getBaseStyle(w, s);
style.background = w.bgColor;
style.background = hexToRgba(w.bgColor, clamp((w.bgOpacity ?? 255) / 255, 0, 1));
style.borderRadius = `${w.radius * s}px`;
Object.assign(style, getBorderStyle(w, s));
return style;
});

View File

@ -18,6 +18,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -45,6 +45,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -6,6 +6,10 @@
<div :class="rowClass"><label :class="labelClass">Zeiger</label><input :class="colorInputClass" type="color" v-model="widget.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -38,6 +38,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
</div>
</template>

View File

@ -70,6 +70,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -23,6 +23,10 @@
<h4 :class="headingClass">Stil</h4>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Helligkeit</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Glow -->
<h4 :class="headingClass">Glow</h4>

View File

@ -67,6 +67,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -150,6 +150,10 @@
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input :class="colorInputClass" type="color" v-model="widget.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Ringfarbe</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Ring Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Glow -->
<h4 :class="headingClass">Glow</h4>

View File

@ -0,0 +1,29 @@
<template>
<div>
<h4 :class="headingClass">Fuellung</h4>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" min="0" max="200" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<h4 :class="headingClass">Schatten</h4>
<div :class="rowClass"><label :class="labelClass">Aktiv</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.shadow.enabled"></div>
<div :class="rowClass"><label :class="labelClass">X</label><input :class="inputClass" type="number" v-model.number="widget.shadow.x"></div>
<div :class="rowClass"><label :class="labelClass">Y</label><input :class="inputClass" type="number" v-model.number="widget.shadow.y"></div>
<div :class="rowClass"><label :class="labelClass">Blur</label><input :class="inputClass" type="number" min="0" max="40" v-model.number="widget.shadow.blur"></div>
<div :class="rowClass"><label :class="labelClass">Spread</label><input :class="inputClass" type="number" min="0" max="20" v-model.number="widget.shadow.spread"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.shadow.color"></div>
</div>
</template>
<script setup>
import { rowClass, labelClass, inputClass, headingClass, colorInputClass } from '../shared/styles';
defineProps({
widget: { type: Object, required: true }
});
</script>

View File

@ -217,6 +217,10 @@
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input :class="colorInputClass" type="color" v-model="widget.textColor"></div>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -10,6 +10,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -21,6 +21,10 @@
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
<h4 :class="headingClass">Rand</h4>
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" min="0" max="32" v-model.number="widget.borderWidth"></div>
<div :class="rowClass"><label :class="labelClass">Farbe</label><input :class="colorInputClass" type="color" v-model="widget.borderColor"></div>
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.borderOpacity"></div>
<!-- Shadow -->
<h4 :class="headingClass">Schatten</h4>

View File

@ -48,6 +48,18 @@ export function getShadowStyle(widget, scale) {
};
}
// Border style helper (0-255 opacity, width in widget px)
export function getBorderStyle(widget, scale) {
const width = Math.max(0, (widget.borderWidth || 0) * scale);
const alpha = clamp((widget.borderOpacity ?? 0) / 255, 0, 1);
if (width <= 0 || alpha <= 0) {
return { border: 'none' };
}
return {
border: `${width}px solid ${hexToRgba(widget.borderColor || '#ffffff', alpha)}`
};
}
// PowerNode text splitting
export function splitPowerNodeText(text) {
if (typeof text !== 'string') return { label: '', value: '' };

View File

@ -14,7 +14,8 @@ export const WIDGET_TYPES = {
POWERLINK: 8,
CHART: 9,
CLOCK: 10,
ROOMCARD: 11
ROOMCARD: 11,
RECTANGLE: 12
};
export const ICON_POSITIONS = {
@ -48,7 +49,8 @@ export const TYPE_KEYS = {
8: 'powerlink',
9: 'chart',
10: 'clock',
11: 'roomcard'
11: 'roomcard',
12: 'rectangle'
};
export const TYPE_LABELS = {
@ -63,7 +65,8 @@ export const TYPE_LABELS = {
powerlink: 'Power Link',
chart: 'Chart',
clock: 'Uhr (Analog)',
roomcard: 'Room Card'
roomcard: 'Room Card',
rectangle: 'Rechteck'
};
@ -107,7 +110,8 @@ export const sourceOptions = {
powerlink: [0, 1, 3, 5, 6, 7],
chart: [1, 3, 5, 6, 7],
clock: [11],
roomcard: [0, 1, 3, 5, 6, 7] // Temperature sources
roomcard: [0, 1, 3, 5, 6, 7], // Temperature sources
rectangle: [0]
};
export const chartPeriods = [
@ -462,5 +466,32 @@ export const WIDGET_DEFAULTS = {
cardStyle: 0, // 0=Bubble (round), 1=Tile (rectangular)
subButtons: [],
textLines: [] // Variable text lines with icon, text, textSrc, knxAddr, fontSize
},
rectangle: {
w: 220,
h: 140,
text: '',
textSrc: 0,
fontSize: 1,
textAlign: TEXT_ALIGNS.CENTER,
textColor: '#FFFFFF',
bgColor: '#2E7DD1',
bgOpacity: 180,
radius: 14,
borderWidth: 2,
borderColor: '#FFFFFF',
borderOpacity: 180,
shadow: { enabled: false, x: 2, y: 2, blur: 10, spread: 0, color: '#000000' },
isToggle: false,
knxAddrWrite: 0,
knxAddr: 0,
action: 0,
targetScreen: 0,
iconCodepoint: 0,
iconPosition: 0,
iconSize: 1,
iconGap: 0,
iconPositionX: 0,
iconPositionY: 0
}
};

View File

@ -160,6 +160,9 @@ export const useEditorStore = defineStore('editor', () => {
bgColor: defaults.bgColor,
bgOpacity: 0,
radius: 0,
borderWidth: 0,
borderColor: defaults.borderColor || '#ffffff',
borderOpacity: 0,
shadow: { ...defaults.shadow, enabled: false },
isToggle: false,
knxAddrWrite: 0,
@ -337,6 +340,7 @@ export const useEditorStore = defineStore('editor', () => {
case 'chart': typeValue = WIDGET_TYPES.CHART; break;
case 'clock': typeValue = WIDGET_TYPES.CLOCK; break;
case 'roomcard': typeValue = WIDGET_TYPES.ROOMCARD; break;
case 'rectangle': typeValue = WIDGET_TYPES.RECTANGLE; break;
default: typeValue = WIDGET_TYPES.LABEL;
}
@ -402,6 +406,9 @@ export const useEditorStore = defineStore('editor', () => {
bgColor: defaults.bgColor,
bgOpacity: defaults.bgOpacity,
radius: defaults.radius,
borderWidth: defaults.borderWidth ?? 0,
borderColor: defaults.borderColor || '#ffffff',
borderOpacity: defaults.borderOpacity ?? 0,
shadow: { ...defaults.shadow },
isToggle: defaults.isToggle,
knxAddrWrite: defaults.knxAddrWrite,
@ -462,6 +469,9 @@ export const useEditorStore = defineStore('editor', () => {
bgColor: labelDefaults.bgColor,
bgOpacity: 0,
radius: 0,
borderWidth: 0,
borderColor: labelDefaults.borderColor || '#ffffff',
borderOpacity: 0,
shadow: { ...labelDefaults.shadow, enabled: false },
isToggle: false,
knxAddrWrite: 0,

View File

@ -20,6 +20,7 @@ export function minSizeFor(widget) {
if (key === 'powerlink') return { w: 1, h: 1 };
if (key === 'chart') return { w: 160, h: 120 };
if (key === 'roomcard') return { w: 120, h: 120 };
if (key === 'rectangle') return { w: 40, h: 30 };
return { w: 40, h: 20 };
}
@ -94,6 +95,17 @@ export function normalizeWidget(w, nextWidgetIdRef) {
});
}
// Border style defaults (used by rectangle widget, harmless for others)
if (w.borderWidth === undefined || w.borderWidth === null) {
w.borderWidth = defaults.borderWidth ?? 0;
}
if (!w.borderColor) {
w.borderColor = defaults.borderColor || '#ffffff';
}
if (w.borderOpacity === undefined || w.borderOpacity === null) {
w.borderOpacity = defaults.borderOpacity ?? 0;
}
if (defaults.chart) {
const maxSeries = 3;
if (!w.chart) {