luxen-ui 0.2.1 → 0.3.0

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.
@@ -2240,7 +2240,7 @@
2240
2240
  {
2241
2241
  "kind": "variable",
2242
2242
  "name": "r",
2243
- "default": "class extends e{constructor(...e){super(...e),this._cells=[],this._separatorEl=null,this._initialized=!1,this._updateCells=()=>{let e=this._input.value,t=this._input.maxLength||6,n=Math.min(this._input.selectionStart??0,t-1),r=document.activeElement===this._input;for(let t=0;t<this._cells.length;t++){let i=this._cells[t],a=i.firstElementChild,o=e[t]??``;a.textContent=o,o?i.setAttribute(`data-filled`,``):i.removeAttribute(`data-filled`),r&&t===n?i.setAttribute(`data-active`,``):i.removeAttribute(`data-active`)}},this._clearCells=()=>{for(let e of this._cells)e.removeAttribute(`data-active`)}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),requestAnimationFrame(()=>this._setup())}disconnectedCallback(){super.disconnectedCallback(),this._teardown()}_setup(){let e=this.querySelector(`input`);if(!e)return;this._input=e;let t=Number(getComputedStyle(this).getPropertyValue(`--digits`).trim())||6,n={type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,maxlength:String(t),pattern:String.raw`\\d{${t}}`};for(let[e,t]of Object.entries(n))this._input.hasAttribute(e)||this._input.setAttribute(e,t);this._container=document.createElement(`div`),this._container.className=`l-input-otp-cells`,this._container.setAttribute(`aria-hidden`,`true`);for(let e=0;e<t;e++){let n=document.createElement(`div`);n.className=`l-input-otp-cell`,n.appendChild(document.createElement(`span`)),this._cells.push(n),this._container.appendChild(n),this.separatorAfter&&e===this.separatorAfter-1&&e<t-1&&(this._separatorEl=document.createElement(`span`),this._separatorEl.className=`l-input-otp-separator`,this._container.appendChild(this._separatorEl))}this._input.replaceWith(this._container),this._container.appendChild(this._input),this._initialized=!0,this._updateCells(),this._input.addEventListener(`input`,this._updateCells),this._input.addEventListener(`click`,this._updateCells),this._input.addEventListener(`keyup`,this._updateCells),this._input.addEventListener(`focus`,this._updateCells),this._input.addEventListener(`blur`,this._clearCells)}_teardown(){this._initialized&&=(this._input.removeEventListener(`input`,this._updateCells),this._input.removeEventListener(`click`,this._updateCells),this._input.removeEventListener(`keyup`,this._updateCells),this._input.removeEventListener(`focus`,this._updateCells),this._input.removeEventListener(`blur`,this._clearCells),this._container.replaceWith(this._input),this._separatorEl?.remove(),this._cells=[],this._separatorEl=null,!1)}}"
2243
+ "default": "class extends e{constructor(...e){super(...e),this._cells=[],this._separatorEl=null,this._initialized=!1,this._updateCells=()=>{let e=this._input.value,t=this._input.maxLength||6,n=Math.min(this._input.selectionStart??0,t-1),r=document.activeElement===this._input;for(let t=0;t<this._cells.length;t++){let i=this._cells[t],a=i.firstElementChild,o=e[t]??``;a.textContent=o,o?i.setAttribute(`data-filled`,``):i.removeAttribute(`data-filled`),r&&t===n?i.setAttribute(`data-active`,``):i.removeAttribute(`data-active`)}},this._clearCells=()=>{for(let e of this._cells)e.removeAttribute(`data-active`)},this._scheduleUpdateCells=()=>{requestAnimationFrame(this._updateCells)}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),requestAnimationFrame(()=>this._setup())}disconnectedCallback(){super.disconnectedCallback(),this._teardown()}_setup(){let e=this.querySelector(`input`);if(!e)return;this._input=e;let t=Number(getComputedStyle(this).getPropertyValue(`--digits`).trim())||6,n={type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,maxlength:String(t),pattern:String.raw`\\d{${t}}`};for(let[e,t]of Object.entries(n))this._input.hasAttribute(e)||this._input.setAttribute(e,t);this._container=document.createElement(`div`),this._container.className=`l-input-otp-cells`,this._container.setAttribute(`aria-hidden`,`true`);for(let e=0;e<t;e++){let n=document.createElement(`div`);n.className=`l-input-otp-cell`,n.appendChild(document.createElement(`span`)),this._cells.push(n),this._container.appendChild(n),this.separatorAfter&&e===this.separatorAfter-1&&e<t-1&&(this._separatorEl=document.createElement(`span`),this._separatorEl.className=`l-input-otp-separator`,this._container.appendChild(this._separatorEl))}this._input.replaceWith(this._container),this._container.appendChild(this._input),this._initialized=!0,this._updateCells(),this._input.addEventListener(`input`,this._updateCells),this._input.addEventListener(`click`,this._updateCells),this._input.addEventListener(`keyup`,this._updateCells),this._input.addEventListener(`focus`,this._scheduleUpdateCells),this._input.addEventListener(`blur`,this._clearCells)}_teardown(){this._initialized&&=(this._input.removeEventListener(`input`,this._updateCells),this._input.removeEventListener(`click`,this._updateCells),this._input.removeEventListener(`keyup`,this._updateCells),this._input.removeEventListener(`focus`,this._scheduleUpdateCells),this._input.removeEventListener(`blur`,this._clearCells),this._container.replaceWith(this._input),this._separatorEl?.remove(),this._cells=[],this._separatorEl=null,!1)}}"
2244
2244
  }
2245
2245
  ],
2246
2246
  "exports": [
@@ -4638,11 +4638,31 @@
4638
4638
  },
4639
4639
  {
4640
4640
  "description": "Cell width and height (default: 2.75rem). Font size scales automatically.",
4641
- "name": "--size"
4641
+ "name": "--cell-size"
4642
4642
  },
4643
4643
  {
4644
4644
  "description": "Space between cells (default: 0.5rem).",
4645
- "name": "--gap"
4645
+ "name": "--cell-gap"
4646
+ },
4647
+ {
4648
+ "description": "Cell background color.",
4649
+ "name": "--cell-bg-color"
4650
+ },
4651
+ {
4652
+ "description": "Cell border color.",
4653
+ "name": "--cell-border-color"
4654
+ },
4655
+ {
4656
+ "description": "Cell border-radius.",
4657
+ "name": "--cell-border-radius"
4658
+ },
4659
+ {
4660
+ "description": "Border + ring color of the active (focused) cell.",
4661
+ "name": "--cell-focus-color"
4662
+ },
4663
+ {
4664
+ "description": "`box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).",
4665
+ "name": "--cell-focus-ring"
4646
4666
  }
4647
4667
  ],
4648
4668
  "members": [
@@ -4686,6 +4706,10 @@
4686
4706
  "kind": "field",
4687
4707
  "name": "_clearCells"
4688
4708
  },
4709
+ {
4710
+ "kind": "field",
4711
+ "name": "_scheduleUpdateCells"
4712
+ },
4689
4713
  {
4690
4714
  "kind": "method",
4691
4715
  "name": "emit",
@@ -10762,11 +10786,31 @@
10762
10786
  },
10763
10787
  {
10764
10788
  "description": "Cell width and height (default: 2.75rem). Font size scales automatically.",
10765
- "name": "--size"
10789
+ "name": "--cell-size"
10766
10790
  },
10767
10791
  {
10768
10792
  "description": "Space between cells (default: 0.5rem).",
10769
- "name": "--gap"
10793
+ "name": "--cell-gap"
10794
+ },
10795
+ {
10796
+ "description": "Cell background color.",
10797
+ "name": "--cell-bg-color"
10798
+ },
10799
+ {
10800
+ "description": "Cell border color.",
10801
+ "name": "--cell-border-color"
10802
+ },
10803
+ {
10804
+ "description": "Cell border-radius.",
10805
+ "name": "--cell-border-radius"
10806
+ },
10807
+ {
10808
+ "description": "Border + ring color of the active (focused) cell.",
10809
+ "name": "--cell-focus-color"
10810
+ },
10811
+ {
10812
+ "description": "`box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).",
10813
+ "name": "--cell-focus-ring"
10770
10814
  }
10771
10815
  ],
10772
10816
  "members": [
@@ -10843,6 +10887,11 @@
10843
10887
  "name": "_clearCells",
10844
10888
  "privacy": "private"
10845
10889
  },
