From 92b68ca21aadce0d539fe693eacd644e259b829e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Jul 2026 11:27:35 -0400 Subject: [PATCH] Style kiosk dashboards with the CubeCraft Creations display system The design system was only ever expressed in the production card's stylesheet (custom_components/cubecraft_production/www/ cubecraft-production-card.js), which is gitignored, so the dashboards had no way to share it. Mirror those tokens into an HA theme and apply the card's own motifs to the wall panels. themes/cubecraft.yaml carries the brand gradient, Nunito, the 4px scale, radii, shadows and semantic colours. It uses `modes:` so it follows HA's light/dark setting: light is the system as authored, dark uses violet-tinted neutrals with the primary lifted to #8B8AF2 (5.6:1 on the card surface) since #3B3AB8 is unreadable as an accent on dark. The gradient itself is unchanged in both modes -- white clears AA across its whole span. At-a-Glance, Family Hub and Voice Alarms pick up three motifs from the card: the .title gradient bar, .column header section labels, and the 4px brand left rule on each tile. Tile colour is cut back to brand indigo, with green/amber/red reserved for real signals (printer offline, bin full) -- the same discipline the card uses, where only a blocked order goes red. CubeOps gets the theme too, so the board and the dashboards land on the same ramp instead of the card improvising against HA's defaults. The card injects its @font-face only on its own page, so a small frontend module declares Nunito document-wide. It points at the woff2 the integration already serves rather than vendoring a second copy, which needed a .gitignore carve-out since www/ is excluded wholesale. dakboard-dark is left in place; switching a dashboard back is a one-line change. Not yet validated by `ha core check` -- this worktree is outside the config tree HA actually loads. Co-Authored-By: Claude Opus 5 --- .gitignore | 11 +- configuration.yaml | 6 + dashboards/views/at-a-glance-dashboard.yaml | 135 ++++++-- dashboards/views/cubeops-dashboard.yaml | 13 +- dashboards/views/family-hub-dashboard.yaml | 128 ++++++-- dashboards/views/voice-alarms-dashboard.yaml | 110 ++++++- themes/cubecraft.yaml | 305 +++++++++++++++++++ www/cubecraft/cubecraft-brand-font.js | 49 +++ 8 files changed, 702 insertions(+), 55 deletions(-) create mode 100644 themes/cubecraft.yaml create mode 100644 www/cubecraft/cubecraft-brand-font.js diff --git a/.gitignore b/.gitignore index 069f615..3cffe35 100644 --- a/.gitignore +++ b/.gitignore @@ -112,7 +112,16 @@ home-assistant_v2.db-wal # Dependencies and cache deps/ tts/ -www/ +# www/ is HA's public static dir: HACS frontend plugins, camera snapshots and +# TTS junk all land in it, so it stays ignored — except for the small +# hand-written frontend modules we actually author and want in git. +# (git will not descend into an excluded directory, so each level has to be +# re-included before the next one can be narrowed again.) +!www/ +www/* +!www/cubecraft/ +www/cubecraft/* +!www/cubecraft/*.js custom_components/ image/ ms365_storage/ diff --git a/configuration.yaml b/configuration.yaml index 9ec639e..913ae1d 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -7,6 +7,12 @@ default_config: # Load frontend themes from the themes folder frontend: themes: !include_dir_merge_named themes + # Declares the Nunito @font-face document-wide for the CubeCraft display + # system. A theme can only set font-family, not load the face — see + # www/cubecraft/cubecraft-brand-font.js. Bump ?v= after editing that file to + # get past the browser cache. + extra_module_url: + - /local/cubecraft/cubecraft-brand-font.js?v=1 lovelace: dashboards: !include dashboards/dashboards.yaml diff --git a/dashboards/views/at-a-glance-dashboard.yaml b/dashboards/views/at-a-glance-dashboard.yaml index 6b15d7c..c96ccc0 100644 --- a/dashboards/views/at-a-glance-dashboard.yaml +++ b/dashboards/views/at-a-glance-dashboard.yaml @@ -1,13 +1,33 @@ +# At-a-Glance — wall panel +# +# Styled with the CubeCraft Creations display system. The tokens live in +# themes/cubecraft.yaml, which mirrors the `:host` block of the production +# card's stylesheet (custom_components/cubecraft_production/www/ +# cubecraft-production-card.js). Anything referenced here as --cc-* comes from +# that theme, so this file never hard-codes a brand hex. +# +# Three motifs carry the style, all lifted from the production card: +# .title → gradient title bar, white 800-weight type, brand radius +# .column header → uppercase 700/.05em muted section labels +# article → 8px radius, 4px brand left rule, shadow-sm on each tile +# +# Colour is deliberately restrained: brand indigo for anything informational, +# and green/amber/red kept in reserve so they still mean something when they +# show up. That is the same discipline the production card uses, where only a +# blocked order goes red. title: At-a-Glance -theme: dakboard-dark +theme: cubecraft views: - title: At-a-Glance path: at-a-glance icon: mdi:eye-outline - theme: dakboard-dark + theme: cubecraft type: masonry cards: - # ── Header ── + # ── Brand title bar ── + # Mirrors `.shell.kiosk .title`: gradient ground, white type at 1.75rem, + # brand radius and the brand shadow. ha-markdown supplies its own 16px + # inset, so ha-card only tops that up to roughly the card's 20px/28px. - type: markdown content: | # {{ now().strftime("%A, %B %d, %Y") }} @@ -15,21 +35,28 @@ views: card_mod: style: | ha-card { + background: var(--cc-gradient); + color: #fff; + border: none; + border-radius: var(--cc-radius-brand); + box-shadow: var(--cc-shadow-brand); + padding: 6px 12px; text-align: center; - background: none; - box-shadow: none; - padding: 0 0 4px 0; } ha-card h1 { - font-size: 1.2em; margin: 0; - font-weight: 300; - color: var(--secondary-text-color); + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.02em; + opacity: 0.85; + color: #fff; } ha-card h2 { - font-size: 2em; margin: 2px 0 0 0; - font-weight: 400; + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.01em; + color: #fff; } # ── Weather Now ── @@ -41,127 +68,200 @@ views: round_temperature: true # ── Indoor Climate ── + # First use of the section-label treatment; the rest of the file aliases + # it as *cc_section. Selectors are listed broadly on purpose — a heading + # card's internals are not a stable API, and any selector that misses + # simply leaves that bit at the theme default rather than breaking. - type: heading heading: Indoor Climate heading_style: title icon: mdi:home-thermometer-outline + card_mod: &cc_section + style: | + ha-card { + background: none; + border: none; + box-shadow: none; + padding: 0; + margin: 4px 0 -4px 0; + } + ha-card .heading, + ha-card .content p, + ha-card p { + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + font-size: 0.9375rem; + color: var(--cc-text-muted, var(--secondary-text-color)); + } + ha-card ha-icon, + ha-card ha-svg-icon { + color: var(--cc-accent, var(--primary-color)); + } - type: grid columns: 3 square: false cards: + # First use of the `article` treatment; aliased as *cc_tile below. - type: tile entity: sensor.living_room_temperature name: Living Room icon: mdi:sofa - color: amber + color: primary + card_mod: &cc_tile + style: | + ha-card { + border-radius: var(--cc-radius-md); + border-left: 4px solid var(--cc-accent); + box-shadow: var(--cc-shadow-sm); + } - type: tile entity: sensor.office_temperature name: Office icon: mdi:desk - color: teal + color: primary + card_mod: *cc_tile - type: tile entity: sensor.bedroom_temperature name: Master Bedroom icon: mdi:bed - color: purple + color: primary + card_mod: *cc_tile - type: tile entity: sensor.upstairs_temperature name: Upstairs icon: mdi:stairs - color: indigo + color: primary + card_mod: *cc_tile - type: tile entity: sensor.blink_family_room_temperature name: Family Room icon: mdi:television - color: red + color: primary + card_mod: *cc_tile - type: tile entity: sensor.blink_kitchen_temperature name: Kitchen icon: mdi:stove - color: deep-orange + color: primary + card_mod: *cc_tile # ── 3D Printer Status ── - type: heading heading: 3D Printers heading_style: title icon: mdi:printer-3d-nozzle + card_mod: *cc_section - type: grid columns: 2 square: false cards: + # Online/offline is a genuine success signal, so these keep green and + # take the success-coloured left rule. - type: tile entity: binary_sensor.p1s_01p09c470102673_online name: PrintsCharming icon: mdi:printer-3d-nozzle show_entity_picture: false color: green + card_mod: &cc_tile_success + style: | + ha-card { + border-radius: var(--cc-radius-md); + border-left: 4px solid var(--success-color); + box-shadow: var(--cc-shadow-sm); + } - type: tile entity: binary_sensor.a1_03919d540806387_online name: Ds-Turd-Flinger icon: mdi:printer-3d-nozzle show_entity_picture: false color: green + card_mod: *cc_tile_success - type: tile entity: binary_sensor.a1mini_0309ca580403653_online name: Polly-Pocket icon: mdi:printer-3d-nozzle show_entity_picture: false color: green + card_mod: *cc_tile_success - type: tile entity: binary_sensor.constipation_orion_online name: Constipation-Orion icon: mdi:printer-3d-nozzle show_entity_picture: false color: green + card_mod: *cc_tile_success - type: tile entity: binary_sensor.jordyns_layer_slayer_online name: Jordyn's Layer Slayer icon: mdi:printer-3d-nozzle show_entity_picture: false color: green + card_mod: *cc_tile_success - type: tile entity: binary_sensor.centauri_carbon_sdcp_status name: Centauri Carbon icon: mdi:printer-3d-nozzle show_entity_picture: false color: green + card_mod: *cc_tile_success # ── Litter Boxes ── - type: heading heading: Litter Boxes heading_style: title icon: mdi:cat + card_mod: *cc_section - type: grid columns: 2 square: false cards: + # These are the "something needs doing" tiles — the equivalent of a + # blocked order in the production card, so they take the error rule. - type: tile entity: binary_sensor.cat_crapper_3000_wastebin_filled name: Cat Crapper 3000 icon: mdi:delete-empty show_entity_picture: false color: red + card_mod: &cc_tile_alert + style: | + ha-card { + border-radius: var(--cc-radius-md); + border-left: 4px solid var(--error-color); + box-shadow: var(--cc-shadow-sm); + } - type: tile entity: binary_sensor.cat_crapper_3000_sand_lack name: CC3000 Litter icon: mdi:grain show_entity_picture: false color: amber + card_mod: + style: | + ha-card { + border-radius: var(--cc-radius-md); + border-left: 4px solid var(--warning-color); + box-shadow: var(--cc-shadow-sm); + } - type: tile entity: binary_sensor.poop_box_wastebin_filled name: Poop Box icon: mdi:delete-empty show_entity_picture: false color: red + card_mod: *cc_tile_alert - type: tile entity: binary_sensor.triple_threat_wastebin_filled name: Triple Threat icon: mdi:delete-empty show_entity_picture: false color: red + card_mod: *cc_tile_alert # ── Security ── - type: heading @@ -181,6 +281,7 @@ views: - type: entity entity: alarm_control_panel.blink_rear name: Rear + card_mod: *cc_section # ── Recent Activity ── - type: logbook diff --git a/dashboards/views/cubeops-dashboard.yaml b/dashboards/views/cubeops-dashboard.yaml index b1e8849..68d3c36 100644 --- a/dashboards/views/cubeops-dashboard.yaml +++ b/dashboards/views/cubeops-dashboard.yaml @@ -13,12 +13,21 @@ # and registers its own Lovelace resource, so there is nothing to add under # Settings > Dashboards > Resources. -# kiosk-mode plugin (HACS > Frontend). Without it the board still fills the -# screen; you just keep HA's header and sidebar. +# kiosk-mode plugin (HACS > Frontend). NOT currently installed — there is no +# lovelace-kiosk-mode under www/community — so this block is inert today. +# `theme: cubecraft` below already collapses the header via header-height, so +# installing the plugin now only buys you hide_sidebar. kiosk_mode: hide_header: true hide_sidebar: true +# The CubeCraft display system. The card reads HA's theme variables for its +# surfaces, text and borders (see the `--cc-surface` fallbacks in +# cubecraft-production-card.js), so setting this makes the board and the rest +# of the dashboards land on exactly the same ramp instead of the card +# improvising against HA's defaults. +theme: cubecraft + views: - title: Production # Panel mode gives the card the whole viewport with no dashboard padding. diff --git a/dashboards/views/family-hub-dashboard.yaml b/dashboards/views/family-hub-dashboard.yaml index 951330b..3d1ebea 100644 --- a/dashboards/views/family-hub-dashboard.yaml +++ b/dashboards/views/family-hub-dashboard.yaml @@ -1,41 +1,58 @@ +# Family Hub — wall panel +# +# Styled with the CubeCraft Creations display system; see +# dashboards/views/at-a-glance-dashboard.yaml for the motif notes and +# themes/cubecraft.yaml for the tokens. Everything brand-coloured here comes +# through --cc-* variables rather than a literal hex. title: Family Hub -theme: dakboard-dark +theme: cubecraft views: - title: Family Hub path: family-hub icon: mdi:home-heart - theme: dakboard-dark + theme: cubecraft type: masonry cards: - # ── Date & Greeting Header ── + # ── Brand greeting bar ── + # The production card's `.title` treatment, carrying the date and a + # time-of-day greeting instead of a board name. - type: markdown content: | # {{ now().strftime("%A, %B %d") }} {% set h = now().hour %} - {% if h < 12 %}Good morning{% elif h < 17 %}Good afternoon{% else %}Good evening{% endif %}, family! + ## {% if h < 12 %}Good morning{% elif h < 17 %}Good afternoon{% else %}Good evening{% endif %}, family! + entity_id: + - sensor.time card_mod: style: | ha-card { + background: var(--cc-gradient); + color: #fff; + border: none; + border-radius: var(--cc-radius-brand); + box-shadow: var(--cc-shadow-brand); + padding: 6px 12px; text-align: center; - background: none; - box-shadow: none; - padding: 0 0 8px 0; } ha-card h1 { - font-size: 1.4em; - margin: 0 0 4px 0; - font-weight: 300; - color: var(--secondary-text-color); - } - ha-card p { - font-size: 1.1em; margin: 0; - font-weight: 400; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.02em; + opacity: 0.85; + color: #fff; + } + ha-card h2 { + margin: 2px 0 0 0; + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.01em; + color: #fff; } - entity_id: - - sensor.time # ── Clock ── + # Left on the sunken surface rather than a card, so the gradient bar above + # stays the only heavyweight element in the column. - type: clock clock_size: large clock_style: analog @@ -48,6 +65,7 @@ views: style: | ha-card { background: none; + border: none; box-shadow: none; text-align: center; } @@ -73,8 +91,11 @@ views: card_mod: style: | ha-card { - --calendar-day-color: var(--primary-text-color); - --calendar-day-weekday-color: var(--secondary-text-color); + --calendar-day-color: var(--cc-text, var(--primary-text-color)); + --calendar-day-weekday-color: var(--cc-text-muted, var(--secondary-text-color)); + --fc-today-bg-color: var(--cc-accent-soft); + --fc-event-bg-color: var(--cc-accent); + --fc-event-border-color: var(--cc-accent); } # ── Quick Glance ── @@ -92,64 +113,104 @@ views: - type: entity entity: sensor.office_temperature name: Office + card_mod: &cc_section + style: | + ha-card { + background: none; + border: none; + box-shadow: none; + padding: 0; + margin: 4px 0 -4px 0; + } + ha-card .heading, + ha-card .content p, + ha-card p { + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + font-size: 0.9375rem; + color: var(--cc-text-muted, var(--secondary-text-color)); + } + ha-card ha-icon, + ha-card ha-svg-icon { + color: var(--cc-accent, var(--primary-color)); + } - type: grid columns: 3 square: false cards: - # Outdoor temp + # Temperatures are informational, so they all take the brand rule + # rather than a colour each — see the colour note in at-a-glance. - type: tile entity: sensor.openweathermap_temperature name: Outside icon: mdi:thermometer show_entity_picture: false - color: blue - # Living Room + color: primary + card_mod: &cc_tile + style: | + ha-card { + border-radius: var(--cc-radius-md); + border-left: 4px solid var(--cc-accent); + box-shadow: var(--cc-shadow-sm); + } - type: tile entity: sensor.living_room_temperature name: Living Room icon: mdi:sofa show_entity_picture: false - color: amber - # Office + color: primary + card_mod: *cc_tile - type: tile entity: sensor.office_temperature name: Office icon: mdi:desk show_entity_picture: false - color: teal - # Master Bedroom + color: primary + card_mod: *cc_tile - type: tile entity: sensor.bedroom_temperature name: Master Bedroom icon: mdi:bed show_entity_picture: false - color: purple - # Upstairs + color: primary + card_mod: *cc_tile - type: tile entity: sensor.upstairs_temperature name: Upstairs icon: mdi:stairs show_entity_picture: false - color: indigo - # Family Room (Blink) + color: primary + card_mod: *cc_tile - type: tile entity: sensor.blink_family_room_temperature name: Family Room icon: mdi:television show_entity_picture: false - color: red + color: primary + card_mod: *cc_tile # ── Shopping List ── + - type: heading + heading: Shopping List + heading_style: title + icon: mdi:cart-outline + card_mod: *cc_section + - type: todo-list entity: todo.shopping_list - title: Shopping List hide_completed: true display_order: none # ── Chore Tracker Quick View ── + - type: heading + heading: Chores Today + heading_style: title + icon: mdi:check-circle-outline + card_mod: *cc_section + - type: entities - title: Chores Today show_header_toggle: false entities: - entity: binary_sensor.cat_medication_tracker_all_cats_medicated @@ -171,3 +232,4 @@ views: - type: entity entity: binary_sensor.upstairs_occupancy name: Upstairs + card_mod: *cc_section diff --git a/dashboards/views/voice-alarms-dashboard.yaml b/dashboards/views/voice-alarms-dashboard.yaml index 8cf3d0f..3511c4a 100644 --- a/dashboards/views/voice-alarms-dashboard.yaml +++ b/dashboards/views/voice-alarms-dashboard.yaml @@ -1,23 +1,111 @@ +# Voice Alarms — satellite control panel +# +# Styled with the CubeCraft Creations display system; see +# dashboards/views/at-a-glance-dashboard.yaml for the motif notes and +# themes/cubecraft.yaml for the tokens. +# +# This one is a control panel rather than a glanceable board, so the per-tile +# left rule is dropped — the entity rows already carry their own structure and +# a rule on every card would just add noise. It keeps the gradient title bar +# and the section labels. +# +# Note that the theme collapses the HA toolbar (header-height: 0). This +# dashboard is reached by URL rather than the sidebar (show_in_sidebar: false +# in dashboards/voice-alarms.yaml), so navigate away via the sidebar. title: Voice Alarms +theme: cubecraft views: - title: Voice Alarms path: voice-alarms icon: mdi:alarm-panel + theme: cubecraft type: panel cards: - type: vertical-stack cards: + # ── Brand title bar ── - type: markdown content: | # Voice Alarms + ## Jarvis & ReSpeaker satellites + card_mod: + style: | + ha-card { + background: var(--cc-gradient); + color: #fff; + border: none; + border-radius: var(--cc-radius-brand); + box-shadow: var(--cc-shadow-brand); + padding: 6px 12px; + } + ha-card h1 { + margin: 0; + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.01em; + color: #fff; + } + ha-card h2 { + margin: 2px 0 0 0; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.02em; + opacity: 0.85; + color: #fff; + } - These alarms are managed by Home Assistant and announced on the selected - Jarvis and ReSpeaker satellites. + - type: markdown + content: > + These alarms are managed by Home Assistant and announced on the + selected Jarvis and ReSpeaker satellites. + card_mod: + style: | + ha-card { + background: none; + border: none; + box-shadow: none; + } + ha-card p { + margin: 0; + font-size: 0.9375rem; + line-height: 1.5; + color: var(--cc-text-muted, var(--secondary-text-color)); + } + + - type: heading + heading: Satellites + heading_style: title + icon: mdi:speaker-message + card_mod: + style: | + ha-card { + background: none; + border: none; + box-shadow: none; + padding: 0; + margin: 4px 0 -4px 0; + } + ha-card .heading, + ha-card .content p, + ha-card p { + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + font-size: 0.9375rem; + color: var(--cc-text-muted, var(--secondary-text-color)); + } + ha-card ha-icon, + ha-card ha-svg-icon { + color: var(--cc-accent, var(--primary-color)); + } - type: grid columns: 2 square: false cards: + # Card titles take the production card's column-header treatment: + # uppercase, 700, .05em tracking, muted. First use defines the + # anchor the other three alias. - type: entities title: Office Jarvis show_header_toggle: false @@ -30,6 +118,21 @@ views: name: Alarm time - entity: script.test_voice_alarm_office_jarvis name: Test alarm + card_mod: &cc_panel + style: | + ha-card { + border-radius: var(--cc-radius-brand); + box-shadow: var(--cc-shadow-md); + } + ha-card .card-header, + ha-card h1.card-header { + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + font-size: 0.9375rem; + color: var(--cc-text-muted, var(--secondary-text-color)); + padding-bottom: 4px; + } - type: entities title: ReSpeaker Family Room @@ -43,6 +146,7 @@ views: name: Alarm time - entity: script.test_voice_alarm_respeaker_family_room name: Test alarm + card_mod: *cc_panel - type: entities title: ReSpeaker Living Room @@ -56,6 +160,7 @@ views: name: Alarm time - entity: script.test_voice_alarm_respeaker_living_room name: Test alarm + card_mod: *cc_panel - type: entities title: ReSpeaker Master Bedroom @@ -69,3 +174,4 @@ views: name: Alarm time - entity: script.test_voice_alarm_respeaker_master_bedroom name: Test alarm + card_mod: *cc_panel diff --git a/themes/cubecraft.yaml b/themes/cubecraft.yaml new file mode 100644 index 0000000..02e3508 --- /dev/null +++ b/themes/cubecraft.yaml @@ -0,0 +1,305 @@ +# CubeCraft Creations display system — Home Assistant theme +# +# Source of truth for the tokens below is the production card's stylesheet: +# custom_components/cubecraft_production/www/cubecraft-production-card.js +# Keep the two in sync. The card reads HA theme variables for its surfaces, so +# once this theme is active the CubeOps board and the rest of a dashboard end up +# on the same surfaces, borders and text colours automatically. +# +# The theme carries `modes:`, so it follows HA's light/dark setting. The brand +# gradient, type, radii and semantic colours are identical in both modes; only +# the surface and text ramps flip. +# +# TUNED FOR WALL PANELS: `header-height: 0px` near the bottom collapses the HA +# toolbar, matching what dakboard-dark did on these dashboards. If you ever want +# CubeCraft on a phone or desktop where you still need the toolbar, copy this +# file to themes/cubecraft-desktop.yaml, rename the top-level key, and delete +# that one line. (HA themes cannot inherit from each other, and this repo's YAML +# is loaded by HA's own loader, so a `<<:` merge anchor is not worth the risk of +# taking the whole themes include down.) +# +# Hiding the sidebar as well needs the kiosk-mode frontend plugin (HACS), which +# is not currently installed — see dashboards/views/cubeops-dashboard.yaml. + +cubecraft: + # ───────────────────────────────────────────────────────────────────────── + # Brand tokens — mirrors the `:host` block in cubecraft-production-card.js. + # Exposed as --cc-* so dashboard card_mod blocks can reference them by name + # instead of hard-coding hexes in a dozen places. + # ───────────────────────────────────────────────────────────────────────── + cc-blue: "#3B3AB8" + cc-purple: "#A035CC" + cc-gradient: "linear-gradient(135deg, #3B3AB8 0%, #A035CC 100%)" + + cc-primary-50: "#EBEBF8" + cc-primary-500: "#3B3AB8" + cc-primary-700: "#201F7A" + + cc-neutral-0: "#FFFFFF" + cc-neutral-50: "#FAFAFA" + cc-neutral-100: "#F4F4F6" + cc-neutral-150: "#EDEDF1" + cc-neutral-200: "#E4E4EA" + cc-neutral-400: "#9898A8" + cc-neutral-600: "#52525E" + cc-neutral-900: "#18181D" + + cc-success: "#16A34A" + cc-warning: "#D97706" + cc-warning-bg: "#FEF3C7" + cc-error: "#DC2626" + cc-error-bg: "#FEE2E2" + + # 4px spacing scale + cc-space-1: "4px" + cc-space-2: "8px" + cc-space-3: "12px" + cc-space-4: "16px" + cc-space-5: "20px" + cc-space-6: "24px" + cc-space-7: "28px" + + cc-radius-sm: "6px" + cc-radius-md: "8px" + cc-radius-lg: "12px" + cc-radius-xl: "16px" + cc-radius-brand: "20px" + cc-radius-full: "9999px" + + cc-shadow-sm: "0 2px 6px 0 rgba(0,0,0,.08)" + cc-shadow-md: "0 4px 12px 0 rgba(0,0,0,.10)" + cc-shadow-brand: "0 4px 20px 0 rgba(59,58,184,.25)" + + cc-ease: "cubic-bezier(0,0,.2,1)" + cc-dur: "200ms" + + # ───────────────────────────────────────────────────────────────────────── + # Typography — Nunito 400..800. + # The @font-face has to be declared in the document, not a shadow root; that + # is what www/cubecraft/cubecraft-brand-font.js does (wired up via + # frontend.extra_module_url in configuration.yaml). Without it everything + # below falls back to system-ui and the rest of the theme still works. + # ───────────────────────────────────────────────────────────────────────── + cc-font: "'Nunito', 'Nunito Fallback', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" + primary-font-family: "var(--cc-font)" + ha-font-family-body: "var(--cc-font)" + ha-font-family-heading: "var(--cc-font)" + mdc-typography-font-family: "var(--cc-font)" + paper-font-common-base_-_font-family: "var(--cc-font)" + paper-font-common-nowrap_-_font-family: "var(--cc-font)" + paper-font-body1_-_font-family: "var(--cc-font)" + paper-font-subhead_-_font-family: "var(--cc-font)" + paper-font-headline_-_font-family: "var(--cc-font)" + paper-font-title_-_font-family: "var(--cc-font)" + paper-font-caption_-_font-family: "var(--cc-font)" + + # ───────────────────────────────────────────────────────────────────────── + # Shape — brand radius on cards, pills on controls. + # ───────────────────────────────────────────────────────────────────────── + ha-card-border-radius: "var(--cc-radius-brand)" + ha-card-border-width: "0" + ha-dialog-border-radius: "var(--cc-radius-brand)" + ha-badge-border-radius: "var(--cc-radius-full)" + mdc-shape-small: "var(--cc-radius-full)" + mdc-shape-medium: "var(--cc-radius-lg)" + mdc-shape-large: "var(--cc-radius-xl)" + button-border-radius: "var(--cc-radius-full)" + + # ───────────────────────────────────────────────────────────────────────── + # Colour — brand ramp. Surfaces/text live in the mode blocks below. + # ───────────────────────────────────────────────────────────────────────── + success-color: "var(--cc-success)" + warning-color: "var(--cc-warning)" + error-color: "var(--cc-error)" + + label-badge-green: "var(--cc-success)" + label-badge-yellow: "var(--cc-warning)" + label-badge-red: "var(--cc-error)" + + # Text sitting on top of primary-color (buttons, selected states). White + # clears AA against both ends of the gradient (8.5:1 on the blue, 5.4:1 on + # the purple), so it is safe wherever the gradient is used. + text-primary-color: "#FFFFFF" + app-header-text-color: "#FFFFFF" + mdc-theme-on-primary: "#FFFFFF" + + # Wall-panel chrome: collapse the HA toolbar. Delete this line for a variant + # you navigate by hand (see the header comment). + header-height: "0px" + + modes: + # ═══════════════════════════════════════════════════════════════════════ + # Light — the design system as authored: white surfaces, neutral sunken + # backgrounds, near-black text. + # ═══════════════════════════════════════════════════════════════════════ + light: + cc-surface: "var(--cc-neutral-0)" + cc-surface-sunken: "var(--cc-neutral-100)" + cc-text: "var(--cc-neutral-900)" + cc-text-muted: "var(--cc-neutral-600)" + cc-border: "var(--cc-neutral-150)" + cc-accent: "var(--cc-primary-500)" + cc-accent-soft: "var(--cc-primary-50)" + + primary-color: "#3B3AB8" + accent-color: "#A035CC" + dark-primary-color: "#201F7A" + light-primary-color: "#EBEBF8" + info-color: "#3B3AB8" + + primary-text-color: "#18181D" + secondary-text-color: "#52525E" + disabled-text-color: "#9898A8" + + primary-background-color: "#F4F4F6" + secondary-background-color: "#EDEDF1" + card-background-color: "#FFFFFF" + ha-card-background: "#FFFFFF" + divider-color: "#EDEDF1" + ha-card-box-shadow: "0 4px 12px 0 rgba(0,0,0,.10)" + + app-header-background-color: "#3B3AB8" + more-info-header-background: "#F4F4F6" + + sidebar-background-color: "#FFFFFF" + sidebar-text-color: "#52525E" + sidebar-icon-color: "#9898A8" + sidebar-selected-text-color: "#3B3AB8" + sidebar-selected-icon-color: "#3B3AB8" + + state-icon-color: "#3B3AB8" + state-icon-active-color: "#A035CC" + state-unavailable-color: "#9898A8" + paper-item-icon-color: "#9898A8" + paper-item-icon-active-color: "#3B3AB8" + + switch-checked-color: "#3B3AB8" + switch-checked-track-color: "#3B3AB8" + switch-unchecked-color: "#9898A8" + switch-unchecked-track-color: "#E4E4EA" + slider-color: "#3B3AB8" + slider-bar-color: "#E4E4EA" + + label-badge-blue: "#3B3AB8" + label-badge-gray: "#9898A8" + label-badge-text-color: "#18181D" + ha-label-badge-background-color: "#FFFFFF" + ha-label-badge-border-color: "#EDEDF1" + + table-row-background-color: "#FFFFFF" + table-row-alternative-background-color: "#FAFAFA" + markdown-code-background-color: "#F4F4F6" + code-editor-background-color: "#FAFAFA" + + paper-card-background-color: "#FFFFFF" + paper-card-header-color: "#18181D" + paper-dialog-background-color: "#FFFFFF" + paper-listbox-background-color: "#FFFFFF" + input-fill-color: "#F4F4F6" + input-ink-color: "#18181D" + input-label-ink-color: "#52525E" + iron-overlay-backdrop-background-color: "rgba(24,24,29,.32)" + + mdc-theme-primary: "#3B3AB8" + mdc-theme-secondary: "#A035CC" + mdc-theme-surface: "#FFFFFF" + + # Weather glyphs, kept in the brand family rather than default sky blue. + weather-icon-sun-color: "#D97706" + weather-icon-rain-color: "#3B3AB8" + weather-icon-cloud-front-color: "#9898A8" + weather-icon-cloud-back-color: "#E4E4EA" + weather-icon-moon-color: "#52525E" + + # ═══════════════════════════════════════════════════════════════════════ + # Dark — violet-tinted neutrals so the brand gradient sits in the same + # family as the surfaces rather than floating on plain grey. The primary + # is lightened to #8B8AF2 (5.6:1 on the card surface); #3B3AB8 itself is + # too dark to read as an accent here, though it stays correct inside the + # gradient, which is left untouched. + # ═══════════════════════════════════════════════════════════════════════ + dark: + cc-surface: "#1C1C27" + cc-surface-sunken: "#16161F" + cc-text: "#EDEDF1" + cc-text-muted: "#9898A8" + cc-border: "#2A2A38" + cc-accent: "#8B8AF2" + cc-accent-soft: "#252438" + + primary-color: "#8B8AF2" + accent-color: "#C77BEE" + dark-primary-color: "#3B3AB8" + light-primary-color: "#C9C8FA" + info-color: "#8B8AF2" + + primary-text-color: "#EDEDF1" + secondary-text-color: "#9898A8" + disabled-text-color: "#6A6A7C" + + primary-background-color: "#0E0E14" + secondary-background-color: "#16161F" + card-background-color: "#1C1C27" + ha-card-background: "#1C1C27" + divider-color: "#2A2A38" + ha-card-box-shadow: "0 4px 12px 0 rgba(0,0,0,.45)" + + app-header-background-color: "#16161F" + more-info-header-background: "#16161F" + + sidebar-background-color: "#0E0E14" + sidebar-text-color: "#9898A8" + sidebar-icon-color: "#6A6A7C" + sidebar-selected-text-color: "#8B8AF2" + sidebar-selected-icon-color: "#8B8AF2" + + state-icon-color: "#8B8AF2" + state-icon-active-color: "#C77BEE" + state-unavailable-color: "#6A6A7C" + paper-item-icon-color: "#6A6A7C" + paper-item-icon-active-color: "#8B8AF2" + + switch-checked-color: "#8B8AF2" + switch-checked-track-color: "#8B8AF2" + switch-unchecked-color: "#6A6A7C" + switch-unchecked-track-color: "#2A2A38" + slider-color: "#8B8AF2" + slider-bar-color: "#2A2A38" + + # Semantic colours lifted for dark surfaces; the light-mode values are + # too dense to read at a distance on #1C1C27. + success-color: "#4ADE80" + warning-color: "#FBBF24" + error-color: "#F87171" + label-badge-green: "#4ADE80" + label-badge-yellow: "#FBBF24" + label-badge-red: "#F87171" + label-badge-blue: "#8B8AF2" + label-badge-gray: "#6A6A7C" + label-badge-text-color: "#EDEDF1" + ha-label-badge-background-color: "#1C1C27" + ha-label-badge-border-color: "#2A2A38" + + table-row-background-color: "#1C1C27" + table-row-alternative-background-color: "#20202C" + markdown-code-background-color: "#252433" + code-editor-background-color: "#16161F" + + paper-card-background-color: "#1C1C27" + paper-card-header-color: "#EDEDF1" + paper-dialog-background-color: "#1C1C27" + paper-listbox-background-color: "#1C1C27" + input-fill-color: "#252433" + input-ink-color: "#EDEDF1" + input-label-ink-color: "#9898A8" + iron-overlay-backdrop-background-color: "rgba(0,0,0,.6)" + + mdc-theme-primary: "#8B8AF2" + mdc-theme-secondary: "#C77BEE" + mdc-theme-surface: "#1C1C27" + + weather-icon-sun-color: "#FBBF24" + weather-icon-rain-color: "#8B8AF2" + weather-icon-cloud-front-color: "#9898A8" + weather-icon-cloud-back-color: "#3A3A4A" + weather-icon-moon-color: "#C9C8FA" diff --git a/www/cubecraft/cubecraft-brand-font.js b/www/cubecraft/cubecraft-brand-font.js new file mode 100644 index 0000000..1643ce4 --- /dev/null +++ b/www/cubecraft/cubecraft-brand-font.js @@ -0,0 +1,49 @@ +// CubeCraft Creations brand font loader. +// +// The `cubecraft` theme sets Nunito as the font family, but a theme is only a +// bag of CSS variables — it cannot declare an @font-face. The production card +// declares one for itself (ensureBrandFont() in cubecraft-production-card.js), +// but only on the dashboard where that card is mounted, and only inside that +// page's document. Everything else falls back to system-ui. +// +// Loading this as a frontend module puts the declaration in the document head +// on every dashboard, so the whole UI gets Nunito. +// +// The .woff2 itself is NOT vendored here. It is already shipped and served by +// the cubecraft_production integration at STATIC_URL (see const.py: DOMAIN -> +// "/cubecraft_production"), which keeps a single copy of the file and one +// version to bump. If that integration is ever removed the URL 404s, the +// font-family falls through to the stack in the theme, and nothing else breaks. +// +// Wired up by `frontend.extra_module_url` in configuration.yaml. + +const FONT_STYLE_ID = "cubecraft-brand-font"; +const FONT_URL = "/cubecraft_production/nunito.woff2"; + +// extra_module_url modules are evaluated once per page load, but the card may +// have already injected its own copy under a different id — matching on the +// family as well keeps us from stacking duplicate @font-face rules. +function injectBrandFont() { + if (document.getElementById(FONT_STYLE_ID)) return; + if (document.getElementById("cubecraft-production-font")) return; + + const style = document.createElement("style"); + style.id = FONT_STYLE_ID; + // Variable weight range 400..800 — the card uses 400 (body), 600/700 + // (labels, summaries) and 800 (titles, order numbers). + style.textContent = + "@font-face{" + + "font-family:'Nunito';" + + `src:url('${FONT_URL}') format('woff2');` + + "font-weight:400 800;" + + "font-style:normal;" + + "font-display:swap;" + + "}"; + document.head.appendChild(style); +} + +if (document.head) { + injectBrandFont(); +} else { + document.addEventListener("DOMContentLoaded", injectBrandFont, { once: true }); +}