/** * 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.", });