10890
+ {
10891
+ "kind": "field",
10892
+ "name": "_scheduleUpdateCells",
10893
+ "privacy": "private"
10894
+ },
10846
10895
  {
10847
10896
  "kind": "method",
10848
10897
  "name": "emit",
@@ -1,2 +1,2 @@
1
- import{c as e}from"../../chunks/lit.js";var t=e(`:host{--width:31rem;--border-radius:6px;--padding:1.5rem;--show-duration:.2s;--hide-duration:.2s;--backdrop:var(--l-backdrop);--backdrop-blur:0;display:contents}dialog{box-sizing:border-box;width:var(--width);max-inline-size:min(90vw, var(--width));border-radius:var(--border-radius);background-color:var(--l-color-surface-overlay);max-block-size:min(80dvb,100%);color:var(--l-color-text-primary);opacity:0;transition-property:opacity,display,overlay;transition-duration:var(--hide-duration);transition-behavior:allow-discrete;border:0;margin:auto;padding:0;position:fixed;inset:0}dialog::backdrop{background:var(--backdrop);-webkit-backdrop-filter:blur(var(--backdrop-blur));backdrop-filter:blur(var(--backdrop-blur))}dialog[open]{opacity:1;transition-duration:var(--show-duration);grid-template-rows:auto minmax(0,1fr) auto;display:grid}@starting-style{dialog[open]{opacity:0}}[part=header]{padding:var(--padding);justify-content:space-between;align-items:center;gap:1rem;display:flex}[part=title]{margin:0;font-size:1.125rem;font-weight:600;line-height:1.4}[part=body]{padding-inline:var(--padding);overflow-y:auto}[part=footer]{padding:var(--padding);place-content:end;gap:.5rem;display:flex}::slotted(menu[slot=footer]){display:contents}@media (prefers-reduced-motion:reduce){:host{--show-duration:0s;--hide-duration:0s}}`);export{t as default};
1
+ import{c as e}from"../../chunks/lit.js";var t=e(`:host{--width:31rem;--border-radius:6px;--padding:1.5rem;--show-duration:.2s;--hide-duration:.2s;--backdrop:var(--l-backdrop);--backdrop-blur:0;display:contents}dialog{box-sizing:border-box;width:var(--width);max-inline-size:min(90vw, var(--width));border-radius:var(--border-radius);background-color:var(--l-color-surface-overlay);max-block-size:min(80dvb,100%);color:var(--l-color-text-primary);opacity:0;transition-property:opacity,display,overlay;transition-duration:var(--hide-duration);transition-behavior:allow-discrete;border:0;margin:auto;padding:0;position:fixed;inset:0}dialog::backdrop{background:var(--backdrop);-webkit-backdrop-filter:blur(var(--backdrop-blur));backdrop-filter:blur(var(--backdrop-blur))}dialog[open]{opacity:1;transition-duration:var(--show-duration);grid-template-rows:auto minmax(0,1fr) auto;display:grid}@starting-style{dialog[open]{opacity:0}}[part=header]{padding:var(--padding);justify-content:space-between;align-items:center;gap:1rem;display:flex}[part=title]{margin:0;font-size:1.125rem;font-weight:600;line-height:1.4}[part=body]{padding-inline:var(--padding);grid-row:2;overflow-y:auto}[part=footer]{padding:var(--padding);grid-row:3;place-content:end;gap:.5rem;display:flex}:host([without-header]) [part=body]{padding-block-start:var(--padding)}::slotted(menu[slot=footer]){display:contents}@media (prefers-reduced-motion:reduce){:host{--show-duration:0s;--hide-duration:0s}}`);export{t as default};
2
2
  //# sourceMappingURL=dialog.styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.styles.js","names":[],"sources":["../../../src/html/elements/dialog/dialog.css?inline","../../../src/html/elements/dialog/dialog.styles.ts"],"sourcesContent":[":host {\n --width: 31rem;\n --border-radius: 6px;\n --padding: 1.5rem;\n --show-duration: 200ms;\n --hide-duration: 200ms;\n --backdrop: var(--l-backdrop);\n --backdrop-blur: 0;\n\n display: contents;\n}\n\ndialog {\n position: fixed;\n inset: 0;\n box-sizing: border-box;\n width: var(--width);\n max-inline-size: min(90vw, var(--width));\n max-block-size: min(80dvb, 100%);\n margin: auto;\n padding: 0;\n border: 0;\n border-radius: var(--border-radius);\n background-color: var(--l-color-surface-overlay);\n color: var(--l-color-text-primary);\n\n /* EXIT STATE */\n opacity: 0;\n\n transition-property: opacity, display, overlay;\n transition-duration: var(--hide-duration);\n transition-behavior: allow-discrete;\n\n &::backdrop {\n background: var(--backdrop);\n backdrop-filter: blur(var(--backdrop-blur));\n }\n\n /* OPEN STATE */\n /* grid layout pins header/footer; the middle row (body) scrolls via\n overflow on [part='body'] and minmax(0, 1fr) allowing it to shrink. */\n &[open] {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n opacity: 1;\n transition-duration: var(--show-duration);\n }\n\n /* BEFORE-OPEN STATE */\n @starting-style {\n &[open] {\n opacity: 0;\n }\n }\n}\n\n[part='header'] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 1rem;\n padding: var(--padding);\n}\n\n[part='title'] {\n margin: 0;\n font-size: 1.125rem;\n font-weight: 600;\n line-height: 1.4;\n}\n\n[part='body'] {\n padding-inline: var(--padding);\n overflow-y: auto;\n}\n\n[part='footer'] {\n display: flex;\n place-content: end;\n gap: 0.5rem;\n padding: var(--padding);\n}\n\n::slotted(menu[slot='footer']) {\n display: contents;\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n --show-duration: 0ms;\n --hide-duration: 0ms;\n }\n}\n","import { unsafeCSS } from 'lit';\nimport raw from './dialog.css?inline';\n\n/**\n * Wrapper module: imported by both `dialog.ts` and `drawer.ts`.\n * `unsafeCSS()` is called once here so both importers share the same\n * `CSSResult` instance (one constructed `CSSStyleSheet`, not two).\n */\nexport default unsafeCSS(raw);\n"],"mappings":"wCCQA,IAAA,EAAe,mzCAAc"}
1
+ {"version":3,"file":"dialog.styles.js","names":[],"sources":["../../../src/html/elements/dialog/dialog.css?inline","../../../src/html/elements/dialog/dialog.styles.ts"],"sourcesContent":[":host {\n --width: 31rem;\n --border-radius: 6px;\n --padding: 1.5rem;\n --show-duration: 200ms;\n --hide-duration: 200ms;\n --backdrop: var(--l-backdrop);\n --backdrop-blur: 0;\n\n display: contents;\n}\n\ndialog {\n position: fixed;\n inset: 0;\n box-sizing: border-box;\n width: var(--width);\n max-inline-size: min(90vw, var(--width));\n max-block-size: min(80dvb, 100%);\n margin: auto;\n padding: 0;\n border: 0;\n border-radius: var(--border-radius);\n background-color: var(--l-color-surface-overlay);\n color: var(--l-color-text-primary);\n\n /* EXIT STATE */\n opacity: 0;\n\n transition-property: opacity, display, overlay;\n transition-duration: var(--hide-duration);\n transition-behavior: allow-discrete;\n\n &::backdrop {\n background: var(--backdrop);\n backdrop-filter: blur(var(--backdrop-blur));\n }\n\n /* OPEN STATE */\n /* grid layout pins header/footer; the middle row (body) scrolls via\n overflow on [part='body'] and minmax(0, 1fr) allowing it to shrink. */\n &[open] {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n opacity: 1;\n transition-duration: var(--show-duration);\n }\n\n /* BEFORE-OPEN STATE */\n @starting-style {\n &[open] {\n opacity: 0;\n }\n }\n}\n\n[part='header'] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 1rem;\n padding: var(--padding);\n}\n\n[part='title'] {\n margin: 0;\n font-size: 1.125rem;\n font-weight: 600;\n line-height: 1.4;\n}\n\n/* Pin body and footer so the layout stays correct when [without-header]\n removes the header and only two children auto-place into the grid. */\n[part='body'] {\n grid-row: 2;\n padding-inline: var(--padding);\n overflow-y: auto;\n}\n\n[part='footer'] {\n grid-row: 3;\n display: flex;\n place-content: end;\n gap: 0.5rem;\n padding: var(--padding);\n}\n\n/* Without a header, the body has to provide its own block-start padding\n (normally inherited visually from the header padding above it). */\n:host([without-header]) [part='body'] {\n padding-block-start: var(--padding);\n}\n\n::slotted(menu[slot='footer']) {\n display: contents;\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n --show-duration: 0ms;\n --hide-duration: 0ms;\n }\n}\n","import { unsafeCSS } from 'lit';\nimport raw from './dialog.css?inline';\n\n/**\n * Wrapper module: imported by both `dialog.ts` and `drawer.ts`.\n * `unsafeCSS()` is called once here so both importers share the same\n * `CSSResult` instance (one constructed `CSSStyleSheet`, not two).\n */\nexport default unsafeCSS(raw);\n"],"mappings":"wCCQA,IAAA,EAAe,g5CAAc"}
@@ -9,8 +9,13 @@ import { LuxenElement } from '../../shared/luxen-element';
9
9
  * @customElement l-input-otp
10
10
  *
11
11
  * @cssproperty --digits - Number of digit boxes (default: 6). Must match input's maxlength.
12
- * @cssproperty --size - Cell width and height (default: 2.75rem). Font size scales automatically.
13
- * @cssproperty --gap - Space between cells (default: 0.5rem).
12
+ * @cssproperty --cell-size - Cell width and height (default: 2.75rem). Font size scales automatically.
13
+ * @cssproperty --cell-gap - Space between cells (default: 0.5rem).
14
+ * @cssproperty --cell-bg-color - Cell background color.
15
+ * @cssproperty --cell-border-color - Cell border color.
16
+ * @cssproperty --cell-border-radius - Cell border-radius.
17
+ * @cssproperty --cell-focus-color - Border + ring color of the active (focused) cell.
18
+ * @cssproperty --cell-focus-ring - `box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).
14
19
  */
15
20
  export declare class LuxenInputOtp extends LuxenElement {
16
21
  createRenderRoot(): this;
@@ -27,5 +32,6 @@ export declare class LuxenInputOtp extends LuxenElement {
27
32
  private _teardown;
28
33
  private _updateCells;
29
34
  private _clearCells;
35
+ private _scheduleUpdateCells;
30
36
  }
31
37
  //# sourceMappingURL=input-otp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"input-otp.d.ts","sourceRoot":"","sources":["../../../src/html/elements/input-otp/input-otp.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAc,SAAQ,YAAY;IACpC,gBAAgB;IAIzB,sFAAsF;IAEtF,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,YAAY,CAAS;IAEpB,iBAAiB;IAKjB,oBAAoB;IAO7B,OAAO,CAAC,MAAM;IA2Dd,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,YAAY,CAyBlB;IAEF,OAAO,CAAC,WAAW,CAIjB;CACH"}
1
+ {"version":3,"file":"input-otp.d.ts","sourceRoot":"","sources":["../../../src/html/elements/input-otp/input-otp.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAc,SAAQ,YAAY;IACpC,gBAAgB;IAIzB,sFAAsF;IAEtF,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,YAAY,CAAS;IAEpB,iBAAiB;IAKjB,oBAAoB;IAO7B,OAAO,CAAC,MAAM;IA4Dd,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,YAAY,CAyBlB;IAEF,OAAO,CAAC,WAAW,CAIjB;IAEF,OAAO,CAAC,oBAAoB,CAE1B;CACH"}
@@ -1,2 +1,2 @@
1
- import{LuxenElement as e}from"../../shared/luxen-element.js";import{a as t,t as n}from"../../chunks/decorate.js";var r=class extends e{constructor(...e){super(...e),this._cells=[],this._separatorEl=null,this._initialized=!1,this._updateCells=()=>{let e=this._input.value,t=this._input.maxLength||6,n=Math.min(this._input.selectionStart??0,t-1),r=document.activeElement===this._input;for(let t=0;t<this._cells.length;t++){let i=this._cells[t],a=i.firstElementChild,o=e[t]??``;a.textContent=o,o?i.setAttribute(`data-filled`,``):i.removeAttribute(`data-filled`),r&&t===n?i.setAttribute(`data-active`,``):i.removeAttribute(`data-active`)}},this._clearCells=()=>{for(let e of this._cells)e.removeAttribute(`data-active`)}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),requestAnimationFrame(()=>this._setup())}disconnectedCallback(){super.disconnectedCallback(),this._teardown()}_setup(){let e=this.querySelector(`input`);if(!e)return;this._input=e;let t=Number(getComputedStyle(this).getPropertyValue(`--digits`).trim())||6,n={type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,maxlength:String(t),pattern:String.raw`\d{${t}}`};for(let[e,t]of Object.entries(n))this._input.hasAttribute(e)||this._input.setAttribute(e,t);this._container=document.createElement(`div`),this._container.className=`l-input-otp-cells`,this._container.setAttribute(`aria-hidden`,`true`);for(let e=0;e<t;e++){let n=document.createElement(`div`);n.className=`l-input-otp-cell`,n.appendChild(document.createElement(`span`)),this._cells.push(n),this._container.appendChild(n),this.separatorAfter&&e===this.separatorAfter-1&&e<t-1&&(this._separatorEl=document.createElement(`span`),this._separatorEl.className=`l-input-otp-separator`,this._container.appendChild(this._separatorEl))}this._input.replaceWith(this._container),this._container.appendChild(this._input),this._initialized=!0,this._updateCells(),this._input.addEventListener(`input`,this._updateCells),this._input.addEventListener(`click`,this._updateCells),this._input.addEventListener(`keyup`,this._updateCells),this._input.addEventListener(`focus`,this._updateCells),this._input.addEventListener(`blur`,this._clearCells)}_teardown(){this._initialized&&=(this._input.removeEventListener(`input`,this._updateCells),this._input.removeEventListener(`click`,this._updateCells),this._input.removeEventListener(`keyup`,this._updateCells),this._input.removeEventListener(`focus`,this._updateCells),this._input.removeEventListener(`blur`,this._clearCells),this._container.replaceWith(this._input),this._separatorEl?.remove(),this._cells=[],this._separatorEl=null,!1)}};n([t({type:Number,reflect:!0,attribute:`separator-after`})],r.prototype,`separatorAfter`,void 0);export{r as LuxenInputOtp};
1
+ import{LuxenElement as e}from"../../shared/luxen-element.js";import{a as t,t as n}from"../../chunks/decorate.js";var r=class extends e{constructor(...e){super(...e),this._cells=[],this._separatorEl=null,this._initialized=!1,this._updateCells=()=>{let e=this._input.value,t=this._input.maxLength||6,n=Math.min(this._input.selectionStart??0,t-1),r=document.activeElement===this._input;for(let t=0;t<this._cells.length;t++){let i=this._cells[t],a=i.firstElementChild,o=e[t]??``;a.textContent=o,o?i.setAttribute(`data-filled`,``):i.removeAttribute(`data-filled`),r&&t===n?i.setAttribute(`data-active`,``):i.removeAttribute(`data-active`)}},this._clearCells=()=>{for(let e of this._cells)e.removeAttribute(`data-active`)},this._scheduleUpdateCells=()=>{requestAnimationFrame(this._updateCells)}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),requestAnimationFrame(()=>this._setup())}disconnectedCallback(){super.disconnectedCallback(),this._teardown()}_setup(){let e=this.querySelector(`input`);if(!e)return;this._input=e;let t=Number(getComputedStyle(this).getPropertyValue(`--digits`).trim())||6,n={type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,maxlength:String(t),pattern:String.raw`\d{${t}}`};for(let[e,t]of Object.entries(n))this._input.hasAttribute(e)||this._input.setAttribute(e,t);this._container=document.createElement(`div`),this._container.className=`l-input-otp-cells`,this._container.setAttribute(`aria-hidden`,`true`);for(let e=0;e<t;e++){let n=document.createElement(`div`);n.className=`l-input-otp-cell`,n.appendChild(document.createElement(`span`)),this._cells.push(n),this._container.appendChild(n),this.separatorAfter&&e===this.separatorAfter-1&&e<t-1&&(this._separatorEl=document.createElement(`span`),this._separatorEl.className=`l-input-otp-separator`,this._container.appendChild(this._separatorEl))}this._input.replaceWith(this._container),this._container.appendChild(this._input),this._initialized=!0,this._updateCells(),this._input.addEventListener(`input`,this._updateCells),this._input.addEventListener(`click`,this._updateCells),this._input.addEventListener(`keyup`,this._updateCells),this._input.addEventListener(`focus`,this._scheduleUpdateCells),this._input.addEventListener(`blur`,this._clearCells)}_teardown(){this._initialized&&=(this._input.removeEventListener(`input`,this._updateCells),this._input.removeEventListener(`click`,this._updateCells),this._input.removeEventListener(`keyup`,this._updateCells),this._input.removeEventListener(`focus`,this._scheduleUpdateCells),this._input.removeEventListener(`blur`,this._clearCells),this._container.replaceWith(this._input),this._separatorEl?.remove(),this._cells=[],this._separatorEl=null,!1)}};n([t({type:Number,reflect:!0,attribute:`separator-after`})],r.prototype,`separatorAfter`,void 0);export{r as LuxenInputOtp};
2
2
  //# sourceMappingURL=input-otp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"input-otp.js","names":[],"sources":["../../../src/html/elements/input-otp/input-otp.ts"],"sourcesContent":["import { property } from 'lit/decorators.js';\nimport { LuxenElement } from '../../shared/luxen-element';\n\n/**\n * Enhances a child `<input>` with visual digit cells (Stripe-style OTP input).\n *\n * A single hidden `<input>` handles keyboard, paste, and autocomplete.\n * Visual cells are rendered as real DOM elements with individual borders and focus ring.\n *\n * @summary Stripe-style OTP input with visual digit cells over a hidden native input.\n * @customElement l-input-otp\n *\n * @cssproperty --digits - Number of digit boxes (default: 6). Must match input's maxlength.\n * @cssproperty --size - Cell width and height (default: 2.75rem). Font size scales automatically.\n * @cssproperty --gap - Space between cells (default: 0.5rem).\n */\nexport class LuxenInputOtp extends LuxenElement {\n override createRenderRoot() {\n return this;\n }\n\n /** Position after which to insert a visual separator (e.g., 3 for a 3-3 grouping). */\n @property({ type: Number, reflect: true, attribute: 'separator-after' })\n separatorAfter?: number;\n\n private _input!: HTMLInputElement;\n private _container!: HTMLDivElement;\n private _cells: HTMLDivElement[] = [];\n private _separatorEl: HTMLSpanElement | null = null;\n private _initialized = false;\n\n override connectedCallback() {\n super.connectedCallback();\n requestAnimationFrame(() => this._setup());\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n this._teardown();\n }\n\n // --- Setup / Teardown ---\n\n private _setup() {\n const input = this.querySelector<HTMLInputElement>('input');\n if (!input) return;\n\n this._input = input;\n\n // Derive digit count from --digits CSS custom property (default 6)\n const digits = Number(getComputedStyle(this).getPropertyValue('--digits').trim()) || 6;\n\n // Set sensible defaults — author can still override via HTML attributes\n const defaults: Record<string, string> = {\n type: 'text',\n inputmode: 'numeric',\n autocomplete: 'one-time-code',\n maxlength: String(digits),\n pattern: String.raw`\\d{${digits}}`,\n };\n for (const [attr, value] of Object.entries(defaults)) {\n if (!this._input.hasAttribute(attr)) {\n this._input.setAttribute(attr, value);\n }\n }\n\n // Build visual cells container\n this._container = document.createElement('div');\n this._container.className = 'l-input-otp-cells';\n this._container.setAttribute('aria-hidden', 'true');\n\n for (let i = 0; i < digits; i++) {\n const cell = document.createElement('div');\n cell.className = 'l-input-otp-cell';\n cell.appendChild(document.createElement('span'));\n this._cells.push(cell);\n this._container.appendChild(cell);\n\n // Insert separator after the specified position\n if (this.separatorAfter && i === this.separatorAfter - 1 && i < digits - 1) {\n this._separatorEl = document.createElement('span');\n this._separatorEl.className = 'l-input-otp-separator';\n this._container.appendChild(this._separatorEl);\n }\n }\n\n // Wrap: insert container before input, then move input inside\n this._input.replaceWith(this._container);\n this._container.appendChild(this._input);\n this._initialized = true;\n\n // Populate cells if input already has a value (e.g. disabled with prefilled value)\n this._updateCells();\n\n // Events\n this._input.addEventListener('input', this._updateCells);\n this._input.addEventListener('click', this._updateCells);\n this._input.addEventListener('keyup', this._updateCells);\n this._input.addEventListener('focus', this._updateCells);\n this._input.addEventListener('blur', this._clearCells);\n }\n\n private _teardown() {\n if (!this._initialized) return;\n\n this._input.removeEventListener('input', this._updateCells);\n this._input.removeEventListener('click', this._updateCells);\n this._input.removeEventListener('keyup', this._updateCells);\n this._input.removeEventListener('focus', this._updateCells);\n this._input.removeEventListener('blur', this._clearCells);\n\n // Restore input to direct child\n this._container.replaceWith(this._input);\n this._separatorEl?.remove();\n\n this._cells = [];\n this._separatorEl = null;\n this._initialized = false;\n }\n\n // --- Cell updates ---\n\n private _updateCells = (): void => {\n const value = this._input.value;\n const maxLen = this._input.maxLength || 6;\n const pos = Math.min(this._input.selectionStart ?? 0, maxLen - 1);\n const isFocused = document.activeElement === this._input;\n\n for (let i = 0; i < this._cells.length; i++) {\n const cell = this._cells[i];\n const span = cell.firstElementChild as HTMLSpanElement;\n const char = value[i] ?? '';\n\n span.textContent = char;\n\n if (char) {\n cell.setAttribute('data-filled', '');\n } else {\n cell.removeAttribute('data-filled');\n }\n\n if (isFocused && i === pos) {\n cell.setAttribute('data-active', '');\n } else {\n cell.removeAttribute('data-active');\n }\n }\n };\n\n private _clearCells = (): void => {\n for (const cell of this._cells) {\n cell.removeAttribute('data-active');\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"input-otp.js","names":[],"sources":["../../../src/html/elements/input-otp/input-otp.ts"],"sourcesContent":["import { property } from 'lit/decorators.js';\nimport { LuxenElement } from '../../shared/luxen-element';\n\n/**\n * Enhances a child `<input>` with visual digit cells (Stripe-style OTP input).\n *\n * A single hidden `<input>` handles keyboard, paste, and autocomplete.\n * Visual cells are rendered as real DOM elements with individual borders and focus ring.\n *\n * @summary Stripe-style OTP input with visual digit cells over a hidden native input.\n * @customElement l-input-otp\n *\n * @cssproperty --digits - Number of digit boxes (default: 6). Must match input's maxlength.\n * @cssproperty --cell-size - Cell width and height (default: 2.75rem). Font size scales automatically.\n * @cssproperty --cell-gap - Space between cells (default: 0.5rem).\n * @cssproperty --cell-bg-color - Cell background color.\n * @cssproperty --cell-border-color - Cell border color.\n * @cssproperty --cell-border-radius - Cell border-radius.\n * @cssproperty --cell-focus-color - Border + ring color of the active (focused) cell.\n * @cssproperty --cell-focus-ring - `box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).\n */\nexport class LuxenInputOtp extends LuxenElement {\n override createRenderRoot() {\n return this;\n }\n\n /** Position after which to insert a visual separator (e.g., 3 for a 3-3 grouping). */\n @property({ type: Number, reflect: true, attribute: 'separator-after' })\n separatorAfter?: number;\n\n private _input!: HTMLInputElement;\n private _container!: HTMLDivElement;\n private _cells: HTMLDivElement[] = [];\n private _separatorEl: HTMLSpanElement | null = null;\n private _initialized = false;\n\n override connectedCallback() {\n super.connectedCallback();\n requestAnimationFrame(() => this._setup());\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n this._teardown();\n }\n\n // --- Setup / Teardown ---\n\n private _setup() {\n const input = this.querySelector<HTMLInputElement>('input');\n if (!input) return;\n\n this._input = input;\n\n // Derive digit count from --digits CSS custom property (default 6)\n const digits = Number(getComputedStyle(this).getPropertyValue('--digits').trim()) || 6;\n\n // Set sensible defaults — author can still override via HTML attributes\n const defaults: Record<string, string> = {\n type: 'text',\n inputmode: 'numeric',\n autocomplete: 'one-time-code',\n maxlength: String(digits),\n pattern: String.raw`\\d{${digits}}`,\n };\n for (const [attr, value] of Object.entries(defaults)) {\n if (!this._input.hasAttribute(attr)) {\n this._input.setAttribute(attr, value);\n }\n }\n\n // Build visual cells container\n this._container = document.createElement('div');\n this._container.className = 'l-input-otp-cells';\n this._container.setAttribute('aria-hidden', 'true');\n\n for (let i = 0; i < digits; i++) {\n const cell = document.createElement('div');\n cell.className = 'l-input-otp-cell';\n cell.appendChild(document.createElement('span'));\n this._cells.push(cell);\n this._container.appendChild(cell);\n\n // Insert separator after the specified position\n if (this.separatorAfter && i === this.separatorAfter - 1 && i < digits - 1) {\n this._separatorEl = document.createElement('span');\n this._separatorEl.className = 'l-input-otp-separator';\n this._container.appendChild(this._separatorEl);\n }\n }\n\n // Wrap: insert container before input, then move input inside\n this._input.replaceWith(this._container);\n this._container.appendChild(this._input);\n this._initialized = true;\n\n // Populate cells if input already has a value (e.g. disabled with prefilled value)\n this._updateCells();\n\n // Events — focus is deferred so it runs after the click that triggered it\n // (otherwise selectionStart is stale and the active cell flickers).\n this._input.addEventListener('input', this._updateCells);\n this._input.addEventListener('click', this._updateCells);\n this._input.addEventListener('keyup', this._updateCells);\n this._input.addEventListener('focus', this._scheduleUpdateCells);\n this._input.addEventListener('blur', this._clearCells);\n }\n\n private _teardown() {\n if (!this._initialized) return;\n\n this._input.removeEventListener('input', this._updateCells);\n this._input.removeEventListener('click', this._updateCells);\n this._input.removeEventListener('keyup', this._updateCells);\n this._input.removeEventListener('focus', this._scheduleUpdateCells);\n this._input.removeEventListener('blur', this._clearCells);\n\n // Restore input to direct child\n this._container.replaceWith(this._input);\n this._separatorEl?.remove();\n\n this._cells = [];\n this._separatorEl = null;\n this._initialized = false;\n }\n\n // --- Cell updates ---\n\n private _updateCells = (): void => {\n const value = this._input.value;\n const maxLen = this._input.maxLength || 6;\n const pos = Math.min(this._input.selectionStart ?? 0, maxLen - 1);\n const isFocused = document.activeElement === this._input;\n\n for (let i = 0; i < this._cells.length; i++) {\n const cell = this._cells[i];\n const span = cell.firstElementChild as HTMLSpanElement;\n const char = value[i] ?? '';\n\n span.textContent = char;\n\n if (char) {\n cell.setAttribute('data-filled', '');\n } else {\n cell.removeAttribute('data-filled');\n }\n\n if (isFocused && i === pos) {\n cell.setAttribute('data-active', '');\n } else {\n cell.removeAttribute('data-active');\n }\n }\n };\n\n private _clearCells = (): void => {\n for (const cell of this._cells) {\n cell.removeAttribute('data-active');\n }\n };\n\n private _scheduleUpdateCells = (): void => {\n requestAnimationFrame(this._updateCells);\n };\n}\n"],"mappings":"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"}
@@ -15,13 +15,14 @@
15
15
 
16
16
  l-input-otp {
17
17
  --digits: 6;
18
- --size: 2.75rem;
19
- --_font-size: calc(var(--size) * 0.45);
20
- --_bg: color-mix(in oklab, var(--l-color-text-primary) 4%, var(--l-color-surface));
21
- --_border-color: var(--l-color-border);
22
- --_highlight-color: var(--l-focus-ring);
18
+ --cell-size: 2.75rem;
19
+ --cell-bg-color: color-mix(in oklab, var(--l-color-text-primary) 4%, var(--l-color-surface));
20
+ --cell-border-color: var(--l-color-border);
21
+ --cell-border-radius: var(--radius-md);
22
+ --cell-focus-color: var(--l-focus-ring);
23
+ --cell-focus-ring: 0 0 0 1px var(--cell-focus-color);
23
24
 
24
- display: inline-flex;
25
+ display: inline-block;
25
26
  position: relative;
26
27
  }
27
28
 
@@ -34,7 +35,7 @@
34
35
  .l-input-otp-cells {
35
36
  display: inline-flex;
36
37
  align-items: center;
37
- gap: var(--gap, 0.5rem);
38
+ gap: var(--cell-gap, 0.5rem);
38
39
  position: relative;
39
40
  }
40
41
 
@@ -48,15 +49,15 @@
48
49
  display: flex;
49
50
  align-items: center;
50
51
  justify-content: center;
51
- inline-size: var(--size);
52
- block-size: var(--size);
53
- border: 1px solid var(--_border-color);
54
- border-radius: var(--radius-md);
55
- background: var(--_bg);
52
+ inline-size: var(--cell-size);
53
+ block-size: var(--cell-size);
54
+ border: 1px solid var(--cell-border-color);
55
+ border-radius: var(--cell-border-radius);
56
+ background: var(--cell-bg-color);
56
57
  font-family:
57
58
  ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
58
59
  monospace;
59
- font-size: var(--_font-size);
60
+ font-size: calc(var(--cell-size) * 0.45);
60
61
  font-variant-numeric: tabular-nums;
61
62
  line-height: 1;
62
63
  color: var(--l-color-text-primary);
@@ -73,8 +74,37 @@
73
74
  */
74
75
 
75
76
  l-input-otp:focus-within .l-input-otp-cell[data-active] {
76
- border-color: var(--_highlight-color);
77
- box-shadow: 0 0 0 1px var(--_highlight-color);
77
+ border-color: var(--cell-focus-color);
78
+ box-shadow: var(--cell-focus-ring);
79
+ }
80
+
81
+ /*
82
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
83
+ 🅲🅰🆁🅴🆃 (fake blinking caret in active empty cell)
84
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
85
+ The native caret is hidden via `caret-color: transparent`; this stand-in
86
+ gives the missing point-of-insertion cue inside the active cell. Only
87
+ visible while empty — once a digit is typed, it replaces the caret.
88
+ */
89
+
90
+ l-input-otp:focus-within .l-input-otp-cell[data-active]:not([data-filled])::after {
91
+ content: '';
92
+ inline-size: 1px;
93
+ block-size: 1em;
94
+ background: currentColor;
95
+ animation: l-input-otp-caret 1s steps(2, jump-none) infinite;
96
+ }
97
+
98
+ @keyframes l-input-otp-caret {
99
+ 50% {
100
+ opacity: 0;
101
+ }
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ l-input-otp:focus-within .l-input-otp-cell[data-active]:not([data-filled])::after {
106
+ animation: none;
107
+ }
78
108
  }
79
109
 
80
110
  /*
@@ -122,11 +152,11 @@
122
152
  */
123
153
 
124
154
  l-input-otp[size='sm'] {
125
- --size: 2rem;
155
+ --cell-size: 2rem;
126
156
  }
127
157
 
128
158
  l-input-otp[size='lg'] {
129
- --size: 3.5rem;
159
+ --cell-size: 3.5rem;
130
160
  }
131
161
 
132
162
  /*
@@ -145,20 +175,24 @@
145
175
  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
146
176
  🅿🆁🅾🅶🆁🅴🆂🆂🅸🆅🅴 🅴🅽🅷🅰🅽🅲🅴🅼🅴🅽🆃 🅵🅰🅻🅻🅱🅰🅲🅺
147
177
  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
148
- Before JS loads, the raw input is visible and usable.
178
+ Pre-upgrade reserves the exact box the cells will occupy width scales
179
+ with --digits / --cell-size / --cell-gap so layout doesn't shift on hydration.
180
+ Single soft-tinted rectangle (not per-cell): matches any custom appearance
181
+ cleanly since it doesn't pretend to redraw the cell layout.
149
182
  */
150
183
 
151
- l-input-otp:not(:defined) > input {
152
- all: unset;
184
+ l-input-otp:not(:defined) {
153
185
  display: inline-block;
154
- border: 1px solid var(--l-color-border);
155
- border-radius: var(--radius-md);
156
- padding: 0.5rem 0.75rem;
157
- font-family:
158
- ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
159
- monospace;
160
- font-size: 1.25rem;
161
- letter-spacing: 0.5ch;
162
- color: var(--l-color-text-primary);
186
+ flex-shrink: 0;
187
+ inline-size: calc(
188
+ var(--cell-size) * var(--digits) + var(--cell-gap, 0.5rem) * (var(--digits) - 1)
189
+ );
190
+ block-size: var(--cell-size);
191
+ background: var(--cell-bg-color);
192
+ border-radius: var(--cell-border-radius);
193
+ }
194
+
195
+ l-input-otp:not(:defined) > input {
196
+ display: none;
163
197
  }
164
198
  }
@@ -707,6 +707,16 @@ In dark mode, mixes the base color with black (default 15% black).
707
707
  white-space: nowrap;
708
708
  border: 0;
709
709
  }
