kempo-ui 0.4.24 → 0.4.26

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.
@@ -1 +1 @@
1
- import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";export default class s extends e{static properties={data:{type:String,reflect:!0}};constructor(){super(),this.data="{}"}connectedCallback(){super.connectedCallback(),this.style.display="contents"}set(t,e){const s=JSON.parse(this.data||"{}"),a=!(t in s),n=s[t];s[t]=e,this.data=JSON.stringify(s),this.dispatchEvent(new CustomEvent(a?"context:create":"context:set",{detail:a?{key:t,value:e}:{key:t,value:e,oldValue:n},bubbles:!0,composed:!0}))}get(t){return JSON.parse(this.data||"{}")[t]}has(t){return t in JSON.parse(this.data||"{}")}delete(t){const e=JSON.parse(this.data||"{}");if(!(t in e))return;const s=e[t];delete e[t],this.data=JSON.stringify(e),this.dispatchEvent(new CustomEvent("context:delete",{detail:{key:t,value:s},bubbles:!0,composed:!0}))}clear(){for(const t of Object.keys(JSON.parse(this.data||"{}")))this.delete(t)}getData(){return JSON.parse(this.data||"{}")}renderLightDom(){return t``}}customElements.define("k-context",s);
1
+ import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";const s=Symbol("persist");export default class i extends e{static properties={data:{type:String,reflect:!0},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this[s]=()=>{if(this.persistentId&&window?.localStorage)try{window.localStorage.setItem(`context-persistent-id-${this.persistentId}`,this.data)}catch{}},this.data="{}",this.persistentId=null}connectedCallback(){if(super.connectedCallback(),this.style.display="contents",this.persistentId&&window?.localStorage){const t=window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);t&&(this.data=t)}}set(t,e){const i=JSON.parse(this.data||"{}"),a=!(t in i),n=i[t];i[t]=e,this.data=JSON.stringify(i),this[s](),this.dispatchEvent(new CustomEvent(a?"context:create":"context:set",{detail:a?{key:t,value:e}:{key:t,value:e,oldValue:n},bubbles:!0,composed:!0}))}get(t){return JSON.parse(this.data||"{}")[t]}has(t){return t in JSON.parse(this.data||"{}")}delete(t){const e=JSON.parse(this.data||"{}");if(!(t in e))return;const i=e[t];delete e[t],this.data=JSON.stringify(e),this[s](),this.dispatchEvent(new CustomEvent("context:delete",{detail:{key:t,value:i},bubbles:!0,composed:!0}))}clear(){for(const t of Object.keys(JSON.parse(this.data||"{}")))this.delete(t)}getData(){return JSON.parse(this.data||"{}")}renderLightDom(){return t``}}customElements.define("k-context",i);
@@ -1,4 +1,4 @@
1
- import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from"../lit-all.min.js";import{boolExists as i}from"../utils/propConverters.js";import"./Icon.js";export default class l extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:i},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:i},global:{type:Boolean,reflect:!0,converter:i}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){super.updated(e),e.has("fullscreen")&&(this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))),(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}static open(e,t=0,n={}){const{keyboardControls:i=!0,onClose:l}=n,s=document.createElement("div");s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="0",s.style.height="0",s.style.overflow="hidden",s.style.zIndex="80";const r=e.map(e=>{const t=document.createElement("k-photo-viewer");return t.src=e.src,t.alt=e.alt||"",t.keyboardControls=i,void 0!==e.caption&&("string"==typeof e.caption?t.innerHTML=e.caption:o(e.caption,t)),s.appendChild(t),t});(document.querySelector("[data-overlay-root]")||document.body).appendChild(s);const a=()=>{r.some(e=>e.fullscreen)||(r.forEach(e=>e.removeEventListener("fullscreenclose",a)),s.remove(),l?.())};r.forEach(e=>e.addEventListener("fullscreenclose",a));const c=Math.min(Math.max(t,0),r.length-1);return r[c].open(),r[c]}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
1
+ import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from"../lit-all.min.js";import{boolExists as l}from"../utils/propConverters.js";import"./Icon.js";export default class i extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:l},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:l},global:{type:Boolean,reflect:!0,converter:l}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){if(super.updated(e),e.has("fullscreen")){this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),document.body.classList.toggle("no-scroll",this.fullscreen);const e=this.closest("[data-overlay-root]");e&&e.classList.toggle("no-scroll",this.fullscreen),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))}(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}static open(e,t=0,n={}){const{keyboardControls:l=!0,onClose:i}=n,s=document.createElement("div");s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="0",s.style.height="0",s.style.overflow="hidden",s.style.zIndex="80";const r=e.map(e=>{const t=document.createElement("k-photo-viewer");return t.src=e.src,t.alt=e.alt||"",t.keyboardControls=l,void 0!==e.caption&&("string"==typeof e.caption?t.innerHTML=e.caption:o(e.caption,t)),s.appendChild(t),t});(document.querySelector("[data-overlay-root]")||document.body).appendChild(s);const a=()=>{r.some(e=>e.fullscreen)||(r.forEach(e=>e.removeEventListener("fullscreenclose",a)),s.remove(),i?.())};r.forEach(e=>e.addEventListener("fullscreenclose",a));const c=Math.min(Math.max(t,0),r.length-1);return r[c].open(),r[c]}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
2
2
  :host {
3
3
  display: block;
4
4
  }
