/** * TaskMate shared design layer. * * Injects design-token CSS (custom properties) keyed by [data-tm-design], * and exposes helpers for cards to resolve the active design + build the * editor dropdown. CSS custom properties inherit across shadow-DOM * boundaries, so a card only needs to set data-tm-design on its host and * consume var(--tmd-*) inside its shadow styles. * * Designs: * classic — current look; no tokens (cards fall back to HA theme vars) * playroom — warm, rounded, picture-book * 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", "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). const FONT_IMPORT = "@import url('https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700;800&family=Nunito:wght@400;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700;800&family=Manrope:wght@500;600;700;800&family=Atkinson+Hyperlegible:wght@400;700&display=swap');"; // Design tokens. IMPORTANT: these are :host-scoped so they can be applied // INSIDE each card's shadow root. A document-level [data-tm-design] rule // cannot reach a card host nested in HA's shadow trees, but :host() matches // the host carrying the attribute wherever it lives. Custom properties then // inherit down into the card's shadow DOM. Tokens mirror // docs/design/redesigns/taskmate-redesigns.html (.dir-a / .dir-b / .dir-c). const TOKENS = ` :host([data-tm-design="playroom"]),[data-tm-design="playroom"]{ --tmd-bg:#FFF7EC;--tmd-surface:#FFFFFF;--tmd-surface-2:#FFF0DB;--tmd-border:#F0DFC4; --tmd-text:#3A2E26;--tmd-dim:#9C8676; --tmd-accent:#7C5CE6;--tmd-accent2:#FFC23C;--tmd-good:#36C58E;--tmd-warn:#FFB020;--tmd-bad:#FF6B6B;--tmd-gold:#FFC23C; --tmd-radius:22px;--tmd-radius-sm:14px;--tmd-shadow:0 10px 24px rgba(123,92,230,.12);--tmd-hd-text:#fff; --tmd-font-display:"Baloo 2",cursive;--tmd-font-body:"Nunito",sans-serif;--tmd-font-mono:"Baloo 2",cursive; --tmd-c1:#FF6B6B;--tmd-c2:#4ECDC4;--tmd-c3:#FFB020;--tmd-c4:#6C8DFF;--tmd-c5:#C77DFF;--tmd-c6:#45D483; } :host([data-tm-design="console"]),[data-tm-design="console"]{ --tmd-bg:#EEF2F8;--tmd-surface:#FFFFFF;--tmd-surface-2:#E9EEF5;--tmd-border:#D3DCE7; --tmd-text:#0F1B2D;--tmd-dim:#5C6B80; --tmd-accent:#0E9BC4;--tmd-accent2:#E0357F;--tmd-good:#1AA06A;--tmd-warn:#C98800;--tmd-bad:#E0476A;--tmd-gold:#C98800; --tmd-radius:12px;--tmd-radius-sm:8px;--tmd-shadow:0 6px 18px rgba(18,32,52,.12);--tmd-hd-text:#fff; --tmd-font-display:"Space Grotesk",sans-serif;--tmd-font-body:"Inter",sans-serif;--tmd-font-mono:"JetBrains Mono",monospace; --tmd-c1:#0E9BC4;--tmd-c2:#E0357F;--tmd-c3:#C98800;--tmd-c4:#6A4BE0;--tmd-c5:#1AA06A;--tmd-c6:#E0703D; } :host([data-tm-design="cleanpro"]),[data-tm-design="cleanpro"]{ --tmd-bg:#F6F7F9;--tmd-surface:#FFFFFF;--tmd-surface-2:#F1F3F6;--tmd-border:#E5E8EC; --tmd-text:#1A2230;--tmd-dim:#6B7585; --tmd-accent:#4F6BED;--tmd-accent2:#0FB5A8;--tmd-good:#16A36B;--tmd-warn:#E0A100;--tmd-bad:#DC4C4C;--tmd-gold:#E0A100; --tmd-radius:14px;--tmd-radius-sm:9px;--tmd-shadow:0 1px 2px rgba(16,24,40,.06),0 8px 18px rgba(16,24,40,.06);--tmd-hd-text:#fff; --tmd-font-display:"Manrope",sans-serif;--tmd-font-body:"Inter",sans-serif;--tmd-font-mono:"Inter",sans-serif; --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 light/dark setting. */ :host([data-tm-design="accessible"]),[data-tm-design="accessible"]{ /* Colours are the Okabe-Ito colour-blind-safe palette: distinguishable under protanopia, deuteranopia and tritanopia. Text is near-black on white (~19:1, comfortably past WCAG AAA) and borders are heavy enough to carry meaning without relying on hue. Atkinson Hyperlegible is drawn for low vision — its letterforms stay distinct where similar glyphs (I/l/1, O/0) usually collapse. */ --tmd-bg:#FFFFFF;--tmd-surface:#FFFFFF;--tmd-surface-2:#F2F2F2;--tmd-border:#111111; --tmd-text:#111111;--tmd-dim:#3D3D3D; --tmd-accent:#0072B2;--tmd-accent2:#D55E00;--tmd-good:#009E73;--tmd-warn:#E69F00;--tmd-bad:#D55E00;--tmd-gold:#E69F00; --tmd-radius:10px;--tmd-radius-sm:6px;--tmd-shadow:0 0 0 2px #111111;--tmd-hd-text:#fff; --tmd-font-display:"Atkinson Hyperlegible",sans-serif; --tmd-font-body:"Atkinson Hyperlegible",sans-serif; --tmd-font-mono:"Atkinson Hyperlegible",monospace; --tmd-c1:#0072B2;--tmd-c2:#D55E00;--tmd-c3:#009E73;--tmd-c4:#CC79A7;--tmd-c5:#56B4E9;--tmd-c6:#E69F00; } :host([data-tm-design="console"][data-tm-dark]),[data-tm-design="console"][data-tm-dark]{ --tmd-bg:#0E1320;--tmd-surface:#161D2E;--tmd-surface-2:#1E2740;--tmd-border:#2A3550; --tmd-text:#EAF0FF;--tmd-dim:#8A97B8; --tmd-accent:#3DDBFF;--tmd-accent2:#FF4D9D;--tmd-good:#4BE08B;--tmd-warn:#FFCB45;--tmd-bad:#FF5A7A;--tmd-gold:#FFCB45; --tmd-shadow:0 10px 28px rgba(0,0,0,.5); --tmd-c1:#3DDBFF;--tmd-c2:#FF4D9D;--tmd-c3:#FFCB45;--tmd-c4:#7C5CFF;--tmd-c5:#4BE08B;--tmd-c6:#FF8A3D; } :host([data-tm-design="playroom"][data-tm-dark]),[data-tm-design="playroom"][data-tm-dark]{ --tmd-bg:#241A2B;--tmd-surface:#2C2235;--tmd-surface-2:#382B43;--tmd-border:#473754; --tmd-text:#F6ECEF;--tmd-dim:#B6A3B4;--tmd-accent:#A88BFF; --tmd-shadow:0 10px 24px rgba(0,0,0,.45); } :host([data-tm-design="cleanpro"][data-tm-dark]),[data-tm-design="cleanpro"][data-tm-dark]{ --tmd-bg:#0F141A;--tmd-surface:#181D25;--tmd-surface-2:#212733;--tmd-border:#2C3340; --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, which stay distinguishable against a dark ground. */ --tmd-bg:#0A0A0A;--tmd-surface:#0A0A0A;--tmd-surface-2:#1C1C1C;--tmd-border:#F5F5F5; --tmd-text:#F5F5F5;--tmd-dim:#CFCFCF; --tmd-accent:#56B4E9;--tmd-accent2:#E69F00;--tmd-good:#009E73;--tmd-warn:#F0E442;--tmd-bad:#E69F00;--tmd-gold:#F0E442; --tmd-shadow:0 0 0 2px #F5F5F5; --tmd-c1:#56B4E9;--tmd-c2:#E69F00;--tmd-c3:#009E73;--tmd-c4:#CC79A7;--tmd-c5:#F0E442;--tmd-c6:#0072B2; }`; // Shared component kit, themed by the tokens above. Every designed card // consumes these via __taskmate_design.styles() and adds only its own layout // classes. Headers stay FULL-COLOUR (var(--hd)) in every style per house rule. const KIT = ` .tmd{overflow:hidden;font-family:var(--tmd-font-body);color:var(--tmd-text);background:var(--tmd-surface);border-radius:var(--tmd-radius);box-shadow:var(--tmd-shadow)} .tmd-hd{display:flex;align-items:center;gap:11px;padding:13px 15px;background:var(--hd,var(--tmd-accent));color:#fff} :host([data-tm-design="playroom"]) .tmd-hd{background:linear-gradient(135deg,var(--hd,var(--tmd-accent)),color-mix(in srgb,var(--hd,var(--tmd-accent)) 72%,#fff))} .tmd-hd .ic{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:rgba(255,255,255,.22);font-size:17px;flex:none} .tmd-hd .tt{font-family:var(--tmd-font-display);font-weight:800;font-size:16px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .tmd-hd .tt small{display:block;font-family:var(--tmd-font-body);font-weight:600;font-size:11px;opacity:.85;margin-top:1px} .tmd-hd .pill{margin-left:auto;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;background:rgba(255,255,255,.25);text-transform:capitalize} .tmd-hd .cnt{margin-left:auto;min-width:22px;height:22px;padding:0 6px;border-radius:999px;background:var(--tmd-bad);color:#fff;font-size:12px;font-weight:800;display:grid;place-items:center} .tmd-bd{padding:15px} .tmd-empty{text-align:center;padding:26px 12px;color:var(--tmd-dim);font-size:.9rem} .av{width:var(--av,42px);height:var(--av,42px);border-radius:50%;flex:none;display:grid;place-items:center;font-family:var(--tmd-font-display);font-weight:800;color:#fff;font-size:calc(var(--av,42px)*.4);line-height:1;text-align:center;background:var(--ac,var(--tmd-accent));box-shadow:0 2px 6px rgba(0,0,0,.18);overflow:hidden} :host([data-tm-design="console"]) .av{box-shadow:0 0 0 1px color-mix(in srgb,var(--ac,var(--tmd-accent)) 70%,transparent),0 0 14px color-mix(in srgb,var(--ac,var(--tmd-accent)) 35%,transparent)} .av ha-icon{--mdc-icon-size:calc(var(--av,42px)*.55);color:#fff} .av img{width:100%;height:100%;object-fit:cover;border-radius:50%} .chip{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700;padding:4px 9px;border-radius:999px;background:var(--tmd-surface-2);color:var(--tmd-text);border:1px solid var(--tmd-border)} .chip.soft{background:color-mix(in srgb,var(--tmd-accent) 14%,transparent);border-color:transparent;color:var(--tmd-accent)} .num{font-family:var(--tmd-font-mono);font-weight:800;letter-spacing:-.01em} .big{font-family:var(--tmd-font-display);font-weight:800;line-height:1} .bar{height:10px;border-radius:999px;background:var(--tmd-surface-2);overflow:hidden;border:1px solid var(--tmd-border)} .bar>i{display:block;height:100%;border-radius:999px;background:var(--tmd-accent)} :host([data-tm-design="console"]) .bar>i{background:linear-gradient(90deg,var(--tmd-accent),var(--tmd-accent2));box-shadow:0 0 12px color-mix(in srgb,var(--tmd-accent) 60%,transparent)} .row{display:flex;align-items:center;gap:10px} .muted{color:var(--tmd-dim)} .divide{height:1px;background:var(--tmd-border);margin:12px 0} .lead{color:var(--tmd-accent)} .lead-dot{color:var(--tmd-gold)} .btn{font:inherit;font-family:var(--tmd-font-display);font-weight:800;font-size:13.5px;line-height:1;border:0;cursor:pointer;padding:9px 14px;border-radius:var(--tmd-radius-sm);background:var(--tmd-accent);color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:6px} :host([data-tm-design="console"]) .btn{color:#06101c;background:linear-gradient(135deg,var(--tmd-accent),color-mix(in srgb,var(--tmd-accent) 60%,var(--tmd-accent2)));box-shadow:0 0 14px color-mix(in srgb,var(--tmd-accent) 45%,transparent)} .btn.ghost{background:var(--tmd-surface-2);color:var(--tmd-text);border:1px solid var(--tmd-border)} :host([data-tm-design="console"]) .btn.ghost{box-shadow:none} .btn.good{background:var(--tmd-good);color:#06301f} .btn.bad{background:var(--tmd-bad);color:#3a0d0d} .btn.round{border-radius:50%;width:38px;height:38px;padding:0;justify-content:center;font-size:18px} .btn.sm{padding:6px 10px;font-size:12.5px} .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} /* ── 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"); styleEl.id = "taskmate-design-fonts"; styleEl.textContent = FONT_IMPORT; document.head.appendChild(styleEl); } /** * The design tokens as a Lit CSSResult, for a card to include in its own * `static get styles()` so the :host token rules live inside the card's * shadow root (where they actually apply). Built lazily so the Lit `css` * tag — borrowed from HA's own card base — is resolved after HA has loaded. */ let _tokenStyles = null; function styles() { if (_tokenStyles) return _tokenStyles; const base = customElements.get("hui-masonry-view") || customElements.get("hui-view"); if (!base) return null; const css = Object.getPrototypeOf(base).prototype.css; if (!css) return null; const cssText = TOKENS + "\n" + KIT; const arr = [cssText]; arr.raw = [cssText]; // satisfy css() implementations that read strings.raw try { _tokenStyles = css(arr); } catch (_e) { _tokenStyles = null; } return _tokenStyles; } /** * Global default design. Read the card's own entity first (the overview * sensor exposes card_design at top level); otherwise scan TaskMate sensors * so the default resolves even for cards bound to child-specific entities. */ function _globalDesign(hass, entity) { if (!hass || !hass.states) return "classic"; const own = hass.states[entity] && hass.states[entity].attributes; if (own && IDS.includes(own.card_design)) return own.card_design; for (const eid in hass.states) { if (eid.indexOf("sensor.taskmate") !== 0) continue; const a = hass.states[eid].attributes; if (a && IDS.includes(a.card_design)) return a.card_design; } return "classic"; } /** * Resolve the active design for a card. * Precedence: per-card config.card_design → global default → classic. */ function resolve(hass, config, entity) { const perCard = config && config.card_design; if (perCard && perCard !== "global" && IDS.includes(perCard)) return perCard; return _globalDesign(hass, entity); } // Relative luminance (0..1) of a CSS colour string, or null if unparseable. function _luminance(c) { if (!c) return null; c = c.trim(); let r, g, b; let m = c.match(/^#([0-9a-f]{3})$/i); if (m) { const h = m[1]; r = parseInt(h[0] + h[0], 16); g = parseInt(h[1] + h[1], 16); b = parseInt(h[2] + h[2], 16); } else if ((m = c.match(/^#([0-9a-f]{6})$/i))) { r = parseInt(m[1].slice(0, 2), 16); g = parseInt(m[1].slice(2, 4), 16); b = parseInt(m[1].slice(4, 6), 16); } else if ((m = c.match(/rgba?\(\s*([\d.]+)[ ,]+([\d.]+)[ ,]+([\d.]+)/i))) { r = +m[1]; g = +m[2]; b = +m[3]; } else return null; return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; } /** * Is the card currently in dark mode? Order: * 1. an explicitly-selected dark theme (hass.themes.darkMode) * 2. the ACTUAL rendered background brightness (covers the default theme * following the OS, custom themes, etc.) read from the card's own scope * 3. the OS preference as a last resort. * `el` is the card host; its inherited --card/--primary-background-color * reflects whatever theme is live around it. */ function isDark(hass, el) { if (hass && hass.themes && hass.themes.darkMode) return true; if (el && typeof getComputedStyle === "function") { try { const cs = getComputedStyle(el); const bg = (cs.getPropertyValue("--card-background-color") || cs.getPropertyValue("--primary-background-color") || cs.backgroundColor || "").trim(); const lum = _luminance(bg); if (lum !== null) return lum < 0.5; } catch (_e) { /* ignore */ } } return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); } /** * Resolve the design AND stamp the host element: data-tm-design + (in dark * mode) data-tm-dark. Cards call this once at the top of render() and branch * on the returned id. Returns "classic" when no design applies. */ function apply(el, hass, config, entity) { const design = resolve(hass, config, entity); if (el) { el.setAttribute("data-tm-design", design); el.toggleAttribute("data-tm-dark", design !== "classic" && isDark(hass, el)); } // 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"). */ function editorOptions(t) { const label = (k, fallback) => (t ? t("common.design." + k) : fallback); return [ { value: "global", label: label("use_global", "Use global default") }, { value: "classic", label: label("classic", "Classic") }, { value: "playroom", label: label("playroom", "Playroom") }, { 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") }, ]; } // Raw token + kit CSS for non-Lit consumers (e.g. the admin panel, which // renders to light DOM). Inject into a " + "" + "\"\"" + "
"; document.body.appendChild(overlay); imgEl = overlay.querySelector("img"); capEl = overlay.querySelector(".tm-lb-cap"); closeBtn = overlay.querySelector(".tm-lb-close"); closeBtn.addEventListener("click", close); // Only a click on the dimmed backdrop itself (not image/caption/button) closes. overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); } return function (src, caption, closeLabel) { if (!src) return; if (!overlay) build(); lastFocus = document.activeElement; imgEl.src = src; if (caption) { capEl.textContent = caption; capEl.style.display = ""; } else { capEl.textContent = ""; capEl.style.display = "none"; } closeBtn.setAttribute("aria-label", closeLabel || "Close"); closeBtn.title = closeLabel || "Close"; overlay.style.display = "flex"; void overlay.offsetWidth; // force reflow so the opacity transition runs from 0 overlay.classList.add("open"); document.body.style.overflow = "hidden"; document.addEventListener("keydown", onKey, true); closeBtn.focus(); }; })(); })();