Dashboard Studio 2.5.0: phone sizes Small, Medium, Large and Row with a phone design per card
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -250,6 +250,25 @@
|
||||
return d;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ phone sizes
|
||||
// On a phone dashboard every card comes in four sizes, like widgets on a phone: Small (half width, square),
|
||||
// Medium and Large (full width, two and four rows) and Row (full width, one line). Each has its own design.
|
||||
const PHONE_SIZES = { s: [2, 2], m: [4, 2], l: [4, 4], row: [4, 1] };
|
||||
const phoneSizes = (type) => Widgets[type]?.phone?.sizes || Object.keys(PHONE_SIZES);
|
||||
/** A widget's phone size, from its width and height. */
|
||||
const phoneSizeOf = (w) => (w.w <= 2 ? "s" : w.h <= 1 ? "row" : w.h >= 4 ? "l" : "m");
|
||||
/** The size of this card nearest to a width and height. */
|
||||
function snapPhone(type, w, h) {
|
||||
const d = ([pw, ph]) => Math.abs(pw - w) * 2 + Math.abs(ph - h);
|
||||
const k = [...phoneSizes(type)].sort((a, b) => d(PHONE_SIZES[a]) - d(PHONE_SIZES[b]))[0];
|
||||
return { w: PHONE_SIZES[k][0], h: PHONE_SIZES[k][1] };
|
||||
}
|
||||
/** A card's size on a phone, from its size on a computer: small tiles Small, the rest by their height. */
|
||||
function phoneFit(type, w, h, cols = 12) {
|
||||
const k = w <= cols / 4 && h <= 3 ? "s" : h <= 1 ? "row" : h >= 4 ? "l" : "m";
|
||||
return phoneSizes(type).includes(k) ? { w: PHONE_SIZES[k][0], h: PHONE_SIZES[k][1] } : snapPhone(type, ...PHONE_SIZES[k]);
|
||||
}
|
||||
|
||||
function makeWidget(type, x = 0, y = 0) {
|
||||
const def = Widgets[type];
|
||||
const cfg = {};
|
||||
@@ -1070,8 +1089,12 @@
|
||||
for (const [prop, val] of [["--lamp", lamp], ["--w-bg", st.bg], ["--w-color", st.color], ["--w-accent", st.accent]]) {
|
||||
if (val) el.style.setProperty(prop, val); else el.style.removeProperty(prop);
|
||||
}
|
||||
// phone dashboards: each card's design for its size (Small, Medium, Large or Row)
|
||||
const size = current?.mobile && !child ? phoneSizeOf(w) : null;
|
||||
for (const k of Object.keys(PHONE_SIZES)) el.classList.toggle(`ph-${k}`, size === k);
|
||||
ctx.phone = size;
|
||||
let html;
|
||||
try { html = def.render(w.cfg, ctx); } catch (e) { console.error(e); html = `<div class="empty">${tr("This widget couldn’t be drawn.")}</div>`; }
|
||||
try { html = (size && Widgets.__phoneRender(w.type, w.cfg, ctx, size)) || def.render(w.cfg, ctx); } catch (e) { console.error(e); html = `<div class="empty">${tr("This widget couldn’t be drawn.")}</div>`; }
|
||||
const hiddenNow = editMode && !child && !isVisible(w);
|
||||
el.classList.toggle("cond-hidden", hiddenNow);
|
||||
el.innerHTML = `<div class="w-body ${def.flush ? "flush" : ""}">${html}</div>` +
|
||||
@@ -1143,7 +1166,7 @@
|
||||
const eid = b.dataset.e, type = SUGGEST[domainOf(eid)];
|
||||
const wd = makeWidget(type);
|
||||
if (Widgets[type].fields.some((f) => f.k === "entities")) wd.cfg.entities = [eid]; else wd.cfg.entity = eid;
|
||||
wd.w = current.mobile ? (wd.w >= 3 ? 4 : 2) : Math.min(wd.w, current.grid.cols);
|
||||
if (current.mobile) Object.assign(wd, phoneFit(type, wd.w, wd.h)); else wd.w = Math.min(wd.w, current.grid.cols);
|
||||
Object.assign(wd, findSpot(wd.w, wd.h));
|
||||
mutate(() => tab().widgets.push(wd));
|
||||
$(".sg-list").innerHTML = draw();
|
||||
@@ -1310,7 +1333,8 @@
|
||||
${def.fields.filter((f) => !f.when || f.when(w.cfg)).map((f) => fieldHTML(f, w.cfg[f.k], "cfg")).join("")}
|
||||
${def.container ? childrenHTML(w) : ""}
|
||||
<div class="section"><div class="section-title">${tr("Size and position")}</div>
|
||||
<div class="row">${[["x", "Column"], ["y", "Row"], ["w", "Width"], ["h", "Height"]].map(([k, l]) => `<div class="field"><label for="pos-${k}">${tr(l)}</label><input class="input" id="pos-${k}" type="number" min="${k === "w" || k === "h" ? 1 : 0}" data-path="pos" data-k="${k}" data-num="1" value="${w[k]}"></div>`).join("")}</div></div>
|
||||
${current.mobile ? fieldHTML({ k: "size", t: "seg", l: "Size on the phone", o: phoneSizes(w.type), labels: { s: tr("Small"), m: tr("Medium"), l: tr("Large"), row: tr("Row") } }, phoneSizeOf(w), "phsize") : ""}
|
||||
<div class="row">${(current.mobile ? [["x", "Column"], ["y", "Row"]] : [["x", "Column"], ["y", "Row"], ["w", "Width"], ["h", "Height"]]).map(([k, l]) => `<div class="field"><label for="pos-${k}">${tr(l)}</label><input class="input" id="pos-${k}" type="number" min="${k === "w" || k === "h" ? 1 : 0}" data-path="pos" data-k="${k}" data-num="1" value="${w[k]}"></div>`).join("")}</div></div>
|
||||
${current.tabs.length > 1 ? fieldHTML({ k: "tab", t: "select", l: "Tab", o: current.tabs.map((t) => t.id), labels: Object.fromEntries(current.tabs.map((t) => [t.id, t.name])) }, tab().id, "move") : ""}
|
||||
<div class="section"><div class="section-title">${tr("Style")}</div>${STYLE_FIELDS.map((f) => fieldHTML(f, w.style?.[f.k], "style")).join("")}</div>
|
||||
${visibilityHTML(w)}
|
||||
@@ -1542,6 +1566,13 @@
|
||||
return;
|
||||
}
|
||||
if (path === "layout") { convertLayout(!!v); renderInspector(); return; }
|
||||
if (path === "phsize") {
|
||||
const w = selectedWidget(), [pw, ph] = PHONE_SIZES[v] || [];
|
||||
if (!w || !pw) return;
|
||||
mutate(() => { Object.assign(w, { w: pw, h: ph, x: Math.min(w.x, current.grid.cols - pw) }); resolveCollisions(w); });
|
||||
renderInspector();
|
||||
return;
|
||||
}
|
||||
if (path === "nav") {
|
||||
mutate(() => { current.nav[k] = v; });
|
||||
if (k === "awayTab" || k === "awayPerson") { awayState = null; if (isChange) renderInspector(); }
|
||||
@@ -1696,9 +1727,10 @@
|
||||
for (const t of current.tabs) {
|
||||
const ws = [...t.widgets].sort((a, b) => a.y - b.y || a.x - b.x);
|
||||
for (const w of ws) {
|
||||
if (toMobile) { // phone: small tiles become half width, anything a third wide or more full width
|
||||
w.w = w.w >= from / 3 ? 4 : 2;
|
||||
w.x = w.w === 4 ? 0 : w.x * f >= 1.5 ? 2 : 0;
|
||||
if (toMobile) { // phone: small tiles become Small, the rest Medium, Large or a Row by their height
|
||||
const x0 = w.x;
|
||||
Object.assign(w, phoneFit(w.type, w.w, w.h, from));
|
||||
w.x = w.w === 4 ? 0 : x0 * f >= 1.5 ? 2 : 0;
|
||||
} else {
|
||||
w.w = Math.max(1, Math.min(to, Math.round(w.w * f)));
|
||||
w.x = Math.min(to - w.w, Math.round(w.x * f));
|
||||
@@ -1734,7 +1766,7 @@
|
||||
|
||||
function addWidget(type) {
|
||||
const wd = makeWidget(type);
|
||||
wd.w = current.mobile ? (wd.w >= 3 ? 4 : 2) : Math.min(wd.w, current.grid.cols);
|
||||
if (current.mobile) Object.assign(wd, phoneFit(wd.type, wd.w, wd.h)); else wd.w = Math.min(wd.w, current.grid.cols);
|
||||
Object.assign(wd, findSpot(wd.w, wd.h));
|
||||
mutate(() => tab().widgets.push(wd));
|
||||
select(wd.id);
|
||||
@@ -1994,6 +2026,10 @@
|
||||
drag.el.style.height = `${Math.max(rh, drag.r0.height + dy)}px`;
|
||||
n.w = Math.max(1, Math.min(cols - o.x, o.w + Math.round(dx / (colW + gap))));
|
||||
n.h = Math.max(1, o.h + Math.round(dy / (rh + gap)));
|
||||
if (current.mobile) { // phone dashboards: the nearest of the card's sizes
|
||||
Object.assign(n, snapPhone(drag.w.type, o.w + (dx / (colW + gap)), o.h + (dy / (rh + gap))));
|
||||
n.x = Math.min(n.x, cols - n.w);
|
||||
}
|
||||
}
|
||||
placeEl(drag.ph, n);
|
||||
showGuides(n, drag.w);
|
||||
|
||||
Reference in New Issue
Block a user