@@ -132,4 +132,4 @@ import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from".
132
132
  </div>
133
133
  </div>
134
134
  </div>
135
- `}}customElements.define("k-photo-viewer",l);
135
+ `}}customElements.define("k-photo-viewer",i);
@@ -505,6 +505,7 @@
505
505
  <a href="#basicUsage">Basic Usage</a><br />
506
506
  <a href="#events">Listening to Events</a><br />
507
507
  <a href="#usingInComponents">Using in Components</a><br />
508
+ <a href="#persistence">Persistence</a><br />
508
509
  <a href="#interactiveDemo">Interactive Demo</a><br />
509
510
 
510
511
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
@@ -565,6 +566,17 @@
565
566
  </div>
566
567
  <p>To react to context changes from inside a shadow-DOM component, listen on the context element directly — events bubble and are <code>composed: true</code> so they also cross shadow boundaries.</p>
567
568
 
569
+ <h3 id="persistence"><a href="#persistence" class="no-link">Persistence</a></h3>
570
+ <p>Set a <code>persistent-id</code> to automatically save the store to <code>localStorage</code> and restore it on the next page load — the same convention used by <code>Split</code>, <code>Accordion</code>, <code>Tabs</code> and <code>Aside</code>. The data is reloaded in <code>connectedCallback</code> (before descendants read it) and re-saved on every <code>set()</code>, <code>delete()</code> and <code>clear()</code>.</p>
571
+ <div class="row -mx">
572
+ <div class="col m-span-12 px">
573
+ <k-card label="HTML">
574
+ <pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">k-context</span> <span class="hljs-attr">persistent-id</span>=<span class="hljs-string">&quot;app-settings&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-context</span>&gt;</span></code></pre>
575
+ </k-card>
576
+ </div>
577
+ </div>
578
+ <p>Because the entire store is JSON-serialized into a single value, keep persisted contexts to small, serializable state (settings, selections, ids) — not large or non-JSON data. Scope what you persist by giving config its own <code>persistent-id</code> context and leaving transient state in a separate, non-persistent <code>k-context</code>.</p>
579
+
568
580
  <h3 id="interactiveDemo"><a href="#interactiveDemo" class="no-link">Interactive Demo</a></h3>
569
581
  <div class="row -mx">
570
582
  <div class="col m-span-12 t-span-6 px">
@@ -630,6 +642,11 @@
630
642
  <td><code>String</code> (JSON)</td>
631
643
  <td>A JSON string representing the initial store state. Reflects back to the attribute as state changes via the component's methods. Defaults to <code>'{}'</code>.</td>
632
644
  </tr>
645
+ <tr>
646
+ <td><code>persistent-id</code></td>
647
+ <td><code>String</code></td>
648
+ <td>When set, the store is auto-saved to <code>localStorage</code> (key <code>context-persistent-id-&lt;id&gt;</code>) on every change and restored on connect. Defaults to <code>null</code> (no persistence).</td>
649
+ </tr>
633
650
  </tbody>
634
651
  </table>
635
652
  </div>
