@feezal/feezal-element 3.0.4 → 3.0.8

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.
Files changed (2) hide show
  1. package/feezal-element.js +107 -0
  2. package/package.json +1 -1
package/feezal-element.js CHANGED
@@ -106,6 +106,14 @@ export class FeezalElement extends LitElement {
106
106
  connectedCallback() {
107
107
  super.connectedCallback();
108
108
  this.classList.add('feezal-element');
109
+ // N37: an element stamped into an ALREADY-PAUSED view must not
110
+ // subscribe — pause state is a precondition, not only an event. The
111
+ // visibility controller resumes it via a reconnect cycle later.
112
+ if (window.feezal?.visibility?.isPaused?.(this)) {
113
+ this.__n37Paused = true;
114
+ return;
115
+ }
116
+ this.__n37Paused = false;
109
117
  if (this.visible || !this.dynamicSubscriptions) {
110
118
  this._subscribe();
111
119
  this._conditions.connect();
@@ -117,6 +125,37 @@ export class FeezalElement extends LitElement {
117
125
  this._subscribeAvailability();
118
126
  }
119
127
 
128
+ /**
129
+ * N37 — pause this element's MQTT traffic while its view is hidden.
130
+ * Teardown is uniform for EVERY element: all primary subscriptions live
131
+ * in `_subscriptions` (addSubscription), availability in the N31 arrays,
132
+ * conditions in the engine — regardless of how the element wired them.
133
+ */
134
+ pauseSubscriptions() {
135
+ if (this.__n37Paused) return;
136
+ this.__n37Paused = true;
137
+ this._unsubscribe();
138
+ this._unsubscribeAvailability();
139
+ this._conditions.disconnect();
140
+ }
141
+
142
+ /**
143
+ * N37 — resume by re-running the element's OWN wiring exactly as a fresh
144
+ * mount would: a detach/re-attach cycle runs disconnected/connectedCallback,
145
+ * the one path every element (including manual wirers like the device
146
+ * cards) implements correctly. Retained values repaint instantly from the
147
+ * B40 cache replay.
148
+ */
149
+ resumeSubscriptions() {
150
+ if (!this.__n37Paused) return;
151
+ this.__n37Paused = false;
152
+ const parent = this.parentNode;
153
+ if (!parent) return;
154
+ const next = this.nextSibling;
155
+ this.remove();
156
+ parent.insertBefore(this, next);
157
+ }
158
+
120
159
  disconnectedCallback() {
121
160
  super.disconnectedCallback();
122
161
  this._unsubscribe();
@@ -239,6 +278,11 @@ export class FeezalElement extends LitElement {
239
278
 
240
279
  /** Subscribe to a single topic — convenience wrapper for subclasses. */
241
280
  addSubscription(topic, callback) {
281
+ // N37: wiring attempted while the element's view is paused is dropped
282
+ // — device cards wire in their own connectedCallback AFTER the base
283
+ // guard returned, so the gate must live here to be universal. The
284
+ // resume reconnect cycle re-runs the wiring when the view returns.
285
+ if (this.__n37Paused) return;
242
286
  this._subscriptions.push(feezal.connection.sub(topic, callback));
243
287
  }
244
288
 
@@ -414,6 +458,34 @@ export function availabilityBadge(available) {
414
458
  return available ? '' : html`<span class="feezal-unavail-badge" title="Device unavailable">⚠</span>`;
415
459
  }
416
460
 
461
+ // E124: shared low-battery badge — a crisp, self-drawn tiny horizontal battery
462
+ // showing a single low charge bar (far more recognizable at small sizes than the
463
+ // Material `battery_alert` ligature, and no exclamation clutter). Bottom-right by
464
+ // default; families whose bottom-right corner is taken override via
465
+ // `--feezal-battery-*` (or plain right/left).
466
+ export const feezalBatteryStyles = css`
467
+ .feezal-batt-badge {
468
+ position: absolute;
469
+ bottom: var(--feezal-battery-bottom, 6px);
470
+ right: var(--feezal-battery-right, 6px);
471
+ width: var(--feezal-battery-size, 20px);
472
+ height: var(--feezal-battery-size, 20px);
473
+ color: var(--feezal-battery-color, var(--warning-color, #f59e0b));
474
+ opacity: 0.95; pointer-events: none; z-index: 2;
475
+ }
476
+ .feezal-batt-badge svg { width: 100%; height: 100%; display: block; }
477
+ `;
478
+
479
+ /** Low-battery badge template partial: renders nothing unless the battery is low.
480
+ * A tiny battery outline + terminal with one short bar of charge remaining. */
481
+ export function batteryLowBadge(low) {
482
+ return low ? html`<span class="feezal-batt-badge" title="Battery low"><svg viewBox="0 0 24 24" aria-hidden="true">
483
+ <rect x="2.5" y="8.5" width="15" height="7" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.6"/>
484
+ <rect x="18.6" y="10.6" width="2.1" height="2.8" rx="0.7" fill="currentColor"/>
485
+ <rect x="4.3" y="10.3" width="3" height="3.4" rx="0.6" fill="currentColor"/>
486
+ </svg></span>` : '';
487
+ }
488
+
417
489
  /**
418
490
  * E117: shared descriptor for the `publish-local` attribute — spread into an
419
491
  * element's `feezal.attributes` right after `publish` so the label and help
@@ -430,5 +502,40 @@ export const publishLocalAttribute = {
430
502
  help: 'Publish page-locally instead of to the broker: the payload reaches only subscribers in THIS browser tab (dialog triggers, view switches, wiring elements together). Nothing is sent over MQTT, nothing is retained, and it works while disconnected.'
431
503
  };
432
504
 
505
+ /**
506
+ * E137 — payload comparison, cross-controller shared machinery: string
507
+ * coercion (case-insensitive) plus boolean true/false matching the HA/z2m
508
+ * ON/OFF conventions. Single source — the copies in feezal-glass and the
509
+ * contact card consolidated here (feezal-glass re-exports for back-compat).
510
+ */
511
+ export function payloadMatch(value, configured) {
512
+ if (String(value).toLowerCase() === String(configured).toLowerCase()) return true;
513
+ if (value === true && /^(on|true|1|yes)$/i.test(String(configured))) return true;
514
+ if (value === false && /^(off|false|0|no)$/i.test(String(configured))) return true;
515
+ return false;
516
+ }
517
+
518
+ /**
519
+ * Boolean-attribute converter for DEFAULT-TRUE booleans.
520
+ *
521
+ * Lit's built-in Boolean is *present = true / absent = false*, which cannot
522
+ * represent an explicit "false" for an attribute that defaults to true: the
523
+ * inspector removes the attribute on "off", it serialises as absent via
524
+ * outerHTML, and the viewer's fresh element re-reads its constructor default
525
+ * (true) — so the "off" choice is silently lost on save/deploy.
526
+ *
527
+ * This converter reads the literal "false" / "0" as false (any other present
528
+ * value → true; absent → the constructor default stands), and reflects the
529
+ * value as an explicit "true" / "false" so an OFF state persists. Pair it with
530
+ * the inspector's default-aware boolean write. Apply to any boolean whose
531
+ * descriptor `default` is true:
532
+ *
533
+ * loop: {type: Boolean, reflect: true, converter: feezalBoolean, attribute: 'loop'}
534
+ */
535
+ export const feezalBoolean = {
536
+ fromAttribute: v => v !== null && v !== 'false' && v !== '0',
537
+ toAttribute: v => (v ? 'true' : 'false')
538
+ };
539
+
433
540
  // Re-export so element files can do a single import.
434
541
  export {html, css};
package/package.json CHANGED
@@ -35,5 +35,5 @@
35
35
  "url": "git+https://github.com/feezal/feezal.git",
36
36
  "directory": "www/packages/@feezal/feezal-element"
37
37
  },
38
- "version": "3.0.4"
38
+ "version": "3.0.8"
39
39
  }