Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dash-apps/web-compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ every signal of an accepted kind, grouped by category, and each prop becomes the
control its type implies. A new widget folder is fully editable with no editor
changes.

Every widget can have a border, set in its settings (color and 1-6 px). It is
stored on the item as `"border": { "color", "width" }`, outside `props`, so it
works for every widget and older layouts and app versions ignore it.

Each orientation has its own layout: editing landscape leaves portrait alone.

## Adding a widget
Expand Down
57 changes: 56 additions & 1 deletion dash-apps/web-compose/core/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,60 @@
return control;
}

// The border belongs to the cell, not the widget, so every widget gets it.
// A switch turns it on and off; color and width show only while it is on and
// are remembered while it is off.
function borderControls(item, onChange) {
const pick = {
color: (item.border && item.border.color) || "#3b82f6",
width: (item.border && item.border.width) || 2,
};
const apply = () => {
item.border = { color: pick.color, width: pick.width };
onChange();
};

const toggle = el("input", "field__control field__control--check");
toggle.type = "checkbox";
toggle.checked = !!item.border;

const picker = el("input", "field__control");
picker.type = "color";
picker.value = pick.color;
picker.addEventListener("input", () => {
pick.color = picker.value;
apply();
});

const width = el("select", "field__control");
for (let w = 1; w <= 6; w++) {
const option = el("option", null, w + " px");
option.value = w;
width.appendChild(option);
}
width.value = pick.width;
width.addEventListener("change", () => {
pick.width = Number(width.value);
apply();
});

const colorRow = field(t("editor.borderColor"), picker);
const widthRow = field(t("editor.borderWidth"), width);
const show = () => {
colorRow.hidden = widthRow.hidden = !toggle.checked;
};
show();
toggle.addEventListener("change", () => {
show();
if (toggle.checked) apply();
else {
delete item.border;
onChange();
}
});
return [field(t("editor.border"), toggle), colorRow, widthRow];
}

function openSettings(item) {
const entry = DC.registry.get(item.type);
if (!entry) return;
Expand Down Expand Up @@ -473,8 +527,8 @@
body.appendChild(field(label, bindControl(item, bind, refresh)));
}
}
body.appendChild(el("h3", "sheet__section", t("editor.appearance")));
if ((descriptor.props || []).length) {
body.appendChild(el("h3", "sheet__section", t("editor.appearance")));
for (const prop of descriptor.props) {
const control = propControl(item, prop, refresh);
if (prop.key === "label") {
Expand All @@ -484,6 +538,7 @@
body.appendChild(field(DC.i18n.localized(prop.name), control));
}
}
for (const row of borderControls(item, refresh)) body.appendChild(row);
}

// ---------- document edits ----------
Expand Down
9 changes: 9 additions & 0 deletions dash-apps/web-compose/core/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
"editor.shows": "Shows",
"editor.type": "Type",
"editor.appearance": "Appearance",
"editor.border": "Border",
"editor.borderColor": "Color",
"editor.borderWidth": "Width",
"editor.nospace": "No free space — remove a widget first",
"editor.full": "No space",
"editor.hint": "Drag to move, corner to resize, tap for settings",
Expand Down Expand Up @@ -116,6 +119,9 @@
"editor.shows": "Mit mutat",
"editor.type": "Típus",
"editor.appearance": "Megjelenés",
"editor.border": "Keret",
"editor.borderColor": "Szín",
"editor.borderWidth": "Vastagság",
"editor.nospace": "Nincs szabad hely — előbb távolíts el egy elemet",
"editor.full": "Nincs hely",
"editor.hint": "Húzd a mozgatáshoz, a sarkát a méretezéshez, koppints a beállításokhoz",
Expand Down Expand Up @@ -176,6 +182,9 @@
"editor.shows": "Zeigt",
"editor.type": "Typ",
"editor.appearance": "Darstellung",
"editor.border": "Rahmen",
"editor.borderColor": "Farbe",
"editor.borderWidth": "Breite",
"editor.nospace": "Kein freier Platz — zuerst ein Element entfernen",
"editor.full": "Kein Platz",
"editor.hint": "Ziehen zum Verschieben, Ecke zum Skalieren, tippen für Einstellungen",
Expand Down
3 changes: 2 additions & 1 deletion dash-apps/web-compose/core/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
// "portrait": { "cols": 6, "rows": 12, "items": [ item, ... ] }
// }
// }
// item: { "uid", "type", "x", "y", "w", "h", "bind": { key: signalId }, "props": {} }
// item: { "uid", "type", "x", "y", "w", "h", "bind": { key: signalId }, "props": {},
// "border": { "color": "#rrggbb", "width": 1-6 } (optional) }
//
// Compatibility rules (the document outlives app versions in both directions):
// - unknown widget type -> kept, rendered as a placeholder
Expand Down
6 changes: 6 additions & 0 deletions dash-apps/web-compose/core/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
cell.dataset.uid = item.uid;
cell.dataset.type = item.type;
cell.style.gridArea = (item.y + 1) + " / " + (item.x + 1) + " / span " + item.h + " / span " + item.w;
const border = item.border;
if (border && typeof border.color === "string" && border.color) {
cell.dataset.border = "1";
cell.style.setProperty("--border-color", border.color);
cell.style.setProperty("--border-width", Math.min(6, Math.max(1, Number(border.width) || 2)) + "px");
}
// The body is the size container; the widget renders into an inner root so
// its own @container rules can match it (a container can't query itself).
const body = document.createElement("div");
Expand Down
9 changes: 9 additions & 0 deletions dash-apps/web-compose/styles/compose.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ html, body {
border-radius: var(--radius);
overflow: hidden;
}
/* Drawn above the widget so a widget's own background can't hide it. */
.cell[data-border]::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 0 var(--border-width) var(--border-color);
pointer-events: none;
}
.cell__body { position: absolute; inset: 0; container-type: size; }
.widget { position: absolute; inset: 0; }