@@ -1 +1 @@
1
- import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";export default class s extends e{static properties={data:{type:String,reflect:!0}};constructor(){super(),this.data="{}"}connectedCallback(){super.connectedCallback(),this.style.display="contents"}set(t,e){const s=JSON.parse(this.data||"{}"),a=!(t in s),n=s[t];s[t]=e,this.data=JSON.stringify(s),this.dispatchEvent(new CustomEvent(a?"context:create":"context:set",{detail:a?{key:t,value:e}:{key:t,value:e,oldValue:n},bubbles:!0,composed:!0}))}get(t){return JSON.parse(this.data||"{}")[t]}has(t){return t in JSON.parse(this.data||"{}")}delete(t){const e=JSON.parse(this.data||"{}");if(!(t in e))return;const s=e[t];delete e[t],this.data=JSON.stringify(e),this.dispatchEvent(new CustomEvent("context:delete",{detail:{key:t,value:s},bubbles:!0,composed:!0}))}clear(){for(const t of Object.keys(JSON.parse(this.data||"{}")))this.delete(t)}getData(){return JSON.parse(this.data||"{}")}renderLightDom(){return t``}}customElements.define("k-context",s);
1
+ import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";const s=Symbol("persist");export default class i extends e{static properties={data:{type:String,reflect:!0},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this[s]=()=>{if(this.persistentId&&window?.localStorage)try{window.localStorage.setItem(`context-persistent-id-${this.persistentId}`,this.data)}catch{}},this.data="{}",this.persistentId=null}connectedCallback(){if(super.connectedCallback(),this.style.display="contents",this.persistentId&&window?.localStorage){const t=window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);t&&(this.data=t)}}set(t,e){const i=JSON.parse(this.data||"{}"),a=!(t in i),n=i[t];i[t]=e,this.data=JSON.stringify(i),this[s](),this.dispatchEvent(new CustomEvent(a?"context:create":"context:set",{detail:a?{key:t,value:e}:{key:t,value:e,oldValue:n},bubbles:!0,composed:!0}))}get(t){return JSON.parse(this.data||"{}")[t]}has(t){return t in JSON.parse(this.data||"{}")}delete(t){const e=JSON.parse(this.data||"{}");if(!(t in e))return;const i=e[t];delete e[t],this.data=JSON.stringify(e),this[s](),this.dispatchEvent(new CustomEvent("context:delete",{detail:{key:t,value:i},bubbles:!0,composed:!0}))}clear(){for(const t of Object.keys(JSON.parse(this.data||"{}")))this.delete(t)}getData(){return JSON.parse(this.data||"{}")}renderLightDom(){return t``}}customElements.define("k-context",i);
@@ -1,4 +1,4 @@
1
- import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from"../lit-all.min.js";import{boolExists as i}from"../utils/propConverters.js";import"./Icon.js";export default class l extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:i},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:i},global:{type:Boolean,reflect:!0,converter:i}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){super.updated(e),e.has("fullscreen")&&(this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))),(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}static open(e,t=0,n={}){const{keyboardControls:i=!0,onClose:l}=n,s=document.createElement("div");s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="0",s.style.height="0",s.style.overflow="hidden",s.style.zIndex="80";const r=e.map(e=>{const t=document.createElement("k-photo-viewer");return t.src=e.src,t.alt=e.alt||"",t.keyboardControls=i,void 0!==e.caption&&("string"==typeof e.caption?t.innerHTML=e.caption:o(e.caption,t)),s.appendChild(t),t});(document.querySelector("[data-overlay-root]")||document.body).appendChild(s);const a=()=>{r.some(e=>e.fullscreen)||(r.forEach(e=>e.removeEventListener("fullscreenclose",a)),s.remove(),l?.())};r.forEach(e=>e.addEventListener("fullscreenclose",a));const c=Math.min(Math.max(t,0),r.length-1);return r[c].open(),r[c]}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
1
+ import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from"../lit-all.min.js";import{boolExists as l}from"../utils/propConverters.js";import"./Icon.js";export default class i extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:l},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:l},global:{type:Boolean,reflect:!0,converter:l}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){if(super.updated(e),e.has("fullscreen")){this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),document.body.classList.toggle("no-scroll",this.fullscreen);const e=this.closest("[data-overlay-root]");e&&e.classList.toggle("no-scroll",this.fullscreen),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))}(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}static open(e,t=0,n={}){const{keyboardControls:l=!0,onClose:i}=n,s=document.createElement("div");s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="0",s.style.height="0",s.style.overflow="hidden",s.style.zIndex="80";const r=e.map(e=>{const t=document.createElement("k-photo-viewer");return t.src=e.src,t.alt=e.alt||"",t.keyboardControls=l,void 0!==e.caption&&("string"==typeof e.caption?t.innerHTML=e.caption:o(e.caption,t)),s.appendChild(t),t});(document.querySelector("[data-overlay-root]")||document.body).appendChild(s);const a=()=>{r.some(e=>e.fullscreen)||(r.forEach(e=>e.removeEventListener("fullscreenclose",a)),s.remove(),i?.())};r.forEach(e=>e.addEventListener("fullscreenclose",a));const c=Math.min(Math.max(t,0),r.length-1);return r[c].open(),r[c]}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
2
2
  :host {
3
3
  display: block;
4
4
  }
