/**
* TaskMate Points Card
* A parent-friendly Lovelace card for managing children's points/stars.
* Allows adding or removing points with optional reasons.
*/
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 DESIGN_HEADER = "#8e44ad";
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMatePointsCard extends LitElement {
static get properties() {
return {
hass: { type: Object },
config: { type: Object },
_loading: { type: Object },
_dialog: { type: Object },
_notification: { 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;
}
_t(key, params) {
const fn = window.__taskmate_localize;
return fn ? fn(this.hass, key, params) : key;
}
constructor() {
super();
this._loading = {};
this._dialog = null;
this._notification = null;
this._notificationTimer = null;
}
static get styles() {
const base = css`
:host {
display: block;
--card-primary-color: var(--primary-color, #5c6bc0);
--card-success-color: var(--success-color, #4caf50);
--card-error-color: var(--error-color, #db4437);
--card-warning-color: var(--warning-color, #ff9800);
--text-primary: var(--primary-text-color, #212121);
--text-secondary: var(--secondary-text-color, #757575);
}
ha-card {
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
background: var(--taskmate-header-bg, var(--primary-color));
color: var(--text-primary-color, #fff);
}
.header-content {
display: flex;
align-items: center;
gap: 12px;
}
.header-icon {
--mdc-icon-size: 32px;
opacity: 0.9;
}
.header-title {
font-size: 1.3rem;
font-weight: 500;
}
.card-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Child row styles */
.child-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--card-background-color, #fff);
border: 1px solid var(--divider-color, #e0e0e0);
border-radius: 12px;
transition: box-shadow 0.2s ease;
}
.child-row:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.child-row.loading {
opacity: 0.6;
pointer-events: none;
}
.child-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.child-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--card-primary-color);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.child-avatar ha-icon {
--mdc-icon-size: 28px;
color: var(--text-primary-color, #fff);
}
.child-details {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.child-name {
font-weight: 500;
font-size: 1.1rem;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.child-points {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.95rem;
color: var(--card-warning-color);
font-weight: 600;
}
.child-points ha-icon {
--mdc-icon-size: 18px;
color: #ca8a04;
}
/* Action buttons */
.action-buttons {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.action-button {
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease, box-shadow 0.15s ease;
font-size: 1.5rem;
font-weight: bold;
}
.action-button:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.action-button:active {
transform: scale(0.95);
}
.action-button.remove {
background: var(--error-color, #db4437);
color: var(--text-primary-color, #fff);
}
.action-button.add {
background: var(--success-color, #4caf50);
color: var(--text-primary-color, #fff);
}
.action-button ha-icon {
--mdc-icon-size: 24px;
}
.action-button.loading {
opacity: 0.6;
cursor: not-allowed;
}
/* Quick point buttons */
.quick-buttons {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.quick-buttons-row {
display: flex;
gap: 5px;
align-items: center;
justify-content: flex-end;
}
.quick-btn {
border: none;
border-radius: 8px;
padding: 5px 9px;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease;
white-space: nowrap;
min-width: 36px;
text-align: center;
line-height: 1.4;
}
.quick-btn:hover {
transform: scale(1.08);
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
.quick-btn:active { transform: scale(0.95); }
.quick-btn.add {
background: var(--success-color, #4caf50);
color: var(--text-primary-color, #fff);
}
.quick-btn.remove {
background: var(--error-color, #db4437);
color: var(--text-primary-color, #fff);
}
.quick-btn.custom {
background: var(--secondary-background-color, #f5f5f5);
color: var(--primary-text-color);
border: 1px solid var(--divider-color, #e0e0e0);
font-size: 0.72rem;
}
.quick-btn.custom:hover {
background: var(--divider-color, #e0e0e0);
}
.quick-row-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--secondary-text-color);
text-align: right;
margin-bottom: 1px;
}
/* Dialog overlay */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fade-in 0.2s ease;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.dialog-content {
background: var(--card-background-color, white);
border-radius: 16px;
padding: 24px;
min-width: 300px;
max-width: 400px;
box-shadow: var(--ha-card-box-shadow, 0 8px 32px rgba(0, 0, 0, 0.2));
animation: slide-up 0.3s ease;
}
@keyframes slide-up {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dialog-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.dialog-header .icon-container {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-header .icon-container.add {
background: var(--success-color, #4caf50);
}
.dialog-header .icon-container.remove {
background: var(--error-color, #db4437);
}
.dialog-header .icon-container ha-icon {
--mdc-icon-size: 28px;
color: var(--text-primary-color, #fff);
}
.dialog-header-text {
flex: 1;
}
.dialog-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.dialog-subtitle {
font-size: 0.9rem;
color: var(--text-secondary);
}
.dialog-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
}
.form-group input,
.form-group textarea {
padding: 12px;
border: 1px solid var(--divider-color, #e0e0e0);
border-radius: 8px;
font-size: 1rem;
background: var(--card-background-color, white);
color: var(--text-primary);
transition: border-color 0.2s ease;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--card-primary-color);
}
.form-group input[type="number"] {
font-size: 1.3rem;
font-weight: 600;
text-align: center;
}
.form-group textarea {
resize: vertical;
min-height: 60px;
}
.points-label {
font-size: 0.85rem;
color: var(--text-secondary);
text-align: center;
margin-top: 4px;
}
.dialog-actions {
display: flex;
gap: 12px;
margin-top: 8px;
}
.dialog-button {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.dialog-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.dialog-button:active {
transform: translateY(0);
}
.dialog-button.cancel {
background: var(--secondary-background-color, #f5f5f5);
color: var(--text-secondary);
}
.dialog-button.confirm {
color: var(--text-primary-color, #fff);
}
.dialog-button.confirm.add {
background: var(--success-color, #4caf50);
}
.dialog-button.confirm.remove {
background: var(--error-color, #db4437);
}
.dialog-button.loading {
opacity: 0.6;
cursor: not-allowed;
}
/* Notification toast */
.notification {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 8px;
color: var(--text-primary-color, #fff);
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
box-shadow: var(--ha-card-box-shadow, 0 4px 16px rgba(0, 0, 0, 0.2));
z-index: 10000;
animation: slide-up-notification 0.3s ease;
}
@keyframes slide-up-notification {
from {
transform: translate(-50%, 20px);
opacity: 0;
}
to {
transform: translate(-50%, 0);
opacity: 1;
}
}
.notification.success {
background: var(--success-color, #4caf50);
}
.notification.error {
background: var(--error-color, #db4437);
}
.notification ha-icon {
--mdc-icon-size: 20px;
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--text-secondary);
text-align: center;
}
.empty-state ha-icon {
--mdc-icon-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state .message {
font-size: 1.1rem;
margin-bottom: 4px;
}
.empty-state .submessage {
font-size: 0.9rem;
opacity: 0.8;
}
/* Error state */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--card-error-color);
text-align: center;
}
.error-state ha-icon {
--mdc-icon-size: 48px;
margin-bottom: 16px;
}
/* Responsive adjustments */
@media (max-width: 400px) {
.child-row {
padding: 10px 12px;
}
.child-avatar {
width: 40px;
height: 40px;
}
.child-avatar ha-icon {
--mdc-icon-size: 24px;
}
.child-name {
font-size: 1rem;
}
.action-button {
width: 38px;
height: 38px;
}
.action-button ha-icon {
--mdc-icon-size: 20px;
}
.dialog-content {
min-width: 280px;
padding: 20px;
}
}
/* ── Designed styles (playroom / console / cleanpro) ── */
/* Shared .tmd kit + tokens come from taskmate-design.js styles(). */
.pc-grid { display: flex; flex-direction: column; gap: 10px; }
.pc-row {
display: flex;
align-items: center;
gap: 11px;
background: var(--tmd-surface-2);
border-radius: 18px;
padding: 11px 13px;
}
.pc-row.loading { opacity: 0.55; pointer-events: none; }
.pc-mid { flex: 1; min-width: 0; }
.pc-name { font-weight: 800; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pc-pts { font-family: var(--tmd-font-display); font-weight: 800; font-size: 22px; line-height: 1; color: var(--tmd-accent); }
.pc-pts span { font-size: 12px; margin-left: 2px; }
.pc-sub { font-size: 11px; }
/* Console list variant */
.cn-row { border: 1px solid var(--tmd-border); border-radius: 10px; padding: 10px 11px; }
.cn-name { font-weight: 700; }
.cn-cur { font-size: 11px; }
/* Clean Pro flat-row variant */
.cp-grid { display: flex; flex-direction: column; }
.cp-row { background: transparent; border-radius: 0; padding: 11px 2px; }
.cp-row + .cp-row { border-top: 1px solid var(--tmd-border); }
.cp-name { flex: 1; min-width: 0; font-weight: 600; }
.cp-num { width: 46px; text-align: right; font-size: 16px; }
.pc-quick { gap: 7px; flex-wrap: wrap; }
.pc-reason { margin-top: 12px; cursor: pointer; }
.pc-reason.con { gap: 8px; }
/* Per-child quick add/remove chips (all designed styles) */
.pc-actions { display: flex; flex-direction: column; gap: 5px; align-items: flex-end; flex-shrink: 0; }
.pc-qrow { display: flex; gap: 4px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
.pc-qchip {
font: inherit;
font-family: var(--tmd-font-display);
font-weight: 800;
font-size: 12px;
line-height: 1.3;
border: 0;
cursor: pointer;
padding: 4px 9px;
min-width: 30px;
text-align: center;
border-radius: 999px;
color: #fff;
}
.pc-qchip:disabled { opacity: 0.5; cursor: not-allowed; }
.pc-qchip.add { background: var(--tmd-good); }
.pc-qchip.remove { background: var(--tmd-bad); }
:host([data-tm-design="console"]) .pc-qchip.add {
color: #06301f;
box-shadow: 0 0 10px color-mix(in srgb, var(--tmd-good) 45%, transparent);
}
:host([data-tm-design="console"]) .pc-qchip.remove {
color: #3a0d0d;
box-shadow: 0 0 10px color-mix(in srgb, var(--tmd-bad) 45%, transparent);
}
.pc-qchip.more {
background: var(--tmd-surface-2);
color: var(--tmd-text);
border: 1px solid var(--tmd-border);
padding: 4px 8px;
}
`;
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("Please define an entity (taskmate overview sensor)");
}
this.config = {
title: "Manage Points",
...config,
};
}
getCardSize() {
return 3;
}
static getConfigElement() {
return document.createElement("taskmate-points-card-editor");
}
static getStubConfig() {
return {
entity: "sensor.taskmate_overview",
title: "Manage Points",
};
}
render() {
if (!this.hass || !this.config) {
return html``;
}
const entity = this.hass.states[this.config.entity];
if (!entity) {
return html`