Skip to content

Commit b5d5378

Browse files
authored
Merge pull request #87 from jingle-jew/dev
feat: add camera widgets and refine page creation, touch interactions, and responsive layouts
2 parents 8c7d0f1 + 36a9c57 commit b5d5378

44 files changed

Lines changed: 1664 additions & 223 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev.html

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
.dev-panel button:hover,.dev-panel select:hover{background:rgba(255,255,255,.2)}
1616
.dev-panel select{grid-column:1/-1;width:100%}
1717
@media (max-width:420px){.dev-panel{grid-template-columns:1fr;width:min(260px,calc(100vw - 24px))}.dev-panel button,.dev-panel select{width:100%}}
18-
.dev-weather-panel{position:fixed;z-index:9998;left:50%;bottom:max(18px,env(safe-area-inset-bottom));translate:-50% 0;display:grid;gap:10px;width:min(620px,calc(100vw - 28px));padding:12px;border:1px solid rgba(255,255,255,.16);border-radius:22px;background:rgba(10,10,18,.72);backdrop-filter:blur(24px) saturate(155%);box-shadow:0 22px 70px rgba(0,0,0,.36)}
18+
.dev-weather-tool{position:fixed;z-index:9998;left:max(14px,env(safe-area-inset-left));bottom:max(14px,env(safe-area-inset-bottom));display:grid;justify-items:start;gap:8px}
19+
.dev-weather-tool[hidden]{display:none}
20+
.dev-weather-toggle{appearance:none;display:grid;place-items:center;width:44px;height:44px;border:1px solid rgba(255,255,255,.16);border-radius:50%;padding:0;background:rgba(10,10,18,.68);color:inherit;font:inherit;font-size:20px;cursor:pointer;backdrop-filter:blur(22px) saturate(150%);box-shadow:0 14px 42px rgba(0,0,0,.28)}
21+
.dev-weather-toggle:hover,.dev-weather-toggle[aria-expanded="true"]{background:rgba(255,255,255,.18)}
22+
.dev-weather-panel{position:absolute;left:0;bottom:calc(100% + 8px);display:grid;gap:10px;width:min(620px,calc(100vw - 28px));padding:12px;border:1px solid rgba(255,255,255,.16);border-radius:22px;background:rgba(10,10,18,.72);backdrop-filter:blur(24px) saturate(155%);box-shadow:0 22px 70px rgba(0,0,0,.36)}
1923
.dev-weather-panel[hidden]{display:none}
2024
.dev-weather-panel__title{font-size:12px;font-weight:800;letter-spacing:.02em;opacity:.72}
2125
.dev-weather-panel__controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
@@ -616,8 +620,22 @@
616620
}
617621