@@ -132,4 +132,4 @@ import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from".
132
132
  </div>
133
133
  </div>
134
134
  </div>
135
- `}}customElements.define("k-photo-viewer",l);
135
+ `}}customElements.define("k-photo-viewer",i);
@@ -9,6 +9,7 @@
9
9
  <a href="#basicUsage">Basic Usage</a><br />
10
10
  <a href="#events">Listening to Events</a><br />
11
11
  <a href="#usingInComponents">Using in Components</a><br />
12
+ <a href="#persistence">Persistence</a><br />
12
13
  <a href="#interactiveDemo">Interactive Demo</a><br />
13
14
 
14
15
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
@@ -69,6 +70,17 @@
69
70
  </div>
70
71
  <p>To react to context changes from inside a shadow-DOM component, listen on the context element directly — events bubble and are <code>composed: true</code> so they also cross shadow boundaries.</p>
71
72
 
73
+ <h3 id="persistence"><a href="#persistence" class="no-link">Persistence</a></h3>
74
+ <p>Set a <code>persistent-id</code> to automatically save the store to <code>localStorage</code> and restore it on the next page load — the same convention used by <code>Split</code>, <code>Accordion</code>, <code>Tabs</code> and <code>Aside</code>. The data is reloaded in <code>connectedCallback</code> (before descendants read it) and re-saved on every <code>set()</code>, <code>delete()</code> and <code>clear()</code>.</p>
75
+ <div class="row -mx">
76
+ <div class="col m-span-12 px">
77
+ <k-card label="HTML">
78
+ <pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">k-context</span> <span class="hljs-attr">persistent-id</span>=<span class="hljs-string">&quot;app-settings&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-context</span>&gt;</span></code></pre>
79
+ </k-card>
80
+ </div>
81
+ </div>
82
+ <p>Because the entire store is JSON-serialized into a single value, keep persisted contexts to small, serializable state (settings, selections, ids) — not large or non-JSON data. Scope what you persist by giving config its own <code>persistent-id</code> context and leaving transient state in a separate, non-persistent <code>k-context</code>.</p>
83
+
72
84
  <h3 id="interactiveDemo"><a href="#interactiveDemo" class="no-link">Interactive Demo</a></h3>
73
85
  <div class="row -mx">
74
86
  <div class="col m-span-12 t-span-6 px">
@@ -134,6 +146,11 @@
134
146
  <td><code>String</code> (JSON)</td>
135
147
  <td>A JSON string representing the initial store state. Reflects back to the attribute as state changes via the component's methods. Defaults to <code>'{}'</code>.</td>
136
148
  </tr>
149
+ <tr>
150
+ <td><code>persistent-id</code></td>
151
+ <td><code>String</code></td>
152
+ <td>When set, the store is auto-saved to <code>localStorage</code> (key <code>context-persistent-id-&lt;id&gt;</code>) on every change and restored on connect. Defaults to <code>null</code> (no persistence).</td>
153
+ </tr>
137
154
  </tbody>
138
155
  </table>
139
156
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.4.24",
3
+ "version": "0.4.26",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -4,18 +4,51 @@
4
4
  A non-rendering state container for sharing data between sibling and descendant
5
5
  components, similar in purpose to React's useContext. Place it as an ancestor
6
6
  of components that need shared state; children locate it via closest('k-context').
7
+
8
+ Set a `persistent-id` to auto-save/load the data to localStorage (the same
9
+ convention used by Split, Accordion, Tabs and Aside).
7
10
  */
8
11
  import { html } from '../lit-all.min.js';
9
12
  import LightComponent from './LightComponent.js';
10
13
 