710
+
711
+ /* Hide Shadow-DOM overlay elements until their custom element upgrades.
712
+ Without this, slotted/inner content flashes inline before the upgrade. */
713
+ l-dialog:not(:defined),
714
+ l-drawer:not(:defined),
715
+ l-dropdown:not(:defined),
716
+ l-popover:not(:defined),
717
+ l-tooltip:not(:defined) {
718
+ display: none;
719
+ }
710
720
  }
711
721
 
712
722
  /* https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/theme.css */
@@ -15,13 +15,14 @@
15
15
 
16
16
  l-input-otp {
17
17
  --digits: 6;
18
- --size: 2.75rem;
19
- --_font-size: calc(var(--size) * 0.45);
20
- --_bg: color-mix(in oklab, var(--l-color-text-primary) 4%, var(--l-color-surface));
21
- --_border-color: var(--l-color-border);
22
- --_highlight-color: var(--l-focus-ring);
18
+ --cell-size: 2.75rem;
19
+ --cell-bg-color: color-mix(in oklab, var(--l-color-text-primary) 4%, var(--l-color-surface));
20
+ --cell-border-color: var(--l-color-border);
21
+ --cell-border-radius: var(--radius-md);
22
+ --cell-focus-color: var(--l-focus-ring);
23
+ --cell-focus-ring: 0 0 0 1px var(--cell-focus-color);
23
24
 
24
- display: inline-flex;
25
+ display: inline-block;
25
26
  position: relative;
26
27
  }
