Compare commits

...

10 Commits

Author SHA1 Message Date
Joshua King f8dacd3122 Add CORS allowed origins for HTTP configuration 2026-08-23 11:40:45 -04:00
Joshua King 6e1b7eab74 Use Daylight Calendar Card on the Family Hub dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 15:19:15 -04:00
Joshua King f0af1eacfa Show Mon–Fri school lunch menus in a full-width panel layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 14:47:55 -04:00
Joshua King 1ee7f0c739 Add lunch menus dashboard with week-view school calendars.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 14:13:55 -04:00
Joshua King 8f7bae56db Pin trusted_networks auto-login to the kiosk user. 2026-08-08 22:03:31 -04:00
Joshua King 962628912d Put trusted_networks before homeassistant so login bypass can run. 2026-08-08 21:54:31 -04:00
Joshua King 0bff60aa45 Add trusted_networks auth provider for local LAN access. 2026-08-08 21:37:12 -04:00
root 1d6dce7b42 Correct the Gitea host in docs: cubecraftcreations -> cubecraftlabs
Both CLAUDE.md and print_log_context.md named the mirror as
code.cubecraftcreations.com. The actual `origin` is
https://code.cubecraftlabs.com/overseer/Home-Assistant.git, and the
documented host returns 404 for that repo path, so anyone following the
docs to find or clone the mirror was sent to the wrong server.

Record the full origin URL in CLAUDE.md rather than just the hostname, so
the next reader does not have to infer the path.

Note this is only about the git host. cubecraftcreations.com is used
elsewhere in this setup and is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 13:19:58 -04:00
root 9e061b7078 Fix lovelace url_path: cubeops -> cube-ops
HA requires a dashboard's url_path to contain a hyphen, and a key that
fails that check invalidates the entire `lovelace:` block rather than
just its own entry. `cubeops:` was added in 0c35647, so `ha core check`
has been failing since then and all four YAML dashboards -- Voice Alarms,
Family Hub, At-a-Glance and CubeOps -- were refusing to load.

Rename the key to `cube-ops`, and rename the two files alongside it so
they match, which is the convention the other three already follow
(voice-alarms -> voice-alarms.yaml).

