This commit is contained in:
Home Assistant Version Control
2026-09-25 04:10:30 +00:00
parent 56c80f896e
commit ad634c1b84
60 changed files with 2895 additions and 794 deletions
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Graphit",
"panel.settings_card_design_label": "Standard-Kartendesign",
"panel.settings_card_design_hint": "Der Stil neuer Karten, sofern nicht pro Karte überschrieben.",
"activity.a_chore": "eine lästige Pflicht",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Strafe: {name}",
"activity.reason_perfect_week": "Perfekter Wochenbonus!",
"activity.reason_pool_refund_assignment_changed": "Poolrückerstattung (Zuweisung der Prämie geändert): {name}",
"activity.reason_pool_refund_cost_reduced": "Pool-Rückerstattung (Prämienkosten reduziert): {name}",
"activity.reason_pool_refund_deleted": "Poolrückerstattung (Prämie gelöscht): {name}",
"activity.reason_pool_refund_expired": "Poolrückerstattung (Prämie abgelaufen): {name}",
"activity.reason_pool_refund_funding_changed": "Poolrückerstattung (Finanzierung der Prämie geändert): {name}",
"activity.reason_pool_refund_sold_out": "Poolrückerstattung (Prämie ausverkauft): {name}",
"activity.reason_streak_milestone": "Serie-Meilensteinbonus ({days} Tage Serie!)",
"activity.reason_weekend_bonus": "Wochenendbonus (×{multiplier})",
@@ -158,6 +161,7 @@
"badges.entity_not_found": "{entity} nicht gefunden",
"badges.label": "Abzeichen",
"badges.next_up": "Als Nächstes",
"badges.one_more": "Nur noch eins – dann gehört es dir!",
"badges.title_with_name": "Abzeichen von {name}",
"bonuses.add_bonus": "Bonus hinzufügen",
"bonuses.applying_to": "Beantragung bei {childName} – aktuelles Guthaben: {points} {pointsName}",
@@ -229,6 +233,8 @@
"child.extra_submit": "Senden",
"child.extra_needs_desc": "Schreib zuerst, was du gemacht hast.",
"child.open_ended_tag": "Beschreiben",
"child.weekly_target_done": "Für diese Woche erledigt",
"child.weekly_target_progress": "{done}/{target} diese Woche",
"child.photo_take": "Foto aufnehmen",
"child.photo_hint": "Zeig deine erledigte Aufgabe",
"child.photo_retake": "Neu aufnehmen",
@@ -616,6 +622,9 @@
"panel.chore_daily_cap_label": "Tageslimit (Minuten, 0 = unbegrenzt)",
"panel.chore_daily_limit_label": "Tageslimit",
"panel.chore_days_label": "Wochentage (leer = jeden Tag)",
"panel.chore_weekly_target_pill": "{count}×/Woche",
"panel.chore_weekly_target_hint": "Wie oft pro Woche die Aufgabe erledigt werden muss, an beliebigen der gewählten Tage. Ist die Anzahl erreicht, wird sie nicht mehr angezeigt; montags beginnt die Zählung neu.",
"panel.chore_weekly_target_label": "Wochenziel (0 = aus)",
"panel.chore_description_label": "Beschreibung (optional)",
"panel.chore_enabled_label": "Aktiviert",
"panel.chore_first_occurrence_label": "Erstes Vorkommen",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Graphite",
"panel.settings_card_design_label": "Default card design",
"panel.settings_card_design_hint": "The style new cards use unless overridden per card.",
"activity.a_chore": "a chore",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Penalty: {name}",
"activity.reason_perfect_week": "Perfect week bonus!",
"activity.reason_pool_refund_assignment_changed": "Pool refund (reward assignment changed): {name}",
"activity.reason_pool_refund_cost_reduced": "Pool refund (reward cost reduced): {name}",
"activity.reason_pool_refund_deleted": "Pool refund (reward deleted): {name}",
"activity.reason_pool_refund_expired": "Pool refund (reward expired): {name}",
"activity.reason_pool_refund_funding_changed": "Pool refund (reward funding changed): {name}",
"activity.reason_pool_refund_sold_out": "Pool refund (reward sold out): {name}",
"activity.reason_streak_milestone": "Streak milestone bonus ({days} day streak!)",
"activity.reason_weekend_bonus": "Weekend bonus (×{multiplier})",
@@ -159,6 +162,7 @@
"badges.entity_not_found": "{entity} not found",
"badges.label": "Badges",
"badges.next_up": "Next up",
"badges.one_more": "One more and it's yours!",
"badges.title_with_name": "{name}'s Badges",
"bonuses.add_bonus": "Add Bonus",
"bonuses.applying_to": "Applying to {childName} — current balance: {points} {pointsName}",
@@ -231,6 +235,8 @@
"child.extra_submit": "Send",
"child.extra_needs_desc": "Write what you did first.",
"child.open_ended_tag": "Describe it",
"child.weekly_target_done": "Done for this week",
"child.weekly_target_progress": "{done}/{target} this week",
"child.photo_take": "Take a photo",
"child.photo_hint": "Show your finished chore",
"child.photo_retake": "Retake",
@@ -654,6 +660,9 @@
"panel.chore_daily_cap_label": "Daily cap (minutes, 0 = unlimited)",
"panel.chore_daily_limit_label": "Daily limit",
"panel.chore_days_label": "Days of week (leave empty = every day)",
"panel.chore_weekly_target_pill": "{count}×/week",
"panel.chore_weekly_target_hint": "How many times a week this has to be done, on any of the chosen days. It stops being offered once the count is reached, and resets on Monday.",
"panel.chore_weekly_target_label": "Weekly target (0 = off)",
"panel.chore_description_label": "Description (optional)",
"panel.chore_enabled_label": "Enabled",
"panel.chore_first_occurrence_label": "First occurrence",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Graphite",
"panel.settings_card_design_label": "Default card design",
"panel.settings_card_design_hint": "The style new cards use unless overridden per card.",
"activity.a_chore": "a chore",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Penalty: {name}",
"activity.reason_perfect_week": "Perfect week bonus!",
"activity.reason_pool_refund_assignment_changed": "Pool refund (reward assignment changed): {name}",
"activity.reason_pool_refund_cost_reduced": "Pool refund (reward cost reduced): {name}",
"activity.reason_pool_refund_deleted": "Pool refund (reward deleted): {name}",
"activity.reason_pool_refund_expired": "Pool refund (reward expired): {name}",
"activity.reason_pool_refund_funding_changed": "Pool refund (reward funding changed): {name}",
"activity.reason_pool_refund_sold_out": "Pool refund (reward sold out): {name}",
"activity.reason_streak_milestone": "Streak milestone bonus ({days} day streak!)",
"activity.reason_weekend_bonus": "Weekend bonus (×{multiplier})",
@@ -159,6 +162,7 @@
"badges.entity_not_found": "{entity} not found",
"badges.label": "Badges",
"badges.next_up": "Next up",
"badges.one_more": "One more and it's yours!",
"badges.title_with_name": "{name}'s Badges",
"bonuses.add_bonus": "Add Bonus",
"bonuses.applying_to": "Applying to {childName} — current balance: {points} {pointsName}",
@@ -231,6 +235,8 @@
"child.extra_submit": "Send",
"child.extra_needs_desc": "Write what you did first.",
"child.open_ended_tag": "Describe it",
"child.weekly_target_done": "Done for this week",
"child.weekly_target_progress": "{done}/{target} this week",
"child.photo_take": "Take a photo",
"child.photo_hint": "Show your finished chore",
"child.photo_retake": "Retake",
@@ -654,6 +660,9 @@
"panel.chore_daily_cap_label": "Daily cap (minutes, 0 = unlimited)",
"panel.chore_daily_limit_label": "Daily limit",
"panel.chore_days_label": "Days of week (leave empty = every day)",
"panel.chore_weekly_target_pill": "{count}×/week",
"panel.chore_weekly_target_hint": "How many times a week this has to be done, on any of the chosen days. It stops being offered once the count is reached, and resets on Monday.",
"panel.chore_weekly_target_label": "Weekly target (0 = off)",
"panel.chore_description_label": "Description (optional)",
"panel.chore_enabled_label": "Enabled",
"panel.chore_first_occurrence_label": "First occurrence",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Graphite",
"panel.settings_card_design_label": "Design de carte par défaut",
"panel.settings_card_design_hint": "Le style des nouvelles cartes, sauf remplacement par carte.",
"activity.a_chore": "une tâche",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bonus : {name}",
"activity.reason_penalty": "Pénalité : {name}",
"activity.reason_perfect_week": "Bonus de semaine parfaite !",
"activity.reason_pool_refund_assignment_changed": "Remboursement de cagnotte (attribution de la récompense modifiée) : {name}",
"activity.reason_pool_refund_cost_reduced": "Remboursement de cagnotte (coût réduit) : {name}",
"activity.reason_pool_refund_deleted": "Remboursement de cagnotte (récompense supprimée) : {name}",
"activity.reason_pool_refund_expired": "Remboursement de cagnotte (récompense expirée) : {name}",
"activity.reason_pool_refund_funding_changed": "Remboursement de cagnotte (financement de la récompense modifié) : {name}",
"activity.reason_pool_refund_sold_out": "Remboursement de cagnotte (récompense épuisée) : {name}",
"activity.reason_streak_milestone": "Bonus de palier de série ({days} jours de suite !)",
"activity.reason_weekend_bonus": "Bonus de week-end (×{multiplier})",
@@ -158,6 +161,7 @@
"badges.entity_not_found": "{entity} introuvable",
"badges.label": "Badges",
"badges.next_up": "Prochain",
"badges.one_more": "Encore un et il est à toi !",
"badges.title_with_name": "Badges de {name}",
"bonuses.add_bonus": "Ajouter un bonus",
"bonuses.applying_to": "Application à {childName} — solde actuel : {points} {pointsName}",
@@ -229,6 +233,8 @@
"child.extra_submit": "Envoyer",
"child.extra_needs_desc": "Écris d'abord ce que tu as fait.",
"child.open_ended_tag": "À décrire",
"child.weekly_target_done": "Terminé pour cette semaine",
"child.weekly_target_progress": "{done}/{target} cette semaine",
"child.photo_take": "Prendre une photo",
"child.photo_hint": "Montre ta tâche terminée",
"child.photo_retake": "Reprendre",
@@ -616,6 +622,9 @@
"panel.chore_daily_cap_label": "Plafond journalier (minutes, 0 = illimité)",
"panel.chore_daily_limit_label": "Limite quotidienne",
"panel.chore_days_label": "Jours de la semaine (vide = tous les jours)",
"panel.chore_weekly_target_pill": "{count}×/semaine",
"panel.chore_weekly_target_hint": "Combien de fois par semaine la tâche doit être faite, n'importe lesquels des jours choisis. Elle n'est plus proposée une fois le compte atteint et repart le lundi.",
"panel.chore_weekly_target_label": "Objectif hebdomadaire (0 = désactivé)",
"panel.chore_description_label": "Description (optionnel)",
"panel.chore_enabled_label": "Activé",
"panel.chore_first_occurrence_label": "Première occurrence",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Grafitt",
"panel.settings_card_design_label": "Standard kortdesign",
"panel.settings_card_design_hint": "Stilen nye kort bruker med mindre den overstyres per kort.",
"activity.a_chore": "en oppgave",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Straff: {name}",
"activity.reason_perfect_week": "Perfekt uke-bonus!",
"activity.reason_pool_refund_assignment_changed": "Refusjon fra sparegris (belønningens tildeling endret): {name}",
"activity.reason_pool_refund_cost_reduced": "Refusjon fra sparegris (kostnad redusert): {name}",
"activity.reason_pool_refund_deleted": "Refusjon fra sparegris (belønning slettet): {name}",
"activity.reason_pool_refund_expired": "Refusjon fra sparegris (belønning utløpt): {name}",
"activity.reason_pool_refund_funding_changed": "Refusjon fra sparegris (belønningens finansiering endret): {name}",
"activity.reason_pool_refund_sold_out": "Refusjon fra sparegris (belønning utsolgt): {name}",
"activity.reason_streak_milestone": "Milepælsbonus ({days} dagers strek!)",
"activity.reason_weekend_bonus": "Helgebonus (×{multiplier})",
@@ -158,6 +161,7 @@
"badges.entity_not_found": "{entity} ikke funnet",
"badges.label": "Merker",
"badges.next_up": "Neste",
"badges.one_more": "Bare én til, så er det ditt!",
"badges.title_with_name": "{name}s merker",
"bonuses.add_bonus": "Legg til bonus",
"bonuses.applying_to": "Brukes på {childName} — nåværende saldo: {points} {pointsName}",
@@ -229,6 +233,8 @@
"child.extra_submit": "Send",
"child.extra_needs_desc": "Skriv først hva du gjorde.",
"child.open_ended_tag": "Beskriv",
"child.weekly_target_done": "Ferdig for denne uka",
"child.weekly_target_progress": "{done}/{target} denne uka",
"child.photo_take": "Ta et bilde",
"child.photo_hint": "Vis den ferdige oppgaven",
"child.photo_retake": "Ta på nytt",
@@ -616,6 +622,9 @@
"panel.chore_daily_cap_label": "Daglig tak (minutter, 0 = ubegrenset)",
"panel.chore_daily_limit_label": "Daglig grense",
"panel.chore_days_label": "Ukedager (la stå tomt = hver dag)",
"panel.chore_weekly_target_pill": "{count}×/uke",
"panel.chore_weekly_target_hint": "Hvor mange ganger i uka oppgaven må gjøres, på hvilke som helst av de valgte dagene. Den vises ikke lenger når antallet er nådd, og nullstilles på mandag.",
"panel.chore_weekly_target_label": "Ukesmål (0 = av)",
"panel.chore_description_label": "Beskrivelse (valgfritt)",
"panel.chore_enabled_label": "Aktivert",
"panel.chore_first_occurrence_label": "Første forekomst",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Grafitt",
"panel.settings_card_design_label": "Standard kortdesign",
"panel.settings_card_design_hint": "Stilen nye kort brukar med mindre den blir overstyrt per kort.",
"activity.a_chore": "ei oppgåve",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bonus: {name}",
"activity.reason_penalty": "Straff: {name}",
"activity.reason_perfect_week": "Perfekt veke-bonus!",
"activity.reason_pool_refund_assignment_changed": "Refusjon frå sparegris (løninga si tildeling endra): {name}",
"activity.reason_pool_refund_cost_reduced": "Refusjon frå sparegris (kostnad redusert): {name}",
"activity.reason_pool_refund_deleted": "Refusjon frå sparegris (løning sletta): {name}",
"activity.reason_pool_refund_expired": "Refusjon frå sparegris (løning utløpt): {name}",
"activity.reason_pool_refund_funding_changed": "Refusjon frå sparegris (løninga si finansiering endra): {name}",
"activity.reason_pool_refund_sold_out": "Refusjon frå sparegris (løning utseld): {name}",
"activity.reason_streak_milestone": "Milepælsbonus ({days} dagar på rad!)",
"activity.reason_weekend_bonus": "Helgebonus (×{multiplier})",
@@ -158,6 +161,7 @@
"badges.entity_not_found": "{entity} ikkje funne",
"badges.label": "Merke",
"badges.next_up": "Neste",
"badges.one_more": "Berre éin til, så er det ditt!",
"badges.title_with_name": "Merka til {name}",
"bonuses.add_bonus": "Legg til bonus",
"bonuses.applying_to": "Vert brukt på {childName} — noverande saldo: {points} {pointsName}",
@@ -229,6 +233,8 @@
"child.extra_submit": "Send",
"child.extra_needs_desc": "Skriv først kva du gjorde.",
"child.open_ended_tag": "Beskriv",
"child.weekly_target_done": "Ferdig for denne veka",
"child.weekly_target_progress": "{done}/{target} denne veka",
"child.photo_take": "Ta eit bilete",
"child.photo_hint": "Vis den ferdige oppgåva",
"child.photo_retake": "Ta på nytt",
@@ -616,6 +622,9 @@
"panel.chore_daily_cap_label": "Dagleg tak (minutt, 0 = uavgrensa)",
"panel.chore_daily_limit_label": "Dagleg grense",
"panel.chore_days_label": "Vekedagar (la stå tomt = kvar dag)",
"panel.chore_weekly_target_pill": "{count}×/veke",
"panel.chore_weekly_target_hint": "Kor mange gonger i veka oppgåva må gjerast, på kva som helst av dei valde dagane. Ho blir ikkje vist meir når talet er nådd, og blir nullstilt på måndag.",
"panel.chore_weekly_target_label": "Vekemål (0 = av)",
"panel.chore_description_label": "Skildring (valfri)",
"panel.chore_enabled_label": "Aktivert",
"panel.chore_first_occurrence_label": "Fyrste førekomst",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Grafite",
"panel.settings_card_design_label": "Design de card padrão",
"panel.settings_card_design_hint": "O estilo que novos cards usam, a menos que substituído por card.",
"activity.a_chore": "uma tarefa",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bônus: {name}",
"activity.reason_penalty": "Penalidade: {name}",
"activity.reason_perfect_week": "Bônus de semana perfeita!",
"activity.reason_pool_refund_assignment_changed": "Reembolso do cofrinho (atribuição da recompensa alterada): {name}",
"activity.reason_pool_refund_cost_reduced": "Reembolso do cofrinho (custo reduzido): {name}",
"activity.reason_pool_refund_deleted": "Reembolso do cofrinho (recompensa excluída): {name}",
"activity.reason_pool_refund_expired": "Reembolso do cofrinho (recompensa expirada): {name}",
"activity.reason_pool_refund_funding_changed": "Reembolso do cofrinho (financiamento da recompensa alterado): {name}",
"activity.reason_pool_refund_sold_out": "Reembolso do cofrinho (recompensa esgotada): {name}",
"activity.reason_streak_milestone": "Bônus de marco de sequência ({days} dias seguidos!)",
"activity.reason_weekend_bonus": "Bônus de fim de semana (×{multiplier})",
@@ -158,6 +161,7 @@
"badges.entity_not_found": "{entity} não encontrado",
"badges.label": "Conquistas",
"badges.next_up": "A seguir",
"badges.one_more": "Só mais um e é seu!",
"badges.title_with_name": "Conquistas de {name}",
"bonuses.add_bonus": "Adicionar Bónus",
"bonuses.applying_to": "A aplicar a {childName} — saldo atual: {points} {pointsName}",
@@ -229,6 +233,8 @@
"child.extra_submit": "Enviar",
"child.extra_needs_desc": "Escreva primeiro o que você fez.",
"child.open_ended_tag": "Descrever",
"child.weekly_target_done": "Concluída esta semana",
"child.weekly_target_progress": "{done}/{target} esta semana",
"child.photo_take": "Tirar uma foto",
"child.photo_hint": "Mostre sua tarefa concluída",
"child.photo_retake": "Repetir",
@@ -616,6 +622,9 @@
"panel.chore_daily_cap_label": "Limite diário (minutos, 0 = ilimitado)",
"panel.chore_daily_limit_label": "Limite diário",
"panel.chore_days_label": "Dias da semana (vazio = todos os dias)",
"panel.chore_weekly_target_pill": "{count}×/semana",
"panel.chore_weekly_target_hint": "Quantas vezes por semana a tarefa precisa ser feita, em qualquer um dos dias escolhidos. Ela deixa de aparecer quando a contagem é atingida e reinicia na segunda-feira.",
"panel.chore_weekly_target_label": "Meta semanal (0 = desligado)",
"panel.chore_description_label": "Descrição (opcional)",
"panel.chore_enabled_label": "Ativada",
"panel.chore_first_occurrence_label": "Primeira ocorrência",
@@ -6,6 +6,7 @@
"common.design.playroom": "Playroom",
"common.design.console": "Console",
"common.design.cleanpro": "Clean Pro",
"common.design.graphite": "Grafite",
"panel.settings_card_design_label": "Design de cartão predefinido",
"panel.settings_card_design_hint": "O estilo que os novos cartões usam, salvo substituição por cartão.",
"activity.a_chore": "uma tarefa",
@@ -55,9 +56,11 @@
"activity.reason_bonus": "Bónus: {name}",
"activity.reason_penalty": "Penalidade: {name}",
"activity.reason_perfect_week": "Bónus de semana perfeita!",
"activity.reason_pool_refund_assignment_changed": "Reembolso do mealheiro (atribuição da recompensa alterada): {name}",
"activity.reason_pool_refund_cost_reduced": "Reembolso do mealheiro (custo reduzido): {name}",
"activity.reason_pool_refund_deleted": "Reembolso do mealheiro (recompensa eliminada): {name}",
"activity.reason_pool_refund_expired": "Reembolso do mealheiro (recompensa expirada): {name}",
"activity.reason_pool_refund_funding_changed": "Reembolso do mealheiro (financiamento da recompensa alterado): {name}",
"activity.reason_pool_refund_sold_out": "Reembolso do mealheiro (recompensa esgotada): {name}",
"activity.reason_streak_milestone": "Bónus de marco de sequência ({days} dias seguidos!)",
"activity.reason_weekend_bonus": "Bónus de fim de semana (×{multiplier})",
@@ -158,6 +161,7 @@
"badges.entity_not_found": "{entity} não encontrado",
"badges.label": "Conquistas",
"badges.next_up": "A seguir",
"badges.one_more": "Só mais um e é teu!",
"badges.title_with_name": "Conquistas de {name}",
"bonuses.add_bonus": "Adicionar Bónus",
"bonuses.applying_to": "A aplicar a {childName} — saldo atual: {points} {pointsName}",
@@ -229,6 +233,8 @@
"child.extra_submit": "Enviar",
"child.extra_needs_desc": "Escreve primeiro o que fizeste.",
"child.open_ended_tag": "Descrever",
"child.weekly_target_done": "Concluída esta semana",
"child.weekly_target_progress": "{done}/{target} esta semana",
"child.photo_take": "Tirar uma foto",
"child.photo_hint": "Mostra a tua tarefa terminada",
"child.photo_retake": "Repetir",
@@ -621,6 +627,9 @@
"panel.chore_daily_cap_label": "Limite diário (minutos, 0 = ilimitado)",
"panel.chore_daily_limit_label": "Limite diário",
"panel.chore_days_label": "Dias da semana (deixe vazio = todos os dias)",
"panel.chore_weekly_target_pill": "{count}×/semana",
"panel.chore_weekly_target_hint": "Quantas vezes por semana a tarefa tem de ser feita, em qualquer um dos dias escolhidos. Deixa de ser apresentada quando a contagem é atingida e reinicia à segunda-feira.",
"panel.chore_weekly_target_label": "Meta semanal (0 = desligado)",
"panel.chore_description_label": "Descrição (opcional)",
"panel.chore_enabled_label": "Ativada",
"panel.chore_first_occurrence_label": "Primeira ocorrência",
@@ -73,6 +73,8 @@ class TaskMateActivityCard extends LitElement {
['Pool refund (reward sold out):', 'activity.reason_pool_refund_sold_out'],
['Pool refund (reward cost reduced):', 'activity.reason_pool_refund_cost_reduced'],
['Pool refund (reward deleted):', 'activity.reason_pool_refund_deleted'],
['Pool refund (reward funding changed):', 'activity.reason_pool_refund_funding_changed'],
['Pool refund (reward assignment changed):', 'activity.reason_pool_refund_assignment_changed'],
['Penalty:', 'activity.reason_penalty'],
['Bonus:', 'activity.reason_bonus'],
];
@@ -603,7 +603,9 @@ class TaskMateApprovalsCard extends LitElement {
.ap-cn-photo img { width: 100%; height: 100%; object-fit: cover; }
/* Clean Pro — divided list */
.ap-cp-item { padding: 10px 0; }
.ap-cp-item { padding: 10px 0; flex-wrap: wrap; row-gap: 8px; }
.ap-cp-mid { flex: 1 1 130px; min-width: 130px; }
.ap-cp-actions { display: flex; gap: 8px; flex: 0 0 auto; margin-left: auto; }
.ap-cp-title { font-weight: 600; }
.ap-cp-sub { font-size: 12px; }
.ap-cp-photo { width: 32px; height: 32px; border-radius: 8px; overflow: hidden;
@@ -1048,13 +1050,13 @@ class TaskMateApprovalsCard extends LitElement {
return html`
<div class="row ap-cp-item" style="--ac:${tone}">
${this._av(it.childName, this._childAvatar(it.childId), tone, 38)}
<div style="flex:1;min-width:0">
<div class="ap-cp-mid">
<div class="ap-cp-title">${it.title}</div>
<div class="muted ap-cp-sub">${it.childName}${time ? html` · ${time}` : ""}</div>
</div>
${this._apPhotoDesigned(it, "ap-cp-photo")}
<span class="chip soft ap-gold-soft">+${it.points}</span>
${this._designActions(it, "ghost")}
<div class="ap-cp-actions">${this._designActions(it, "ghost")}</div>
</div>`;
}
@@ -1716,6 +1716,12 @@ class TaskMateChildCard extends LitElement {
border-radius: 3px;
transition: width 0.4s ease;
}
.next-badge-nudge {
margin-top: 5px;
font-size: 11px;
font-weight: 700;
color: var(--primary-color);
}
/* ══════════════════════════════════════════════════════════════════
DESIGNED STYLES (playroom / console / cleanpro)
@@ -1886,6 +1892,9 @@ class TaskMateChildCard extends LitElement {
.tmd-next-badge .bar i {
display: block; height: 100%; border-radius: 999px; transition: width .4s ease;
}
.tmd-next-badge .nudge {
margin-top: 5px; font-size: 11px; font-weight: 800; color: var(--tmd-accent);
}
/* Playroom is chunkier, console squares everything off, accessible needs
a solid ring rather than a dashed one to stay legible. */
:host([data-tm-design="playroom"]) .tmd-next-badge .bar { height: 8px; }
@@ -1895,6 +1904,7 @@ class TaskMateChildCard extends LitElement {
:host([data-tm-design="accessible"]) .tmd-next-badge .bar { height: 10px; }
:host([data-tm-design="accessible"]) .tmd-next-badge .nm,
:host([data-tm-design="accessible"]) .tmd-next-badge .cnt { font-size: 14px; }
:host([data-tm-design="accessible"]) .tmd-next-badge .nudge { font-size: 13px; }
/* Designed: vacation banner + swappable section */
.tmd-vacation {
@@ -1928,6 +1938,7 @@ class TaskMateChildCard extends LitElement {
show_due_days_only: true, // Whether to apply due_days filtering at all
show_badges: true, // Show badge strip between points and chores
show_next_badge: true, // Show progress toward the closest unearned badge
show_badge_nudge: true, // Encouraging line when one away from that badge
header_color: '#9b59b6',
...config,
};
@@ -2131,6 +2142,9 @@ class TaskMateChildCard extends LitElement {
aria-valuenow="${nextBadge.pct}" aria-valuemin="0" aria-valuemax="100">
<i style="width: ${nextBadge.pct}%; background: ${this._tierColor(nextBadge.badge.tier)}"></i>
</div>
${this._badgeNudgeText(nextBadge)
? html`<div class="next-badge-nudge">${this._badgeNudgeText(nextBadge)}</div>`
: ''}
</div>
</div>
` : ''}
@@ -2249,6 +2263,21 @@ class TaskMateChildCard extends LitElement {
return best;
}
/* "Nearly there" (#890) — the line of encouragement under the next-badge bar.
It fires on the last remaining unit and nothing else, measured in the
criterion's own units rather than percent: 90% of a ten-week badge is one
week away, 90% of a hundred-point badge is ten points away. Rare on
purpose — a line that shows for half the climb has stopped meaning
anything by the time it matters. Manual-award badges carry no criterion
and so never nudge. */
_badgeNudgeText(nextBadge) {
if (this.config.show_badge_nudge === false) return "";
const c = nextBadge?.badge?.closest_criterion;
if (!c) return "";
return Number(c.target) - Number(c.current) === 1 ? this._t("badges.one_more") : "";
}
// Built-in badge names arrive from the sensor in English; the localised
// name lives under badge.name_<suffix> (same scheme as the panel).
_badgeName(b) {
@@ -2307,6 +2336,23 @@ class TaskMateChildCard extends LitElement {
}
/** Mirror of _renderChoreCard's "completed today" detection, designed branch only. */
// How many times this chore can still be ticked TODAY.
//
// Normally the chore's own daily_limit, clamped to 1 for a first_come race.
// A weekly target (#883) clamps it again: once the week's quota is full there
// is no allowance left, so a chore with daily_limit 5 and 2/2 done reads as
// finished rather than staying tappable — the tap would only be one
// complete_chore refuses (#805). Only applied once the child has ticked it
// today; a quota filled on earlier days leaves the row locked instead, so a
// green "done" row never appears with no completion behind it to undo.
_effectiveDailyLimit(chore, child, countToday, isFirstCome) {
const limit = isFirstCome ? 1 : (chore.daily_limit || 1);
const target = Number(chore.weekly_target) || 0;
if (target <= 0 || countToday <= 0) return limit;
const doneThisWeek = Number((child?.weekly_chore_progress || {})[chore.id] || 0);
return Math.min(limit, countToday + Math.max(0, target - doneThisWeek));
}
_isChoreDone(chore, child, todaysCompletions) {
// async_parent_complete_chore writes child_id="__parent__" to dismiss a
// chore for the WHOLE pool with zero points — it never credits a specific
@@ -2336,7 +2382,7 @@ class TaskMateChildCard extends LitElement {
// the filter and the backend both clamp it to 1 (coord_assignments). Reading
// the raw daily_limit here would leave the winner's own row looking incomplete
// and tappable, and complete_chore would then silently no-op the tap (#805).
const done = count >= (isFirstCome ? 1 : (chore.daily_limit || 1));
const done = count >= this._effectiveDailyLimit(chore, child, count, isFirstCome);
return { done, completions: childCompletionsToday };
}
@@ -2395,8 +2441,11 @@ class TaskMateChildCard extends LitElement {
// dim/show only — hide is filtered upstream same as recurrence). Never true
// for the winner's own row: _isFirstComeLocked is only set on siblings.
const firstComeLocked = !!chore._isFirstComeLocked && !done;
// Weekly quota filled — nothing owed until Monday, so the tap is refused
// the same way a recurrence window refuses one.
const weeklyDone = !!chore._weeklyTargetMet && !done;
const onAct = () => {
if (loading || blocked || recLocked || firstComeLocked) return;
if (loading || blocked || recLocked || firstComeLocked || weeklyDone) return;
if (done) this._handleUndo(chore, child, completions);
else this._handleComplete(chore, child);
};
@@ -2409,10 +2458,11 @@ class TaskMateChildCard extends LitElement {
(blocked && dependencyMode === "dim") ||
(chore._isRecurrenceLocked && recurrenceDoneMode === "dim") ||
(chore._isFirstComeLocked && firstComeClaimedMode === "dim") ||
weeklyDone ||
chore._isLockedPreview === true
);
return {
chore, child, done, loading, onAct, index: i, dimmed, blocked, recLocked, firstComeLocked,
chore, child, done, loading, onAct, index: i, dimmed, blocked, recLocked, firstComeLocked, weeklyDone,
tone: this._designTone(i),
glyph: this._choreGlyph(chore),
points: chore.effective_points ?? chore.points,
@@ -2494,6 +2544,9 @@ class TaskMateChildCard extends LitElement {
aria-valuenow="${nextBadge.pct}" aria-valuemin="0" aria-valuemax="100">
<i style="width:${nextBadge.pct}%;background:${this._tierColor(nextBadge.badge.tier)}"></i>
</div>
${this._badgeNudgeText(nextBadge)
? html`<div class="nudge">${this._badgeNudgeText(nextBadge)}</div>`
: ""}
</div>
</div>` : ""}
${sectionLine}
@@ -2559,8 +2612,13 @@ class TaskMateChildCard extends LitElement {
? this._t("child.claimed_by_another", { name: r.chore._firstComeCompletedByName })
: this._t("child.claimed_by_another_generic"))
: "";
// Weekly target (#883): shown from the first tick so the child can see
// where they are, not only once the quota is full.
const weeklyLabel = r.chore._weeklyTarget > 0
? this._t("child.weekly_target_progress", { done: r.chore._weeklyProgress, target: r.chore._weeklyTarget })
: "";
return html`
${r.mandatory || r.photo || r.openEnded || depNames.length || recLabel || firstComeLabel ? html`
${r.mandatory || r.photo || r.openEnded || depNames.length || recLabel || firstComeLabel || weeklyLabel ? html`
<div class="tmd-meta">
${r.mandatory ? html`<span class="tmd-tag mandatory">⚠ ${this._t("child.mandatory")}</span>` : ""}
${r.photo ? html`<span class="tmd-tag photo">📷 ${this._t("child.photo_needed")}</span>` : ""}
@@ -2568,6 +2626,7 @@ class TaskMateChildCard extends LitElement {
${depNames.length ? html`<span class="tmd-tag">🔒 ${this._t("child.blocked_by_dependency", { chores: depNames.join(", ") })}</span>` : ""}
${recLabel ? html`<span class="tmd-tag">🕒 ${recLabel}</span>` : ""}
${firstComeLabel ? html`<span class="tmd-tag">✅ ${firstComeLabel}</span>` : ""}
${weeklyLabel ? html`<span class="tmd-tag">📅 ${weeklyLabel}</span>` : ""}
</div>` : ""}
${showDesc ? html`<div class="tmd-desc">${r.chore.description}</div>` : ""}`;
}
@@ -2583,7 +2642,7 @@ class TaskMateChildCard extends LitElement {
_designDoneBtn(r, label, cls) {
return html`<button class="btn ${cls || ""}"
?disabled=${r.loading || r.blocked || r.recLocked || r.firstComeLocked}
?disabled=${r.loading || r.blocked || r.recLocked || r.firstComeLocked || r.weeklyDone}
@click=${(e) => { e.stopPropagation(); r.onAct(); }}>${label}</button>`;
}
@@ -2990,6 +3049,20 @@ class TaskMateChildCard extends LitElement {
chore._isRecurrenceLocked = chore._isRecurring && !availableNow && !completedToday;
if (chore._isRecurrenceLocked && recurrenceDoneMode === 'hide') return false;
// Weekly target (#883): the child picks which days, so the chore keeps
// being offered until the week's quota is filled. The count comes from
// the per-child map on the overview sensor — the card only ever sees
// today's completions, so it can't total a week by itself. Excluding
// completedToday keeps the row live for an undo when today's tick was
// the one that filled it, exactly as the recurrence lock does.
chore._weeklyTarget = Number(chore.weekly_target) || 0;
chore._weeklyProgress = chore._weeklyTarget > 0
? Number((child.weekly_chore_progress || {})[chore.id] || 0)
: 0;
chore._weeklyTargetMet = chore._weeklyTarget > 0
&& chore._weeklyProgress >= chore._weeklyTarget
&& !completedToday;
// Mark whether this chore's time period has elapsed (grace-aware).
chore._isTimeElapsed = this._isTimePeriodElapsed(chore);
@@ -3494,7 +3567,7 @@ class TaskMateChildCard extends LitElement {
// Clamped for first_come the same way the filter and the backend clamp it: a
// stored daily_limit > 1 must not make the winner's finished row read as still
// outstanding, or it invites a tap that complete_chore silently swallows (#805).
const dailyLimit = isFirstCome ? 1 : (chore.daily_limit || 1);
const dailyLimit = this._effectiveDailyLimit(chore, child, completionsToday, isFirstCome);
// Check for optimistic completions (chores just completed but not yet confirmed by HA)
const optimisticKey = `${chore.id}_${child.id}`;
@@ -3547,6 +3620,9 @@ class TaskMateChildCard extends LitElement {
const firstComeClaimedMode = this.config.first_come_claimed_mode || 'hide';
const firstComeLocked = !!chore._isFirstComeLocked && !isCompletedForToday;
const notAvailableFirstCome = firstComeLocked && firstComeClaimedMode === 'dim';
// Weekly quota filled (#883). Dimmed and non-interactive: the backend
// would refuse the completion, so a live row would just swallow the tap.
const weeklyDone = !!chore._weeklyTargetMet && !isCompletedForToday;
// Elapsed: only dim incomplete chores — completed ones keep their green "done" style
const elapsedTimeMode = this.config.elapsed_time_mode || 'dim';
const timeElapsed = chore._isTimeElapsed && !isCompletedForToday && elapsedTimeMode === 'dim';
@@ -3570,13 +3646,14 @@ class TaskMateChildCard extends LitElement {
if (depBlocked) return; // Prerequisite chore not approved yet
if (timeElapsed) return; // Time period passed — not interactive
if (firstComeLocked) return; // Claimed by another child today — not interactive
if (weeklyDone) return; // Weekly target already met — nothing owed until Monday
if (isCompletedForToday) {
this._handleUndo(chore, child, childCompletionsToday);
} else {
this._handleComplete(chore, child);
}
};
const isInteractive = !(isLoading || notDueToday || recurrenceLocked || isLockedPreview || depBlocked || timeElapsed || firstComeLocked);
const isInteractive = !(isLoading || notDueToday || recurrenceLocked || isLockedPreview || depBlocked || timeElapsed || firstComeLocked || weeklyDone);
const handleRowKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
@@ -3596,13 +3673,15 @@ class TaskMateChildCard extends LitElement {
? (chore._firstComeCompletedByName
? this._t('child.claimed_by_another', { name: chore._firstComeCompletedByName })
: this._t('child.claimed_by_another_generic'))
: weeklyDone
? this._t('child.weekly_target_done')
: isCompletedForToday
? this._t('child.click_to_undo')
: this._t('child.click_to_complete');
return html`
<div
class="chore-card ${chore.mandatory ? "mandatory" : ""} ${isLoading ? "loading" : ""} ${isCelebrating ? "celebrating" : ""} ${isCompletedForToday ? "completed" : ""} ${notDueToday ? "not-due-today" : ""} ${notAvailableRecurrence ? "recurrence-unavailable" : ""} ${notAvailableFirstCome ? "first-come-unavailable" : ""} ${timeElapsed ? "time-elapsed" : ""} ${isLockedPreview ? "chore-locked" : ""} ${depDimmed ? "dependency-blocked" : ""}"
class="chore-card ${chore.mandatory ? "mandatory" : ""} ${isLoading ? "loading" : ""} ${isCelebrating ? "celebrating" : ""} ${isCompletedForToday ? "completed" : ""} ${notDueToday ? "not-due-today" : ""} ${notAvailableRecurrence || weeklyDone ? "recurrence-unavailable" : ""} ${notAvailableFirstCome ? "first-come-unavailable" : ""} ${timeElapsed ? "time-elapsed" : ""} ${isLockedPreview ? "chore-locked" : ""} ${depDimmed ? "dependency-blocked" : ""}"
role="button"
tabindex="${isInteractive ? '0' : '-1'}"
aria-disabled="${isInteractive ? 'false' : 'true'}"
@@ -3646,6 +3725,7 @@ class TaskMateChildCard extends LitElement {
<ha-icon icon="${pointsIcon}"></ha-icon>
+${chore.effective_points ?? chore.points}
${dailyLimit > 1 ? html`<span style="font-size: 0.8em; opacity: 0.7;">(${completionsToday}/${dailyLimit})</span>` : ''}
${chore._weeklyTarget > 0 ? html`<span style="font-size: 0.8em; opacity: 0.7;">${this._t('child.weekly_target_progress', { done: chore._weeklyProgress, target: chore._weeklyTarget })}</span>` : ''}
</div>
${chore.require_photo && !isCompletedForToday ? html`
<div class="recurrence-label">
@@ -13,11 +13,21 @@
* console — dark gamified HUD
* cleanpro — calm productivity / SaaS
* accessible — high contrast, colour-blind safe, dyslexia-friendly type
* graphite — corporate/system look; reuses the cleanpro LAYOUT (see
* LAYOUT_ALIAS) and restyles it entirely through tokens + kit
*
* Mirrors window.__taskmate_localize: exposed globally, no ES module imports.
*/
(function () {
const IDS = ["classic", "playroom", "console", "cleanpro", "accessible"];
const IDS = ["classic", "playroom", "console", "cleanpro", "accessible", "graphite"];
// A design whose markup is another design's. apply() stamps the real id on
// the host (so the style's CSS applies) but hands cards the layout id, so
// every existing `design === "cleanpro"` branch picks graphite up too. Without
// this a new id falls down each card's final `else`, which is playroom in
// some cards and cleanpro in others — the classic "works on one style,
// invisible on the rest" bug.
const LAYOUT_ALIAS = { graphite: "cleanpro" };
// Fonts must be loaded at the document level (an @import inside a shadow
// stylesheet is honoured, but loading once globally avoids N duplicate fetches).
@@ -56,6 +66,23 @@
--tmd-c1:#4F6BED;--tmd-c2:#0FB5A8;--tmd-c3:#E0A100;--tmd-c4:#7A5AF0;--tmd-c5:#E0567A;--tmd-c6:#2BA84A;
}
:host([data-tm-design="graphite"]),[data-tm-design="graphite"]{
/* Neutrals carry the card; the only saturated element is the header band
(per-card header_color). Actions are near-black rather than a coloured
accent, and colour is reserved for meaning: good / warn / bad. */
--tmd-bg:#EFEFF2;--tmd-surface:#FFFFFF;--tmd-surface-2:#F5F5F7;--tmd-border:#D2D2D7;
--tmd-text:#1D1D1F;--tmd-dim:#6E6E73;
--tmd-accent:#1D1D1F;--tmd-accent2:#6E6E73;--tmd-good:#1D7A4C;--tmd-warn:#B25E00;--tmd-bad:#B3261E;--tmd-gold:#B25E00;
--tmd-radius:18px;--tmd-radius-sm:12px;--tmd-shadow:none;--tmd-hd-text:#fff;
/* The system stack resolves to real SF Pro on Apple hardware; Inter (already
in FONT_IMPORT) is the fallback everywhere else. Figures are kept aligned
with font-variant-numeric rather than a separate monospace face. */
--tmd-font-display:-apple-system,BlinkMacSystemFont,"SF Pro Display","Inter",system-ui,sans-serif;
--tmd-font-body:-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",system-ui,sans-serif;
--tmd-font-mono:-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",system-ui,sans-serif;
--tmd-c1:#1F5F8B;--tmd-c2:#146356;--tmd-c3:#8A4B12;--tmd-c4:#3E4B8B;--tmd-c5:#6B3D6B;--tmd-c6:#2F4858;
}
/* ── Dark-mode variants ──────────────────────────────────────────────────
Cards stamp data-tm-dark on the host when HA is in dark mode. Each design has
a light base (above) and a dark override here, so all three follow HA's
@@ -93,6 +120,16 @@
--tmd-text:#E6EAF1;--tmd-dim:#97A1B0;--tmd-accent:#6E86F5;--tmd-accent2:#1FC7B8;
--tmd-shadow:0 1px 2px rgba(0,0,0,.4),0 8px 18px rgba(0,0,0,.35);
}
:host([data-tm-design="graphite"][data-tm-dark]),[data-tm-design="graphite"][data-tm-dark]{
/* True black behind elevated sheets. --tmd-accent stays a mid neutral so a
card with no header_color still has a legible band with white text; the
button colour is derived from --tmd-text instead, so it inverts properly. */
--tmd-bg:#000000;--tmd-surface:#1C1C1E;--tmd-surface-2:#2C2C2E;--tmd-border:#38383A;
--tmd-text:#F5F5F7;--tmd-dim:#98989F;
--tmd-accent:#3A3A3C;--tmd-accent2:#98989F;--tmd-good:#30A46C;--tmd-warn:#D98324;--tmd-bad:#E5484D;--tmd-gold:#D98324;
--tmd-shadow:none;
--tmd-c1:#2C7CB0;--tmd-c2:#1E8C77;--tmd-c3:#B06A26;--tmd-c4:#5B6AC0;--tmd-c5:#945993;--tmd-c6:#4A6B80;
}
:host([data-tm-design="accessible"][data-tm-dark]),[data-tm-design="accessible"][data-tm-dark]{
/* Pure black would bloom on OLED and is harsh for astigmatism; #0A0A0A with
near-white text still clears AAA. Hues are the Okabe-Ito light variants,
@@ -145,7 +182,142 @@
.stat{background:var(--tmd-surface-2);border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm);padding:10px 11px}
.stat .k{font-size:11px;font-weight:700;color:var(--tmd-dim);text-transform:uppercase;letter-spacing:.04em}
.stat .v{font-family:var(--tmd-font-display);font-weight:800;font-size:20px;margin-top:2px}
.grid{display:grid;gap:10px}`;
.grid{display:grid;gap:10px}
/* ── Graphite ─────────────────────────────────────────────────────────────
Graphite has no render path of its own: it borrows the Clean Pro markup
(LAYOUT_ALIAS) and is defined entirely by the rules below, so all cards
adopt it at once. Selectors are :host([...]) + class, which out-specifies
the '.tmd-check .c-num' pattern the cards use for their own design CSS. */
:host([data-tm-design="graphite"]) .tmd{border:1px solid var(--tmd-border);box-shadow:none}
/* The header band stays full colour — it is the one saturated element. */
:host([data-tm-design="graphite"]) .tmd-hd{padding:13px 15px 12px}
:host([data-tm-design="graphite"]) .tmd-hd .tt{font-weight:600;font-size:17px;letter-spacing:-.42px}
:host([data-tm-design="graphite"]) .tmd-hd .tt small{font-weight:400;font-size:13px;opacity:.78;letter-spacing:-.06px;margin-top:2px}
:host([data-tm-design="graphite"]) .tmd-hd .ic{border-radius:50%;background:rgba(255,255,255,.2)}
:host([data-tm-design="graphite"]) .tmd-hd .pill{background:transparent;padding:0;font-weight:500;font-size:15px;letter-spacing:-.2px;text-transform:none}
:host([data-tm-design="graphite"]) .tmd-hd .cnt{background:rgba(255,255,255,.22);font-weight:500;font-variant-numeric:tabular-nums}
/* Actions are near-black on light and near-white on dark. Deriving them from
--tmd-text (not --tmd-accent) is what makes both directions invert. */
:host([data-tm-design="graphite"]) .btn{background:var(--tmd-text);color:var(--tmd-surface);
font-family:var(--tmd-font-body);font-weight:500;letter-spacing:-.2px;
border-radius:9px;box-shadow:none}
:host([data-tm-design="graphite"]) .btn.ghost{background:transparent;color:var(--tmd-text);border:1px solid var(--tmd-border)}
/* "good" marks the primary confirm (Approve / Dismiss). Graphite draws the
primary action in near-black like every other one; green is reserved for
reporting a completed state, not for a button. */
:host([data-tm-design="graphite"]) .btn.good{background:var(--tmd-text);color:var(--tmd-surface)}
:host([data-tm-design="graphite"]) .btn.bad{background:transparent;color:var(--tmd-bad)}
:host([data-tm-design="graphite"]) .btn.round{border-radius:50%}
/* Figures hold their column as they tick over. */
:host([data-tm-design="graphite"]) .num,
:host([data-tm-design="graphite"]) .big,
:host([data-tm-design="graphite"]) .stat .v{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-.6px}
/* A chip reports a value here; it is not a highlight. */
:host([data-tm-design="graphite"]) .chip{background:transparent;border-color:transparent;color:var(--tmd-dim);
font-weight:500;font-variant-numeric:tabular-nums}
:host([data-tm-design="graphite"]) .chip.soft{color:var(--tmd-dim)}
/* 4px track rather than a fat rounded pill. */
:host([data-tm-design="graphite"]) .bar{height:4px;border:0;border-radius:2px;background:var(--tmd-border)}
:host([data-tm-design="graphite"]) .bar>i{box-shadow:none}
:host([data-tm-design="graphite"]) .av{box-shadow:none}
:host([data-tm-design="graphite"]) .divide{background:var(--tmd-border)}
/* Stat cells are separated by hairlines, not drawn as nested boxes. */
:host([data-tm-design="graphite"]) .stat{background:transparent;border:0;border-radius:0;padding:0 12px}
:host([data-tm-design="graphite"]) .stat .k{text-transform:none;letter-spacing:-.04px;font-weight:400;font-size:12.5px}
/* Grouped inset list: one hairline container, separators starting past the
leading control, and the leading badge reduced to a completion mark. */
:host([data-tm-design="graphite"]) .tmd-checklist{border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm)}
:host([data-tm-design="graphite"]) .tmd-check{position:relative;padding:9px 12px;min-height:46px;box-sizing:border-box}
:host([data-tm-design="graphite"]) .tmd-check+.tmd-check{border-top:0}
:host([data-tm-design="graphite"]) .tmd-check+.tmd-check::before{content:"";position:absolute;top:0;left:46px;right:0;height:1px;background:var(--tmd-border)}
:host([data-tm-design="graphite"]) .tmd-check .c-num{width:22px;height:22px;border-radius:50%;
background:transparent;border:1.5px solid var(--tmd-border);color:transparent;font-size:0}
:host([data-tm-design="graphite"]) .tmd-check.done .c-num{background:var(--tmd-good);border-color:var(--tmd-good);color:#fff;font-size:13px}
:host([data-tm-design="graphite"]) .tmd-check .c-name{font-weight:450;font-size:15px;letter-spacing:-.18px}
/* Dimming already says "done"; the strike-through is redundant noise. */
:host([data-tm-design="graphite"]) .tmd-check.done .c-name{text-decoration:none}
:host([data-tm-design="graphite"]) .tmd-check.done{opacity:.52}
/* Meta reads as quiet text, not as a row of coloured pills. */
:host([data-tm-design="graphite"]) .tmd-tag{background:transparent;border-color:transparent;
color:var(--tmd-dim);font-weight:400;font-size:12.5px;letter-spacing:-.02px;padding:0;text-transform:none}
:host([data-tm-design="graphite"]) .tmd-tag.mandatory{color:var(--tmd-bad)}
:host([data-tm-design="graphite"]) .tmd-tag.photo{color:var(--tmd-dim)}
/* The decorative emoji tile in a card header is the loudest "kids app" tell
left in the designed markup, and it is chrome rather than user data — the
child card puts a real avatar here instead, which is kept. */
:host([data-tm-design="graphite"]) .tmd-hd .ic{display:none}
/* Sentence case throughout: tracked-out uppercase labels are not this style. */
:host([data-tm-design="graphite"]) .lbl,
:host([data-tm-design="graphite"]) .ap-group-label,
:host([data-tm-design="graphite"]) .rw-jackpot-label,
:host([data-tm-design="graphite"]) .tmd-badges .lbl{text-transform:none;letter-spacing:-.04px;font-weight:500;font-size:13px;color:var(--tmd-dim)}
/* Badge strip + next-badge: outlines rather than filled colour discs. */
:host([data-tm-design="graphite"]) .tmd-badges,
:host([data-tm-design="graphite"]) .tmd-next-badge{background:transparent;border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm)}
:host([data-tm-design="graphite"]) .tmd-badge-mini{background:transparent;border:1px solid var(--tmd-border);box-shadow:none}
/* The glyphs inside these discs are drawn white for a filled tier colour;
once the fill is gone they have to take the text colour or they vanish. */
:host([data-tm-design="graphite"]) .tmd-badge-mini,
:host([data-tm-design="graphite"]) .tmd-badge-mini ha-icon,
:host([data-tm-design="graphite"]) .tmd-next-badge .ic,
:host([data-tm-design="graphite"]) .tmd-next-badge .ic ha-icon{color:var(--tmd-text)}
:host([data-tm-design="graphite"]) .tmd-next-badge .ic{background:transparent;box-shadow:none}
:host([data-tm-design="graphite"]) .tmd-next-badge .bar{height:4px;border:0;background:var(--tmd-border)}
:host([data-tm-design="graphite"]) .tmd-next-badge .nm{font-weight:500}
/* Points display: tiles become a plain figure block. */
:host([data-tm-design="graphite"]) .cp-tile{background:transparent;border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm);box-shadow:none}
:host([data-tm-design="graphite"]) .cp-pts,
:host([data-tm-design="graphite"]) .cp-total{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-1.2px}
:host([data-tm-design="graphite"]) .cp-name{font-weight:500;letter-spacing:-.18px}
:host([data-tm-design="graphite"]) .cp-rank,
:host([data-tm-design="graphite"]) .cp-chips .chip{background:transparent;border-color:transparent;color:var(--tmd-dim);font-weight:500}
/* Rewards: a grouped list of fixed costs, not a deck of tinted cards. */
:host([data-tm-design="graphite"]) .rw-list{border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm);gap:0}
:host([data-tm-design="graphite"]) .rw-card{background:transparent;border:0;border-radius:0;box-shadow:none}
:host([data-tm-design="graphite"]) .rw-card+.rw-card{border-top:1px solid var(--tmd-border)}
:host([data-tm-design="graphite"]) .rw-name{font-weight:500;letter-spacing:-.18px}
:host([data-tm-design="graphite"]) .rw-cost{font-variant-numeric:tabular-nums;font-weight:500;color:var(--tmd-dim)}
:host([data-tm-design="graphite"]) .rw-emoji{font-size:0}
:host([data-tm-design="graphite"]) .rw-pool-bar{height:4px;border:0;background:var(--tmd-border)}
:host([data-tm-design="graphite"]) .rw-jackpot{background:transparent;border:1px solid var(--tmd-border)}
/* Approvals: one grouped queue; the destructive action is text, not a slab. */
:host([data-tm-design="graphite"]) .ap-cp-item{background:transparent;border:0;border-radius:0;box-shadow:none}
:host([data-tm-design="graphite"]) .ap-cp-item+.ap-cp-item{border-top:1px solid var(--tmd-border)}
:host([data-tm-design="graphite"]) .ap-cp-title{font-weight:500;letter-spacing:-.18px}
:host([data-tm-design="graphite"]) .ap-cp-sub{color:var(--tmd-dim);font-weight:400}
:host([data-tm-design="graphite"]) .ap-cp-photo{border-radius:8px;border:1px solid var(--tmd-border)}
:host([data-tm-design="graphite"]) .ap-gold-soft{background:transparent;color:var(--tmd-dim)}
:host([data-tm-design="graphite"]) .approve-all-btn{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.45);font-weight:500}
/* Leaderboard: rank, name, figure — right-aligned and tabular. */
:host([data-tm-design="graphite"]) .lb-cp-row{background:transparent;border:0;border-radius:0}
:host([data-tm-design="graphite"]) .lb-cp-row+.lb-cp-row{border-top:1px solid var(--tmd-border)}
:host([data-tm-design="graphite"]) .lb-cp-rank{background:transparent;border:0;color:var(--tmd-dim);font-variant-numeric:tabular-nums;font-weight:500}
:host([data-tm-design="graphite"]) .lb-cp-name{font-weight:500;letter-spacing:-.18px}
:host([data-tm-design="graphite"]) .lb-cp-score{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-.6px}
:host([data-tm-design="graphite"]) .lb-cp-unit{color:var(--tmd-dim);font-weight:400}
:host([data-tm-design="graphite"]) .lb-cp-row.win{background:transparent}
:host([data-tm-design="graphite"]) .lb-cp-meta ha-icon{--mdc-icon-size:14px;vertical-align:-2px;margin-right:3px;color:var(--tmd-dim)}
/* Overview: hairline-separated stats, then a grouped per-child list. */
:host([data-tm-design="graphite"]) .ov-kids{border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm)}
:host([data-tm-design="graphite"]) .ov-kid{background:transparent;border:0;border-radius:0}
:host([data-tm-design="graphite"]) .ov-kid+.ov-kid{border-top:1px solid var(--tmd-border)}
:host([data-tm-design="graphite"]) .ov-kid-flag{background:transparent;border:0;color:var(--tmd-dim);font-weight:400}
:host([data-tm-design="graphite"]) .ov-alert{background:transparent;border:1px solid var(--tmd-border);color:var(--tmd-text)}
:host([data-tm-design="graphite"]) .ov-today .val,
:host([data-tm-design="graphite"]) .pts{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-.6px}
:host([data-tm-design="graphite"]) .nm,
:host([data-tm-design="graphite"]) .ttl{font-weight:500;letter-spacing:-.18px}`;
if (!document.getElementById("taskmate-design-fonts")) {
const styleEl = document.createElement("style");
@@ -249,7 +421,14 @@
el.setAttribute("data-tm-design", design);
el.toggleAttribute("data-tm-dark", design !== "classic" && isDark(hass, el));
}
return design;
// The host keeps the real id (its CSS keys off it); the card gets the
// layout id so aliased styles reuse an existing render path.
return LAYOUT_ALIAS[design] || design;
}
/** The real design id stamped on a card host (not the layout alias). */
function active(el) {
return (el && el.getAttribute("data-tm-design")) || "classic";
}
/** ha-form select options for a per-card design override (includes "use global"). */
@@ -262,6 +441,7 @@
{ value: "console", label: label("console", "Console") },
{ value: "cleanpro", label: label("cleanpro", "Clean Pro") },
{ value: "accessible", label: label("accessible", "Accessible") },
{ value: "graphite", label: label("graphite", "Graphite") },
];
}
@@ -307,7 +487,7 @@
`;
}
window.__taskmate_design = { IDS, resolve, isDark, apply, editorOptions, styles, cssText, tokensCSS: TOKENS, colourPicker };
window.__taskmate_design = { IDS, resolve, isDark, apply, active, editorOptions, styles, cssText, tokensCSS: TOKENS, colourPicker };
/**
* The single place that decides what a chore looks like (#750).
@@ -659,10 +659,17 @@ class TaskMateLeaderboardCard extends LitElement {
_lbMetaChips(r, ctx, cls) {
const sortBy = ctx.sortBy;
const chips = [];
if (this.config.show_streak !== false && sortBy !== "streak") chips.push(html`<span>🔥 ${r.streak}</span>`);
if (this.config.show_weekly !== false && sortBy !== "weekly") chips.push(html`<span>📅 ${r.weekly}</span>`);
if (sortBy !== "points") chips.push(html`<span>⭐ ${r.points}</span>`);
if (this.config.show_career !== false && sortBy !== "career") chips.push(html`<span>🏆 ${r.career}</span>`);
// Graphite carries no emoji chrome, so the same figures get monoline
// icons there. active() returns the real design id (apply() hands the
// render path the layout alias, which for graphite is "cleanpro").
const mono = window.__taskmate_design?.active?.(this) === "graphite";
const stat = (icon, emoji, value) => (mono
? html`<span><ha-icon icon="${icon}"></ha-icon>${value}</span>`
: html`<span>${emoji} ${value}</span>`);
if (this.config.show_streak !== false && sortBy !== "streak") chips.push(stat("mdi:fire", "🔥", r.streak));
if (this.config.show_weekly !== false && sortBy !== "weekly") chips.push(stat("mdi:calendar-blank-outline", "📅", r.weekly));
if (sortBy !== "points") chips.push(stat("mdi:star-outline", "⭐", r.points));
if (this.config.show_career !== false && sortBy !== "career") chips.push(stat("mdi:trophy-outline", "🏆", r.career));
if (!chips.length) return "";
return html`<div class="row muted ${cls}">${chips}</div>`;
}
@@ -581,6 +581,12 @@ class TaskMateOverviewCard extends LitElement {
const perChild = availability[c.id];
if (perChild && perChild[child.id] === false) return false;
}
// Weekly target (#883): the week's quota is filled, so the chore isn't
// outstanding today. Filtering here rather than at the two call sites
// keeps it out of the progress ring as well — otherwise the ring could
// never reach 100% once a weekly chore was finished for the week.
const weeklyTarget = Number(c.weekly_target) || 0;
if (weeklyTarget > 0 && Number((child.weekly_chore_progress || {})[c.id] || 0) >= weeklyTarget) return false;
return true;
});
}
@@ -345,6 +345,8 @@ class TaskMatePanel extends HTMLElement {
['Pool refund (reward sold out):', 'activity.reason_pool_refund_sold_out'],
['Pool refund (reward cost reduced):', 'activity.reason_pool_refund_cost_reduced'],
['Pool refund (reward deleted):', 'activity.reason_pool_refund_deleted'],
['Pool refund (reward funding changed):', 'activity.reason_pool_refund_funding_changed'],
['Pool refund (reward assignment changed):', 'activity.reason_pool_refund_assignment_changed'],
['Penalty:', 'activity.reason_penalty'],
['Bonus:', 'activity.reason_bonus'],
];
@@ -1417,6 +1419,7 @@ class TaskMatePanel extends HTMLElement {
timed_rate_points: 10, timed_rate_minutes: 5, timed_max_daily_minutes: 0,
assigned_to: [], requires_approval: true,
time_category: "anytime", completion_sound: "coin", daily_limit: 1,
weekly_target: 0,
difficulty: "medium",
claim_allowance_minutes: 0,
schedule_mode: "specific_days",
@@ -1657,6 +1660,7 @@ class TaskMatePanel extends HTMLElement {
completion_sound: d.completion_sound || "coin",
difficulty: d.difficulty || "medium",
daily_limit: Number(d.daily_limit) || 1,
weekly_target: Math.max(0, Number(d.weekly_target) || 0),
schedule_mode: d.schedule_mode || "specific_days",
due_days: d.due_days || [],
recurrence: d.recurrence || "weekly",
@@ -3235,7 +3239,7 @@ class TaskMatePanel extends HTMLElement {
ts: c.approved_at || c.claimed_at, kind: "claim",
child: (childById[c.child_id] || {}).name || "?",
label: `${this._t("panel.activity_claimed_reward", {name: (rewardById[c.reward_id] || {}).name || this._t("panel.activity_deleted_reward")})}`,
points: -((rewardById[c.reward_id] || {}).cost || 0),
points: -(c.approved_cost ?? (rewardById[c.reward_id] || {}).cost ?? 0),
}));
transactions.forEach(t => events.push({
ts: t.created_at, kind: "manual",
@@ -3283,6 +3287,10 @@ class TaskMatePanel extends HTMLElement {
chorePoints = Math.floor(c.timed_duration_seconds / rateSeconds) * (chore.timed_rate_points || 0);
}
}
// What the child was promised when they submitted beats anything
// recalculated here: a chore edited while the work sat in the
// queue, or a speed/roulette bonus that no longer applies.
chorePoints = c.submitted_points ?? chorePoints;
const photoCap = [choreName, (child && child.name) || "", c.completed_at ? new Date(c.completed_at).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""].filter(Boolean).join(" · ");
return `
<div class="tm-approval-item">
@@ -3464,6 +3472,11 @@ class TaskMatePanel extends HTMLElement {
? this._t("panel.common_one_shot")
: ((c.due_days || []).length === 0 ? this._t("panel.common_daily") : (c.due_days || []).map(d => this._labelOf(DAYS, d)).join(" · "));
const schedClass = c.schedule_mode === "recurring" ? "tm-pill-accent" : c.schedule_mode === "one_shot" ? "tm-pill-warn" : "tm-pill-success";
// A weekly quota doesn't replace the day list — the chore is still only
// offered on those days, it just stops once the week's count is filled.
const weeklyPill = Number(c.weekly_target) > 0
? ` <span class="tm-pill tm-pill-accent">${this._t("panel.chore_weekly_target_pill", { count: Number(c.weekly_target) })}</span>`
: "";
const modeBadge = c.assignment_mode && c.assignment_mode !== "everyone"
? `<span class="tm-pill tm-pill-${this._esc(c.assignment_mode)}">${this._t(`panel.assign_${c.assignment_mode}_short`)}</span>` : "";
const nameCell = renaming
@@ -3485,7 +3498,7 @@ class TaskMatePanel extends HTMLElement {
<td><span class="tm-pill">${this._esc(this._timeCategoryLabel(c.time_category))}</span></td>
<td>${assignedNames} ${modeBadge}</td>
<td>${currentName}</td>
<td><span class="tm-pill ${schedClass} tm-pill-dot">${this._esc(schedLabel)}</span></td>
<td><span class="tm-pill ${schedClass} tm-pill-dot">${this._esc(schedLabel)}</span>${weeklyPill}</td>
<td>${c.requires_approval ? `<span class='tm-yes'>${this._t("panel.common_yes")}</span>` : `<span class='tm-no'>${this._t("panel.common_no")}</span>`}</td>
</tr>
`;
@@ -4539,7 +4552,7 @@ class TaskMatePanel extends HTMLElement {
<div class="tm-setting-row">
<div class="tm-setting-label">${this._t("panel.settings_card_design_label")}<small>${this._t("panel.settings_card_design_hint")}</small></div>
<select class="tm-select" data-setting="card_design">
${["classic","playroom","console","cleanpro"].map(v => `<option value="${v}" ${v === (s.card_design || "classic") ? "selected" : ""}>${this._esc(this._t("common.design." + v))}</option>`).join("")}
${["classic","playroom","console","cleanpro","accessible","graphite"].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">
@@ -5433,6 +5446,8 @@ class TaskMatePanel extends HTMLElement {
`).join("")}
</div>
</div>
${this._field(this._t("panel.chore_weekly_target_label"), "weekly_target", d.weekly_target || 0, "number",
this._t("panel.chore_weekly_target_hint"))}
` : "",
showRecurring ? `
<div class="tm-field-row">
@@ -1042,6 +1042,10 @@ class TaskMateParentDashboardCard extends LitElement {
if (c.schedule_mode === 'recurring') {
if (perChild && perChild[child.id] === false) return false;
}
// Weekly target (#883): quota filled for the week, so it is neither
// outstanding nor part of today's total.
const weeklyTarget = Number(c.weekly_target) || 0;
if (weeklyTarget > 0 && Number((child.weekly_chore_progress || {})[c.id] || 0) >= weeklyTarget) return false;
return true;
});
const childChoreIds = new Set(childChores.map(c => c.id));