27
28
 
@@ -34,7 +35,7 @@
34
35
  .l-input-otp-cells {
35
36
  display: inline-flex;
36
37
  align-items: center;
37
- gap: var(--gap, 0.5rem);
38
+ gap: var(--cell-gap, 0.5rem);
38
39
  position: relative;
39
40
  }
40
41
 
@@ -48,15 +49,15 @@
48
49
  display: flex;
49
50
  align-items: center;
50
51
  justify-content: center;
51
- inline-size: var(--size);
52
- block-size: var(--size);
53
- border: 1px solid var(--_border-color);
54
- border-radius: var(--radius-md);
55
- background: var(--_bg);
52
+ inline-size: var(--cell-size);
53
+ block-size: var(--cell-size);
54
+ border: 1px solid var(--cell-border-color);
55
+ border-radius: var(--cell-border-radius);
56
+ background: var(--cell-bg-color);
56
57
  font-family:
57
58
  ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
58
59
  monospace;
59
- font-size: var(--_font-size);
60
+ font-size: calc(var(--cell-size) * 0.45);
60
61
  font-variant-numeric: tabular-nums;
61
62
  line-height: 1;
62
63
  color: var(--l-color-text-primary);
@@ -73,8 +74,37 @@
73
74
  */
74
75
 
75
76
  l-input-otp:focus-within .l-input-otp-cell[data-active] {
76
- border-color: var(--_highlight-color);
77
- box-shadow: 0 0 0 1px var(--_highlight-color);
77
+ border-color: var(--cell-focus-color);
78
+ box-shadow: var(--cell-focus-ring);
79
+ }
80
+
81
+ /*
82
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
83
+ 🅲🅰🆁🅴🆃 (fake blinking caret in active empty cell)
84
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
85
+ The native caret is hidden via `caret-color: transparent`; this stand-in
86
+ gives the missing point-of-insertion cue inside the active cell. Only
87
+ visible while empty — once a digit is typed, it replaces the caret.
88
+ */
89
+
90
+ l-input-otp:focus-within .l-input-otp-cell[data-active]:not([data-filled])::after {
91
+ content: '';
92
+ inline-size: 1px;
93
+ block-size: 1em;
94
+ background: currentColor;
95
+ animation: l-input-otp-caret 1s steps(2, jump-none) infinite;
96
+ }
97
+
98
+ @keyframes l-input-otp-caret {
99
+ 50% {
100
+ opacity: 0;
101
+ }
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ l-input-otp:focus-within .l-input-otp-cell[data-active]:not([data-filled])::after {
106
+ animation: none;
107
+ }
78
108
  }