618622
function mountDevWeatherPanel() {
623+
let tool = document.querySelector("[data-dev-weather-tool]");
619624
let panel = document.querySelector("[data-dev-weather-panel]");
620-
if (!panel) {
625+
if (!tool) {
626+
tool = document.createElement("div");
627+
tool.className = "dev-weather-tool";
628+
tool.dataset.devWeatherTool = "true";
629+
tool.hidden = true;
630+
631+
const toggle = document.createElement("button");
632+
toggle.type = "button";
633+
toggle.className = "dev-weather-toggle";
634+
toggle.dataset.devWeatherToggle = "true";
635+
toggle.setAttribute("aria-label", "Ouvrir les contrôles météo de développement");
636+
toggle.setAttribute("aria-expanded", "false");
637+
toggle.textContent = "☁";
638+
621639
panel = document.createElement("section");
622640
panel.className = "dev-weather-panel";
623641
panel.dataset.devWeatherPanel = "true";
@@ -666,20 +684,31 @@
666684
controls.append(weatherControl, momentControl);
667685

668686
panel.append(title, controls);
669-
document.body.append(panel);
687+
toggle.addEventListener("click", () => {
688+
panel.hidden = !panel.hidden;
689+
toggle.setAttribute("aria-expanded", String(!panel.hidden));
690+
});
691+
tool.append(panel, toggle);
692+
document.body.append(tool);
670693
}
671694

672695
syncDevWeatherPanelVisibility();
673696
return panel;
674697
}
675698

676699
function syncDevWeatherPanelVisibility() {
700+
const tool = document.querySelector("[data-dev-weather-tool]");
677701
const panel = document.querySelector("[data-dev-weather-panel]");
678-
if (!panel) return;
702+
const toggle = document.querySelector("[data-dev-weather-toggle]");
703+
if (!tool || !panel) return;
679704
const weatherActive = hub.dataset.weatherPageActive === "true"
680705
|| hub.dataset.activePageType === "weather"
681706
|| Boolean(hub.shadowRoot?.querySelector?.('.mha-page-panel[data-page-type="weather"]'));
682-
panel.hidden = !weatherActive;
707+
tool.hidden = !weatherActive;
708+
if (!weatherActive) {
709+
panel.hidden = true;
710+
toggle?.setAttribute("aria-expanded", "false");
711+
}
683712
}
684713

685714
function observeDevWeatherPanel() {

mha-widget-hub.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ constructor(){
271271
setPageCreatorOpen:(open)=>{this._pageCreatorOpen=open;},
272272
getNewPageType:()=>this._newPageType,
273273
setNewPageType:(type)=>{this._newPageType=type;},
274+
getNewPageName:()=>this._newPageName,
275+
setNewPageName:(name)=>{this._newPageName=name;},
274276
getNewPageIcon:()=>this._newPageIcon,
275277
setNewPageIcon:(icon)=>{this._newPageIcon=icon;},
276278
getDockSettingsPageId:()=>this._dockSettingsPageId,

src/admin/entity-permissions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const MHA_ENTITY_DOMAINS = Object.freeze([
55
Object.freeze({ value: "switch", label: "Switches" }),
66
Object.freeze({ value: "input_boolean", label: "Booleans" }),
77
Object.freeze({ value: "button", label: "Buttons" }),
8+
Object.freeze({ value: "camera", label: "Cameras" }),
89
Object.freeze({ value: "weather", label: "Weather" }),
910
Object.freeze({ value: "media_player", label: "Media players" }),
1011
Object.freeze({ value: "climate", label: "Climate" }),

src/core/hub-runtime-defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function createHubRuntimeDefaults() {
4747
_pendingWidgetPlacement: null,
4848
_pageCreatorOpen: false,
4949
_newPageType: "grid",
50+
_newPageName: "",
5051
_newPageIcon: "grid",
5152
_mediaPageSettingsOpen: false,
5253
_dockPosition: "left",

src/i18n/en.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export const en = Object.freeze({
142142
pageCreatorTitle: "New page",
143143
pageCreatorHint: "Choose the kind of page to create.",
144144
pageCreatorTypeLabel: "Page type",
145+
pageCreatorNameLabel: "Name",
146+
pageCreatorNamePlaceholder: "Page name",
145147
pageCreatorIconLabel: "Dock icon",
146148
pageCreatorCreate: "Create page",
147149
addWidget: "Add widget",
@@ -447,6 +449,7 @@ export const en = Object.freeze({
447449
switch: "Switches",
448450
input_boolean: "Booleans",
449451
button: "Buttons",
452+
camera: "Cameras",
450453
weather: "Weather",
451454
media_player: "Media players",
452455
climate: "Climate",
@@ -486,13 +489,22 @@ export const en = Object.freeze({
486489
configureSlider: "Configure slider",
487490
configureToggle: "Configure toggle",
488491
configureButton: "Configure button",
492+
configureCamera: "Configure camera",
493+
refresh: "Refresh",
494+
refreshOptions: Object.freeze({
495+
oneSecond: "Every second",
496+
threeSeconds: "Every 3 seconds",
497+
fiveSeconds: "Every 5 seconds",
498+
onClick: "On click",
499+
}),
489500
configureWeather: "Configure weather",
490501
configureMedia: "Configure media",
491502
configureModesRoutines: "Configure Modes & Routines",
492503
configureLight: "Configure light",
493504
sliderHint: "Choose the action, device, and display name.",
494505
toggleHint: "Choose the device type, entity, and display name.",
495506
buttonHint: "Choose an authorized entity or a Home Assistant service.",
507+
cameraHint: "Choose the camera entity and the name displayed in the header.",
496508
weatherHint: "Choose the authorized weather entity to display.",
497509
mediaHint: "Choose the media player and display name.",
498510
scenesButtonHint: "Configure only this button with a Mode or Routine.",
@@ -505,6 +517,10 @@ export const en = Object.freeze({
505517
device: "Device",
506518
deviceType: "Device type",
507519
entity: "Entity",
520+
cameraEntity: "Camera entity",
521+
displayName: "Display name",
522+
iconSearch: "Search icons",
523+
iconEmpty: "No icons found",
508524
haDomain: "HA domain",
509525
haService: "HA service",
510526
jsonData: "JSON data",
@@ -518,6 +534,7 @@ export const en = Object.freeze({
518534
noWeatherEntity: "No authorized and available weather entity.",
519535
noMediaPlayer: "No authorized and available media player.",
520536
noEntity: "No authorized and available entity.",
537+
noCameraEntity: "No authorized and available camera entity.",
521538
noMediaDevice: "No media device available",
522539
noLight: "No light available",
523540
noSwitch: "No switch available",
@@ -554,6 +571,12 @@ export const en = Object.freeze({
554571
input_boolean: "No boolean available",
555572
}),
556573
}),
574+
camera: Object.freeze({
575+
title: "Camera",
576+
imageAlt: "Camera image",
577+
unavailable: "Camera image unavailable",
578+
refreshNow: "Refresh camera image",
579+
}),
557580
weather: Object.freeze({
558581
title: "Weather",
559582
forecast: "Forecasts",

src/i18n/es.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export const es = Object.freeze({
142142
pageCreatorTitle: "Nueva página",
143143
pageCreatorHint: "Elige el tipo de página que quieres crear.",
144144
pageCreatorTypeLabel: "Tipo de página",
145+
pageCreatorNameLabel: "Nombre",
146+
pageCreatorNamePlaceholder: "Nombre de la página",
145147
pageCreatorIconLabel: "Icono del dock",
146148
pageCreatorCreate: "Crear página",
147149
addWidget: "Añadir widget",
@@ -447,6 +449,7 @@ export const es = Object.freeze({
447449
switch: "Interruptores",
448450
input_boolean: "Booleanos",
449451
button: "Botones",
452+
camera: "Cámaras",
450453
weather: "Clima",
451454
media_player: "Reproductores multimedia",
452455
climate: "Clima interior",
@@ -486,13 +489,22 @@ export const es = Object.freeze({
486489
configureSlider: "Configurar slider",
487490
configureToggle: "Configurar toggle",
488491
configureButton: "Configurar botón",
492+
configureCamera: "Configurar cámara",
493+
refresh: "Actualización",
494+
refreshOptions: Object.freeze({
495+
oneSecond: "Cada segundo",
496+
threeSeconds: "Cada 3 segundos",
497+
fiveSeconds: "Cada 5 segundos",
498+
onClick: "Al hacer clic",
499+
}),
489500
configureWeather: "Configurar clima",
490501
configureMedia: "Configurar media",
491502
configureModesRoutines: "Configurar Modes & Routines",
492503
configureLight: "Configurar luz",
493504
sliderHint: "Elige la acción, el dispositivo y el nombre mostrado.",
494505
toggleHint: "Elige el tipo de dispositivo, la entidad y el nombre mostrado.",
495506
buttonHint: "Elige una entidad autorizada o un servicio de Home Assistant.",
507+
cameraHint: "Elige la entidad de cámara y el nombre mostrado en el encabezado.",
496508
weatherHint: "Elige la entidad de clima autorizada para mostrar.",
497509
mediaHint: "Elige el reproductor multimedia y el nombre mostrado.",
498510
scenesButtonHint: "Configura solo este botón con un Mode o una Routine.",
@@ -505,6 +517,10 @@ export const es = Object.freeze({
505517
device: "Dispositivo",
506518
deviceType: "Tipo de dispositivo",
507519
entity: "Entidad",
520+
cameraEntity: "Entidad de cámara",
521+
displayName: "Nombre mostrado",
522+
iconSearch: "Buscar iconos",
523+
iconEmpty: "No se encontraron iconos",
508524
haDomain: "Dominio HA",
509525
haService: "Servicio HA",
510526
jsonData: "Datos JSON",
@@ -518,6 +534,7 @@ export const es = Object.freeze({
518534
noWeatherEntity: "No hay entidad de clima autorizada y disponible.",
519535
noMediaPlayer: "No hay reproductor multimedia autorizado y disponible.",
520536
noEntity: "No hay entidad autorizada y disponible.",
537+
noCameraEntity: "No hay entidad de cámara autorizada y disponible.",
521538
noMediaDevice: "No hay dispositivo multimedia disponible",
522539
noLight: "No hay luz disponible",
523540
noSwitch: "No hay interruptor disponible",
@@ -554,6 +571,12 @@ export const es = Object.freeze({
554571
input_boolean: "No hay booleano disponible",
555572
}),
556573
}),
574+
camera: Object.freeze({
575+
title: "Cámara",
576+
imageAlt: "Imagen de la cámara",
577+
unavailable: "Imagen de la cámara no disponible",
578+
refreshNow: "Actualizar la imagen de la cámara",
579+
}),
557580
weather: Object.freeze({
558581
title: "Clima",
559582
forecast: "Pronóstico",

src/i18n/fr.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export const fr = Object.freeze({
142142
pageCreatorTitle: "Nouvelle page",
143143
pageCreatorHint: "Choisis le type de page à créer.",
144144
pageCreatorTypeLabel: "Type de page",
145+
pageCreatorNameLabel: "Nom",
146+
pageCreatorNamePlaceholder: "Nom de la page",
145147
pageCreatorIconLabel: "Icône du dock",
146148
pageCreatorCreate: "Créer la page",
147149
addWidget: "Ajouter un widget",
@@ -447,6 +449,7 @@ export const fr = Object.freeze({
447449
switch: "Interrupteurs",
448450
input_boolean: "Booléens",
449451
button: "Boutons",
452+
camera: "Caméras",
450453
weather: "Météo",
451454
media_player: "Lecteurs média",
452455
climate: "Climat",
@@ -486,13 +489,22 @@ export const fr = Object.freeze({
486489
configureSlider: "Configurer le slider",
487490
configureToggle: "Configurer le toggle",
488491
configureButton: "Configurer le bouton",
492+
configureCamera: "Configurer la caméra",
493+
refresh: "Actualisation",
494+
refreshOptions: Object.freeze({
495+
oneSecond: "Chaque seconde",
496+
threeSeconds: "Toutes les 3 secondes",
497+
fiveSeconds: "Toutes les 5 secondes",
498+
onClick: "Au clic",
499+
}),
489500
configureWeather: "Configurer la météo",
490501
configureMedia: "Configurer le média",
491502
configureModesRoutines: "Configurer Modes & Routines",
492503
configureLight: "Configurer la lumière",
493504
sliderHint: "Choisis l’action, l’appareil et le nom affiché.",
494505
toggleHint: "Choisis le type d’appareil, l’entité et le nom affiché.",
495506
buttonHint: "Choisis une entité autorisée ou un service Home Assistant.",
507+
cameraHint: "Choisis l’entité caméra et le nom affiché dans l’en-tête.",
496508
weatherHint: "Choisis l’entité météo autorisée à afficher.",
497509
mediaHint: "Choisis le lecteur média et le nom affiché.",
498510
scenesButtonHint: "Configure seulement ce bouton avec un Mode ou une Routine.",
@@ -505,6 +517,10 @@ export const fr = Object.freeze({
505517
device: "Appareil",
506518
deviceType: "Type d’appareil",
507519
entity: "Entité",
520+
cameraEntity: "Entité caméra",
521+
displayName: "Nom affiché",
522+
iconSearch: "Rechercher une icône",
523+
iconEmpty: "Aucune icône trouvée",
508524
haDomain: "Domaine HA",
509525
haService: "Service HA",
510526
jsonData: "Données JSON",
@@ -518,6 +534,7 @@ export const fr = Object.freeze({
518534
noWeatherEntity: "Aucune entité météo autorisée et disponible.",
519535
noMediaPlayer: "Aucun lecteur média autorisé et disponible.",
520536
noEntity: "Aucune entité autorisée et disponible.",
537+
noCameraEntity: "Aucune entité caméra autorisée et disponible.",
521538
noMediaDevice: "Aucun appareil média disponible",
522539
noLight: "Aucune lumière disponible",
523540
noSwitch: "Aucun interrupteur disponible",
@@ -554,6 +571,12 @@ export const fr = Object.freeze({
554571
input_boolean: "Aucun booléen disponible",
555572
}),
556573
}),
574+
camera: Object.freeze({
575+
title: "Caméra",
576+
imageAlt: "Image de la caméra",
577+
unavailable: "Image de la caméra indisponible",
578+
refreshNow: "Actualiser l’image de la caméra",
579+
}),
557580
weather: Object.freeze({
558581
title: "Météo",
559582
forecast: "Prévisions",

src/layout/grid-runtime.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,21 +181,19 @@ export function calculateGridTrackMetrics({
181181
}
182182

183183
/*
184-
* Tablet/desktop keep the shell-owned panel/container rectangle, but tracks
185-
* should stay square or close to square. Any excess space remains in the
186-
* panel as breathing room instead of stretching widgets vertically or
187-
* horizontally.
184+
* Tablet/desktop keep horizontal and vertical sizing independent.
185+
*
186+
* A status-bar or another vertical reservation changes the available
187+
* height, but it must not feed back into column width. The grid therefore
188+
* continues to fill the same horizontal panel rectangle while row height
189+
* adapts to the remaining vertical space. Only excessive vertical stretch
190+
* is capped relative to the width-derived column size.
188191
*/
189192
const safeAspectRatio = Math.max(1, Number(maxTrackAspectRatio) || 1);
190-
const minUnit = Math.max(1, Math.min(unitX, unitY));
191-
const maxAllowedUnit = minUnit * safeAspectRatio;
192-
const columnSize = Math.max(
193-
1,
194-
unitX <= unitY ? unitX : Math.min(unitX, maxAllowedUnit),
195-
);
193+
const columnSize = Math.max(1, unitX);
196194
const rowSize = Math.max(
197195
1,
198-
unitY <= unitX ? unitY : Math.min(unitY, maxAllowedUnit),
196+
Math.min(unitY, columnSize * safeAspectRatio),
199197
);
200198
if (!Number.isFinite(columnSize) || !Number.isFinite(rowSize)) return null;
201199

src/layout/responsive-dock-coordinator.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,13 @@ export function createResponsiveDockCoordinator(host) {
247247
const widgetArea = grid.closest(".mha-widget-area");
248248
const isMobileLayout = () => isMobileLauncherLayout();
249249
const isLandscape = () => isMobileLandscapeLayout();
250+
const isWeatherBottomDockLayout = () => (
251+
!isMobileLayout()
252+
&& host.dataset?.activePageType === "weather"
253+
&& host.dataset?.dockPosition === "bottom"
254+
);
250255

251-
if (!widgetArea || !isMobileLayout()) return;
256+
if (!widgetArea || (!isMobileLayout() && !isWeatherBottomDockLayout())) return;
252257
if (isLandscape()) {
253258
host.classList.add("is-mobile-floating-controls-hidden");
254259
return;
@@ -268,11 +273,14 @@ export function createResponsiveDockCoordinator(host) {
268273
syncStatusBarFillScrollState(previousScrollTop > 4);
269274
const threshold = 10;
270275
const onScroll = () => {
271-
if (!isMobileLayout() || isLandscape()) {
276+
const mobileLayout = isMobileLayout();
277+
const weatherBottomDockLayout = isWeatherBottomDockLayout();
278+
if ((!mobileLayout && !weatherBottomDockLayout) || isLandscape()) {
272279
host.classList.toggle(
273280
"is-mobile-floating-controls-hidden",
274-
isMobileLayout() && isLandscape(),
281+
mobileLayout && isLandscape(),
275282
);
283+
host.classList.remove("is-dock-hidden");
276284
syncStatusBarFillScrollState(false);
277285
previousScrollTop = getScrollTop();
278286
return;
@@ -281,14 +289,19 @@ export function createResponsiveDockCoordinator(host) {
281289
const currentScrollTop = getScrollTop();
282290
syncStatusBarFillScrollState(currentScrollTop > 4);
283291
if (currentScrollTop <= 4) {
284-
host.classList.remove("is-mobile-floating-controls-hidden");
292+
host.classList.remove("is-mobile-floating-controls-hidden", "is-dock-hidden");
285293
previousScrollTop = currentScrollTop;
286294
return;
287295
}
288296

289297
const delta = currentScrollTop - previousScrollTop;
290-
if (delta > threshold) host.classList.add("is-mobile-floating-controls-hidden");
291-
else if (delta < -threshold) host.classList.remove("is-mobile-floating-controls-hidden");
298+
if (delta > threshold) {
299+
if (weatherBottomDockLayout) host.classList.add("is-dock-hidden");
300+
else host.classList.add("is-mobile-floating-controls-hidden");
301+
} else if (delta < -threshold) {
302+
if (weatherBottomDockLayout) host.classList.remove("is-dock-hidden");
303+
else host.classList.remove("is-mobile-floating-controls-hidden");
304+
}
292305

293306
if (Math.abs(delta) > threshold) previousScrollTop = currentScrollTop;
294307
};

0 commit comments

Comments
 (0)