This changes the kiosk URL to https://home.vault983.com/cube-ops. Any
wall display still pointed at /cubeops needs updating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:41:17 -04:00
root 92b68ca21a 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 <noreply@anthropic.com>
2026-07-27 11:27:35 -04:00
15 changed files with 972 additions and 67 deletions
+10 -1
View File
@@ -112,7 +112,16 @@ home-assistant_v2.db-wal
# Dependencies and cache # Dependencies and cache
deps/ deps/
tts/ 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/ custom_components/
image/ image/
ms365_storage/ ms365_storage/
+1 -1
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What this repo is ## What this repo is
The Home Assistant configuration directory (`/config` on the live HAOS host, mirrored to this git repo). It is **configuration, not application code** — there is no build or test suite. "Running" a change means HA loads it; validation is by config-check and reload, not unit tests. The repo is also mirrored to a self-hosted Gitea at `code.cubecraftcreations.com`. The Home Assistant configuration directory (`/config` on the live HAOS host, mirrored to this git repo). It is **configuration, not application code** — there is no build or test suite. "Running" a change means HA loads it; validation is by config-check and reload, not unit tests. The repo is also mirrored to a self-hosted Gitea at `code.cubecraftlabs.com` (`origin` is `https://code.cubecraftlabs.com/overseer/Home-Assistant.git`).
Edits here only take effect after they reach the live host and HA reloads the affected domain. There is no compile step; a malformed YAML file breaks the domain it belongs to (or all of HA if it's `configuration.yaml`). Edits here only take effect after they reach the live host and HA reloads the affected domain. There is no compile step; a malformed YAML file breaks the domain it belongs to (or all of HA if it's `configuration.yaml`).
+18
View File
@@ -1,5 +1,15 @@
homeassistant: homeassistant:
packages: !include_dir_named packages/ packages: !include_dir_named packages/
auth_providers:
- type: trusted_networks
trusted_networks:
- 192.168.69.0/24
- 10.60.1.0/24
trusted_users:
192.168.69.0/24: 66e8deaeb6944b65a6ed652d671599e2
10.60.1.0/24: 66e8deaeb6944b65a6ed652d671599e2
allow_bypass_login: true
- type: homeassistant
# Loads default set of integrations. Do not remove. # Loads default set of integrations. Do not remove.
default_config: default_config:
@@ -7,6 +17,12 @@ default_config:
# Load frontend themes from the themes folder # Load frontend themes from the themes folder
frontend: frontend:
themes: !include_dir_merge_named themes 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: lovelace:
dashboards: !include dashboards/dashboards.yaml dashboards: !include dashboards/dashboards.yaml
@@ -28,6 +44,8 @@ http:
use_x_forwarded_for: true use_x_forwarded_for: true
trusted_proxies: trusted_proxies:
- 10.60.1.1 - 10.60.1.1
cors_allowed_origins:
- https://dakboard.com
### notify_events ### ### notify_events ###
notify_events: notify_events:
@@ -2,4 +2,4 @@ mode: yaml
title: CubeOps title: CubeOps
icon: mdi:alarm-panel icon: mdi:alarm-panel
show_in_sidebar: true show_in_sidebar: true
filename: dashboards/views/cubeops-dashboard.yaml filename: dashboards/views/cube-ops-dashboard.yaml
+6 -1
View File
@@ -1,4 +1,9 @@
voice-alarms: !include voice-alarms.yaml voice-alarms: !include voice-alarms.yaml
family-hub: !include family-hub.yaml family-hub: !include family-hub.yaml
at-a-glance: !include at-a-glance.yaml at-a-glance: !include at-a-glance.yaml
cubeops: !include cubeops.yaml lunch-menus: !include lunch-menus.yaml
# Each key is the dashboard's url_path, and HA requires it to contain a hyphen
# (see homeassistant/components/lovelace: url_path validation). A hyphen-less
# key fails validation for the whole `lovelace:` block, not just its own entry,
# which takes every dashboard here down with it.
cube-ops: !include cube-ops.yaml
+5
View File
@@ -0,0 +1,5 @@
mode: yaml
title: Lunch Menus
icon: mdi:food-apple
show_in_sidebar: true
filename: dashboards/views/lunch-menus-dashboard.yaml
+118 -17
View File
@@ -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 title: At-a-Glance
theme: dakboard-dark theme: cubecraft
views: views:
- title: At-a-Glance - title: At-a-Glance
path: at-a-glance path: at-a-glance
icon: mdi:eye-outline icon: mdi:eye-outline
theme: dakboard-dark theme: cubecraft
type: masonry type: masonry
cards: 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 - type: markdown
content: | content: |
# {{ now().strftime("%A, %B %d, %Y") }} # {{ now().strftime("%A, %B %d, %Y") }}
@@ -15,21 +35,28 @@ views:
card_mod: card_mod:
style: | style: |
ha-card { 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; text-align: center;
background: none;
box-shadow: none;
padding: 0 0 4px 0;
} }
ha-card h1 { ha-card h1 {
font-size: 1.2em;
margin: 0; margin: 0;
font-weight: 300; font-size: 1rem;
color: var(--secondary-text-color); font-weight: 600;
letter-spacing: 0.02em;
opacity: 0.85;
color: #fff;
} }
ha-card h2 { ha-card h2 {
font-size: 2em;
margin: 2px 0 0 0; margin: 2px 0 0 0;
font-weight: 400; font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.01em;
color: #fff;
} }
# ── Weather Now ── # ── Weather Now ──
@@ -41,127 +68,200 @@ views:
round_temperature: true round_temperature: true
# ── Indoor Climate ── # ── 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 - type: heading
heading: Indoor Climate heading: Indoor Climate
heading_style: title heading_style: title
icon: mdi:home-thermometer-outline 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 - type: grid
columns: 3 columns: 3
square: false square: false
cards: cards:
# First use of the `article` treatment; aliased as *cc_tile below.
- type: tile - type: tile
entity: sensor.living_room_temperature entity: sensor.living_room_temperature
name: Living Room name: Living Room
icon: mdi:sofa 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 - type: tile
entity: sensor.office_temperature entity: sensor.office_temperature
name: Office name: Office
icon: mdi:desk icon: mdi:desk
color: teal color: primary
card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.bedroom_temperature entity: sensor.bedroom_temperature
name: Master Bedroom name: Master Bedroom
icon: mdi:bed icon: mdi:bed
color: purple color: primary
card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.upstairs_temperature entity: sensor.upstairs_temperature
name: Upstairs name: Upstairs
icon: mdi:stairs icon: mdi:stairs
color: indigo color: primary
card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.blink_family_room_temperature entity: sensor.blink_family_room_temperature
name: Family Room name: Family Room
icon: mdi:television icon: mdi:television
color: red color: primary
card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.blink_kitchen_temperature entity: sensor.blink_kitchen_temperature
name: Kitchen name: Kitchen
icon: mdi:stove icon: mdi:stove
color: deep-orange color: primary
card_mod: *cc_tile
# ── 3D Printer Status ── # ── 3D Printer Status ──
- type: heading - type: heading
heading: 3D Printers heading: 3D Printers
heading_style: title heading_style: title
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
card_mod: *cc_section
- type: grid - type: grid
columns: 2 columns: 2
square: false square: false
cards: cards:
# Online/offline is a genuine success signal, so these keep green and
# take the success-coloured left rule.
- type: tile - type: tile
entity: binary_sensor.p1s_01p09c470102673_online entity: binary_sensor.p1s_01p09c470102673_online
name: PrintsCharming name: PrintsCharming
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
show_entity_picture: false show_entity_picture: false
color: green 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 - type: tile
entity: binary_sensor.a1_03919d540806387_online entity: binary_sensor.a1_03919d540806387_online
name: Ds-Turd-Flinger name: Ds-Turd-Flinger
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
show_entity_picture: false show_entity_picture: false
color: green color: green
card_mod: *cc_tile_success
- type: tile - type: tile
entity: binary_sensor.a1mini_0309ca580403653_online entity: binary_sensor.a1mini_0309ca580403653_online
name: Polly-Pocket name: Polly-Pocket
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
show_entity_picture: false show_entity_picture: false
color: green color: green
card_mod: *cc_tile_success
- type: tile - type: tile
entity: binary_sensor.constipation_orion_online entity: binary_sensor.constipation_orion_online
name: Constipation-Orion name: Constipation-Orion
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
show_entity_picture: false show_entity_picture: false
color: green color: green
card_mod: *cc_tile_success
- type: tile - type: tile
entity: binary_sensor.jordyns_layer_slayer_online entity: binary_sensor.jordyns_layer_slayer_online
name: Jordyn's Layer Slayer name: Jordyn's Layer Slayer
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
show_entity_picture: false show_entity_picture: false
color: green color: green
card_mod: *cc_tile_success
- type: tile - type: tile
entity: binary_sensor.centauri_carbon_sdcp_status entity: binary_sensor.centauri_carbon_sdcp_status
name: Centauri Carbon name: Centauri Carbon
icon: mdi:printer-3d-nozzle icon: mdi:printer-3d-nozzle
show_entity_picture: false show_entity_picture: false
color: green color: green
card_mod: *cc_tile_success
# ── Litter Boxes ── # ── Litter Boxes ──
- type: heading - type: heading
heading: Litter Boxes heading: Litter Boxes
heading_style: title heading_style: title
icon: mdi:cat icon: mdi:cat
card_mod: *cc_section
- type: grid - type: grid
columns: 2 columns: 2
square: false square: false
cards: 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 - type: tile
entity: binary_sensor.cat_crapper_3000_wastebin_filled entity: binary_sensor.cat_crapper_3000_wastebin_filled
name: Cat Crapper 3000 name: Cat Crapper 3000
icon: mdi:delete-empty icon: mdi:delete-empty
show_entity_picture: false show_entity_picture: false
color: red 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 - type: tile
entity: binary_sensor.cat_crapper_3000_sand_lack entity: binary_sensor.cat_crapper_3000_sand_lack
name: CC3000 Litter name: CC3000 Litter
icon: mdi:grain icon: mdi:grain
show_entity_picture: false show_entity_picture: false
color: amber 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 - type: tile
entity: binary_sensor.poop_box_wastebin_filled entity: binary_sensor.poop_box_wastebin_filled
name: Poop Box name: Poop Box
icon: mdi:delete-empty icon: mdi:delete-empty
show_entity_picture: false show_entity_picture: false
color: red color: red
card_mod: *cc_tile_alert
- type: tile - type: tile
entity: binary_sensor.triple_threat_wastebin_filled entity: binary_sensor.triple_threat_wastebin_filled
name: Triple Threat name: Triple Threat
icon: mdi:delete-empty icon: mdi:delete-empty
show_entity_picture: false show_entity_picture: false
color: red color: red
card_mod: *cc_tile_alert
# ── Security ── # ── Security ──
- type: heading - type: heading
@@ -181,6 +281,7 @@ views:
- type: entity - type: entity
entity: alarm_control_panel.blink_rear entity: alarm_control_panel.blink_rear
name: Rear name: Rear
card_mod: *cc_section
# ── Recent Activity ── # ── Recent Activity ──
- type: logbook - type: logbook
@@ -1,10 +1,12 @@
# CubeOps — Cubecraft Production workboard # CubeOps — Cubecraft Production workboard
# #
# Registered as the `cubeops` dashboard in dashboards/dashboards.yaml (mode: # Registered as the `cube-ops` dashboard in dashboards/dashboards.yaml (mode:
# yaml), so this file is the source of truth. Do not edit it through HA's raw # yaml), so this file is the source of truth. Do not edit it through HA's raw
# configuration editor — that writes to storage and cannot come back to git. # configuration editor — that writes to storage and cannot come back to git.
# #
# Kiosk URL: https://home.vault983.com/cubeops # Kiosk URL: https://home.vault983.com/cube-ops
# (Renamed from /cubeops — HA rejects a url_path with no hyphen. Any wall
# display still pointed at the old URL needs updating.)
# #
# The card polls every 30 seconds, so a display left open stays current and # The card polls every 30 seconds, so a display left open stays current and
# picks back up when a kiosk rotation returns to it. # picks back up when a kiosk rotation returns to it.
@@ -13,12 +15,21 @@
# and registers its own Lovelace resource, so there is nothing to add under # and registers its own Lovelace resource, so there is nothing to add under
# Settings > Dashboards > Resources. # Settings > Dashboards > Resources.
# kiosk-mode plugin (HACS > Frontend). Without it the board still fills the # kiosk-mode plugin (HACS > Frontend). NOT currently installed — there is no
# screen; you just keep HA's header and sidebar. # 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: kiosk_mode:
hide_header: true hide_header: true
hide_sidebar: 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: views:
- title: Production - title: Production
# Panel mode gives the card the whole viewport with no dashboard padding. # Panel mode gives the card the whole viewport with no dashboard padding.
+96 -39
View File
@@ -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 title: Family Hub
theme: dakboard-dark theme: cubecraft
views: views:
- title: Family Hub - title: Family Hub
path: family-hub path: family-hub
icon: mdi:home-heart icon: mdi:home-heart
theme: dakboard-dark theme: cubecraft
type: masonry type: masonry
cards: 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 - type: markdown
content: | content: |
# {{ now().strftime("%A, %B %d") }} # {{ now().strftime("%A, %B %d") }}
{% set h = now().hour %} {% 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: card_mod:
style: | style: |
ha-card { 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; text-align: center;
background: none;
box-shadow: none;
padding: 0 0 8px 0;
} }
ha-card h1 { 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; 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 ── # ── 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 - type: clock
clock_size: large clock_size: large
clock_style: analog clock_style: analog
@@ -48,6 +65,7 @@ views:
style: | style: |
ha-card { ha-card {
background: none; background: none;
border: none;
box-shadow: none; box-shadow: none;
text-align: center; text-align: center;
} }
@@ -62,20 +80,18 @@ views:
round_temperature: false round_temperature: false
# ── Family Calendar ── # ── Family Calendar ──
- type: calendar # Daylight Calendar Card (HACS: daylight-calendar-card). Agenda mirrors
# the old stock listWeek “upcoming” feel on a wall panel.
- type: custom:daylight-calendar-card
title: Upcoming Events title: Upcoming Events
initial_view: listWeek default_view: agenda
color_scheme: auto
preference_storage_key: family_hub_calendar
entities: entities:
- calendar.family_events - calendar.family_events
- calendar.birthdays - calendar.birthdays
- calendar.holidays_in_united_states - calendar.holidays_in_united_states
- calendar.gmail_calendar - calendar.gmail_calendar
card_mod:
style: |
ha-card {
--calendar-day-color: var(--primary-text-color);
--calendar-day-weekday-color: var(--secondary-text-color);
}
# ── Quick Glance ── # ── Quick Glance ──
- type: heading - type: heading
@@ -92,64 +108,104 @@ views:
- type: entity - type: entity
entity: sensor.office_temperature entity: sensor.office_temperature
name: Office 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 - type: grid
columns: 3 columns: 3
square: false square: false
cards: 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 - type: tile
entity: sensor.openweathermap_temperature entity: sensor.openweathermap_temperature
name: Outside name: Outside
icon: mdi:thermometer icon: mdi:thermometer
show_entity_picture: false show_entity_picture: false
color: blue color: primary
# Living Room 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 - type: tile
entity: sensor.living_room_temperature entity: sensor.living_room_temperature
name: Living Room name: Living Room
icon: mdi:sofa icon: mdi:sofa
show_entity_picture: false show_entity_picture: false
color: amber color: primary
# Office card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.office_temperature entity: sensor.office_temperature
name: Office name: Office
icon: mdi:desk icon: mdi:desk
show_entity_picture: false show_entity_picture: false
color: teal color: primary
# Master Bedroom card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.bedroom_temperature entity: sensor.bedroom_temperature
name: Master Bedroom name: Master Bedroom
icon: mdi:bed icon: mdi:bed
show_entity_picture: false show_entity_picture: false
color: purple color: primary
# Upstairs card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.upstairs_temperature entity: sensor.upstairs_temperature
name: Upstairs name: Upstairs
icon: mdi:stairs icon: mdi:stairs
show_entity_picture: false show_entity_picture: false
color: indigo color: primary
# Family Room (Blink) card_mod: *cc_tile
- type: tile - type: tile
entity: sensor.blink_family_room_temperature entity: sensor.blink_family_room_temperature
name: Family Room name: Family Room
icon: mdi:television icon: mdi:television
show_entity_picture: false show_entity_picture: false
color: red color: primary
card_mod: *cc_tile
# ── Shopping List ── # ── Shopping List ──
- type: heading
heading: Shopping List
heading_style: title
icon: mdi:cart-outline
card_mod: *cc_section
- type: todo-list - type: todo-list
entity: todo.shopping_list entity: todo.shopping_list
title: Shopping List
hide_completed: true hide_completed: true
display_order: none display_order: none
# ── Chore Tracker Quick View ── # ── Chore Tracker Quick View ──
- type: heading
heading: Chores Today
heading_style: title
icon: mdi:check-circle-outline
card_mod: *cc_section
- type: entities - type: entities
title: Chores Today
show_header_toggle: false show_header_toggle: false
entities: entities:
- entity: binary_sensor.cat_medication_tracker_all_cats_medicated - entity: binary_sensor.cat_medication_tracker_all_cats_medicated
@@ -171,3 +227,4 @@ views:
- type: entity - type: entity
entity: binary_sensor.upstairs_occupancy entity: binary_sensor.upstairs_occupancy
name: Upstairs name: Upstairs
card_mod: *cc_section
+177
View File
@@ -0,0 +1,177 @@
# Lunch Menus — school cafeteria calendars (MonFri each)
#
# Panel layout so the title spans the full width and the three school cards
# sit in one row. Menus come from packages/lunch_menus.yaml (stock calendar
# cards cannot limit to a school-week window).
title: Lunch Menus
theme: cubecraft
views:
- title: Lunch Menus
path: lunch-menus
icon: mdi:food-apple
theme: cubecraft
type: panel
cards:
- type: vertical-stack
cards:
- type: markdown
content: |
{% set wd = now().weekday() %}
{% if wd >= 5 %}
{% set monday = today_at() + timedelta(days=(7 - wd)) %}
{% else %}
{% set monday = today_at() - timedelta(days=wd) %}
{% endif %}
{% set friday = monday + timedelta(days=4) %}
# School Lunch Menus
## {{ monday.strftime("%b %-d") }} {{ friday.strftime("%b %-d, %Y") }}
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;
}
ha-card h1 {
margin: 0;
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.5rem;
font-weight: 800;
letter-spacing: -0.01em;
color: #fff;
}
- type: grid
columns: 3
square: false
cards:
- type: markdown
title: BHS Lunch Menu
entity_id: sensor.bhs_lunch_menu_week
content: |
{% set events = state_attr('sensor.bhs_lunch_menu_week', 'events') or [] %}
{% if events is string %}{% set events = [] %}{% endif %}
{% set wd = now().weekday() %}
{% if wd >= 5 %}
{% set monday = today_at() + timedelta(days=(7 - wd)) %}
{% else %}
{% set monday = today_at() - timedelta(days=wd) %}
{% endif %}
{% for offset in range(5) %}
{% set d = (monday + timedelta(days=offset)).date() %}
{% set d_str = d.isoformat() %}
{% set ns = namespace(found=false) %}
### {{ d.strftime("%A") }}
{{ d.strftime("%b %-d") }}
{% for e in events if e.start is defined and e.start[:10] == d_str %}
{% set ns.found = true %}
- {{ e.summary }}
{% endfor %}
{% if not ns.found %}
_No menu listed_
{% endif %}
{% endfor %}
card_mod: &cc_menu
style: |
ha-card .card-header {
color: var(--cc-text, var(--primary-text-color));
font-weight: 700;
}
ha-markdown {
padding-top: 0 !important;
}
ha-markdown h3 {
margin: 0.85rem 0 0.15rem 0;
font-size: 1rem;
font-weight: 700;
color: var(--cc-accent, var(--primary-color));
}
ha-markdown h3:first-child {
margin-top: 0;
}
ha-markdown p {
margin: 0 0 0.35rem 0;
color: var(--cc-text-muted, var(--secondary-text-color));
font-size: 0.85rem;
}
ha-markdown ul {
margin: 0 0 0.5rem 0;
padding-left: 1.1rem;
}
ha-markdown li {
margin: 0.15rem 0;
color: var(--cc-text, var(--primary-text-color));
}
ha-markdown em {
color: var(--cc-text-muted, var(--secondary-text-color));
}
- type: markdown
title: BIS School Lunches
entity_id: sensor.bis_lunch_menu_week
content: |
{% set events = state_attr('sensor.bis_lunch_menu_week', 'events') or [] %}
{% if events is string %}{% set events = [] %}{% endif %}
{% set wd = now().weekday() %}
{% if wd >= 5 %}
{% set monday = today_at() + timedelta(days=(7 - wd)) %}
{% else %}
{% set monday = today_at() - timedelta(days=wd) %}
{% endif %}
{% for offset in range(5) %}
{% set d = (monday + timedelta(days=offset)).date() %}
{% set d_str = d.isoformat() %}
{% set ns = namespace(found=false) %}
### {{ d.strftime("%A") }}
{{ d.strftime("%b %-d") }}
{% for e in events if e.start is defined and e.start[:10] == d_str %}
{% set ns.found = true %}
- {{ e.summary }}
{% endfor %}
{% if not ns.found %}
_No menu listed_
{% endif %}
{% endfor %}
card_mod: *cc_menu
- type: markdown
title: HMS Lunch Menu
entity_id: sensor.hms_lunch_menu_week
content: |
{% set events = state_attr('sensor.hms_lunch_menu_week', 'events') or [] %}
{% if events is string %}{% set events = [] %}{% endif %}
{% set wd = now().weekday() %}
{% if wd >= 5 %}
{% set monday = today_at() + timedelta(days=(7 - wd)) %}
{% else %}
{% set monday = today_at() - timedelta(days=wd) %}
{% endif %}
{% for offset in range(5) %}
{% set d = (monday + timedelta(days=offset)).date() %}
{% set d_str = d.isoformat() %}
{% set ns = namespace(found=false) %}
### {{ d.strftime("%A") }}
{{ d.strftime("%b %-d") }}
{% for e in events if e.start is defined and e.start[:10] == d_str %}
{% set ns.found = true %}
- {{ e.summary }}
{% endfor %}
{% if not ns.found %}
_No menu listed_
{% endif %}
{% endfor %}
card_mod: *cc_menu
+108 -2
View File
@@ -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 title: Voice Alarms
theme: cubecraft
views: views:
- title: Voice Alarms - title: Voice Alarms
path: voice-alarms path: voice-alarms
icon: mdi:alarm-panel icon: mdi:alarm-panel
theme: cubecraft
type: panel type: panel
cards: cards:
- type: vertical-stack - type: vertical-stack
cards: cards:
# ── Brand title bar ──
- type: markdown - type: markdown
content: | content: |
# Voice Alarms # 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 - type: markdown
Jarvis and ReSpeaker satellites. 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 - type: grid
columns: 2 columns: 2
square: false square: false
cards: 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 - type: entities
title: Office Jarvis title: Office Jarvis
show_header_toggle: false show_header_toggle: false
@@ -30,6 +118,21 @@ views:
name: Alarm time name: Alarm time
- entity: script.test_voice_alarm_office_jarvis - entity: script.test_voice_alarm_office_jarvis
name: Test alarm 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 - type: entities
title: ReSpeaker Family Room title: ReSpeaker Family Room
@@ -43,6 +146,7 @@ views:
name: Alarm time name: Alarm time
- entity: script.test_voice_alarm_respeaker_family_room - entity: script.test_voice_alarm_respeaker_family_room
name: Test alarm name: Test alarm
card_mod: *cc_panel
- type: entities - type: entities
title: ReSpeaker Living Room title: ReSpeaker Living Room
@@ -56,6 +160,7 @@ views:
name: Alarm time name: Alarm time
- entity: script.test_voice_alarm_respeaker_living_room - entity: script.test_voice_alarm_respeaker_living_room
name: Test alarm name: Test alarm
card_mod: *cc_panel
- type: entities - type: entities
title: ReSpeaker Master Bedroom title: ReSpeaker Master Bedroom
@@ -69,3 +174,4 @@ views:
name: Alarm time name: Alarm time
- entity: script.test_voice_alarm_respeaker_master_bedroom - entity: script.test_voice_alarm_respeaker_master_bedroom
name: Test alarm name: Test alarm
card_mod: *cc_panel
+62
View File
@@ -0,0 +1,62 @@
################################################################################
# lunch_menus.yaml
#
# Pulls MonFri of the current school week from each lunch Google Calendar into
# template sensors. On Sat/Sun, advances to next week's Monday. The Lunch Menus
# dashboard renders those attributes — the stock calendar card has no school-
# week view.
################################################################################
template:
- trigger:
- platform: time_pattern
minutes: "/15"
- platform: homeassistant
event: start
- platform: event
event_type: event_template_reloaded
action:
- variables:
# weekday(): Mon=0 … Sun=6. Weekend → next Monday.
school_monday: >-
{% set wd = now().weekday() %}
{% if wd >= 5 %}
{{ (today_at() + timedelta(days=(7 - wd))).isoformat() }}
{% else %}
{{ (today_at() - timedelta(days=wd)).isoformat() }}
{% endif %}
- action: calendar.get_events
target:
entity_id:
- calendar.bhs_lunch_menu
- calendar.bis_school_lunches
- calendar.hms_lunch_menu
data:
start_date_time: "{{ school_monday }}"
duration:
days: 5
response_variable: agenda
sensor:
- name: "BHS Lunch Menu Week"
unique_id: bhs_lunch_menu_week
icon: mdi:food-apple
state: "{{ agenda['calendar.bhs_lunch_menu'].events | count }}"
attributes:
events: "{{ agenda['calendar.bhs_lunch_menu'].events }}"
week_start: "{{ school_monday }}"
- name: "BIS Lunch Menu Week"
unique_id: bis_lunch_menu_week
icon: mdi:food-apple
state: "{{ agenda['calendar.bis_school_lunches'].events | count }}"
attributes:
events: "{{ agenda['calendar.bis_school_lunches'].events }}"
week_start: "{{ school_monday }}"
- name: "HMS Lunch Menu Week"
unique_id: hms_lunch_menu_week
icon: mdi:food-apple
state: "{{ agenda['calendar.hms_lunch_menu'].events | count }}"
attributes:
events: "{{ agenda['calendar.hms_lunch_menu'].events }}"
week_start: "{{ school_monday }}"
+1 -1
View File
@@ -55,6 +55,6 @@ The finish automation condition checks `| length == 36` on the stored ID.
## HA Setup ## HA Setup
- HAOS on x86 server - HAOS on x86 server
- Gitea repo at code.cubecraftcreations.com (username: overseer) - Gitea repo at code.cubecraftlabs.com (username: overseer)
- IoT VLAN: 192.168.69.x, Main: 192.168.1.x - IoT VLAN: 192.168.69.x, Main: 192.168.1.x
- ~150 devices, UniFi networking - ~150 devices, UniFi networking
+305
View File
@@ -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/cube-ops-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"
+49
View File
@@ -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 });
}