79
109
 
80
110
  /*
@@ -122,11 +152,11 @@
122
152
  */
123
153
 
124
154
  l-input-otp[size='sm'] {
125
- --size: 2rem;
155
+ --cell-size: 2rem;
126
156
  }
127
157
 
128
158
  l-input-otp[size='lg'] {
129
- --size: 3.5rem;
159
+ --cell-size: 3.5rem;
130
160
  }
131
161
 
132
162
  /*
@@ -145,20 +175,24 @@
145
175
  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
146
176
  🅿🆁🅾🅶🆁🅴🆂🆂🅸🆅🅴 🅴🅽🅷🅰🅽🅲🅴🅼🅴🅽🆃 🅵🅰🅻🅻🅱🅰🅲🅺
147
177
  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
148
- Before JS loads, the raw input is visible and usable.
178
+ Pre-upgrade reserves the exact box the cells will occupy width scales
179
+ with --digits / --cell-size / --cell-gap so layout doesn't shift on hydration.
180
+ Single soft-tinted rectangle (not per-cell): matches any custom appearance
181
+ cleanly since it doesn't pretend to redraw the cell layout.
149
182
  */
150
183
 
151
- l-input-otp:not(:defined) > input {
152
- all: unset;
184
+ l-input-otp:not(:defined) {
153
185
  display: inline-block;
154
- border: 1px solid var(--l-color-border);
155
- border-radius: var(--radius-md);
156
- padding: 0.5rem 0.75rem;
157
- font-family:
158
- ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
159
- monospace;
160
- font-size: 1.25rem;
161
- letter-spacing: 0.5ch;
162
- color: var(--l-color-text-primary);
186
+ flex-shrink: 0;
187
+ inline-size: calc(
188
+ var(--cell-size) * var(--digits) + var(--cell-gap, 0.5rem) * (var(--digits) - 1)
189
+ );
190
+ block-size: var(--cell-size);
191
+ background: var(--cell-bg-color);
192
+ border-radius: var(--cell-border-radius);
193
+ }
194
+
195
+ l-input-otp:not(:defined) > input {
196
+ display: none;
163
197
  }
164
198
  }