14
+ /*
15
+ Symbols
16
+ */
17
+ const persist = Symbol('persist');
18
+
11
19
  export default class Context extends LightComponent {
20
+ /*
21
+ Reactive Properties / Attributes
22
+ */
12
23
  static properties = {
13
- data: { type: String, reflect: true }
24
+ data: { type: String, reflect: true },
25
+ persistentId: { type: String, reflect: true, attribute: 'persistent-id' }
14
26
  };
15
27
 
28
+ /*
29
+ Constructor
30
+ */
16
31
  constructor() {
17
32
  super();
33
+
34
+ /*
35
+ Private Methods
36
+ */
37
+ // Mirror the data blob into localStorage whenever a persistent-id is set, so the
38
+ // next page load can restore it. Guarded so a full/blocked store (private mode,
39
+ // quota) never throws out of a set/delete.
40
+ this[persist] = () => {
41
+ if(!this.persistentId || !window?.localStorage) return;
42
+ try {
43
+ window.localStorage.setItem(`context-persistent-id-${this.persistentId}`, this.data);
44
+ } catch { /* storage unavailable — keep working in-memory */ }
45
+ };
46
+
47
+ /*
48
+ Init Props
49
+ */
18
50
  this.data = '{}';
51
+ this.persistentId = null;
19
52
  }
20
53
 
21
54
  /*
@@ -24,6 +57,14 @@ export default class Context extends LightComponent {
24
57
  connectedCallback() {
25
58
  super.connectedCallback();
26
59
  this.style.display = 'contents';
60
+ // Restore persisted state up front, before descendants read it on their own
61
+ // connect. Setting data directly (rather than per-key set) intentionally skips
62
+ // events: the context is an ancestor, so children read the loaded values via
63
+ // closest('k-context').get() when they first render.
64
+ if(this.persistentId && window?.localStorage){
65
+ const stored = window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);
66
+ if(stored) this.data = stored;
67
+ }
27
68
  }
28
69
 
29
70
  /*
@@ -35,6 +76,7 @@ export default class Context extends LightComponent {
35
76
  const oldValue = obj[key];
36
77
  obj[key] = value;
37
78
  this.data = JSON.stringify(obj);
79
+ this[persist]();
38
80
  this.dispatchEvent(new CustomEvent(isNew ? 'context:create' : 'context:set', {
39
81
  detail: isNew ? { key, value } : { key, value, oldValue },
40
82
  bubbles: true,
@@ -56,6 +98,7 @@ export default class Context extends LightComponent {
56
98
  const value = obj[key];
57
99
  delete obj[key];
58
100
  this.data = JSON.stringify(obj);
101
+ this[persist]();
59
102
  this.dispatchEvent(new CustomEvent('context:delete', {
60
103
  detail: { key, value },
61
104
  bubbles: true,
@@ -104,7 +104,11 @@ export default class PhotoViewer extends ShadowComponent {
104
104
  }));
105
105
  this.dispatchEvent(new CustomEvent(this.fullscreen ? 'fullscreen' : 'fullscreenclose'));
106
106
  this.updateNavigationState();
107
-
107
+
108
+ document.body.classList.toggle('no-scroll', this.fullscreen);
109
+ const overlayRoot = this.closest('[data-overlay-root]');
110
+ if(overlayRoot) overlayRoot.classList.toggle('no-scroll', this.fullscreen);
111
+
108
112
  if(this.keyboardControls) {
109
113
  if(this.fullscreen) {
110
114
  document.addEventListener('keydown', this.handleKeydown);
@@ -10,6 +10,17 @@ const createContext = async () => {
10
10
  return { container, el };
11
11
  };
12
12
 
13
+ const createPersistentContext = async (persistentId) => {
14
+ const container = document.createElement('div');
15
+ container.innerHTML = `<k-context persistent-id="${persistentId}"></k-context>`;
16
+ document.body.appendChild(container);
17
+ const el = container.querySelector('k-context');
18
+ await el.updateComplete;
19
+ return { container, el };
20
+ };
21
+
22
+ const storageKey = (id) => `context-persistent-id-${id}`;
23
+
13
24
  const cleanup = (container) => {
14
25
  if(container && container.parentNode){
15
26
  container.parentNode.removeChild(container);
@@ -234,5 +245,68 @@ export default {
234
245
  }
235
246
  cleanup(container);
236
247
  pass('getData() returns a shallow copy of all data');
248
+ },
249
+
250
+ /*
251
+ persistent-id (localStorage auto save/load)
252
+ */
253
+ 'set() writes data to localStorage when persistent-id is set': async ({pass, fail}) => {
254
+ const id = 'persist-set-' + Date.now();
255
+ window.localStorage.removeItem(storageKey(id));
256
+ const { container, el } = await createPersistentContext(id);
257
+ el.set('theme', 'dark');
258
+ let stored;
259
+ try { stored = JSON.parse(window.localStorage.getItem(storageKey(id))); } catch(e) {
260
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
261
+ return fail('localStorage value is not valid JSON');
262
+ }
263
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
264
+ if(!stored || stored.theme !== 'dark'){
265
+ return fail(`Expected stored {theme:'dark'}, got ${JSON.stringify(stored)}`);
266
+ }
267
+ pass('set() persisted to localStorage');
268
+ },
269
+
270
+ 'restores data from localStorage on connect': async ({pass, fail}) => {
271
+ const id = 'persist-load-' + Date.now();
272
+ window.localStorage.setItem(storageKey(id), JSON.stringify({ count: 7 }));
273
+ const { container, el } = await createPersistentContext(id);
274
+ const value = el.get('count');
275
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
276
+ if(value !== 7){
277
+ return fail(`Expected restored count to be 7, got ${value}`);
278
+ }
279
+ pass('data restored from localStorage on connect');
280
+ },
281
+
282
+ 'delete() updates localStorage': async ({pass, fail}) => {
283
+ const id = 'persist-delete-' + Date.now();
284
+ window.localStorage.removeItem(storageKey(id));
285
+ const { container, el } = await createPersistentContext(id);
286
+ el.set('a', 1);
287
+ el.set('b', 2);
288
+ el.delete('a');
289
+ let stored;
290
+ try { stored = JSON.parse(window.localStorage.getItem(storageKey(id))); } catch(e) {
291
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
292
+ return fail('localStorage value is not valid JSON');
293
+ }
294
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
295
+ if(!stored || 'a' in stored || stored.b !== 2){
296
+ return fail(`Expected stored to drop 'a' and keep b:2, got ${JSON.stringify(stored)}`);
297
+ }
298
+ pass('delete() updated localStorage');
299
+ },
300
+
301
+ 'does not write to localStorage without a persistent-id': async ({pass, fail}) => {
302
+ const before = window.localStorage.length;
303
+ const { container, el } = await createContext();
304
+ el.set('x', 1);
305
+ const after = window.localStorage.length;
306
+ cleanup(container);
307
+ if(after !== before){
308
+ return fail('A context without persistent-id should not touch localStorage');
309
+ }
310
+ pass('no persistent-id means no localStorage writes');
237
311
  }
238
312
  };