Expand Down
2 changes: 2 additions & 0 deletions dash-apps/web-compose/styles/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
border-bottom: 1px solid color-mix(in srgb, var(--track) 60%, transparent);
font-size: 14px;
}
/* display:flex above would otherwise win over the hidden attribute. */
.field[hidden] { display: none; }
.field__label { color: var(--text-dim); }
.field__control {
appearance: none;
Expand Down
78 changes: 78 additions & 0 deletions dash-apps/web-compose/widgets/_generated.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,84 @@
.bar__track { flex: 1; height: clamp(6px, 16cqh, 14px); }
}

/* --- battery */
.w-battery {
--level-color: var(--text-dim);
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding: clamp(8px, 7cqmin, 24px);
}
.w-battery[data-level="ok"] { --level-color: var(--accent, #22c55e); }
.w-battery[data-level="low"] { --level-color: #f59e0b; }
.w-battery[data-level="critical"] { --level-color: var(--danger); }
.battery__label {
color: var(--text-dim);
font-size: clamp(11px, 11cqmin, 22px);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.battery__row {
display: flex;
align-items: center;
gap: 0.6em;
min-width: 0;
/* ~5.6em of content (icon + gap + "100%"): size by width as well as height. */
font-size: clamp(10px, min(28cqh, 14cqw), 30px);
}
.battery__icon {
display: flex;
align-items: center;
flex: none;
height: 0.75em;
width: 1.9em;
}
.battery__shell {
position: relative;
flex: 1;
height: 100%;
padding: max(1.5px, 0.07em);
border: max(1.5px, 0.08em) solid var(--level-color);
border-radius: 0.22em;
transition: border-color 120ms linear;
}
.battery__fill {
width: 100%;
height: 100%;
background: var(--level-color);
border-radius: 0.1em;
transform-origin: left center;
transform: scaleX(0);
will-change: transform;
transition: background-color 120ms linear;
}
.battery__cap {
flex: none;
width: 0.1em;
height: 0.34em;
margin-inline-start: 1px;
border-radius: 0 0.1em 0.1em 0;
background: var(--level-color);
}
.battery__value {
color: var(--text);
font-size: 0.9em;
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1;
white-space: nowrap;
}
.battery__unit {
color: var(--text-dim);
font-size: 0.45em;
margin-inline-start: 0.3em;
}
/* No room for a label next to the icon: drop it. */
@container (max-height: 60px) { .battery__label { display: none; } }

/* --- lamp */
.w-lamp {
display: flex;
Expand Down
84 changes: 84 additions & 0 deletions dash-apps/web-compose/widgets/_generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,90 @@
}
return create;
})());
// --- battery
register({"type":"battery","version":1,"renderer":"dom","name":{"en":"Battery","hu":"Akkumulátor","de":"Batterie"},"defaultSize":{"w":2,"h":1},"minSize":{"w":1,"h":1},"binds":[{"key":"value","kinds":["number"],"name":{"en":"Value","hu":"Érték","de":"Wert"},"default":"batteryPercent"}],"props":[{"key":"label","type":"string","default":"","name":{"en":"Label","hu":"Felirat","de":"Beschriftung"}},{"key":"showLabel","type":"bool","default":true,"name":{"en":"Show label","hu":"Felirat mutatása","de":"Beschriftung anzeigen"}},{"key":"showValue","type":"bool","default":true,"name":{"en":"Show value","hu":"Érték mutatása","de":"Wert anzeigen"}},{"key":"accent","type":"color","default":"","name":{"en":"Full color","hu":"Teli szín","de":"Farbe bei voll"}}]}, (function () {
// Battery icon: a shell whose fill follows the signal and changes color as the
// charge drops (low <= 20 %, critical <= 10 %, like the car's own indicator).
// The fill is one transform and the color one data attribute, each written
// only when it changes.
const LOW = 20, CRITICAL = 10;

function create(ctx) {
const { el, props } = ctx;
if (props.accent) el.style.setProperty("--accent", props.accent);

let label = null;
if (props.showLabel) {
label = document.createElement("div");
label.className = "battery__label";
label.textContent = props.label || ctx.label("value");
} else {
el.dataset.bare = "1";
}

const icon = document.createElement("div");
icon.className = "battery__icon";
const shell = document.createElement("div");
shell.className = "battery__shell";
const fill = document.createElement("div");
fill.className = "battery__fill";
shell.appendChild(fill);
const cap = document.createElement("div");
cap.className = "battery__cap";
icon.append(shell, cap);

const row = document.createElement("div");
row.className = "battery__row";
row.appendChild(icon);

let text = null;
if (props.showValue) {
const value = document.createElement("span");
value.className = "battery__value";
text = document.createTextNode("");
const unit = document.createElement("span");
unit.className = "battery__unit";
unit.textContent = ctx.unit("value");
value.append(text, unit);
row.appendChild(value);
}

if (label) el.append(label);
el.append(row);
el.dataset.level = "none";

const ratio = (v) => {
const [min, max] = ctx.range("value") || [0, 100];
return max > min ? Math.min(1, Math.max(0, (v - min) / (max - min))) : 0;
};

let lastText = null, lastTransform = null, lastLevel = "none";
return {
update(values) {
const v = values.value;
const r = v === undefined ? 0 : Math.round(ratio(v) * 100) / 100;
const transform = "scaleX(" + r + ")";
if (transform !== lastTransform) {
lastTransform = transform;
fill.style.transform = transform;
}
const level = v === undefined ? "none" : r * 100 <= CRITICAL ? "critical" : r * 100 <= LOW ? "low" : "ok";
if (level !== lastLevel) {
lastLevel = level;
el.dataset.level = level;
}
if (text) {
const next = ctx.format("value", v);
if (next !== lastText) {
lastText = next;
text.nodeValue = next;
}
}
},
};
}
return create;
})());
// --- lamp
register({"type":"lamp","version":1,"renderer":"dom","name":{"en":"Indicator","hu":"Jelzőfény","de":"Kontrollleuchte"},"defaultSize":{"w":2,"h":2},"minSize":{"w":1,"h":1},"binds":[{"key":"value","kinds":["bool"],"name":{"en":"Condition","hu":"Feltétel","de":"Bedingung"},"default":"adasOn"},{"key":"steering","kinds":["number"],"default":"egoSteeringAngle","hidden":true}],"props":[{"key":"label","type":"string","default":"","name":{"en":"Label","hu":"Felirat","de":"Beschriftung"}},{"key":"icon","type":"enum","default":"dot","options":["dot","arrow-left","arrow-right","warning","wheel"],"name":{"en":"Icon","hu":"Ikon","de":"Symbol"}},{"key":"color","type":"color","default":"#22c55e","name":{"en":"Active color","hu":"Aktív szín","de":"Aktive Farbe"}},{"key":"blink","type":"bool","default":false,"name":{"en":"Blink when active","hu":"Villogjon aktívan","de":"Blinken wenn aktiv"}},{"key":"hideWhenOff","type":"bool","default":false,"name":{"en":"Hide when off","hu":"Rejtve, ha nem aktív","de":"Ausblenden wenn aus"}},{"key":"showLabel","type":"bool","default":false,"name":{"en":"Show label","hu":"Felirat mutatása","de":"Beschriftung anzeigen"}}]}, (function () {
// On/off indicator for any bool signal. The whole visual state is one
Expand Down
Loading
Loading