@@ -707,6 +707,16 @@ In dark mode, mixes the base color with black (default 15% black).
707
707
  white-space: nowrap;
708
708
  border: 0;
709
709
  }
710
+
711
+ /* Hide Shadow-DOM overlay elements until their custom element upgrades.
712
+ Without this, slotted/inner content flashes inline before the upgrade. */
713
+ l-dialog:not(:defined),
714
+ l-drawer:not(:defined),
715
+ l-dropdown:not(:defined),
716
+ l-popover:not(:defined),
717
+ l-tooltip:not(:defined) {
718
+ display: none;
719
+ }
710
720
  }
711
721
 
712
722
  /* https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/theme.css */
@@ -2240,7 +2240,7 @@
2240
2240
  {
2241
2241
  "kind": "variable",
2242
2242
  "name": "r",
2243
- "default": "class extends e{constructor(...e){super(...e),this._cells=[],this._separatorEl=null,this._initialized=!1,this._updateCells=()=>{let e=this._input.value,t=this._input.maxLength||6,n=Math.min(this._input.selectionStart??0,t-1),r=document.activeElement===this._input;for(let t=0;t<this._cells.length;t++){let i=this._cells[t],a=i.firstElementChild,o=e[t]??``;a.textContent=o,o?i.setAttribute(`data-filled`,``):i.removeAttribute(`data-filled`),r&&t===n?i.setAttribute(`data-active`,``):i.removeAttribute(`data-active`)}},this._clearCells=()=>{for(let e of this._cells)e.removeAttribute(`data-active`)}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),requestAnimationFrame(()=>this._setup())}disconnectedCallback(){super.disconnectedCallback(),this._teardown()}_setup(){let e=this.querySelector(`input`);if(!e)return;this._input=e;let t=Number(getComputedStyle(this).getPropertyValue(`--digits`).trim())||6,n={type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,maxlength:String(t),pattern:String.raw`\\d{${t}}`};for(let[e,t]of Object.entries(n))this._input.hasAttribute(e)||this._input.setAttribute(e,t);this._container=document.createElement(`div`),this._container.className=`l-input-otp-cells`,this._container.setAttribute(`aria-hidden`,`true`);for(let e=0;e<t;e++){let n=document.createElement(`div`);n.className=`l-input-otp-cell`,n.appendChild(document.createElement(`span`)),this._cells.push(n),this._container.appendChild(n),this.separatorAfter&&e===this.separatorAfter-1&&e<t-1&&(this._separatorEl=document.createElement(`span`),this._separatorEl.className=`l-input-otp-separator`,this._container.appendChild(this._separatorEl))}this._input.replaceWith(this._container),this._container.appendChild(this._input),this._initialized=!0,this._updateCells(),this._input.addEventListener(`input`,this._updateCells),this._input.addEventListener(`click`,this._updateCells),this._input.addEventListener(`keyup`,this._updateCells),this._input.addEventListener(`focus`,this._updateCells),this._input.addEventListener(`blur`,this._clearCells)}_teardown(){this._initialized&&=(this._input.removeEventListener(`input`,this._updateCells),this._input.removeEventListener(`click`,this._updateCells),this._input.removeEventListener(`keyup`,this._updateCells),this._input.removeEventListener(`focus`,this._updateCells),this._input.removeEventListener(`blur`,this._clearCells),this._container.replaceWith(this._input),this._separatorEl?.remove(),this._cells=[],this._separatorEl=null,!1)}}"
2243
+ "default": "class extends e{constructor(...e){super(...e),this._cells=[],this._separatorEl=null,this._initialized=!1,this._updateCells=()=>{let e=this._input.value,t=this._input.maxLength||6,n=Math.min(this._input.selectionStart??0,t-1),r=document.activeElement===this._input;for(let t=0;t<this._cells.length;t++){let i=this._cells[t],a=i.firstElementChild,o=e[t]??``;a.textContent=o,o?i.setAttribute(`data-filled`,``):i.removeAttribute(`data-filled`),r&&t===n?i.setAttribute(`data-active`,``):i.removeAttribute(`data-active`)}},this._clearCells=()=>{for(let e of this._cells)e.removeAttribute(`data-active`)},this._scheduleUpdateCells=()=>{requestAnimationFrame(this._updateCells)}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),requestAnimationFrame(()=>this._setup())}disconnectedCallback(){super.disconnectedCallback(),this._teardown()}_setup(){let e=this.querySelector(`input`);if(!e)return;this._input=e;let t=Number(getComputedStyle(this).getPropertyValue(`--digits`).trim())||6,n={type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,maxlength:String(t),pattern:String.raw`\\d{${t}}`};for(let[e,t]of Object.entries(n))this._input.hasAttribute(e)||this._input.setAttribute(e,t);this._container=document.createElement(`div`),this._container.className=`l-input-otp-cells`,this._container.setAttribute(`aria-hidden`,`true`);for(let e=0;e<t;e++){let n=document.createElement(`div`);n.className=`l-input-otp-cell`,n.appendChild(document.createElement(`span`)),this._cells.push(n),this._container.appendChild(n),this.separatorAfter&&e===this.separatorAfter-1&&e<t-1&&(this._separatorEl=document.createElement(`span`),this._separatorEl.className=`l-input-otp-separator`,this._container.appendChild(this._separatorEl))}this._input.replaceWith(this._container),this._container.appendChild(this._input),this._initialized=!0,this._updateCells(),this._input.addEventListener(`input`,this._updateCells),this._input.addEventListener(`click`,this._updateCells),this._input.addEventListener(`keyup`,this._updateCells),this._input.addEventListener(`focus`,this._scheduleUpdateCells),this._input.addEventListener(`blur`,this._clearCells)}_teardown(){this._initialized&&=(this._input.removeEventListener(`input`,this._updateCells),this._input.removeEventListener(`click`,this._updateCells),this._input.removeEventListener(`keyup`,this._updateCells),this._input.removeEventListener(`focus`,this._scheduleUpdateCells),this._input.removeEventListener(`blur`,this._clearCells),this._container.replaceWith(this._input),this._separatorEl?.remove(),this._cells=[],this._separatorEl=null,!1)}}"
2244
2244
  }