@@ -872,5 +872,69 @@ export default {
872
872
  }
873
873
 
874
874
  pass('Container mounts into the [data-overlay-root] element');
875
+ },
876
+
877
+ /*
878
+ Scroll Lock Tests
879
+ */
880
+ 'should add no-scroll to body when opened': async ({pass, fail}) => {
881
+ const { container, viewer } = await createPhotoViewer();
882
+
883
+ viewer.open();
884
+ await viewer.updateComplete;
885
+
886
+ const hasClass = document.body.classList.contains('no-scroll');
887
+ viewer.close();
888
+ await viewer.updateComplete;
889
+
890
+ cleanup(container);
891
+ if(!hasClass){
892
+ fail('document.body should have no-scroll class when fullscreen');
893
+ return;
894
+ }
895
+
896
+ pass('document.body gets no-scroll class when opened');
897
+ },
898
+
899
+ 'should remove no-scroll from body when closed': async ({pass, fail}) => {
900
+ const { container, viewer } = await createPhotoViewer({ fullscreen: true });
901
+
902
+ viewer.close();
903
+ await viewer.updateComplete;
904
+
905
+ const hasClass = document.body.classList.contains('no-scroll');
906
+ cleanup(container);
907
+
908
+ if(hasClass){
909
+ fail('document.body should not have no-scroll class when closed');
910
+ return;
911
+ }
912
+
913
+ pass('document.body loses no-scroll class when closed');
914
+ },
915
+
916
+ 'should add no-scroll to the nearest [data-overlay-root] when opened': async ({pass, fail}) => {
917
+ const root = document.createElement('div');
918
+ root.setAttribute('data-overlay-root', '');
919
+ document.body.appendChild(root);
920
+
921
+ const viewer = document.createElement('k-photo-viewer');
922
+ root.appendChild(viewer);
923
+ await viewer.updateComplete;
924
+
925
+ viewer.open();
926
+ await viewer.updateComplete;
927
+
928
+ const hasClass = root.classList.contains('no-scroll');
929
+ viewer.close();
930
+ await viewer.updateComplete;
931
+ root.remove();
932
+
933
+ if(!hasClass){
934
+ fail('[data-overlay-root] should have no-scroll class when a descendant viewer is fullscreen');
935
+ return;
936
+ }
937
+
938
+ pass('[data-overlay-root] gets no-scroll class when opened');
875
939
  }
876
940
  };