1260 lines
46 KiB
JavaScript
1260 lines
46 KiB
JavaScript
/**
|
|
* TaskMate Points Display Card
|
|
* A kid-friendly display card showing how many points a child (or all children) has.
|
|
*
|
|
* Modes:
|
|
* single — One child, large animated point total. Great for a child's own dashboard.
|
|
* multi — Grid showing every child with their own score. Good for family TV display.
|
|
* cumulative — Combined family total with individual breakdowns below.
|
|
*
|
|
* Config options:
|
|
* entity — sensor.taskmate_overview (required)
|
|
* mode — single | multi | cumulative (default: single)
|
|
* child_id — required for single mode
|
|
* title — optional header title override
|
|
* show_streak — show streak badge (default: true)
|
|
* show_weekly — show this week's points alongside all-time (default: true)
|
|
* show_rank — show rank medal in multi/cumulative modes (default: true)
|
|
* animate — animate number on load (default: true)
|
|
* header_color — hex colour for the card header (default: #9b59b6)
|
|
* primary_display — current_points | career_score (default: current_points)
|
|
*
|
|
* Version: 1.0.0
|
|
*/
|
|
|
|
const LitElement = customElements.get("hui-masonry-view")
|
|
? Object.getPrototypeOf(customElements.get("hui-masonry-view"))
|
|
: Object.getPrototypeOf(customElements.get("hui-view"));
|
|
|
|
const html = LitElement.prototype.html;
|
|
const css = LitElement.prototype.css;
|
|
|
|
const DEFAULT_HEADER = "#9b59b6";
|
|
|
|
const CHILD_COLOURS = [
|
|
"#e74c3c", "#3498db", "#27ae60", "#f39c12",
|
|
"#9b59b6", "#1abc9c", "#e67e22", "#2980b9",
|
|
];
|
|
|
|
const RANK_MEDAL = ["\u{1F947}", "\u{1F948}", "\u{1F949}"];
|
|
|
|
/* ─── Utility helpers ──────────────────────────────────────────────────── */
|
|
|
|
function getChildren(state) {
|
|
return state.attributes.children || [];
|
|
}
|
|
|
|
function weeklyPoints(child, tz) {
|
|
const todayKey = new Date().toLocaleDateString("en-CA", { timeZone: tz });
|
|
const today = new Date(todayKey + "T12:00:00"); // noon avoids DST edge cases
|
|
const monday = new Date(today);
|
|
monday.setDate(today.getDate() - ((today.getDay() + 6) % 7));
|
|
const mondayKey = monday.toLocaleDateString("en-CA");
|
|
|
|
const history = child.history || [];
|
|
return history
|
|
.filter(e => e.approved
|
|
&& new Date(e.timestamp).toLocaleDateString("en-CA", { timeZone: tz }) >= mondayKey)
|
|
.reduce((s, e) => s + (e.points || 0), 0);
|
|
}
|
|
|
|
function childAvatar(child, colour) {
|
|
const av = child.avatar || "mdi:account-circle";
|
|
const isIcon = av.startsWith("mdi:");
|
|
const initials = (child.name || "?")
|
|
.split(" ")
|
|
.map(w => w[0])
|
|
.slice(0, 2)
|
|
.join("")
|
|
.toUpperCase();
|
|
return html`
|
|
<div class="avatar" style="background:${colour}">
|
|
${isIcon
|
|
? html`<ha-icon icon="${av}"></ha-icon>`
|
|
: av
|
|
? html`<img src="${av}" alt="${child.name}">`
|
|
: initials}
|
|
</div>`;
|
|
}
|
|
|
|
/* ─── Main card ────────────────────────────────────────────────────────── */
|
|
|
|
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
|
|
|
|
class TaskMatePointsDisplayCard extends LitElement {
|
|
|
|
static get properties() {
|
|
return {
|
|
hass: { type: Object },
|
|
config: { type: Object },
|
|
_animated: { type: Object },
|
|
};
|
|
}
|
|
|
|
shouldUpdate(changedProps) {
|
|
if (changedProps.has("hass")) {
|
|
return window.__taskmate_hasChanged
|
|
? window.__taskmate_hasChanged(changedProps.get("hass"), this.hass, this.config?.entity)
|
|
: true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this._animated = {};
|
|
}
|
|
|
|
_t(key, params) {
|
|
const fn = window.__taskmate_localize;
|
|
return fn ? fn(this.hass, key, params) : key;
|
|
}
|
|
|
|
static get styles() {
|
|
const base = css`
|
|
:host { display: block; }
|
|
ha-card { overflow: hidden; font-family: inherit; }
|
|
|
|
/* ── Header ── */
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 18px;
|
|
color: white;
|
|
gap: 10px;
|
|
}
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.header-icon { --mdc-icon-size: 26px; opacity: 0.9; flex-shrink: 0; }
|
|
.card-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
.mode-badge {
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 10px;
|
|
padding: 3px 10px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: capitalize;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Shared body ── */
|
|
.card-body { padding: 20px 18px 22px; }
|
|
|
|
/* ── Avatar ── */
|
|
.avatar {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.3rem;
|
|
font-weight: 800;
|
|
color: white;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
.avatar ha-icon {
|
|
--mdc-icon-size: 32px;
|
|
color: white;
|
|
}
|
|
.avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* ══════════════════════════════════════════
|
|
SINGLE MODE
|
|
══════════════════════════════════════════ */
|
|
.single-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 18px;
|
|
text-align: center;
|
|
}
|
|
.single-identity {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.single-wrap .avatar {
|
|
width: 80px;
|
|
height: 80px;
|
|
font-size: 1.8rem;
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
}
|
|
.single-wrap .avatar ha-icon {
|
|
--mdc-icon-size: 48px;
|
|
}
|
|
.child-name {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--primary-text-color);
|
|
}
|
|
|
|
/* Big points number */
|
|
.big-points {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
background: var(--card-background-color, #fff);
|
|
border: 2px solid var(--divider-color, #e0e0e0);
|
|
border-radius: 24px;
|
|
padding: 22px 40px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.big-points::before {
|
|
content: "\u2B50";
|
|
font-size: 7rem;
|
|
position: absolute;
|
|
right: -12px;
|
|
bottom: -18px;
|
|
opacity: 0.06;
|
|
line-height: 1;
|
|
pointer-events: none;
|
|
}
|
|
.points-label {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--secondary-text-color);
|
|
margin-bottom: 4px;
|
|
}
|
|
.points-number {
|
|
font-size: 4.5rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
letter-spacing: -0.02em;
|
|
color: var(--primary-text-color);
|
|
transition: color 0.3s;
|
|
}
|
|
.points-star {
|
|
font-size: 2rem;
|
|
animation: star-spin 3s linear infinite;
|
|
display: inline-block;
|
|
margin-right: 6px;
|
|
vertical-align: middle;
|
|
}
|
|
@keyframes star-spin {
|
|
0% { transform: rotate(0deg) scale(1); }
|
|
50% { transform: rotate(180deg) scale(1.15); }
|
|
100% { transform: rotate(360deg) scale(1); }
|
|
}
|
|
|
|
/* Secondary metric line */
|
|
.secondary-info {
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
color: var(--secondary-text-color);
|
|
margin-top: 8px;
|
|
}
|
|
.secondary-info .secondary-value {
|
|
font-weight: 700;
|
|
color: var(--primary-text-color);
|
|
}
|
|
|
|
/* Stats row under big number */
|
|
.stats-row {
|
|
display: flex;
|
|
gap: 14px;
|
|
width: 100%;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
.stat-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: var(--secondary-background-color, #f5f5f5);
|
|
border-radius: 20px;
|
|
padding: 7px 14px;
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
color: var(--primary-text-color);
|
|
white-space: nowrap;
|
|
}
|
|
.stat-pill ha-icon { --mdc-icon-size: 17px; opacity: 0.75; }
|
|
.stat-pill .pill-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
color: var(--secondary-text-color);
|
|
}
|
|
|
|
|
|
/* ══════════════════════════════════════════
|
|
MULTI MODE
|
|
══════════════════════════════════════════ */
|
|
.multi-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
.child-tile {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 18px 12px;
|
|
background: var(--card-background-color, #fff);
|
|
border: 1px solid var(--divider-color, #e0e0e0);
|
|
border-radius: 18px;
|
|
text-align: center;
|
|
position: relative;
|
|
transition: box-shadow 0.2s;
|
|
overflow: hidden;
|
|
}
|
|
.child-tile:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
|
|
.child-tile .rank-badge {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 10px;
|
|
font-size: 1.1rem;
|
|
line-height: 1;
|
|
}
|
|
.child-tile .avatar {
|
|
width: 56px;
|
|
height: 56px;
|
|
font-size: 1.2rem;
|
|
}
|
|
.child-tile .child-name {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
}
|
|
.child-tile .tile-points {
|
|
font-size: 2.4rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
.child-tile .tile-star {
|
|
font-size: 1rem;
|
|
vertical-align: middle;
|
|
margin-right: 2px;
|
|
display: inline-block;
|
|
animation: star-spin 4s linear infinite;
|
|
}
|
|
.child-tile .tile-label {
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--secondary-text-color);
|
|
}
|
|
.child-tile .tile-weekly {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
color: var(--secondary-text-color);
|
|
}
|
|
.child-tile .streak-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
background: var(--secondary-background-color, #f5f5f5);
|
|
border-radius: 12px;
|
|
padding: 3px 8px;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Top-child accent bar */
|
|
.child-tile.top-child::after {
|
|
content: "";
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
border-radius: 0 0 18px 18px;
|
|
}
|
|
|
|
/* ══════════════════════════════════════════
|
|
CUMULATIVE MODE
|
|
══════════════════════════════════════════ */
|
|
.cumulative-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
.cumulative-total {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
padding: 22px;
|
|
background: var(--card-background-color, #fff);
|
|
border: 2px solid var(--divider-color, #e0e0e0);
|
|
border-radius: 20px;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.cumulative-total::before {
|
|
content: "\u{1F31F}";
|
|
font-size: 6rem;
|
|
position: absolute;
|
|
right: -10px;
|
|
bottom: -14px;
|
|
opacity: 0.07;
|
|
pointer-events: none;
|
|
}
|
|
.cumulative-total .points-label { margin-bottom: 6px; }
|
|
.cumulative-total .points-number { font-size: 3.8rem; }
|
|
.family-label {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
color: var(--secondary-text-color);
|
|
margin-top: 4px;
|
|
}
|
|
.divider-label {
|
|
text-align: center;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--secondary-text-color);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.divider-label::before,
|
|
.divider-label::after {
|
|
content: "";
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--divider-color, #e0e0e0);
|
|
}
|
|
|
|
.cumul-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 14px;
|
|
background: var(--card-background-color, #fff);
|
|
border: 1px solid var(--divider-color, #e0e0e0);
|
|
border-radius: 14px;
|
|
}
|
|
.cumul-row .child-name {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
flex: 1;
|
|
min-width: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.cumul-row .cumul-points {
|
|
font-size: 1.5rem;
|
|
font-weight: 900;
|
|
color: var(--primary-text-color);
|
|
flex-shrink: 0;
|
|
}
|
|
.cumul-row .cumul-star {
|
|
font-size: 0.85rem;
|
|
animation: star-spin 5s linear infinite;
|
|
display: inline-block;
|
|
margin-right: 2px;
|
|
}
|
|
.cumul-bar-wrap {
|
|
width: 100%;
|
|
background: var(--secondary-background-color, #f0f0f0);
|
|
border-radius: 6px;
|
|
height: 6px;
|
|
overflow: hidden;
|
|
margin-top: 4px;
|
|
}
|
|
.cumul-bar-fill {
|
|
height: 100%;
|
|
border-radius: 6px;
|
|
transition: width 1s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.cumul-row-inner {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ── Empty state ── */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 32px 16px;
|
|
color: var(--secondary-text-color);
|
|
font-size: 0.9rem;
|
|
}
|
|
.empty-state ha-icon { --mdc-icon-size: 40px; opacity: 0.35; display: block; margin: 0 auto 10px; }
|
|
|
|
/* Shared .tmd kit + design tokens are provided by taskmate-design.js styles(). */
|
|
|
|
/* Points Display — Playroom */
|
|
.pl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 11px; }
|
|
.pl-tile { background: var(--tmd-surface-2); border-radius: 18px; padding: 22px 12px 15px; text-align: center;
|
|
position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
|
.pl-medal { position: absolute; top: 9px; left: 11px; font-size: 16px; line-height: 1; }
|
|
.pl-pts { font-family: var(--tmd-font-display); font-weight: 800; font-size: 1.85rem; line-height: 1;
|
|
color: var(--tmd-accent); }
|
|
.pl-pts span { font-size: 0.9rem; margin-left: 2px; }
|
|
.pl-name { font-weight: 800; font-size: 0.95rem; }
|
|
.pl-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
|
|
|
|
/* Points Display — Console */
|
|
.cn-list { display: flex; flex-direction: column; gap: 9px; }
|
|
.cn-row { display: flex; align-items: center; gap: 10px; background: var(--tmd-surface-2);
|
|
border: 1px solid var(--tmd-border); border-radius: 10px; padding: 11px; }
|
|
.cn-rank { font-size: 20px; width: 34px; color: var(--tmd-dim); }
|
|
.cn-mid { flex: 1; min-width: 0; }
|
|
.cn-name { font-weight: 700; margin-bottom: 6px; }
|
|
.cn-right { text-align: right; }
|
|
.cn-pts { font-size: 22px; }
|
|
.cn-sub { font-size: 10px; }
|
|
|
|
/* Points Display — Clean Pro */
|
|
.cp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 4px; }
|
|
.cp-tile { text-align: center; padding: 8px 6px; display: flex; flex-direction: column;
|
|
align-items: center; gap: 6px; }
|
|
.cp-pts { font-size: 1.6rem; }
|
|
.cp-name { font-size: 0.82rem; font-weight: 600; }
|
|
.cp-rank { background: transparent; }
|
|
.cp-chips { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
|
|
.cp-total { justify-content: space-between; }
|
|
|
|
/* Points Display — Cumulative (designed) */
|
|
.cumul-total-d { text-align: center; padding: 8px 0 14px; }
|
|
.cumul-big { font-size: 2.3rem; color: var(--tmd-accent); margin-top: 2px; }
|
|
.cumul-big-star { font-size: 1rem; }
|
|
.cumul-list { display: flex; flex-direction: column; gap: 10px; }
|
|
.cumul-row-d { display: flex; align-items: center; gap: 11px; background: var(--tmd-surface-2);
|
|
border: 1px solid var(--tmd-border); border-radius: 14px; padding: 11px; }
|
|
.cumul-mid { flex: 1; min-width: 0; }
|
|
.cumul-head { justify-content: space-between; margin-bottom: 6px; }
|
|
.cumul-name { font-weight: 700; }
|
|
`;
|
|
const tokens = window.__taskmate_design && window.__taskmate_design.styles
|
|
? window.__taskmate_design.styles() : null;
|
|
return tokens ? [tokens, base] : base;
|
|
}
|
|
|
|
setConfig(config) {
|
|
if (!config.entity) throw new Error("TaskMate Points Display: 'entity' is required.");
|
|
this.config = {
|
|
mode: "single",
|
|
primary_display: "current_points",
|
|
show_streak: true,
|
|
show_weekly: true,
|
|
show_rank: true,
|
|
animate: true,
|
|
header_color: DEFAULT_HEADER,
|
|
...config,
|
|
};
|
|
}
|
|
|
|
static getConfigElement() {
|
|
return document.createElement("taskmate-points-display-card-editor");
|
|
}
|
|
|
|
static getStubConfig() {
|
|
return { entity: "sensor.taskmate_overview", mode: "multi" };
|
|
}
|
|
|
|
getCardSize() { return 3; }
|
|
|
|
/* ── Data helpers ───────────────────────────────────────────────────── */
|
|
|
|
_stateObj() {
|
|
return this.hass?.states?.[this.config.entity];
|
|
}
|
|
|
|
_allChildren() {
|
|
const s = this._stateObj();
|
|
return s ? getChildren(s) : [];
|
|
}
|
|
|
|
_isCareerMode() {
|
|
return this.config.primary_display === "career_score";
|
|
}
|
|
|
|
_primaryValue(child) {
|
|
return this._isCareerMode() ? (child.career_score || 0) : (child.points || 0);
|
|
}
|
|
|
|
_secondaryValue(child) {
|
|
return this._isCareerMode() ? (child.points || 0) : (child.career_score || 0);
|
|
}
|
|
|
|
_primaryLabel() {
|
|
return this._isCareerMode()
|
|
? this._t("points_display.career_score_label")
|
|
: this._t("points_display.total_points");
|
|
}
|
|
|
|
_secondaryLabel() {
|
|
return this._isCareerMode()
|
|
? this._t("points_display.current_points_label")
|
|
: this._t("points_display.career_score_label");
|
|
}
|
|
|
|
_rankedChildren() {
|
|
return [...this._allChildren()].sort((a, b) => this._primaryValue(b) - this._primaryValue(a));
|
|
}
|
|
|
|
_singleChild() {
|
|
const cid = this.config.child_id;
|
|
return this._allChildren().find(c => c.id === cid) || null;
|
|
}
|
|
|
|
/* ── Render helpers ─────────────────────────────────────────────────── */
|
|
|
|
_headerStyle() {
|
|
return `background: ${_safeColor(this.config.header_color, DEFAULT_HEADER)};`;
|
|
}
|
|
|
|
_defaultTitle() {
|
|
const mode = this.config.mode || "single";
|
|
if (mode === "single") {
|
|
const c = this._singleChild();
|
|
return c ? this._t("points_display.single_title", { name: c.name }) : this._t("points_display.default_title");
|
|
}
|
|
if (mode === "cumulative") return this._t("points_display.family_title");
|
|
return this._t("points_display.board_title");
|
|
}
|
|
|
|
/* ── Single mode ────────────────────────────────────────────────────── */
|
|
|
|
_renderSingle() {
|
|
const child = this._singleChild();
|
|
if (!child) {
|
|
return html`<div class="empty-state">
|
|
<ha-icon icon="mdi:account-question"></ha-icon>
|
|
${this._t("points_display.empty_no_child")}<br>
|
|
${this._t("points_display.empty_set_child_id")}
|
|
</div>`;
|
|
}
|
|
|
|
const children = this._allChildren();
|
|
const colour = CHILD_COLOURS[children.indexOf(child) % CHILD_COLOURS.length];
|
|
const primary = this._primaryValue(child);
|
|
const secondary = this._secondaryValue(child);
|
|
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
const weekly = weeklyPoints(child, tz);
|
|
const streak = child.streak || 0;
|
|
const rank = this._rankedChildren().findIndex(c => c.id === child.id) + 1;
|
|
|
|
return html`
|
|
<div class="single-wrap">
|
|
<div class="single-identity">
|
|
${childAvatar(child, colour)}
|
|
<div class="child-name">${child.name}</div>
|
|
</div>
|
|
|
|
<div class="big-points">
|
|
<div class="points-label">${this._primaryLabel()}</div>
|
|
<div class="points-number" style="color:${colour}">
|
|
<span class="points-star">\u2B50</span>${primary.toLocaleString()}
|
|
</div>
|
|
<div class="secondary-info">
|
|
${this._secondaryLabel()}: <span class="secondary-value">${secondary.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-row">
|
|
${this.config.show_weekly ? html`
|
|
<div class="stat-pill">
|
|
<ha-icon icon="mdi:calendar-week"></ha-icon>
|
|
<div>
|
|
<div>${this._t("points_display.weekly_pts", { count: weekly })}</div>
|
|
<div class="pill-label">${this._t("points_display.this_week")}</div>
|
|
</div>
|
|
</div>` : ""}
|
|
${this.config.show_streak ? html`
|
|
<div class="stat-pill">
|
|
<ha-icon icon="mdi:fire"></ha-icon>
|
|
<div>
|
|
<div>${streak === 1
|
|
? this._t("points_display.streak_day", { count: streak })
|
|
: this._t("points_display.streak_days", { count: streak })}</div>
|
|
<div class="pill-label">${this._t("points_display.streak")}</div>
|
|
</div>
|
|
</div>` : ""}
|
|
${this.config.show_rank && children.length > 1 ? html`
|
|
<div class="stat-pill">
|
|
<ha-icon icon="mdi:trophy"></ha-icon>
|
|
<div>
|
|
<div>${rank <= 3 ? RANK_MEDAL[rank - 1] : `#${rank}`}</div>
|
|
<div class="pill-label">${this._t("points_display.rank")}</div>
|
|
</div>
|
|
</div>` : ""}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
/* ── Multi mode ─────────────────────────────────────────────────────── */
|
|
|
|
_renderMulti() {
|
|
const ranked = this._rankedChildren();
|
|
if (!ranked.length) {
|
|
return html`<div class="empty-state">
|
|
<ha-icon icon="mdi:account-group"></ha-icon>
|
|
${this._t("points_display.empty_no_children")}
|
|
</div>`;
|
|
}
|
|
|
|
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
return html`
|
|
<div class="multi-grid">
|
|
${ranked.map((child, idx) => {
|
|
const colour = CHILD_COLOURS[this._allChildren().indexOf(child) % CHILD_COLOURS.length];
|
|
const primary = this._primaryValue(child);
|
|
const secondary = this._secondaryValue(child);
|
|
const weekly = weeklyPoints(child, tz);
|
|
const streak = child.streak || 0;
|
|
const isTop = idx === 0;
|
|
return html`
|
|
<div class="child-tile ${isTop ? "top-child" : ""}"
|
|
style="${isTop ? `border-color:${colour};` : ""}">
|
|
${isTop ? html`<div class="rank-badge">\u{1F947}</div>` :
|
|
idx < 3 && this.config.show_rank ? html`<div class="rank-badge">${RANK_MEDAL[idx]}</div>` : ""}
|
|
${childAvatar(child, colour)}
|
|
<div class="child-name">${child.name}</div>
|
|
<div class="tile-points" style="color:${colour}">
|
|
<span class="tile-star">\u2B50</span>${primary.toLocaleString()}
|
|
</div>
|
|
<div class="tile-label">${this._isCareerMode()
|
|
? this._t("points_display.career_score_label")
|
|
: this._t("points_display.points_label")}</div>
|
|
<div class="secondary-info">
|
|
${this._secondaryLabel()}: <span class="secondary-value">${secondary.toLocaleString()}</span>
|
|
</div>
|
|
${this.config.show_weekly ? html`
|
|
<div class="tile-weekly">${this._t("points_display.weekly_plus", { count: weekly })}</div>` : ""}
|
|
${this.config.show_streak ? html`
|
|
<div class="streak-chip">\u{1F525} ${streak}</div>` : ""}
|
|
${isTop ? html`<div style="position:absolute;bottom:0;left:0;right:0;height:4px;background:${colour};border-radius:0 0 18px 18px;"></div>` : ""}
|
|
</div>`;
|
|
})}
|
|
</div>`;
|
|
}
|
|
|
|
/* ── Cumulative mode ────────────────────────────────────────────────── */
|
|
|
|
_renderCumulative() {
|
|
const ranked = this._rankedChildren();
|
|
if (!ranked.length) {
|
|
return html`<div class="empty-state">
|
|
<ha-icon icon="mdi:account-group"></ha-icon>
|
|
${this._t("points_display.empty_no_children")}
|
|
</div>`;
|
|
}
|
|
|
|
const total = ranked.reduce((s, c) => s + this._primaryValue(c), 0);
|
|
const secTotal = ranked.reduce((s, c) => s + this._secondaryValue(c), 0);
|
|
|
|
return html`
|
|
<div class="cumulative-wrap">
|
|
<div class="cumulative-total">
|
|
<div class="points-label">${this._t("points_display.combined_family_total")}</div>
|
|
<div class="points-number" style="color:${_safeColor(this.config.header_color, DEFAULT_HEADER)}">
|
|
<span class="points-star">\u{1F31F}</span>${total.toLocaleString()}
|
|
</div>
|
|
<div class="secondary-info">
|
|
${this._secondaryLabel()}: <span class="secondary-value">${secTotal.toLocaleString()}</span>
|
|
</div>
|
|
<div class="family-label">${this._t("points_display.family_subtitle", { count: ranked.length })}</div>
|
|
</div>
|
|
|
|
<div class="divider-label">${this._t("points_display.individual_scores")}</div>
|
|
|
|
${ranked.map((child, idx) => {
|
|
const colour = CHILD_COLOURS[this._allChildren().indexOf(child) % CHILD_COLOURS.length];
|
|
const primary = this._primaryValue(child);
|
|
const secondary = this._secondaryValue(child);
|
|
const pct = total > 0 ? Math.round((primary / total) * 100) : 0;
|
|
return html`
|
|
<div class="cumul-row">
|
|
${childAvatar(child, colour)}
|
|
<div class="cumul-row-inner">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;">
|
|
<span class="child-name">${child.name}</span>
|
|
<span class="cumul-points" style="color:${colour}">
|
|
<span class="cumul-star">\u2B50</span>${primary.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div class="secondary-info" style="margin-top:0;margin-bottom:4px;">
|
|
${this._secondaryLabel()}: <span class="secondary-value">${secondary.toLocaleString()}</span>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
<div class="cumul-bar-wrap" style="flex:1;">
|
|
<div class="cumul-bar-fill" style="width:${pct}%;background:${colour};"></div>
|
|
</div>
|
|
<span style="font-size:0.75rem;font-weight:600;color:var(--secondary-text-color);flex-shrink:0;">${pct}%</span>
|
|
</div>
|
|
</div>
|
|
${this.config.show_rank && idx < 3 ? html`
|
|
<div style="font-size:1.4rem;flex-shrink:0;">${RANK_MEDAL[idx]}</div>` : ""}
|
|
</div>`;
|
|
})}
|
|
</div>`;
|
|
}
|
|
|
|
/* ── Main render ────────────────────────────────────────────────────── */
|
|
|
|
render() {
|
|
const design = window.__taskmate_design
|
|
? window.__taskmate_design.apply(this, this.hass, this.config, this.config.entity)
|
|
: "classic";
|
|
if (design !== "classic") return this._renderDesigned(design);
|
|
|
|
const mode = this.config.mode || "single";
|
|
const title = this.config.title || this._defaultTitle();
|
|
|
|
const modeLabel = {
|
|
single: this._t("points_display.mode_single"),
|
|
multi: this._t("points_display.mode_multi"),
|
|
cumulative: this._t("points_display.mode_cumulative"),
|
|
}[mode] || mode;
|
|
|
|
const modeIcon = { single: "mdi:star-circle", multi: "mdi:account-group", cumulative: "mdi:sigma" }[mode] || "mdi:star";
|
|
|
|
let body;
|
|
if (mode === "multi") body = this._renderMulti();
|
|
else if (mode === "cumulative") body = this._renderCumulative();
|
|
else body = this._renderSingle();
|
|
|
|
return html`
|
|
<ha-card>
|
|
<div class="card-header" style="${this._headerStyle()}">
|
|
<div class="header-left">
|
|
<ha-icon class="header-icon" icon="${modeIcon}"></ha-icon>
|
|
<span class="card-title">${title}</span>
|
|
</div>
|
|
<div class="mode-badge">${modeLabel}</div>
|
|
</div>
|
|
<div class="card-body">${body}</div>
|
|
</ha-card>`;
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════
|
|
DESIGNED STYLES (playroom / console / cleanpro) — see taskmate-design.js
|
|
Ported from docs/design/redesigns (§card-points-display).
|
|
══════════════════════════════════════════════════════════════════════ */
|
|
|
|
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
|
|
|
_av(child, tone, size) {
|
|
const a = child.avatar || "";
|
|
const inner = a.startsWith("mdi:")
|
|
? html`<ha-icon icon="${a}"></ha-icon>`
|
|
: a
|
|
? html`<img src="${a}" alt="${child.name}">`
|
|
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
|
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
|
}
|
|
|
|
_renderDesigned(design) {
|
|
const mode = this.config.mode || "single";
|
|
const title = this.config.title || this._defaultTitle();
|
|
const hd = _safeColor(this.config.header_color, DEFAULT_HEADER);
|
|
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
let ranked = this._rankedChildren();
|
|
if (mode === "single") {
|
|
const c = this._singleChild();
|
|
ranked = c ? [c] : [];
|
|
}
|
|
|
|
const header = html`
|
|
<div class="tmd-hd">
|
|
<span class="ic">⭐</span>
|
|
<span class="tt">${title}</span>
|
|
${mode === "multi" ? html`<span class="pill">${this._t("points_display.mode_multi")}</span>` : ""}
|
|
</div>`;
|
|
|
|
if (!ranked.length) {
|
|
return html`<ha-card class="tmd" style="--hd:${hd}">
|
|
${header}
|
|
<div class="tmd-bd"><div class="tmd-empty">${this._t("points_display.empty_no_children")}</div></div>
|
|
</ha-card>`;
|
|
}
|
|
|
|
const rows = ranked.map((child) => ({
|
|
child,
|
|
tone: this._designTone(this._allChildren().indexOf(child)),
|
|
points: this._primaryValue(child),
|
|
weekly: weeklyPoints(child, tz),
|
|
// Overview summary exposes current_streak (child.streak doesn't exist).
|
|
streak: child.current_streak ?? child.streak ?? 0,
|
|
pct: 0,
|
|
}));
|
|
const total = rows.reduce((s, r) => s + r.points, 0);
|
|
rows.forEach((r) => { r.pct = total > 0 ? Math.round((r.points / total) * 100) : 0; });
|
|
rows.forEach((r, i) => { r.rank = i; });
|
|
|
|
const body =
|
|
mode === "cumulative" ? this._cumulBody(rows, total) :
|
|
design === "playroom" ? this._designPlayroom(rows) :
|
|
design === "console" ? this._designConsole(rows) :
|
|
this._designCleanpro(rows, total);
|
|
|
|
return html`<ha-card class="tmd" style="--hd:${hd}">${header}<div class="tmd-bd">${body}</div></ha-card>`;
|
|
}
|
|
|
|
_designPlayroom(rows) {
|
|
const showRank = this.config.show_rank !== false;
|
|
const showWeekly = this.config.show_weekly !== false;
|
|
const showStreak = this.config.show_streak !== false;
|
|
return html`
|
|
<div class="pl-grid">
|
|
${rows.map((r) => html`
|
|
<div class="pl-tile" style="--ac:${r.tone}">
|
|
${showRank && r.rank < 3 ? html`<div class="pl-medal">${RANK_MEDAL[r.rank]}</div>` : ""}
|
|
${this._av(r.child, r.tone, 52)}
|
|
<div class="pl-pts">${r.points.toLocaleString()}<span>⭐</span></div>
|
|
<div class="pl-name">${r.child.name}</div>
|
|
${showWeekly || (showStreak && r.streak) ? html`
|
|
<div class="pl-chips">
|
|
${showWeekly ? html`<div class="chip soft">${this._t("points_display.weekly_plus", { count: r.weekly })}</div>` : ""}
|
|
${showStreak && r.streak ? html`<div class="chip soft">\u{1F525} ${r.streak}</div>` : ""}
|
|
</div>` : ""}
|
|
</div>`)}
|
|
</div>`;
|
|
}
|
|
|
|
_designConsole(rows) {
|
|
const max = Math.max(...rows.map((r) => r.points), 1);
|
|
const showRank = this.config.show_rank !== false;
|
|
const showWeekly = this.config.show_weekly !== false;
|
|
const showStreak = this.config.show_streak !== false;
|
|
return html`
|
|
<div class="cn-list">
|
|
${rows.map((r) => html`
|
|
<div class="cn-row" style="--ac:${r.tone}">
|
|
${showRank ? html`<div class="num cn-rank ${r.rank === 0 ? "lead" : ""}">#${r.rank + 1}</div>` : ""}
|
|
${this._av(r.child, r.tone, 38)}
|
|
<div class="cn-mid">
|
|
<div class="cn-name">${r.child.name}</div>
|
|
<div class="bar"><i style="width:${Math.round((r.points / max) * 100)}%"></i></div>
|
|
</div>
|
|
<div class="cn-right">
|
|
<div class="num cn-pts ${r.rank === 0 ? "lead" : ""}">${r.points.toLocaleString()}</div>
|
|
<div class="cn-sub muted">XP${showWeekly ? html` · +${r.weekly}` : ""}${showStreak && r.streak ? html` · \u{1F525}${r.streak}` : ""}</div>
|
|
</div>
|
|
</div>`)}
|
|
</div>`;
|
|
}
|
|
|
|
_designCleanpro(rows, total) {
|
|
const showRank = this.config.show_rank !== false;
|
|
const showWeekly = this.config.show_weekly !== false;
|
|
const showStreak = this.config.show_streak !== false;
|
|
return html`
|
|
<div class="cp-grid">
|
|
${rows.map((r) => html`
|
|
<div class="cp-tile" style="--ac:${r.tone}">
|
|
${this._av(r.child, r.tone, 46)}
|
|
<div class="big cp-pts">${r.points.toLocaleString()}</div>
|
|
<div class="cp-name">${r.child.name}</div>
|
|
<div class="cp-chips">
|
|
${showRank ? html`<div class="chip cp-rank">${r.rank === 0
|
|
? html`<span class="lead-dot">●</span> ` : ""}#${r.rank + 1}</div>` : ""}
|
|
${showWeekly ? html`<div class="chip cp-rank">+${r.weekly}</div>` : ""}
|
|
${showStreak && r.streak ? html`<div class="chip cp-rank">\u{1F525} ${r.streak}</div>` : ""}
|
|
</div>
|
|
</div>`)}
|
|
</div>
|
|
${rows.length > 1 ? html`
|
|
<div class="divide"></div>
|
|
<div class="row cp-total">
|
|
<span class="muted">${this._t("points_display.combined_family_total")}</span>
|
|
<span class="num">${total.toLocaleString()} ${this._t("points_display.points_label")}</span>
|
|
</div>` : ""}`;
|
|
}
|
|
|
|
_cumulBody(rows, total) {
|
|
return html`
|
|
<div class="cumul-total-d">
|
|
<div class="muted">${this._t("points_display.combined_family_total")}</div>
|
|
<div class="big cumul-big">${total.toLocaleString()} <span class="cumul-big-star">⭐</span></div>
|
|
</div>
|
|
<div class="cumul-list">
|
|
${rows.map((r) => html`
|
|
<div class="cumul-row-d" style="--ac:${r.tone}">
|
|
${this._av(r.child, r.tone, 40)}
|
|
<div class="cumul-mid">
|
|
<div class="row cumul-head">
|
|
<span class="cumul-name">${r.child.name}</span>
|
|
<span class="num">${r.points.toLocaleString()}</span>
|
|
</div>
|
|
<div class="bar"><i style="width:${r.pct}%"></i></div>
|
|
</div>
|
|
</div>`)}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
customElements.define("taskmate-points-display-card", TaskMatePointsDisplayCard);
|
|
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
UI EDITOR
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
|
|
class TaskMatePointsDisplayCardEditor extends LitElement {
|
|
|
|
static get properties() {
|
|
return {
|
|
hass: { type: Object },
|
|
config: { type: Object },
|
|
_children: { type: Array },
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this._children = [];
|
|
}
|
|
|
|
_t(key, params) {
|
|
const fn = window.__taskmate_localize;
|
|
return fn ? fn(this.hass, key, params) : key;
|
|
}
|
|
|
|
static get styles() {
|
|
return css`
|
|
:host { display: block; padding: 8px 0; }
|
|
ha-form { display: block; margin-bottom: 16px; }
|
|
.colour-field { display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border: 1px solid var(--outline-color, var(--divider-color, #e0e0e0)); border-radius: 4px; background: var(--mdc-text-field-fill-color, var(--card-background-color)); }
|
|
.colour-field-label { font-size: 0.82rem; color: var(--primary-color); font-weight: 500; }
|
|
.colour-field-body { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
.colour-swatch-wrapper { position: relative; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; cursor: pointer; border: 2px solid var(--divider-color, #e0e0e0); flex-shrink: 0; }
|
|
.colour-swatch-wrapper input[type="color"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; border: 0; padding: 0; }
|
|
.colour-swatch-preview { position: absolute; inset: 0; pointer-events: none; }
|
|
.colour-hex { font-family: var(--code-font-family, monospace); font-size: 0.85rem; color: var(--secondary-text-color); min-width: 70px; }
|
|
.colour-presets { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
.preset-swatch { width: 22px; height: 22px; border-radius: 50%; cursor: pointer; border: 2px solid var(--divider-color, #e0e0e0); transition: transform 0.1s; padding: 0; }
|
|
.preset-swatch:hover { transform: scale(1.15); }
|
|
.preset-swatch.active { border-color: var(--primary-text-color); box-shadow: 0 0 0 2px var(--primary-color); }
|
|
.colour-reset { font-size: 0.78rem; color: var(--secondary-text-color); background: none; border: 1px solid var(--divider-color, #e0e0e0); border-radius: 4px; padding: 4px 10px; cursor: pointer; margin-left: auto; }
|
|
.colour-helper { color: var(--secondary-text-color); font-size: 0.82rem; line-height: 1.3; }
|
|
`;
|
|
}
|
|
|
|
setConfig(config) {
|
|
this.config = config;
|
|
}
|
|
|
|
updated(changed) {
|
|
if (changed.has('hass') && this.hass && this.config?.entity) {
|
|
this._loadChildren();
|
|
}
|
|
}
|
|
|
|
_loadChildren() {
|
|
const state = this.hass?.states?.[this.config.entity];
|
|
if (!state) { this._children = []; return; }
|
|
this._children = getChildren(state);
|
|
}
|
|
|
|
_buildSchema() {
|
|
const mode = this.config.mode || 'single';
|
|
const schema = [
|
|
{ name: 'entity', selector: { entity: { domain: 'sensor' } } },
|
|
{ name: 'title', selector: { text: {} } },
|
|
{
|
|
name: 'mode',
|
|
selector: {
|
|
select: {
|
|
options: [
|
|
{ value: 'single', label: this._t('points_display.editor.mode_single') },
|
|
{ value: 'multi', label: this._t('points_display.editor.mode_multi') },
|
|
{ value: 'cumulative', label: this._t('points_display.editor.mode_cumulative') },
|
|
],
|
|
mode: 'dropdown',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'card_design',
|
|
selector: {
|
|
select: {
|
|
options: window.__taskmate_design
|
|
? window.__taskmate_design.editorOptions(this._t.bind(this))
|
|
: [{ value: 'global', label: 'Use global default' }],
|
|
mode: 'dropdown',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'primary_display',
|
|
selector: {
|
|
select: {
|
|
options: [
|
|
{ value: 'current_points', label: this._t('points_display.editor.primary_current_points') },
|
|
{ value: 'career_score', label: this._t('points_display.editor.primary_career_score') },
|
|
],
|
|
mode: 'dropdown',
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
if (mode === 'single') {
|
|
schema.push({
|
|
name: 'child_id',
|
|
selector: {
|
|
select: {
|
|
options: [
|
|
{ value: '', label: this._t('points_display.editor.select_child') },
|
|
...this._children.map((c) => ({ value: c.id, label: c.name })),
|
|
],
|
|
mode: 'dropdown',
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
schema.push({ name: 'show_weekly', selector: { boolean: {} } });
|
|
schema.push({ name: 'show_streak', selector: { boolean: {} } });
|
|
if (mode !== 'single') {
|
|
schema.push({ name: 'show_rank', selector: { boolean: {} } });
|
|
}
|
|
return schema;
|
|
}
|
|
|
|
_computeLabel = (entry) => {
|
|
const labels = {
|
|
entity: this._t('points_display.editor.entity_label'),
|
|
title: this._t('common.editor.card_title'),
|
|
mode: this._t('points_display.editor.mode_label'),
|
|
card_design: this._t('common.design.field_label'),
|
|
primary_display: this._t('points_display.editor.primary_display'),
|
|
child_id: this._t('points_display.editor.child_label'),
|
|
show_weekly: this._t('points_display.editor.show_weekly'),
|
|
show_streak: this._t('points_display.editor.show_streak'),
|
|
show_rank: this._t('points_display.editor.show_rank'),
|
|
};
|
|
return labels[entry.name] ?? entry.name;
|
|
};
|
|
|
|
_computeHelper = () => '';
|
|
|
|
render() {
|
|
if (!this.config) return html``;
|
|
const mode = this.config.mode || 'single';
|
|
const data = {
|
|
entity: this.config.entity || '',
|
|
title: this.config.title || '',
|
|
mode,
|
|
card_design: this.config.card_design || 'global',
|
|
primary_display: this.config.primary_display || 'current_points',
|
|
child_id: this.config.child_id || '',
|
|
show_weekly: this.config.show_weekly !== false,
|
|
show_streak: this.config.show_streak !== false,
|
|
show_rank: this.config.show_rank !== false,
|
|
};
|
|
return html`
|
|
<ha-form
|
|
.hass=${this.hass}
|
|
.data=${data}
|
|
.schema=${this._buildSchema()}
|
|
.computeLabel=${this._computeLabel}
|
|
.computeHelper=${this._computeHelper}
|
|
@value-changed=${this._formChanged}
|
|
></ha-form>
|
|
${this._renderColourPicker('header_color', DEFAULT_HEADER)}
|
|
`;
|
|
}
|
|
|
|
_renderColourPicker(key, defaultValue) {
|
|
const d = window.__taskmate_design;
|
|
const current = this.config[key] || defaultValue;
|
|
if (!d || !d.colourPicker) return html``;
|
|
return d.colourPicker({
|
|
defaultValue, current,
|
|
label: this._t('common.editor.header_colour'),
|
|
helper: this._t('common.editor.header_colour_helper'),
|
|
resetLabel: this._t('common.reset'),
|
|
onInput: (v) => this._update(key, v),
|
|
onPreset: (v) => this._update(key, v),
|
|
onReset: () => this._update(key, defaultValue),
|
|
});
|
|
}
|
|
|
|
_formChanged(e) {
|
|
const newValues = e.detail.value || {};
|
|
const newConfig = { ...this.config };
|
|
for (const [key, value] of Object.entries(newValues)) {
|
|
if (value === '' || value === null || value === undefined) {
|
|
delete newConfig[key];
|
|
} else if (key === 'card_design' && value === 'global') {
|
|
delete newConfig[key];
|
|
} else if (key === 'mode' && value === 'single') {
|
|
delete newConfig[key];
|
|
} else if (key === 'primary_display' && value === 'current_points') {
|
|
delete newConfig[key];
|
|
} else if ((key === 'show_weekly' || key === 'show_streak' || key === 'show_rank') && value === true) {
|
|
delete newConfig[key];
|
|
} else {
|
|
newConfig[key] = value;
|
|
}
|
|
}
|
|
this._fire(newConfig);
|
|
}
|
|
|
|
_fire(config) {
|
|
this.dispatchEvent(new CustomEvent('config-changed', {
|
|
detail: { config },
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
_set(key, value) {
|
|
this._fire({ ...this.config, [key]: value });
|
|
}
|
|
}
|
|
|
|
customElements.define("taskmate-points-display-card-editor", TaskMatePointsDisplayCardEditor);
|
|
|
|
/* ── Registration ─────────────────────────────────────────────────────── */
|
|
|
|
window.customCards = window.customCards || [];
|
|
window.customCards.push({
|
|
type: "taskmate-points-display-card",
|
|
name: "TaskMate \u2014 Points Display",
|
|
description: "Kid-friendly display of points. Supports single child, all children, or combined family total.",
|
|
preview: true,
|
|
configElement: "taskmate-points-display-card-editor",
|
|
getEntitySuggestion: (hass, entityId) =>
|
|
window.__taskmate_suggest(hass, entityId, "taskmate-points-display-card", "overview"),
|
|
});
|
|
|
|
// Version is injected by the HA resource URL (?v=x.x.x) and read from the DOM
|
|
const _tmVersion = new URLSearchParams(
|
|
Array.from(document.querySelectorAll('script[src*="/taskmate-points-display-card.js"]'))
|
|
.map(s => s.src.split("?")[1]).find(Boolean) || ""
|
|
).get("v") || "?";
|
|
console.info(
|
|
"%c TASKMATE POINTS DISPLAY CARD %c v" + _tmVersion + " ",
|
|
"background:#566573;color:white;font-weight:bold;padding:2px 4px;border-radius:4px 0 0 4px;",
|
|
"background:#2c3e50;color:white;font-weight:bold;padding:2px 4px;border-radius:0 4px 4px 0;"
|
|
);
|