2245
2245
  ],
2246
2246
  "exports": [
@@ -4638,11 +4638,31 @@
4638
4638
  },
4639
4639
  {
4640
4640
  "description": "Cell width and height (default: 2.75rem). Font size scales automatically.",
4641
- "name": "--size"
4641
+ "name": "--cell-size"
4642
4642
  },
4643
4643
  {
4644
4644
  "description": "Space between cells (default: 0.5rem).",
4645
- "name": "--gap"
4645
+ "name": "--cell-gap"
4646
+ },
4647
+ {
4648
+ "description": "Cell background color.",
4649
+ "name": "--cell-bg-color"
4650
+ },
4651
+ {
4652
+ "description": "Cell border color.",
4653
+ "name": "--cell-border-color"
4654
+ },
4655
+ {
4656
+ "description": "Cell border-radius.",
4657
+ "name": "--cell-border-radius"
4658
+ },
4659
+ {
4660
+ "description": "Border + ring color of the active (focused) cell.",
4661
+ "name": "--cell-focus-color"
4662
+ },
4663
+ {
4664
+ "description": "`box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).",
4665
+ "name": "--cell-focus-ring"
4646
4666
  }
4647
4667
  ],
4648
4668
  "members": [
@@ -4686,6 +4706,10 @@
4686
4706
  "kind": "field",
4687
4707
  "name": "_clearCells"
4688
4708
  },
4709
+ {
4710
+ "kind": "field",
4711
+ "name": "_scheduleUpdateCells"
4712
+ },
4689
4713
  {
4690
4714
  "kind": "method",
4691
4715
  "name": "emit",
@@ -10762,11 +10786,31 @@
10762
10786
  },
10763
10787
  {
10764
10788
  "description": "Cell width and height (default: 2.75rem). Font size scales automatically.",
10765
- "name": "--size"
10789
+ "name": "--cell-size"
10766
10790
  },
10767
10791
  {
10768
10792
  "description": "Space between cells (default: 0.5rem).",
10769
- "name": "--gap"
10793
+ "name": "--cell-gap"
10794
+ },
10795
+ {
10796
+ "description": "Cell background color.",
10797
+ "name": "--cell-bg-color"
10798
+ },
10799
+ {
10800
+ "description": "Cell border color.",
10801
+ "name": "--cell-border-color"
10802
+ },
10803
+ {
10804
+ "description": "Cell border-radius.",
10805
+ "name": "--cell-border-radius"
10806
+ },
10807
+ {
10808
+ "description": "Border + ring color of the active (focused) cell.",
10809
+ "name": "--cell-focus-color"
10810
+ },
10811
+ {
10812
+ "description": "`box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).",
10813
+ "name": "--cell-focus-ring"
10770
10814
  }
10771
10815
  ],
10772
10816
  "members": [
@@ -10843,6 +10887,11 @@
10843
10887
  "name": "_clearCells",
10844
10888
  "privacy": "private"
10845
10889
  },
