luxen-ui 0.18.2 → 0.19.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.
- package/README.md +1 -0
- package/cdn/color-scheme.d.ts +76 -0
- package/cdn/color-scheme.d.ts.map +1 -0
- package/cdn/color-scheme.js +2 -0
- package/cdn/color-scheme.js.map +1 -0
- package/cdn/config.d.ts +23 -0
- package/cdn/config.d.ts.map +1 -1
- package/cdn/custom-elements.json +142 -2
- package/cdn/elements/color-scheme-icon/color-scheme-icon.d.ts +55 -0
- package/cdn/elements/color-scheme-icon/color-scheme-icon.d.ts.map +1 -0
- package/cdn/elements/color-scheme-icon/color-scheme-icon.js +44 -0
- package/cdn/elements/color-scheme-icon/color-scheme-icon.js.map +1 -0
- package/cdn/elements/color-scheme-icon/index.d.ts +2 -0
- package/cdn/elements/color-scheme-icon/index.d.ts.map +1 -0
- package/cdn/elements/color-scheme-icon/index.js +2 -0
- package/cdn/elements/color-scheme-icon/index.js.map +1 -0
- package/cdn/elements/dropdown-item/dropdown-item.d.ts +9 -1
- package/cdn/elements/dropdown-item/dropdown-item.d.ts.map +1 -1
- package/cdn/elements/dropdown-item/dropdown-item.js +32 -23
- package/cdn/elements/dropdown-item/dropdown-item.js.map +1 -1
- package/cdn/elements/segmented-control/segmented-control.d.ts +2 -0
- package/cdn/elements/segmented-control/segmented-control.d.ts.map +1 -1
- package/cdn/elements/segmented-control/segmented-control.js +1 -1
- package/cdn/elements/segmented-control/segmented-control.js.map +1 -1
- package/cdn/elements/tabs/tabs.d.ts +28 -1
- package/cdn/elements/tabs/tabs.d.ts.map +1 -1
- package/cdn/elements/tabs/tabs.js +1 -1
- package/cdn/elements/tabs/tabs.js.map +1 -1
- package/cdn/index.d.ts +2 -0
- package/cdn/index.d.ts.map +1 -1
- package/cdn/index.js +1 -1
- package/cdn/registry.d.ts +1 -1
- package/cdn/registry.d.ts.map +1 -1
- package/cdn/registry.js.map +1 -1
- package/cdn/standalone.css +23 -4
- package/cdn/standalone.js.map +1 -1
- package/cdn/styles/elements/tabs/enclosed.css +9 -1
- package/cdn/styles/elements/tabs/line.css +14 -3
- package/dist/color-scheme.d.ts +76 -0
- package/dist/color-scheme.d.ts.map +1 -0
- package/dist/color-scheme.js +203 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/css/elements/tabs/enclosed.css +9 -1
- package/dist/css/elements/tabs/line.css +14 -3
- package/dist/custom-elements.json +142 -2
- package/dist/elements/color-scheme-icon/color-scheme-icon.css +98 -0
- package/dist/elements/color-scheme-icon/color-scheme-icon.d.ts +55 -0
- package/dist/elements/color-scheme-icon/color-scheme-icon.d.ts.map +1 -0
- package/dist/elements/color-scheme-icon/color-scheme-icon.js +138 -0
- package/dist/elements/color-scheme-icon/index.d.ts +2 -0
- package/dist/elements/color-scheme-icon/index.d.ts.map +1 -0
- package/dist/elements/color-scheme-icon/index.js +4 -0
- package/dist/elements/dropdown-item/dropdown-item.d.ts +9 -1
- package/dist/elements/dropdown-item/dropdown-item.d.ts.map +1 -1
- package/dist/elements/dropdown-item/dropdown-item.js +49 -26
- package/dist/elements/segmented-control/segmented-control.d.ts +2 -0
- package/dist/elements/segmented-control/segmented-control.d.ts.map +1 -1
- package/dist/elements/segmented-control/segmented-control.js +11 -1
- package/dist/elements/tabs/tabs.d.ts +28 -1
- package/dist/elements/tabs/tabs.d.ts.map +1 -1
- package/dist/elements/tabs/tabs.js +121 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/metadata/color-scheme-icon.json +66 -0
- package/dist/metadata/dropdown-item.json +9 -1
- package/dist/metadata/index.json +82 -3
- package/dist/metadata/tabs.json +6 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/templates/elements/color-scheme-icon.md +279 -0
- package/dist/templates/elements/dropdown.md +54 -1
- package/dist/templates/elements/tabs.md +40 -7
- package/elements.json +8 -0
- package/package.json +5 -1
- package/vite-plugin.ts +71 -8
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
import{tagName as e}from"../../registry.js";import{i as t}from"../../chunks/lit.js";import{i as n,r,t as i}from"../../chunks/decorate.js";import{a,t as o}from"../../chunks/lit-html.js";import{LuxenElement as s}from"../../shared/luxen-element.js";import c from"../../shared/styles/host.styles.js";import{PopoverController as l}from"../../shared/controllers/popover.js";var u=t(`:host{display:block}:host([disabled]){pointer-events:none;opacity:.5}.item{box-sizing:border-box;cursor:pointer;min-block-size:36px;color:var(--l-color-text-primary);white-space:nowrap;text-align:start;border-radius:4px;outline:none;align-items:center;gap:8px;padding:.375rem .5rem;font-size:.875rem;line-height:1.5;display:flex}.item:focus-visible{background:var(--l-color-bg-state-hover);outline:2px solid var(--l-focus-ring);outline-offset:-2px}@media (hover:hover){.item:hover{background:var(--l-color-bg-state-hover)}}:host([submenu-open]) .item{background:var(--l-color-bg-state-hover)}:host([variant=destructive]) .item{color:var(--l-color-text-danger)}:host([variant=destructive]) .item:focus-visible{background:var(--l-color-bg-fill-danger-soft);outline-color:var(--l-color-text-danger)}@media (hover:hover){:host([variant=destructive]) .item:hover{background:var(--l-color-bg-fill-danger-soft)}}.check{flex-shrink:0;width:16px;display:flex}.submenu-indicator{background-color:currentColor;flex-shrink:0;block-size:16px;inline-size:16px;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 50%/contain no-repeat}:host(:dir(rtl)) .submenu-indicator{scale:-1 1}.submenu{inset:unset;box-sizing:border-box;width:max-content;padding:var(--padding,.25rem);border:1px solid var(--l-color-border-overlay);border-radius:var(--border-radius,6px);background:var(--background,var(--l-color-surface-overlay));color:var(--l-color-text-primary);box-shadow:var(--shadow,0 4px 6px -1px #00000014, 0 2px 4px -2px #0000000f);margin:0;font-size:.875rem;line-height:1.5;overflow:visible}slot[name=submenu]::slotted(hr){height:1px;margin-block:var(--padding,.25rem);margin-inline:calc(var(--padding,.25rem) * -1);background:var(--l-color-divider);border:0;display:block}:host(:not([checked])) .check svg{visibility:hidden}::slotted([slot=prefix]),::slotted([slot=suffix]){flex-shrink:0;display:flex}.label{flex:1}`),d=class extends s{constructor(...t){super(...t),this._submenuFloating=new l(this,{getTriggerElement:()=>this,getFloatingElement:()=>this._submenuEl,getArrowElement:()=>null}),this.#e=``,this.#t=!1,this.#n=`normal`,this.#r=void 0,this.#i=!1,this.#a=!1,this.#o=!1,this._onSubmenuSlotChange=t=>{let n=t.target;this._hasSubmenu=n.assignedElements().some(t=>t.tagName===e(`dropdown-item`).toUpperCase())}}static{this.styles=[c,u]}#e;get value(){return this.#e}set value(e){this.#e=e}#t;get disabled(){return this.#t}set disabled(e){this.#t=e}#n;get type(){return this.#n}set type(e){this.#n=e}#r;get variant(){return this.#r}set variant(e){this.#r=e}#i;get checked(){return this.#i}set checked(e){this.#i=e}#a;get submenuOpen(){return this.#a}set submenuOpen(e){this.#a=e}#o;get
|
|
1
|
+
import{tagName as e}from"../../registry.js";import{i as t}from"../../chunks/lit.js";import{i as n,r,t as i}from"../../chunks/decorate.js";import{a,t as o}from"../../chunks/lit-html.js";import{LuxenElement as s}from"../../shared/luxen-element.js";import c from"../../shared/styles/host.styles.js";import{PopoverController as l}from"../../shared/controllers/popover.js";var u=t(`:host{display:block}:host([disabled]){pointer-events:none;opacity:.5}.item{box-sizing:border-box;cursor:pointer;min-block-size:36px;color:var(--l-color-text-primary);white-space:nowrap;text-align:start;border-radius:4px;outline:none;align-items:center;gap:8px;padding:.375rem .5rem;font-size:.875rem;line-height:1.5;display:flex}.item:focus-visible{background:var(--l-color-bg-state-hover);outline:2px solid var(--l-focus-ring);outline-offset:-2px}@media (hover:hover){.item:hover{background:var(--l-color-bg-state-hover)}}:host([submenu-open]) .item{background:var(--l-color-bg-state-hover)}:host([variant=destructive]) .item{color:var(--l-color-text-danger)}:host([variant=destructive]) .item:focus-visible{background:var(--l-color-bg-fill-danger-soft);outline-color:var(--l-color-text-danger)}@media (hover:hover){:host([variant=destructive]) .item:hover{background:var(--l-color-bg-fill-danger-soft)}}.check{flex-shrink:0;width:16px;display:flex}.submenu-indicator{background-color:currentColor;flex-shrink:0;block-size:16px;inline-size:16px;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 50%/contain no-repeat}:host(:dir(rtl)) .submenu-indicator{scale:-1 1}.submenu{inset:unset;box-sizing:border-box;width:max-content;padding:var(--padding,.25rem);border:1px solid var(--l-color-border-overlay);border-radius:var(--border-radius,6px);background:var(--background,var(--l-color-surface-overlay));color:var(--l-color-text-primary);box-shadow:var(--shadow,0 4px 6px -1px #00000014, 0 2px 4px -2px #0000000f);margin:0;font-size:.875rem;line-height:1.5;overflow:visible}slot[name=submenu]::slotted(hr){height:1px;margin-block:var(--padding,.25rem);margin-inline:calc(var(--padding,.25rem) * -1);background:var(--l-color-divider);border:0;display:block}:host(:not([checked])) .check svg{visibility:hidden}::slotted([slot=prefix]),::slotted([slot=suffix]){flex-shrink:0;display:flex}.label{flex:1}`),d=class extends s{constructor(...t){super(...t),this._submenuFloating=new l(this,{getTriggerElement:()=>this,getFloatingElement:()=>this._submenuEl,getArrowElement:()=>null}),this.#e=``,this.#t=!1,this.#n=`normal`,this.#r=void 0,this.#i=!1,this.#a=!1,this.#o=`start`,this.#s=!1,this._onSubmenuSlotChange=t=>{let n=t.target;this._hasSubmenu=n.assignedElements().some(t=>t.tagName===e(`dropdown-item`).toUpperCase())}}static{this.styles=[c,u]}#e;get value(){return this.#e}set value(e){this.#e=e}#t;get disabled(){return this.#t}set disabled(e){this.#t=e}#n;get type(){return this.#n}set type(e){this.#n=e}#r;get variant(){return this.#r}set variant(e){this.#r=e}#i;get checked(){return this.#i}set checked(e){this.#i=e}#a;get submenuOpen(){return this.#a}set submenuOpen(e){this.#a=e}#o;get checkPlacement(){return this.#o}set checkPlacement(e){this.#o=e}#s;get _hasSubmenu(){return this.#s}set _hasSubmenu(e){this.#s=e}get hasSubmenu(){return this._hasSubmenu}connectedCallback(){super.connectedCallback(),this.hasAttribute(`role`)||this.setAttribute(`role`,`none`)}get _submenuEl(){return this.shadowRoot.querySelector(`.submenu`)}getTextLabel(){let e=this.shadowRoot?.querySelector(`slot:not([name])`);return e?e.assignedNodes({flatten:!0}).map(e=>e.textContent??``).join(``).trim():(this.textContent??``).trim()}getSubmenuItems(){let t=this.shadowRoot?.querySelector(`slot[name="submenu"]`);return t?t.assignedElements().filter(t=>t.tagName===e(`dropdown-item`).toUpperCase()&&!t.disabled):[]}openSubmenu(){!this._hasSubmenu||this.disabled||this.submenuOpen||(this.submenuOpen=!0)}closeSubmenu(){for(let e of this.getSubmenuItems())e.closeSubmenu();this.submenuOpen&&=!1}updated(e){e.has(`submenuOpen`)&&this._handleSubmenuOpenChange()}_getDuration(e){let t=parseFloat(getComputedStyle(this).getPropertyValue(e));return Number.isNaN(t)?150:t}async _handleSubmenuOpenChange(){let e=this._submenuEl;if(!e)return;let t={placement:getComputedStyle(this).direction===`rtl`?`left-start`:`right-start`,distance:0,skidding:-parseFloat(getComputedStyle(e).paddingTop||`0`)};if(this.submenuOpen){if(!this.isConnected||(e.showPopover(),await this._submenuFloating.updatePosition(t),!this.submenuOpen))return;await this._submenuFloating.animateShow(e,this._getDuration(`--show-duration`)),this._submenuFloating.startPositioning(t)}else{if(!e.matches(`:popover-open`))return;this._submenuFloating.stopPositioning(),await this._submenuFloating.animateHide(e,this._getDuration(`--hide-duration`)),e.matches(`:popover-open`)&&e.hidePopover()}}_renderCheck(){return a`
|
|
2
|
+
<span
|
|
3
|
+
class="check"
|
|
4
|
+
aria-hidden="true"
|
|
5
|
+
>
|
|
6
|
+
<svg
|
|
7
|
+
width="16"
|
|
8
|
+
height="16"
|
|
9
|
+
viewBox="0 0 16 16"
|
|
10
|
+
fill="none"
|
|
11
|
+
>
|
|
12
|
+
<path
|
|
13
|
+
d="M3.5 8.5L6.5 11.5L12.5 4.5"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
stroke-width="1.5"
|
|
16
|
+
stroke-linecap="round"
|
|
17
|
+
stroke-linejoin="round"
|
|
18
|
+
/>
|
|
19
|
+
</svg>
|
|
20
|
+
</span>
|
|
21
|
+
`}render(){let e=this.type===`checkbox`,t=e&&this.checkPlacement===`start`;return a`
|
|
2
22
|
<div
|
|
3
23
|
class="item"
|
|
4
24
|
role=${e?`menuitemcheckbox`:`menuitem`}
|
|
@@ -9,33 +29,22 @@ import{tagName as e}from"../../registry.js";import{i as t}from"../../chunks/lit.
|
|
|
9
29
|
aria-controls=${this._hasSubmenu?`submenu`:o}
|
|
10
30
|
tabindex="-1"
|
|
11
31
|
>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
fill="none"
|
|
22
|
-
>
|
|
23
|
-
<path
|
|
24
|
-
d="M3.5 8.5L6.5 11.5L12.5 4.5"
|
|
25
|
-
stroke="currentColor"
|
|
26
|
-
stroke-width="1.5"
|
|
27
|
-
stroke-linecap="round"
|
|
28
|
-
stroke-linejoin="round"
|
|
29
|
-
/>
|
|
30
|
-
</svg>
|
|
31
|
-
</span>
|
|
32
|
-
`:a` <slot name="prefix"></slot> `}
|
|
32
|
+
<!-- The check owns the leading column by default, which is why a
|
|
33
|
+
checkbox item has no room for a prefix there. Moving it to the
|
|
34
|
+
trailing edge frees that column, so an item can carry its own icon
|
|
35
|
+
and still show whether it is on — a theme row with a sun/moon, a
|
|
36
|
+
Show-grid row with a grid glyph. The aria-checked above is what
|
|
37
|
+
assistive tech reads, and it is unaffected by where the mark sits.
|
|
38
|
+
(No backticks in this comment: it lives inside a template literal.) -->
|
|
39
|
+
${t?this._renderCheck():o}
|
|
40
|
+
${t?o:a`<slot name="prefix"></slot>`}
|
|
33
41
|
<span
|
|
34
42
|
class="label"
|
|
35
43
|
id="label"
|
|
36
44
|
><slot></slot
|
|
37
45
|
></span>
|
|
38
46
|
<slot name="suffix"></slot>
|
|
47
|
+
${e&&this.checkPlacement===`end`?this._renderCheck():o}
|
|
39
48
|
${this._hasSubmenu?a`<span
|
|
40
49
|
class="submenu-indicator"
|
|
41
50
|
aria-hidden="true"
|
|
@@ -54,5 +63,5 @@ import{tagName as e}from"../../registry.js";import{i as t}from"../../chunks/lit.
|
|
|
54
63
|
@slotchange=${this._onSubmenuSlotChange}
|
|
55
64
|
></slot>
|
|
56
65
|
</div>
|
|
57
|
-
`}};i([n()],d.prototype,`value`,null),i([n({type:Boolean,reflect:!0})],d.prototype,`disabled`,null),i([n()],d.prototype,`type`,null),i([n({reflect:!0})],d.prototype,`variant`,null),i([n({type:Boolean,reflect:!0})],d.prototype,`checked`,null),i([n({type:Boolean,reflect:!0,attribute:`submenu-open`})],d.prototype,`submenuOpen`,null),i([r()],d.prototype,`_hasSubmenu`,null);export{d as DropdownItem};
|
|
66
|
+
`}};i([n()],d.prototype,`value`,null),i([n({type:Boolean,reflect:!0})],d.prototype,`disabled`,null),i([n()],d.prototype,`type`,null),i([n({reflect:!0})],d.prototype,`variant`,null),i([n({type:Boolean,reflect:!0})],d.prototype,`checked`,null),i([n({type:Boolean,reflect:!0,attribute:`submenu-open`})],d.prototype,`submenuOpen`,null),i([n({reflect:!0,attribute:`check-placement`})],d.prototype,`checkPlacement`,null),i([r()],d.prototype,`_hasSubmenu`,null);export{d as DropdownItem};
|
|
58
67
|
//# sourceMappingURL=dropdown-item.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown-item.js","names":[],"sources":["../../../src/html/elements/dropdown-item/dropdown-item.css?inline","../../../src/html/elements/dropdown-item/dropdown-item.ts"],"sourcesContent":[":host {\n display: block;\n}\n\n:host([disabled]) {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: 8px;\n box-sizing: border-box;\n min-block-size: 36px;\n padding: 0.375rem 0.5rem;\n cursor: pointer;\n outline: none;\n border-radius: 4px;\n font-size: 0.875rem;\n line-height: 1.5;\n color: var(--l-color-text-primary);\n white-space: nowrap;\n text-align: start;\n}\n\n.item:focus-visible {\n /* A ring distinct from the hover tint (which is the same low-contrast fill).\n Negative offset keeps the 2px ring inside the row so it never collides with\n the panel edge or a neighboring item. */\n background: var(--l-color-bg-state-hover);\n outline: 2px solid var(--l-focus-ring);\n outline-offset: -2px;\n}\n\n@media (hover: hover) {\n .item:hover {\n background: var(--l-color-bg-state-hover);\n }\n}\n\n/* Parent item stays highlighted while its submenu is open. */\n:host([submenu-open]) .item {\n background: var(--l-color-bg-state-hover);\n}\n\n/* Destructive variant — a delete-style action. Red label at rest, danger-tinted\n fill and focus ring on hover/focus so the highlight reads as \"danger\" rather\n than the neutral hover tint. */\n:host([variant='destructive']) .item {\n color: var(--l-color-text-danger);\n}\n\n:host([variant='destructive']) .item:focus-visible {\n background: var(--l-color-bg-fill-danger-soft);\n outline-color: var(--l-color-text-danger);\n}\n\n@media (hover: hover) {\n :host([variant='destructive']) .item:hover {\n background: var(--l-color-bg-fill-danger-soft);\n }\n}\n\n.check {\n display: flex;\n width: 16px;\n flex-shrink: 0;\n}\n\n/* Inline-end chevron marking an item that opens a submenu. */\n.submenu-indicator {\n inline-size: 16px;\n block-size: 16px;\n flex-shrink: 0;\n background-color: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")\n no-repeat center / contain;\n}\n\n:host(:dir(rtl)) .submenu-indicator {\n scale: -1 1;\n}\n\n/* Submenu panel — mirrors the l-dropdown panel. The public custom properties\n (--padding, --background, --border-radius, --shadow) inherit from the\n ancestor l-dropdown through the light DOM, so a themed dropdown themes its\n submenus too; their fallbacks repeat the dropdown defaults for standalone use. */\n.submenu {\n inset: unset;\n overflow: visible;\n box-sizing: border-box;\n width: max-content;\n padding: var(--padding, 0.25rem);\n margin: 0;\n border: 1px solid var(--l-color-border-overlay);\n border-radius: var(--border-radius, 6px);\n background: var(--background, var(--l-color-surface-overlay));\n color: var(--l-color-text-primary);\n box-shadow: var(--shadow, 0 4px 6px -1px rgb(0 0 0 / 8%), 0 2px 4px -2px rgb(0 0 0 / 6%));\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n/* Lightweight separator alternative to <l-divider> — same dimensions, no extra import. */\nslot[name='submenu']::slotted(hr) {\n display: block;\n height: 1px;\n margin-block: var(--padding, 0.25rem);\n margin-inline: calc(var(--padding, 0.25rem) * -1);\n border: 0;\n background: var(--l-color-divider);\n}\n\n:host(:not([checked])) .check svg {\n visibility: hidden;\n}\n\n::slotted([slot='prefix']),\n::slotted([slot='suffix']) {\n display: flex;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1;\n}\n","import { html, nothing, unsafeCSS, type PropertyValues } from 'lit';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { property, state } from 'lit/decorators.js';\nimport type { Placement } from '@floating-ui/dom';\nimport { PopoverController } from '../../shared/controllers/popover.js';\nimport { tagName } from '../../registry.js';\nimport hostStyles from '../../shared/styles/host.styles.js';\nimport rawStyles from './dropdown-item.css?inline';\n\nconst styles = unsafeCSS(rawStyles);\n\n/**\n * A menu item for use inside `<l-dropdown>`.\n *\n * @slot - Label text.\n * @slot prefix - Leading content (e.g. icon).\n * @slot suffix - Trailing content.\n * @slot submenu - Nested `l-dropdown-item` elements rendered in a submenu panel anchored to this item. Drop an `<hr>` between them for a separator.\n *\n * @csspart submenu - The floating submenu panel.\n *\n * @cssproperty --color - Text color.\n *\n * @attribute variant - destructive — Renders the item as a destructive action (red), for delete-style entries.\n *\n * @customElement l-dropdown-item\n */\nexport class DropdownItem extends LuxenElement {\n static override styles = [hostStyles, styles];\n\n private _submenuFloating = new PopoverController(this, {\n getTriggerElement: () => this,\n getFloatingElement: () => this._submenuEl,\n getArrowElement: () => null,\n });\n\n /** The value associated with this item. */\n @property()\n accessor value = '';\n\n /** Disables the item. */\n @property({ type: Boolean, reflect: true })\n accessor disabled = false;\n\n /** The type of item: `normal` or `checkbox`. */\n @property()\n accessor type: 'normal' | 'checkbox' = 'normal';\n\n /** Visual variant. `destructive` marks a delete-style action in red. */\n @property({ reflect: true })\n accessor variant: 'destructive' | undefined = undefined;\n\n /** Whether the checkbox item is checked. */\n @property({ type: Boolean, reflect: true })\n accessor checked = false;\n\n /** Whether this item's submenu is open. Managed by the parent `l-dropdown`. */\n @property({ type: Boolean, reflect: true, attribute: 'submenu-open' })\n accessor submenuOpen = false;\n\n @state()\n private accessor _hasSubmenu = false;\n\n /** Whether this item has nested `slot=\"submenu\"` items. */\n get hasSubmenu(): boolean {\n return this._hasSubmenu;\n }\n\n override connectedCallback() {\n super.connectedCallback();\n // The host is a generic wrapper between the `role=\"menu\"` panel and the\n // `role=\"menuitem\"` row in its shadow root; `none` removes it from the\n // accessibility tree so the menu owns its menuitems directly (ARIA\n // required-owned-elements). The inner row keeps its menuitem role.\n // Guard preserves a consumer-set role.\n if (!this.hasAttribute('role')) this.setAttribute('role', 'none');\n }\n\n private get _submenuEl(): HTMLElement | null {\n return this.shadowRoot!.querySelector<HTMLElement>('.submenu');\n }\n\n /** Returns the text label of this item (excludes nested submenu items). */\n getTextLabel(): string {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');\n if (!slot) return (this.textContent ?? '').trim();\n return slot\n .assignedNodes({ flatten: true })\n .map((node) => node.textContent ?? '')\n .join('')\n .trim();\n }\n\n /** Returns the enabled `l-dropdown-item` elements slotted into this item's submenu. */\n getSubmenuItems(): DropdownItem[] {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"submenu\"]');\n if (!slot) return [];\n return (slot.assignedElements() as DropdownItem[]).filter(\n (el) => el.tagName === tagName('dropdown-item').toUpperCase() && !el.disabled,\n );\n }\n\n /** Opens this item's submenu panel. No-op without submenu items. */\n openSubmenu() {\n if (!this._hasSubmenu || this.disabled || this.submenuOpen) return;\n this.submenuOpen = true;\n }\n\n /** Closes this item's submenu panel, including any open nested submenus. */\n closeSubmenu() {\n for (const child of this.getSubmenuItems()) child.closeSubmenu();\n if (this.submenuOpen) this.submenuOpen = false;\n }\n\n override updated(changed: PropertyValues<this>) {\n if (changed.has('submenuOpen')) {\n void this._handleSubmenuOpenChange();\n }\n }\n\n private _getDuration(prop: '--show-duration' | '--hide-duration'): number {\n const parsed = parseFloat(getComputedStyle(this).getPropertyValue(prop));\n return Number.isNaN(parsed) ? 150 : parsed;\n }\n\n private async _handleSubmenuOpenChange() {\n const panel = this._submenuEl;\n if (!panel) return;\n\n const isRTL = getComputedStyle(this).direction === 'rtl';\n const posOpts = {\n placement: (isRTL ? 'left-start' : 'right-start') as Placement,\n distance: 0,\n // Pull the panel up by its own padding so its first item lines up with this item.\n skidding: -parseFloat(getComputedStyle(panel).paddingTop || '0'),\n };\n\n if (this.submenuOpen) {\n if (!this.isConnected) return;\n panel.showPopover();\n await this._submenuFloating.updatePosition(posOpts);\n if (!this.submenuOpen) return;\n await this._submenuFloating.animateShow(panel, this._getDuration('--show-duration'));\n this._submenuFloating.startPositioning(posOpts);\n } else {\n if (!panel.matches(':popover-open')) return;\n this._submenuFloating.stopPositioning();\n await this._submenuFloating.animateHide(panel, this._getDuration('--hide-duration'));\n if (panel.matches(':popover-open')) panel.hidePopover();\n }\n }\n\n private _onSubmenuSlotChange = (e: Event) => {\n const slot = e.target as HTMLSlotElement;\n this._hasSubmenu = slot\n .assignedElements()\n .some((el) => el.tagName === tagName('dropdown-item').toUpperCase());\n };\n\n override render() {\n const isCheckbox = this.type === 'checkbox';\n\n return html`\n <div\n class=\"item\"\n role=${isCheckbox ? 'menuitemcheckbox' : 'menuitem'}\n aria-checked=${isCheckbox ? String(this.checked) : nothing}\n aria-disabled=${this.disabled ? 'true' : nothing}\n aria-haspopup=${this._hasSubmenu ? 'menu' : nothing}\n aria-expanded=${this._hasSubmenu ? String(this.submenuOpen) : nothing}\n aria-controls=${this._hasSubmenu ? 'submenu' : nothing}\n tabindex=\"-1\"\n >\n ${isCheckbox\n ? html`\n <span\n class=\"check\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M3.5 8.5L6.5 11.5L12.5 4.5\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n `\n : html` <slot name=\"prefix\"></slot> `}\n <span\n class=\"label\"\n id=\"label\"\n ><slot></slot\n ></span>\n <slot name=\"suffix\"></slot>\n ${this._hasSubmenu\n ? html`<span\n class=\"submenu-indicator\"\n aria-hidden=\"true\"\n ></span>`\n : nothing}\n </div>\n <div\n class=\"submenu\"\n part=\"submenu\"\n id=\"submenu\"\n popover=\"manual\"\n role=\"menu\"\n aria-labelledby=\"label\"\n >\n <slot\n name=\"submenu\"\n @slotchange=${this._onSubmenuSlotChange}\n ></slot>\n </div>\n `;\n }\n}\n"],"mappings":"gXCSA,IAAM,EAAS,EAAU,6xEAAS,EAkBrB,EAAb,cAAkC,CAAa,qDAGlB,IAAI,EAAkB,KAAM,CACrD,sBAAyB,KACzB,uBAA0B,KAAK,WAC/B,oBAAuB,IACzB,CAAC,UAIgB,WAIG,WAImB,iBAIO,IAAA,WAI3B,WAII,WAGQ,6BA2FC,GAAa,CAC3C,IAAM,EAAO,EAAE,OACf,KAAK,YAAc,EAChB,iBAAiB,CAAC,CAClB,KAAM,GAAO,EAAG,UAAY,EAAQ,eAAe,CAAC,CAAC,YAAY,CAAC,CACvE,qBAjIyB,CAAC,EAAY,CAAM,SAUnC,OAAA,8CAIA,UAAA,iDAIA,MAAA,6CAIA,SAAA,gDAIA,SAAA,gDAIA,aAAA,oDAGQ,aAAA,6CAGjB,IAAI,YAAsB,CACxB,OAAO,KAAK,WACd,CAEA,mBAA6B,CAC3B,MAAM,kBAAkB,EAMnB,KAAK,aAAa,MAAM,GAAG,KAAK,aAAa,OAAQ,MAAM,CAClE,CAEA,IAAY,YAAiC,CAC3C,OAAO,KAAK,WAAY,cAA2B,UAAU,CAC/D,CAGA,cAAuB,CACrB,IAAM,EAAO,KAAK,YAAY,cAA+B,kBAAkB,EAE/E,OADK,EACE,EACJ,cAAc,CAAE,QAAS,EAAK,CAAC,CAAC,CAChC,IAAK,GAAS,EAAK,aAAe,EAAE,CAAC,CACrC,KAAK,EAAE,CAAC,CACR,KAAK,GALW,KAAK,aAAe,GAAA,CAAI,KAAK,CAMlD,CAGA,iBAAkC,CAChC,IAAM,EAAO,KAAK,YAAY,cAA+B,sBAAsB,EAEnF,OADK,EACG,EAAK,iBAAiB,CAAC,CAAoB,OAChD,GAAO,EAAG,UAAY,EAAQ,eAAe,CAAC,CAAC,YAAY,GAAK,CAAC,EAAG,QACvE,EAHkB,CAAC,CAIrB,CAGA,aAAc,CACR,CAAC,KAAK,aAAe,KAAK,UAAY,KAAK,cAC/C,KAAK,YAAc,GACrB,CAGA,cAAe,CACb,IAAK,IAAM,KAAS,KAAK,gBAAgB,EAAG,EAAM,aAAa,EAC/D,AAAsB,KAAK,cAAc,EAC3C,CAEA,QAAiB,EAA+B,CAC1C,EAAQ,IAAI,aAAa,GAC3B,KAAU,yBAAyB,CAEvC,CAEA,aAAqB,EAAqD,CACxE,IAAM,EAAS,WAAW,iBAAiB,IAAI,CAAC,CAAC,iBAAiB,CAAI,CAAC,EACvE,OAAO,OAAO,MAAM,CAAM,EAAI,IAAM,CACtC,CAEA,MAAc,0BAA2B,CACvC,IAAM,EAAQ,KAAK,WACnB,GAAI,CAAC,EAAO,OAGZ,IAAM,EAAU,CACd,UAFY,iBAAiB,IAAI,CAAC,CAAC,YAAc,MAE7B,aAAe,cACnC,SAAU,EAEV,SAAU,CAAC,WAAW,iBAAiB,CAAK,CAAC,CAAC,YAAc,GAAG,CACjE,EAEA,GAAI,KAAK,YAAa,CAIpB,GAHI,CAAC,KAAK,cACV,EAAM,YAAY,EAClB,MAAM,KAAK,iBAAiB,eAAe,CAAO,EAC9C,CAAC,KAAK,aAAa,OACvB,MAAM,KAAK,iBAAiB,YAAY,EAAO,KAAK,aAAa,iBAAiB,CAAC,EACnF,KAAK,iBAAiB,iBAAiB,CAAO,CAChD,KAAO,CACL,GAAI,CAAC,EAAM,QAAQ,eAAe,EAAG,OACrC,KAAK,iBAAiB,gBAAgB,EACtC,MAAM,KAAK,iBAAiB,YAAY,EAAO,KAAK,aAAa,iBAAiB,CAAC,EAC/E,EAAM,QAAQ,eAAe,GAAG,EAAM,YAAY,CACxD,CACF,CASA,QAAkB,CAChB,IAAM,EAAa,KAAK,OAAS,WAEjC,MAAO,EAAI;;;eAGA,EAAa,mBAAqB,WAAW;uBACrC,EAAa,OAAO,KAAK,OAAO,EAAI,EAAQ;wBAC3C,KAAK,SAAW,OAAS,EAAQ;wBACjC,KAAK,YAAc,OAAS,EAAQ;wBACpC,KAAK,YAAc,OAAO,KAAK,WAAW,EAAI,EAAQ;wBACtD,KAAK,YAAc,UAAY,EAAQ;;;UAGrD,EACE,CAAI;;;;;;;;;;;;;;;;;;;;cAqBJ,CAAI,gCAAgC;;;;;;;UAOtC,KAAK,YACH,CAAI;;;sBAIJ,EAAQ;;;;;;;;;;;;wBAYI,KAAK,qBAAqB;;;KAIhD,CACF,KA3LG,EAAS,CAAA,EAAA,EAAA,UAAA,QAAA,IAAA,KAIT,EAAS,CAAE,KAAM,QAAS,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,WAAA,IAAA,KAIzC,EAAS,CAAA,EAAA,EAAA,UAAA,OAAA,IAAA,KAIT,EAAS,CAAE,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,UAAA,IAAA,KAI1B,EAAS,CAAE,KAAM,QAAS,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,UAAA,IAAA,KAIzC,EAAS,CAAE,KAAM,QAAS,QAAS,GAAM,UAAW,cAAe,CAAC,CAAA,EAAA,EAAA,UAAA,cAAA,IAAA,KAGpE,EAAM,CAAA,EAAA,EAAA,UAAA,cAAA,IAAA"}
|
|
1
|
+
{"version":3,"file":"dropdown-item.js","names":[],"sources":["../../../src/html/elements/dropdown-item/dropdown-item.css?inline","../../../src/html/elements/dropdown-item/dropdown-item.ts"],"sourcesContent":[":host {\n display: block;\n}\n\n:host([disabled]) {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: 8px;\n box-sizing: border-box;\n min-block-size: 36px;\n padding: 0.375rem 0.5rem;\n cursor: pointer;\n outline: none;\n border-radius: 4px;\n font-size: 0.875rem;\n line-height: 1.5;\n color: var(--l-color-text-primary);\n white-space: nowrap;\n text-align: start;\n}\n\n.item:focus-visible {\n /* A ring distinct from the hover tint (which is the same low-contrast fill).\n Negative offset keeps the 2px ring inside the row so it never collides with\n the panel edge or a neighboring item. */\n background: var(--l-color-bg-state-hover);\n outline: 2px solid var(--l-focus-ring);\n outline-offset: -2px;\n}\n\n@media (hover: hover) {\n .item:hover {\n background: var(--l-color-bg-state-hover);\n }\n}\n\n/* Parent item stays highlighted while its submenu is open. */\n:host([submenu-open]) .item {\n background: var(--l-color-bg-state-hover);\n}\n\n/* Destructive variant — a delete-style action. Red label at rest, danger-tinted\n fill and focus ring on hover/focus so the highlight reads as \"danger\" rather\n than the neutral hover tint. */\n:host([variant='destructive']) .item {\n color: var(--l-color-text-danger);\n}\n\n:host([variant='destructive']) .item:focus-visible {\n background: var(--l-color-bg-fill-danger-soft);\n outline-color: var(--l-color-text-danger);\n}\n\n@media (hover: hover) {\n :host([variant='destructive']) .item:hover {\n background: var(--l-color-bg-fill-danger-soft);\n }\n}\n\n.check {\n display: flex;\n width: 16px;\n flex-shrink: 0;\n}\n\n/* Inline-end chevron marking an item that opens a submenu. */\n.submenu-indicator {\n inline-size: 16px;\n block-size: 16px;\n flex-shrink: 0;\n background-color: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")\n no-repeat center / contain;\n}\n\n:host(:dir(rtl)) .submenu-indicator {\n scale: -1 1;\n}\n\n/* Submenu panel — mirrors the l-dropdown panel. The public custom properties\n (--padding, --background, --border-radius, --shadow) inherit from the\n ancestor l-dropdown through the light DOM, so a themed dropdown themes its\n submenus too; their fallbacks repeat the dropdown defaults for standalone use. */\n.submenu {\n inset: unset;\n overflow: visible;\n box-sizing: border-box;\n width: max-content;\n padding: var(--padding, 0.25rem);\n margin: 0;\n border: 1px solid var(--l-color-border-overlay);\n border-radius: var(--border-radius, 6px);\n background: var(--background, var(--l-color-surface-overlay));\n color: var(--l-color-text-primary);\n box-shadow: var(--shadow, 0 4px 6px -1px rgb(0 0 0 / 8%), 0 2px 4px -2px rgb(0 0 0 / 6%));\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n/* Lightweight separator alternative to <l-divider> — same dimensions, no extra import. */\nslot[name='submenu']::slotted(hr) {\n display: block;\n height: 1px;\n margin-block: var(--padding, 0.25rem);\n margin-inline: calc(var(--padding, 0.25rem) * -1);\n border: 0;\n background: var(--l-color-divider);\n}\n\n:host(:not([checked])) .check svg {\n visibility: hidden;\n}\n\n::slotted([slot='prefix']),\n::slotted([slot='suffix']) {\n display: flex;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1;\n}\n","import { html, nothing, unsafeCSS, type PropertyValues } from 'lit';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { property, state } from 'lit/decorators.js';\nimport type { Placement } from '@floating-ui/dom';\nimport { PopoverController } from '../../shared/controllers/popover.js';\nimport { tagName } from '../../registry.js';\nimport hostStyles from '../../shared/styles/host.styles.js';\nimport rawStyles from './dropdown-item.css?inline';\n\nconst styles = unsafeCSS(rawStyles);\n\n/**\n * A menu item for use inside `<l-dropdown>`.\n *\n * @slot - Label text.\n * @slot prefix - Leading content (e.g. icon). A `type=\"checkbox\"` item ignores it unless `check-placement=\"end\"` frees the leading column.\n * @slot suffix - Trailing content.\n * @slot submenu - Nested `l-dropdown-item` elements rendered in a submenu panel anchored to this item. Drop an `<hr>` between them for a separator.\n *\n * @csspart submenu - The floating submenu panel.\n *\n * @cssproperty --color - Text color.\n *\n * @attribute variant - destructive — Renders the item as a destructive action (red), for delete-style entries.\n *\n * @customElement l-dropdown-item\n */\nexport class DropdownItem extends LuxenElement {\n static override styles = [hostStyles, styles];\n\n private _submenuFloating = new PopoverController(this, {\n getTriggerElement: () => this,\n getFloatingElement: () => this._submenuEl,\n getArrowElement: () => null,\n });\n\n /** The value associated with this item. */\n @property()\n accessor value = '';\n\n /** Disables the item. */\n @property({ type: Boolean, reflect: true })\n accessor disabled = false;\n\n /** The type of item: `normal` or `checkbox`. */\n @property()\n accessor type: 'normal' | 'checkbox' = 'normal';\n\n /** Visual variant. `destructive` marks a delete-style action in red. */\n @property({ reflect: true })\n accessor variant: 'destructive' | undefined = undefined;\n\n /** Whether the checkbox item is checked. */\n @property({ type: Boolean, reflect: true })\n accessor checked = false;\n\n /** Whether this item's submenu is open. Managed by the parent `l-dropdown`. */\n @property({ type: Boolean, reflect: true, attribute: 'submenu-open' })\n accessor submenuOpen = false;\n\n /**\n * Which side of a `type=\"checkbox\"` item carries the check mark. `start` (the\n * default) puts it in the leading column, where it takes the place of any\n * `prefix`. `end` moves it to the trailing edge, freeing that column so the\n * item can show its own icon alongside the state.\n */\n @property({ reflect: true, attribute: 'check-placement' })\n accessor checkPlacement: 'start' | 'end' = 'start';\n\n @state()\n private accessor _hasSubmenu = false;\n\n /** Whether this item has nested `slot=\"submenu\"` items. */\n get hasSubmenu(): boolean {\n return this._hasSubmenu;\n }\n\n override connectedCallback() {\n super.connectedCallback();\n // The host is a generic wrapper between the `role=\"menu\"` panel and the\n // `role=\"menuitem\"` row in its shadow root; `none` removes it from the\n // accessibility tree so the menu owns its menuitems directly (ARIA\n // required-owned-elements). The inner row keeps its menuitem role.\n // Guard preserves a consumer-set role.\n if (!this.hasAttribute('role')) this.setAttribute('role', 'none');\n }\n\n private get _submenuEl(): HTMLElement | null {\n return this.shadowRoot!.querySelector<HTMLElement>('.submenu');\n }\n\n /** Returns the text label of this item (excludes nested submenu items). */\n getTextLabel(): string {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');\n if (!slot) return (this.textContent ?? '').trim();\n return slot\n .assignedNodes({ flatten: true })\n .map((node) => node.textContent ?? '')\n .join('')\n .trim();\n }\n\n /** Returns the enabled `l-dropdown-item` elements slotted into this item's submenu. */\n getSubmenuItems(): DropdownItem[] {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"submenu\"]');\n if (!slot) return [];\n return (slot.assignedElements() as DropdownItem[]).filter(\n (el) => el.tagName === tagName('dropdown-item').toUpperCase() && !el.disabled,\n );\n }\n\n /** Opens this item's submenu panel. No-op without submenu items. */\n openSubmenu() {\n if (!this._hasSubmenu || this.disabled || this.submenuOpen) return;\n this.submenuOpen = true;\n }\n\n /** Closes this item's submenu panel, including any open nested submenus. */\n closeSubmenu() {\n for (const child of this.getSubmenuItems()) child.closeSubmenu();\n if (this.submenuOpen) this.submenuOpen = false;\n }\n\n override updated(changed: PropertyValues<this>) {\n if (changed.has('submenuOpen')) {\n void this._handleSubmenuOpenChange();\n }\n }\n\n private _getDuration(prop: '--show-duration' | '--hide-duration'): number {\n const parsed = parseFloat(getComputedStyle(this).getPropertyValue(prop));\n return Number.isNaN(parsed) ? 150 : parsed;\n }\n\n private async _handleSubmenuOpenChange() {\n const panel = this._submenuEl;\n if (!panel) return;\n\n const isRTL = getComputedStyle(this).direction === 'rtl';\n const posOpts = {\n placement: (isRTL ? 'left-start' : 'right-start') as Placement,\n distance: 0,\n // Pull the panel up by its own padding so its first item lines up with this item.\n skidding: -parseFloat(getComputedStyle(panel).paddingTop || '0'),\n };\n\n if (this.submenuOpen) {\n if (!this.isConnected) return;\n panel.showPopover();\n await this._submenuFloating.updatePosition(posOpts);\n if (!this.submenuOpen) return;\n await this._submenuFloating.animateShow(panel, this._getDuration('--show-duration'));\n this._submenuFloating.startPositioning(posOpts);\n } else {\n if (!panel.matches(':popover-open')) return;\n this._submenuFloating.stopPositioning();\n await this._submenuFloating.animateHide(panel, this._getDuration('--hide-duration'));\n if (panel.matches(':popover-open')) panel.hidePopover();\n }\n }\n\n private _renderCheck() {\n return html`\n <span\n class=\"check\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M3.5 8.5L6.5 11.5L12.5 4.5\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n `;\n }\n\n private _onSubmenuSlotChange = (e: Event) => {\n const slot = e.target as HTMLSlotElement;\n this._hasSubmenu = slot\n .assignedElements()\n .some((el) => el.tagName === tagName('dropdown-item').toUpperCase());\n };\n\n override render() {\n const isCheckbox = this.type === 'checkbox';\n const leadingCheck = isCheckbox && this.checkPlacement === 'start';\n\n return html`\n <div\n class=\"item\"\n role=${isCheckbox ? 'menuitemcheckbox' : 'menuitem'}\n aria-checked=${isCheckbox ? String(this.checked) : nothing}\n aria-disabled=${this.disabled ? 'true' : nothing}\n aria-haspopup=${this._hasSubmenu ? 'menu' : nothing}\n aria-expanded=${this._hasSubmenu ? String(this.submenuOpen) : nothing}\n aria-controls=${this._hasSubmenu ? 'submenu' : nothing}\n tabindex=\"-1\"\n >\n <!-- The check owns the leading column by default, which is why a\n checkbox item has no room for a prefix there. Moving it to the\n trailing edge frees that column, so an item can carry its own icon\n and still show whether it is on — a theme row with a sun/moon, a\n Show-grid row with a grid glyph. The aria-checked above is what\n assistive tech reads, and it is unaffected by where the mark sits.\n (No backticks in this comment: it lives inside a template literal.) -->\n ${leadingCheck ? this._renderCheck() : nothing}\n ${leadingCheck ? nothing : html`<slot name=\"prefix\"></slot>`}\n <span\n class=\"label\"\n id=\"label\"\n ><slot></slot\n ></span>\n <slot name=\"suffix\"></slot>\n ${isCheckbox && this.checkPlacement === 'end' ? this._renderCheck() : nothing}\n ${this._hasSubmenu\n ? html`<span\n class=\"submenu-indicator\"\n aria-hidden=\"true\"\n ></span>`\n : nothing}\n </div>\n <div\n class=\"submenu\"\n part=\"submenu\"\n id=\"submenu\"\n popover=\"manual\"\n role=\"menu\"\n aria-labelledby=\"label\"\n >\n <slot\n name=\"submenu\"\n @slotchange=${this._onSubmenuSlotChange}\n ></slot>\n </div>\n `;\n }\n}\n"],"mappings":"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"}
|
|
@@ -40,6 +40,8 @@ export declare class SegmentedControl extends LuxenFormAssociatedElement {
|
|
|
40
40
|
private _setupTimer;
|
|
41
41
|
private _buttons;
|
|
42
42
|
private _resizeObserver;
|
|
43
|
+
/** Segments the consumer authored `aria-disabled` on — never ours to clear. */
|
|
44
|
+
private _authoredDisabled;
|
|
43
45
|
/** Value of the selected segment (its `value` attribute, else its index). */
|
|
44
46
|
value: string;
|
|
45
47
|
/** Control height, aligned with buttons and form controls at the same size. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmented-control.d.ts","sourceRoot":"","sources":["../../../src/html/elements/segmented-control/segmented-control.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE7D,sEAAsE;AACtE,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBACX,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAKzC;AAED,UAAU,wBAAwB;IAChC,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,qBAAa,gBAAiB,SAAQ,0BAA0B;IACrD,gBAAgB;IAIzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,eAAe,CAA+B;
|
|
1
|
+
{"version":3,"file":"segmented-control.d.ts","sourceRoot":"","sources":["../../../src/html/elements/segmented-control/segmented-control.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE7D,sEAAsE;AACtE,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBACX,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAKzC;AAED,UAAU,wBAAwB;IAChC,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,qBAAa,gBAAiB,SAAQ,0BAA0B;IACrD,gBAAgB;IAIzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,eAAe,CAA+B;IACtD,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB,CAAoC;IAE7D,6EAA6E;IAE7E,KAAK,SAAM;IAEX,+EAA+E;IAE/E,IAAI,EAAE,oBAAoB,CAAQ;IAElC,yEAAyE;IAEzE,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,oDAAoD;IAEpD,SAAS,UAAS;IAIT,iBAAiB;IAUjB,oBAAoB;IAM7B,mFAAmF;IACnF,OAAO,CAAC,SAAS;IAOR,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAsBrC,iBAAiB;IAMjB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,cAAc;IAOjF,OAAO,CAAC,MAAM;IAyDd,OAAO,CAAC,SAAS;IAUjB,wFAAwF;IACxF,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,OAAO;IAuBf,qFAAqF;IACrF,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,WAAW;IAInB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,QAAQ,CAQd;IAKF,OAAO,CAAC,UAAU,CAkChB;CACH;AAQD,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,CAAC,SAAS,MAAM,wBAAwB,EACvD,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,wBAAwB,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3E,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;IACR,gBAAgB,CACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,kCAAkC,EAC5C,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;IACR,mBAAmB,CAAC,CAAC,SAAS,MAAM,wBAAwB,EAC1D,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,wBAAwB,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3E,OAAO,CAAC,EAAE,OAAO,GAAG,oBAAoB,GACvC,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,kCAAkC,EAC5C,OAAO,CAAC,EAAE,OAAO,GAAG,oBAAoB,GACvC,IAAI,CAAC;CACT"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{i as e,t}from"../../chunks/decorate.js";import{LuxenFormAssociatedElement as n}from"../../shared/luxen-form-associated-element.js";var r=class extends Event{constructor(e,t){super(`change`,{bubbles:!0,composed:!1,cancelable:!1}),this.value=e,this.index=t}},i=class extends n{constructor(...e){super(...e),this._initialized=!1,this._setupTimer=0,this._buttons=[],this._resizeObserver=null,this.value=``,this.size=`md`,this.fullWidth=!1,this._onClick=e=>{if(this.disabled)return;let t=e.target.closest(`button`);if(!t)return;let n=this._buttons.indexOf(t);n<0||this._isDisabled(t)||(this._select(n),t.focus())},this._onKeyDown=e=>{if(this.disabled)return;let t=e,n=e.target;if(n.getAttribute(`role`)!==`radio`)return;let r=this._buttons.indexOf(n);if(r<0)return;let i=r;switch(t.key){case`ArrowRight`:case`ArrowDown`:i=this._nextEnabled(r,1);break;case`ArrowLeft`:case`ArrowUp`:i=this._nextEnabled(r,-1);break;case`Home`:i=this._nextEnabled(-1,1);break;case`End`:i=this._nextEnabled(this._buttons.length,-1);break;case` `:break;default:return}e.preventDefault(),this._select(i),this._buttons[i].focus()}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this._trySetup()||(this._setupTimer=window.setTimeout(()=>this._trySetup(),0))}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._setupTimer),this._teardown()}_trySetup(){return this._initialized||!this.isConnected?!0:this.querySelectorAll(`button`).length<2?!1:(this._setup(),!0)}updated(e){this._initialized&&(e.has(`value`)&&this._select(this._resolveActiveIndex(),!1),e.has(`size`)&&this._updateIndicator(),e.has(`label`)&&(this.label?this.setAttribute(`aria-label`,this.label):this.removeAttribute(`aria-label`)),e.has(`disabled`)&&this._applyDisabledState())}formResetCallback(){this.value=this._defaultFormValue,super.formResetCallback()}formStateRestoreCallback(e,t){this.value=e,super.formStateRestoreCallback(e,t)}_setup(){if(this._buttons=Array.from(this.querySelectorAll(`button`)),this._buttons.length<2)return;this._initialized=!0,this.setAttribute(`role`,`radiogroup`),this.label&&this.setAttribute(`aria-label`,this.label);let e=this._resolveActiveIndex();for(let t=0;t<this._buttons.length;t++){let n=this._buttons[t];n.type=`button`,n.setAttribute(`role`,`radio`),n.setAttribute(`aria-checked`,String(t===e)),n.setAttribute(`tabindex`,t===e?`0`:`-1`),(n.textContent??``).trim()===``&&(n.setAttribute(`data-icon-only`,``),!n.hasAttribute(`aria-label`)&&!n.hasAttribute(`aria-labelledby`)&&console.warn(`<${this.localName}>: icon-only segment #${t} has no accessible name; add aria-label.`))}this.addEventListener(`click`,this._onClick),this.addEventListener(`keydown`,this._onKeyDown),this._applyDisabledState(),this._syncValueFromIndex(e),this._defaultFormValue=this.value,this._syncFormValue(this.value),this._updateIndicator(),this._resizeObserver=new ResizeObserver(()=>this._updateIndicator()),this._resizeObserver.observe(this)}_teardown(){this.removeEventListener(`click`,this._onClick),this.removeEventListener(`keydown`,this._onKeyDown),this._resizeObserver?.disconnect(),this._resizeObserver=null,this._initialized=!1}_resolveActiveIndex(){let{_buttons:e}=this;if(this.value!==``){let t=e.findIndex(e=>(e.getAttribute(`value`)??``)===this.value);if(t>=0)return t;let n=Number(this.value);if(Number.isInteger(n)&&n>=0&&n<e.length)return n}let t=e.findIndex(e=>e.getAttribute(`aria-checked`)===`true`);if(t>=0)return t;let n=e.findIndex(e=>!this._isDisabled(e));return n>=0?n:0}_syncValueFromIndex(e){this.value=this._buttons[e]?.getAttribute(`value`)??String(e)}_select(e,t=!0){if(e<0||e>=this._buttons.length)return;let n=this._buttons[e].getAttribute(`aria-checked`)!==`true`;for(let t=0;t<this._buttons.length;t++){let n=t===e;this._buttons[t].setAttribute(`aria-checked`,String(n)),this._buttons[t].setAttribute(`tabindex`,n&&!this.disabled?`0`:`-1`)}this._syncValueFromIndex(e),this._syncFormValue(this.value),this._updateIndicator(),t&&n&&(this.hasInteracted=!0,this.dispatchEvent(new r(this.value,e)))}_nextEnabled(e,t){let n=this._buttons.length;for(let r=1;r<=n;r++){let i=(e+t*r+n*r)%n;if(!this._isDisabled(this._buttons[i]))return i}return e}_isDisabled(e){return e.disabled||e.getAttribute(`aria-disabled`)===`true`}_applyDisabledState(){for(let e of this._buttons)this.disabled?(e.setAttribute(`aria-disabled`,`true`),e.setAttribute(`tabindex`,`-1`)):(e.removeAttribute(`aria-disabled`),e.setAttribute(`tabindex`,e.getAttribute(`aria-checked`)===`true`?`0`:`-1`))}_updateIndicator(){let e=this._buttons.find(e=>e.getAttribute(`aria-checked`)===`true`);e&&(this.style.setProperty(`--_indicator-left`,`${e.offsetLeft}px`),this.style.setProperty(`--_indicator-width`,`${e.offsetWidth}px`))}};t([e({reflect:!0})],i.prototype,`value`,void 0),t([e({reflect:!0})],i.prototype,`size`,void 0),t([e({reflect:!0})],i.prototype,`label`,void 0),t([e({type:Boolean,reflect:!0,attribute:`full-width`})],i.prototype,`fullWidth`,void 0);export{r as SegmentChangeEvent,i as SegmentedControl};
|
|
1
|
+
import{i as e,t}from"../../chunks/decorate.js";import{LuxenFormAssociatedElement as n}from"../../shared/luxen-form-associated-element.js";var r=class extends Event{constructor(e,t){super(`change`,{bubbles:!0,composed:!1,cancelable:!1}),this.value=e,this.index=t}},i=class extends n{constructor(...e){super(...e),this._initialized=!1,this._setupTimer=0,this._buttons=[],this._resizeObserver=null,this._authoredDisabled=new WeakSet,this.value=``,this.size=`md`,this.fullWidth=!1,this._onClick=e=>{if(this.disabled)return;let t=e.target.closest(`button`);if(!t)return;let n=this._buttons.indexOf(t);n<0||this._isDisabled(t)||(this._select(n),t.focus())},this._onKeyDown=e=>{if(this.disabled)return;let t=e,n=e.target;if(n.getAttribute(`role`)!==`radio`)return;let r=this._buttons.indexOf(n);if(r<0)return;let i=r;switch(t.key){case`ArrowRight`:case`ArrowDown`:i=this._nextEnabled(r,1);break;case`ArrowLeft`:case`ArrowUp`:i=this._nextEnabled(r,-1);break;case`Home`:i=this._nextEnabled(-1,1);break;case`End`:i=this._nextEnabled(this._buttons.length,-1);break;case` `:break;default:return}e.preventDefault(),this._select(i),this._buttons[i].focus()}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this._trySetup()||(this._setupTimer=window.setTimeout(()=>this._trySetup(),0))}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._setupTimer),this._teardown()}_trySetup(){return this._initialized||!this.isConnected?!0:this.querySelectorAll(`button`).length<2?!1:(this._setup(),!0)}updated(e){this._initialized&&(e.has(`value`)&&this._select(this._resolveActiveIndex(),!1),e.has(`size`)&&this._updateIndicator(),e.has(`label`)&&(this.label?this.setAttribute(`aria-label`,this.label):this.removeAttribute(`aria-label`)),e.has(`disabled`)&&this._applyDisabledState())}formResetCallback(){this.value=this._defaultFormValue,super.formResetCallback()}formStateRestoreCallback(e,t){this.value=e,super.formStateRestoreCallback(e,t)}_setup(){if(this._buttons=Array.from(this.querySelectorAll(`button`)),this._buttons.length<2)return;this._initialized=!0,this.setAttribute(`role`,`radiogroup`),this.label&&this.setAttribute(`aria-label`,this.label);let e=this._resolveActiveIndex();for(let t=0;t<this._buttons.length;t++){let n=this._buttons[t];n.type=`button`,n.setAttribute(`role`,`radio`),n.setAttribute(`aria-checked`,String(t===e)),n.setAttribute(`tabindex`,t===e?`0`:`-1`),n.getAttribute(`aria-disabled`)===`true`&&this._authoredDisabled.add(n),(n.textContent??``).trim()===``&&(n.setAttribute(`data-icon-only`,``),!n.hasAttribute(`aria-label`)&&!n.hasAttribute(`aria-labelledby`)&&console.warn(`<${this.localName}>: icon-only segment #${t} has no accessible name; add aria-label.`))}this.addEventListener(`click`,this._onClick),this.addEventListener(`keydown`,this._onKeyDown),this._applyDisabledState(),this._syncValueFromIndex(e),this._defaultFormValue=this.value,this._syncFormValue(this.value),this._updateIndicator(),this._resizeObserver=new ResizeObserver(()=>this._updateIndicator()),this._resizeObserver.observe(this)}_teardown(){this.removeEventListener(`click`,this._onClick),this.removeEventListener(`keydown`,this._onKeyDown),this._resizeObserver?.disconnect(),this._resizeObserver=null,this._initialized=!1}_resolveActiveIndex(){let{_buttons:e}=this;if(this.value!==``){let t=e.findIndex(e=>(e.getAttribute(`value`)??``)===this.value);if(t>=0)return t;let n=Number(this.value);if(Number.isInteger(n)&&n>=0&&n<e.length)return n}let t=e.findIndex(e=>e.getAttribute(`aria-checked`)===`true`);if(t>=0)return t;let n=e.findIndex(e=>!this._isDisabled(e));return n>=0?n:0}_syncValueFromIndex(e){this.value=this._buttons[e]?.getAttribute(`value`)??String(e)}_select(e,t=!0){if(e<0||e>=this._buttons.length)return;let n=this._buttons[e].getAttribute(`aria-checked`)!==`true`;for(let t=0;t<this._buttons.length;t++){let n=t===e;this._buttons[t].setAttribute(`aria-checked`,String(n)),this._buttons[t].setAttribute(`tabindex`,n&&!this.disabled?`0`:`-1`)}this._syncValueFromIndex(e),this._syncFormValue(this.value),this._updateIndicator(),t&&n&&(this.hasInteracted=!0,this.dispatchEvent(new r(this.value,e)))}_nextEnabled(e,t){let n=this._buttons.length;for(let r=1;r<=n;r++){let i=(e+t*r+n*r)%n;if(!this._isDisabled(this._buttons[i]))return i}return e}_isDisabled(e){return e.disabled||e.getAttribute(`aria-disabled`)===`true`}_applyDisabledState(){for(let e of this._buttons)this.disabled?(e.setAttribute(`aria-disabled`,`true`),e.setAttribute(`tabindex`,`-1`)):(this._authoredDisabled.has(e)||e.removeAttribute(`aria-disabled`),e.setAttribute(`tabindex`,e.getAttribute(`aria-checked`)===`true`?`0`:`-1`))}_updateIndicator(){let e=this._buttons.find(e=>e.getAttribute(`aria-checked`)===`true`);e&&(this.style.setProperty(`--_indicator-left`,`${e.offsetLeft}px`),this.style.setProperty(`--_indicator-width`,`${e.offsetWidth}px`))}};t([e({reflect:!0})],i.prototype,`value`,void 0),t([e({reflect:!0})],i.prototype,`size`,void 0),t([e({reflect:!0})],i.prototype,`label`,void 0),t([e({type:Boolean,reflect:!0,attribute:`full-width`})],i.prototype,`fullWidth`,void 0);export{r as SegmentChangeEvent,i as SegmentedControl};
|
|
2
2
|
//# sourceMappingURL=segmented-control.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../../src/html/elements/segmented-control/segmented-control.ts"],"sourcesContent":["import { property } from 'lit/decorators.js';\nimport { LuxenFormAssociatedElement } from '../../shared/luxen-form-associated-element.js';\n\nexport type SegmentedControlSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Fired when the selected segment changes. Bubbles; not composed. */\nexport class SegmentChangeEvent extends Event {\n readonly value: string;\n readonly index: number;\n constructor(value: string, index: number) {\n super('change', { bubbles: true, composed: false, cancelable: false });\n this.value = value;\n this.index = index;\n }\n}\n\ninterface SegmentedControlEventMap {\n change: SegmentChangeEvent;\n}\n\n/**\n * @summary A single-select segmented control: a compact, mutually-exclusive\n * switch between a few options (radio-group semantics with a sliding pill).\n * Progressively enhances light-DOM `<button>`s so it works before JS runs.\n * Form-associated: give it a `name` and its selected `value` is submitted with\n * the form (and restored on reset), like a native radio group.\n *\n * @example\n * Mark the initially-selected segment with `aria-checked=\"true\"` (or set\n * `value` to its `value` attribute) so it is styled before the script upgrades\n * the element.\n * ```html\n * <l-segmented-control label=\"Metric\" value=\"cost\">\n * <button value=\"volume\">Volume</button>\n * <button value=\"cost\" aria-checked=\"true\">Cost</button>\n * </l-segmented-control>\n * ```\n *\n * @event change - Fired when the selected segment changes. Not cancelable. Bubbles. Properties: `value: string`, `index: number`.\n *\n * @cssproperty [--indicator-color=var(--l-color-surface)] - Background of the sliding pill behind the selected segment.\n * @cssproperty [--border-radius=var(--l-radius-lg)] - Corner radius of the track.\n *\n * @customElement l-segmented-control\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- typed addEventListener overloads merged below; no uninitialized properties.\nexport class SegmentedControl extends LuxenFormAssociatedElement {\n override createRenderRoot() {\n return this;\n }\n\n private _initialized = false;\n private _setupTimer = 0;\n private _buttons: HTMLButtonElement[] = [];\n private _resizeObserver: ResizeObserver | null = null;\n\n /** Value of the selected segment (its `value` attribute, else its index). */\n @property({ reflect: true })\n value = '';\n\n /** Control height, aligned with buttons and form controls at the same size. */\n @property({ reflect: true })\n size: SegmentedControlSize = 'md';\n\n /** Accessible label announced for the group. Not displayed on screen. */\n @property({ reflect: true })\n label?: string;\n\n /** Stretch segments to fill the container width. */\n @property({ type: Boolean, reflect: true, attribute: 'full-width' })\n fullWidth = false;\n\n // --- Lifecycle ---\n\n override connectedCallback() {\n super.connectedCallback();\n // Children may not be parsed yet when the element upgrades mid-parse: try\n // synchronously, then retry once on a macrotask. setTimeout, not rAF — rAF\n // is suspended in hidden documents, so the element would stay inert there.\n if (!this._trySetup()) {\n this._setupTimer = window.setTimeout(() => this._trySetup(), 0);\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n clearTimeout(this._setupTimer);\n this._teardown();\n }\n\n /** @returns true when setup ran or was already done; false to schedule a retry. */\n private _trySetup(): boolean {\n if (this._initialized || !this.isConnected) return true;\n if (this.querySelectorAll('button').length < 2) return false;\n this._setup();\n return true;\n }\n\n override updated(changed: Map<string, unknown>) {\n if (!this._initialized) return;\n if (changed.has('value')) {\n this._select(this._resolveActiveIndex(), false);\n }\n if (changed.has('size')) {\n this._updateIndicator();\n }\n if (changed.has('label')) {\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else {\n this.removeAttribute('aria-label');\n }\n }\n if (changed.has('disabled')) {\n this._applyDisabledState();\n }\n }\n\n // --- Form lifecycle ---\n\n override formResetCallback() {\n // Restoring `value` re-selects the default segment via `updated()`.\n this.value = this._defaultFormValue;\n super.formResetCallback();\n }\n\n override formStateRestoreCallback(state: string, mode: 'restore' | 'autocomplete') {\n this.value = state;\n super.formStateRestoreCallback(state, mode);\n }\n\n // --- Setup / Teardown ---\n\n private _setup() {\n this._buttons = Array.from(this.querySelectorAll('button'));\n if (this._buttons.length < 2) return;\n this._initialized = true;\n\n this.setAttribute('role', 'radiogroup');\n if (this.label) this.setAttribute('aria-label', this.label);\n\n const activeIndex = this._resolveActiveIndex();\n\n for (let i = 0; i < this._buttons.length; i++) {\n const btn = this._buttons[i];\n // Force type=button so a segment never submits the enclosing form.\n btn.type = 'button';\n btn.setAttribute('role', 'radio');\n btn.setAttribute('aria-checked', String(i === activeIndex));\n btn.setAttribute('tabindex', i === activeIndex ? '0' : '-1');\n\n // A segment with no visible text is icon-only: square it and require an\n // accessible name (the icon itself is decorative to assistive tech).\n if ((btn.textContent ?? '').trim() === '') {\n btn.setAttribute('data-icon-only', '');\n if (!btn.hasAttribute('aria-label') && !btn.hasAttribute('aria-labelledby')) {\n // eslint-disable-next-line no-console\n console.warn(\n `<${this.localName}>: icon-only segment #${i} has no accessible name; add aria-label.`,\n );\n }\n }\n }\n\n this.addEventListener('click', this._onClick);\n this.addEventListener('keydown', this._onKeyDown);\n\n // Reflect a whole-control `disabled` onto the segments (aria + tab order).\n this._applyDisabledState();\n\n // Canonicalize `value` to the active segment now that roles are set, and\n // seed the form value (the value a form reset restores to).\n this._syncValueFromIndex(activeIndex);\n this._defaultFormValue = this.value;\n this._syncFormValue(this.value);\n\n // Initial pill position. Synchronous first (offset reads force layout),\n // then a ResizeObserver keeps it correct across hidden→visible transitions\n // and size changes — rAF is suspended in hidden documents, so it must not\n // be the only scheduling mechanism.\n this._updateIndicator();\n this._resizeObserver = new ResizeObserver(() => this._updateIndicator());\n this._resizeObserver.observe(this);\n }\n\n private _teardown() {\n this.removeEventListener('click', this._onClick);\n this.removeEventListener('keydown', this._onKeyDown);\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n this._initialized = false;\n }\n\n // --- Selection ---\n\n /** Resolve the selected index from `value`, else an authored `aria-checked`, else 0. */\n private _resolveActiveIndex(): number {\n const { _buttons } = this;\n if (this.value !== '') {\n const byValue = _buttons.findIndex((b) => (b.getAttribute('value') ?? '') === this.value);\n if (byValue >= 0) return byValue;\n const asIndex = Number(this.value);\n if (Number.isInteger(asIndex) && asIndex >= 0 && asIndex < _buttons.length) return asIndex;\n }\n const checked = _buttons.findIndex((b) => b.getAttribute('aria-checked') === 'true');\n if (checked >= 0) return checked;\n // No explicit selection: default to the first enabled segment (else 0).\n const firstEnabled = _buttons.findIndex((b) => !this._isDisabled(b));\n return firstEnabled >= 0 ? firstEnabled : 0;\n }\n\n private _syncValueFromIndex(index: number) {\n this.value = this._buttons[index]?.getAttribute('value') ?? String(index);\n }\n\n private _select(index: number, emitEvent = true) {\n if (index < 0 || index >= this._buttons.length) return;\n // Whether this is an actual change — re-selecting the current segment must\n // not re-fire `change` (matches native radio / `<select>`).\n const changed = this._buttons[index].getAttribute('aria-checked') !== 'true';\n\n for (let i = 0; i < this._buttons.length; i++) {\n const isActive = i === index;\n this._buttons[i].setAttribute('aria-checked', String(isActive));\n // Disabled control keeps every segment out of the tab order.\n this._buttons[i].setAttribute('tabindex', isActive && !this.disabled ? '0' : '-1');\n }\n\n this._syncValueFromIndex(index);\n this._syncFormValue(this.value);\n this._updateIndicator();\n\n if (emitEvent && changed) {\n this.hasInteracted = true;\n this.dispatchEvent(new SegmentChangeEvent(this.value, index));\n }\n }\n\n /** Index of the next non-disabled segment from `from`, moving by `dir`, wrapping. */\n private _nextEnabled(from: number, dir: 1 | -1): number {\n const len = this._buttons.length;\n for (let step = 1; step <= len; step++) {\n const i = (from + dir * step + len * step) % len;\n if (!this._isDisabled(this._buttons[i])) return i;\n }\n return from;\n }\n\n private _isDisabled(btn: HTMLButtonElement): boolean {\n return btn.disabled || btn.getAttribute('aria-disabled') === 'true';\n }\n\n /** Reflect the host `disabled` state onto the segments (aria + tab order). */\n private _applyDisabledState() {\n for (const btn of this._buttons) {\n if (this.disabled) {\n btn.setAttribute('aria-disabled', 'true');\n btn.setAttribute('tabindex', '-1');\n } else {\n btn.removeAttribute('aria-disabled');\n // Restore the roving tabindex (only the selected segment is tabbable).\n btn.setAttribute('tabindex', btn.getAttribute('aria-checked') === 'true' ? '0' : '-1');\n }\n }\n }\n\n // --- Indicator ---\n\n private _updateIndicator() {\n const active = this._buttons.find((b) => b.getAttribute('aria-checked') === 'true');\n if (!active) return;\n this.style.setProperty('--_indicator-left', `${active.offsetLeft}px`);\n this.style.setProperty('--_indicator-width', `${active.offsetWidth}px`);\n }\n\n // --- Event handlers ---\n\n private _onClick = (e: Event) => {\n if (this.disabled) return;\n const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('button');\n if (!btn) return;\n const index = this._buttons.indexOf(btn);\n if (index < 0 || this._isDisabled(btn)) return;\n this._select(index);\n btn.focus();\n };\n\n // Typed `Event` (not `KeyboardEvent`): the listener is attached to `this`,\n // whose merged addEventListener overloads (see the interface below) fall back\n // to the platform `EventListener` signature for non-map events like `keydown`.\n private _onKeyDown = (e: Event) => {\n if (this.disabled) return;\n const ke = e as KeyboardEvent;\n const target = e.target as HTMLElement;\n if (target.getAttribute('role') !== 'radio') return;\n\n const current = this._buttons.indexOf(target as HTMLButtonElement);\n if (current < 0) return;\n\n let next = current;\n switch (ke.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n next = this._nextEnabled(current, 1);\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n next = this._nextEnabled(current, -1);\n break;\n case 'Home':\n next = this._nextEnabled(-1, 1);\n break;\n case 'End':\n next = this._nextEnabled(this._buttons.length, -1);\n break;\n case ' ':\n // Space (re)selects the focused segment (APG radio group).\n break;\n default:\n return;\n }\n e.preventDefault();\n this._select(next);\n this._buttons[next].focus();\n };\n}\n\n// Type `addEventListener('change', …)` so listeners receive a `SegmentChangeEvent`\n// (with `.value`/`.index`) instead of a bare `Event`. `change` collides with\n// lib.dom's `GlobalEventHandlersEventMap`, so it can't be augmented globally —\n// the overloads are declaration-merged onto this element's interface instead.\n// The interface MUST come after the class (the CEM analyzer dedups on the first\n// declaration of the name). See `tabs.ts` for the fully-annotated reference.\nexport interface SegmentedControl {\n addEventListener<K extends keyof SegmentedControlEventMap>(\n type: K,\n listener: (this: SegmentedControl, ev: SegmentedControlEventMap[K]) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | AddEventListenerOptions,\n ): void;\n removeEventListener<K extends keyof SegmentedControlEventMap>(\n type: K,\n listener: (this: SegmentedControl, ev: SegmentedControlEventMap[K]) => void,\n options?: boolean | EventListenerOptions,\n ): void;\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | EventListenerOptions,\n ): void;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../../src/html/elements/segmented-control/segmented-control.ts"],"sourcesContent":["import { property } from 'lit/decorators.js';\nimport { LuxenFormAssociatedElement } from '../../shared/luxen-form-associated-element.js';\n\nexport type SegmentedControlSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Fired when the selected segment changes. Bubbles; not composed. */\nexport class SegmentChangeEvent extends Event {\n readonly value: string;\n readonly index: number;\n constructor(value: string, index: number) {\n super('change', { bubbles: true, composed: false, cancelable: false });\n this.value = value;\n this.index = index;\n }\n}\n\ninterface SegmentedControlEventMap {\n change: SegmentChangeEvent;\n}\n\n/**\n * @summary A single-select segmented control: a compact, mutually-exclusive\n * switch between a few options (radio-group semantics with a sliding pill).\n * Progressively enhances light-DOM `<button>`s so it works before JS runs.\n * Form-associated: give it a `name` and its selected `value` is submitted with\n * the form (and restored on reset), like a native radio group.\n *\n * @example\n * Mark the initially-selected segment with `aria-checked=\"true\"` (or set\n * `value` to its `value` attribute) so it is styled before the script upgrades\n * the element.\n * ```html\n * <l-segmented-control label=\"Metric\" value=\"cost\">\n * <button value=\"volume\">Volume</button>\n * <button value=\"cost\" aria-checked=\"true\">Cost</button>\n * </l-segmented-control>\n * ```\n *\n * @event change - Fired when the selected segment changes. Not cancelable. Bubbles. Properties: `value: string`, `index: number`.\n *\n * @cssproperty [--indicator-color=var(--l-color-surface)] - Background of the sliding pill behind the selected segment.\n * @cssproperty [--border-radius=var(--l-radius-lg)] - Corner radius of the track.\n *\n * @customElement l-segmented-control\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- typed addEventListener overloads merged below; no uninitialized properties.\nexport class SegmentedControl extends LuxenFormAssociatedElement {\n override createRenderRoot() {\n return this;\n }\n\n private _initialized = false;\n private _setupTimer = 0;\n private _buttons: HTMLButtonElement[] = [];\n private _resizeObserver: ResizeObserver | null = null;\n /** Segments the consumer authored `aria-disabled` on — never ours to clear. */\n private _authoredDisabled = new WeakSet<HTMLButtonElement>();\n\n /** Value of the selected segment (its `value` attribute, else its index). */\n @property({ reflect: true })\n value = '';\n\n /** Control height, aligned with buttons and form controls at the same size. */\n @property({ reflect: true })\n size: SegmentedControlSize = 'md';\n\n /** Accessible label announced for the group. Not displayed on screen. */\n @property({ reflect: true })\n label?: string;\n\n /** Stretch segments to fill the container width. */\n @property({ type: Boolean, reflect: true, attribute: 'full-width' })\n fullWidth = false;\n\n // --- Lifecycle ---\n\n override connectedCallback() {\n super.connectedCallback();\n // Children may not be parsed yet when the element upgrades mid-parse: try\n // synchronously, then retry once on a macrotask. setTimeout, not rAF — rAF\n // is suspended in hidden documents, so the element would stay inert there.\n if (!this._trySetup()) {\n this._setupTimer = window.setTimeout(() => this._trySetup(), 0);\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n clearTimeout(this._setupTimer);\n this._teardown();\n }\n\n /** @returns true when setup ran or was already done; false to schedule a retry. */\n private _trySetup(): boolean {\n if (this._initialized || !this.isConnected) return true;\n if (this.querySelectorAll('button').length < 2) return false;\n this._setup();\n return true;\n }\n\n override updated(changed: Map<string, unknown>) {\n if (!this._initialized) return;\n if (changed.has('value')) {\n this._select(this._resolveActiveIndex(), false);\n }\n if (changed.has('size')) {\n this._updateIndicator();\n }\n if (changed.has('label')) {\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else {\n this.removeAttribute('aria-label');\n }\n }\n if (changed.has('disabled')) {\n this._applyDisabledState();\n }\n }\n\n // --- Form lifecycle ---\n\n override formResetCallback() {\n // Restoring `value` re-selects the default segment via `updated()`.\n this.value = this._defaultFormValue;\n super.formResetCallback();\n }\n\n override formStateRestoreCallback(state: string, mode: 'restore' | 'autocomplete') {\n this.value = state;\n super.formStateRestoreCallback(state, mode);\n }\n\n // --- Setup / Teardown ---\n\n private _setup() {\n this._buttons = Array.from(this.querySelectorAll('button'));\n if (this._buttons.length < 2) return;\n this._initialized = true;\n\n this.setAttribute('role', 'radiogroup');\n if (this.label) this.setAttribute('aria-label', this.label);\n\n const activeIndex = this._resolveActiveIndex();\n\n for (let i = 0; i < this._buttons.length; i++) {\n const btn = this._buttons[i];\n // Force type=button so a segment never submits the enclosing form.\n btn.type = 'button';\n btn.setAttribute('role', 'radio');\n btn.setAttribute('aria-checked', String(i === activeIndex));\n btn.setAttribute('tabindex', i === activeIndex ? '0' : '-1');\n\n // Record an authored `aria-disabled` before the host-level state is\n // mirrored onto the segments: `_applyDisabledState()` clears that mirror\n // whenever the control is enabled, and must not take this one with it.\n if (btn.getAttribute('aria-disabled') === 'true') this._authoredDisabled.add(btn);\n\n // A segment with no visible text is icon-only: square it and require an\n // accessible name (the icon itself is decorative to assistive tech).\n if ((btn.textContent ?? '').trim() === '') {\n btn.setAttribute('data-icon-only', '');\n if (!btn.hasAttribute('aria-label') && !btn.hasAttribute('aria-labelledby')) {\n // eslint-disable-next-line no-console\n console.warn(\n `<${this.localName}>: icon-only segment #${i} has no accessible name; add aria-label.`,\n );\n }\n }\n }\n\n this.addEventListener('click', this._onClick);\n this.addEventListener('keydown', this._onKeyDown);\n\n // Reflect a whole-control `disabled` onto the segments (aria + tab order).\n this._applyDisabledState();\n\n // Canonicalize `value` to the active segment now that roles are set, and\n // seed the form value (the value a form reset restores to).\n this._syncValueFromIndex(activeIndex);\n this._defaultFormValue = this.value;\n this._syncFormValue(this.value);\n\n // Initial pill position. Synchronous first (offset reads force layout),\n // then a ResizeObserver keeps it correct across hidden→visible transitions\n // and size changes — rAF is suspended in hidden documents, so it must not\n // be the only scheduling mechanism.\n this._updateIndicator();\n this._resizeObserver = new ResizeObserver(() => this._updateIndicator());\n this._resizeObserver.observe(this);\n }\n\n private _teardown() {\n this.removeEventListener('click', this._onClick);\n this.removeEventListener('keydown', this._onKeyDown);\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n this._initialized = false;\n }\n\n // --- Selection ---\n\n /** Resolve the selected index from `value`, else an authored `aria-checked`, else 0. */\n private _resolveActiveIndex(): number {\n const { _buttons } = this;\n if (this.value !== '') {\n const byValue = _buttons.findIndex((b) => (b.getAttribute('value') ?? '') === this.value);\n if (byValue >= 0) return byValue;\n const asIndex = Number(this.value);\n if (Number.isInteger(asIndex) && asIndex >= 0 && asIndex < _buttons.length) return asIndex;\n }\n const checked = _buttons.findIndex((b) => b.getAttribute('aria-checked') === 'true');\n if (checked >= 0) return checked;\n // No explicit selection: default to the first enabled segment (else 0).\n const firstEnabled = _buttons.findIndex((b) => !this._isDisabled(b));\n return firstEnabled >= 0 ? firstEnabled : 0;\n }\n\n private _syncValueFromIndex(index: number) {\n this.value = this._buttons[index]?.getAttribute('value') ?? String(index);\n }\n\n private _select(index: number, emitEvent = true) {\n if (index < 0 || index >= this._buttons.length) return;\n // Whether this is an actual change — re-selecting the current segment must\n // not re-fire `change` (matches native radio / `<select>`).\n const changed = this._buttons[index].getAttribute('aria-checked') !== 'true';\n\n for (let i = 0; i < this._buttons.length; i++) {\n const isActive = i === index;\n this._buttons[i].setAttribute('aria-checked', String(isActive));\n // Disabled control keeps every segment out of the tab order.\n this._buttons[i].setAttribute('tabindex', isActive && !this.disabled ? '0' : '-1');\n }\n\n this._syncValueFromIndex(index);\n this._syncFormValue(this.value);\n this._updateIndicator();\n\n if (emitEvent && changed) {\n this.hasInteracted = true;\n this.dispatchEvent(new SegmentChangeEvent(this.value, index));\n }\n }\n\n /** Index of the next non-disabled segment from `from`, moving by `dir`, wrapping. */\n private _nextEnabled(from: number, dir: 1 | -1): number {\n const len = this._buttons.length;\n for (let step = 1; step <= len; step++) {\n const i = (from + dir * step + len * step) % len;\n if (!this._isDisabled(this._buttons[i])) return i;\n }\n return from;\n }\n\n private _isDisabled(btn: HTMLButtonElement): boolean {\n return btn.disabled || btn.getAttribute('aria-disabled') === 'true';\n }\n\n /** Reflect the host `disabled` state onto the segments (aria + tab order). */\n private _applyDisabledState() {\n for (const btn of this._buttons) {\n if (this.disabled) {\n btn.setAttribute('aria-disabled', 'true');\n btn.setAttribute('tabindex', '-1');\n } else {\n // Only clear the mirror this method set: a segment the consumer marked\n // `aria-disabled` stays disabled when the control is re-enabled.\n if (!this._authoredDisabled.has(btn)) btn.removeAttribute('aria-disabled');\n // Restore the roving tabindex (only the selected segment is tabbable).\n btn.setAttribute('tabindex', btn.getAttribute('aria-checked') === 'true' ? '0' : '-1');\n }\n }\n }\n\n // --- Indicator ---\n\n private _updateIndicator() {\n const active = this._buttons.find((b) => b.getAttribute('aria-checked') === 'true');\n if (!active) return;\n this.style.setProperty('--_indicator-left', `${active.offsetLeft}px`);\n this.style.setProperty('--_indicator-width', `${active.offsetWidth}px`);\n }\n\n // --- Event handlers ---\n\n private _onClick = (e: Event) => {\n if (this.disabled) return;\n const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('button');\n if (!btn) return;\n const index = this._buttons.indexOf(btn);\n if (index < 0 || this._isDisabled(btn)) return;\n this._select(index);\n btn.focus();\n };\n\n // Typed `Event` (not `KeyboardEvent`): the listener is attached to `this`,\n // whose merged addEventListener overloads (see the interface below) fall back\n // to the platform `EventListener` signature for non-map events like `keydown`.\n private _onKeyDown = (e: Event) => {\n if (this.disabled) return;\n const ke = e as KeyboardEvent;\n const target = e.target as HTMLElement;\n if (target.getAttribute('role') !== 'radio') return;\n\n const current = this._buttons.indexOf(target as HTMLButtonElement);\n if (current < 0) return;\n\n let next = current;\n switch (ke.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n next = this._nextEnabled(current, 1);\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n next = this._nextEnabled(current, -1);\n break;\n case 'Home':\n next = this._nextEnabled(-1, 1);\n break;\n case 'End':\n next = this._nextEnabled(this._buttons.length, -1);\n break;\n case ' ':\n // Space (re)selects the focused segment (APG radio group).\n break;\n default:\n return;\n }\n e.preventDefault();\n this._select(next);\n this._buttons[next].focus();\n };\n}\n\n// Type `addEventListener('change', …)` so listeners receive a `SegmentChangeEvent`\n// (with `.value`/`.index`) instead of a bare `Event`. `change` collides with\n// lib.dom's `GlobalEventHandlersEventMap`, so it can't be augmented globally —\n// the overloads are declaration-merged onto this element's interface instead.\n// The interface MUST come after the class (the CEM analyzer dedups on the first\n// declaration of the name). See `tabs.ts` for the fully-annotated reference.\nexport interface SegmentedControl {\n addEventListener<K extends keyof SegmentedControlEventMap>(\n type: K,\n listener: (this: SegmentedControl, ev: SegmentedControlEventMap[K]) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | AddEventListenerOptions,\n ): void;\n removeEventListener<K extends keyof SegmentedControlEventMap>(\n type: K,\n listener: (this: SegmentedControl, ev: SegmentedControlEventMap[K]) => void,\n options?: boolean | EventListenerOptions,\n ): void;\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | EventListenerOptions,\n ): void;\n}\n"],"mappings":"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"}
|
|
@@ -28,7 +28,15 @@ interface TabsEventMap {
|
|
|
28
28
|
* </l-tabs>
|
|
29
29
|
* ```
|
|
30
30
|
*
|
|
31
|
-
* @
|
|
31
|
+
* @example
|
|
32
|
+
* A tab button marked `disabled` (or `aria-disabled="true"`) is skipped by
|
|
33
|
+
* arrow keys, `Home` and `End`, and cannot be selected by a click or by
|
|
34
|
+
* `value` — it still owns its panel, so the tab/panel order is unchanged.
|
|
35
|
+
* ```html
|
|
36
|
+
* <button disabled>Billing</button>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @event change - Fired when the active tab changes. Bubbles. Not cancelable. Properties: `index: number`, `name: string | null`.
|
|
32
40
|
*
|
|
33
41
|
* @cssproperty [--indicator-color=var(--l-color-text-primary)] - `line` variant: color of the active underline that slides under the selected tab.
|
|
34
42
|
* @cssproperty [--indicator-thickness=2px] - `line` variant: thickness of the active underline.
|
|
@@ -64,6 +72,25 @@ export declare class Tabs extends LuxenElement {
|
|
|
64
72
|
private _setup;
|
|
65
73
|
private _teardown;
|
|
66
74
|
private _selectTab;
|
|
75
|
+
/**
|
|
76
|
+
* Both spellings count, and they behave identically here. `aria-disabled`
|
|
77
|
+
* leaves the button focusable in principle, but a roving tabindex hands
|
|
78
|
+
* `tabindex="0"` to the selected tab only — so a tab that can never be
|
|
79
|
+
* selected is never in the tab sequence either, and the arrow keys skip it
|
|
80
|
+
* like a native one. It is the escape hatch for consumers who cannot set the
|
|
81
|
+
* native attribute (a framework binding, a non-`button` element), not a
|
|
82
|
+
* "reachable but inert" variant.
|
|
83
|
+
*/
|
|
84
|
+
private _isDisabled;
|
|
85
|
+
private _isSelectable;
|
|
86
|
+
/** Index of the currently selected tab, read back from the DOM. */
|
|
87
|
+
private _activeIndex;
|
|
88
|
+
/** Resolve the initial selection from `value`, falling back to the first enabled tab. */
|
|
89
|
+
private _resolveActiveIndex;
|
|
90
|
+
/** Index of the next enabled tab from `from`, moving by `dir`, wrapping. */
|
|
91
|
+
private _nextEnabled;
|
|
92
|
+
/** Index of the first (`dir` 1) or last (`dir` -1) enabled tab. */
|
|
93
|
+
private _edgeEnabled;
|
|
67
94
|
private _updateIndicator;
|
|
68
95
|
private _onClick;
|
|
69
96
|
private _onKeyDown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tabs/tabs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAG7D,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;AAC9C,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,CAAC;AAExD,gEAAgE;AAChE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;gBACjB,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;CAK/C;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,eAAe,CAAC;CACzB;AAED
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tabs/tabs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAG7D,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;AAC9C,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,CAAC;AAExD,gEAAgE;AAChE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;gBACjB,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;CAK/C;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,eAAe,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,qBAAa,IAAK,SAAQ,YAAY;IAC3B,gBAAgB;IAIzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,eAAe,CAA+B;IAEtD,sBAAsB;IAEtB,OAAO,EAAE,WAAW,CAAU;IAE9B,yCAAyC;IAEzC,KAAK,SAAO;IAEZ,4CAA4C;IAE5C,SAAS,UAAS;IAElB,uBAAuB;IAEvB,WAAW,EAAE,eAAe,CAAgB;IAInC,iBAAiB;IAUjB,oBAAoB;IAM7B,mFAAmF;IACnF,OAAO,CAAC,SAAS;IAOR,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAuB9C,OAAO,CAAC,MAAM;IAgEd,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,UAAU;IAmClB;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,aAAa;IASrB,mEAAmE;IACnE,OAAO,CAAC,YAAY;IAKpB,yFAAyF;IACzF,OAAO,CAAC,mBAAmB;IAS3B,4EAA4E;IAC5E,OAAO,CAAC,YAAY;IASpB,mEAAmE;IACnE,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,QAAQ,CAUd;IAEF,OAAO,CAAC,UAAU,CAqChB;CACH;AA8BD,MAAM,WAAW,IAAI;IACnB,gBAAgB,CAAC,CAAC,SAAS,MAAM,YAAY,EAC3C,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,EACnD,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;IACR,gBAAgB,CACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,kCAAkC,EAC5C,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;IACR,mBAAmB,CAAC,CAAC,SAAS,MAAM,YAAY,EAC9C,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,EACnD,OAAO,CAAC,EAAE,OAAO,GAAG,oBAAoB,GACvC,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,kCAAkC,EAC5C,OAAO,CAAC,EAAE,OAAO,GAAG,oBAAoB,GACvC,IAAI,CAAC;CACT"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{uniqueId as e}from"../../registry.js";import{i as t,t as n}from"../../chunks/decorate.js";import{LuxenElement as r}from"../../shared/luxen-element.js";var i=class extends Event{constructor(e,t){super(`change`,{bubbles:!0,composed:!1,cancelable:!1}),this.index=e,this.name=t}},a=class extends r{constructor(...t){super(...t),this._initialized=!1,this._setupTimer=0,this._instanceId=e(`tabs`),this._tablistEl=null,this._tabs=[],this._panels=[],this._resizeObserver=null,this.variant=`line`,this.value=`0`,this.fullWidth=!1,this.orientation=`horizontal`,this._onClick=e=>{let t=e.target.closest(`[role="tab"]`);if(!t)return;let n=this._tabs.indexOf(t);n
|
|
1
|
+
import{uniqueId as e}from"../../registry.js";import{i as t,t as n}from"../../chunks/decorate.js";import{LuxenElement as r}from"../../shared/luxen-element.js";var i=class extends Event{constructor(e,t){super(`change`,{bubbles:!0,composed:!1,cancelable:!1}),this.index=e,this.name=t}},a=class extends r{constructor(...t){super(...t),this._initialized=!1,this._setupTimer=0,this._instanceId=e(`tabs`),this._tablistEl=null,this._tabs=[],this._panels=[],this._resizeObserver=null,this.variant=`line`,this.value=`0`,this.fullWidth=!1,this.orientation=`horizontal`,this._onClick=e=>{let t=e.target.closest(`[role="tab"]`);if(!t)return;let n=this._tabs.indexOf(t);n<0||this._isDisabled(t)||(this._selectTab(n),t.focus())},this._onKeyDown=e=>{let t=e.target;if(t.getAttribute(`role`)!==`tab`)return;let n=this.orientation===`horizontal`,r=n?`ArrowLeft`:`ArrowUp`,i=n?`ArrowRight`:`ArrowDown`,a=this._tabs.indexOf(t);if(a<0)return;let o;switch(e.key){case i:o=this._nextEnabled(a,1);break;case r:o=this._nextEnabled(a,-1);break;case`Home`:o=this._edgeEnabled(1);break;case`End`:o=this._edgeEnabled(-1);break;default:return}e.preventDefault(),this._isSelectable(o)&&(this._selectTab(o),this._tabs[o].focus())}}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this._trySetup()||(this._setupTimer=window.setTimeout(()=>this._trySetup(),0))}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._setupTimer),this._teardown()}_trySetup(){return this._initialized||!this.isConnected?!0:this.children.length<2?!1:(this._setup(),!0)}updated(e){if(e.has(`value`)&&this._tabs.length){let e=Number(this.value);if(this._isSelectable(e))this._selectTab(e,!1);else{let e=this._activeIndex();queueMicrotask(()=>{this.value=String(e)})}}e.has(`orientation`)&&this._tablistEl&&this._tablistEl.setAttribute(`aria-orientation`,this.orientation)}_setup(){let e=Array.from(this.children);if(e.length<2)return;this._initialized=!0,this._tablistEl=e[0],this._tablistEl.setAttribute(`role`,`tablist`),this._tablistEl.setAttribute(`aria-orientation`,this.orientation),this._tabs=Array.from(this._tablistEl.querySelectorAll(`button`)),this._panels=e.slice(1);let t=this._resolveActiveIndex();for(let e=0;e<this._tabs.length;e++){let n=this._tabs[e],r=this._panels[e],i=n.getAttribute(`name`)??String(e),a=`${this._instanceId}-tab-${i}`,o=`${this._instanceId}-panel-${i}`;n.setAttribute(`role`,`tab`),n.setAttribute(`id`,a),n.setAttribute(`aria-selected`,String(e===t)),n.setAttribute(`tabindex`,e===t?`0`:`-1`),r&&(n.setAttribute(`aria-controls`,o),r.setAttribute(`role`,`tabpanel`),r.setAttribute(`id`,o),r.setAttribute(`aria-labelledby`,a),r.setAttribute(`tabindex`,`0`),e===t?r.hidden=!1:r.hidden=!0)}Number(this.value)!==t&&(this.value=String(t)),this._tablistEl.addEventListener(`click`,this._onClick),this._tablistEl.addEventListener(`keydown`,this._onKeyDown),this._updateIndicator(),this._resizeObserver=new ResizeObserver(()=>this._updateIndicator()),this._resizeObserver.observe(this._tablistEl)}_teardown(){this._tablistEl?.removeEventListener(`click`,this._onClick),this._tablistEl?.removeEventListener(`keydown`,this._onKeyDown),this._resizeObserver?.disconnect(),this._resizeObserver=null,this._initialized=!1}_selectTab(e,t=!0){if(!this._isSelectable(e))return;let n=this._tabs[e].getAttribute(`aria-selected`)!==`true`;for(let t=0;t<this._tabs.length;t++){let n=this._tabs[t],r=this._panels[t],i=t===e;n.setAttribute(`aria-selected`,String(i)),n.setAttribute(`tabindex`,i?`0`:`-1`),r&&(r.hidden=!i)}if(this.value=String(e),this._updateIndicator(),t&&n){let t=this._tabs[e]?.getAttribute(`name`)??null;this.dispatchEvent(new i(e,t))}}_isDisabled(e){return e.disabled||e.getAttribute(`aria-disabled`)===`true`}_isSelectable(e){return Number.isInteger(e)&&e>=0&&e<this._tabs.length&&!this._isDisabled(this._tabs[e])}_activeIndex(){let e=this._tabs.findIndex(e=>e.getAttribute(`aria-selected`)===`true`);return e>=0?e:0}_resolveActiveIndex(){let e=Number(this.value)||0;if(this._isSelectable(e))return e;let t=this._tabs.findIndex(e=>!this._isDisabled(e));return t>=0?t:0}_nextEnabled(e,t){let n=this._tabs.length;for(let r=1;r<=n;r++){let i=(e+t*r+n*r)%n;if(!this._isDisabled(this._tabs[i]))return i}return e}_edgeEnabled(e){let t=this._tabs.length,n=e===1?0:t-1;for(let r=n;r>=0&&r<t;r+=e)if(!this._isDisabled(this._tabs[r]))return r;return n}_updateIndicator(){if(!this._tablistEl)return;let e=this._tabs[this._activeIndex()];e&&(this.orientation===`vertical`?(this._tablistEl.style.setProperty(`--_indicator-top`,`${e.offsetTop}px`),this._tablistEl.style.setProperty(`--_indicator-height`,`${e.offsetHeight}px`)):(this._tablistEl.style.setProperty(`--_indicator-left`,`${e.offsetLeft}px`),this._tablistEl.style.setProperty(`--_indicator-width`,`${e.offsetWidth}px`)))}};n([t({reflect:!0})],a.prototype,`variant`,void 0),n([t({reflect:!0})],a.prototype,`value`,void 0),n([t({type:Boolean,reflect:!0,attribute:`full-width`})],a.prototype,`fullWidth`,void 0),n([t({reflect:!0})],a.prototype,`orientation`,void 0);export{a as Tabs,i as TabsChangeEvent};
|
|
2
2
|
//# sourceMappingURL=tabs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.js","names":[],"sources":["../../../src/html/elements/tabs/tabs.ts"],"sourcesContent":["import { property } from 'lit/decorators.js';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { uniqueId } from '../../registry.js';\n\nexport type TabsVariant = 'enclosed' | 'line';\nexport type TabsOrientation = 'horizontal' | 'vertical';\n\n/** Fired when the active tab changes. Bubbles; not composed. */\nexport class TabsChangeEvent extends Event {\n readonly index: number;\n readonly name: string | null;\n constructor(index: number, name: string | null) {\n super('change', { bubbles: true, composed: false, cancelable: false });\n this.index = index;\n this.name = name;\n }\n}\n\ninterface TabsEventMap {\n change: TabsChangeEvent;\n}\n\n/**\n * @summary A tabs component that progressively enhances light DOM markup\n * with ARIA roles, keyboard navigation, and animated indicators.\n *\n * @example\n * Mark the initially-active tab with `aria-selected=\"true\"` so it is styled as\n * selected before the script upgrades the element (progressive enhancement).\n * ```html\n * <l-tabs variant=\"enclosed\">\n * <div>\n * <button aria-selected=\"true\">Tab 1</button>\n * <button>Tab 2</button>\n * </div>\n * <div>Content 1</div>\n * <div>Content 2</div>\n * </l-tabs>\n * ```\n *\n * @event change - Fired when the active tab changes. Bubbles. Properties: `index: number`, `name: string | null`.\n *\n * @cssproperty [--indicator-color=var(--l-color-text-primary)] - `line` variant: color of the active underline that slides under the selected tab.\n * @cssproperty [--indicator-thickness=2px] - `line` variant: thickness of the active underline.\n * @cssproperty [--track-color=var(--l-color-border)] - `line` variant: color of the static bottom border the tabs sit on.\n * @cssproperty [--track-thickness=1px] - `line` variant: thickness of the static bottom border.\n * @cssproperty [--hover-color=var(--l-color-bg-state-hover)] - `line` variant: fill of the pill that appears behind a hovered tab label.\n * @cssproperty [--hover-inset=4px] - `line` variant: block-axis gap between the hover pill and the tab edges, keeping it detached from the bottom border.\n *\n * @customElement l-tabs\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- typed addEventListener overloads merged below; no uninitialized properties.\nexport class Tabs extends LuxenElement {\n override createRenderRoot() {\n return this;\n }\n\n private _initialized = false;\n private _setupTimer = 0;\n private _instanceId = uniqueId('tabs');\n private _tablistEl: HTMLElement | null = null;\n private _tabs: HTMLButtonElement[] = [];\n private _panels: HTMLElement[] = [];\n private _resizeObserver: ResizeObserver | null = null;\n\n /** Visual variant. */\n @property({ reflect: true })\n variant: TabsVariant = 'line';\n\n /** Index of the active tab (0-based). */\n @property({ reflect: true })\n value = '0';\n\n /** Stretch tabs to fill container width. */\n @property({ type: Boolean, reflect: true, attribute: 'full-width' })\n fullWidth = false;\n\n /** Tab orientation. */\n @property({ reflect: true })\n orientation: TabsOrientation = 'horizontal';\n\n // --- Lifecycle ---\n\n override connectedCallback() {\n super.connectedCallback();\n // Children may not be parsed yet when the element upgrades mid-parse: try\n // synchronously, then retry once on a macrotask. setTimeout, not rAF — rAF\n // is suspended in hidden documents, so the element would stay inert there.\n if (!this._trySetup()) {\n this._setupTimer = window.setTimeout(() => this._trySetup(), 0);\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n clearTimeout(this._setupTimer);\n this._teardown();\n }\n\n /** @returns true when setup ran or was already done; false to schedule a retry. */\n private _trySetup(): boolean {\n if (this._initialized || !this.isConnected) return true;\n if (this.children.length < 2) return false;\n this._setup();\n return true;\n }\n\n override updated(changed: Map<string, unknown>) {\n if (changed.has('value') && this._tabs.length) {\n this._selectTab(Number(this.value), false);\n }\n if (changed.has('orientation') && this._tablistEl) {\n this._tablistEl.setAttribute('aria-orientation', this.orientation);\n }\n }\n\n // --- Setup / Teardown ---\n\n private _setup() {\n const children = Array.from(this.children) as HTMLElement[];\n if (children.length < 2) return;\n this._initialized = true;\n\n // First child is the tablist container\n this._tablistEl = children[0];\n this._tablistEl.setAttribute('role', 'tablist');\n this._tablistEl.setAttribute('aria-orientation', this.orientation);\n\n // Buttons inside tablist become tabs\n this._tabs = Array.from(this._tablistEl.querySelectorAll('button'));\n\n // Remaining children become panels\n this._panels = children.slice(1);\n\n const activeIndex = Number(this.value) || 0;\n\n // Enhance tabs\n for (let i = 0; i < this._tabs.length; i++) {\n const tab = this._tabs[i];\n const panel = this._panels[i];\n const name = tab.getAttribute('name') ?? String(i);\n const tabId = `${this._instanceId}-tab-${name}`;\n const panelId = `${this._instanceId}-panel-${name}`;\n\n tab.setAttribute('role', 'tab');\n tab.setAttribute('id', tabId);\n tab.setAttribute('aria-selected', String(i === activeIndex));\n tab.setAttribute('tabindex', i === activeIndex ? '0' : '-1');\n\n if (panel) {\n tab.setAttribute('aria-controls', panelId);\n panel.setAttribute('role', 'tabpanel');\n panel.setAttribute('id', panelId);\n panel.setAttribute('aria-labelledby', tabId);\n panel.setAttribute('tabindex', '0');\n if (i !== activeIndex) {\n panel.hidden = true;\n } else {\n panel.hidden = false;\n }\n }\n }\n\n // Attach listeners\n this._tablistEl.addEventListener('click', this._onClick);\n this._tablistEl.addEventListener('keydown', this._onKeyDown);\n\n // Initial indicator position. Synchronous first (offset reads force\n // layout), then a ResizeObserver keeps it correct across hidden→visible\n // transitions and size changes — rAF is suspended in hidden documents,\n // so it must not be the only scheduling mechanism here.\n this._updateIndicator();\n this._resizeObserver = new ResizeObserver(() => this._updateIndicator());\n this._resizeObserver.observe(this._tablistEl);\n }\n\n private _teardown() {\n this._tablistEl?.removeEventListener('click', this._onClick);\n this._tablistEl?.removeEventListener('keydown', this._onKeyDown);\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n this._initialized = false;\n }\n\n // --- Tab selection ---\n\n private _selectTab(index: number, emitEvent = true) {\n if (index < 0 || index >= this._tabs.length) return;\n\n for (let i = 0; i < this._tabs.length; i++) {\n const tab = this._tabs[i];\n const panel = this._panels[i];\n const isActive = i === index;\n\n tab.setAttribute('aria-selected', String(isActive));\n tab.setAttribute('tabindex', isActive ? '0' : '-1');\n\n if (panel) {\n panel.hidden = !isActive;\n }\n }\n\n this.value = String(index);\n this._updateIndicator();\n\n if (emitEvent) {\n const name = this._tabs[index]?.getAttribute('name') ?? null;\n this.dispatchEvent(new TabsChangeEvent(index, name));\n }\n }\n\n // --- Indicator ---\n\n private _updateIndicator() {\n if (!this._tablistEl) return;\n const activeTab = this._tabs[Number(this.value)];\n if (!activeTab) return;\n\n const isVertical = this.orientation === 'vertical';\n\n if (isVertical) {\n this._tablistEl.style.setProperty('--_indicator-top', `${activeTab.offsetTop}px`);\n this._tablistEl.style.setProperty('--_indicator-height', `${activeTab.offsetHeight}px`);\n } else {\n this._tablistEl.style.setProperty('--_indicator-left', `${activeTab.offsetLeft}px`);\n this._tablistEl.style.setProperty('--_indicator-width', `${activeTab.offsetWidth}px`);\n }\n }\n\n // --- Event handlers ---\n\n private _onClick = (e: Event) => {\n const tab = (e.target as HTMLElement).closest<HTMLButtonElement>('[role=\"tab\"]');\n if (!tab) return;\n const index = this._tabs.indexOf(tab);\n if (index >= 0) {\n this._selectTab(index);\n tab.focus();\n }\n };\n\n private _onKeyDown = (e: KeyboardEvent) => {\n const target = e.target as HTMLElement;\n if (target.getAttribute('role') !== 'tab') return;\n\n const isHorizontal = this.orientation === 'horizontal';\n const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';\n const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';\n\n let index = this._tabs.indexOf(target as HTMLButtonElement);\n\n switch (e.key) {\n case nextKey:\n e.preventDefault();\n index = (index + 1) % this._tabs.length;\n this._selectTab(index);\n this._tabs[index].focus();\n break;\n case prevKey:\n e.preventDefault();\n index = (index - 1 + this._tabs.length) % this._tabs.length;\n this._selectTab(index);\n this._tabs[index].focus();\n break;\n case 'Home':\n e.preventDefault();\n this._selectTab(0);\n this._tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n this._selectTab(this._tabs.length - 1);\n this._tabs[this._tabs.length - 1].focus();\n break;\n }\n };\n}\n\n// Type `addEventListener('change', …)` on this element so the listener receives\n// a `TabsChangeEvent` (with `.index`/`.name`) instead of a bare `Event`, with no\n// cast. This is the canonical reference for the pattern used across the library.\n//\n// Why a same-named interface and not a global augmentation: `change` already\n// exists in lib.dom's `GlobalEventHandlersEventMap` (typed `Event`), and TS\n// forbids re-declaring an existing key with a different type — so colliding\n// names (`change`, `select`, `input`, `toggle`) can't be typed globally. Merging\n// a same-named `interface Tabs` into the `class Tabs` adds these overloads to\n// the element's own type only. (Non-colliding names like `show`/`story-change`\n// ARE augmented globally in their event-class module — no per-element block.)\n//\n// In `(type: K, listener: (ev: TabsEventMap[K]) => void)`, K is pinned to the\n// string literal you pass, and `TabsEventMap[K]` indexes the event type from it\n// (`'change'` → `TabsChangeEvent`). `this: Tabs` is a fake parameter that types\n// `this` inside a non-arrow listener; it emits no runtime code.\n//\n// Three load-bearing details:\n// 1. The `(type: string, …)` fallback overloads are REQUIRED: declaring our\n// own `addEventListener` hides ALL inherited `HTMLElement` overloads, so\n// without them `addEventListener('click', …)` would stop compiling. The\n// typed overload comes first so `'change'` resolves before the fallback.\n// 2. This interface sits AFTER the class: the CEM analyzer dedups on the first\n// declaration of the name, so an interface before the class would shadow it\n// and the manifest would extract nothing for this element.\n// 3. The merge is flagged by `no-unsafe-declaration-merging` (disabled on the\n// class above) — safe here because we merge only method overloads, never\n// uninitialized properties.\nexport interface Tabs {\n addEventListener<K extends keyof TabsEventMap>(\n type: K,\n listener: (this: Tabs, ev: TabsEventMap[K]) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | AddEventListenerOptions,\n ): void;\n removeEventListener<K extends keyof TabsEventMap>(\n type: K,\n listener: (this: Tabs, ev: TabsEventMap[K]) => void,\n options?: boolean | EventListenerOptions,\n ): void;\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | EventListenerOptions,\n ): void;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tabs.js","names":[],"sources":["../../../src/html/elements/tabs/tabs.ts"],"sourcesContent":["import { property } from 'lit/decorators.js';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { uniqueId } from '../../registry.js';\n\nexport type TabsVariant = 'enclosed' | 'line';\nexport type TabsOrientation = 'horizontal' | 'vertical';\n\n/** Fired when the active tab changes. Bubbles; not composed. */\nexport class TabsChangeEvent extends Event {\n readonly index: number;\n readonly name: string | null;\n constructor(index: number, name: string | null) {\n super('change', { bubbles: true, composed: false, cancelable: false });\n this.index = index;\n this.name = name;\n }\n}\n\ninterface TabsEventMap {\n change: TabsChangeEvent;\n}\n\n/**\n * @summary A tabs component that progressively enhances light DOM markup\n * with ARIA roles, keyboard navigation, and animated indicators.\n *\n * @example\n * Mark the initially-active tab with `aria-selected=\"true\"` so it is styled as\n * selected before the script upgrades the element (progressive enhancement).\n * ```html\n * <l-tabs variant=\"enclosed\">\n * <div>\n * <button aria-selected=\"true\">Tab 1</button>\n * <button>Tab 2</button>\n * </div>\n * <div>Content 1</div>\n * <div>Content 2</div>\n * </l-tabs>\n * ```\n *\n * @example\n * A tab button marked `disabled` (or `aria-disabled=\"true\"`) is skipped by\n * arrow keys, `Home` and `End`, and cannot be selected by a click or by\n * `value` — it still owns its panel, so the tab/panel order is unchanged.\n * ```html\n * <button disabled>Billing</button>\n * ```\n *\n * @event change - Fired when the active tab changes. Bubbles. Not cancelable. Properties: `index: number`, `name: string | null`.\n *\n * @cssproperty [--indicator-color=var(--l-color-text-primary)] - `line` variant: color of the active underline that slides under the selected tab.\n * @cssproperty [--indicator-thickness=2px] - `line` variant: thickness of the active underline.\n * @cssproperty [--track-color=var(--l-color-border)] - `line` variant: color of the static bottom border the tabs sit on.\n * @cssproperty [--track-thickness=1px] - `line` variant: thickness of the static bottom border.\n * @cssproperty [--hover-color=var(--l-color-bg-state-hover)] - `line` variant: fill of the pill that appears behind a hovered tab label.\n * @cssproperty [--hover-inset=4px] - `line` variant: block-axis gap between the hover pill and the tab edges, keeping it detached from the bottom border.\n *\n * @customElement l-tabs\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- typed addEventListener overloads merged below; no uninitialized properties.\nexport class Tabs extends LuxenElement {\n override createRenderRoot() {\n return this;\n }\n\n private _initialized = false;\n private _setupTimer = 0;\n private _instanceId = uniqueId('tabs');\n private _tablistEl: HTMLElement | null = null;\n private _tabs: HTMLButtonElement[] = [];\n private _panels: HTMLElement[] = [];\n private _resizeObserver: ResizeObserver | null = null;\n\n /** Visual variant. */\n @property({ reflect: true })\n variant: TabsVariant = 'line';\n\n /** Index of the active tab (0-based). */\n @property({ reflect: true })\n value = '0';\n\n /** Stretch tabs to fill container width. */\n @property({ type: Boolean, reflect: true, attribute: 'full-width' })\n fullWidth = false;\n\n /** Tab orientation. */\n @property({ reflect: true })\n orientation: TabsOrientation = 'horizontal';\n\n // --- Lifecycle ---\n\n override connectedCallback() {\n super.connectedCallback();\n // Children may not be parsed yet when the element upgrades mid-parse: try\n // synchronously, then retry once on a macrotask. setTimeout, not rAF — rAF\n // is suspended in hidden documents, so the element would stay inert there.\n if (!this._trySetup()) {\n this._setupTimer = window.setTimeout(() => this._trySetup(), 0);\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n clearTimeout(this._setupTimer);\n this._teardown();\n }\n\n /** @returns true when setup ran or was already done; false to schedule a retry. */\n private _trySetup(): boolean {\n if (this._initialized || !this.isConnected) return true;\n if (this.children.length < 2) return false;\n this._setup();\n return true;\n }\n\n override updated(changed: Map<string, unknown>) {\n if (changed.has('value') && this._tabs.length) {\n const requested = Number(this.value);\n if (this._isSelectable(requested)) {\n this._selectTab(requested, false);\n } else {\n // The target is disabled or out of range, so the selection stays put —\n // and `value` must not keep claiming a tab that was never selected.\n // Reverting here would schedule an update from within an update, so\n // defer it (same reason as a vetoed dialog `show`).\n const current = this._activeIndex();\n queueMicrotask(() => {\n this.value = String(current);\n });\n }\n }\n if (changed.has('orientation') && this._tablistEl) {\n this._tablistEl.setAttribute('aria-orientation', this.orientation);\n }\n }\n\n // --- Setup / Teardown ---\n\n private _setup() {\n const children = Array.from(this.children) as HTMLElement[];\n if (children.length < 2) return;\n this._initialized = true;\n\n // First child is the tablist container\n this._tablistEl = children[0];\n this._tablistEl.setAttribute('role', 'tablist');\n this._tablistEl.setAttribute('aria-orientation', this.orientation);\n\n // Buttons inside tablist become tabs\n this._tabs = Array.from(this._tablistEl.querySelectorAll('button'));\n\n // Remaining children become panels. Disabled buttons stay in `_tabs` so the\n // tab↔panel pairing keeps following DOM order — they are filtered out at\n // navigation time, not here.\n this._panels = children.slice(1);\n\n const activeIndex = this._resolveActiveIndex();\n\n // Enhance tabs\n for (let i = 0; i < this._tabs.length; i++) {\n const tab = this._tabs[i];\n const panel = this._panels[i];\n const name = tab.getAttribute('name') ?? String(i);\n const tabId = `${this._instanceId}-tab-${name}`;\n const panelId = `${this._instanceId}-panel-${name}`;\n\n tab.setAttribute('role', 'tab');\n tab.setAttribute('id', tabId);\n tab.setAttribute('aria-selected', String(i === activeIndex));\n tab.setAttribute('tabindex', i === activeIndex ? '0' : '-1');\n\n if (panel) {\n tab.setAttribute('aria-controls', panelId);\n panel.setAttribute('role', 'tabpanel');\n panel.setAttribute('id', panelId);\n panel.setAttribute('aria-labelledby', tabId);\n panel.setAttribute('tabindex', '0');\n if (i !== activeIndex) {\n panel.hidden = true;\n } else {\n panel.hidden = false;\n }\n }\n }\n\n // `value` may have pointed at a disabled tab: canonicalize it to the tab\n // that is actually selected.\n if (Number(this.value) !== activeIndex) this.value = String(activeIndex);\n\n // Attach listeners\n this._tablistEl.addEventListener('click', this._onClick);\n this._tablistEl.addEventListener('keydown', this._onKeyDown);\n\n // Initial indicator position. Synchronous first (offset reads force\n // layout), then a ResizeObserver keeps it correct across hidden→visible\n // transitions and size changes — rAF is suspended in hidden documents,\n // so it must not be the only scheduling mechanism here.\n this._updateIndicator();\n this._resizeObserver = new ResizeObserver(() => this._updateIndicator());\n this._resizeObserver.observe(this._tablistEl);\n }\n\n private _teardown() {\n this._tablistEl?.removeEventListener('click', this._onClick);\n this._tablistEl?.removeEventListener('keydown', this._onKeyDown);\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n this._initialized = false;\n }\n\n // --- Tab selection ---\n\n private _selectTab(index: number, emitEvent = true) {\n // A disabled tab is never selectable: it cannot take focus, so selecting it\n // would move the roving tabindex entry point onto an unfocusable button and\n // drop the whole tablist out of the tab sequence (WCAG 2.1.1).\n if (!this._isSelectable(index)) return;\n\n // Whether this is an actual change — re-selecting the current tab must not\n // re-fire `change` (the event is documented as firing when the active tab\n // changes, and this matches native `change` on radios / `<select>`).\n const changed = this._tabs[index].getAttribute('aria-selected') !== 'true';\n\n for (let i = 0; i < this._tabs.length; i++) {\n const tab = this._tabs[i];\n const panel = this._panels[i];\n const isActive = i === index;\n\n tab.setAttribute('aria-selected', String(isActive));\n tab.setAttribute('tabindex', isActive ? '0' : '-1');\n\n if (panel) {\n panel.hidden = !isActive;\n }\n }\n\n this.value = String(index);\n this._updateIndicator();\n\n if (emitEvent && changed) {\n const name = this._tabs[index]?.getAttribute('name') ?? null;\n this.dispatchEvent(new TabsChangeEvent(index, name));\n }\n }\n\n // --- Disabled tabs ---\n\n /**\n * Both spellings count, and they behave identically here. `aria-disabled`\n * leaves the button focusable in principle, but a roving tabindex hands\n * `tabindex=\"0\"` to the selected tab only — so a tab that can never be\n * selected is never in the tab sequence either, and the arrow keys skip it\n * like a native one. It is the escape hatch for consumers who cannot set the\n * native attribute (a framework binding, a non-`button` element), not a\n * \"reachable but inert\" variant.\n */\n private _isDisabled(tab: HTMLButtonElement): boolean {\n return tab.disabled || tab.getAttribute('aria-disabled') === 'true';\n }\n\n private _isSelectable(index: number): boolean {\n return (\n Number.isInteger(index) &&\n index >= 0 &&\n index < this._tabs.length &&\n !this._isDisabled(this._tabs[index])\n );\n }\n\n /** Index of the currently selected tab, read back from the DOM. */\n private _activeIndex(): number {\n const i = this._tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true');\n return i >= 0 ? i : 0;\n }\n\n /** Resolve the initial selection from `value`, falling back to the first enabled tab. */\n private _resolveActiveIndex(): number {\n const requested = Number(this.value) || 0;\n if (this._isSelectable(requested)) return requested;\n const firstEnabled = this._tabs.findIndex((t) => !this._isDisabled(t));\n // Every tab disabled: nothing is focusable anyway, so index 0 is as good as\n // any and the tablist is simply unreachable — which is what was asked for.\n return firstEnabled >= 0 ? firstEnabled : 0;\n }\n\n /** Index of the next enabled tab from `from`, moving by `dir`, wrapping. */\n private _nextEnabled(from: number, dir: 1 | -1): number {\n const len = this._tabs.length;\n for (let step = 1; step <= len; step++) {\n const i = (from + dir * step + len * step) % len;\n if (!this._isDisabled(this._tabs[i])) return i;\n }\n return from;\n }\n\n /** Index of the first (`dir` 1) or last (`dir` -1) enabled tab. */\n private _edgeEnabled(dir: 1 | -1): number {\n const len = this._tabs.length;\n const start = dir === 1 ? 0 : len - 1;\n for (let i = start; i >= 0 && i < len; i += dir) {\n if (!this._isDisabled(this._tabs[i])) return i;\n }\n return start;\n }\n\n // --- Indicator ---\n\n private _updateIndicator() {\n if (!this._tablistEl) return;\n const activeTab = this._tabs[this._activeIndex()];\n if (!activeTab) return;\n\n const isVertical = this.orientation === 'vertical';\n\n if (isVertical) {\n this._tablistEl.style.setProperty('--_indicator-top', `${activeTab.offsetTop}px`);\n this._tablistEl.style.setProperty('--_indicator-height', `${activeTab.offsetHeight}px`);\n } else {\n this._tablistEl.style.setProperty('--_indicator-left', `${activeTab.offsetLeft}px`);\n this._tablistEl.style.setProperty('--_indicator-width', `${activeTab.offsetWidth}px`);\n }\n }\n\n // --- Event handlers ---\n\n private _onClick = (e: Event) => {\n const tab = (e.target as HTMLElement).closest<HTMLButtonElement>('[role=\"tab\"]');\n if (!tab) return;\n const index = this._tabs.indexOf(tab);\n // A native `disabled` button dispatches no click at all, but an\n // `aria-disabled` one does — `_selectTab` refuses it, and focus must not\n // follow either.\n if (index < 0 || this._isDisabled(tab)) return;\n this._selectTab(index);\n tab.focus();\n };\n\n private _onKeyDown = (e: KeyboardEvent) => {\n const target = e.target as HTMLElement;\n if (target.getAttribute('role') !== 'tab') return;\n\n const isHorizontal = this.orientation === 'horizontal';\n const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';\n const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';\n\n const current = this._tabs.indexOf(target as HTMLButtonElement);\n if (current < 0) return;\n\n // Every move lands on an enabled tab: selection follows focus here\n // (automatic activation), so a disabled target would split the two apart.\n let index: number;\n switch (e.key) {\n case nextKey:\n index = this._nextEnabled(current, 1);\n break;\n case prevKey:\n index = this._nextEnabled(current, -1);\n break;\n case 'Home':\n index = this._edgeEnabled(1);\n break;\n case 'End':\n index = this._edgeEnabled(-1);\n break;\n default:\n return;\n }\n\n e.preventDefault();\n // Only reachable when every tab is disabled — then there is nowhere to go\n // and focus must not be dragged onto a disabled tab.\n if (!this._isSelectable(index)) return;\n this._selectTab(index);\n this._tabs[index].focus();\n };\n}\n\n// Type `addEventListener('change', …)` on this element so the listener receives\n// a `TabsChangeEvent` (with `.index`/`.name`) instead of a bare `Event`, with no\n// cast. This is the canonical reference for the pattern used across the library.\n//\n// Why a same-named interface and not a global augmentation: `change` already\n// exists in lib.dom's `GlobalEventHandlersEventMap` (typed `Event`), and TS\n// forbids re-declaring an existing key with a different type — so colliding\n// names (`change`, `select`, `input`, `toggle`) can't be typed globally. Merging\n// a same-named `interface Tabs` into the `class Tabs` adds these overloads to\n// the element's own type only. (Non-colliding names like `show`/`story-change`\n// ARE augmented globally in their event-class module — no per-element block.)\n//\n// In `(type: K, listener: (ev: TabsEventMap[K]) => void)`, K is pinned to the\n// string literal you pass, and `TabsEventMap[K]` indexes the event type from it\n// (`'change'` → `TabsChangeEvent`). `this: Tabs` is a fake parameter that types\n// `this` inside a non-arrow listener; it emits no runtime code.\n//\n// Three load-bearing details:\n// 1. The `(type: string, …)` fallback overloads are REQUIRED: declaring our\n// own `addEventListener` hides ALL inherited `HTMLElement` overloads, so\n// without them `addEventListener('click', …)` would stop compiling. The\n// typed overload comes first so `'change'` resolves before the fallback.\n// 2. This interface sits AFTER the class: the CEM analyzer dedups on the first\n// declaration of the name, so an interface before the class would shadow it\n// and the manifest would extract nothing for this element.\n// 3. The merge is flagged by `no-unsafe-declaration-merging` (disabled on the\n// class above) — safe here because we merge only method overloads, never\n// uninitialized properties.\nexport interface Tabs {\n addEventListener<K extends keyof TabsEventMap>(\n type: K,\n listener: (this: Tabs, ev: TabsEventMap[K]) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | AddEventListenerOptions,\n ): void;\n removeEventListener<K extends keyof TabsEventMap>(\n type: K,\n listener: (this: Tabs, ev: TabsEventMap[K]) => void,\n options?: boolean | EventListenerOptions,\n ): void;\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | EventListenerOptions,\n ): void;\n}\n"],"mappings":"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"}
|
package/cdn/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from './events/index.js';
|
|
|
4
4
|
export { define } from './define.js';
|
|
5
5
|
export { setPrefix, getPrefix, tagName, cls } from './registry.js';
|
|
6
6
|
export type { ElementBaseName } from './registry.js';
|
|
7
|
+
export { colorScheme } from './color-scheme.js';
|
|
8
|
+
export type { ColorScheme, ColorSchemeConfig, ColorSchemeListener } from './color-scheme.js';
|
|
7
9
|
export { defineConfig } from './config.js';
|
|
8
10
|
export type { LuxenConfig, LuxenEmitTypesConfig } from './config.js';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/cdn/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/html/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AACvF,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/html/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AACvF,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
|
package/cdn/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./config.js";import"./registry.js";import"./define.js";import"./shared/luxen-element.js";import"./events/show.js";import"./events/after-show.js";import"./events/hide.js";import"./events/after-hide.js";import"./shared/luxen-form-associated-element.js";
|
|
1
|
+
import"./color-scheme.js";import"./config.js";import"./registry.js";import"./define.js";import"./shared/luxen-element.js";import"./events/show.js";import"./events/after-show.js";import"./events/hide.js";import"./events/after-hide.js";import"./shared/luxen-form-associated-element.js";
|
package/cdn/registry.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Every component base name the library ships. */
|
|
2
|
-
export type ElementBaseName = 'alert' | 'alert-dialog' | 'avatar' | 'badge' | 'button-group' | 'carousel' | 'carousel-item' | 'combobox' | 'dialog' | 'divider' | 'drawer' | 'dropdown' | 'dropdown-item' | 'dropdown-label' | 'form-field' | 'icon' | 'input-group' | 'input-otp' | 'input-stepper' | 'popover' | 'prose-editor' | 'skeleton' | 'slider' | 'spinner' | 'rating' | 'segmented-control' | 'select' | 'sticky-bar' | 'stories' | 'story' | 'stories-viewer' | 'tabs' | 'tag' | 'toast' | 'toast-item' | 'tooltip' | 'tree' | 'tree-item';
|
|
2
|
+
export type ElementBaseName = 'alert' | 'alert-dialog' | 'avatar' | 'badge' | 'button-group' | 'carousel' | 'carousel-item' | 'combobox' | 'dialog' | 'divider' | 'drawer' | 'dropdown' | 'dropdown-item' | 'dropdown-label' | 'form-field' | 'icon' | 'input-group' | 'input-otp' | 'input-stepper' | 'popover' | 'prose-editor' | 'skeleton' | 'slider' | 'spinner' | 'rating' | 'segmented-control' | 'select' | 'sticky-bar' | 'stories' | 'story' | 'stories-viewer' | 'tabs' | 'tag' | 'color-scheme-icon' | 'toast' | 'toast-item' | 'tooltip' | 'tree' | 'tree-item';
|
|
3
3
|
/** Set the global prefix. Must be called before defining elements. */
|
|
4
4
|
export declare function setPrefix(prefix: {
|
|
5
5
|
element: string;
|
package/cdn/registry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/html/registry.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,cAAc,GACd,QAAQ,GACR,OAAO,GACP,cAAc,GACd,UAAU,GACV,eAAe,GACf,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,YAAY,GACZ,MAAM,GACN,aAAa,GACb,WAAW,GACX,eAAe,GACf,SAAS,GACT,cAAc,GACd,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,mBAAmB,GACnB,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,OAAO,GACP,gBAAgB,GAChB,MAAM,GACN,KAAK,GACL,OAAO,GACP,YAAY,GACZ,SAAS,GACT,MAAM,GACN,WAAW,CAAC;AAMhB,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGxE;AAED,gCAAgC;AAChC,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAE5D;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAEzD;AAED,sEAAsE;AACtE,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAID,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE9D"}
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/html/registry.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,cAAc,GACd,QAAQ,GACR,OAAO,GACP,cAAc,GACd,UAAU,GACV,eAAe,GACf,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,YAAY,GACZ,MAAM,GACN,aAAa,GACb,WAAW,GACX,eAAe,GACf,SAAS,GACT,cAAc,GACd,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,mBAAmB,GACnB,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,OAAO,GACP,gBAAgB,GAChB,MAAM,GACN,KAAK,GACL,mBAAmB,GACnB,OAAO,GACP,YAAY,GACZ,SAAS,GACT,MAAM,GACN,WAAW,CAAC;AAMhB,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGxE;AAED,gCAAgC;AAChC,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAE5D;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAEzD;AAED,sEAAsE;AACtE,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAID,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE9D"}
|
package/cdn/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","names":[],"sources":["../src/html/registry.ts"],"sourcesContent":["/** Every component base name the library ships. */\nexport type ElementBaseName =\n | 'alert'\n | 'alert-dialog'\n | 'avatar'\n | 'badge'\n | 'button-group'\n | 'carousel'\n | 'carousel-item'\n | 'combobox'\n | 'dialog'\n | 'divider'\n | 'drawer'\n | 'dropdown'\n | 'dropdown-item'\n | 'dropdown-label'\n | 'form-field'\n | 'icon'\n | 'input-group'\n | 'input-otp'\n | 'input-stepper'\n | 'popover'\n | 'prose-editor'\n | 'skeleton'\n | 'slider'\n | 'spinner'\n | 'rating'\n | 'segmented-control'\n | 'select'\n | 'sticky-bar'\n | 'stories'\n | 'story'\n | 'stories-viewer'\n | 'tabs'\n | 'tag'\n | 'toast'\n | 'toast-item'\n | 'tooltip'\n | 'tree'\n | 'tree-item';\n\nlet _elementPrefix = 'l';\nlet _cssPrefix = 'l';\nconst _registered = new Set<ElementBaseName>();\n\n/** Set the global prefix. Must be called before defining elements. */\nexport function setPrefix(prefix: { element: string; css: string }): void {\n _elementPrefix = prefix.element;\n _cssPrefix = prefix.css;\n}\n\n/** Get the current prefixes. */\nexport function getPrefix(): { element: string; css: string } {\n return { element: _elementPrefix, css: _cssPrefix };\n}\n\n/** Derive the full tag name from element prefix + base name. */\nexport function tagName(baseName: ElementBaseName): string {\n return `${_elementPrefix}-${baseName}`;\n}\n\n/** Prefix a CSS class name. `cls('toast-icon')` → `'l-toast-icon'` */\nexport function cls(name: string): string {\n return `${_cssPrefix}-${name}`;\n}\n\n/** Generate a globally unique prefixed ID. `uniqueId('toast')` → `'l:toast:1'` */\nlet _uid = 0;\nexport function uniqueId(base: string): string {\n return `${_cssPrefix}:${base}:${++_uid}`;\n}\n\n/** Check if a base name is already registered. */\nexport function isRegistered(baseName: ElementBaseName): boolean {\n return _registered.has(baseName);\n}\n\n/** Mark a base name as registered. Called internally by define(). */\nexport function markRegistered(baseName: ElementBaseName): void {\n _registered.add(baseName);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"registry.js","names":[],"sources":["../src/html/registry.ts"],"sourcesContent":["/** Every component base name the library ships. */\nexport type ElementBaseName =\n | 'alert'\n | 'alert-dialog'\n | 'avatar'\n | 'badge'\n | 'button-group'\n | 'carousel'\n | 'carousel-item'\n | 'combobox'\n | 'dialog'\n | 'divider'\n | 'drawer'\n | 'dropdown'\n | 'dropdown-item'\n | 'dropdown-label'\n | 'form-field'\n | 'icon'\n | 'input-group'\n | 'input-otp'\n | 'input-stepper'\n | 'popover'\n | 'prose-editor'\n | 'skeleton'\n | 'slider'\n | 'spinner'\n | 'rating'\n | 'segmented-control'\n | 'select'\n | 'sticky-bar'\n | 'stories'\n | 'story'\n | 'stories-viewer'\n | 'tabs'\n | 'tag'\n | 'color-scheme-icon'\n | 'toast'\n | 'toast-item'\n | 'tooltip'\n | 'tree'\n | 'tree-item';\n\nlet _elementPrefix = 'l';\nlet _cssPrefix = 'l';\nconst _registered = new Set<ElementBaseName>();\n\n/** Set the global prefix. Must be called before defining elements. */\nexport function setPrefix(prefix: { element: string; css: string }): void {\n _elementPrefix = prefix.element;\n _cssPrefix = prefix.css;\n}\n\n/** Get the current prefixes. */\nexport function getPrefix(): { element: string; css: string } {\n return { element: _elementPrefix, css: _cssPrefix };\n}\n\n/** Derive the full tag name from element prefix + base name. */\nexport function tagName(baseName: ElementBaseName): string {\n return `${_elementPrefix}-${baseName}`;\n}\n\n/** Prefix a CSS class name. `cls('toast-icon')` → `'l-toast-icon'` */\nexport function cls(name: string): string {\n return `${_cssPrefix}-${name}`;\n}\n\n/** Generate a globally unique prefixed ID. `uniqueId('toast')` → `'l:toast:1'` */\nlet _uid = 0;\nexport function uniqueId(base: string): string {\n return `${_cssPrefix}:${base}:${++_uid}`;\n}\n\n/** Check if a base name is already registered. */\nexport function isRegistered(baseName: ElementBaseName): boolean {\n return _registered.has(baseName);\n}\n\n/** Mark a base name as registered. Called internally by define(). */\nexport function markRegistered(baseName: ElementBaseName): void {\n _registered.add(baseName);\n}\n"],"mappings":"AA0CA,IAAI,EAAiB,IACjB,EAAa,IACX,EAAc,IAAI,IAcxB,SAAgB,EAAQ,EAAmC,CACzD,MAAO,GAAG,EAAe,GAAG,GAC9B,CAGA,SAAgB,EAAI,EAAsB,CACxC,MAAO,GAAG,EAAW,GAAG,GAC1B,CAGA,IAAI,EAAO,EACX,SAAgB,EAAS,EAAsB,CAC7C,MAAO,GAAG,EAAW,GAAG,EAAK,GAAG,EAAE,GACpC,CAGA,SAAgB,EAAa,EAAoC,CAC/D,OAAO,EAAY,IAAI,CAAQ,CACjC,CAGA,SAAgB,EAAe,EAAiC,CAC9D,EAAY,IAAI,CAAQ,CAC1B"}
|