Files
HomeAssistantVS/custom_components/climate_scheduler/frontend/keyframe-timeline.js
T
2026-07-08 10:43:39 -04:00

2226 lines
105 KiB
JavaScript

/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$2=globalThis,e$2=t$2.ShadowRoot&&(void 0===t$2.ShadyCSS||t$2.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$2=Symbol(),o$4=new WeakMap;let n$3 = class n{constructor(t,e,o){if(this._$cssResult$=true,o!==s$2)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$2&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$4.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$4.set(s,t));}return t}toString(){return this.cssText}};const r$4=t=>new n$3("string"==typeof t?t:t+"",void 0,s$2),i$3=(t,...e)=>{const o=1===t.length?t[0]:e.reduce((e,s,o)=>e+(t=>{if(true===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[o+1],t[0]);return new n$3(o,t,s$2)},S$1=(s,o)=>{if(e$2)s.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of o){const o=document.createElement("style"),n=t$2.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o);}},c$2=e$2?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$4(e)})(t):t;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const{is:i$2,defineProperty:e$1,getOwnPropertyDescriptor:h$1,getOwnPropertyNames:r$3,getOwnPropertySymbols:o$3,getPrototypeOf:n$2}=Object,a$1=globalThis,c$1=a$1.trustedTypes,l$1=c$1?c$1.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t);}catch(t){i=null;}}return i}},f$1=(t,s)=>!i$2(t,s),b$1={attribute:true,type:String,converter:u$1,reflect:false,useDefault:false,hasChanged:f$1};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;let y$1 = class y extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b$1){if(s.state&&(s.attribute=false),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=true),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e$1(this.prototype,t,h);}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h$1(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t;}};return {get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i);},configurable:true,enumerable:true}}static getPropertyOptions(t){return this.elementProperties.get(t)??b$1}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$2(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties);}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=true,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...r$3(t),...o$3(t)];for(const i of s)this.createProperty(i,t[i]);}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i);}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$2(s));}else void 0!==s&&i.push(c$2(s));return i}static _$Eu(t,s){const i=s.attribute;return false===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=false,this.hasUpdated=false,this._$Em=null,this._$Ev();}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this));}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.();}removeController(t){this._$EO?.delete(t);}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t);}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(true),this._$EO?.forEach(t=>t.hostConnected?.());}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.());}attributeChangedCallback(t,s,i){this._$AK(t,i);}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&true===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null;}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e;const r=h.fromAttribute(s,t.type);this[e]=r??this._$Ej?.get(e)??r,this._$Em=null;}}requestUpdate(t,s,i,e=false,h){if(void 0!==t){const r=this.constructor;if(false===e&&(h=this[t]),i??=r.getPropertyOptions(t),!((i.hasChanged??f$1)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(r._$Eu(t,i))))return;this.C(t,s,i);} false===this.isUpdatePending&&(this._$ES=this._$EP());}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),true!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),true===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t));}async _$EP(){this.isUpdatePending=true;try{await this._$ES;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0;}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];true!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e);}}let t=false;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(s)):this._$EM();}catch(s){throw t=false,this._$EM(),s}t&&this._$AE(s);}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=true,this.firstUpdated(t)),this.updated(t);}_$EM(){this._$AL=new Map,this.isUpdatePending=false;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return true}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM();}updated(t){}firstUpdated(t){}};y$1.elementStyles=[],y$1.shadowRootOptions={mode:"open"},y$1[d$1("elementProperties")]=new Map,y$1[d$1("finalized")]=new Map,p$1?.({ReactiveElement:y$1}),(a$1.reactiveElementVersions??=[]).push("2.1.2");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$1=globalThis,i$1=t=>t,s$1=t$1.trustedTypes,e=s$1?s$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,h="$lit$",o$2=`lit$${Math.random().toFixed(9).slice(2)}$`,n$1="?"+o$2,r$2=`<${n$1}>`,l=document,c=()=>l.createComment(""),a=t=>null===t||"object"!=typeof t&&"function"!=typeof t,u=Array.isArray,d=t=>u(t)||"function"==typeof t?.[Symbol.iterator],f="[ \t\n\f\r]",v=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),E=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e?e.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?"<svg>":3===i?"<math>":"",c=v;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,f=0;for(;f<s.length&&(c.lastIndex=f,u=c.exec(s),null!==u);)f=c.lastIndex,c===v?"!--"===u[1]?c=_:void 0!==u[1]?c=m:void 0!==u[2]?(y.test(u[2])&&(n=RegExp("</"+u[2],"g")),c=p):void 0!==u[3]&&(c=p):c===p?">"===u[0]?(c=n??v,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?p:'"'===u[3]?$:g):c===$||c===g?c=p:c===_||c===m?c=v:(c=p,n=void 0);const x=c===p&&t[i+1].startsWith("/>")?" ":"";l+=c===v?s+r$2:d>=0?(e.push(a),s.slice(0,d)+h+s.slice(d)+o$2+x):s+o$2+(-2===d?i:x);}return [V(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=P.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(h)){const i=v[a++],s=r.getAttribute(t).split(o$2),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:l,name:e[2],strings:s,ctor:"."===e[1]?I:"?"===e[1]?L:"@"===e[1]?z:H}),r.removeAttribute(t);}else t.startsWith(o$2)&&(d.push({type:6,index:l}),r.removeAttribute(t));if(y.test(r.tagName)){const t=r.textContent.split(o$2),i=t.length-1;if(i>0){r.textContent=s$1?s$1.emptyScript:"";for(let s=0;s<i;s++)r.append(t[s],c()),P.nextNode(),d.push({type:2,index:++l});r.append(t[i],c());}}}else if(8===r.nodeType)if(r.data===n$1)d.push({type:2,index:l});else {let t=-1;for(;-1!==(t=r.data.indexOf(o$2,t+1));)d.push({type:7,index:l}),t+=o$2.length-1;}l++;}}static createElement(t,i){const s=l.createElement("template");return s.innerHTML=t,s}}function M(t,i,s=t,e){if(i===E)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=a(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(false),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=M(t,h._$AS(t,i.values),h,e)),i}class R{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??l).importNode(i,true);P.currentNode=e;let h=P.nextNode(),o=0,n=0,r=s[0];for(;void 0!==r;){if(o===r.index){let i;2===r.type?i=new k(h,h.nextSibling,this,t):1===r.type?i=new r.ctor(h,r.name,r.strings,this,t):6===r.type&&(i=new Z(h,this,t)),this._$AV.push(i),r=s[++n];}o!==r?.index&&(h=P.nextNode(),o++);}return P.currentNode=l,e}p(t){let i=0;for(const s of this._$AV) void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class k{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=A,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??true;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=M(this,t,i),a(t)?t===A||null==t||""===t?(this._$AH!==A&&this._$AR(),this._$AH=A):t!==this._$AH&&t!==E&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):d(t)?this.k(t):this._(t);}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t));}_(t){this._$AH!==A&&a(this._$AH)?this._$AA.nextSibling.data=t:this.T(l.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=S.createElement(V(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new R(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=C.get(t.strings);return void 0===i&&C.set(t.strings,i=new S(t)),i}k(t){u(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new k(this.O(c()),this.O(c()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,s){for(this._$AP?.(false,true,s);t!==this._$AB;){const s=i$1(t).nextSibling;i$1(t).remove(),t=s;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class H{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=A,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=M(this,e[s+n],i,n),r===E&&(r=this._$AH[n]),o||=!a(r)||r!==this._$AH[n],r===A?t=A:t!==A&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===A?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class I extends H{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===A?void 0:t;}}class L extends H{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==A);}}class z extends H{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=M(this,t,i,0)??A)===E)return;const s=this._$AH,e=t===A&&s!==A||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==A&&(s===A||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class Z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){M(this,t);}}const B=t$1.litHtmlPolyfillSupport;B?.(S,k),(t$1.litHtmlVersions??=[]).push("3.3.2");const D=(t,i,s)=>{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new k(i.insertBefore(c(),t),t,void 0,s??{});}return h._$AI(t),h};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const s=globalThis;class i extends y$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=D(r,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return E}}i._$litElement$=true,i["finalized"]=true,s.litElementHydrateSupport?.({LitElement:i});const o$1=s.litElementPolyfillSupport;o$1?.({LitElement:i});(s.litElementVersions??=[]).push("4.2.2");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t=t=>(e,o)=>{ void 0!==o?o.addInitializer(()=>{customElements.define(t,e);}):customElements.define(t,e);};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const o={attribute:true,type:String,converter:u$1,reflect:false,hasChanged:f$1},r$1=(t=o,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),"setter"===n&&((t=Object.create(t)).wrapped=true),s.set(r.name,t),"accessor"===n){const{name:o}=r;return {set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t,true,r);},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t,true,r);}}throw Error("Unsupported decorator location: "+n)};function n(t){return (e,o)=>"object"==typeof o?r$1(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/function r(r){return n({...r,state:true,attribute:false})}
// Color palette for automatic background graph coloring
const BACKGROUND_GRAPH_COLORS = [
'#f44336', // Red
'#9c27b0', // Purple
'#3f51b5', // Indigo
'#00bcd4', // Cyan
'#009688', // Teal
'#4caf50', // Green
'#8bc34a', // Light Green
'#cddc39', // Lime
'#ffeb3b', // Yellow
'#ffc107', // Amber
'#ff9800', // Orange
'#ff5722', // Deep Orange
'#795548', // Brown
'#607d8b', // Blue Grey
];
let KeyframeTimeline = class KeyframeTimeline extends i {
constructor() {
super(...arguments);
this.duration = 24; // hours
this.slots = 96; // time divisions (e.g., 15-min intervals in 24h)
this.keyframes = [];
this.minValue = 5; // Minimum Y axis value (default: 5°C)
this.maxValue = 30; // Maximum Y axis value (default: 30°C)
this.snapValue = 0; // Y axis snap step (0 = no snapping)
this.xAxisLabel = ''; // X axis label
this.yAxisLabel = ''; // Y axis label
this.title = ''; // Title displayed in top left
this.showHeader = true; // Show header with controls
this.allowCollapse = true; // Allow collapsing the timeline
this.readonly = false; // Disable all interactions
this.showCurrentTime = false; // Automatically show indicator at current time
this.backgroundGraphs = []; // Background reference graphs
this.advanceHistory = []; // Array of {activated_at, target_time, cancelled_at, target_node}
this.tooltipMode = 'cursor'; // Tooltip display mode (set from global settings)
this.canvasWidth = 0;
this.canvasHeight = 600;
this.showConfig = false;
this.draggingIndex = null;
this.draggingSegment = null;
this.selectedKeyframeIndex = null;
this.collapsed = false;
this.showScrollNavLeft = false;
this.showScrollNavRight = false;
this.undoStack = []; // Changed to store full keyframe arrays
this.legendCollapsed = true;
this.isDragging = false;
this.hasMoved = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.isPanning = false;
this.panStartX = 0;
this.panStartScrollLeft = 0;
this.lastClickTime = 0;
this.lastClickIndex = -1; // Track which keyframe was last clicked
this.lastClickX = 0;
this.instanceId = Math.random().toString(36).substring(7); // Unique instance ID for debugging
this.lastClickY = 0;
this.justDeletedKeyframe = false; // Prevent dblclick from adding after delete
this.holdStartX = 0;
this.holdStartY = 0;
this.hoverRenderPending = false;
this.hoverX = 0;
this.hoverY = 0;
}
willUpdate(changedProperties) {
// Property change tracking
}
firstUpdated() {
const canvasEl = this.shadowRoot?.querySelector('canvas');
if (canvasEl) {
this.canvas = canvasEl;
this.ctx = canvasEl.getContext('2d') || undefined;
this.updateCanvasSize();
}
this.tooltipEl = this.shadowRoot?.querySelector('.cursor-tooltip');
this.wrapperEl = this.shadowRoot?.querySelector('.timeline-canvas-wrapper');
this.checkScrollVisibility();
// Listen for scroll events to update button visibility
if (this.wrapperEl) {
this.wrapperEl.addEventListener('scroll', () => this.checkScrollVisibility());
}
// Update canvas size on window resize
window.addEventListener('resize', () => {
this.updateCanvasSize();
this.drawTimeline();
this.checkScrollVisibility();
});
// Draw immediately - CSS variables should now be properly set via styles.css
requestAnimationFrame(() => {
this.updateCanvasSize();
this.drawTimeline();
});
// Setup current time indicator if enabled
if (this.showCurrentTime) {
this.updateCurrentTime();
this.startCurrentTimeTimer();
}
// Setup keyboard shortcuts
this.keyboardHandler = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
this.undo();
}
};
document.addEventListener('keydown', this.keyboardHandler);
}
disconnectedCallback() {
super.disconnectedCallback();
this.stopCurrentTimeTimer();
// Remove keyboard listener
if (this.keyboardHandler) {
document.removeEventListener('keydown', this.keyboardHandler);
}
}
updated(changedProperties) {
super.updated(changedProperties);
// Redraw when keyframes, backgroundGraphs or advanceHistory changes
if (changedProperties.has('keyframes')) {
this.drawTimeline();
this.updateNavigationButtonsState();
}
// Redraw when backgroundGraphs or advanceHistory changes
if (changedProperties.has('backgroundGraphs') || changedProperties.has('advanceHistory')) {
this.drawTimeline();
}
// Start/stop timer when showCurrentTime changes
if (changedProperties.has('showCurrentTime')) {
if (this.showCurrentTime) {
this.updateCurrentTime();
this.startCurrentTimeTimer();
}
else {
this.stopCurrentTimeTimer();
}
}
}
updateCurrentTime() {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// Calculate time as decimal hours
const currentTime = hours + (minutes / 60) + (seconds / 3600);
// Only update if within the timeline duration
if (currentTime <= this.duration) {
this.indicatorTime = currentTime;
this.drawTimeline();
}
}
startCurrentTimeTimer() {
// Update every 10 seconds for smooth movement
this.currentTimeTimer = window.setInterval(() => {
this.updateCurrentTime();
}, 10000);
}
stopCurrentTimeTimer() {
if (this.currentTimeTimer) {
window.clearInterval(this.currentTimeTimer);
this.currentTimeTimer = undefined;
}
}
updateCanvasSize() {
if (!this.canvas)
return;
// Use offsetWidth to get actual canvas width (not clipped by scroll container)
const canvasCSSWidth = this.canvas.offsetWidth;
this.canvasWidth = canvasCSSWidth * window.devicePixelRatio;
// Read height from CSS variables
const computedStyle = getComputedStyle(this);
const cssHeightVar = this.collapsed ? '--timeline-height-collapsed' : '--timeline-height';
const cssHeight = computedStyle.getPropertyValue(cssHeightVar).trim();
const baseHeight = parseInt(cssHeight);
this.canvasHeight = baseHeight * window.devicePixelRatio;
this.canvas.width = this.canvasWidth;
this.canvas.height = this.canvasHeight;
}
normalizeValue(value) {
// Convert value from minValue-maxValue range to 0-1 range
return (value - this.minValue) / (this.maxValue - this.minValue);
}
denormalizeValue(normalized) {
// Convert value from 0-1 range to minValue-maxValue range
return normalized * (this.maxValue - this.minValue) + this.minValue;
}
getThemeColor(cssVar) {
return getComputedStyle(this).getPropertyValue(cssVar).trim();
}
getBackgroundGraphColor(bgGraph, graphIndex) {
return bgGraph.color || BACKGROUND_GRAPH_COLORS[graphIndex % BACKGROUND_GRAPH_COLORS.length];
}
toggleLegend(e) {
e.stopPropagation();
this.legendCollapsed = !this.legendCollapsed;
}
getBaseFontSize() {
// Get computed font size from host element to respect browser/accessibility settings
const computedStyle = getComputedStyle(this);
return parseFloat(computedStyle.fontSize);
}
snapValueToGrid(value) {
// Snap value to grid if snapValue is set
if (this.snapValue > 0) {
const snapped = Math.round(value / this.snapValue) * this.snapValue;
return this.roundToStepPrecision(snapped, this.snapValue);
}
return this.roundToPrecision(value, 4);
}
roundToStepPrecision(value, step) {
return this.roundToPrecision(value, this.getStepPrecision(step));
}
roundToPrecision(value, precision) {
const safePrecision = Math.max(0, Math.min(precision, 10));
const factor = 10 ** safePrecision;
return Math.round((value + Number.EPSILON) * factor) / factor;
}
getStepPrecision(step) {
if (!Number.isFinite(step) || step <= 0) {
return 3;
}
const stepString = step.toString().toLowerCase();
if (stepString.includes('e-')) {
const exponent = Number(stepString.split('e-')[1]);
return Number.isFinite(exponent) ? exponent : 3;
}
const decimalPart = stepString.split('.')[1];
return decimalPart ? decimalPart.length : 0;
}
getGraphDimensions(rect) {
const labelHeight = 30;
const leftMargin = 35;
const yAxisWidth = 35;
const rightMargin = 35;
const topMargin = 25;
const bottomMargin = 25;
const graphHeight = rect.height - labelHeight - topMargin - bottomMargin;
// Use canvas width (not rect width which is clipped by scroll container)
const canvasWidthCSS = this.canvas.width / (window.devicePixelRatio || 1);
const graphWidth = canvasWidthCSS - leftMargin - yAxisWidth - rightMargin;
return {
labelHeight,
leftMargin,
yAxisWidth,
rightMargin,
topMargin,
bottomMargin,
graphHeight,
graphWidth,
canvasWidthCSS
};
}
sortKeyframes() {
// Keep track of what was selected/dragging before sort
const selectedKeyframe = this.selectedKeyframeIndex !== null ? this.keyframes[this.selectedKeyframeIndex] : null;
const draggingKeyframe = this.draggingIndex !== null ? this.keyframes[this.draggingIndex] : null;
// Sort keyframes by time
this.keyframes = [...this.keyframes].sort((a, b) => a.time - b.time);
// Update indices to point to same keyframes after sort
if (selectedKeyframe) {
this.selectedKeyframeIndex = this.keyframes.findIndex(kf => kf === selectedKeyframe);
}
if (draggingKeyframe) {
this.draggingIndex = this.keyframes.findIndex(kf => kf === draggingKeyframe);
}
}
drawTimeline() {
if (!this.ctx || !this.canvas)
return;
this.canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio;
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.canvasWidth / this.slots;
// Reserve space at bottom for labels, left for Y axis, right margin, and top for margin
const labelHeight = 30 * dpr;
const leftMargin = 35 * dpr;
const yAxisWidth = 35 * dpr;
const rightMargin = 35 * dpr;
const topMargin = 45 * dpr;
const bottomMargin = 25 * dpr;
const graphHeight = this.canvasHeight - labelHeight - topMargin - bottomMargin;
const graphWidth = this.canvasWidth - leftMargin - yAxisWidth - rightMargin;
// Draw Y axis labels and horizontal grid lines
const numYLabels = this.collapsed ? 2 : 5;
const baseFontSize = this.getBaseFontSize();
const yLabelOffset = 5 * dpr; // Distance from Y-axis to label text
const yAxisLabelOffset = -10 * dpr; // Distance from left edge to vertical Y-axis label
const xAxisLabelOffset = 25 * dpr; // Distance from bottom to horizontal X-axis label
this.ctx.fillStyle = this.getThemeColor('--canvas-text-secondary');
this.ctx.font = `${baseFontSize * dpr}px sans-serif`;
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'middle';
this.ctx.strokeStyle = this.getThemeColor('--canvas-grid-line');
this.ctx.lineWidth = 1 * dpr;
for (let i = 0; i < numYLabels; i++) {
const ratio = i / (numYLabels - 1);
const value = this.minValue + ratio * (this.maxValue - this.minValue);
const y = topMargin + (graphHeight * (1 - ratio));
// Draw label
this.ctx.fillText(value.toFixed(1), leftMargin + yAxisWidth - yLabelOffset, y);
// Draw horizontal grid line
this.ctx.beginPath();
this.ctx.moveTo(leftMargin + yAxisWidth, y);
this.ctx.lineTo(leftMargin + yAxisWidth + graphWidth, y);
this.ctx.stroke();
}
// Draw Y axis label (vertical text on left side)
if (this.yAxisLabel && !this.collapsed) {
this.ctx.save();
this.ctx.translate(leftMargin + yAxisLabelOffset, this.canvasHeight / 2);
this.ctx.rotate(-Math.PI / 2);
this.ctx.fillStyle = this.getThemeColor('--canvas-text-primary');
this.ctx.font = `${baseFontSize * dpr}px sans-serif`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(this.yAxisLabel, 0, 0);
this.ctx.restore();
}
// Adjust slot width for graph area
graphWidth / this.slots;
// Draw hour markers (full height) and labels
this.ctx.fillStyle = this.getThemeColor('--canvas-text-secondary');
this.ctx.font = `${baseFontSize * dpr}px sans-serif`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
const hoursToShow = Math.ceil(this.duration);
const labelInterval = this.collapsed ? 3 : 1; // Show labels every 3 hours when collapsed
for (let i = 0; i <= hoursToShow; i++) {
const x = leftMargin + yAxisWidth + ((i / this.duration) * graphWidth);
// Draw hour marker line with thicker lines every 6 hours
const isMajorLine = i % 6 === 0;
this.ctx.strokeStyle = isMajorLine
? this.getThemeColor('--canvas-grid-line-major')
: this.getThemeColor('--canvas-grid-line');
this.ctx.lineWidth = isMajorLine ? 2 * dpr : 1 * dpr;
this.ctx.beginPath();
this.ctx.moveTo(x, topMargin);
this.ctx.lineTo(x, topMargin + graphHeight);
this.ctx.stroke();
// Draw hour label only at specified intervals (below graph area)
if (i % labelInterval === 0) {
const hour = i % 24;
const label = hour === 0 ? '00' : hour.toString().padStart(2, '0');
this.ctx.fillText(label, x, topMargin + graphHeight + (15 * dpr));
}
}
// Draw axis lines (like SVG graph)
this.ctx.strokeStyle = this.getThemeColor('--canvas-text-primary');
this.ctx.lineWidth = 2 * dpr;
// X-axis (bottom)
this.ctx.beginPath();
this.ctx.moveTo(leftMargin + yAxisWidth, topMargin + graphHeight);
this.ctx.lineTo(leftMargin + yAxisWidth + graphWidth, topMargin + graphHeight);
this.ctx.stroke();
// Y-axis (left)
this.ctx.beginPath();
this.ctx.moveTo(leftMargin + yAxisWidth, topMargin);
this.ctx.lineTo(leftMargin + yAxisWidth, topMargin + graphHeight);
this.ctx.stroke();
// Current time indicator (green dashed line like SVG)
const now = new Date();
const currentHours = now.getHours() + now.getMinutes() / 60;
const currentX = leftMargin + yAxisWidth + ((currentHours / this.duration) * graphWidth);
this.ctx.strokeStyle = '#00ff00';
this.ctx.lineWidth = 2 * dpr;
this.ctx.setLineDash([5 * dpr, 5 * dpr]);
this.ctx.globalAlpha = 0.7;
this.ctx.beginPath();
this.ctx.moveTo(currentX, topMargin);
this.ctx.lineTo(currentX, topMargin + graphHeight);
this.ctx.stroke();
this.ctx.setLineDash([]);
this.ctx.globalAlpha = 1.0;
// Current time label at top
const hours = Math.floor(currentHours);
const minutes = Math.floor((currentHours - hours) * 60);
const timeLabel = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
this.ctx.fillStyle = this.getThemeColor('--canvas-text-primary');
this.ctx.font = `bold ${baseFontSize * dpr}px sans-serif`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'bottom';
this.ctx.fillText(timeLabel, currentX, topMargin - (5 * dpr));
// Draw X axis label (below time labels)
if (this.xAxisLabel && !this.collapsed) {
this.ctx.fillStyle = this.getThemeColor('--canvas-text-primary');
this.ctx.font = `${baseFontSize * dpr}px sans-serif`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'top';
this.ctx.fillText(this.xAxisLabel, leftMargin + yAxisWidth + (graphWidth / 2), this.canvasHeight - xAxisLabelOffset);
}
// Draw background graphs (reference data)
const ctx = this.ctx;
this.backgroundGraphs.forEach((bgGraph, graphIndex) => {
if (bgGraph.keyframes.length === 0)
return;
// Sort keyframes by time for proper line drawing
const sortedKeyframes = [...bgGraph.keyframes].sort((a, b) => a.time - b.time);
// Use specified color or cycle through palette
const color = this.getBackgroundGraphColor(bgGraph, graphIndex);
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbaMatch) {
ctx.strokeStyle = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, 0.5)`;
}
else {
// Try hex to rgba conversion
const hexMatch = color.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
if (hexMatch) {
const r = parseInt(hexMatch[1], 16);
const g = parseInt(hexMatch[2], 16);
const b = parseInt(hexMatch[3], 16);
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.5)`;
}
else {
ctx.strokeStyle = color;
}
}
ctx.lineWidth = 1.5 * dpr;
ctx.setLineDash([3 * dpr, 3 * dpr]); // Dashed line for background
// Draw lines between keyframes
for (let i = 0; i < sortedKeyframes.length - 1; i++) {
const kf1 = sortedKeyframes[i];
const kf2 = sortedKeyframes[i + 1];
const x1 = leftMargin + yAxisWidth + ((kf1.time / this.duration) * graphWidth);
const y1 = topMargin + ((1 - this.normalizeValue(kf1.value)) * graphHeight);
const x2 = leftMargin + yAxisWidth + ((kf2.time / this.duration) * graphWidth);
const y2 = topMargin + ((1 - this.normalizeValue(kf2.value)) * graphHeight);
// Draw line between points
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.setLineDash([]); // Reset dash
// Draw small circle markers at each keyframe (smaller than main graph)
sortedKeyframes.forEach(kf => {
const x = leftMargin + yAxisWidth + ((kf.time / this.duration) * graphWidth);
const y = topMargin + ((1 - this.normalizeValue(kf.value)) * graphHeight);
this.ctx.fillStyle = color;
this.ctx.beginPath();
this.ctx.arc(x, y, 2 * dpr, 0, Math.PI * 2);
this.ctx.fill();
});
});
// Draw FLAT/STEP lines between keyframes (like scheduler-card)
if (this.keyframes.length > 0) {
this.ctx.strokeStyle = this.getThemeColor('--keyframe-color');
this.ctx.lineWidth = 3 * dpr;
// Keyframes are already sorted by time
for (let i = 0; i < this.keyframes.length - 1; i++) {
const kf1 = this.keyframes[i];
const kf2 = this.keyframes[i + 1];
const x1 = leftMargin + yAxisWidth + ((kf1.time / this.duration) * graphWidth);
const y1 = topMargin + ((1 - this.normalizeValue(kf1.value)) * graphHeight);
const x2 = leftMargin + yAxisWidth + ((kf2.time / this.duration) * graphWidth);
const y2 = topMargin + ((1 - this.normalizeValue(kf2.value)) * graphHeight);
// Draw flat line (hold value until next keyframe)
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y1); // Horizontal to next time
this.ctx.lineTo(x2, y2); // Vertical step to new value
this.ctx.stroke();
}
// Wraparound: extend last keyframe to end, then wrap to first keyframe
if (this.keyframes.length > 0) {
const lastKf = this.keyframes[this.keyframes.length - 1];
const firstKf = this.keyframes[0];
const lastX = leftMargin + yAxisWidth + ((lastKf.time / this.duration) * graphWidth);
const lastY = topMargin + ((1 - this.normalizeValue(lastKf.value)) * graphHeight);
const firstY = topMargin + ((1 - this.normalizeValue(firstKf.value)) * graphHeight);
// Determine the starting value (from previous day or from last keyframe)
const startValue = this.previousDayEndValue !== undefined ? this.previousDayEndValue : lastKf.value;
topMargin + ((1 - this.normalizeValue(startValue)) * graphHeight);
// Calculate the position of the last hour marker (24:00/00:00)
const endX = leftMargin + yAxisWidth + ((this.duration / this.duration) * graphWidth);
// Extend last keyframe to right edge
this.ctx.beginPath();
this.ctx.moveTo(lastX, lastY);
this.ctx.lineTo(endX, lastY);
this.ctx.stroke();
// Extend from left edge to first keyframe
// In multi-day mode, use previousDayEndValue; in 24hr mode, wrap to own last value
const wrapValue = this.previousDayEndValue !== undefined && this.previousDayEndValue !== null
? this.previousDayEndValue
: lastKf.value;
const wrapY = topMargin + ((1 - this.normalizeValue(wrapValue)) * graphHeight);
if (firstKf.time > 0) {
// Hold wrap value horizontally, then step to first keyframe
const firstX = leftMargin + yAxisWidth + ((firstKf.time / this.duration) * graphWidth);
this.ctx.beginPath();
this.ctx.moveTo(leftMargin + yAxisWidth, wrapY);
this.ctx.lineTo(firstX, wrapY);
this.ctx.lineTo(firstX, firstY);
this.ctx.stroke();
}
else {
// First keyframe at 0, just draw vertical step
this.ctx.beginPath();
this.ctx.moveTo(leftMargin + yAxisWidth, wrapY);
this.ctx.lineTo(leftMargin + yAxisWidth, firstY);
this.ctx.stroke();
}
}
}
// Draw advance history markers
if (this.advanceHistory && this.advanceHistory.length > 0) {
this.advanceHistory.forEach(event => {
if (event.target_node && event.target_node.temp !== null && event.target_node.temp !== undefined) {
// Parse activated_at time to get hour position
const activatedDate = new Date(event.activated_at);
const activatedHours = activatedDate.getHours() + (activatedDate.getMinutes() / 60);
// Only draw if within today's 24-hour range
if (activatedHours >= 0 && activatedHours < 24) {
const x = leftMargin + yAxisWidth + ((activatedHours / this.duration) * graphWidth);
const y = topMargin + ((1 - this.normalizeValue(event.target_node.temp)) * graphHeight);
// Draw diamond marker for advance activation
this.ctx.save();
this.ctx.fillStyle = '#00ff00';
this.ctx.strokeStyle = '#00aa00';
this.ctx.lineWidth = 2 * dpr;
this.ctx.translate(x, y);
this.ctx.rotate(Math.PI / 4);
const markerSize = 8 * dpr;
this.ctx.fillRect(-markerSize, -markerSize, markerSize * 2, markerSize * 2);
this.ctx.strokeRect(-markerSize, -markerSize, markerSize * 2, markerSize * 2);
this.ctx.restore();
}
}
});
}
// Draw current time indicator (vertical bar) if set
if (this.indicatorTime !== undefined && this.indicatorTime >= 0 && this.indicatorTime <= this.duration) {
const indicatorX = leftMargin + yAxisWidth + ((this.indicatorTime / this.duration) * graphWidth);
this.ctx.strokeStyle = '#00ff00';
this.ctx.lineWidth = 2 * dpr;
this.ctx.setLineDash([5 * dpr, 5 * dpr]); // Dashed line
// Draw vertical line through graph area
this.ctx.beginPath();
this.ctx.moveTo(indicatorX, topMargin);
this.ctx.lineTo(indicatorX, topMargin + graphHeight);
this.ctx.stroke();
this.ctx.setLineDash([]); // Reset dash
}
// Draw keyframe markers
this.keyframes.forEach((kf, index) => {
const x = leftMargin + yAxisWidth + ((kf.time / this.duration) * graphWidth);
const y = topMargin + ((1 - this.normalizeValue(kf.value)) * graphHeight);
// Highlight if being dragged or selected
const isDragging = this.draggingIndex === index;
const isSelected = this.selectedKeyframeIndex === index;
if (isDragging) {
this.ctx.fillStyle = this.getThemeColor('--keyframe-dragging-color');
}
else if (isSelected) {
this.ctx.fillStyle = this.getThemeColor('--keyframe-selected-color');
}
else {
this.ctx.fillStyle = this.getThemeColor('--keyframe-color');
}
// Draw diamond marker (smaller when collapsed)
this.ctx.save();
this.ctx.translate(x, y);
this.ctx.rotate(Math.PI / 4);
const baseSize = this.collapsed ? 4 : 6;
const size = (isDragging || isSelected) ? baseSize + 2 : baseSize;
this.ctx.fillRect(-size * dpr, -size * dpr, size * 2 * dpr, size * 2 * dpr);
this.ctx.restore();
// Draw selection ring for selected keyframe (hide when collapsed)
if (isSelected && !isDragging && !this.collapsed) {
this.ctx.strokeStyle = this.getThemeColor('--keyframe-selected-color');
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.arc(x, y, 12 * dpr, 0, Math.PI * 2);
this.ctx.stroke();
}
// Draw hover ring for draggable indication (hide when collapsed)
if (!isDragging && !isSelected && !this.collapsed) {
const textColor = this.getThemeColor('--canvas-text-secondary');
// Apply opacity to text color for hover ring
const rgbaMatch = textColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbaMatch) {
this.ctx.strokeStyle = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, 0.3)`;
}
else {
this.ctx.strokeStyle = textColor;
}
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.arc(x, y, 10 * dpr, 0, Math.PI * 2);
this.ctx.stroke();
}
// Draw value label above each keyframe (hide when collapsed)
if (!this.collapsed) {
const valueText = kf.value.toFixed(this.snapValue < 1 ? 1 : 0);
this.ctx.font = `${baseFontSize * dpr}px system-ui, -apple-system, sans-serif`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'bottom';
// Position label above the keyframe with some padding
const labelY = y - 18 * dpr;
// Draw background for better readability
const textMetrics = this.ctx.measureText(valueText);
const textWidth = textMetrics.width;
const textHeight = 14 * dpr;
const padding = 4 * dpr;
this.ctx.fillStyle = this.getThemeColor('--canvas-label-bg');
this.ctx.fillRect(x - textWidth / 2 - padding, labelY - textHeight - padding, textWidth + padding * 2, textHeight + padding * 2);
// Draw text - use keyframe color
if (isDragging) {
this.ctx.fillStyle = this.getThemeColor('--keyframe-dragging-color');
}
else if (isSelected) {
this.ctx.fillStyle = this.getThemeColor('--keyframe-selected-color');
}
else {
this.ctx.fillStyle = this.getThemeColor('--keyframe-color');
}
this.ctx.fillText(valueText, x, labelY);
}
});
// Tooltip is rendered as a lightweight DOM overlay to avoid canvas redraw on hover.
}
buildHoverTooltip(x, y, rect) {
const { leftMargin, yAxisWidth, rightMargin, topMargin, graphHeight, graphWidth } = this.getGraphDimensions(rect);
// Check if in graph area
if (x < leftMargin + yAxisWidth || x > rect.width - rightMargin ||
y < topMargin || y > topMargin + graphHeight) {
return null;
}
const adjustedX = x - leftMargin - yAxisWidth;
const time = (adjustedX / graphWidth) * this.duration;
let tooltipLines = [];
if (this.tooltipMode === 'cursor') {
// Show interpolated value at cursor position on schedule line
const tooltipValue = this.getInterpolatedValue(time);
if (tooltipValue !== null) {
const hours = Math.floor(time);
const minutes = Math.round((time - hours) * 60);
tooltipLines.push({
text: `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')} - ${tooltipValue.toFixed(1)}°`,
color: this.getThemeColor('--keyframe-color')
});
}
}
else {
// Get current time in decimal hours
const now = new Date();
const currentTime = now.getHours() + now.getMinutes() / 60;
// Only show history if hovering over past time or close to current time (within 30 minutes)
const timeDiff = time - currentTime;
const showHistory = timeDiff <= 0.5; // Show if in past or within 30 minutes of now
if (showHistory && this.backgroundGraphs && this.backgroundGraphs.length > 0) {
this.backgroundGraphs.forEach((historyGraph, graphIndex) => {
if (!historyGraph || !historyGraph.keyframes || historyGraph.keyframes.length === 0)
return;
let displayPoint = null;
if (time >= currentTime) {
// At or past current time (but close to now): show latest temperature
displayPoint = historyGraph.keyframes[historyGraph.keyframes.length - 1];
}
else {
// Before current time: find closest data point to hover time
let closestPoint = null;
let closestDist = Infinity;
for (const point of historyGraph.keyframes) {
const dist = Math.abs(point.time - time);
if (dist < closestDist) {
closestDist = dist;
closestPoint = point;
}
}
if (closestPoint && closestDist < 0.5) { // Within 30 minutes
displayPoint = closestPoint;
}
}
if (displayPoint) {
const label = historyGraph.label || 'Temperature';
const color = historyGraph.color || BACKGROUND_GRAPH_COLORS[graphIndex % BACKGROUND_GRAPH_COLORS.length];
tooltipLines.push({ text: `${label}: ${displayPoint.value.toFixed(1)}°`, color });
}
});
}
}
if (tooltipLines.length === 0)
return null;
return {
lines: tooltipLines
};
}
hideHoverTooltip() {
if (!this.tooltipEl)
return;
this.tooltipEl.hidden = true;
}
getOpaqueColor(color) {
if (!this.shadowRoot)
return color;
if (!this.colorProbeEl) {
this.colorProbeEl = document.createElement('span');
this.colorProbeEl.style.position = 'absolute';
this.colorProbeEl.style.visibility = 'hidden';
this.colorProbeEl.style.pointerEvents = 'none';
this.colorProbeEl.style.width = '0';
this.colorProbeEl.style.height = '0';
this.colorProbeEl.style.overflow = 'hidden';
this.shadowRoot.appendChild(this.colorProbeEl);
}
this.colorProbeEl.style.color = color;
const resolved = getComputedStyle(this.colorProbeEl).color;
const rgbaMatch = resolved.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9]*\.?[0-9]+)\s*\)/i);
if (rgbaMatch) {
return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
}
return resolved || color;
}
renderHoverTooltip() {
if (!this.canvas || !this.tooltipEl || this.collapsed || this.isDragging || this.isPanning) {
this.hideHoverTooltip();
return;
}
const rect = this.canvas.getBoundingClientRect();
const tooltip = this.buildHoverTooltip(this.hoverX, this.hoverY, rect);
if (!tooltip) {
this.hideHoverTooltip();
return;
}
this.tooltipEl.replaceChildren();
tooltip.lines.forEach(line => {
const lineEl = document.createElement('div');
lineEl.className = 'cursor-tooltip-line';
lineEl.textContent = line.text;
if (line.color) {
lineEl.style.color = this.getOpaqueColor(line.color);
}
this.tooltipEl.appendChild(lineEl);
});
this.tooltipEl.hidden = false;
// Stable anchor: above-left by default; flip horizontally to the right when near left edge.
const cursorOffsetX = 32;
const cursorOffsetY = 10;
const margin = 10;
const tooltipWidth = this.tooltipEl.offsetWidth;
const tooltipHeight = this.tooltipEl.offsetHeight;
const maxX = Math.max(margin, rect.width - tooltipWidth - margin);
const maxY = Math.max(margin, rect.height - tooltipHeight - margin);
let tooltipX = this.hoverX - tooltipWidth - cursorOffsetX;
if (tooltipX < margin) {
tooltipX = this.hoverX + cursorOffsetX;
}
tooltipX = Math.max(margin, Math.min(tooltipX, maxX));
const tooltipY = Math.max(margin, Math.min(this.hoverY - tooltipHeight - cursorOffsetY, maxY));
this.tooltipEl.style.left = `${Math.round(tooltipX)}px`;
this.tooltipEl.style.top = `${Math.round(tooltipY)}px`;
}
queueHoverTooltipRender(x, y) {
this.hoverX = x;
this.hoverY = y;
if (this.hoverRenderPending)
return;
this.hoverRenderPending = true;
requestAnimationFrame(() => {
this.hoverRenderPending = false;
this.renderHoverTooltip();
});
}
getInterpolatedValue(time) {
if (this.keyframes.length === 0)
return null;
if (this.keyframes.length === 1)
return this.keyframes[0].value;
// Find surrounding keyframes
let beforeKf = this.keyframes[0];
let afterKf = this.keyframes[this.keyframes.length - 1];
for (let i = 0; i < this.keyframes.length - 1; i++) {
if (this.keyframes[i].time <= time && this.keyframes[i + 1].time >= time) {
beforeKf = this.keyframes[i];
afterKf = this.keyframes[i + 1];
break;
}
}
// Linear interpolation
if (beforeKf.time === afterKf.time)
return beforeKf.value;
const ratio = (time - beforeKf.time) / (afterKf.time - beforeKf.time);
return beforeKf.value + (afterKf.value - beforeKf.value) * ratio;
}
handleCanvasMouseDown(e) {
if (!this.canvas || this.collapsed || this.readonly)
return;
const rect = this.canvas.getBoundingClientRect();
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
const x = clientX - rect.left;
const y = clientY - rect.top;
const { leftMargin, yAxisWidth, rightMargin, topMargin, bottomMargin, graphHeight, graphWidth } = this.getGraphDimensions(rect);
// Check if scrollable (only in expanded mode)
const isScrollable = !this.collapsed && this.wrapperEl && this.wrapperEl.scrollWidth > this.wrapperEl.clientWidth;
// For touch devices, set up long-press detection FIRST (before any returns)
if (e instanceof TouchEvent) {
const currentTime = Date.now();
const timeSinceLastClick = currentTime - this.lastClickTime;
const dx = x - this.lastClickX;
const dy = y - this.lastClickY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Double-tap detected (within 300ms and 30px)
if (timeSinceLastClick < 300 && distance < 30) {
this.handleDoubleClick(x, y, rect);
this.lastClickTime = 0; // Reset to prevent triple-tap
return;
}
this.lastClickTime = currentTime;
this.lastClickX = x;
this.lastClickY = y;
// Start long-press timer for touch devices (600ms)
this.holdStartX = x;
this.holdStartY = y;
this.clearHoldTimer();
this.holdTimer = window.setTimeout(() => {
this.handleContextMenu(x, y, rect);
// Clear dragging state if long press completes
this.isDragging = false;
this.draggingIndex = null;
}, 600);
}
// Check if clicking on existing keyframe
const clickedIndex = this.keyframes.findIndex(kf => {
const kfX = leftMargin + yAxisWidth + ((kf.time / this.duration) * graphWidth);
const kfY = topMargin + ((1 - this.normalizeValue(kf.value)) * graphHeight);
const distance = Math.sqrt(Math.pow(x - kfX, 2) + Math.pow(y - kfY, 2));
return distance < 20;
});
if (clickedIndex >= 0) {
// Check for double-click to delete (desktop)
if (e instanceof MouseEvent) {
const currentTime = Date.now();
const timeSinceLastClick = currentTime - this.lastClickTime;
// Double-click detected on same keyframe (within 300ms)
if (timeSinceLastClick < 300 && this.lastClickIndex === clickedIndex) {
this.deleteKeyframe(clickedIndex);
this.lastClickTime = 0; // Reset
this.lastClickIndex = -1;
this.draggingIndex = null; // Clear dragging state
this.isDragging = false;
this.justDeletedKeyframe = true; // Prevent dblclick handler from adding
setTimeout(() => this.justDeletedKeyframe = false, 100); // Reset after event propagation
e.preventDefault();
e.stopPropagation();
return;
}
this.lastClickTime = currentTime;
this.lastClickIndex = clickedIndex;
}
// Prepare to drag existing keyframe (but wait to see if it's a long press or just a click)
this.draggingIndex = clickedIndex;
this.hasMoved = false; // Reset movement tracking
this.dragStartX = x;
this.dragStartY = y;
// Don't set isDragging yet - we'll set it on first move
// This allows clicks to work properly
e.preventDefault();
return;
}
// Check if clicking on a segment (line between two keyframes)
const segmentIndex = this.findSegmentAtPoint(x, y, rect);
if (segmentIndex >= 0) {
// Prepare to drag segment
this.draggingSegment = {
startIndex: segmentIndex,
endIndex: segmentIndex + 1,
initialStartTime: this.keyframes[segmentIndex].time,
initialEndTime: this.keyframes[segmentIndex + 1].time,
initialPointerX: x
};
this.hasMoved = false;
this.dragStartX = x;
e.preventDefault();
return;
}
// If scrollable and not clicking on keyframe, prepare for panning
if (isScrollable) {
this.isPanning = true;
this.panStartX = clientX;
this.panStartScrollLeft = this.wrapperEl.scrollLeft;
e.preventDefault();
}
}
handleCanvasMouseMove(e) {
// Update hover position for tooltip (only for mouse, not touch)
if (e instanceof MouseEvent && !this.isDragging && !this.isPanning && this.canvas) {
this.queueHoverTooltipRender(e.offsetX, e.offsetY);
}
else if (e instanceof TouchEvent) {
this.hideHoverTooltip();
}
// Handle panning first (takes priority when active)
if (this.isPanning && this.wrapperEl) {
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
const deltaX = clientX - this.panStartX;
this.wrapperEl.scrollLeft = this.panStartScrollLeft - deltaX;
e.preventDefault();
return;
}
// Cancel long-press if moving during touch hold
if (e instanceof TouchEvent && this.holdTimer) {
const rect = this.canvas?.getBoundingClientRect();
if (rect) {
this.wrapperEl?.scrollLeft || 0;
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
const dx = x - this.holdStartX;
const dy = y - this.holdStartY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Cancel hold if moved more than 5px - user is dragging (match threshold below)
if (distance > 5) {
this.clearHoldTimer();
}
}
}
if (!this.canvas)
return;
// Handle segment dragging
if (this.draggingSegment !== null) {
const rect = this.canvas.getBoundingClientRect();
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
this.wrapperEl?.scrollLeft || 0;
const currentX = clientX - rect.left;
const dx = currentX - this.dragStartX;
const distance = Math.abs(dx);
if (distance < 5)
return; // Minimum movement threshold
if (!this.hasMoved) {
this.saveUndoState();
this.hasMoved = true;
}
const { leftMargin, yAxisWidth, graphWidth } = this.getGraphDimensions(rect);
// Calculate time delta
const pixelDelta = currentX - this.draggingSegment.initialPointerX;
const timeDelta = (pixelDelta / graphWidth) * this.duration;
// Apply delta to both keyframes
let newStartTime = this.draggingSegment.initialStartTime + timeDelta;
let newEndTime = this.draggingSegment.initialEndTime + timeDelta;
// Constrain to not pass adjacent keyframes
const { startIndex, endIndex } = this.draggingSegment;
if (startIndex > 0) {
const prevTime = this.keyframes[startIndex - 1].time;
const minGap = this.duration / this.slots;
if (newStartTime < prevTime + minGap) {
const shift = (prevTime + minGap) - newStartTime;
newStartTime += shift;
newEndTime += shift;
}
}
if (endIndex < this.keyframes.length - 1) {
const nextTime = this.keyframes[endIndex + 1].time;
const minGap = this.duration / this.slots;
if (newEndTime > nextTime - minGap) {
const shift = newEndTime - (nextTime - minGap);
newStartTime -= shift;
newEndTime -= shift;
}
}
// Clamp to boundaries (max 23:59 to prevent 24:00)
newStartTime = Math.max(0, newStartTime);
newEndTime = Math.min(23 + (59 / 60), newEndTime);
// Snap times to nearest slot (15-minute intervals)
const slotDuration = this.duration / this.slots;
newStartTime = Math.round(newStartTime / slotDuration) * slotDuration;
newEndTime = Math.round(newEndTime / slotDuration) * slotDuration;
// Update keyframes
this.keyframes[startIndex].time = newStartTime;
this.keyframes[endIndex].time = newEndTime;
// Fire update event for settings panel
this.dispatchEvent(new CustomEvent('nodeSettingsUpdate', {
detail: {
index: startIndex,
keyframe: this.keyframes[startIndex]
},
bubbles: true,
composed: true
}));
this.drawTimeline();
return;
}
// Handle single keyframe dragging
if (this.draggingIndex === null)
return;
const rect = this.canvas.getBoundingClientRect();
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
this.wrapperEl?.scrollLeft || 0;
const currentX = clientX - rect.left;
const currentY = clientY - rect.top;
// Calculate distance from drag start
const dx = currentX - this.dragStartX;
const dy = currentY - this.dragStartY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Only consider it a move if moved more than 5 pixels
if (distance < 5)
return;
// Start dragging on first move
if (!this.isDragging) {
this.isDragging = true;
// Save undo state when starting to drag
this.saveUndoState();
}
this.hasMoved = true; // Track that movement occurred
if (!this.isDragging)
return;
let x = currentX;
let y = currentY;
const { leftMargin, yAxisWidth, rightMargin, topMargin, bottomMargin, graphHeight, graphWidth, canvasWidthCSS } = this.getGraphDimensions(rect);
// Clamp to canvas bounds (graph area only)
x = Math.max(leftMargin + yAxisWidth, Math.min(x, canvasWidthCSS - rightMargin));
y = Math.max(topMargin, Math.min(y, topMargin + graphHeight));
// Snap time to nearest slot (adjust for Y axis offset)
const adjustedX = x - leftMargin - yAxisWidth;
const slotWidth = graphWidth / this.slots;
const slotIndex = Math.round(adjustedX / slotWidth);
// Clamp to 23:59 (23.9833 hours) to prevent 24:00
let time = Math.min((slotIndex / this.slots) * this.duration, 23 + (59 / 60));
// Constrain time to not pass adjacent keyframes (array is already sorted)
if (this.draggingIndex > 0) {
const prevTime = this.keyframes[this.draggingIndex - 1].time;
const minTime = prevTime + (this.duration / this.slots); // At least one slot apart
time = Math.max(time, minTime);
}
if (this.draggingIndex < this.keyframes.length - 1) {
const nextTime = this.keyframes[this.draggingIndex + 1].time;
const maxTime = nextTime - (this.duration / this.slots); // At least one slot apart
time = Math.min(time, maxTime);
}
// Value is in minValue-maxValue range (adjust for top margin)
const adjustedY = y - topMargin;
const normalizedValue = Math.max(0, Math.min(1, 1 - (adjustedY / graphHeight)));
let value = Math.max(this.minValue, Math.min(this.maxValue, this.denormalizeValue(normalizedValue)));
// Apply snapping if configured
value = this.snapValueToGrid(value);
// Update keyframe position
const oldTime = this.keyframes[this.draggingIndex].time;
this.keyframes = this.keyframes.map((kf, i) => i === this.draggingIndex ? { time, value } : kf);
// Re-sort if time changed (might change position in array)
if (Math.abs(time - oldTime) > 0.01) {
this.sortKeyframes();
}
// Fire update event for settings panel
this.dispatchEvent(new CustomEvent('nodeSettingsUpdate', {
detail: {
index: this.draggingIndex,
keyframe: this.keyframes[this.draggingIndex]
},
bubbles: true,
composed: true
}));
this.drawTimeline();
e.preventDefault();
}
handleCanvasMouseUp(e) {
this.clearHoldTimer();
this.hideHoverTooltip();
// Handle segment dragging completion
if (this.draggingSegment !== null && this.hasMoved) {
this.dispatchEvent(new CustomEvent('segment-moved', {
detail: {
startIndex: this.draggingSegment.startIndex,
endIndex: this.draggingSegment.endIndex
},
bubbles: true,
composed: true
}));
this.draggingSegment = null;
this.hasMoved = false;
return;
}
// Handle single keyframe dragging completion
if (this.draggingIndex !== null && this.hasMoved) {
this.dispatchEvent(new CustomEvent('keyframe-moved', {
detail: {
index: this.draggingIndex,
keyframe: this.keyframes[this.draggingIndex]
},
bubbles: true,
composed: true
}));
}
// Handle tap selection for touch devices (when not dragging)
if (e instanceof TouchEvent && this.draggingIndex !== null && !this.hasMoved) {
// Select the keyframe
this.selectedKeyframeIndex = this.draggingIndex;
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframe-clicked', {
detail: {
index: this.draggingIndex,
keyframe: this.keyframes[this.draggingIndex]
},
bubbles: true,
composed: true
}));
}
this.draggingIndex = null;
this.draggingSegment = null;
this.isDragging = false;
this.hasMoved = false;
this.isPanning = false; // Reset panning state
}
handleCanvasMouseLeave(e) {
// Clear hover tooltip overlay
this.hideHoverTooltip();
// Also handle mouseup logic if needed
this.handleCanvasMouseUp(e);
}
handleCanvasClick(e) {
if (!this.canvas || this.collapsed || this.readonly)
return;
// If we just finished dragging, don't process click
if (this.hasMoved)
return;
const rect = this.canvas.getBoundingClientRect();
this.wrapperEl?.scrollLeft || 0;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Use CSS pixels for click detection (canvas drawing uses device pixels with dpr scaling)
const { leftMargin, yAxisWidth, topMargin, graphHeight, graphWidth } = this.getGraphDimensions(rect);
// Check if clicking on existing keyframe
const clickedIndex = this.keyframes.findIndex(kf => {
const kfX = leftMargin + yAxisWidth + ((kf.time / this.duration) * graphWidth);
const kfY = topMargin + ((1 - this.normalizeValue(kf.value)) * graphHeight);
const distance = Math.sqrt(Math.pow(x - kfX, 2) + Math.pow(y - kfY, 2));
return distance < 20;
});
if (clickedIndex >= 0) {
// Select the keyframe
this.selectedKeyframeIndex = clickedIndex;
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframe-clicked', {
detail: {
index: clickedIndex,
keyframe: this.keyframes[clickedIndex]
},
bubbles: true,
composed: true
}));
}
else {
// Clicked on empty area - deselect
this.selectedKeyframeIndex = null;
this.drawTimeline();
}
}
handleCanvasContextMenu(e) {
if (!this.canvas || this.collapsed || this.readonly)
return;
e.preventDefault(); // Prevent default context menu
const rect = this.canvas.getBoundingClientRect();
this.wrapperEl?.scrollLeft || 0;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.handleContextMenu(x, y, rect);
}
handleContextMenu(x, y, rect) {
const { leftMargin, yAxisWidth, topMargin, graphHeight, graphWidth } = this.getGraphDimensions(rect);
// Check if clicking on existing keyframe
const clickedIndex = this.keyframes.findIndex(kf => {
const kfX = leftMargin + yAxisWidth + ((kf.time / this.duration) * graphWidth);
const kfY = topMargin + ((1 - this.normalizeValue(kf.value)) * graphHeight);
const distance = Math.sqrt(Math.pow(x - kfX, 2) + Math.pow(y - kfY, 2));
return distance < 20;
});
// Delete keyframe if clicked on one
if (clickedIndex >= 0) {
this.deleteKeyframe(clickedIndex);
}
}
clearHoldTimer() {
if (this.holdTimer) {
window.clearTimeout(this.holdTimer);
this.holdTimer = undefined;
}
}
handleCanvasDoubleClick(e) {
if (!this.canvas || this.collapsed || this.readonly || this.justDeletedKeyframe)
return;
const rect = this.canvas.getBoundingClientRect();
this.wrapperEl?.scrollLeft || 0;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.handleDoubleClick(x, y, rect);
}
handleDoubleClick(x, y, rect) {
const { leftMargin, yAxisWidth, topMargin, graphHeight, graphWidth } = this.getGraphDimensions(rect);
// Check if clicking on existing keyframe
const clickedIndex = this.keyframes.findIndex(kf => {
const kfX = leftMargin + yAxisWidth + ((kf.time / this.duration) * graphWidth);
const kfY = topMargin + ((1 - this.normalizeValue(kf.value)) * graphHeight);
const distance = Math.sqrt(Math.pow(x - kfX, 2) + Math.pow(y - kfY, 2));
return distance < 20;
});
if (clickedIndex >= 0)
return; // Don't add if clicking on existing
// Check if click is within graph area
if (x < leftMargin + yAxisWidth || y < topMargin || y > topMargin + graphHeight)
return;
// Snap time to nearest slot (adjust for Y axis offset)
const adjustedX = x - leftMargin - yAxisWidth;
const slotWidth = graphWidth / this.slots;
const slotIndex = Math.round(adjustedX / slotWidth);
// Clamp to 23:59 (23.9833 hours) to prevent 24:00
const time = Math.min((slotIndex / this.slots) * this.duration, 23 + (59 / 60));
// Value is in minValue-maxValue range (adjust for top margin)
const adjustedY = y - topMargin;
const normalizedValue = 1 - (adjustedY / graphHeight);
let value = this.denormalizeValue(normalizedValue);
// Apply snapping if configured
value = this.snapValueToGrid(value);
// Save state before adding
this.saveUndoState();
this.keyframes = [...this.keyframes, { time, value }];
this.sortKeyframes(); // Keep array sorted
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframe-added', {
detail: { time, value },
bubbles: true,
composed: true
}));
}
// Helper method to delete a keyframe
deleteKeyframe(index) {
if (index < 0 || index >= this.keyframes.length)
return;
// Save state before deleting
this.saveUndoState();
const deletedKeyframe = this.keyframes[index];
// Remove from keyframes
this.keyframes = this.keyframes.filter((_, i) => i !== index);
// Clear selection if deleting selected keyframe
if (this.selectedKeyframeIndex === index) {
this.selectedKeyframeIndex = null;
}
else if (this.selectedKeyframeIndex !== null && this.selectedKeyframeIndex > index) {
// Adjust selection index if deleting a keyframe before selected one
this.selectedKeyframeIndex--;
}
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframe-deleted', {
detail: { index, keyframe: deletedKeyframe },
bubbles: true,
composed: true
}));
}
// Find segment (line between keyframes) at given point
findSegmentAtPoint(x, y, rect) {
const { leftMargin, yAxisWidth, topMargin, graphHeight, graphWidth } = this.getGraphDimensions(rect);
// Check each segment
for (let i = 0; i < this.keyframes.length - 1; i++) {
const kf1 = this.keyframes[i];
const kf2 = this.keyframes[i + 1];
const x1 = leftMargin + yAxisWidth + ((kf1.time / this.duration) * graphWidth);
const y1 = topMargin + ((1 - this.normalizeValue(kf1.value)) * graphHeight);
const x2 = leftMargin + yAxisWidth + ((kf2.time / this.duration) * graphWidth);
const y2 = topMargin + ((1 - this.normalizeValue(kf2.value)) * graphHeight);
// Check distance to horizontal line segment (flat hold)
if (x >= x1 && x <= x2) {
const distanceToHorizontal = Math.abs(y - y1);
if (distanceToHorizontal < 10)
return i;
}
// Check distance to vertical line segment (step)
if (Math.abs(x - x2) < 10 && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) {
return i;
}
}
return -1;
}
clearKeyframes() {
// Save state before clearing
this.saveUndoState();
this.keyframes = [];
this.selectedKeyframeIndex = null; // Clear selection
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframes-cleared', {
bubbles: true,
composed: true
}));
}
// Save current state to undo stack
emitUndoStackChanged(action, size, removedCount = 0) {
this.dispatchEvent(new CustomEvent('undo-stack-changed', {
detail: {
action,
size,
removedCount,
keyframeCount: this.keyframes.length
},
bubbles: true,
composed: true
}));
}
saveUndoState() {
// Deep copy current keyframes
const stateCopy = this.keyframes.map(kf => ({ ...kf }));
this.undoStack = [...this.undoStack, stateCopy];
this.emitUndoStackChanged('push', this.undoStack.length);
// Limit undo stack to 50 entries
if (this.undoStack.length > 50) {
const removedCount = this.undoStack.length - 50;
this.undoStack = this.undoStack.slice(-50);
this.emitUndoStackChanged('trim', this.undoStack.length, removedCount);
}
this.updateUndoButtonState();
this.updateNavigationButtonsState();
}
// Public undo method (can be called externally or via Ctrl+Z)
undo() {
if (this.undoStack.length === 0)
return;
// Pop last state from undo stack
const previousState = this.undoStack[this.undoStack.length - 1];
this.undoStack = this.undoStack.slice(0, -1);
this.emitUndoStackChanged('pop', this.undoStack.length, 1);
// Restore previous state
this.keyframes = previousState.map(kf => ({ ...kf }));
this.drawTimeline();
this.updateUndoButtonState();
this.updateNavigationButtonsState();
this.dispatchEvent(new CustomEvent('keyframe-restored', {
detail: { keyframes: this.keyframes },
bubbles: true,
composed: true
}));
}
// Set external undo button reference
setUndoButton(buttonElement) {
this.undoButton = buttonElement;
// Add click handler
if (this.undoButton) {
this.undoButton.addEventListener('click', () => this.undo());
this.updateUndoButtonState();
}
}
// Set external previous button reference
setPreviousButton(buttonElement) {
this.previousButton = buttonElement;
if (this.previousButton) {
this.previousButton.addEventListener('click', () => this.selectPrevious());
this.updateNavigationButtonsState();
}
}
// Set external next button reference
setNextButton(buttonElement) {
this.nextButton = buttonElement;
if (this.nextButton) {
this.nextButton.addEventListener('click', () => this.selectNext());
this.updateNavigationButtonsState();
}
}
// Set external clear button reference
setClearButton(buttonElement) {
if (buttonElement) {
buttonElement.addEventListener('click', () => this.clearKeyframes());
}
}
// Update navigation buttons disabled state
updateNavigationButtonsState() {
const canNavigate = this.keyframes.length >= 2;
if (this.previousButton) {
this.previousButton.disabled = !canNavigate;
}
if (this.nextButton) {
this.nextButton.disabled = !canNavigate;
}
}
// Update undo button disabled state
updateUndoButtonState() {
if (this.undoButton) {
if (this.undoStack.length > 0) {
this.undoButton.disabled = false;
this.undoButton.style.opacity = '1';
}
else {
this.undoButton.disabled = true;
this.undoButton.style.opacity = '0.5';
}
}
}
selectPrevious() {
if (this.keyframes.length === 0)
return;
// Array is already sorted by time
if (this.selectedKeyframeIndex === null || this.selectedKeyframeIndex === 0) {
// No selection or at start - select last keyframe
this.selectedKeyframeIndex = this.keyframes.length - 1;
}
else {
// Select previous
this.selectedKeyframeIndex--;
}
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframe-selected', {
detail: {
index: this.selectedKeyframeIndex,
keyframe: this.keyframes[this.selectedKeyframeIndex]
},
bubbles: true,
composed: true
}));
}
selectNext() {
if (this.keyframes.length === 0)
return;
// Array is already sorted by time
if (this.selectedKeyframeIndex === null || this.selectedKeyframeIndex >= this.keyframes.length - 1) {
// No selection or at end - select first keyframe
this.selectedKeyframeIndex = 0;
}
else {
// Select next
this.selectedKeyframeIndex++;
}
this.drawTimeline();
this.dispatchEvent(new CustomEvent('keyframe-selected', {
detail: {
index: this.selectedKeyframeIndex,
keyframe: this.keyframes[this.selectedKeyframeIndex]
},
bubbles: true,
composed: true
}));
}
toggleConfig() {
this.showConfig = !this.showConfig;
}
toggleCollapse() {
if (!this.allowCollapse)
return; // Don't allow collapsing if disabled
this.hideHoverTooltip();
this.collapsed = !this.collapsed;
// Update canvas height after state change
setTimeout(() => {
this.updateCanvasSize();
this.drawTimeline();
this.checkScrollVisibility();
}, 50);
}
checkScrollVisibility() {
if (!this.wrapperEl)
return;
const isScrollable = !this.collapsed && this.wrapperEl.scrollWidth > this.wrapperEl.clientWidth;
if (!isScrollable) {
this.showScrollNavLeft = false;
this.showScrollNavRight = false;
return;
}
const scrollLeft = this.wrapperEl.scrollLeft;
const maxScroll = this.wrapperEl.scrollWidth - this.wrapperEl.clientWidth;
// Show left button if not at start (with 1px tolerance)
this.showScrollNavLeft = scrollLeft > 1;
// Show right button if not at end (with 1px tolerance)
this.showScrollNavRight = scrollLeft < maxScroll - 1;
}
scrollToStart() {
if (this.wrapperEl) {
this.wrapperEl.scrollTo({ left: 0, behavior: 'smooth' });
}
}
scrollToEnd() {
if (this.wrapperEl) {
this.wrapperEl.scrollTo({ left: this.wrapperEl.scrollWidth, behavior: 'smooth' });
}
}
updateSlots(e) {
const input = e.target;
const newSlots = Math.max(1, Math.min(288, parseInt(input.value) || 1));
if (newSlots !== this.slots) {
this.slots = newSlots;
this.updateCanvasSize();
this.drawTimeline();
}
}
updateDuration(e) {
const input = e.target;
const newDuration = Math.max(1, Math.min(168, parseInt(input.value) || 1));
if (newDuration !== this.duration) {
this.duration = newDuration;
this.drawTimeline();
}
}
updatePreviousDayEnd(e) {
const input = e.target;
if (input.value === '') {
this.previousDayEndValue = undefined;
}
else {
const value = parseFloat(input.value);
this.previousDayEndValue = Math.max(this.minValue, Math.min(this.maxValue, value));
}
this.drawTimeline();
}
updateMinValue(e) {
const input = e.target;
const newMin = parseFloat(input.value);
if (!isNaN(newMin) && newMin < this.maxValue) {
this.minValue = newMin;
this.drawTimeline();
}
}
updateMaxValue(e) {
const input = e.target;
const newMax = parseFloat(input.value);
if (!isNaN(newMax) && newMax > this.minValue) {
this.maxValue = newMax;
this.drawTimeline();
}
}
updateSnapValue(e) {
const input = e.target;
if (input.value === '') {
this.snapValue = 0;
}
else {
const newSnap = parseFloat(input.value);
this.snapValue = !isNaN(newSnap) && newSnap >= 0 ? newSnap : 0;
}
}
updateXAxisLabel(e) {
const input = e.target;
this.xAxisLabel = input.value;
this.drawTimeline();
}
updateYAxisLabel(e) {
const input = e.target;
this.yAxisLabel = input.value;
this.drawTimeline();
}
render() {
const slotDuration = this.duration / this.slots;
const slotMinutes = slotDuration * 60;
return b `
<div class="timeline-container">
${!this.showHeader && this.title && this.allowCollapse ? b `
<div class="timeline-title" @click=${this.toggleCollapse} title="Click to ${this.collapsed ? 'expand' : 'collapse'}">
${this.title}
</div>
` : !this.showHeader && this.title ? b `
<div class="timeline-title" style="cursor: default;">
${this.title}
</div>
` : ''}
${this.showHeader ? b `
<div class="timeline-header">
<span @click=${this.allowCollapse ? this.toggleCollapse : null} title="${this.allowCollapse ? `Click to ${this.collapsed ? 'expand' : 'collapse'}` : ''}" style="cursor: ${this.allowCollapse ? 'pointer' : 'default'};">
${this.title || `Timeline Editor (${this.duration}h • ${this.slots} slots @ ${slotMinutes.toFixed(0)}min)`}
</span>
<div class="timeline-controls">
<button class="secondary" @click=${this.selectPrevious} ?disabled=${this.keyframes.length === 0} title="Previous keyframe">
</button>
<button class="secondary" @click=${this.selectNext} ?disabled=${this.keyframes.length === 0} title="Next keyframe">
</button>
${this.allowCollapse ? b `
<button class="secondary" @click=${this.toggleCollapse}>
${this.collapsed ? '▼ Expand' : '▲ Collapse'}
</button>
` : ''}
<button class="secondary" @click=${this.toggleConfig}>
${this.showConfig ? 'Hide' : 'Show'} Config
</button>
<button class="secondary" @click=${this.undo} ?disabled=${this.undoStack.length === 0}>
↶ Undo
</button>
<button @click=${this.clearKeyframes}>Clear</button>
</div>
</div>
` : ''}
${this.showConfig ? b `
<div class="config-panel">
<div class="config-row">
<label>Duration (hours):</label>
<input
type="number"
min="1"
max="168"
.value=${this.duration.toString()}
@change=${this.updateDuration}
/>
</div>
<div class="config-row">
<label>Time Slots:</label>
<input
type="number"
min="1"
max="288"
.value=${this.slots.toString()}
@change=${this.updateSlots}
/>
</div>
<div class="config-row">
<label>Prev Day End:</label>
<input
type="number"
min="${this.minValue}"
max="${this.maxValue}"
step="0.01"
placeholder="Auto"
.value=${this.previousDayEndValue?.toString() || ''}
@change=${this.updatePreviousDayEnd}
/>
</div>
<div class="config-row">
<label>Min Value:</label>
<input
type="number"
step="0.1"
.value=${this.minValue.toString()}
@change=${this.updateMinValue}
/>
</div>
<div class="config-row">
<label>Max Value:</label>
<input
type="number"
step="0.1"
.value=${this.maxValue.toString()}
@change=${this.updateMaxValue}
/>
</div>
<div class="config-row">
<label>Snap Value:</label>
<input
type="number"
min="0"
step="0.01"
placeholder="None"
.value=${this.snapValue > 0 ? this.snapValue.toString() : ''}
@change=${this.updateSnapValue}
/>
</div>
<div class="config-row">
<label>X Axis Label:</label>
<input
type="text"
placeholder="e.g. Time"
.value=${this.xAxisLabel}
@input=${this.updateXAxisLabel}
/>
</div>
<div class="config-row">
<label>Y Axis Label:</label>
<input
type="text"
placeholder="e.g. Value"
.value=${this.yAxisLabel}
@input=${this.updateYAxisLabel}
/>
</div>
</div>
` : ''}
<div style="position: relative;">
${this.backgroundGraphs.length > 0 ? b `
<div class="graph-legend ${this.legendCollapsed ? 'collapsed' : ''}">
<div class="graph-legend-header" @click=${this.toggleLegend}>
<span class="graph-legend-toggle">▶</span>
<span>Legend</span>
</div>
${!this.legendCollapsed ? b `
<div class="graph-legend-items">
${this.backgroundGraphs.map((bgGraph, graphIndex) => b `
<div class="graph-legend-item" title=${bgGraph.label || `Entity ${graphIndex + 1}`}>
<span class="graph-legend-swatch" style="background: ${this.getBackgroundGraphColor(bgGraph, graphIndex)};"></span>
<span class="graph-legend-label">${bgGraph.label || `Entity ${graphIndex + 1}`}</span>
</div>
`)}
</div>
` : ''}
</div>
` : ''}
<div class="timeline-canvas-wrapper ${this.collapsed ? '' : 'expanded'}" @click=${this.collapsed && this.allowCollapse ? this.toggleCollapse : null}>
<div class="timeline-canvas ${this.collapsed ? 'collapsed' : ''} ${this.isDragging ? 'dragging' : ''}">
<canvas
style="touch-action: none;"
@click=${this.handleCanvasClick}
@dblclick=${this.handleCanvasDoubleClick}
@contextmenu=${this.handleCanvasContextMenu}
@mousedown=${this.handleCanvasMouseDown}
@mousemove=${this.handleCanvasMouseMove}
@mouseup=${this.handleCanvasMouseUp}
@mouseleave=${this.handleCanvasMouseLeave}
@touchstart=${this.handleCanvasMouseDown}
@touchmove=${this.handleCanvasMouseMove}
@touchend=${this.handleCanvasMouseUp}
@touchcancel=${this.handleCanvasMouseUp}
></canvas>
<div class="cursor-tooltip" hidden></div>
</div>
${this.collapsed ? b `<div class="expand-hint">Click to expand</div>` : ''}
</div>
${this.showScrollNavLeft ? b `
<div class="scroll-nav left" @click=${this.scrollToStart}>◀</div>
` : ''}
${this.showScrollNavRight ? b `
<div class="scroll-nav right" @click=${this.scrollToEnd}>▶</div>
` : ''}
</div>
<div class="info">
Double-click to add • Drag to move • Right-click/hold to delete • ${this.keyframes.length} keyframes
</div>
</div>
`;
}
};
KeyframeTimeline.styles = i$3 `
:host {
display: block;
width: 100%;
--timeline-height: 400px;
--timeline-height-collapsed: 100px;
--timeline-bg: var(--card-background-color, #1c1c1c);
--timeline-track: var(--secondary-background-color, #2c2c2c);
--timeline-ruler: var(--divider-color, rgba(255, 255, 255, 0.12));
--keyframe-color: var(--accent-color, var(--primary-color, #03a9f4));
--keyframe-selected-color: var(--success-color, #4caf50);
--keyframe-dragging-color: var(--warning-color, #ff9800);
--canvas-text-primary: var(--primary-text-color, rgba(255, 255, 255, 0.9));
--canvas-text-secondary: var(--secondary-text-color, rgba(255, 255, 255, 0.7));
--canvas-grid-line: rgba(68, 68, 68, 0.3);
--canvas-grid-line-major: rgba(68, 68, 68, 0.5);
--canvas-label-bg: var(--card-background-color, rgba(0, 0, 0, 0.7));
--indicator-color: var(--accent-color, #ff9800);
}
.timeline-container {
background: var(--background);
border-radius: 4px;
padding: 0;
user-select: none;
}
.timeline-title {
font-size: 16px;
font-weight: 600;
color: var(--primary-text-color, #e1e1e1);
cursor: pointer;
user-select: none;
margin-bottom: 12px;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
color: var(--primary-text-color, #e1e1e1);
}
.timeline-header span {
cursor: pointer;
user-select: none;
}
.timeline-controls {
display: flex;
gap: 8px;
}
button {
background: var(--primary-color, #03a9f4);
color: var(--primary-text-color, white);
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
opacity: 0.9;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button.secondary {
background: var(--secondary-background-color, #2c2c2c);
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
}
.config-panel {
background: var(--secondary-background-color, #2c2c2c);
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
}
.config-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.config-row label {
min-width: 80px;
font-size: 14px;
}
input[type="number"], input[type="text"] {
background: var(--timeline-track);
color: var(--primary-text-color, #e1e1e1);
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
}
.slot-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 8px;
}
.slot-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
background: var(--timeline-track);
border-radius: 4px;
}
.slot-color {
width: 20px;
height: 20px;
border-radius: 3px;
cursor: pointer;
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
}
.timeline-canvas-wrapper {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
background: var(--timeline-track);
border-radius: 4px;
transition: height 0.3s ease;
cursor: pointer;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb-color, rgba(128, 128, 128, 0.4)) transparent;
}
.timeline-canvas-wrapper::-webkit-scrollbar {
height: 8px;
}
.timeline-canvas-wrapper::-webkit-scrollbar-track {
background: transparent;
}
.timeline-canvas-wrapper::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-color, rgba(128, 128, 128, 0.4));
border-radius: 4px;
}
.timeline-canvas-wrapper::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-color-hover, rgba(128, 128, 128, 0.6));
}
.timeline-canvas-wrapper.expanded {
cursor: default;
}
.timeline-canvas-wrapper:not(.expanded) {
overflow-x: hidden;
}
.timeline-canvas {
min-width: max(100%, 800px);
height: var(--timeline-height);
background: var(--timeline-track);
cursor: crosshair;
position: relative;
touch-action: none;
transition: height 0.3s ease;
}
.timeline-canvas.collapsed {
min-width: 100%;
height: var(--timeline-height-collapsed);
cursor: pointer;
}
.timeline-canvas.dragging {
cursor: grabbing;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
@media (max-width: 800px) {
.timeline-canvas:not(.collapsed) {
min-width: 800px;
}
}
.expand-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--card-background-color, rgba(0, 0, 0, 0.7));
color: var(--primary-text-color, white);
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.2));
}
.timeline-canvas-wrapper:hover .expand-hint {
opacity: 1;
}
.timeline-canvas-wrapper.expanded .expand-hint {
display: none;
}
.scroll-nav {
position: absolute;
top: 10px;
transform: none;
background: var(--card-background-color, rgba(0, 0, 0, 0.6));
color: var(--primary-text-color, white);
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.2));
border-radius: 4px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: background 0.2s ease;
user-select: none;
pointer-events: auto; /* Ensure button is clickable but doesn't block canvas unnecessarily */
}
.scroll-nav:hover {
background: var(--secondary-background-color, rgba(0, 0, 0, 0.8));
}
.scroll-nav.left {
left: -10px;
}
.scroll-nav.right {
right: -10px;
}
.graph-legend {
position: absolute;
top: 10px;
left: 70px;
z-index: 12;
min-width: 160px;
max-width: 240px;
background: rgba(0, 0, 0, 0.5);
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.2));
border-radius: 6px;
color: var(--canvas-text-primary);
font-size: 12px;
}
.graph-legend-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
cursor: pointer;
user-select: none;
font-weight: 600;
}
.graph-legend-toggle {
display: inline-block;
font-size: 11px;
transform: rotate(90deg);
transition: transform 0.2s ease;
}
.graph-legend.collapsed .graph-legend-toggle {
transform: rotate(0deg);
}
.graph-legend-items {
padding: 0 8px 8px 8px;
display: flex;
flex-direction: column;
gap: 6px;
max-height: 180px;
overflow-y: auto;
}
.graph-legend-item {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.graph-legend-swatch {
width: 10px;
height: 10px;
border-radius: 2px;
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.3));
flex: 0 0 auto;
}
.graph-legend-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--canvas-text-secondary);
}
.cursor-tooltip {
position: absolute;
z-index: 20;
pointer-events: none;
background-color: rgba(var(--rgb-card-background-color, 224, 224, 224), 0.9);
background-image: none;
border: 1px solid var(--primary-color);
border-radius: 4px;
padding: 8px;
font-size: 13px;
line-height: 18px;
white-space: nowrap;
}
.cursor-tooltip-line {
color: var(--keyframe-color);
font-weight: 600;
opacity: 1;
}
.info {
margin-top: 12px;
font-size: 12px;
color: var(--secondary-text-color, #9e9e9e);
}
`;
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "duration", void 0);
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "slots", void 0);
__decorate([
n({ type: Array })
], KeyframeTimeline.prototype, "keyframes", void 0);
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "previousDayEndValue", void 0);
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "minValue", void 0);
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "maxValue", void 0);
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "snapValue", void 0);
__decorate([
n({ type: String })
], KeyframeTimeline.prototype, "xAxisLabel", void 0);
__decorate([
n({ type: String })
], KeyframeTimeline.prototype, "yAxisLabel", void 0);
__decorate([
n({ type: String })
], KeyframeTimeline.prototype, "title", void 0);
__decorate([
n({ type: Boolean })
], KeyframeTimeline.prototype, "showHeader", void 0);
__decorate([
n({ type: Boolean })
], KeyframeTimeline.prototype, "allowCollapse", void 0);
__decorate([
n({ type: Boolean })
], KeyframeTimeline.prototype, "readonly", void 0);
__decorate([
n({ type: Number })
], KeyframeTimeline.prototype, "indicatorTime", void 0);
__decorate([
n({ type: Boolean })
], KeyframeTimeline.prototype, "showCurrentTime", void 0);
__decorate([
n({ type: Array })
], KeyframeTimeline.prototype, "backgroundGraphs", void 0);
__decorate([
n({ type: Array })
], KeyframeTimeline.prototype, "advanceHistory", void 0);
__decorate([
n({ type: String, attribute: false })
], KeyframeTimeline.prototype, "tooltipMode", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "canvasWidth", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "canvasHeight", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "showConfig", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "draggingIndex", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "draggingSegment", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "selectedKeyframeIndex", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "collapsed", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "showScrollNavLeft", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "showScrollNavRight", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "undoStack", void 0);
__decorate([
r()
], KeyframeTimeline.prototype, "legendCollapsed", void 0);
KeyframeTimeline = __decorate([
t('keyframe-timeline')
], KeyframeTimeline);
export { KeyframeTimeline };
//# sourceMappingURL=keyframe-timeline.js.map