158 files

This commit is contained in:
Home Assistant Version Control
2026-08-11 11:40:58 +00:00
parent c6db6ca558
commit 5388267a3e
158 changed files with 11840 additions and 698 deletions
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "Punkte manuell eingeben",
"activity.reason_allocated_to_pool": "Dem Pool zugewiesen: {name}",
"activity.reason_admin_adjustment": "Manuelle Anpassung",
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Strafe: {name}",
"activity.reason_perfect_week": "Perfekter Wochenbonus!",
@@ -801,6 +802,8 @@
"panel.settings_points_icon_hint": "Wähle ein MDI-Symbol",
"panel.settings_points_icon_label": "Punkte-Symbol",
"panel.settings_points_name_hint": "Wie du sie nennst, z.B. Sterne oder Münzen",
"panel.settings_quick_points_label": "Schnellbeträge für Punkte",
"panel.settings_quick_points_hint": "Kommagetrennte Beträge für die +/-Schaltflächen auf jeder Kinderkarte. Es werden bis zu drei verwendet.",
"panel.settings_points_name_label": "Punktename",
"panel.settings_retention_hint": "Wie viele Tage Erledigungsverlauf gespeichert werden",
"panel.settings_retention_label": "Aufbewahrung",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Fotonachweis verlangen",
"panel.chore_require_photo_hint": "Erledigungen müssen von Eltern bestätigt werden; ein Beweisfoto (falls vorhanden) erscheint bei den Genehmigungen.",
"panel.activity_view_photo": "Foto ansehen",
"panel.adjust_add": "Hinzufügen",
"panel.adjust_add_title": "{child} {amount} {points} hinzufügen",
"panel.adjust_amount": "Betrag",
"panel.adjust_custom_title": "Punkte von {child} um einen eigenen Betrag anpassen",
"panel.adjust_dialog_title": "Punkte anpassen — {name}",
"panel.adjust_done": "{sign}{amount} {points} für {child}",
"panel.adjust_err_amount": "Gib einen Betrag zwischen 1 und 10000 ein.",
"panel.adjust_reason": "Grund (optional)",
"panel.adjust_remove": "Abziehen",
"panel.adjust_remove_title": "{child} {amount} {points} abziehen",
"panel.swap_btn": "Tausch anfragen",
"panel.swap_dialog_title": "Tausch: {chore}",
"panel.swap_requester": "Heutigen Dienst übergeben an",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Versäumt",
"panel.health_count_storage": "Speicher",
"panel.chore_icon_label": "Bild",
"panel.chore_image_label": "Bild",
"panel.chore_image_hint": "Ein Foto, das statt des Symbols angezeigt wird. Beim Hochladen auf 512px verkleinert.",
"panel.chore_image_upload": "Hochladen",
"panel.chore_image_remove": "Entfernen",
"panel.chore_image_empty": "Kein Bild",
"panel.chore_image_uploading": "Wird hochgeladen…",
"panel.chore_image_failed": "Hochladen fehlgeschlagen",
"panel.chore_image_too_large": "Dieses Bild ist zu groß (max. 2 MB).",
"panel.chore_image_bad_type": "Wähle ein JPEG-, PNG- oder WebP-Bild.",
"child.editor.pre_reader": "Nur-Bilder-Modus (für kleine Kinder)",
"child.editor.pre_reader_labels": "Namen im Bildmodus anzeigen",
"common.design.accessible": "Barrierefrei"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "points manually",
"activity.reason_allocated_to_pool": "Allocated to pool: {name}",
"activity.reason_admin_adjustment": "Manual adjustment",
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Penalty: {name}",
"activity.reason_perfect_week": "Perfect week bonus!",
@@ -839,6 +840,8 @@
"panel.settings_points_icon_hint": "Pick any MDI icon",
"panel.settings_points_icon_label": "Points icon",
"panel.settings_points_name_hint": "What you call them, e.g. Stars or Coins",
"panel.settings_quick_points_label": "Quick point amounts",
"panel.settings_quick_points_hint": "Comma-separated amounts for the + and buttons on each child card. Up to three are used.",
"panel.settings_points_name_label": "Points name",
"panel.settings_retention_hint": "How many days of completion history to keep",
"panel.settings_retention_label": "Retention",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Require photo proof",
"panel.chore_require_photo_hint": "Completions must be parent-approved; an evidence photo (if provided) shows in approvals.",
"panel.activity_view_photo": "View photo",
"panel.adjust_add": "Add",
"panel.adjust_add_title": "Add {amount} {points} to {child}",
"panel.adjust_amount": "Amount",
"panel.adjust_custom_title": "Adjust {child}'s points by a custom amount",
"panel.adjust_dialog_title": "Adjust points — {name}",
"panel.adjust_done": "{sign}{amount} {points} for {child}",
"panel.adjust_err_amount": "Enter an amount between 1 and 10000.",
"panel.adjust_reason": "Reason (optional)",
"panel.adjust_remove": "Remove",
"panel.adjust_remove_title": "Remove {amount} {points} from {child}",
"panel.swap_btn": "Request swap",
"panel.swap_dialog_title": "Swap: {chore}",
"panel.swap_requester": "Give today's turn to",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Misses",
"panel.health_count_storage": "Storage",
"panel.chore_icon_label": "Picture",
"panel.chore_image_label": "Picture",
"panel.chore_image_hint": "A photo shown instead of the icon. Resized to 512px on upload.",
"panel.chore_image_upload": "Upload",
"panel.chore_image_remove": "Remove",
"panel.chore_image_empty": "No picture",
"panel.chore_image_uploading": "Uploading…",
"panel.chore_image_failed": "Upload failed",
"panel.chore_image_too_large": "That image is too large (max 2 MB).",
"panel.chore_image_bad_type": "Choose a JPEG, PNG or WebP image.",
"child.editor.pre_reader": "Picture-only mode (for young children)",
"child.editor.pre_reader_labels": "Show names in picture mode",
"common.design.accessible": "Accessible"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "points manually",
"activity.reason_allocated_to_pool": "Allocated to pool: {name}",
"activity.reason_admin_adjustment": "Manual adjustment",
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Penalty: {name}",
"activity.reason_perfect_week": "Perfect week bonus!",
@@ -839,6 +840,8 @@
"panel.settings_points_icon_hint": "Pick any MDI icon",
"panel.settings_points_icon_label": "Points icon",
"panel.settings_points_name_hint": "What you call them, e.g. Stars or Coins",
"panel.settings_quick_points_label": "Quick point amounts",
"panel.settings_quick_points_hint": "Comma-separated amounts for the + and buttons on each child card. Up to three are used.",
"panel.settings_points_name_label": "Points name",
"panel.settings_retention_hint": "How many days of completion history to keep",
"panel.settings_retention_label": "Retention",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Require photo proof",
"panel.chore_require_photo_hint": "Completions must be parent-approved; an evidence photo (if provided) shows in approvals.",
"panel.activity_view_photo": "View photo",
"panel.adjust_add": "Add",
"panel.adjust_add_title": "Add {amount} {points} to {child}",
"panel.adjust_amount": "Amount",
"panel.adjust_custom_title": "Adjust {child}'s points by a custom amount",
"panel.adjust_dialog_title": "Adjust points — {name}",
"panel.adjust_done": "{sign}{amount} {points} for {child}",
"panel.adjust_err_amount": "Enter an amount between 1 and 10000.",
"panel.adjust_reason": "Reason (optional)",
"panel.adjust_remove": "Remove",
"panel.adjust_remove_title": "Remove {amount} {points} from {child}",
"panel.swap_btn": "Request swap",
"panel.swap_dialog_title": "Swap: {chore}",
"panel.swap_requester": "Give today's turn to",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Misses",
"panel.health_count_storage": "Storage",
"panel.chore_icon_label": "Picture",
"panel.chore_image_label": "Picture",
"panel.chore_image_hint": "A photo shown instead of the icon. Resized to 512px on upload.",
"panel.chore_image_upload": "Upload",
"panel.chore_image_remove": "Remove",
"panel.chore_image_empty": "No picture",
"panel.chore_image_uploading": "Uploading…",
"panel.chore_image_failed": "Upload failed",
"panel.chore_image_too_large": "That image is too large (max 2 MB).",
"panel.chore_image_bad_type": "Choose a JPEG, PNG or WebP image.",
"child.editor.pre_reader": "Picture-only mode (for young children)",
"child.editor.pre_reader_labels": "Show names in picture mode",
"common.design.accessible": "Accessible"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "points manuellement",
"activity.reason_allocated_to_pool": "Alloué à la cagnotte : {name}",
"activity.reason_admin_adjustment": "Ajustement manuel",
"activity.reason_bonus": "Bonus : {name}",
"activity.reason_penalty": "Pénalité : {name}",
"activity.reason_perfect_week": "Bonus de semaine parfaite !",
@@ -801,6 +802,8 @@
"panel.settings_points_icon_hint": "Choisissez une icône MDI",
"panel.settings_points_icon_label": "Icône des points",
"panel.settings_points_name_hint": "Comment vous les appelez, ex. Étoiles ou Pièces",
"panel.settings_quick_points_label": "Montants rapides de points",
"panel.settings_quick_points_hint": "Montants séparés par des virgules pour les boutons + et de chaque carte enfant. Trois au maximum sont utilisés.",
"panel.settings_points_name_label": "Nom des points",
"panel.settings_retention_hint": "Combien de jours d'historique de complétion garder",
"panel.settings_retention_label": "Rétention",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Exiger une preuve photo",
"panel.chore_require_photo_hint": "Les réalisations doivent être approuvées par un parent ; une photo de preuve (si fournie) s'affiche dans les approbations.",
"panel.activity_view_photo": "Voir la photo",
"panel.adjust_add": "Ajouter",
"panel.adjust_add_title": "Ajouter {amount} {points} à {child}",
"panel.adjust_amount": "Montant",
"panel.adjust_custom_title": "Ajuster les points de {child} d'un montant personnalisé",
"panel.adjust_dialog_title": "Ajuster les points — {name}",
"panel.adjust_done": "{sign}{amount} {points} pour {child}",
"panel.adjust_err_amount": "Saisissez un montant entre 1 et 10000.",
"panel.adjust_reason": "Motif (facultatif)",
"panel.adjust_remove": "Retirer",
"panel.adjust_remove_title": "Retirer {amount} {points} à {child}",
"panel.swap_btn": "Demander un échange",
"panel.swap_dialog_title": "Échange : {chore}",
"panel.swap_requester": "Donner le tour d'aujourd'hui à",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Manquements",
"panel.health_count_storage": "Stockage",
"panel.chore_icon_label": "Image",
"panel.chore_image_label": "Image",
"panel.chore_image_hint": "Une photo affichée à la place de l'icône. Redimensionnée à 512px à l'envoi.",
"panel.chore_image_upload": "Envoyer",
"panel.chore_image_remove": "Retirer",
"panel.chore_image_empty": "Aucune image",
"panel.chore_image_uploading": "Envoi…",
"panel.chore_image_failed": "Échec de l'envoi",
"panel.chore_image_too_large": "Cette image est trop volumineuse (max 2 Mo).",
"panel.chore_image_bad_type": "Choisissez une image JPEG, PNG ou WebP.",
"child.editor.pre_reader": "Mode images seules (jeunes enfants)",
"child.editor.pre_reader_labels": "Afficher les noms en mode images",
"common.design.accessible": "Accessible"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "poeng manuelt",
"activity.reason_allocated_to_pool": "Tildelt til sparegris: {name}",
"activity.reason_admin_adjustment": "Manuell justering",
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Straff: {name}",
"activity.reason_perfect_week": "Perfekt uke-bonus!",
@@ -801,6 +802,8 @@
"panel.settings_points_icon_hint": "Velg et MDI-ikon",
"panel.settings_points_icon_label": "Poengikon",
"panel.settings_points_name_hint": "Hva du kaller dem, f.eks. Stjerner eller Mynter",
"panel.settings_quick_points_label": "Hurtigbeløp for poeng",
"panel.settings_quick_points_hint": "Kommaseparerte beløp for +- og -knappene på hvert barnekort. Opptil tre brukes.",
"panel.settings_points_name_label": "Poengnavn",
"panel.settings_retention_hint": "Hvor mange dager med fullføringshistorikk som beholdes",
"panel.settings_retention_label": "Oppbevaring",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Krev bildebevis",
"panel.chore_require_photo_hint": "Fullføringer må godkjennes av forelder; et bevisbilde (hvis lagt ved) vises i godkjenninger.",
"panel.activity_view_photo": "Vis bilde",
"panel.adjust_add": "Legg til",
"panel.adjust_add_title": "Legg til {amount} {points} til {child}",
"panel.adjust_amount": "Beløp",
"panel.adjust_custom_title": "Juster poengene til {child} med et eget beløp",
"panel.adjust_dialog_title": "Juster poeng — {name}",
"panel.adjust_done": "{sign}{amount} {points} til {child}",
"panel.adjust_err_amount": "Skriv inn et beløp mellom 1 og 10000.",
"panel.adjust_reason": "Årsak (valgfritt)",
"panel.adjust_remove": "Trekk fra",
"panel.adjust_remove_title": "Trekk fra {amount} {points} fra {child}",
"panel.swap_btn": "Be om bytte",
"panel.swap_dialog_title": "Bytte: {chore}",
"panel.swap_requester": "Gi dagens tur til",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Glemt",
"panel.health_count_storage": "Lagring",
"panel.chore_icon_label": "Bilde",
"panel.chore_image_label": "Bilde",
"panel.chore_image_hint": "Et bilde som vises i stedet for ikonet. Skaleres til 512px ved opplasting.",
"panel.chore_image_upload": "Last opp",
"panel.chore_image_remove": "Fjern",
"panel.chore_image_empty": "Ingen bilde",
"panel.chore_image_uploading": "Laster opp…",
"panel.chore_image_failed": "Opplasting mislyktes",
"panel.chore_image_too_large": "Bildet er for stort (maks 2 MB).",
"panel.chore_image_bad_type": "Velg et JPEG-, PNG- eller WebP-bilde.",
"child.editor.pre_reader": "Kun bilder (for små barn)",
"child.editor.pre_reader_labels": "Vis navn i bildemodus",
"common.design.accessible": "Tilgjengelig"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "poeng manuelt",
"activity.reason_allocated_to_pool": "Tildelt til sparegris: {name}",
"activity.reason_admin_adjustment": "Manuell justering",
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Straff: {name}",
"activity.reason_perfect_week": "Perfekt veke-bonus!",
@@ -801,6 +802,8 @@
"panel.settings_points_icon_hint": "Vel eit MDI-ikon",
"panel.settings_points_icon_label": "Poengikon",
"panel.settings_points_name_hint": "Kva du kallar dei, t.d. Stjerner eller Myntar",
"panel.settings_quick_points_label": "Snøggbeløp for poeng",
"panel.settings_quick_points_hint": "Kommaseparerte beløp for +- og -knappane på kvart barnekort. Opptil tre blir brukte.",
"panel.settings_points_name_label": "Poengnamn",
"panel.settings_retention_hint": "Kor mange dagar med fullføringshistorikk å ta vare på",
"panel.settings_retention_label": "Oppbevaring",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Krev biletbevis",
"panel.chore_require_photo_hint": "Fullføringar må godkjennast av forelder; eit bevisbilete (om lagt ved) vert vist i godkjenningar.",
"panel.activity_view_photo": "Vis bilete",
"panel.adjust_add": "Legg til",
"panel.adjust_add_title": "Legg til {amount} {points} til {child}",
"panel.adjust_amount": "Beløp",
"panel.adjust_custom_title": "Juster poenga til {child} med eit eige beløp",
"panel.adjust_dialog_title": "Juster poeng — {name}",
"panel.adjust_done": "{sign}{amount} {points} til {child}",
"panel.adjust_err_amount": "Skriv inn eit beløp mellom 1 og 10000.",
"panel.adjust_reason": "Årsak (valfritt)",
"panel.adjust_remove": "Trekk frå",
"panel.adjust_remove_title": "Trekk frå {amount} {points} frå {child}",
"panel.swap_btn": "Be om byte",
"panel.swap_dialog_title": "Byte: {chore}",
"panel.swap_requester": "Gi dagens tur til",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Gløymt",
"panel.health_count_storage": "Lagring",
"panel.chore_icon_label": "Bilete",
"panel.chore_image_label": "Bilete",
"panel.chore_image_hint": "Eit bilete som blir vist i staden for ikonet. Skalert til 512px ved opplasting.",
"panel.chore_image_upload": "Last opp",
"panel.chore_image_remove": "Fjern",
"panel.chore_image_empty": "Ikkje noko bilete",
"panel.chore_image_uploading": "Lastar opp…",
"panel.chore_image_failed": "Opplastinga mislukkast",
"panel.chore_image_too_large": "Biletet er for stort (maks 2 MB).",
"panel.chore_image_bad_type": "Vel eit JPEG-, PNG- eller WebP-bilete.",
"child.editor.pre_reader": "Berre bilete (for små born)",
"child.editor.pre_reader_labels": "Vis namn i biletemodus",
"common.design.accessible": "Tilgjengeleg"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "pontos manualmente",
"activity.reason_allocated_to_pool": "Alocado ao cofrinho: {name}",
"activity.reason_admin_adjustment": "Ajuste manual",
"activity.reason_bonus": "Bônus: {name}",
"activity.reason_penalty": "Penalidade: {name}",
"activity.reason_perfect_week": "Bônus de semana perfeita!",
@@ -801,6 +802,8 @@
"panel.settings_points_icon_hint": "Escolha qualquer ícone MDI",
"panel.settings_points_icon_label": "Ícone dos pontos",
"panel.settings_points_name_hint": "Como você os chama, ex: Estrelas ou Moedas",
"panel.settings_quick_points_label": "Valores rápidos de pontos",
"panel.settings_quick_points_hint": "Valores separados por vírgulas para os botões + e em cada cartão de criança. São usados até três.",
"panel.settings_points_name_label": "Nome dos pontos",
"panel.settings_retention_hint": "Quantos dias de histórico de conclusões manter",
"panel.settings_retention_label": "Retenção",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Exigir prova fotográfica",
"panel.chore_require_photo_hint": "As conclusões precisam ser aprovadas pelos pais; uma foto de prova (se fornecida) aparece nas aprovações.",
"panel.activity_view_photo": "Ver foto",
"panel.adjust_add": "Adicionar",
"panel.adjust_add_title": "Adicionar {amount} {points} para {child}",
"panel.adjust_amount": "Valor",
"panel.adjust_custom_title": "Ajustar os pontos de {child} com um valor personalizado",
"panel.adjust_dialog_title": "Ajustar pontos — {name}",
"panel.adjust_done": "{sign}{amount} {points} para {child}",
"panel.adjust_err_amount": "Digite um valor entre 1 e 10000.",
"panel.adjust_reason": "Motivo (opcional)",
"panel.adjust_remove": "Remover",
"panel.adjust_remove_title": "Remover {amount} {points} de {child}",
"panel.swap_btn": "Pedir troca",
"panel.swap_dialog_title": "Troca: {chore}",
"panel.swap_requester": "Dar a vez de hoje a",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Falhas",
"panel.health_count_storage": "Armazenamento",
"panel.chore_icon_label": "Imagem",
"panel.chore_image_label": "Imagem",
"panel.chore_image_hint": "Uma foto exibida no lugar do ícone. Redimensionada para 512px no envio.",
"panel.chore_image_upload": "Enviar",
"panel.chore_image_remove": "Remover",
"panel.chore_image_empty": "Sem imagem",
"panel.chore_image_uploading": "Enviando…",
"panel.chore_image_failed": "Falha no envio",
"panel.chore_image_too_large": "Essa imagem é muito grande (máx. 2 MB).",
"panel.chore_image_bad_type": "Escolha uma imagem JPEG, PNG ou WebP.",
"child.editor.pre_reader": "Modo só imagens (crianças pequenas)",
"child.editor.pre_reader_labels": "Mostrar nomes no modo imagens",
"common.design.accessible": "Acessível"
@@ -51,6 +51,7 @@
"activity.no_events_for_filter": "No matching events",
"activity.points_manually": "pontos manualmente",
"activity.reason_allocated_to_pool": "Alocado ao mealheiro: {name}",
"activity.reason_admin_adjustment": "Ajuste manual",
"activity.reason_bonus": "Bónus: {name}",
"activity.reason_penalty": "Penalidade: {name}",
"activity.reason_perfect_week": "Bónus de semana perfeita!",
@@ -806,6 +807,8 @@
"panel.settings_points_icon_hint": "Escolha qualquer ícone MDI",
"panel.settings_points_icon_label": "Ícone dos pontos",
"panel.settings_points_name_hint": "Como os chama, ex. Estrelas ou Moedas",
"panel.settings_quick_points_label": "Valores rápidos de pontos",
"panel.settings_quick_points_hint": "Valores separados por vírgulas para os botões + e em cada cartão de criança. São usados até três.",
"panel.settings_points_name_label": "Nome dos pontos",
"panel.settings_retention_hint": "Quantos dias de histórico de conclusões manter",
"panel.settings_retention_label": "Retenção",
@@ -1402,6 +1405,16 @@
"panel.chore_require_photo_label": "Exigir prova fotográfica",
"panel.chore_require_photo_hint": "As conclusões têm de ser aprovadas pelos pais; uma foto de prova (se fornecida) aparece nas aprovações.",
"panel.activity_view_photo": "Ver foto",
"panel.adjust_add": "Adicionar",
"panel.adjust_add_title": "Adicionar {amount} {points} a {child}",
"panel.adjust_amount": "Valor",
"panel.adjust_custom_title": "Ajustar os pontos de {child} com um valor personalizado",
"panel.adjust_dialog_title": "Ajustar pontos — {name}",
"panel.adjust_done": "{sign}{amount} {points} para {child}",
"panel.adjust_err_amount": "Introduza um valor entre 1 e 10000.",
"panel.adjust_reason": "Motivo (opcional)",
"panel.adjust_remove": "Retirar",
"panel.adjust_remove_title": "Retirar {amount} {points} a {child}",
"panel.swap_btn": "Pedir troca",
"panel.swap_dialog_title": "Troca: {chore}",
"panel.swap_requester": "Dar a vez de hoje a",
@@ -1713,6 +1726,15 @@
"panel.health_count_mandatory_misses": "Falhas",
"panel.health_count_storage": "Armazenamento",
"panel.chore_icon_label": "Imagem",
"panel.chore_image_label": "Imagem",
"panel.chore_image_hint": "Uma foto mostrada em vez do ícone. Redimensionada para 512px no envio.",
"panel.chore_image_upload": "Carregar",
"panel.chore_image_remove": "Remover",
"panel.chore_image_empty": "Sem imagem",
"panel.chore_image_uploading": "A carregar…",
"panel.chore_image_failed": "Falha no carregamento",
"panel.chore_image_too_large": "Essa imagem é demasiado grande (máx. 2 MB).",
"panel.chore_image_bad_type": "Escolha uma imagem JPEG, PNG ou WebP.",
"child.editor.pre_reader": "Modo só imagens (crianças pequenas)",
"child.editor.pre_reader_labels": "Mostrar nomes no modo imagens",
"common.design.accessible": "Acessível"
@@ -85,6 +85,11 @@ class TaskMateActivityCard extends LitElement {
if (reason.startsWith('Perfect week bonus!')) {
return this._t('activity.reason_perfect_week');
}
// Manual adjustment from the admin panel (#746). Deliberately NOT in
// _UNDO_DENY_PREFIXES — a manual adjustment must stay reversible.
if (reason === 'Admin panel adjustment') {
return this._t('activity.reason_admin_adjustment');
}
const weekendMatch = reason.match(/^Weekend bonus \(×(\d+)\)$/);
if (weekendMatch) {
return this._t('activity.reason_weekend_bonus', { multiplier: weekendMatch[1] });
@@ -914,6 +914,24 @@ class TaskMateChildCard extends LitElement {
/* A chore picture sits where the digit would, so it has to carry the
same white-on-colour weight the numeral gets from its text-shadow. */
/* Uploaded chore pictures (#750). min-width/min-height:0 is load-bearing:
grid and flex items default to min-*:auto, whose content-based minimum
is the image's intrinsic aspect ratio, so without it a PORTRAIT photo
overflows a square slot (measured 64x90 in a 64x64 pre-reader tile)
because that minimum beats height:100%. Landscape photos hide it. */
.chore-badge-img {
width: 100%; height: 100%; min-width: 0; min-height: 0;
object-fit: cover; border-radius: inherit; display: block;
}
.tmd-glyph-img {
width: 1.4em; height: 1.4em; min-width: 0; min-height: 0;
object-fit: cover; border-radius: 6px; display: block;
}
.pre-tile-icon img {
width: 100%; height: 100%; min-width: 0; min-height: 0;
object-fit: cover; border-radius: 12px; display: block;
}
.chore-number-badge ha-icon {
--mdc-icon-size: 22px;
color: #fff;
@@ -2365,9 +2383,14 @@ class TaskMateChildCard extends LitElement {
* The icon inherits the row's accent so it reads as part of the style.
*/
_choreGlyph(chore) {
return chore.icon
? html`<ha-icon icon="${chore.icon}" class="tmd-glyph-icon"></ha-icon>`
: this._choreEmoji(chore);
const v = window.__taskmate_chore_visual(chore);
if (v.kind === "image") {
return html`<img class="tmd-glyph-img" src="${v.url}" alt="" loading="lazy">`;
}
if (v.kind === "icon") {
return html`<ha-icon icon="${v.icon}" class="tmd-glyph-icon"></ha-icon>`;
}
return this._choreEmoji(chore);
}
/** Mirror of _renderChoreCard's "completed today" detection, designed branch only. */
@@ -3302,7 +3325,8 @@ class TaskMateChildCard extends LitElement {
const points = chore.effective_points ?? chore.points ?? 0;
const stars = Math.max(1, Math.min(5, Math.round(points / 2) || 1));
const icon = chore.icon
const v = window.__taskmate_chore_visual(chore);
const icon = (v.kind === "icon" ? v.icon : "")
|| this._getTimeCategoryIcon(chore.time_category)
|| 'mdi:checkbox-marked-circle-outline';
@@ -3316,7 +3340,11 @@ class TaskMateChildCard extends LitElement {
? this._handleUndo(chore, child, childCompletionsToday)
: this._handleComplete(chore, child))}
>
<div class="pre-tile-icon"><ha-icon icon="${icon}"></ha-icon></div>
<div class="pre-tile-icon">
${v.kind === "image"
? html`<img src="${v.url}" alt="" loading="lazy">`
: html`<ha-icon icon="${icon}"></ha-icon>`}
</div>
${isDone ? html`<div class="pre-tile-tick"><ha-icon icon="mdi:check-bold"></ha-icon></div>` : ''}
<div class="pre-tile-stars">
${Array.from({ length: stars }, () => html`<ha-icon icon="mdi:star"></ha-icon>`)}
@@ -3336,11 +3364,14 @@ class TaskMateChildCard extends LitElement {
* a picture can't work on one and be invisible on the other.
*/
_choreNumberBadge(chore, colorClass, choreNumber) {
const v = window.__taskmate_chore_visual(chore);
return html`
<div class="chore-number-badge ${colorClass}">
${chore.icon
? html`<ha-icon icon="${chore.icon}"></ha-icon>`
: choreNumber}
${v.kind === "image"
? html`<img class="chore-badge-img" src="${v.url}" alt="" loading="lazy">`
: v.kind === "icon"
? html`<ha-icon icon="${v.icon}"></ha-icon>`
: choreNumber}
</div>`;
}
@@ -309,6 +309,24 @@
window.__taskmate_design = { IDS, resolve, isDark, apply, editorOptions, styles, cssText, tokensCSS: TOKENS, colourPicker };
/**
* The single place that decides what a chore looks like (#750).
*
* Precedence: uploaded picture MDI icon the call site's own fallback.
*
* Returns a plain descriptor rather than a template, for two reasons: this
* file has no ES-module imports and no Lit dependency and must keep it that
* way, and the `icon` case is not uniform across sites the reorder card
* renders chore.icon as an <ha-icon> only when it starts with "mdi:" and as
* raw text otherwise, because the field can hold an emoji. A descriptor lets
* each site keep its existing icon and fallback handling untouched.
*/
window.__taskmate_chore_visual = window.__taskmate_chore_visual || function (chore) {
if (chore && chore.image_url) return { kind: "image", url: chore.image_url };
if (chore && chore.icon) return { kind: "icon", icon: chore.icon };
return { kind: "none" };
};
// ── Card-picker entity suggestions (HA 2026.6+) ────────────────────────────
// Custom cards may add getEntitySuggestion(hass, entityId) to their
// window.customCards entry; when a user picks an entity in the card picker,
@@ -330,6 +330,27 @@ class TaskMatePanel extends HTMLElement {
return fn ? fn(this._hass, key, params) : key;
}
// Reason prefixes for *derived* (automatic) transactions, which can't be
// undone in isolation — mirrors the backend deny-list in coord_points.py and
// the copy in taskmate-activity-card.js. Everything else (penalty, bonus,
// gift, manual add/remove) is reversible.
//
// This is a deny-list, NOT an allow-list (#761): manual adjustments carry
// arbitrary or empty reasons, so they cannot be recognised positively.
// tests/test_panel_undo_deny_list.py pins all three copies together.
static get _UNDO_DENY_PREFIXES() {
return [
"Weekend bonus", "Streak milestone bonus", "Perfect week bonus",
"Allocated to pool:", "Pool refund", "Points decay",
"Savings interest", "Badge",
];
}
_txnReversible(reason) {
const r = reason || "";
return !TaskMatePanel._UNDO_DENY_PREFIXES.some(p => r.startsWith(p));
}
// Transaction reasons are stored in English in the DB (e.g. "Penalty: Messy room").
// Mirror the activity-card mapping so panel views render in the user's language.
_translateReason(reason) {
@@ -352,6 +373,11 @@ class TaskMatePanel extends HTMLElement {
if (reason.startsWith('Perfect week bonus!')) {
return this._t('activity.reason_perfect_week');
}
// Manual adjustment from the admin panel (#746). An exact match rather than a
// prefix: unlike "Bonus: <name>" there is no trailing detail to extract.
if (reason === 'Admin panel adjustment') {
return this._t('activity.reason_admin_adjustment');
}
const weekendMatch = reason.match(/^Weekend bonus \(×(\d+)\)$/);
if (weekendMatch) {
return this._t('activity.reason_weekend_bonus', { multiplier: weekendMatch[1] });
@@ -496,6 +522,18 @@ class TaskMatePanel extends HTMLElement {
if (act === "add-child") { this._openChildDialog(null); return; }
if (act === "gift-points") { this._openGiftDialog(); return; }
if (act === "save-gift") { this._doGiftPoints(); return; }
if (act === "adjust-points") { this._doAdjustPoints(t.dataset.id, Number(t.dataset.delta)); return; }
if (act === "adjust-points-custom") { this._openAdjustDialog(t.dataset.id); return; }
if (act === "chore-image-pick") { this._pickChoreImage(); return; }
if (act === "chore-image-remove") {
// Clears the dialog value only — the file is deleted on save by the
// coordinator, so cancelling the dialog cannot destroy the picture.
this._dialog.data.image_url = "";
this._render();
return;
}
if (act === "save-adjust-add") { this._saveAdjustDialog(1); return; }
if (act === "save-adjust-remove") { this._saveAdjustDialog(-1); return; }
if (act === "edit-child") { this._openChildDialog(t.dataset.id); return; }
if (act === "delete-child") { this._confirmDelete("child", t.dataset.id); return; }
if (act === "save-child") { this._doSaveChild(); return; }
@@ -1402,6 +1440,85 @@ class TaskMatePanel extends HTMLElement {
}
}
_pickChoreImage() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/jpeg,image/png,image/webp";
input.addEventListener("change", () => {
const file = input.files && input.files[0];
if (file) this._uploadChoreImage(file);
});
input.click();
}
// Downscale to 512px before upload: these render in slots between 20 and
// 128px, so anything larger is bytes every device re-downloads for nothing.
_downscaleChoreImage(file) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const max = 512;
let { width, height } = img;
if (width > max || height > max) {
const scale = Math.min(max / width, max / height);
width = Math.round(width * scale);
height = Math.round(height * scale);
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
canvas.toBlob(b => (b ? resolve(b) : reject(new Error("encode failed"))), "image/jpeg", 0.8);
};
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("decode failed")); };
img.src = url;
});
}
async _uploadChoreImage(file) {
if (this._choreImageBusy) return;
this._choreImageBusy = true;
this._render();
try {
const blob = await this._downscaleChoreImage(file);
const post = () => {
const token = this._hass && this._hass.auth && this._hass.auth.data
&& this._hass.auth.data.access_token;
const fd = new FormData();
fd.append("file", blob, "chore.jpg");
return fetch("/api/taskmate/image", {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: fd,
});
};
// The cached access token can go stale mid-session, so refresh and retry
// once on a 401 before giving up. Mirrors the child card's photo upload.
if (this._hass && this._hass.auth && this._hass.auth.expired
&& this._hass.auth.refreshAccessToken) {
try { await this._hass.auth.refreshAccessToken(); } catch (e) { /* surfaced below */ }
}
let resp = await post();
if (resp.status === 401 && this._hass && this._hass.auth
&& this._hass.auth.refreshAccessToken) {
await this._hass.auth.refreshAccessToken();
resp = await post();
}
if (resp.status === 413) { this._showToast("err", this._t("panel.chore_image_too_large")); return; }
if (resp.status === 400) { this._showToast("err", this._t("panel.chore_image_bad_type")); return; }
if (!resp.ok) { this._showToast("err", this._t("panel.chore_image_failed")); return; }
const body = await resp.json();
if (this._dialog && body.image_url) this._dialog.data.image_url = body.image_url;
} catch (err) {
this._showToast("err", this._t("panel.chore_image_failed"));
} finally {
this._choreImageBusy = false;
this._render();
}
}
async _doSaveChore() {
this._syncIconPickers();
const d = this._dialog.data;
@@ -1411,6 +1528,7 @@ class TaskMatePanel extends HTMLElement {
name: d.name.trim(),
description: d.description || "",
icon: d.icon || "",
image_url: d.image_url || "",
points: Number(d.points) || 0,
assigned_to: d.assigned_to || [],
requires_approval: !!d.requires_approval,
@@ -2914,6 +3032,7 @@ class TaskMatePanel extends HTMLElement {
<div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.total_points_earned || 0)}</div><div class="tm-stat-label">${this._t("panel.child_stat_earned")}</div></div>
<div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.total_chores_completed || 0)}</div><div class="tm-stat-label">${this._t("panel.child_stat_done")}</div></div>
</div>
${this._renderAdjustStrip(child, pointsName)}
<div class="tm-card-foot">
<button type="button" class="tm-btn tm-btn-sm" data-act="edit-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_edit")}</button>
<button type="button" class="tm-btn tm-btn-sm" data-act="reorder-chores-for-child" data-id="${this._esc(child.id)}" title="${this._t("panel.chore_order_title")}"> ${this._t("panel.chore_order_title")}</button>
@@ -2923,6 +3042,36 @@ class TaskMatePanel extends HTMLElement {
`;
}
// Manual point adjustment (#746). Minus amounts descend and plus amounts
// ascend so the two smallest meet in the middle and the row reads outward
// from zero. The two `-set` spans are load-bearing: with all seven buttons as
// direct flex children, a narrow card wraps only the last one and orphans ⋯
// on a row of its own. Grouping makes each side wrap as a whole unit.
_renderAdjustStrip(child, pointsName) {
const amounts = this._quickPointAmounts();
const id = this._esc(child.id);
const name = child.name || this._t("panel.child_unnamed");
const btn = (delta) => {
const amount = Math.abs(delta);
const label = this._t(delta < 0 ? "panel.adjust_remove_title" : "panel.adjust_add_title",
{ amount, points: pointsName, child: name });
return `<button type="button" class="tm-btn tm-btn-sm ${delta < 0 ? "tm-btn-danger" : "tm-btn-raised"}"
data-act="adjust-points" data-id="${id}" data-delta="${delta}"
title="${this._esc(label)}" aria-label="${this._esc(label)}">${delta < 0 ? "" : "+"}${amount}</button>`;
};
const customLabel = this._t("panel.adjust_custom_title", { child: name });
return `
<div class="tm-points-adjust">
<span class="tm-points-adjust-set">${amounts.slice().reverse().map(a => btn(-a)).join("")}</span>
<span class="tm-points-adjust-gap"></span>
<span class="tm-points-adjust-set">
${amounts.map(a => btn(a)).join("")}
<button type="button" class="tm-btn tm-btn-sm" data-act="adjust-points-custom" data-id="${id}"
title="${this._esc(customLabel)}" aria-label="${this._esc(customLabel)}"></button>
</span>
</div>`;
}
// -- Activity tab ------------------------------------------------------
_renderActivityTab() {
const pendingCompletions = this._state.pending_completions || [];
@@ -3080,7 +3229,7 @@ class TaskMatePanel extends HTMLElement {
<tbody>
${[...transactions].reverse().map(t => {
const child = childById[t.child_id];
const undoable = typeof t.reason === "string" && (t.reason.startsWith("Penalty: ") || t.reason.startsWith("Bonus: "));
const undoable = this._txnReversible(t.reason);
return `
<tr class="tm-row">
<td class="tm-meta">${this._esc(this._timeAgo(t.created_at))}</td>
@@ -4197,6 +4346,10 @@ class TaskMatePanel extends HTMLElement {
${["classic","playroom","console","cleanpro"].map(v => `<option value="${v}" ${v === (s.card_design || "classic") ? "selected" : ""}>${this._esc(this._t("common.design." + v))}</option>`).join("")}
</select>
</div>
<div class="tm-setting-row">
<div class="tm-setting-label">${this._t("panel.settings_quick_points_label")}<small>${this._t("panel.settings_quick_points_hint")}</small></div>
<input type="text" class="tm-input" data-setting="quick_point_amounts" value="${this._esc(s.quick_point_amounts == null ? "" : s.quick_point_amounts)}" placeholder="5, 10, 20">
</div>
</div>
</div>
@@ -4773,6 +4926,73 @@ class TaskMatePanel extends HTMLElement {
this._showToast("ok", this._t("panel.gift_sent"));
}
_openAdjustDialog(childId) {
const child = (this._state.children || []).find(c => c.id === childId);
if (!child) return;
this._openDialog({ kind: "adjust", data: { child_id: childId, points: 10, reason: "" } });
}
_renderAdjustDialog() {
const d = this._dialog.data;
const child = (this._state.children || []).find(c => c.id === d.child_id) || {};
return this._dialogShell(
this._t("panel.adjust_dialog_title", { name: child.name || this._t("panel.child_unnamed") }),
[
this._field(this._t("panel.adjust_amount"), "points", d.points, "number"),
this._field(this._t("panel.adjust_reason"), "reason", d.reason, "text"),
].join(""),
`<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
<button type="button" class="tm-btn tm-btn-danger" data-act="save-adjust-remove">${this._t("panel.adjust_remove")}</button>
<button type="button" class="tm-btn tm-btn-raised" data-act="save-adjust-add">${this._t("panel.adjust_add")}</button>`
);
}
// `sign` is +1 or -1, taken from which footer button was pressed, so the
// dialog needs no direction control of its own.
async _saveAdjustDialog(sign) {
const d = this._dialog.data;
const amount = Number(d.points);
if (!Number.isInteger(amount) || amount < 1 || amount > 10000) {
this._showToast("err", this._t("panel.adjust_err_amount"));
return;
}
const reason = String(d.reason || "").trim() || "Admin panel adjustment";
const ok = await this._doAdjustPoints(d.child_id, sign * amount, reason);
if (ok) this._closeDialog(true);
}
// Manual point adjustment (#746). The busy key is the CHILD, not the button,
// so a laggy tablet can neither double-award one amount nor race +5 against
// 10 on the same balance.
async _doAdjustPoints(childId, delta, reason = "Admin panel adjustment") {
const amount = Math.abs(Number(delta) || 0);
if (!childId || !Number.isInteger(amount) || amount < 1 || amount > 10000) {
this._showToast("err", this._t("panel.adjust_err_amount"));
return false;
}
if (!this._adjustBusy) this._adjustBusy = new Set();
if (this._adjustBusy.has(childId)) return false;
this._adjustBusy.add(childId);
try {
const service = delta > 0 ? "add_points" : "remove_points";
const data = { child_id: childId, points: amount };
if (reason) data.reason = reason;
const { ok, err } = await this._callService(service, data);
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return false; }
await this._fetchState();
const child = (this._state.children || []).find(c => c.id === childId);
this._showToast("ok", this._t("panel.adjust_done", {
sign: delta > 0 ? "+" : "",
amount,
points: this._state.settings.points_name || this._t("common.points"),
child: (child && child.name) || "",
}));
return true;
} finally {
this._adjustBusy.delete(childId);
}
}
_openSwapDialog(choreId) {
const c = (this._state.chores || []).find(x => x.id === choreId);
if (!c) return;
@@ -4813,6 +5033,7 @@ class TaskMatePanel extends HTMLElement {
_renderDialog() {
if (this._dialog.kind === "swap") return this._renderSwapDialog();
if (this._dialog.kind === "gift") return this._renderGiftDialog();
if (this._dialog.kind === "adjust") return this._renderAdjustDialog();
if (this._dialog.kind === "child") return this._renderChildDialog();
if (this._dialog.kind === "chore") return this._renderChoreDialog();
if (this._dialog.kind === "reward") return this._renderRewardDialog();
@@ -4935,6 +5156,7 @@ class TaskMatePanel extends HTMLElement {
`<div class="tm-field-row">
${this._field(this._t("panel.chore_description_label"), "description", d.description, "text")}
${this._iconPickerField(this._t("panel.chore_icon_label"), "icon", d.icon)}
${this._choreImageField(d.image_url || "")}
</div>`,
this._select(this._t("panel.chore_task_type_label"), "task_type", d.task_type || "standard", [
{ v: "standard", l: this._t("panel.chore_task_type_standard") },
@@ -5706,6 +5928,27 @@ class TaskMatePanel extends HTMLElement {
</div>`;
}
/** The chore picture well (#750): thumbnail or empty state, Upload, Remove. */
_choreImageField(value) {
const busy = this._choreImageBusy;
return `
<div class="tm-field">
<span class="tm-field-label">${this._t("panel.chore_image_label")}</span>
<div class="tm-image-well">
${value
? `<img class="tm-image-thumb" src="${this._esc(value)}" alt="">`
: `<div class="tm-image-empty">${this._t("panel.chore_image_empty")}</div>`}
<div class="tm-image-actions">
<button type="button" class="tm-btn tm-btn-sm" data-act="chore-image-pick" ${busy ? "disabled" : ""}>
${busy ? this._t("panel.chore_image_uploading") : this._t("panel.chore_image_upload")}
</button>
${value ? `<button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="chore-image-remove">${this._t("panel.chore_image_remove")}</button>` : ""}
</div>
</div>
<span class="tm-field-hint">${this._t("panel.chore_image_hint")}</span>
</div>`;
}
_iconPickerField(label, name, value) {
return `
<div class="tm-field">
@@ -5929,6 +6172,20 @@ class TaskMatePanel extends HTMLElement {
return new Intl.NumberFormat().format(n);
}
// Quick point-adjust amounts (#746). Stored as a comma-separated string like
// "5, 10, 20" — same shape as streak_milestones — so the generic settings
// collector can round-trip it as a plain text input. Junk degrades to the
// default rather than rendering a broken button row.
_quickPointAmounts() {
const raw = (this._state.settings || {}).quick_point_amounts;
const parsed = String(raw == null ? "" : raw)
.split(",")
.map(p => Number(p.trim()))
.filter(n => Number.isInteger(n) && n >= 1 && n <= 10000);
const unique = [...new Set(parsed)].slice(0, 3);
return unique.length ? unique : [5, 10, 20];
}
_mdi(name) {
return `<ha-icon icon="${this._esc(name || "mdi:account-circle")}"></ha-icon>`;
}
@@ -6408,6 +6665,36 @@ class TaskMatePanel extends HTMLElement {
}
.tm-stat-highlight .tm-stat-value { color: var(--tm-gold); }
/* Manual point adjustment strip on child cards (#746). Each -set is a
nowrap group so the row folds as "minus / plus + ⋯" on a narrow card
instead of orphaning the button on a line of its own. */
/* Chore picture well (#750). */
.tm-image-well {
display: flex; align-items: center; gap: 12px;
padding: 10px; border: 1px solid var(--tm-border);
border-radius: var(--tm-radius-sm); background: var(--tm-surface-2);
}
.tm-image-thumb {
width: 56px; height: 56px; min-width: 0; min-height: 0;
object-fit: cover; border-radius: var(--tm-radius-sm); display: block;
border: 1px solid var(--tm-border);
}
.tm-image-empty {
width: 56px; height: 56px; display: grid; place-items: center;
border: 1px dashed var(--tm-border); border-radius: var(--tm-radius-sm);
color: var(--tm-text-faint); font-size: 11px; text-align: center;
}
.tm-image-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.tm-points-adjust {
display: flex; gap: 4px; flex-wrap: wrap;
align-items: center;
padding-top: 10px;
}
.tm-points-adjust-set { display: flex; gap: 4px; align-items: center; }
.tm-points-adjust-gap { flex: 1 1 8px; min-width: 4px; }
.tm-points-adjust .tm-btn { min-width: 40px; justify-content: center; padding-inline: 8px; }
.tm-card-foot {
display: flex; gap: 6px;
margin-top: auto;
@@ -224,6 +224,20 @@ class TaskMateReorderCard extends LitElement {
font-weight: 600;
}
/* Uploaded chore picture (#750). Sized explicitly, NOT width:100%:
.chore-icon is applied straight to the element with no wrapper box, so
a percentage would resolve against the flex row and stretch the photo
across it. min-*:0 stops a portrait photo overflowing the square slot
via the grid/flex min-*:auto intrinsic-ratio minimum. */
.chore-icon-img {
width: 24px; height: 24px; min-width: 0; min-height: 0;
flex: 0 0 24px;
object-fit: cover; border-radius: 6px; display: block;
}
/* The designed row nests the glyph in a content-sized <span class="d-emoji">. */
.d-emoji .chore-icon-img {
width: 20px; height: 20px; flex: 0 0 20px; border-radius: 5px;
}
.chore-icon {
--mdc-icon-size: 24px;
color: var(--secondary-text-color);
@@ -911,10 +925,13 @@ class TaskMateReorderCard extends LitElement {
_renderDesignedChoreItem(chore, index, total, category, pointsIcon) {
const isFirst = index === 0;
const isLast = index === total - 1;
const choreIcon = chore.icon || "mdi:broom";
const emoji = choreIcon.startsWith("mdi:")
? html`<ha-icon icon="${choreIcon}"></ha-icon>`
: html`${choreIcon}`;
const v = window.__taskmate_chore_visual(chore);
const choreIcon = (v.kind === "icon" ? v.icon : "") || "mdi:broom";
const emoji = v.kind === "image"
? html`<img class="chore-icon-img" src="${v.url}" alt="" loading="lazy">`
: choreIcon.startsWith("mdi:")
? html`<ha-icon icon="${choreIcon}"></ha-icon>`
: html`${choreIcon}`;
return html`
<div
@@ -972,7 +989,12 @@ class TaskMateReorderCard extends LitElement {
<ha-icon icon="mdi:drag-vertical"></ha-icon>
</div>
<span class="order-number">${index + 1}</span>
<ha-icon class="chore-icon" icon="${chore.icon || "mdi:broom"}"></ha-icon>
${(() => {
const v = window.__taskmate_chore_visual(chore);
return v.kind === "image"
? html`<img class="chore-icon chore-icon-img" src="${v.url}" alt="" loading="lazy">`
: html`<ha-icon class="chore-icon" icon="${v.kind === "icon" ? v.icon : "mdi:broom"}"></ha-icon>`;
})()}
<div class="chore-info">
<span class="chore-name">${chore.name}</span>
<span class="chore-points">
@@ -275,7 +275,12 @@ class TaskMateRoutineCard extends LitElement {
</div>
<div class="body">
<div class="icon"><ha-icon icon="${chore.icon || "mdi:checkbox-marked-circle-outline"}"></ha-icon></div>
${(() => {
const v = window.__taskmate_chore_visual(chore);
return v.kind === "image"
? html`<div class="icon"><img class="chore-icon-img" src="${v.url}" alt="" loading="lazy"></div>`
: html`<div class="icon"><ha-icon icon="${v.kind === "icon" ? v.icon : "mdi:checkbox-marked-circle-outline"}"></ha-icon></div>`;
})()}
<div class="task">${chore.name}</div>
${chore.description ? html`<div class="desc">${chore.description}</div>` : ""}
<div class="points">
@@ -441,6 +446,13 @@ class TaskMateRoutineCard extends LitElement {
padding: 20px 26px 8px;
gap: 14px;
}
/* Uploaded chore picture (#750). .icon IS a sized box (128px, 104px on
small screens), so 100% is correct here. min-*:0 stops a portrait
photo overflowing it via the grid min-*:auto intrinsic-ratio minimum. */
.icon .chore-icon-img {
width: 100%; height: 100%; min-width: 0; min-height: 0;
object-fit: cover; border-radius: inherit; display: block;
}
.icon {
width: 128px; height: 128px;
border-radius: var(--tmd-radius, 34px);