Fixes
This commit is contained in:
parent
f7f3f8946c
commit
ef66dfaa24
@ -5,6 +5,8 @@ idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "
|
|||||||
"widgets/LedWidget.cpp"
|
"widgets/LedWidget.cpp"
|
||||||
"widgets/WidgetFactory.cpp"
|
"widgets/WidgetFactory.cpp"
|
||||||
"widgets/IconWidget.cpp"
|
"widgets/IconWidget.cpp"
|
||||||
|
"widgets/TabViewWidget.cpp"
|
||||||
|
"widgets/TabPageWidget.cpp"
|
||||||
"webserver/WebServer.cpp"
|
"webserver/WebServer.cpp"
|
||||||
"webserver/StaticFileHandlers.cpp"
|
"webserver/StaticFileHandlers.cpp"
|
||||||
"webserver/ConfigHandlers.cpp"
|
"webserver/ConfigHandlers.cpp"
|
||||||
|
|||||||
@ -15,6 +15,8 @@ enum class WidgetType : uint8_t {
|
|||||||
BUTTON = 1,
|
BUTTON = 1,
|
||||||
LED = 2,
|
LED = 2,
|
||||||
ICON = 3,
|
ICON = 3,
|
||||||
|
TABVIEW = 4,
|
||||||
|
TABPAGE = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class IconPosition : uint8_t {
|
enum class IconPosition : uint8_t {
|
||||||
|
|||||||
43
main/widgets/TabPageWidget.cpp
Normal file
43
main/widgets/TabPageWidget.cpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#include "TabPageWidget.hpp"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
static const char* TAG = "TabPageWidget";
|
||||||
|
|
||||||
|
TabPageWidget::TabPageWidget(const WidgetConfig& config)
|
||||||
|
: Widget(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TabPageWidget::~TabPageWidget() {
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t* TabPageWidget::create(lv_obj_t* parent) {
|
||||||
|
// Parent MUST be a TabView
|
||||||
|
if (!parent || !lv_obj_check_type(parent, &lv_tabview_class)) {
|
||||||
|
ESP_LOGE(TAG, "Parent of TabPage must be a TabView!");
|
||||||
|
// Fallback: create a container so we don't crash
|
||||||
|
obj_ = lv_obj_create(parent ? parent : lv_scr_act());
|
||||||
|
return obj_;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj_ = lv_tabview_add_tab(parent, config_.text);
|
||||||
|
|
||||||
|
// Pages fill the parent, so x/y/w/h are ignored usually,
|
||||||
|
// but LVGL handles layout.
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Created TabPage '%s'", config_.text);
|
||||||
|
return obj_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPageWidget::applyStyle() {
|
||||||
|
if (obj_ == nullptr) return;
|
||||||
|
|
||||||
|
// Pages usually transparent, but we can style them
|
||||||
|
// Don't use applyCommonStyle fully as it sets pos/size which we don't want
|
||||||
|
|
||||||
|
if (config_.bgOpacity > 0) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
main/widgets/TabPageWidget.hpp
Normal file
12
main/widgets/TabPageWidget.hpp
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Widget.hpp"
|
||||||
|
|
||||||
|
class TabPageWidget : public Widget {
|
||||||
|
public:
|
||||||
|
explicit TabPageWidget(const WidgetConfig& config);
|
||||||
|
~TabPageWidget() override;
|
||||||
|
|
||||||
|
lv_obj_t* create(lv_obj_t* parent) override;
|
||||||
|
void applyStyle() override;
|
||||||
|
};
|
||||||
53
main/widgets/TabViewWidget.cpp
Normal file
53
main/widgets/TabViewWidget.cpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#include "TabViewWidget.hpp"
|
||||||
|
#include "../WidgetManager.hpp"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
static const char* TAG = "TabViewWidget";
|
||||||
|
|
||||||
|
TabViewWidget::TabViewWidget(const WidgetConfig& config)
|
||||||
|
: Widget(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TabViewWidget::~TabViewWidget() {
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t* TabViewWidget::create(lv_obj_t* parent) {
|
||||||
|
// Determine tab position based on config
|
||||||
|
// We can reuse iconPosition property: 0=Top, 1=Bottom, 2=Left, 3=Right
|
||||||
|
lv_dir_t tabPos = LV_DIR_TOP;
|
||||||
|
if (config_.iconPosition == 1) tabPos = LV_DIR_BOTTOM;
|
||||||
|
else if (config_.iconPosition == 2) tabPos = LV_DIR_LEFT;
|
||||||
|
else if (config_.iconPosition == 3) tabPos = LV_DIR_RIGHT;
|
||||||
|
|
||||||
|
// Use iconSize as tab height (e.g., 50px)
|
||||||
|
lv_coord_t tabHeight = config_.iconSize > 0 ? config_.iconSize * 10 : 50;
|
||||||
|
|
||||||
|
// LVGL v9 API: Only parent as argument
|
||||||
|
obj_ = lv_tabview_create(parent);
|
||||||
|
lv_tabview_set_tab_bar_position(obj_, tabPos);
|
||||||
|
lv_tabview_set_tab_bar_size(obj_, tabHeight);
|
||||||
|
|
||||||
|
lv_obj_set_pos(obj_, config_.x, config_.y);
|
||||||
|
lv_obj_set_size(obj_, config_.width > 0 ? config_.width : 300,
|
||||||
|
config_.height > 0 ? config_.height : 200);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Created TabView at %d,%d", config_.x, config_.y);
|
||||||
|
return obj_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabViewWidget::applyStyle() {
|
||||||
|
if (obj_ == nullptr) return;
|
||||||
|
|
||||||
|
// Apply styling to the main container
|
||||||
|
applyCommonStyle();
|
||||||
|
|
||||||
|
// Style the tab buttons (Tab Bar in v9)
|
||||||
|
lv_obj_t* bar = lv_tabview_get_tab_bar(obj_);
|
||||||
|
|
||||||
|
lv_obj_set_style_bg_color(bar, lv_color_make(
|
||||||
|
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
|
||||||
|
|
||||||
|
lv_obj_set_style_text_color(bar, lv_color_make(
|
||||||
|
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
||||||
|
}
|
||||||
12
main/widgets/TabViewWidget.hpp
Normal file
12
main/widgets/TabViewWidget.hpp
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Widget.hpp"
|
||||||
|
|
||||||
|
class TabViewWidget : public Widget {
|
||||||
|
public:
|
||||||
|
explicit TabViewWidget(const WidgetConfig& config);
|
||||||
|
~TabViewWidget() override;
|
||||||
|
|
||||||
|
lv_obj_t* create(lv_obj_t* parent) override;
|
||||||
|
void applyStyle() override;
|
||||||
|
};
|
||||||
@ -3,6 +3,8 @@
|
|||||||
#include "ButtonWidget.hpp"
|
#include "ButtonWidget.hpp"
|
||||||
#include "LedWidget.hpp"
|
#include "LedWidget.hpp"
|
||||||
#include "IconWidget.hpp"
|
#include "IconWidget.hpp"
|
||||||
|
#include "TabViewWidget.hpp"
|
||||||
|
#include "TabPageWidget.hpp"
|
||||||
|
|
||||||
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
||||||
if (!config.visible) return nullptr;
|
if (!config.visible) return nullptr;
|
||||||
@ -16,6 +18,10 @@ std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
|||||||
return std::make_unique<LedWidget>(config);
|
return std::make_unique<LedWidget>(config);
|
||||||
case WidgetType::ICON:
|
case WidgetType::ICON:
|
||||||
return std::make_unique<IconWidget>(config);
|
return std::make_unique<IconWidget>(config);
|
||||||
|
case WidgetType::TABVIEW:
|
||||||
|
return std::make_unique<TabViewWidget>(config);
|
||||||
|
case WidgetType::TABPAGE:
|
||||||
|
return std::make_unique<TabPageWidget>(config);
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,8 @@
|
|||||||
:scale="store.canvasScale"
|
:scale="store.canvasScale"
|
||||||
:selected="store.selectedWidgetId === widget.id"
|
:selected="store.selectedWidgetId === widget.id"
|
||||||
@select="store.selectedWidgetId = widget.id"
|
@select="store.selectedWidgetId = widget.id"
|
||||||
@drag-start="startDrag(widget.id, $event)"
|
@drag-start="startDrag($event)"
|
||||||
@resize-start="startResize(widget.id, $event)"
|
@resize-start="startResize($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,7 +70,8 @@ function snap(val) {
|
|||||||
let dragState = null;
|
let dragState = null;
|
||||||
let resizeState = null;
|
let resizeState = null;
|
||||||
|
|
||||||
function startDrag(id, e) {
|
function startDrag(payload) {
|
||||||
|
const { id, event: e } = payload;
|
||||||
if (e.target.closest('.resize-handle')) return;
|
if (e.target.closest('.resize-handle')) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
store.selectedWidgetId = id;
|
store.selectedWidgetId = id;
|
||||||
@ -123,10 +124,20 @@ function drag(e) {
|
|||||||
if (w.parentId !== -1) {
|
if (w.parentId !== -1) {
|
||||||
const parent = store.activeScreen.widgets.find(p => p.id === w.parentId);
|
const parent = store.activeScreen.widgets.find(p => p.id === w.parentId);
|
||||||
if (parent) {
|
if (parent) {
|
||||||
|
if (parent.type === WIDGET_TYPES.TABPAGE) {
|
||||||
|
// Parent is a TabPage (w=0, h=0), so look at grandparent (TabView)
|
||||||
|
const grandParent = store.activeScreen.widgets.find(gp => gp.id === parent.parentId);
|
||||||
|
if (grandParent) {
|
||||||
|
maxW = grandParent.w;
|
||||||
|
maxH = grandParent.h;
|
||||||
|
// Adjust for tab bar? For simplicity use full size for now
|
||||||
|
}
|
||||||
|
} else {
|
||||||
maxW = parent.w;
|
maxW = parent.w;
|
||||||
maxH = parent.h;
|
maxH = parent.h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
w.x = Math.max(0, Math.min(maxW - w.w, Math.round(nx)));
|
w.x = Math.max(0, Math.min(maxW - w.w, Math.round(nx)));
|
||||||
w.y = Math.max(0, Math.min(maxH - w.h, Math.round(ny)));
|
w.y = Math.max(0, Math.min(maxH - w.h, Math.round(ny)));
|
||||||
@ -141,7 +152,8 @@ function endDrag() {
|
|||||||
document.removeEventListener('touchend', endDrag);
|
document.removeEventListener('touchend', endDrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startResize(id, e) {
|
function startResize(payload) {
|
||||||
|
const { id, event: e } = payload;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
store.selectedWidgetId = id;
|
store.selectedWidgetId = id;
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,10 @@
|
|||||||
<span class="element-title">Icon</span>
|
<span class="element-title">Icon</span>
|
||||||
<span class="element-sub">Symbol</span>
|
<span class="element-sub">Symbol</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="element-btn" @click="store.addWidget('tabview')">
|
||||||
|
<span class="element-title">Tabs</span>
|
||||||
|
<span class="element-sub">Container</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="key === 'tabview'">
|
||||||
|
<h4>Tabs</h4>
|
||||||
|
<div class="prop-row"><label>Position</label>
|
||||||
|
<select 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="prop-row"><label>Tab Hoehe</label>
|
||||||
|
<input type="number" v-model.number="w.iconSize" min="1" max="20">
|
||||||
|
<span style="font-size:10px; color:var(--muted); margin-left:4px">x10px</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="key === 'tabpage'">
|
||||||
|
<h4>Tab Seite</h4>
|
||||||
|
<div class="prop-row"><label>Titel</label><input type="text" v-model="w.text"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Typography -->
|
<!-- Typography -->
|
||||||
<template v-if="key === 'label' || key === 'button'">
|
<template v-if="key === 'label' || key === 'button'">
|
||||||
<h4>Typo</h4>
|
<h4>Typo</h4>
|
||||||
|
|||||||
@ -2,19 +2,37 @@
|
|||||||
<div class="tree-node">
|
<div class="tree-node">
|
||||||
<div
|
<div
|
||||||
class="tree-item"
|
class="tree-item"
|
||||||
:class="{ active: store.selectedWidgetId === node.id, hidden: !node.visible }"
|
:class="{
|
||||||
|
active: store.selectedWidgetId === node.id,
|
||||||
|
hidden: !node.visible,
|
||||||
|
'drag-over': isDragOver
|
||||||
|
}"
|
||||||
@click.stop="store.selectedWidgetId = node.id"
|
@click.stop="store.selectedWidgetId = node.id"
|
||||||
:style="{ paddingLeft: `${level * 12 + 10}px` }"
|
:style="{ paddingLeft: `${level * 16 + 8}px` }"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="onDragStart($event, node)"
|
||||||
|
@dragover.prevent="onDragOver($event)"
|
||||||
|
@dragleave="isDragOver = false"
|
||||||
|
@drop.stop="onDrop($event, node)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tree-expander"
|
||||||
|
@click.stop="toggleExpand"
|
||||||
|
:style="{ visibility: node.children.length > 0 ? 'visible' : 'hidden' }"
|
||||||
>
|
>
|
||||||
<span class="tree-icon" v-if="node.children.length > 0">
|
|
||||||
{{ expanded ? '▼' : '▶' }}
|
{{ expanded ? '▼' : '▶' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="tree-tag">{{ TYPE_LABELS[typeKeyFor(node.type)] }}</span>
|
|
||||||
|
<span class="tree-icon-type material-symbols-outlined">{{ getIconForType(node.type) }}</span>
|
||||||
|
|
||||||
|
<div class="tree-content">
|
||||||
<span class="tree-name">{{ node.text || TYPE_LABELS[typeKeyFor(node.type)] }}</span>
|
<span class="tree-name">{{ node.text || TYPE_LABELS[typeKeyFor(node.type)] }}</span>
|
||||||
<span class="tree-id">#{{ node.id }}</span>
|
<span class="tree-type">{{ TYPE_LABELS[typeKeyFor(node.type)] }} #{{ node.id }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tree-children" v-if="node.children.length > 0">
|
<div class="tree-children" v-if="node.children.length > 0 && expanded">
|
||||||
|
<div class="tree-guide" :style="{ left: `${level * 16 + 15}px` }"></div>
|
||||||
<TreeItem
|
<TreeItem
|
||||||
v-for="child in node.children"
|
v-for="child in node.children"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
@ -29,7 +47,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useEditorStore } from '../stores/editor';
|
import { useEditorStore } from '../stores/editor';
|
||||||
import { typeKeyFor } from '../utils';
|
import { typeKeyFor } from '../utils';
|
||||||
import { TYPE_LABELS } from '../constants';
|
import { TYPE_LABELS, WIDGET_TYPES } from '../constants';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: Object,
|
node: Object,
|
||||||
@ -38,58 +56,151 @@ const props = defineProps({
|
|||||||
|
|
||||||
const store = useEditorStore();
|
const store = useEditorStore();
|
||||||
const expanded = ref(true);
|
const expanded = ref(true);
|
||||||
|
const isDragOver = ref(false);
|
||||||
|
|
||||||
|
function toggleExpand() {
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconForType(type) {
|
||||||
|
switch(type) {
|
||||||
|
case WIDGET_TYPES.LABEL: return 'text_fields';
|
||||||
|
case WIDGET_TYPES.BUTTON: return 'smart_button';
|
||||||
|
case WIDGET_TYPES.LED: return 'light_mode';
|
||||||
|
case WIDGET_TYPES.ICON: return 'image';
|
||||||
|
case WIDGET_TYPES.TABVIEW: return 'tab';
|
||||||
|
case WIDGET_TYPES.TABPAGE: return 'article';
|
||||||
|
default: return 'widgets';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(e, node) {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', node.id.toString());
|
||||||
|
store.selectedWidgetId = node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(e) {
|
||||||
|
// Only allow dropping on containers (Button, TabView, TabPage)
|
||||||
|
// Actually, dropping ON a widget means "put inside".
|
||||||
|
// Dropping BETWEEN is harder to implement in this simple tree.
|
||||||
|
// Let's allow dropping on anything that CAN accept children.
|
||||||
|
const type = props.node.type;
|
||||||
|
const canAccept = type === WIDGET_TYPES.BUTTON ||
|
||||||
|
type === WIDGET_TYPES.TABVIEW ||
|
||||||
|
type === WIDGET_TYPES.TABPAGE;
|
||||||
|
|
||||||
|
if (canAccept) {
|
||||||
|
isDragOver.value = true;
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
} else {
|
||||||
|
e.dataTransfer.dropEffect = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e, targetNode) {
|
||||||
|
isDragOver.value = false;
|
||||||
|
const draggedId = parseInt(e.dataTransfer.getData('text/plain'));
|
||||||
|
|
||||||
|
if (draggedId === targetNode.id) return; // Drop on self
|
||||||
|
|
||||||
|
// Check compatibility
|
||||||
|
const type = targetNode.type;
|
||||||
|
const canAccept = type === WIDGET_TYPES.BUTTON ||
|
||||||
|
type === WIDGET_TYPES.TABVIEW ||
|
||||||
|
type === WIDGET_TYPES.TABPAGE;
|
||||||
|
|
||||||
|
if (canAccept) {
|
||||||
|
store.reparentWidget(draggedId, targetNode.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.tree-node {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-item {
|
.tree-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
background: var(--panel-2);
|
background: transparent;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
padding: 6px 10px;
|
padding: 4px 6px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.2s ease;
|
margin-bottom: 1px;
|
||||||
margin-bottom: 4px;
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item:hover {
|
||||||
|
background: var(--panel-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-item.active {
|
.tree-item.active {
|
||||||
border-color: var(--accent-2);
|
background: rgba(125, 211, 176, 0.15);
|
||||||
background: rgba(125, 211, 176, 0.1);
|
border-color: rgba(125, 211, 176, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item.drag-over {
|
||||||
|
background: rgba(246, 193, 119, 0.2);
|
||||||
|
border: 1px dashed var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-item.hidden {
|
.tree-item.hidden {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-tag {
|
.tree-expander {
|
||||||
font-size: 10px;
|
width: 16px;
|
||||||
padding: 2px 6px;
|
height: 16px;
|
||||||
border-radius: 999px;
|
display: flex;
|
||||||
background: rgba(125, 211, 176, 0.15);
|
align-items: center;
|
||||||
color: var(--accent-2);
|
justify-content: center;
|
||||||
text-transform: uppercase;
|
font-size: 8px;
|
||||||
letter-spacing: 0.06em;
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-expander:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-icon-type {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-name {
|
.tree-name {
|
||||||
flex: 1;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-id {
|
.tree-type {
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-icon {
|
.tree-children {
|
||||||
font-size: 10px;
|
position: relative;
|
||||||
color: var(--muted);
|
}
|
||||||
width: 12px;
|
|
||||||
|
.tree-guide {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -9,18 +9,43 @@
|
|||||||
'widget-icon': isIcon
|
'widget-icon': isIcon
|
||||||
}"
|
}"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
@mousedown="$emit('drag-start', $event)"
|
@mousedown.stop="!isTabPage && $emit('drag-start', { id: widget.id, event: $event })"
|
||||||
@touchstart="$emit('drag-start', $event)"
|
@touchstart.stop="!isTabPage && $emit('drag-start', { id: widget.id, event: $event })"
|
||||||
@click.stop="$emit('select')"
|
@click.stop="$emit('select')"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
v-if="selected"
|
|
||||||
class="resize-handle"
|
|
||||||
@mousedown.stop="$emit('resize-start', $event)"
|
|
||||||
@touchstart.stop="$emit('resize-start', $event)"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Recursive Children -->
|
<!-- Recursive Children -->
|
||||||
|
<!-- Special handling for TabView to render structure -->
|
||||||
|
<template v-if="isTabView">
|
||||||
|
<div class="tabview-container" :class="'pos-'+tabPosition" :style="tabViewStyle">
|
||||||
|
<div class="tab-buttons" :style="tabBtnsStyle">
|
||||||
|
<div
|
||||||
|
v-for="(child, idx) in children"
|
||||||
|
:key="child.id"
|
||||||
|
class="tab-btn"
|
||||||
|
:class="{ active: activePageId === child.id }"
|
||||||
|
@click.stop="activeTabIndex = idx; store.selectedWidgetId = child.id"
|
||||||
|
>
|
||||||
|
{{ child.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<WidgetElement
|
||||||
|
v-for="child in children"
|
||||||
|
:key="child.id"
|
||||||
|
:widget="child"
|
||||||
|
:scale="scale"
|
||||||
|
:selected="store.selectedWidgetId === child.id"
|
||||||
|
:style="{ display: activePageId === child.id ? 'block' : 'none' }"
|
||||||
|
@select="store.selectedWidgetId = child.id"
|
||||||
|
@drag-start="$emit('drag-start', $event)"
|
||||||
|
@resize-start="$emit('resize-start', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Standard Recursive Children (for Buttons, Pages, etc) -->
|
||||||
|
<template v-else>
|
||||||
<WidgetElement
|
<WidgetElement
|
||||||
v-for="child in children"
|
v-for="child in children"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
@ -31,6 +56,7 @@
|
|||||||
@drag-start="$emit('drag-start', $event)"
|
@drag-start="$emit('drag-start', $event)"
|
||||||
@resize-start="$emit('resize-start', $event)"
|
@resize-start="$emit('resize-start', $event)"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Icon-only Widget -->
|
<!-- Icon-only Widget -->
|
||||||
<template v-if="isIcon">
|
<template v-if="isIcon">
|
||||||
@ -58,8 +84,16 @@
|
|||||||
|
|
||||||
<!-- Standard (no icon) -->
|
<!-- Standard (no icon) -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ widget.text }}
|
<span v-if="!isTabView && !isTabPage">{{ widget.text }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Resize Handle (at end to be on top) -->
|
||||||
|
<div
|
||||||
|
v-if="selected"
|
||||||
|
class="resize-handle"
|
||||||
|
@mousedown.stop="$emit('resize-start', { id: widget.id, event: $event })"
|
||||||
|
@touchstart.stop="$emit('resize-start', { id: widget.id, event: $event })"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -88,6 +122,37 @@ const isLabel = computed(() => props.widget.type === WIDGET_TYPES.LABEL);
|
|||||||
const isButton = computed(() => props.widget.type === WIDGET_TYPES.BUTTON);
|
const isButton = computed(() => props.widget.type === WIDGET_TYPES.BUTTON);
|
||||||
const isLed = computed(() => props.widget.type === WIDGET_TYPES.LED);
|
const isLed = computed(() => props.widget.type === WIDGET_TYPES.LED);
|
||||||
const isIcon = computed(() => props.widget.type === WIDGET_TYPES.ICON);
|
const isIcon = computed(() => props.widget.type === WIDGET_TYPES.ICON);
|
||||||
|
const isTabView = computed(() => props.widget.type === WIDGET_TYPES.TABVIEW);
|
||||||
|
const isTabPage = computed(() => props.widget.type === WIDGET_TYPES.TABPAGE);
|
||||||
|
|
||||||
|
const tabPosition = computed(() => props.widget.iconPosition || 0); // 0=Top, 1=Bottom...
|
||||||
|
const tabHeight = computed(() => (props.widget.iconSize || 5) * 10);
|
||||||
|
|
||||||
|
// Find active tab index (client-side state only, maybe store in widget temporarily?)
|
||||||
|
// For designer simplicity: show all tabs content stacked, or just first one?
|
||||||
|
// Better: mimic LVGL. We need state. Let's use a local ref or just show the selected one in tree?
|
||||||
|
// To keep it simple: Show the active tab based on which child is selected or default to first.
|
||||||
|
import { ref } from 'vue';
|
||||||
|
const activeTabIndex = ref(0);
|
||||||
|
|
||||||
|
const activePageId = computed(() => {
|
||||||
|
// If a child page is selected, make it active
|
||||||
|
const selectedChild = children.value.find(c => store.selectedWidgetId === c.id);
|
||||||
|
if (selectedChild) return selectedChild.id;
|
||||||
|
|
||||||
|
// If a widget inside a page is selected
|
||||||
|
if (store.selectedWidget && store.selectedWidget.parentId !== -1) {
|
||||||
|
// Find ancestor page
|
||||||
|
let curr = store.selectedWidget;
|
||||||
|
while (curr && curr.parentId !== -1 && curr.parentId !== props.widget.id) {
|
||||||
|
curr = store.activeScreen.widgets.find(w => w.id === curr.parentId);
|
||||||
|
}
|
||||||
|
if (curr && curr.parentId === props.widget.id) return curr.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children.value.length > 0) return children.value[activeTabIndex.value]?.id;
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
const hasIcon = computed(() => {
|
const hasIcon = computed(() => {
|
||||||
return (isLabel.value || isButton.value) && props.widget.iconCodepoint > 0;
|
return (isLabel.value || isButton.value) && props.widget.iconCodepoint > 0;
|
||||||
@ -213,10 +278,44 @@ const computedStyle = computed(() => {
|
|||||||
} else {
|
} else {
|
||||||
style.boxShadow = 'inset 0 0 0 1px rgba(255,255,255,0.12)';
|
style.boxShadow = 'inset 0 0 0 1px rgba(255,255,255,0.12)';
|
||||||
}
|
}
|
||||||
|
} else if (isTabView.value) {
|
||||||
|
style.background = w.bgColor;
|
||||||
|
style.borderRadius = `${w.radius * s}px`;
|
||||||
|
// No flex here, we handle internal layout manually
|
||||||
|
} else if (isTabPage.value) {
|
||||||
|
style.position = 'relative'; // Relative to content area
|
||||||
|
style.width = '100%';
|
||||||
|
style.height = '100%';
|
||||||
|
style.left = '0';
|
||||||
|
style.top = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tabViewStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: tabPosition.value === 0 || tabPosition.value === 1 ? 'column' : 'row',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabBtnsStyle = computed(() => {
|
||||||
|
const s = props.scale;
|
||||||
|
const h = tabHeight.value * s;
|
||||||
|
const isVert = tabPosition.value === 0 || tabPosition.value === 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
[isVert ? 'height' : 'width']: `${h}px`,
|
||||||
|
order: tabPosition.value === 1 || tabPosition.value === 3 ? 2 : 0, // Bottom/Right
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: isVert ? 'row' : 'column',
|
||||||
|
background: 'rgba(0,0,0,0.2)',
|
||||||
|
overflow: 'hidden'
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -239,4 +338,43 @@ const computedStyle = computed(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TabView Styles */
|
||||||
|
.tabview-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
border-right: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-2 .tab-btn, .pos-3 .tab-btn {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.pos-2 .tab-btn.active, .pos-3 .tab-btn.active {
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 2px solid var(--accent);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -6,7 +6,9 @@ export const WIDGET_TYPES = {
|
|||||||
LABEL: 0,
|
LABEL: 0,
|
||||||
BUTTON: 1,
|
BUTTON: 1,
|
||||||
LED: 2,
|
LED: 2,
|
||||||
ICON: 3
|
ICON: 3,
|
||||||
|
TABVIEW: 4,
|
||||||
|
TABPAGE: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ICON_POSITIONS = {
|
export const ICON_POSITIONS = {
|
||||||
@ -26,16 +28,21 @@ export const TYPE_KEYS = {
|
|||||||
0: 'label',
|
0: 'label',
|
||||||
1: 'button',
|
1: 'button',
|
||||||
2: 'led',
|
2: 'led',
|
||||||
3: 'icon'
|
3: 'icon',
|
||||||
|
4: 'tabview',
|
||||||
|
5: 'tabpage'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_LABELS = {
|
export const TYPE_LABELS = {
|
||||||
label: 'Label',
|
label: 'Label',
|
||||||
button: 'Button',
|
button: 'Button',
|
||||||
led: 'LED',
|
led: 'LED',
|
||||||
icon: 'Icon'
|
icon: 'Icon',
|
||||||
|
tabview: 'Tabs',
|
||||||
|
tabpage: 'Seite'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const textSources = {
|
export const textSources = {
|
||||||
0: 'Statisch',
|
0: 'Statisch',
|
||||||
1: 'KNX Temperatur',
|
1: 'KNX Temperatur',
|
||||||
@ -151,5 +158,47 @@ export const WIDGET_DEFAULTS = {
|
|||||||
iconPosition: 0,
|
iconPosition: 0,
|
||||||
iconSize: 3,
|
iconSize: 3,
|
||||||
iconGap: 8
|
iconGap: 8
|
||||||
|
},
|
||||||
|
tabview: {
|
||||||
|
w: 400,
|
||||||
|
h: 300,
|
||||||
|
text: '',
|
||||||
|
textSrc: 0,
|
||||||
|
fontSize: 1,
|
||||||
|
textColor: '#FFFFFF',
|
||||||
|
bgColor: '#2a3543',
|
||||||
|
bgOpacity: 255,
|
||||||
|
radius: 8,
|
||||||
|
shadow: { enabled: false },
|
||||||
|
isToggle: false,
|
||||||
|
knxAddrWrite: 0,
|
||||||
|
knxAddr: 0,
|
||||||
|
action: 0,
|
||||||
|
targetScreen: 0,
|
||||||
|
iconCodepoint: 0,
|
||||||
|
iconPosition: 0, // 0=Top, 1=Bottom, 2=Left, 3=Right
|
||||||
|
iconSize: 5, // Used for Tab Height (x10) -> 50px
|
||||||
|
iconGap: 0
|
||||||
|
},
|
||||||
|
tabpage: {
|
||||||
|
w: 0, // Ignored
|
||||||
|
h: 0, // Ignored
|
||||||
|
text: 'Tab',
|
||||||
|
textSrc: 0,
|
||||||
|
fontSize: 1,
|
||||||
|
textColor: '#FFFFFF',
|
||||||
|
bgColor: '#1A1A2E',
|
||||||
|
bgOpacity: 0, // Transparent by default
|
||||||
|
radius: 0,
|
||||||
|
shadow: { enabled: false },
|
||||||
|
isToggle: false,
|
||||||
|
knxAddrWrite: 0,
|
||||||
|
knxAddr: 0,
|
||||||
|
action: 0,
|
||||||
|
targetScreen: 0,
|
||||||
|
iconCodepoint: 0,
|
||||||
|
iconPosition: 0,
|
||||||
|
iconSize: 1,
|
||||||
|
iconGap: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -188,8 +188,21 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
case 'button': typeValue = WIDGET_TYPES.BUTTON; break;
|
case 'button': typeValue = WIDGET_TYPES.BUTTON; break;
|
||||||
case 'led': typeValue = WIDGET_TYPES.LED; break;
|
case 'led': typeValue = WIDGET_TYPES.LED; break;
|
||||||
case 'icon': typeValue = WIDGET_TYPES.ICON; break;
|
case 'icon': typeValue = WIDGET_TYPES.ICON; break;
|
||||||
|
case 'tabview': typeValue = WIDGET_TYPES.TABVIEW; break;
|
||||||
|
case 'tabpage': typeValue = WIDGET_TYPES.TABPAGE; break;
|
||||||
default: typeValue = WIDGET_TYPES.LABEL;
|
default: typeValue = WIDGET_TYPES.LABEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case: Add Page to existing TabView
|
||||||
|
if (typeStr === 'tabview' && selectedWidgetId.value !== null) {
|
||||||
|
const sel = activeScreen.value.widgets.find(w => w.id === selectedWidgetId.value);
|
||||||
|
if (sel && sel.type === WIDGET_TYPES.TABVIEW) {
|
||||||
|
// User clicked "Tabs" while a TabView is selected -> Add a Page
|
||||||
|
addWidget('tabpage');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defaults = WIDGET_DEFAULTS[typeStr];
|
const defaults = WIDGET_DEFAULTS[typeStr];
|
||||||
|
|
||||||
// Determine parent
|
// Determine parent
|
||||||
@ -199,11 +212,16 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
if (selectedWidgetId.value !== null) {
|
if (selectedWidgetId.value !== null) {
|
||||||
const parent = activeScreen.value.widgets.find(w => w.id === selectedWidgetId.value);
|
const parent = activeScreen.value.widgets.find(w => w.id === selectedWidgetId.value);
|
||||||
// Only BUTTON can be a parent for now (as a container)
|
if (parent) {
|
||||||
if (parent && parent.type === WIDGET_TYPES.BUTTON) {
|
if (parent.type === WIDGET_TYPES.BUTTON || parent.type === WIDGET_TYPES.TABPAGE) {
|
||||||
parentId = parent.id;
|
parentId = parent.id;
|
||||||
startX = 10; // Relative to parent
|
startX = 10;
|
||||||
startY = 10;
|
startY = 10;
|
||||||
|
} else if (parent.type === WIDGET_TYPES.TABVIEW && typeStr === 'tabpage') {
|
||||||
|
parentId = parent.id;
|
||||||
|
startX = 0;
|
||||||
|
startY = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,14 +255,66 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
activeScreen.value.widgets.push(w);
|
activeScreen.value.widgets.push(w);
|
||||||
selectedWidgetId.value = w.id;
|
selectedWidgetId.value = w.id;
|
||||||
|
|
||||||
|
// Auto-create pages for new TabView
|
||||||
|
if (typeStr === 'tabview') {
|
||||||
|
// Deselect to reset parent logic for recursive calls, or explicitly pass parent
|
||||||
|
// Easier: just manually push 2 pages
|
||||||
|
const p1 = { ...WIDGET_DEFAULTS.tabpage, id: nextWidgetId.value++, parentId: w.id, text: 'Tab 1', type: WIDGET_TYPES.TABPAGE };
|
||||||
|
const p2 = { ...WIDGET_DEFAULTS.tabpage, id: nextWidgetId.value++, parentId: w.id, text: 'Tab 2', type: WIDGET_TYPES.TABPAGE };
|
||||||
|
activeScreen.value.widgets.push(p1);
|
||||||
|
activeScreen.value.widgets.push(p2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteWidget() {
|
function deleteWidget() {
|
||||||
if (!activeScreen.value || selectedWidgetId.value === null) return;
|
if (!activeScreen.value || selectedWidgetId.value === null) return;
|
||||||
activeScreen.value.widgets = activeScreen.value.widgets.filter(w => w.id !== selectedWidgetId.value);
|
|
||||||
|
// Also delete children recursively?
|
||||||
|
// For now, simpler: reparent children to root or delete them.
|
||||||
|
// Let's delete them to avoid orphans.
|
||||||
|
const idToDelete = selectedWidgetId.value;
|
||||||
|
const deleteIds = [idToDelete];
|
||||||
|
|
||||||
|
// Find all descendants
|
||||||
|
let found = true;
|
||||||
|
while (found) {
|
||||||
|
found = false;
|
||||||
|
activeScreen.value.widgets.forEach(w => {
|
||||||
|
if (deleteIds.includes(w.parentId) && !deleteIds.includes(w.id)) {
|
||||||
|
deleteIds.push(w.id);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
activeScreen.value.widgets = activeScreen.value.widgets.filter(w => !deleteIds.includes(w.id));
|
||||||
selectedWidgetId.value = null;
|
selectedWidgetId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reparentWidget(widgetId, newParentId) {
|
||||||
|
if (!activeScreen.value) return;
|
||||||
|
|
||||||
|
// Prevent cycles: Check if newParentId is a descendant of widgetId
|
||||||
|
let curr = newParentId;
|
||||||
|
while (curr !== -1) {
|
||||||
|
if (curr === widgetId) return; // Cycle detected
|
||||||
|
const p = activeScreen.value.widgets.find(w => w.id === curr);
|
||||||
|
if (!p) break;
|
||||||
|
curr = p.parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = activeScreen.value.widgets.find(w => w.id === widgetId);
|
||||||
|
if (widget) {
|
||||||
|
// Adjust position to be relative to new parent (simplified: reset to 0,0 or keep absolute?)
|
||||||
|
// Keeping absolute is hard because we don't know absolute pos easily here.
|
||||||
|
// Resetting to 10,10 is safer.
|
||||||
|
widget.x = 10;
|
||||||
|
widget.y = 10;
|
||||||
|
widget.parentId = newParentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
knxAddresses,
|
knxAddresses,
|
||||||
@ -264,6 +334,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
addScreen,
|
addScreen,
|
||||||
deleteScreen,
|
deleteScreen,
|
||||||
addWidget,
|
addWidget,
|
||||||
deleteWidget
|
deleteWidget,
|
||||||
|
reparentWidget
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -402,7 +402,6 @@ body {
|
|||||||
.canvas {
|
.canvas {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--canvas-bg);
|
background: var(--canvas-bg);
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export function minSizeFor(widget) {
|
|||||||
if (key === 'button') return { w: 60, h: 30 };
|
if (key === 'button') return { w: 60, h: 30 };
|
||||||
if (key === 'led') return { w: 20, h: 20 };
|
if (key === 'led') return { w: 20, h: 20 };
|
||||||
if (key === 'icon') return { w: 24, h: 24 };
|
if (key === 'icon') return { w: 24, h: 24 };
|
||||||
|
if (key === 'tabview') return { w: 100, h: 100 };
|
||||||
return { w: 40, h: 20 };
|
return { w: 40, h: 20 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user