/**
* Carte Lovelace pour l'intégration CookCLI — liste navigable des recettes.
*
* Le détail d'une recette est une VUE DISTINCTE par recette (path = le
* "view_path" renvoyé par le backend), générée par une dashboard strategy
* (voir cookcli-recipe-strategy.js). C'est nécessaire : une view strategy ne
* régénère son contenu qu'au montage de la vue, pas à chaque changement de
* l'URL — avec une seule vue partagée + un paramètre `?path=`, il fallait
* recharger la page pour voir une autre recette. Une vraie vue par recette
* force un remontage à chaque navigation, donc ça marche en SPA normal.
*
* Consomme cookcli/recipes (path, view_path, name, time, servings, tags,
* image_url).
*
* Installation :
* 1. Copie ce fichier dans config/www/cookcli-card.js
* 2. Paramètres -> Tableaux de bord -> menu ⋮ -> Ressources -> Ajouter :
* URL "/local/cookcli-card.js", type "Module JavaScript"
* 3. Cette carte est normalement insérée automatiquement par la dashboard
* strategy cookcli-recipe-strategy.js — voir son README pour la config
* complète du tableau de bord. Utilisable aussi seule en carte manuelle,
* y compris sur un AUTRE tableau de bord que celui des recettes :
* type: custom:cookcli-card
* title: Mes recettes
* dashboard_path: test-cuisine # optionnel — voir plus bas
*
* Par défaut, un clic navigue vers une vue du tableau de bord COURANT (celui
* où la carte est affichée). Si tu places cette carte sur un autre tableau
* de bord que celui qui contient les vues générées par la dashboard
* strategy (ex: carte sur /lovelace, vues sur /test-cuisine), précise
* `dashboard_path: test-cuisine` pour que la navigation cible le bon
* tableau de bord plutôt que celui où la carte est physiquement posée.
*/
class CookCliCard extends HTMLElement {
static getConfigElement() {
return document.createElement("cookcli-card-editor");
}
static getStubConfig() {
return { title: "Recettes" };
}
setConfig(config) {
this._config = config || {};
this._recipes = null;
this._loading = false;
this._error = null;
if (!this.shadowRoot) {
this.attachShadow({ mode: "open" });
this.shadowRoot.addEventListener("click", (event) => this._onClick(event));
}
this._render();
}
set hass(hass) {
this._hass = hass;
if (this._recipes === null && !this._loading) {
this._fetchRecipes();
}
}
getCardSize() {
return 6;
}
async _fetchRecipes() {
this._loading = true;
this._error = null;
this._render();
try {
const msg = { type: "cookcli/recipes" };
if (this._config.entry_id) msg.entry_id = this._config.entry_id;
const result = await this._hass.connection.sendMessagePromise(msg);
this._recipes = result.recipes || [];
} catch (err) {
this._error = (err && err.message) || "Erreur inconnue";
} finally {
this._loading = false;
this._render();
}
}
_navigateToView(viewPath) {
const dashboardRoot = this._config.dashboard_path
? `/${this._config.dashboard_path.replace(/^\/+|\/+$/g, "")}`
// Racine du dashboard courant, ex: "/lovelace-cookcli" depuis
// "/lovelace-cookcli/recettes" — on y accroche la vue de la recette.
: window.location.pathname.split("/").slice(0, 2).join("/");
const url = `${dashboardRoot}/${viewPath}`;
// Navigation interne HA, sans recharger la page (comme un tap_action navigate).
window.history.pushState(null, "", url);
window.dispatchEvent(new CustomEvent("location-changed", { detail: { replace: false } }));
}
_onClick(event) {
const itemEl = event.target.closest("[data-view-path]");
if (itemEl) {
this._navigateToView(itemEl.dataset.viewPath);
}
}
_escape(str) {
const div = document.createElement("div");
div.textContent = str ?? "";
return div.innerHTML;
}
_renderList() {
if (this._loading) {
return `
Chargement…
`;
}
if (this._error) {
return `${this._escape(this._error)}
`;
}
if (!this._recipes || this._recipes.length === 0) {
return `Aucune recette trouvée.
`;
}
const rows = this._recipes
.map((r) => {
const meta = [];
if (r.time) meta.push(`⏱ ${this._escape(r.time)}`);
if (r.servings) meta.push(`🍽 ${this._escape(String(r.servings))}`);
const thumb = r.image_url
? `
`
: ``;
return `
${thumb}
${this._escape(r.name)}
${meta.join("")}
`;
})
.join("");
return `${rows}
`;
}
_render() {
if (!this.shadowRoot) return;
const title = this._config.title || "Recettes";
this.shadowRoot.innerHTML = `
${this._renderList()}
`;
}
_styles() {
return `
.card-content { padding: 0 16px 16px; }
.state-msg { padding: 16px 0; color: var(--secondary-text-color); }
.state-msg.error { color: var(--error-color, #db4437); }
.recipe-list { display: flex; flex-direction: column; }
.recipe-row {
display: flex; align-items: center; gap: 12px;
padding: 10px 4px; border-bottom: 1px solid var(--divider-color);
cursor: pointer;
}
.recipe-row:last-child { border-bottom: none; }
.recipe-row:hover { background: var(--secondary-background-color); }
.recipe-thumb {
width: 56px; height: 56px; border-radius: 8px; object-fit: cover;
flex-shrink: 0; background: var(--secondary-background-color);
}
.recipe-thumb-placeholder { background: var(--divider-color); }
.recipe-row-text { min-width: 0; }
.recipe-name {
font-weight: 500; color: var(--primary-text-color);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.recipe-meta {
display: flex; gap: 12px; font-size: 0.85em;
color: var(--secondary-text-color); white-space: nowrap;
}
`;
}
}
class CookCliCardEditor extends LitElement {
setConfig(config) {
this._config = config || {};
}
configChanged(newConfig) {
const event = new Event("config-changed", {
bubbles: true,
composed: true,
});
event.detail = { config: newConfig };
this.dispatchEvent(event);
}
static getConfigForm() {
return {
schema: [
{ name: "label", selector: { label: {} } },
{ name: "entity", required: true, selector: { entity: {} } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
{
name: "attribute",
selector: {
attribute: {},
},
context: {
filter_entity: "entity",
},
},
{ name: "unit", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{ name: "state_color", selector: { boolean: {} } },
],
},
],
computeLabel: (schema) => {
if (schema.name === "icon") return "Special Icon";
return undefined;
},
computeHelper: (schema) => {
switch (schema.name) {
case "entity":
return "This text describes the function of the entity selector";
case "unit":
return "The unit of measurement for this card";
}
return undefined;
},
assertConfig: (config) => {
if (config.other_option) {
throw new Error("'other_option' is unexpected.");
}
},
};
}
/*static getConfigForm() {
return {
schema: [
{
name: "title",
selector: { text: {} },
},
{
name: "dashboard_path",
selector: { text: {} },
helper: "Laisser vide pour naviguer dans le dashboard courant",
},
{
name: "entry_id",
selector: { text: {} },
helper: "Optionnel — ID d'entrée si plusieurs serveurs CookCLI",
},
],
computeLabel: (schema) => {
const labels = {
title: "Titre",
dashboard_path: "Chemin du dashboard",
entry_id: "Entry ID",
};
return labels[schema.name] || schema.name;
},
computeHelper: (schema) => {
switch (schema.name) {
case "entity":
return "This text describes the function of the entity selector";
case "unit":
return "The unit of measurement for this card";
}
return undefined;
},
assertConfig: (config) => {
if (config.other_option) {
throw new Error("'other_option' is unexpected.");
}
},
};
}*/
}
customElements.define("cookcli-card", CookCliCard);
customElements.define("cookcli-card-editor", CookCliCardEditor);
window.customCards = window.customCards || [];
window.customCards.push({
type: "cookcli-card",
name: "CookCLI Recipes",
description: "Liste navigable de tes recettes CookCLI.",
});