myio-js-library 0.1.557 → 0.1.559

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -908,7 +908,7 @@ module.exports = __toCommonJS(src_exports);
908
908
  // package.json
909
909
  var package_default = {
910
910
  name: "myio-js-library",
911
- version: "0.1.557",
911
+ version: "0.1.559",
912
912
  description: "A clean, standalone JS SDK for MYIO projects",
913
913
  license: "MIT",
914
914
  repository: "github:gh-myio/myio-js-library",
@@ -12855,6 +12855,26 @@ var CSS_STRING = `
12855
12855
  margin-left: 4px;
12856
12856
  }
12857
12857
 
12858
+ /* Consumo ainda carregando (spinner no lugar do valor) */
12859
+ .myio-ho-card__value.is-loading .num {
12860
+ display: inline-flex;
12861
+ align-items: center;
12862
+ gap: 8px;
12863
+ font-size: 14px;
12864
+ font-weight: 600;
12865
+ color: var(--myio-muted, #9ca3af);
12866
+ }
12867
+ .myio-ho-card__value-spinner {
12868
+ width: 14px;
12869
+ height: 14px;
12870
+ border: 2px solid rgba(124, 58, 237, 0.2);
12871
+ border-top-color: var(--myio-brand-700, #7c3aed);
12872
+ border-radius: 50%;
12873
+ animation: myio-consumption-spin 0.8s linear infinite;
12874
+ flex-shrink: 0;
12875
+ }
12876
+ @keyframes myio-consumption-spin { to { transform: rotate(360deg); } }
12877
+
12858
12878
  /* Efficiency bar */
12859
12879
  .myio-ho-card__eff {
12860
12880
  display: flex;
@@ -16527,7 +16547,20 @@ function paint(root, state6) {
16527
16547
  const primaryValue = formatValueByDomain(entityObject.val, entityObject.domain);
16528
16548
  const numSpan = root.querySelector(".myio-ho-card__value .num");
16529
16549
  const unitSpan = root.querySelector(".myio-ho-card__value .unit");
16530
- numSpan.textContent = primaryValue;
16550
+ const valueBox = root.querySelector(".myio-ho-card__value");
16551
+ const dataLoading = !!entityObject.dataLoading && entityObject.domain !== "temperature";
16552
+ if (valueBox) {
16553
+ valueBox.classList.toggle("is-loading", dataLoading);
16554
+ if (dataLoading) valueBox.setAttribute("aria-busy", "true");
16555
+ else valueBox.removeAttribute("aria-busy");
16556
+ }
16557
+ if (dataLoading) {
16558
+ numSpan.innerHTML = '<span class="myio-ho-card__value-spinner" aria-hidden="true"></span>Carregando\u2026';
16559
+ numSpan.title = "Carregando consumo do per\xEDodo";
16560
+ } else {
16561
+ numSpan.textContent = primaryValue;
16562
+ numSpan.removeAttribute("title");
16563
+ }
16531
16564
  const barContainer = root.querySelector(".bar");
16532
16565
  const effContainer = root.querySelector(".myio-ho-card__eff");
16533
16566
  if (state6.enableSelection) {
@@ -71709,6 +71742,25 @@ var HeaderView = class {
71709
71742
  transition: color 0.2s ease;
71710
71743
  }
71711
71744
 
71745
+ /* Consumo ainda carregando */
71746
+ .${HEADER_CSS_PREFIX}-kpi-loading {
71747
+ display: inline-flex;
71748
+ align-items: center;
71749
+ gap: 10px;
71750
+ font-size: 1rem;
71751
+ font-weight: 600;
71752
+ opacity: 0.85;
71753
+ }
71754
+ .${HEADER_CSS_PREFIX}-kpi-spinner {
71755
+ width: 18px;
71756
+ height: 18px;
71757
+ border: 2.5px solid rgba(255, 255, 255, 0.3);
71758
+ border-top-color: currentColor;
71759
+ border-radius: 50%;
71760
+ animation: myio-consumption-spin 0.8s linear infinite;
71761
+ }
71762
+ @keyframes myio-consumption-spin { to { transform: rotate(360deg); } }
71763
+
71712
71764
  .${HEADER_CSS_PREFIX}-card__subrow {
71713
71765
  display: flex;
71714
71766
  align-items: center;
@@ -72147,7 +72199,28 @@ var HeaderView = class {
72147
72199
  /**
72148
72200
  * Update energy card
72149
72201
  */
72202
+ /** Spinner nos KPIs de consumo (energia e água) enquanto o período carrega */
72203
+ setConsumptionLoading(loading) {
72204
+ if (!loading) {
72205
+ [this.energyKpiEl, this.waterKpiEl].forEach((el2) => {
72206
+ if (el2?.querySelector(`.${HEADER_CSS_PREFIX}-kpi-loading`)) el2.textContent = "-";
72207
+ });
72208
+ return;
72209
+ }
72210
+ const html = `<span class="${HEADER_CSS_PREFIX}-kpi-loading" aria-busy="true"><span class="${HEADER_CSS_PREFIX}-kpi-spinner" aria-hidden="true"></span>Carregando\u2026</span>`;
72211
+ if (this.energyKpiEl) this.energyKpiEl.innerHTML = html;
72212
+ if (this.waterKpiEl) this.waterKpiEl.innerHTML = html;
72213
+ if (this.energyTrendEl) this.energyTrendEl.textContent = "";
72214
+ if (this.waterTrendEl) this.waterTrendEl.textContent = "";
72215
+ }
72150
72216
  updateEnergyCard(data) {
72217
+ if (data.loading) {
72218
+ if (this.energyKpiEl) {
72219
+ this.energyKpiEl.innerHTML = `<span class="${HEADER_CSS_PREFIX}-kpi-loading" aria-busy="true"><span class="${HEADER_CSS_PREFIX}-kpi-spinner" aria-hidden="true"></span>Carregando\u2026</span>`;
72220
+ }
72221
+ if (this.energyTrendEl) this.energyTrendEl.textContent = "";
72222
+ return;
72223
+ }
72151
72224
  if (this.energyKpiEl) {
72152
72225
  if (data.isFiltered && data.unfilteredTotal > 0) {
72153
72226
  const filteredFormatted = this.formatEnergy(data.customerTotal);
@@ -72196,6 +72269,13 @@ var HeaderView = class {
72196
72269
  * Update water card
72197
72270
  */
72198
72271
  updateWaterCard(data) {
72272
+ if (data.loading) {
72273
+ if (this.waterKpiEl) {
72274
+ this.waterKpiEl.innerHTML = `<span class="${HEADER_CSS_PREFIX}-kpi-loading" aria-busy="true"><span class="${HEADER_CSS_PREFIX}-kpi-spinner" aria-hidden="true"></span>Carregando\u2026</span>`;
72275
+ }
72276
+ if (this.waterTrendEl) this.waterTrendEl.textContent = "";
72277
+ return;
72278
+ }
72199
72279
  if (this.waterKpiEl) {
72200
72280
  if (data.isFiltered && data.unfilteredTotal > 0) {
72201
72281
  const filteredFormatted = this.formatWater(data.filteredTotal);
@@ -73181,6 +73261,9 @@ function createHeaderComponent(params) {
73181
73261
  tooltipData.temperature = data;
73182
73262
  view.updateTemperatureCard(data);
73183
73263
  }
73264
+ function setConsumptionLoading(loading) {
73265
+ view.setConsumptionLoading(loading);
73266
+ }
73184
73267
  function updateWaterKPI(data) {
73185
73268
  tooltipData.water = data;
73186
73269
  view.updateWaterCard(data);
@@ -73236,6 +73319,7 @@ function createHeaderComponent(params) {
73236
73319
  updateEnergyKPI,
73237
73320
  updateTemperatureKPI,
73238
73321
  updateWaterKPI,
73322
+ setConsumptionLoading,
73239
73323
  updateTooltipData,
73240
73324
  // Shopping/Filter
73241
73325
  updateShoppings,
@@ -73668,13 +73752,13 @@ function getStyles() {
73668
73752
  color: #ffffff;
73669
73753
  cursor: pointer;
73670
73754
  overflow: hidden;
73671
- box-shadow: 0 0 0 1px rgba(62, 26, 125, 0.5), 0 4px 16px rgba(62, 26, 125, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2);
73755
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--fc-compare-btn) 50%, transparent), 0 4px 16px color-mix(in srgb, var(--fc-compare-btn) 40%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.2);
73672
73756
  transition: var(--fc-transition);
73673
73757
  }
73674
73758
 
73675
73759
  .myio-footer-compare:hover {
73676
73760
  background: linear-gradient(135deg, var(--fc-compare-btn-hover) 0%, var(--fc-compare-btn) 100%);
73677
- box-shadow: 0 0 0 1px rgba(62, 26, 125, 0.7), 0 6px 24px rgba(62, 26, 125, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.3);
73761
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--fc-compare-btn) 70%, transparent), 0 6px 24px color-mix(in srgb, var(--fc-compare-btn) 60%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.3);
73678
73762
  transform: translateY(-2px);
73679
73763
  }
73680
73764
 
@@ -73837,6 +73921,71 @@ function getStyles() {
73837
73921
  text-shadow: none;
73838
73922
  }
73839
73923
 
73924
+ /* ==========================================
73925
+ Brand (paleta do dashboard): fundo s\xF3lido na cor do tema, conte\xFAdo branco
73926
+ ========================================== */
73927
+
73928
+ .myio-footer-component--brand {
73929
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
73930
+ backdrop-filter: none;
73931
+ -webkit-backdrop-filter: none;
73932
+ }
73933
+ .myio-footer-component--brand .myio-footer-chip {
73934
+ background: var(--fc-chip-bg);
73935
+ box-shadow: none;
73936
+ }
73937
+ .myio-footer-component--brand .myio-footer-chip:hover {
73938
+ background: rgba(255, 255, 255, 0.2);
73939
+ border-color: rgba(255, 255, 255, 0.32);
73940
+ box-shadow: none;
73941
+ transform: translateY(-2px);
73942
+ }
73943
+ .myio-footer-component--brand .myio-footer-chip-remove {
73944
+ background: rgba(255, 255, 255, 0.1);
73945
+ border-color: rgba(255, 255, 255, 0.2);
73946
+ color: rgba(255, 255, 255, 0.85);
73947
+ }
73948
+ .myio-footer-component--brand .myio-footer-chip-remove:hover {
73949
+ background: rgba(255, 107, 107, 0.22);
73950
+ border-color: rgba(255, 107, 107, 0.5);
73951
+ color: #fff;
73952
+ }
73953
+ .myio-footer-component--brand .myio-footer-meta {
73954
+ background: var(--fc-meta-bg);
73955
+ box-shadow: none;
73956
+ cursor: default;
73957
+ }
73958
+ .myio-footer-component--brand .myio-footer-meta-title,
73959
+ .myio-footer-component--brand .myio-footer-totals,
73960
+ .myio-footer-component--brand .myio-footer-chip-name,
73961
+ .myio-footer-component--brand .myio-footer-chip-value {
73962
+ text-shadow: none;
73963
+ }
73964
+ .myio-footer-component--brand .myio-footer-empty {
73965
+ background: var(--fc-empty-bg);
73966
+ color: rgba(255, 255, 255, 0.85);
73967
+ text-shadow: none;
73968
+ animation: none;
73969
+ }
73970
+ .myio-footer-component--brand .myio-footer-clear-btn {
73971
+ background: var(--fc-clear-btn);
73972
+ border-color: rgba(255, 255, 255, 0.24);
73973
+ }
73974
+ .myio-footer-component--brand .myio-footer-clear-btn:hover {
73975
+ background: rgba(255, 255, 255, 0.2);
73976
+ box-shadow: none;
73977
+ }
73978
+ .myio-footer-component--brand .myio-footer-clear-btn:disabled {
73979
+ background: rgba(255, 255, 255, 0.08);
73980
+ border-color: rgba(255, 255, 255, 0.14);
73981
+ color: rgba(255, 255, 255, 0.45);
73982
+ }
73983
+ .myio-footer-component--brand .myio-footer-compare:disabled {
73984
+ background: rgba(255, 255, 255, 0.14);
73985
+ color: rgba(255, 255, 255, 0.55);
73986
+ opacity: 1;
73987
+ }
73988
+
73840
73989
  /* ==========================================
73841
73990
  Responsive
73842
73991
  ========================================== */
@@ -73917,6 +74066,7 @@ var ChipRenderer = class {
73917
74066
  const chip = document.createElement("div");
73918
74067
  chip.className = "myio-footer-chip";
73919
74068
  chip.dataset.entityId = entity.id;
74069
+ chip.dataset.fullName = entity.name || "";
73920
74070
  const content = document.createElement("div");
73921
74071
  content.className = "myio-footer-chip-content";
73922
74072
  const name = document.createElement("span");
@@ -74027,7 +74177,43 @@ var FooterView = class {
74027
74177
  getThemeConfig() {
74028
74178
  const themeConfig = this.theme === "dark" ? this.configTemplate.darkMode : this.configTemplate.lightMode;
74029
74179
  const defaults = this.theme === "dark" ? DEFAULT_DARK_THEME2 : DEFAULT_LIGHT_THEME2;
74030
- return { ...defaults, ...themeConfig };
74180
+ return { ...defaults, ...themeConfig, ...this.getBrandOverrides() };
74181
+ }
74182
+ /** Accent do tema do dashboard (createMyIOTheme → window.MyIOUtils.theme), se houver. */
74183
+ getDashboardAccent() {
74184
+ const theme = window.MyIOUtils?.theme;
74185
+ if (!theme?.accent) return null;
74186
+ return { accent: theme.accent, accentText: theme.accentText || "#ffffff" };
74187
+ }
74188
+ /**
74189
+ * Com paleta do dashboard o footer segue o padrão do dashboard de shopping: fundo sólido na cor
74190
+ * do tema e conteúdo branco (mesma identidade do header/tabs).
74191
+ */
74192
+ getBrandOverrides() {
74193
+ const brand = this.getDashboardAccent();
74194
+ if (!brand) return {};
74195
+ const a = brand.accent;
74196
+ return {
74197
+ primaryColor: a,
74198
+ primaryHoverColor: `color-mix(in srgb, ${a} 85%, #fff)`,
74199
+ primaryDarkColor: `color-mix(in srgb, ${a} 70%, #000)`,
74200
+ backgroundGradientStart: a,
74201
+ backgroundGradientEnd: a,
74202
+ borderTopColor: "rgba(255, 255, 255, 0.18)",
74203
+ textColor: brand.accentText,
74204
+ mutedTextColor: "rgba(255, 255, 255, 0.75)",
74205
+ chipBackgroundColor: "rgba(255, 255, 255, 0.12)",
74206
+ chipBorderColor: "rgba(255, 255, 255, 0.24)",
74207
+ chipTextColor: brand.accentText,
74208
+ compareButtonColor: `color-mix(in srgb, ${a} 55%, #0f172a)`,
74209
+ compareButtonHoverColor: `color-mix(in srgb, ${a} 70%, #0f172a)`,
74210
+ clearButtonColor: "rgba(255, 255, 255, 0.12)",
74211
+ clearButtonTextColor: brand.accentText,
74212
+ metaBackgroundColor: "rgba(255, 255, 255, 0.10)",
74213
+ metaBorderColor: "rgba(255, 255, 255, 0.24)",
74214
+ emptyBackgroundColor: "rgba(255, 255, 255, 0.06)",
74215
+ emptyBorderColor: "rgba(255, 255, 255, 0.35)"
74216
+ };
74031
74217
  }
