From d9b61ae9906e706a61980af633c19f2562c2a8fe Mon Sep 17 00:00:00 2001 From: Abdulla Date: Mon, 5 Oct 2026 22:00:31 +0400 Subject: [PATCH] Add Dartec Glass, Glass Lite, Soft and Material (v1.3.0) The four remaining themes from the approved drafts (#1), in themes/dartec.yaml beside Dartec and Dartec Graphite, which are unchanged. Light and dark in full, theme variables only. Ported as v1.2.0 ported Dartec and Graphite: the shipped font stack (no Dubai), state-cover-active-color, ink text on the light dark-mode teal, and info, sidebar and code-editor colours set to match. Light warning is #c96e05 in Glass, Glass Lite and Material: the drafts' #d97706 was below 3:1 on their cards. contrast.py now measures gradient grounds and glass cards (worst case over every colour of lovelace-background, with a translucent card blended over it, and the solid dialog surface): 252 of 252 pairs pass WCAG AA, with Dartec's and Graphite's 84 figures unchanged. New check_themes.py, run in CI: HA's themes map shape, no duplicate keys, light and dark with the core colours, valid colour values, no card-mod, url(), JavaScript or copper. README lists the six themes and which goes where; new CHANGELOG. --- .github/workflows/validate.yml | 1 + CHANGELOG.md | 44 +++++ README.md | 21 +- scripts/check_themes.py | 168 ++++++++++++++++ scripts/contrast.py | 96 ++++++++- themes/dartec.yaml | 350 ++++++++++++++++++++++++++++++++- 6 files changed, 667 insertions(+), 13 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 scripts/check_themes.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a139d97..dfb026f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -19,6 +19,7 @@ jobs: with: python-version: "3.12" - run: pip install pyyaml + - run: python scripts/check_themes.py - run: python scripts/contrast.py - name: LICENSE is present run: test -s LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c65f8b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +Each version is a GitHub release with a tag of the same name; HACS installs the release, not the branch. Theme names never change between versions, because homes store their default theme by name. + +## v1.3.0 + +Four new themes, completing the family the owner approved in [#1](https://github.com/kaboomAE/dartec-theme/issues/1). **Dartec** and **Dartec Graphite** are unchanged, so homes that use them see nothing new. + +### New themes + +- **Dartec Glass**: frosted, translucent cards (`ha-card-background` with `ha-card-backdrop-filter`) over a teal gradient ground (`lovelace-background`), with 14 px corners. No photograph and no download: the ground is a gradient. The top bar stays solid. Blur is costly to draw, so Glass is for phones and capable tablets, not wall panels. +- **Dartec Glass Lite**: Glass's colours and ground with solid cards and no blur, for wall panels and slower tablets. +- **Dartec Soft**: borderless paper cards on the brand's two-layer shadow in light mode, a hairline edge instead of a shadow in dark mode, 12 px corners. +- **Dartec Material**: tonal surfaces from a Material 3 "tonal spot" scheme generated from the brand teal #1A6B6B, with the dark ground lifted off near-black; 16 px cards with no border or shadow. + +Each is in `themes/dartec.yaml` beside the other two, sets light and dark in full, uses theme variables only, and names Lateef and IBM Plex Mono as Dartec and Graphite do. + +### Changes from the approved drafts + +- The font stack is the one v1.2.0 ships (`'Dartec Lateef', Roboto, …`). The drafts named Dubai first, whose licence does not allow it to be redistributed. +- Covers use `state-cover-active-color`, which Home Assistant reads, instead of the drafts' `state-cover-open-color`. +- Text on the light dark-mode teal is ink, not white, as in Dartec and Graphite. +- Light-mode warning is `#c96e05` in Glass, Glass Lite and Material, as in Graphite: the drafts' `#d97706` was 2.89:1, 2.99:1 and 2.75:1 on their cards, below the 3:1 for icons. The drafts' contrast table did not measure warnings. +- Info colour, the sidebar's selected item and the dark-mode code editor are set to match each theme. + +### Checks + +- `scripts/contrast.py` now measures views whose ground is a gradient and glass cards: every pair on a card or the page is measured against each colour of the gradient, with a translucent card blended over it, and the worst ratio counts. Dialogs, which use the solid `card-background-color`, are measured too. **252 of 252 pairs pass WCAG AA** across the six themes; Dartec's and Graphite's 84 figures are unchanged. +- New `scripts/check_themes.py`, run in CI: each file loads as Home Assistant's themes map, no key is set twice, every theme writes light and dark with the core colours, every colour value is valid, and nothing uses card-mod, `url()`, JavaScript or copper. + +## v1.2.0 (2026-09-25) + +- **Dartec, corrected:** the accent is brand teal instead of copper; the AC's cool, dry and fan modes, lights that are on and moving covers use brand colours; dark-mode text on teal is ink. Fonts use the `ha-font-family-*` variables Home Assistant reads since 2025.5. +- **New: Dartec Graphite**, the panel and night theme. +- MIT licence for the files (the name "Dartec" is not licensed). Requires Home Assistant 2025.5 or later. +- `scripts/contrast.py` checks WCAG AA on every change: 84 of 84 pairs pass. + +## v1.1.0 (2026-08-26) + +- The theme is renamed from `DarTec` to `Dartec`, per the brand guidelines. Re-apply it after updating: the theme's key changed. + +## v1.0.0 (2026-08-25) + +- First release: teal primary, copper accent, warm neutrals, light and dark modes. diff --git a/README.md b/README.md index 362efe4..7c14f2a 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,14 @@ The official [Dartec Smart Homes](https://dartec.ae) themes for Home Assistant, |---|---| | **Dartec** | The house theme: brand teal on warm cream and paper. Teal for everything interactive, a readable gold for lights that are on. | | **Dartec Graphite** | Quiet warm-graphite surfaces with grey icons, and teal only where something is on. The theme for wall panels, and in dark mode for bedrooms at night. | +| **Dartec Glass** | Frosted, translucent cards over a deep teal gradient. The most distinctive look, and the costliest to draw: for phones and capable tablets, not wall panels. | +| **Dartec Glass Lite** | Glass's colours and gradient with solid cards and no blur, so it draws about as cheaply as an opaque theme. The Glass look for wall panels and slower tablets. | +| **Dartec Soft** | Borderless paper cards that float on a soft shadow in light mode, with a hairline edge in dark mode. | +| **Dartec Material** | Tonal surfaces generated from the brand teal with Material 3's colour method, in rounder 16 px cards with no border or shadow. | - **Light and dark** are both written in full; Home Assistant follows the device's setting. - **Theme variables only.** No card-mod, no JavaScript, no images, nothing downloaded. -- **WCAG AA in both modes.** Text is at least 4.5:1 and icons and controls at least 3:1, checked on every change by [`scripts/contrast.py`](scripts/contrast.py). +- **WCAG AA in both modes.** Text is at least 4.5:1 and icons and controls at least 3:1, checked on every change by [`scripts/contrast.py`](scripts/contrast.py). Where a view's ground is a gradient, or a card is glass, the worst place a card can sit is what is measured. [`scripts/check_themes.py`](scripts/check_themes.py) checks each theme's structure and every colour value. - **No copper.** The Dartec brand reserves copper for content written by AI. ## Screenshots @@ -31,6 +35,17 @@ Home Assistant 2026.9 on Dartec's test bench, with the themes installed through | ![Dartec Graphite, light](docs/screenshots/graphite-overview-light-en.webp) | ![Dartec Graphite, dark](docs/screenshots/graphite-overview-dark-en.webp) | | ![Dartec Graphite, device states, light](docs/screenshots/graphite-states-light.webp) | ![Dartec Graphite, dark, Arabic](docs/screenshots/graphite-overview-dark-ar.webp) | +Screenshots of Glass, Glass Lite, Soft and Material are still to come. + +## Which theme where + +- **A home's default:** Dartec. +- **Wall panels:** Dartec Graphite, or Dartec Glass Lite for the Glass look. Bedroom panels at night: Graphite in dark mode. +- **Dartec Glass** blurs what is behind every card. Drawn without a graphics chip, as a stand-in for a weak tablet, the blur multiplied the drawing work about elevenfold and dropped a third of the frames while scrolling. Keep it to phones and capable tablets until it has been measured on the real panels, and give the panels Glass Lite. +- **Soft and Material** are alternatives a family can pick for themselves. + +A theme picked in a person's own profile overrides the home's default, so a panel's own account can use one theme while the family's phones use another. + ## Install Homes managed by Dartec HA Manager receive these themes automatically. To install them by hand: @@ -41,7 +56,7 @@ Homes managed by Dartec HA Manager receive these themes automatically. To instal frontend: themes: !include_dir_merge_named themes ``` -3. Restart Home Assistant, then pick **Dartec** or **Dartec Graphite** in your profile, or let your Dartec installer set it for the home. +3. Restart Home Assistant, then pick a Dartec theme in your profile, or let your Dartec installer set it for the home. Since Home Assistant 2026.2, a theme chosen in a person's own profile overrides the home's default theme. @@ -49,7 +64,7 @@ Requires Home Assistant 2025.5 or later, the first release that reads the `ha-fo ## Fonts -Both themes name Dartec's Arabic face, Lateef, and IBM Plex Mono for code. A theme can name a font but cannot load one, so these appear only on pages that load them; until then Arabic uses the device's own font. Latin text is Roboto, which Home Assistant ships itself. Dubai, the brand's Latin face, is not used: its licence does not allow it to be redistributed. +Every theme names Dartec's Arabic face, Lateef, and IBM Plex Mono for code. A theme can name a font but cannot load one, so these appear only on pages that load them; until then Arabic uses the device's own font. Latin text is Roboto, which Home Assistant ships itself. Dubai, the brand's Latin face, is not used: its licence does not allow it to be redistributed. ## Licence diff --git a/scripts/check_themes.py b/scripts/check_themes.py new file mode 100644 index 0000000..92bc828 --- /dev/null +++ b/scripts/check_themes.py @@ -0,0 +1,168 @@ +"""Structure checks for every theme file in themes/. + + python scripts/check_themes.py # exit 1 on any problem + +Each file must load as Home Assistant's `frontend: themes:` map: theme names +mapping to string variables, with an optional `modes` holding `light` and +`dark`, each a map of strings (the frontend's THEME_SCHEMA). On top of that, +for these themes: + +- no key appears twice in any map (PyYAML would silently keep the last one); +- every theme writes both light and dark, and each mode sets the core colours; +- every colour value is valid: a hex colour, rgb()/rgba() in range, var(), or a + gradient made of those; +- theme variables only: no card-mod, no JavaScript, no url() (no images or + downloads); +- no copper, which the brand reserves for content written by AI. +""" +import pathlib +import re +import sys + +import yaml + +ROOT = pathlib.Path(__file__).resolve().parent.parent + +# Every mode of every theme sets these itself rather than inheriting them. +CORE = [ + "primary-background-color", "secondary-background-color", "card-background-color", + "primary-text-color", "secondary-text-color", "disabled-text-color", "divider-color", + "primary-color", "accent-color", "text-primary-color", + "state-icon-color", "state-active-color", + "error-color", "warning-color", "success-color", + "sidebar-background-color", +] + +# The copper accents of v1.1.0 (light and dark). +COPPER = {"#a8551f", "#e08b52"} + +HEX = r"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})(?![0-9a-zA-Z])" +NUM = r"\s*(\d+(?:\.\d+)?)\s*" +RGB = rf"rgba?\({NUM},{NUM},{NUM}(?:,{NUM})?\)" +VAR = r"var\(--[a-z0-9_-]+(?:\s*,\s*[^()]+)?\)" +GRADIENT = re.compile(r"(?:repeating-)?(?:linear|radial|conic)-gradient\(") + + +class UniqueKeyLoader(yaml.SafeLoader): + pass + + +def _no_duplicates(loader, node, deep=False): + seen = {} + for key_node, _ in node.value: + key = loader.construct_object(key_node, deep=deep) + if key in seen: + raise yaml.constructor.ConstructorError( + None, None, f"duplicate key {key!r} (first at line {seen[key] + 1})", + key_node.start_mark) + seen[key] = key_node.start_mark.line + return loader.construct_mapping(node, deep) + + +UniqueKeyLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_duplicates) + + +def colour_problems(key, value): + """Problems with the colours in one value; [] if it holds none or all are valid.""" + problems = [] + for m in re.finditer(r"#[0-9a-zA-Z]+", value): + if not re.fullmatch(HEX, m.group()): + problems.append(f"{key}: {m.group()!r} is not a hex colour") + for m in re.finditer(r"rgba?\([^)]*\)", value): + rm = re.fullmatch(RGB, m.group()) + if not rm: + problems.append(f"{key}: {m.group()!r} is not a valid rgb()/rgba()") + continue + r, g, b, a = rm.groups() + if not all(0 <= float(c) <= 255 for c in (r, g, b)): + problems.append(f"{key}: {m.group()!r} has a channel outside 0-255") + if m.group().startswith("rgba(") and a is None: + problems.append(f"{key}: {m.group()!r} has no alpha") + if a is not None and not 0 <= float(a) <= 1: + problems.append(f"{key}: {m.group()!r} has an alpha outside 0-1") + if key.endswith("-color") and not re.fullmatch(rf"{HEX}|{RGB}|{VAR}", value.strip()): + problems.append(f"{key}: {value!r} is not a single colour") + if key.endswith("-background") and not ( + re.fullmatch(rf"{HEX}|{RGB}|{VAR}|none|transparent", value.strip()) + or GRADIENT.match(value.strip())): + problems.append(f"{key}: {value!r} is neither a colour nor a gradient") + if GRADIENT.match(value.strip()) and value.count("(") != value.count(")"): + problems.append(f"{key}: unbalanced parentheses") + return problems + + +def check_vars(where, mapping): + problems = [] + if not isinstance(mapping, dict) or not mapping: + return [f"{where}: must be a non-empty map"] + for key, value in mapping.items(): + if not isinstance(key, str) or not re.fullmatch(r"[a-z0-9][a-z0-9_-]*", key): + problems.append(f"{where}: {key!r} is not a theme variable name") + continue + if not isinstance(value, str): + problems.append(f"{where}: {key} must be a quoted string, got {type(value).__name__}") + continue + low = value.lower() + if "card-mod" in key or "card-mod" in low: + problems.append(f"{where}: {key} uses card-mod") + if "url(" in low or "javascript" in low or "<" in low: + problems.append(f"{where}: {key} loads something ({value!r})") + for hex_ in re.findall(HEX, low): + if hex_ in COPPER: + problems.append(f"{where}: {key} is copper ({hex_})") + problems += [f"{where}: {p}" for p in colour_problems(key, value)] + return problems + + +def check_file(path): + try: + data = yaml.load(path.read_text(encoding="utf-8"), Loader=UniqueKeyLoader) + except yaml.YAMLError as e: + return [f"{path.name}: {e}"], [] + if not isinstance(data, dict) or not data: + return [f"{path.name}: must be a map of theme names"], [] + problems, names = [], [] + for name, theme in data.items(): + names.append(name) + where = f"{path.name}: {name}" + if not isinstance(name, str) or not name.startswith("Dartec"): + problems.append(f"{where}: theme names start with 'Dartec'") + if not isinstance(theme, dict): + problems.append(f"{where}: must be a map") + continue + modes = theme.get("modes") + base = {k: v for k, v in theme.items() if k != "modes"} + problems += check_vars(f"{where}", base) + if not isinstance(modes, dict) or set(modes) != {"light", "dark"}: + problems.append(f"{where}: modes must be exactly light and dark") + continue + for mode in ("light", "dark"): + problems += check_vars(f"{where} ({mode})", modes[mode]) + if isinstance(modes[mode], dict): + missing = [k for k in CORE if k not in modes[mode]] + if missing: + problems.append(f"{where} ({mode}): does not set {', '.join(missing)}") + return problems, names + + +def main(): + files = sorted((ROOT / "themes").glob("*.yaml")) + if not files: + print("no theme files in themes/") + return 1 + problems, names = [], [] + for path in files: + p, n = check_file(path) + problems += p + names += n + if len(names) != len(set(names)): + problems.append("a theme name is used twice across files") + for p in problems: + print(f"FAIL {p}") + print(f"{len(names)} themes in {len(files)} file(s): {', '.join(names)}") + print("All structure checks pass." if not problems else f"{len(problems)} problem(s).") + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/contrast.py b/scripts/contrast.py index fe24a86..f75ef4c 100644 --- a/scripts/contrast.py +++ b/scripts/contrast.py @@ -7,6 +7,20 @@ WCAG AA sets for it: 4.5:1 for text, 3:1 for icons and controls (1.4.11). Variables a theme does not set fall back to Home Assistant's own defaults, following the var() chains in the frontend's color.globals.ts. + +Where a theme draws its cards or its view on more than one colour, every pair +on a card or on the page is measured against each of them and the worst ratio +is reported: + +- the view's ground is every colour along `lovelace-background` (each gradient + stop and ten steps between neighbouring stops) as well as + `primary-background-color`; +- a translucent `ha-card-background` (glass) is blended over each colour of + that ground, and the solid `card-background-color` that dialogs and pop-ups + use is measured as well. + +A blur behind a glass card averages the ground's colours, so the worst single +colour under it is the worst case. """ import pathlib import re @@ -86,7 +100,34 @@ def ratio(fg, bg): return (a + 0.05) / (b + 0.05) -def resolve(values, key, seen=()): +COLOUR = re.compile(r"#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])|rgba?\([^)]*\)") + + +def parse_colour(text): + """'#rgb', '#rrggbb', 'rgb(r, g, b)' or 'rgba(r, g, b, a)' as (r, g, b, a).""" + text = text.strip() + if text.startswith("#"): + h = text[1:] + if len(h) not in (3, 6): + raise ValueError(f"{text!r}: hex colours here are #rgb or #rrggbb") + if len(h) == 3: + h = "".join(c * 2 for c in h) + return (*(int(h[i:i + 2], 16) for i in (0, 2, 4)), 1.0) + m = re.fullmatch(r"rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)", text) + if not m: + raise ValueError(f"{text!r} is not a colour this script reads") + r, g, b = (float(m.group(i)) for i in (1, 2, 3)) + a = float(m.group(4)) if m.group(4) is not None else 1.0 + if not all(0 <= c <= 255 for c in (r, g, b)) or not 0 <= a <= 1: + raise ValueError(f"{text!r} is out of range") + return r, g, b, a + + +def to_hex(rgb): + return "#" + "".join(f"{round(c):02x}" for c in rgb[:3]) + + +def resolve_raw(values, key, seen=()): if key in seen: raise ValueError(f"var() loop at {key}") value = values.get(key, HA_DEFAULTS.get(key)) @@ -95,10 +136,48 @@ def resolve(values, key, seen=()): value = str(value).strip() m = re.fullmatch(r"var\(--([\w-]+)\)", value) if m: - return resolve(values, m.group(1), seen + (key,)) + return resolve_raw(values, m.group(1), seen + (key,)) + return value + + +def resolve(values, key): + value = resolve_raw(values, key) if not re.fullmatch(r"#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?", value): raise ValueError(f"{key} is {value!r}; only opaque hex colours can be measured") - return value.lower() + return to_hex(parse_colour(value)) + + +def ground(values): + """Every colour the view's own background shows, opaque.""" + colours = [parse_colour(resolve(values, "primary-background-color"))] + if "lovelace-background" in values: + stops = [parse_colour(c) for c in COLOUR.findall(resolve_raw(values, "lovelace-background"))] + if not stops: + raise ValueError("lovelace-background names no colour") + if any(s[3] < 1 for s in stops): + raise ValueError("lovelace-background has a translucent stop; cannot measure") + colours.append(stops[0]) + for a, b in zip(stops, stops[1:]): + colours += [tuple(a[i] + (b[i] - a[i]) * t / 10 for i in range(4)) for t in range(1, 11)] + return colours + + +def surfaces(values, key): + """The opaque colours a pair's background can be, as hex.""" + if key == "primary-background-color": + found = ground(values) + elif key == "card-background-color": + found = [parse_colour(resolve(values, "card-background-color"))] + if "ha-card-background" in values: + r, g, b, a = parse_colour(resolve_raw(values, "ha-card-background")) + if a == 1: + found.append((r, g, b, 1.0)) + else: + found += [(r * a + gr * (1 - a), g * a + gg * (1 - a), b * a + gb * (1 - a), 1.0) + for gr, gg, gb, _ in ground(values)] + else: + return [resolve(values, key)] + return list(dict.fromkeys(to_hex(c) for c in found)) def themes(): @@ -115,8 +194,11 @@ def main(markdown=False): rows, failures = [], 0 for name, mode, values in themes(): for label, fg, bg, bar in PAIRS: - f, b = resolve(values, fg), resolve(values, bg) - r = ratio(f, b) + f = resolve(values, fg) + under = surfaces(values, bg) + r, b = min((ratio(f, c), c) for c in under) + if len(under) > 1: + b = f"{b} (worst of {len(under)})" ok = r >= bar failures += not ok rows.append((name, mode, label, f, b, r, bar, ok)) @@ -125,7 +207,7 @@ def main(markdown=False): print("| Theme | Mode | Pair | Foreground | Background | Ratio | Bar | AA |") print("|---|---|---|---|---|---|---|---|") for name, mode, label, f, b, r, bar, ok in rows: - print(f"| {name} | {mode} | {label} | `{f}` | `{b}` | {r:.2f} | {bar:g} | {'pass' if ok else '**FAIL**'} |") + print(f"| {name} | {mode} | {label} | `{f}` | `{b.split()[0]}`{b[7:]} | {r:.2f} | {bar:g} | {'pass' if ok else '**FAIL**'} |") print() print("Brand reference (not drawn by these themes):") print() @@ -133,7 +215,7 @@ def main(markdown=False): print(f"- {label}: {ratio(f, b):.2f}") else: for name, mode, label, f, b, r, bar, ok in rows: - print(f"{'ok ' if ok else 'FAIL'} {name:18} {mode:5} {label:24} {f} on {b} {r:5.2f} (>= {bar:g})") + print(f"{'ok ' if ok else 'FAIL'} {name:18} {mode:5} {label:24} {f} on {b:22} {r:5.2f} (>= {bar:g})") print() for label, f, b in BRAND: print(f"brand {label:36} {ratio(f, b):5.2f}") diff --git a/themes/dartec.yaml b/themes/dartec.yaml index b87d292..cbb72c5 100644 --- a/themes/dartec.yaml +++ b/themes/dartec.yaml @@ -4,10 +4,16 @@ # Dartec the house theme: teal on warm cream and paper. # Dartec Graphite quiet neutral surfaces, teal only where something is on. # The panel and night theme. +# Dartec Glass frosted cards over a teal gradient ground. Blur is costly +# to draw: not for wall panels. +# Dartec Glass Lite Glass without blur or transparency, for wall panels and +# slower tablets. +# Dartec Soft borderless paper cards with a soft shadow. +# Dartec Material tonal surfaces generated from the brand teal. # -# Homes store their default theme by name, so these names never change. Both -# themes set light and dark in full, use theme variables only (no card-mod, no -# JavaScript, no images), and pass WCAG AA in both modes: see +# Homes store their default theme by name, so these names never change. Every +# theme sets light and dark in full, uses theme variables only (no card-mod, no +# JavaScript, no images), and passes WCAG AA in both modes: see # scripts/contrast.py. Copper is not used: the brand reserves it for content # written by AI. # @@ -176,3 +182,341 @@ Dartec Graphite: sidebar-selected-icon-color: "#5fb8b1" sidebar-selected-text-color: "#5fb8b1" code-editor-background-color: "#121314" + +# ── Dartec Glass ───────────────────────────────────────────────────────────── +# Frosted cards over a teal gradient ground: ha-card-background is translucent +# and ha-card-backdrop-filter blurs what is behind it. The ground is a gradient, +# not a photograph, so nothing is downloaded. Contrast is measured on the card +# blended over every colour of the gradient, worst case. Drawn without a GPU, +# the blur multiplied the drawing work about elevenfold: wall panels take Glass +# Lite. +# The top bar stays solid. +Dartec Glass: + primary-color: "#1a6b6b" + accent-color: "#1a6b6b" + dark-primary-color: "#0f4f4f" + light-primary-color: "#e6f0f0" + + ha-font-family-body: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-heading: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-code: "'Dartec Plex Mono', 'IBM Plex Mono', ui-monospace, Menlo, Consolas, 'Liberation Mono', monospace" + + # Glass reads as glass only with soft edges: a step up from the brand's 8px + ha-card-border-radius: "14px" + ha-dialog-border-radius: "16px" + ha-card-border-width: "1px" + + modes: + light: + lovelace-background: "linear-gradient(165deg, #d7e8e7 0%, #eef2ee 45%, #ebe9df 100%)" + primary-background-color: "#eef2ee" + secondary-background-color: "#e3e9e5" + app-header-background-color: "#e3ecea" + app-header-text-color: "#22251f" + card-background-color: "#f7f8f4" # dialogs and pop-ups: solid + ha-card-background: "rgba(255, 253, 248, 0.62)" + ha-card-backdrop-filter: "blur(16px) saturate(120%)" + ha-card-border-color: "rgba(143, 131, 104, 0.28)" + ha-card-box-shadow: "0 1px 2px rgba(15, 79, 79, 0.06), 0 10px 30px rgba(15, 79, 79, 0.08)" + primary-text-color: "#1d211c" + secondary-text-color: "#4a4f45" + disabled-text-color: "#61655a" + divider-color: "rgba(143, 131, 104, 0.35)" + primary-color: "#1a6b6b" + accent-color: "#1a6b6b" + text-primary-color: "#ffffff" + state-active-color: "#1a6b6b" + state-icon-color: "#1a6b6b" + state-climate-cool-color: "#1a6b6b" + state-climate-dry-color: "#8a6a12" + state-climate-fan_only-color: "#0f4f4f" + state-light-on-color: "#a88400" + state-switch-on-color: "#1a6b6b" + state-cover-active-color: "#1a6b6b" + error-color: "#dc2626" + warning-color: "#c96e05" # #d97706 is 2.89:1 on glass over the ground + success-color: "#047857" + info-color: "#1a6b6b" + sidebar-background-color: "#eef2ee" + sidebar-selected-icon-color: "#1a6b6b" + sidebar-selected-text-color: "#1a6b6b" + + dark: + lovelace-background: "radial-gradient(120% 90% at 15% 0%, #164645 0%, #0e3030 45%, #0d1214 100%)" + primary-background-color: "#0f1416" + secondary-background-color: "#16201f" + app-header-background-color: "#122c2c" + app-header-text-color: "#f2f0ea" + card-background-color: "#1c2a2a" + ha-card-background: "rgba(232, 230, 223, 0.08)" + ha-card-backdrop-filter: "blur(18px) saturate(140%)" + ha-card-border-color: "rgba(232, 230, 223, 0.14)" + ha-card-box-shadow: "none" + primary-text-color: "#f2f0ea" + secondary-text-color: "#d6dad4" + disabled-text-color: "#9aa19b" + divider-color: "rgba(232, 230, 223, 0.16)" + primary-color: "#8fd8d2" + accent-color: "#8fd8d2" + text-primary-color: "#0f1416" # text on light teal: ink, not white + state-active-color: "#8fd8d2" + state-icon-color: "#8fd8d2" + state-climate-cool-color: "#8fd8d2" + state-climate-dry-color: "#ecd06f" + state-climate-fan_only-color: "#bdeae6" + state-light-on-color: "#fde047" + state-switch-on-color: "#8fd8d2" + state-cover-active-color: "#8fd8d2" + error-color: "#f87171" + warning-color: "#fbbf24" + success-color: "#34d399" + info-color: "#8fd8d2" + sidebar-background-color: "#0f1416" + sidebar-selected-icon-color: "#8fd8d2" + sidebar-selected-text-color: "#8fd8d2" + code-editor-background-color: "#0f1416" + +# ── Dartec Glass Lite ──────────────────────────────────────────────────────── +# Glass without blur or transparency, for wall panels and slower tablets. The +# same ground; the cards are the solid colour the glass cards average to, so it +# costs no more to draw than an opaque theme. +Dartec Glass Lite: + primary-color: "#1a6b6b" + accent-color: "#1a6b6b" + dark-primary-color: "#0f4f4f" + light-primary-color: "#e6f0f0" + + ha-font-family-body: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-heading: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-code: "'Dartec Plex Mono', 'IBM Plex Mono', ui-monospace, Menlo, Consolas, 'Liberation Mono', monospace" + + ha-card-border-radius: "14px" + ha-dialog-border-radius: "16px" + ha-card-border-width: "1px" + + modes: + light: + lovelace-background: "linear-gradient(165deg, #d7e8e7 0%, #eef2ee 45%, #ebe9df 100%)" + primary-background-color: "#eef2ee" + secondary-background-color: "#e3e9e5" + app-header-background-color: "#e3ecea" + app-header-text-color: "#22251f" + card-background-color: "#f8f8f3" + ha-card-background: "#f8f8f3" + ha-card-backdrop-filter: "none" + ha-card-border-color: "#d9d4c4" + ha-card-box-shadow: "none" + primary-text-color: "#1d211c" + secondary-text-color: "#4a4f45" + disabled-text-color: "#61655a" + divider-color: "#d9d4c4" + primary-color: "#1a6b6b" + accent-color: "#1a6b6b" + text-primary-color: "#ffffff" + state-active-color: "#1a6b6b" + state-icon-color: "#1a6b6b" + state-climate-cool-color: "#1a6b6b" + state-climate-dry-color: "#8a6a12" + state-climate-fan_only-color: "#0f4f4f" + state-light-on-color: "#a88400" + state-switch-on-color: "#1a6b6b" + state-cover-active-color: "#1a6b6b" + error-color: "#dc2626" + warning-color: "#c96e05" # #d97706 is 2.99:1 on this card + success-color: "#047857" + info-color: "#1a6b6b" + sidebar-background-color: "#eef2ee" + sidebar-selected-icon-color: "#1a6b6b" + sidebar-selected-text-color: "#1a6b6b" + + dark: + lovelace-background: "radial-gradient(120% 90% at 15% 0%, #164645 0%, #0e3030 45%, #0d1214 100%)" + primary-background-color: "#0f1416" + secondary-background-color: "#16201f" + app-header-background-color: "#122c2c" + app-header-text-color: "#f2f0ea" + card-background-color: "#23302f" + ha-card-background: "#23302f" + ha-card-backdrop-filter: "none" + ha-card-border-color: "#34403f" + ha-card-box-shadow: "none" + primary-text-color: "#f2f0ea" + secondary-text-color: "#c3c8c2" + disabled-text-color: "#9aa19b" + divider-color: "#34403f" + primary-color: "#6cc7c0" + accent-color: "#6cc7c0" + text-primary-color: "#0f1416" # text on light teal: ink, not white + state-active-color: "#6cc7c0" + state-icon-color: "#6cc7c0" + state-climate-cool-color: "#6cc7c0" + state-climate-dry-color: "#e3c25a" + state-climate-fan_only-color: "#a3e0db" + state-light-on-color: "#facc15" + state-switch-on-color: "#6cc7c0" + state-cover-active-color: "#6cc7c0" + error-color: "#f87171" + warning-color: "#fbbf24" + success-color: "#34d399" + info-color: "#6cc7c0" + sidebar-background-color: "#0f1416" + sidebar-selected-icon-color: "#6cc7c0" + sidebar-selected-text-color: "#6cc7c0" + code-editor-background-color: "#0f1416" + +# ── Dartec Soft ────────────────────────────────────────────────────────────── +# Borderless paper cards that float on the brand's two-layer shadow in light +# mode, and sit on a hairline instead of a shadow in dark mode. +Dartec Soft: + primary-color: "#1a6b6b" + accent-color: "#1a6b6b" + dark-primary-color: "#0f4f4f" + light-primary-color: "#e6f0f0" + + ha-font-family-body: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-heading: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-code: "'Dartec Plex Mono', 'IBM Plex Mono', ui-monospace, Menlo, Consolas, 'Liberation Mono', monospace" + + ha-card-border-radius: "12px" + ha-dialog-border-radius: "14px" + + modes: + light: + primary-background-color: "#f1f0ea" + secondary-background-color: "#e8e6dc" + card-background-color: "#fffdf8" # paper + ha-card-border-width: "0px" + ha-card-box-shadow: "0 1px 2px rgba(34, 37, 31, 0.06), 0 6px 20px rgba(34, 37, 31, 0.07)" + primary-text-color: "#22251f" + secondary-text-color: "#52564c" + disabled-text-color: "#666a5c" + divider-color: "#e2ddcf" + primary-color: "#1a6b6b" + accent-color: "#1a6b6b" + text-primary-color: "#ffffff" + state-active-color: "#1a6b6b" + state-icon-color: "#1a6b6b" + state-climate-cool-color: "#1a6b6b" + state-climate-dry-color: "#8a6a12" + state-climate-fan_only-color: "#0f4f4f" + state-light-on-color: "#a88400" + state-switch-on-color: "#1a6b6b" + state-cover-active-color: "#1a6b6b" + error-color: "#dc2626" + warning-color: "#d97706" + success-color: "#047857" + info-color: "#1a6b6b" + sidebar-background-color: "#fffdf8" + sidebar-selected-icon-color: "#1a6b6b" + sidebar-selected-text-color: "#1a6b6b" + + dark: + primary-background-color: "#17181a" + secondary-background-color: "#1f2124" + card-background-color: "#232529" + ha-card-border-width: "1px" + ha-card-border-color: "rgba(232, 230, 223, 0.07)" + ha-card-box-shadow: "none" + primary-text-color: "#e8e6df" + secondary-text-color: "#aeada4" + disabled-text-color: "#8a8a81" + divider-color: "#33353a" + primary-color: "#4db3ac" + accent-color: "#4db3ac" + text-primary-color: "#17181a" # text on light teal: ink, not white + state-active-color: "#4db3ac" + state-icon-color: "#4db3ac" + state-climate-cool-color: "#4db3ac" + state-climate-dry-color: "#d9b44a" + state-climate-fan_only-color: "#7fd0c9" + state-light-on-color: "#facc15" + state-switch-on-color: "#4db3ac" + state-cover-active-color: "#4db3ac" + error-color: "#f87171" + warning-color: "#fbbf24" + success-color: "#34d399" + info-color: "#4db3ac" + sidebar-background-color: "#1e2024" + sidebar-selected-icon-color: "#4db3ac" + sidebar-selected-text-color: "#7fd0c9" + code-editor-background-color: "#17181a" + +# ── Dartec Material ────────────────────────────────────────────────────────── +# Tonal surfaces: a Material 3 "tonal spot" scheme generated from the brand +# teal #1A6B6B (materialyoucolor 3.0.4), with the dark ground lifted off +# near-black to the brand's rule. 16px cards with no border or shadow. No +# material-you JavaScript. +Dartec Material: + primary-color: "#1b6969" + accent-color: "#1b6969" + dark-primary-color: "#0f4f4f" + light-primary-color: "#e6f0f0" + + ha-font-family-body: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-heading: "'Dartec Lateef', Roboto, Noto, system-ui, sans-serif" + ha-font-family-code: "'Dartec Plex Mono', 'IBM Plex Mono', ui-monospace, Menlo, Consolas, 'Liberation Mono', monospace" + + ha-card-border-radius: "16px" + ha-dialog-border-radius: "24px" + ha-card-border-width: "0px" + ha-card-box-shadow: "none" + + modes: + light: + primary-background-color: "#f6faf9" + secondary-background-color: "#e7f0ef" + app-header-background-color: "#e7f0ef" + app-header-text-color: "#2a3434" + card-background-color: "#e7f0ef" + primary-text-color: "#1f2828" + secondary-text-color: "#4c5757" + disabled-text-color: "#5f6a6a" + divider-color: "#a9b4b3" + primary-color: "#1b6969" + accent-color: "#1b6969" + text-primary-color: "#ffffff" + state-active-color: "#1b6969" + state-icon-color: "#1b6969" + state-climate-cool-color: "#1b6969" + state-climate-dry-color: "#7a5e10" + state-climate-fan_only-color: "#3a647d" + state-light-on-color: "#a88400" + state-switch-on-color: "#1b6969" + state-cover-active-color: "#1b6969" + error-color: "#a83836" + warning-color: "#c96e05" # #d97706 is 2.75:1 on this card + success-color: "#047857" + info-color: "#1b6969" + sidebar-background-color: "#eef5f4" + sidebar-selected-icon-color: "#1b6969" + sidebar-selected-text-color: "#1b6969" + + dark: + primary-background-color: "#111717" + secondary-background-color: "#192121" + app-header-background-color: "#192121" + app-header-text-color: "#dce8e7" + card-background-color: "#1d2827" + primary-text-color: "#dce8e7" + secondary-text-color: "#a8b3b3" + disabled-text-color: "#8a9595" + divider-color: "#3f4a4a" + primary-color: "#9bd0cf" + accent-color: "#9bd0cf" + text-primary-color: "#111717" # text on light teal: ink, not white + state-active-color: "#9bd0cf" + state-icon-color: "#9bd0cf" + state-climate-cool-color: "#9bd0cf" + state-climate-dry-color: "#e3c25a" + state-climate-fan_only-color: "#d2ecff" + state-light-on-color: "#facc15" + state-switch-on-color: "#9bd0cf" + state-cover-active-color: "#9bd0cf" + error-color: "#fa746f" + warning-color: "#fbbf24" + success-color: "#34d399" + info-color: "#9bd0cf" + sidebar-background-color: "#131b1b" + sidebar-selected-icon-color: "#9bd0cf" + sidebar-selected-text-color: "#9bd0cf" + code-editor-background-color: "#111717"