10890
+ {
10891
+ "kind": "field",
10892
+ "name": "_scheduleUpdateCells",
10893
+ "privacy": "private"
10894
+ },
10846
10895
  {
10847
10896
  "kind": "method",
10848
10897
  "name": "emit",
@@ -69,18 +69,28 @@ dialog {
69
69
  line-height: 1.4;
70
70
  }
71
71
 
72
+ /* Pin body and footer so the layout stays correct when [without-header]
73
+ removes the header and only two children auto-place into the grid. */
72
74
  [part='body'] {
75
+ grid-row: 2;
73
76
  padding-inline: var(--padding);
74
77
  overflow-y: auto;
75
78
  }
76
79
 
77
80
  [part='footer'] {
81
+ grid-row: 3;
78
82
  display: flex;
79
83
  place-content: end;
80
84
  gap: 0.5rem;
81
85
  padding: var(--padding);
82
86
  }
83
87
 
88
+ /* Without a header, the body has to provide its own block-start padding
89
+ (normally inherited visually from the header padding above it). */
90
+ :host([without-header]) [part='body'] {
91
+ padding-block-start: var(--padding);
92
+ }
93
+
84
94
  ::slotted(menu[slot='footer']) {
85
95
  display: contents;
86
96
  }
@@ -9,8 +9,13 @@ import { LuxenElement } from '../../shared/luxen-element';
9
9
  * @customElement l-input-otp
10
10
  *
11
11
  * @cssproperty --digits - Number of digit boxes (default: 6). Must match input's maxlength.
12
- * @cssproperty --size - Cell width and height (default: 2.75rem). Font size scales automatically.
13
- * @cssproperty --gap - Space between cells (default: 0.5rem).
12
+ * @cssproperty --cell-size - Cell width and height (default: 2.75rem). Font size scales automatically.
13
+ * @cssproperty --cell-gap - Space between cells (default: 0.5rem).
14
+ * @cssproperty --cell-bg-color - Cell background color.
15
+ * @cssproperty --cell-border-color - Cell border color.
16
+ * @cssproperty --cell-border-radius - Cell border-radius.
17
+ * @cssproperty --cell-focus-color - Border + ring color of the active (focused) cell.
18
+ * @cssproperty --cell-focus-ring - `box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).
14
19
  */
15
20
  export declare class LuxenInputOtp extends LuxenElement {
16
21
  createRenderRoot(): this;
@@ -27,5 +32,6 @@ export declare class LuxenInputOtp extends LuxenElement {
27
32
  private _teardown;
28
33
  private _updateCells;
29
34
  private _clearCells;
35
+ private _scheduleUpdateCells;
30
36
  }
31
37
  //# sourceMappingURL=input-otp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"input-otp.d.ts","sourceRoot":"","sources":["../../../src/html/elements/input-otp/input-otp.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAc,SAAQ,YAAY;IACpC,gBAAgB;IAIzB,sFAAsF;IAEtF,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,YAAY,CAAS;IAEpB,iBAAiB;IAKjB,oBAAoB;IAO7B,OAAO,CAAC,MAAM;IA2Dd,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,YAAY,CAyBlB;IAEF,OAAO,CAAC,WAAW,CAIjB;CACH"}
1
+ {"version":3,"file":"input-otp.d.ts","sourceRoot":"","sources":["../../../src/html/elements/input-otp/input-otp.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAc,SAAQ,YAAY;IACpC,gBAAgB;IAIzB,sFAAsF;IAEtF,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,YAAY,CAAS;IAEpB,iBAAiB;IAKjB,oBAAoB;IAO7B,OAAO,CAAC,MAAM;IA4Dd,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,YAAY,CAyBlB;IAEF,OAAO,CAAC,WAAW,CAIjB;IAEF,OAAO,CAAC,oBAAoB,CAE1B;CACH"}
@@ -16,8 +16,13 @@ import { LuxenElement } from '../../shared/luxen-element';
16
16
  * @customElement l-input-otp
17
17
  *
18
18
  * @cssproperty --digits - Number of digit boxes (default: 6). Must match input's maxlength.
19
- * @cssproperty --size - Cell width and height (default: 2.75rem). Font size scales automatically.
20
- * @cssproperty --gap - Space between cells (default: 0.5rem).
19
+ * @cssproperty --cell-size - Cell width and height (default: 2.75rem). Font size scales automatically.
20
+ * @cssproperty --cell-gap - Space between cells (default: 0.5rem).
21
+ * @cssproperty --cell-bg-color - Cell background color.
22
+ * @cssproperty --cell-border-color - Cell border color.
23
+ * @cssproperty --cell-border-radius - Cell border-radius.
24
+ * @cssproperty --cell-focus-color - Border + ring color of the active (focused) cell.
25
+ * @cssproperty --cell-focus-ring - `box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).
21
26
  */
22
27
  export class LuxenInputOtp extends LuxenElement {
23
28
  constructor() {
@@ -55,6 +60,9 @@ export class LuxenInputOtp extends LuxenElement {
55
60
  cell.removeAttribute('data-active');
56
61
  }
57
62
  };
63
+ this._scheduleUpdateCells = () => {
64
+ requestAnimationFrame(this._updateCells);
65
+ };
58
66
  }
59
67
  createRenderRoot() {
60
68
  return this;
@@ -111,11 +119,12 @@ export class LuxenInputOtp extends LuxenElement {
111
119
  this._initialized = true;
112
120
  // Populate cells if input already has a value (e.g. disabled with prefilled value)
113
121
  this._updateCells();
114
- // Events
122
+ // Events — focus is deferred so it runs after the click that triggered it
123
+ // (otherwise selectionStart is stale and the active cell flickers).
115
124
  this._input.addEventListener('input', this._updateCells);
116
125
  this._input.addEventListener('click', this._updateCells);
117
126
  this._input.addEventListener('keyup', this._updateCells);
118
- this._input.addEventListener('focus', this._updateCells);
127
+ this._input.addEventListener('focus', this._scheduleUpdateCells);
119
128
  this._input.addEventListener('blur', this._clearCells);
120
129
  }
121
130
  _teardown() {
@@ -124,7 +133,7 @@ export class LuxenInputOtp extends LuxenElement {
124
133
  this._input.removeEventListener('input', this._updateCells);
125
134
  this._input.removeEventListener('click', this._updateCells);
126
135
  this._input.removeEventListener('keyup', this._updateCells);
127
- this._input.removeEventListener('focus', this._updateCells);
136
+ this._input.removeEventListener('focus', this._scheduleUpdateCells);
128
137
  this._input.removeEventListener('blur', this._clearCells);
129
138
  // Restore input to direct child
130
139
  this._container.replaceWith(this._input);
@@ -345,6 +345,74 @@ Add `autofocus` to any focusable element inside the dialog to focus it automatic
345
345
  </l-dialog>
346
346
  ```
347
347
 
348
+ ### Without header
349
+
350
+ Add `without-header` to drop the header row entirely (title and close slot). Useful for confirmation prompts where the body already carries the heading. Provide an accessible heading inside the body and rely on `Escape` or a footer action to close.
351
+
352
+ ```html
353
+ <button
354
+ type="button"
355
+ class="l-button"
356
+ command="--show"
357
+ commandfor="dialog-without-header"
358
+ >
359
+ Delete account
360
+ </button>
361
+
362
+ <l-dialog
363
+ id="dialog-without-header"
364
+ without-header
365
+ >
366
+ <div class="grid justify-items-center gap-3 pb-2 text-center">
367
+ <span
368
+ class="flex size-12 items-center justify-center rounded-full bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300"
369
+ >
370
+ <svg
371
+ class="size-6"
372
+ viewBox="0 0 24 24"
373
+ fill="none"
374
+ stroke="currentColor"
375
+ stroke-width="2"
376
+ stroke-linecap="round"
377
+ stroke-linejoin="round"
378
+ aria-hidden="true"
379
+ >
380
+ <path d="M12 9v4" />
381
+ <path d="M12 17h.01" />
382
+ <path
383
+ d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
384
+ />
385
+ </svg>
386
+ </span>
387
+ <h2 class="text-lg font-semibold">Delete your account?</h2>
388
+ <p class="text-sm">
389
+ This action is permanent and cannot be undone. All your data, including projects and billing
390
+ history, will be erased.
391
+ </p>
392
+ </div>
393
+ <menu slot="footer">
394
+ <button
395
+ autofocus
396
+ type="button"
397
+ class="l-button"
398
+ command="--hide"
399
+ commandfor="dialog-without-header"
400
+ >
401
+ Cancel
402
+ </button>
403
+ <button
404
+ type="button"
405
+ class="l-button"
406
+ data-variant="destructive"
407
+ command="--hide"
408
+ commandfor="dialog-without-header"
409
+ >
410
+ Delete account
411
+ </button>
412
+ </menu>
413
+ </l-dialog>
414
+ ```
415
+
348
416
  ## Accessibility
349
417
 
350
418
  ### Criteria
@@ -392,6 +460,14 @@ import 'luxen-ui/dialog';
392
460
 
393
461
  Open and close the dialog by toggling its `open` property, or via the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API) from any light-DOM button. Custom commands must start with `--`.
394
462
 
463
+ >
464
+ > The Invoker Commands API is [✓ Baseline Newly Available (since 2025-12-12)](https://web-platform-dx.github.io/web-features-explorer/features/invoker-commands/). For older browser versions, load the [`invokers-polyfill`](https://npmx.dev/package/invokers-polyfill) once at app startup:
465
+ >
466
+ > ```js
467
+ > import 'invokers-polyfill';
468
+ > ```
469
+ >
470
+
395
471
  <ApiTable :data="[
396
472
  { Command: '--show', Description: 'Sets `open = true`' },
397
473
  { Command: '--hide', Description: 'Sets `open = false`' },
@@ -357,6 +357,14 @@ import 'luxen-ui/drawer';
357
357
 
358
358
  Open and close the drawer by toggling its `open` property, or via the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API) from any light-DOM button. Custom commands must start with `--`.
359
359
 
360
+ >
361
+ > The Invoker Commands API is [✓ Baseline Newly Available (since 2025-12-12)](https://web-platform-dx.github.io/web-features-explorer/features/invoker-commands/). For older browser versions, load the [`invokers-polyfill`](https://npmx.dev/package/invokers-polyfill) once at app startup:
362
+ >
363
+ > ```js
364
+ > import 'invokers-polyfill';
365
+ > ```
366
+ >
367
+
360
368
  <ApiTable :data="[
361
369
  { Command: '--show', Description: 'Sets `open = true`' },
362
370
  { Command: '--hide', Description: 'Sets `open = false`' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxen-ui",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Modern web components and CSS-first UI library built with Lit. Framework-agnostic, customizable prefix, design tokens.",
5
5
  "keywords": [
6
6
  "custom-elements",