74032
74218
  /**
74033
74219
  * Apply theme colors as CSS custom properties
@@ -74060,7 +74246,8 @@ var FooterView = class {
74060
74246
  */
74061
74247
  getClassName() {
74062
74248
  const base = "myio-footer-component";
74063
- return this.theme === "light" ? `${base} ${base}--light` : base;
74249
+ const brand = this.getDashboardAccent() ? ` ${base}--brand` : "";
74250
+ return (this.theme === "light" ? `${base} ${base}--light` : base) + brand;
74064
74251
  }
74065
74252
  /**
74066
74253
  * Render the footer component
@@ -74118,6 +74305,27 @@ var FooterView = class {
74118
74305
  * Bind event listeners
74119
74306
  */
74120
74307
  bindEvents() {
74308
+ this.footerEl.addEventListener("mouseover", (e) => {
74309
+ const target = e.target;
74310
+ if (target.closest(".myio-footer-chip-remove")) return hideFooterZoom();
74311
+ const chip = target.closest(".myio-footer-chip");
74312
+ if (chip) {
74313
+ const name = chip.dataset.fullName || chip.querySelector(".myio-footer-chip-name")?.textContent || "";
74314
+ const value = chip.querySelector(".myio-footer-chip-value")?.textContent || "";
74315
+ showFooterZoom(chip, name, value);
74316
+ return;
74317
+ }
74318
+ const meta = target.closest(".myio-footer-meta");
74319
+ if (meta) {
74320
+ showFooterZoom(meta, "Sele\xE7\xE3o", this.totalsEl?.textContent || "");
74321
+ }
74322
+ });
74323
+ this.footerEl.addEventListener("mouseout", (e) => {
74324
+ const to = e.relatedTarget;
74325
+ const from = e.target.closest(".myio-footer-chip, .myio-footer-meta");
74326
+ if (from && to && from.contains(to)) return;
74327
+ hideFooterZoom();
74328
+ });
74121
74329
  window.addEventListener("myio:theme-change", ((e) => {
74122
74330
  this.setTheme(e.detail.mode);
74123
74331
  }));
@@ -74199,6 +74407,7 @@ var FooterView = class {
74199
74407
  */
74200
74408
  renderDock(entities) {
74201
74409
  if (!this.dockEl) return;
74410
+ hideFooterZoom();
74202
74411
  if (entities.length === 0) {
74203
74412
  const emptyEl = this.chipRenderer.createEmptyState(this.emptyMessage);
74204
74413
  this.dockEl.replaceChildren(emptyEl);
@@ -74269,6 +74478,7 @@ var FooterView = class {
74269
74478
  */
74270
74479
  destroy() {
74271
74480
  this.eventHandlers.clear();
74481
+ hideFooterZoom();
74272
74482
  if (this.footerEl.parentNode) {
74273
74483
  this.footerEl.parentNode.removeChild(this.footerEl);
74274
74484
  }
@@ -74278,6 +74488,58 @@ var FooterView = class {
74278
74488
  this.clearBtnEl = null;
74279
74489
  }
74280
74490
  };
74491
+ var FOOTER_ZOOM_ID = "myio-footer-chip-zoom";
74492
+ function ensureFooterZoomEl() {
74493
+ if (!document.getElementById("myio-footer-chip-zoom-styles")) {
74494
+ const st = document.createElement("style");
74495
+ st.id = "myio-footer-chip-zoom-styles";
74496
+ st.textContent = `
74497
+ #${FOOTER_ZOOM_ID} {
74498
+ position: fixed; z-index: 2147483000; pointer-events: none;
74499
+ display: flex; flex-direction: column; gap: 2px;
74500
+ min-width: 140px; max-width: 360px; padding: 10px 14px;
74501
+ background: var(--myio-brand-700, #3e1a7d); color: #fff;
74502
+ border: 1px solid rgba(255, 255, 255, 0.28); border-radius: 10px;
74503
+ box-shadow: 0 10px 28px rgba(15, 23, 42, 0.35);
74504
+ font-family: 'Nunito', system-ui, sans-serif;
74505
+ transform: translate(-50%, 6px) scale(0.92); transform-origin: 50% 100%;
74506
+ opacity: 0; transition: opacity 0.12s ease, transform 0.12s ease;
74507
+ }
74508
+ #${FOOTER_ZOOM_ID}.show { opacity: 1; transform: translate(-50%, 0) scale(1); }
74509
+ #${FOOTER_ZOOM_ID} .zoom-name {
74510
+ font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.01em;
74511
+ white-space: normal; word-break: break-word; line-height: 1.2;
74512
+ }
74513
+ #${FOOTER_ZOOM_ID} .zoom-value {
74514
+ font-size: 22px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.15;
74515
+ }
74516
+ `;
74517
+ document.head.appendChild(st);
74518
+ }
74519
+ let el2 = document.getElementById(FOOTER_ZOOM_ID);
74520
+ if (!el2) {
74521
+ el2 = document.createElement("div");
74522
+ el2.id = FOOTER_ZOOM_ID;
74523
+ el2.setAttribute("role", "tooltip");
74524
+ el2.innerHTML = '<span class="zoom-name"></span><span class="zoom-value"></span>';
74525
+ document.body.appendChild(el2);
74526
+ }
74527
+ return el2;
74528
+ }
74529
+ function showFooterZoom(anchor, name, value) {
74530
+ const el2 = ensureFooterZoomEl();
74531
+ el2.querySelector(".zoom-name").textContent = name || "";
74532
+ el2.querySelector(".zoom-value").textContent = value || "";
74533
+ const r = anchor.getBoundingClientRect();
74534
+ const half = 75;
74535
+ const left = Math.min(Math.max(r.left + r.width / 2, half + 8), window.innerWidth - half - 8);
74536
+ el2.style.left = `${left}px`;
74537
+ el2.style.bottom = `${window.innerHeight - r.top + 8}px`;
74538
+ el2.classList.add("show");
74539
+ }
74540
+ function hideFooterZoom() {
74541
+ document.getElementById(FOOTER_ZOOM_ID)?.classList.remove("show");
74542
+ }
74281
74543
 
74282
74544
  // src/components/footer/AlertDialogs.ts
74283
74545
  var currentOverlay = null;
@@ -118243,14 +118505,14 @@ var MenuView = class {
118243
118505
  ========================================== */
118244
118506
 
118245
118507
  .myio-toolbar-item--nav {
118246
- background: var(--menu-nav-bg, #3e1a7d);
118508
+ background: var(--menu-nav-bg, var(--myio-brand-700, #3e1a7d));
118247
118509
  color: var(--menu-nav-color, #ffffff);
118248
118510
  font-weight: 600;
118249
118511
  padding: 8px 16px;
118250
118512
  }
118251
118513
 
118252
118514
  .myio-toolbar-item--nav:hover {
118253
- background: var(--menu-nav-hover-bg, #4c2391);
118515
+ background: var(--menu-nav-hover-bg, color-mix(in srgb, var(--myio-brand-700, #3e1a7d) 85%, #fff));
118254
118516
  }
118255
118517
 
118256
118518
  .myio-toolbar-item--nav .nav-label {
@@ -118264,7 +118526,7 @@ var MenuView = class {
118264
118526
  ========================================== */
118265
118527
 
118266
118528
  .myio-toolbar-item--filter {
118267
- color: var(--menu-filter-color, #6366f1);
118529
+ color: var(--menu-filter-color, var(--myio-brand-700, #6366f1));
118268
118530
  }
118269
118531
 
118270
118532
  .myio-toolbar-item--filter:hover {
@@ -118287,7 +118549,7 @@ var MenuView = class {
118287
118549
  min-width: 340px;
118288
118550
  padding: 0;
118289
118551
  background: transparent;
118290
- border: 2px solid var(--menu-date-border, #3e1a7d);
118552
+ border: 2px solid var(--menu-date-border, var(--myio-brand-700, #3e1a7d));
118291
118553
  border-radius: 8px;
118292
118554
  overflow: hidden;
118293
118555
  transition: all 0.2s ease;
@@ -118295,17 +118557,17 @@ var MenuView = class {
118295
118557
  }
118296
118558
 
118297
118559
  .myio-toolbar-item--date:hover {
118298
- border-color: var(--menu-date-hover-border, #5a2d9e);
118299
- box-shadow: 0 0 0 3px rgba(62, 26, 125, 0.15);
118560
+ border-color: var(--menu-date-hover-border, color-mix(in srgb, var(--myio-brand-700, #3e1a7d) 80%, #fff));
118561
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--myio-brand-700, #3e1a7d) 15%, transparent);
118300
118562
  }
118301
118563
 
118302
118564
  .myio-toolbar-item--date:focus-within {
118303
- border-color: var(--menu-date-focus-border, #3e1a7d);
118304
- box-shadow: 0 0 0 3px rgba(62, 26, 125, 0.25);
118565
+ border-color: var(--menu-date-focus-border, var(--myio-brand-700, #3e1a7d));
118566
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--myio-brand-700, #3e1a7d) 25%, transparent);
118305
118567
  }
118306
118568
 
118307
118569
  .myio-toolbar-item--date .ico {
118308
- background: var(--menu-date-icon-bg, #3e1a7d);
118570
+ background: var(--menu-date-icon-bg, var(--myio-brand-700, #3e1a7d));
118309
118571
  color: white;
118310
118572
  padding: 0 12px;
118311
118573
  margin: 0;
@@ -123722,6 +123984,20 @@ var HEADER_DEVICES_GRID_STYLES = `
123722
123984
  body.light-mode .telemetry-grid-wrap.maximized .telemetry-grid-header {
123723
123985
  background: #f8fafc;
123724
123986
  }
123987
+
123988
+ /* Consumo ainda carregando */
123989
+ .hdg-value-spinner {
123990
+ display: inline-block;
123991
+ width: 13px;
123992
+ height: 13px;
123993
+ margin-right: 6px;
123994
+ vertical-align: -2px;
123995
+ border: 2px solid rgba(124, 58, 237, 0.2);
123996
+ border-top-color: var(--myio-brand-700, #7c3aed);
123997
+ border-radius: 50%;
123998
+ animation: myio-consumption-spin 0.8s linear infinite;
123999
+ }
124000
+ @keyframes myio-consumption-spin { to { transform: rotate(360deg); } }
123725
124001
  `;
123726
124002
  function injectHeaderDevicesGridStyles() {
123727
124003
  const styleId = "hdg-styles";
@@ -123739,14 +124015,15 @@ var DOMAIN_CONFIG7 = {
123739
124015
  consumptionLabel: "Consumo Total",
123740
124016
  zeroLabel: "Sem Consumo",
123741
124017
  unit: "kWh",
123742
- formatValue: (value) => formatEnergy(value, void 0, 2)
124018
+ // 3 casas: mesmo padrão do dashboard de shopping (RFC-0108, measurementSettings default)
124019
+ formatValue: (value) => formatEnergy(value, void 0, 3)
123743
124020
  },
123744
124021
  water: {
123745
124022
  totalLabel: "Total Hidr\xF4metros",
123746
124023
  consumptionLabel: "Consumo Total",
123747
124024
  zeroLabel: "Sem Consumo",
123748
124025
  unit: "m\xB3",
123749
- formatValue: (value) => formatWater(value)
124026
+ formatValue: (value) => `${(Number(value) || 0).toLocaleString("pt-BR", { minimumFractionDigits: 3, maximumFractionDigits: 3 })} m\xB3`
123750
124027
  },
123751
124028
  temperature: {
123752
124029
  totalLabel: "Total Sensores",
@@ -123914,6 +124191,16 @@ var HeaderDevicesGridView = class {
123914
124191
  const percentage = total > 0 ? (online / total * 100).toFixed(1) : "0.0";
123915
124192
  connectivityEl.textContent = `${online}/${total} (${percentage}%)`;
123916
124193
  totalEl.textContent = total.toString();
124194
+ if (stats.loading) {
124195
+ const spin = '<span class="hdg-value-spinner" aria-hidden="true"></span>';
124196
+ consumptionEl.innerHTML = `${spin}Carregando\u2026`;
124197
+ zeroEl.innerHTML = spin;
124198
+ consumptionEl.setAttribute("aria-busy", "true");
124199
+ zeroEl.setAttribute("aria-busy", "true");
124200
+ return;
124201
+ }
124202
+ consumptionEl.removeAttribute("aria-busy");
124203
+ zeroEl.removeAttribute("aria-busy");
123917
124204
  consumptionEl.textContent = this.domainConfig.formatValue(consumption);
123918
124205
  zeroEl.textContent = zeroCount.toString();
123919
124206
  }
@@ -124020,7 +124307,8 @@ var HeaderDevicesGridController = class {
124020
124307
  online,
124021
124308
  total: devices.length,
124022
124309
  consumption: displayValue,
124023
- zeroCount
124310
+ zeroCount,
124311
+ loading: this.domain !== "temperature" && devices.some((d) => d.dataLoading)
124024
124312
  });
124025
124313
  }
124026
124314
  setThemeMode(mode) {
@@ -124302,6 +124590,16 @@ function generateFilterModalStyles(containerId, modalClass, primaryColor) {
124302
124590
  opacity: 0.5;
124303
124591
  }
124304
124592
  #${containerId} .filter-tab:hover { opacity: 0.8; transform: translateY(-1px); }
124593
+ /* Categorias (deviceProfile): r\xF3tulos longos s\xE3o encurtados (nome completo no title) */
124594
+ #${containerId} .filter-tab--category {
124595
+ max-width: 100%;
124596
+ background: color-mix(in srgb, var(--myio-brand-700, #3e1a7d) 8%, #fff);
124597
+ color: var(--myio-brand-700, #3e1a7d);
124598
+ }
124599
+ #${containerId}[data-theme="dark"] .filter-tab--category {
124600
+ background: color-mix(in srgb, var(--myio-brand-700, #3e1a7d) 30%, #1e293b);
124601
+ color: #e2e8f0;
124602
+ }
124305
124603
  #${containerId} .filter-tab.active { opacity: 1; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
124306
124604
  #${containerId} .filter-tab[data-filter="all"] { background: #e2e8f0; color: #475569; }
124307
124605
  #${containerId} .filter-tab[data-filter="online"],
@@ -124585,6 +124883,28 @@ function generateFilterModalStyles(containerId, modalClass, primaryColor) {
124585
124883
  }
124586
124884
 
124587
124885
  // src/components/filter-modal/FilterModalView.ts
124886
+ var CATEGORY_META = {
124887
+ ELEVADOR: { label: "Elevadores", icon: "\u{1F6D7}" },
124888
+ ESCADA_ROLANTE: { label: "Escadas Rolantes", icon: "\u{1F4F6}" },
124889
+ CHILLER: { label: "Chillers", icon: "\u2744\uFE0F" },
124890
+ FANCOIL: { label: "Fancoils", icon: "\u{1F300}" },
124891
+ BOMBA_CAG: { label: "Bombas CAG", icon: "\u{1F4A7}" },
124892
+ BOMBA_HIDRAULICA: { label: "Bombas Hidr\xE1ulicas", icon: "\u{1F4A7}" },
124893
+ BOMBA_INCENDIO: { label: "Bombas de Inc\xEAndio", icon: "\u{1F9EF}" },
124894
+ MOTOR: { label: "Motores", icon: "\u2699\uFE0F" },
124895
+ "3F_MEDIDOR": { label: "Lojas", icon: "\u{1F3EA}" },
124896
+ ENTRADA: { label: "Entrada", icon: "\u{1F50C}" },
124897
+ HIDROMETRO: { label: "Hidr\xF4metros", icon: "\u{1F4A7}" },
124898
+ HIDROMETRO_AREA_COMUM: { label: "Hidr\xF4metros \xC1rea Comum", icon: "\u{1F4A7}" },
124899
+ HIDROMETRO_SHOPPING: { label: "Hidr\xF4metros Entrada", icon: "\u{1F4A7}" },
124900
+ TERMOSTATO: { label: "Termostatos", icon: "\u{1F321}\uFE0F" }
124901
+ };
124902
+ function categoryMeta(profile) {
124903
+ const key = String(profile || "").toUpperCase();
124904
+ if (CATEGORY_META[key]) return CATEGORY_META[key];
124905
+ const label = key.split(/[_\s]+/).filter(Boolean).map((w) => w.charAt(0) + w.slice(1).toLowerCase()).join(" ");
124906
+ return { label: label || key, icon: "\u{1F3F7}\uFE0F" };
124907
+ }
124588
124908
  var FilterModalView = class {
124589
124909
  containerId;
124590
124910
  widgetName;
@@ -124601,6 +124921,13 @@ var FilterModalView = class {
124601
124921
  onApply;
124602
124922
  onClose;
124603
124923
  globalContainer = null;
124924
+ /** Abas de categoria (uma por deviceProfile presente nos itens) — recriadas a cada open() */
124925
+ dynamicTabs = [];
124926
+ dynamicIcons = {};
124927
+ dynamicTitles = {};
124928
+ groups = FILTER_GROUPS;
124929
+ /** Moldura padrão (header no acento do tema + maximizar/fechar + footer premium) */
124930
+ chrome = null;
124604
124931
  escHandler = null;
124605
124932
  currentItems = [];
124606
124933
  constructor(params) {
@@ -124622,7 +124949,7 @@ var FilterModalView = class {
124622
124949
  });
124623
124950
  }
124624
124951
  generateFilterTabsHTML(counts) {
124625
- const allTab = this.filterTabs.find((t) => t.id === "all");
124952
+ const allTab = this.allTabs().find((t) => t.id === "all");
124626
124953
  let html = "";
124627
124954
  if (allTab) {
124628
124955
  const icon2 = FILTER_TAB_ICONS["all"] || "";
@@ -124634,19 +124961,19 @@ var FilterModalView = class {
124634
124961
  </div>
124635
124962
  `;
124636
124963
  }
124637
- FILTER_GROUPS.forEach((group) => {
124638
- const groupTabs = this.filterTabs.filter((t) => group.filters.includes(t.id));
124964
+ this.groups.forEach((group) => {
124965
+ const groupTabs = this.allTabs().filter((t) => group.filters.includes(t.id));
124639
124966
  if (groupTabs.length === 0) return;
124640
124967
  html += `
124641
124968
  <div class="filter-group">
124642
124969
  <span class="filter-group-label">${group.label}</span>
124643
124970
  <div class="filter-group-tabs">
124644
124971
  ${groupTabs.map((tab) => {
124645
- const icon2 = FILTER_TAB_ICONS[tab.id] || "";
124972
+ const icon2 = FILTER_TAB_ICONS[tab.id] || this.dynamicIcons[tab.id] || "";
124646
124973
  const count = counts[tab.id] || 0;
124647
124974
  const expandBtn = count > 0 ? `<button class="filter-tab-expand" data-expand-filter="${tab.id}" title="Ver dispositivos">+</button>` : "";
124648
124975
  return `
124649
- <button class="filter-tab active" data-filter="${tab.id}">
124976
+ <button class="filter-tab active${tab.id.startsWith("cat_") ? " filter-tab--category" : ""}" data-filter="${tab.id}" title="${this.dynamicTitles[tab.id] || tab.label}">
124650
124977
  ${icon2} ${tab.label} (<span id="count${tab.id.charAt(0).toUpperCase() + tab.id.slice(1)}">${count}</span>)${expandBtn}
124651
124978
  </button>
124652
124979
  `;
@@ -124716,9 +125043,29 @@ var FilterModalView = class {
124716
125043
  </div>
124717
125044
  `;
124718
125045
  }
125046
+ allTabs() {
125047
+ return [...this.filterTabs, ...this.dynamicTabs];
125048
+ }
125049
+ /** Grupo "Categoria": uma aba por deviceProfile presente nos itens (ex.: Elevadores, Escadas Rolantes). */
125050
+ buildCategoryTabs(items) {
125051
+ const profileOf3 = (item) => String(item.deviceProfile || "").trim().toUpperCase();
125052
+ const profiles = Array.from(new Set(items.map(profileOf3).filter(Boolean)));
125053
+ profiles.sort((a, b) => categoryMeta(a).label.localeCompare(categoryMeta(b).label, "pt-BR"));
125054
+ this.dynamicIcons = {};
125055
+ this.dynamicTitles = {};
125056
+ this.dynamicTabs = profiles.map((profile) => {
125057
+ const id = `cat_${profile.replace(/[^A-Za-z0-9]/g, "_")}`;
125058
+ const meta = categoryMeta(profile);
125059
+ this.dynamicIcons[id] = meta.icon;
125060
+ this.dynamicTitles[id] = meta.label;
125061
+ const label = meta.label.length > 22 ? `${meta.label.slice(0, 21).trimEnd()}\u2026` : meta.label;
125062
+ return { id, label, filter: (item) => profileOf3(item) === profile };
125063
+ });
125064
+ this.groups = this.dynamicTabs.length ? [...FILTER_GROUPS, { id: "category", label: "Categoria", filters: this.dynamicTabs.map((t) => t.id) }] : FILTER_GROUPS;
125065
+ }
124719
125066
  calculateCounts(items) {
124720
125067
  const counts = {};
124721
- this.filterTabs.forEach((tab) => {
125068
+ this.allTabs().forEach((tab) => {
124722
125069
  counts[tab.id] = items.filter(tab.filter).length;
124723
125070
  });
124724
125071
  return counts;
@@ -124859,7 +125206,7 @@ var FilterModalView = class {
124859
125206
  bindFilterTabHandlers(modal, items) {
124860
125207
  const filterTabsEl = modal.querySelectorAll(".filter-tab");
124861
125208
  const getFilterFn = (filterId) => {
124862
- const tabConfig = this.filterTabs.find((t) => t.id === filterId);
125209
+ const tabConfig = this.allTabs().find((t) => t.id === filterId);
124863
125210
  return tabConfig ? tabConfig.filter : () => false;
124864
125211
  };
124865
125212
  const applyActiveFilters = () => {
@@ -124868,16 +125215,16 @@ var FilterModalView = class {
124868
125215
  return groupActiveFilters.some((filterId) => getFilterFn(filterId)(item));
124869
125216
  };
124870
125217
  let filteredItems = [...items];
124871
- for (let i = 0; i < FILTER_GROUPS.length; i++) {
124872
- const group = FILTER_GROUPS[i];
125218
+ for (let i = 0; i < this.groups.length; i++) {
125219
+ const group = this.groups[i];
124873
125220
  const activeInGroup = Array.from(filterTabsEl).filter(
124874
125221
  (t) => group.filters.includes(t.getAttribute("data-filter") || "") && t.classList.contains("active")
124875
125222
  ).map((t) => t.getAttribute("data-filter") || "");
124876
125223
  if (activeInGroup.length > 0) {
124877
125224
  filteredItems = filteredItems.filter((item) => itemPassesGroup(item, activeInGroup));
124878
125225
  }
124879
- for (let j = i + 1; j < FILTER_GROUPS.length; j++) {
124880
- const nextGroup = FILTER_GROUPS[j];
125226
+ for (let j = i + 1; j < this.groups.length; j++) {
125227
+ const nextGroup = this.groups[j];
124881
125228
  nextGroup.filters.forEach((filterId) => {
124882
125229
  const tab = Array.from(filterTabsEl).find((t) => t.getAttribute("data-filter") === filterId);
124883
125230
  if (!tab) return;
@@ -124889,7 +125236,7 @@ var FilterModalView = class {
124889
125236
  }
124890
125237
  const activeFilters = Array.from(filterTabsEl).filter((t) => t.classList.contains("active") && t.getAttribute("data-filter") !== "all").map((t) => t.getAttribute("data-filter") || "");
124891
125238
  const activeByGroup = {};
124892
- FILTER_GROUPS.forEach((group) => {
125239
+ this.groups.forEach((group) => {
124893
125240
  const activeInGroup = activeFilters.filter((id) => group.filters.includes(id));
124894
125241
  if (activeInGroup.length > 0) activeByGroup[group.id] = activeInGroup;
124895
125242
  });
@@ -124909,7 +125256,7 @@ var FilterModalView = class {
124909
125256
  };
124910
125257
  const getFilteredItems = () => {
124911
125258
  let filteredItems = [...items];
124912
- FILTER_GROUPS.forEach((group) => {
125259
+ this.groups.forEach((group) => {
124913
125260
  const activeInGroup = Array.from(filterTabsEl).filter(
124914
125261
  (t) => group.filters.includes(t.getAttribute("data-filter") || "") && t.classList.contains("active")
124915
125262
  ).map((t) => t.getAttribute("data-filter") || "");
@@ -124922,7 +125269,7 @@ var FilterModalView = class {
124922
125269
  return filteredItems;
124923
125270
  };
124924
125271
  const updateFilterCounts = (filteredItems) => {
124925
- this.filterTabs.forEach((tabConfig) => {
125272
+ this.allTabs().forEach((tabConfig) => {
124926
125273
  if (tabConfig.id === "all") {
124927
125274
  const countEl = modal.querySelector("#countAll");
124928
125275
  if (countEl) countEl.textContent = String(filteredItems.length);
@@ -124971,7 +125318,7 @@ var FilterModalView = class {
124971
125318
  connectivityTab.classList.add("active");
124972
125319
  }
124973
125320
  const filteredItems2 = getFilteredItems();
124974
- const typeIds = ["elevators", "escalators", "hvac", "others", "commonArea", "stores"];
125321
+ const typeIds = ["elevators", "escalators", "hvac", "others", "commonArea", "stores", ...this.dynamicTabs.map((t) => t.id)];
124975
125322
  typeIds.forEach((typeId) => {
124976
125323
  const typeTab = Array.from(filterTabsEl).find((t) => t.getAttribute("data-filter") === typeId);
124977
125324
  if (!typeTab) return;
@@ -125001,7 +125348,7 @@ var FilterModalView = class {
125001
125348
  expandBtns.forEach((btn) => {
125002
125349
  const filterId = btn.getAttribute("data-expand-filter");
125003
125350
  if (!filterId) return;
125004
- const filterTabConfig = this.filterTabs.find((t) => t.id === filterId);
125351
+ const filterTabConfig = this.allTabs().find((t) => t.id === filterId);
125005
125352
  const filterFn = filterTabConfig?.filter || (() => false);
125006
125353
  btn.addEventListener("mouseenter", (e) => {
125007
125354
  e.stopPropagation();
@@ -125022,9 +125369,9 @@ var FilterModalView = class {
125022
125369
  showDeviceTooltip(triggerEl, filterId, devices) {
125023
125370
  const InfoTooltip2 = window.MyIOLibrary;
125024
125371
  if (!InfoTooltip2?.InfoTooltip) return;
125025
- const filterTabConfig = this.filterTabs.find((t) => t.id === filterId);
125372
+ const filterTabConfig = this.allTabs().find((t) => t.id === filterId);
125026
125373
  const label = filterTabConfig?.label || filterId;
125027
- const icon2 = FILTER_TAB_ICONS[filterId] || "\u{1F4CB}";
125374
+ const icon2 = FILTER_TAB_ICONS[filterId] || this.dynamicIcons[filterId] || "\u{1F4CB}";
125028
125375
  let devicesHtml = "";
125029
125376
  if (devices.length === 0) {
125030
125377
  devicesHtml = `
@@ -125101,12 +125448,17 @@ var FilterModalView = class {
125101
125448
  this.globalContainer.innerHTML = `<style>${generateFilterModalStyles(this.containerId, this.modalClass, this.primaryColor)}</style>${this.generateModalHTML()}`;
125102
125449
  document.body.appendChild(this.globalContainer);
125103
125450
  const modal2 = this.globalContainer.querySelector("#filterModal");
125104
- if (modal2) this.setupHandlers(modal2, items);
125451
+ if (modal2) {
125452
+ this.setupHandlers(modal2, items);
125453
+ this.mountChrome(modal2);
125454
+ }
125105
125455
  }
125106
125456
  }
125107
125457
  this.globalContainer.setAttribute("data-theme", this.themeMode);
125458
+ if (this.chrome && this.chrome.getTheme() !== this.themeMode) this.chrome.setTheme(this.themeMode);
125108
125459
  const modal = this.globalContainer.querySelector("#filterModal");
125109
125460
  if (!modal) return;
125461
+ this.buildCategoryTabs(items);
125110
125462
  const counts = this.calculateCounts(items);
125111
125463
  const tabsContainer = modal.querySelector("#filterTabsContainer");
125112
125464
  if (tabsContainer) {
@@ -125121,6 +125473,28 @@ var FilterModalView = class {
125121
125473
  modal.classList.remove("hidden");
125122
125474
  document.body.classList.add("filter-modal-open");
125123
125475
  }
125476
+ /** Troca o header próprio pela moldura padrão das modais premium e adiciona o footer premium. */
125477
+ mountChrome(modal) {
125478
+ const card = modal.querySelector(`.${this.modalClass}-card`);
125479
+ if (!card || !this.globalContainer) return;
125480
+ this.chrome = createPremiumModalChrome({
125481
+ modalId: `${this.containerId}Chrome`,
125482
+ icon: "\u{1F50D}",
125483
+ title: "Filtrar e Ordenar",
125484
+ modalEl: card,
125485
+ headerSlot: modal.querySelector(`#${this.containerId}Header`),
125486
+ themeTarget: this.globalContainer,
125487
+ // estilos escuros da modal são por #container[data-theme]
125488
+ theme: this.themeMode === "dark" ? "dark" : "light",
125489
+ showThemeToggle: true,
125490
+ maximizedClass: "maximized",
125491
+ headerRadius: "16px 16px 0 0",
125492
+ onClose: () => this.close(),
125493
+ onThemeChange: (t) => {
125494
+ this.themeMode = t;
125495
+ }
125496
+ });
125497
+ }
125124
125498
  close() {
125125
125499
  if (this.globalContainer) {
125126
125500
  const modal = this.globalContainer.querySelector("#filterModal");
@@ -125135,12 +125509,15 @@ var FilterModalView = class {
125135
125509
  if (this.globalContainer) {
125136
125510
  this.globalContainer.setAttribute("data-theme", mode);
125137
125511
  }
125512
+ this.chrome?.setTheme(mode === "dark" ? "dark" : "light");
125138
125513
  }
125139
125514
  destroy() {
125140
125515
  if (this.escHandler) {
125141
125516
  document.removeEventListener("keydown", this.escHandler);
125142
125517
  this.escHandler = null;
125143
125518
  }
125519
+ this.chrome?.destroy();
125520
+ this.chrome = null;
125144
125521
  if (this.globalContainer) {
125145
125522
  this.globalContainer.remove();
125146
125523
  this.globalContainer = null;