luxen-ui 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"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 *\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 * @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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"}
@@ -87,11 +87,25 @@ export declare class Tag extends LuxenElement {
87
87
  accessor control: TagControl;
88
88
  /** Disable the tag — dims it and blocks selection and removal. */
89
89
  accessor disabled: boolean;
90
+ constructor();
90
91
  willUpdate(): void;
91
92
  updated(): void;
92
93
  private _requestRemove;
93
94
  private _onKeyDown;
94
95
  private _toggle;
96
+ /**
97
+ * Events do not propagate *down* into a shadow tree, so a click dispatched on
98
+ * the host — `tag.click()`, a test runner resolving the chip by its test id,
99
+ * a runner computing the chip's centre (`elementFromPoint()` retargets shadow
100
+ * content back to the host) — never reaches the toggle button or the
101
+ * checkbox, and silently does nothing. Delegate it.
102
+ *
103
+ * Only when the host *is* the target: a real gesture originates at the inner
104
+ * button, at the checkbox, or at slotted content, and is already handled
105
+ * where it lands — so this never double-toggles. `removable` stays out of it;
106
+ * removing is the × button's own gesture.
107
+ */
108
+ private _onHostClick;
95
109
  private _onCheckboxChange;
96
110
  private _label;
97
111
  private _removeButton;
@@ -1 +1 @@
1
- {"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tag/tag.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAS7D,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACzC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC;AAE7C;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;;CAIxC;AAED,uEAAuE;AACvE,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;gBACf,QAAQ,EAAE,OAAO;CAI9B;AAED,UAAU,WAAW;IACnB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,2BAA2B;QACnC,MAAM,EAAE,cAAc,CAAC;KACxB;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,qBAAa,GAAI,SAAQ,YAAY;IACnC,OAAgB,MAAM,4BAA4C;IAElE,OAAO,CAAC,SAAS,CAAgC;IAEjD,+CAA+C;IAE/C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAQ;IAE9B,kEAAkE;IAElE,QAAQ,CAAC,SAAS,UAAS;IAE3B,oEAAoE;IAEpE,QAAQ,CAAC,UAAU,UAAS;IAE5B,4EAA4E;IAE5E,QAAQ,CAAC,QAAQ,UAAS;IAE1B;;;;OAIG;IAEH,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAU;IAEtC,kEAAkE;IAElE,QAAQ,CAAC,QAAQ,UAAS;IAEjB,UAAU;IAOV,OAAO;IAWhB,OAAO,CAAC,cAAc,CAKpB;IAEF,OAAO,CAAC,UAAU,CAMhB;IAEF,OAAO,CAAC,OAAO,CAKb;IAEF,OAAO,CAAC,iBAAiB,CAGvB;IAEF,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,aAAa;IAaZ,MAAM;CAqDhB;AAQD,MAAM,WAAW,GAAG;IAClB,gBAAgB,CAAC,CAAC,SAAS,MAAM,WAAW,EAC1C,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EACjD,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,WAAW,EAC7C,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EACjD,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
+ {"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tag/tag.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAS7D,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACzC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC;AAE7C;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;;CAIxC;AAED,uEAAuE;AACvE,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;gBACf,QAAQ,EAAE,OAAO;CAI9B;AAED,UAAU,WAAW;IACnB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,2BAA2B;QACnC,MAAM,EAAE,cAAc,CAAC;KACxB;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,qBAAa,GAAI,SAAQ,YAAY;IACnC,OAAgB,MAAM,4BAA4C;IAElE,OAAO,CAAC,SAAS,CAAgC;IAEjD,+CAA+C;IAE/C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAQ;IAE9B,kEAAkE;IAElE,QAAQ,CAAC,SAAS,UAAS;IAE3B,oEAAoE;IAEpE,QAAQ,CAAC,UAAU,UAAS;IAE5B,4EAA4E;IAE5E,QAAQ,CAAC,QAAQ,UAAS;IAE1B;;;;OAIG;IAEH,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAU;IAEtC,kEAAkE;IAElE,QAAQ,CAAC,QAAQ,UAAS;;IASjB,UAAU;IAOV,OAAO;IAWhB,OAAO,CAAC,cAAc,CAKpB;IAEF,OAAO,CAAC,UAAU,CAMhB;IAEF,OAAO,CAAC,OAAO,CAKb;IAEF;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,YAAY,CAMlB;IAEF,OAAO,CAAC,iBAAiB,CAGvB;IAEF,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,aAAa;IAaZ,MAAM;CAqDhB;AAQD,MAAM,WAAW,GAAG;IAClB,gBAAgB,CAAC,CAAC,SAAS,MAAM,WAAW,EAC1C,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EACjD,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,WAAW,EAC7C,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EACjD,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,4 +1,4 @@
1
- import{cls as e}from"../../registry.js";import{i as t}from"../../chunks/lit.js";import{i as n,t as r}from"../../chunks/decorate.js";import{a as i,t as a}from"../../chunks/lit-html.js";import{LuxenElement as o}from"../../shared/luxen-element.js";import{LocalizeController as s}from"../../shared/localize.js";import c from"../../shared/styles/host.styles.js";import l from"../../shared/styles/checkbox-appearance.styles.js";var u=t(`@layer components{:host{--_height:var(--height,1.5rem);--_font-size:var(--font-size,var(--l-text-xs));--_padding-inline:var(--padding-inline,.5rem);--_gap:.25rem;--_background:var(--background,var(--l-color-bg-fill-neutral-subtle));--_color:var(--color,var(--l-color-text-neutral));--_border-strength:14%;vertical-align:middle;max-inline-size:100%;display:inline-flex}:host([size=sm]){--_padding-inline:var(--padding-inline,.375rem);--_gap:.125rem}:host([size=lg]){--_height:var(--height,1.75rem);--_font-size:var(--font-size,var(--l-text-sm));--_padding-inline:var(--padding-inline,.625rem)}:host([disabled]) .base{cursor:not-allowed}.base{align-items:center;gap:var(--_gap);min-block-size:var(--_height);padding-inline:var(--_padding-inline);border:1px solid color-mix(in oklab, currentColor var(--_border-strength), transparent);border-radius:var(--border-radius,var(--l-radius-full));background-color:var(--_background);color:var(--_color);font-family:var(--l-font-neo-grotesque);font-size:var(--_font-size);font-weight:var(--l-font-weight-medium);min-inline-size:0;line-height:1;transition:background-color .12s,border-color .12s;display:inline-flex}:host(:hover:not([disabled])) .base{background-color:color-mix(in oklab, var(--_background), currentColor 7%);border-color:color-mix(in oklab, currentColor calc(var(--_border-strength) + 12%), transparent)}.content{overflow-clip-margin:.2em;text-overflow:ellipsis;white-space:nowrap;align-items:center;min-inline-size:0;display:inline-flex;overflow:clip}::slotted([slot=prefix]){flex:none;display:inline-flex}::slotted([slot=suffix]){font-weight:var(--l-font-weight-normal);font-variant-numeric:tabular-nums;flex:none}:host([selectable]){--_height:var(--height,1.75rem);--_accent:var(--l-form-control-activated-color);--_selected-color:var(--selected-color,var(--lightningcss-light,color-mix(in oklab, var(--_accent), black 14%))var(--lightningcss-dark,color-mix(in oklab, var(--_accent), white 45%)));--_selected-background:var(--selected-background,color-mix(in oklab, var(--_selected-color) 10%, var(--l-color-surface)))}:host([selectable][size=lg]){--_height:var(--height,2rem)}:host([selectable][selected]){--_background:var(--_selected-background);--_color:var(--_selected-color);--_border-strength:45%}:host([selectable]) .base{cursor:pointer;-webkit-user-select:none;user-select:none}:host([selectable][disabled]) .base{cursor:not-allowed}.toggle{align-self:stretch;align-items:center;gap:var(--_gap);min-inline-size:0;margin-inline:calc(var(--_padding-inline) * -1);padding-inline:var(--_padding-inline);color:inherit;font:inherit;text-align:start;cursor:inherit;background:0 0;border:0;flex:auto;display:inline-flex}:host([removable]) .toggle{margin-inline-end:0;padding-inline-end:0}:host([selectable]) .toggle:focus-visible,:host([selectable]) .checkbox:focus-visible{outline:none}.base:has(:is(.toggle,.checkbox):focus-visible){outline:2px solid var(--l-focus-ring);outline-offset:2px}.checkbox{--accent:var(--selected-color,var(--l-form-control-activated-color));--_halo:var(--l-form-control-border-color-hover);font:inherit;flex:none;transition-property:background-color,border-color,box-shadow}.checkbox:checked,.checkbox:indeterminate{--_halo:var(--accent)}@media (hover:hover){:host([selectable]:not([disabled])) .base:hover .checkbox:not(:disabled){box-shadow:0 0 0 calc(var(--size) * var(--l-form-control-toggle-halo-ratio)) color-mix(in oklab, var(--_halo), transparent 84%)}}@media (forced-colors:active){:host([selectable][selected]) .base{border-color:highlight}}.remove{block-size:1.5rem;inline-size:1.5rem;margin-inline:calc(var(--_gap) * -.5) calc(var(--_padding-inline) * -.5);border-radius:var(--l-radius-full);color:inherit;cursor:pointer;background:radial-gradient(circle at center, color-mix(in oklab, currentColor 15%, transparent) 60%, transparent 62%) no-repeat center / 0 0;border:0;flex:none;place-items:center;padding:0;transition:scale .12s,background-size .14s;display:grid}.remove:after{content:"";opacity:.55;background-color:currentColor;block-size:.875rem;inline-size:.875rem;transition:opacity .12s;-webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\"><path d=\\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\\" /></svg>") 50%/contain no-repeat;mask:url("data:image/svg+xml;utf8,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\"><path d=\\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\\" /></svg>") 50%/contain no-repeat}.remove:hover{background-size:1.125rem 1.125rem}.remove:hover:after{opacity:1}.remove:active{background-image:radial-gradient(circle at center, color-mix(in oklab, currentColor 26%, transparent) 60%, transparent 62%);scale:.86}.remove:focus-visible{outline:2px solid var(--l-focus-ring);outline-offset:1px}.remove:disabled{cursor:not-allowed;opacity:.4}@media (prefers-reduced-motion:reduce){.base,.checkbox,.remove,.remove:after{transition-duration:0s}.remove:active{scale:1}}}`),d=class extends Event{constructor(){super(`remove`,{bubbles:!1,composed:!1,cancelable:!0})}},f=class extends Event{constructor(e){super(`change`,{bubbles:!0,composed:!1,cancelable:!1}),this.selected=e}},p=class extends o{constructor(...e){super(...e),this._localize=new s(this),this.#e=`md`,this.#t=!1,this.#n=!1,this.#r=!1,this.#i=`none`,this.#a=!1,this._requestRemove=()=>{if(this.disabled)return;let e=new d;this.dispatchEvent(e),e.defaultPrevented||this.remove()},this._onKeyDown=e=>{!this.removable||this.disabled||(e.key===`Backspace`||e.key===`Delete`)&&(e.preventDefault(),this._requestRemove())},this._toggle=()=>{this.disabled||(this.selected=!this.selected,this.dispatchEvent(new f(this.selected)))},this._onCheckboxChange=e=>{this.selected=e.target.checked,this.dispatchEvent(new f(this.selected))}}static{this.styles=[c,l,u]}#e;get size(){return this.#e}set size(e){this.#e=e}#t;get removable(){return this.#t}set removable(e){this.#t=e}#n;get selectable(){return this.#n}set selectable(e){this.#n=e}#r;get selected(){return this.#r}set selected(e){this.#r=e}#i;get control(){return this.#i}set control(e){this.#i=e}#a;get disabled(){return this.#a}set disabled(e){this.#a=e}willUpdate(){this.control===`checkbox`&&(this.selectable=!0)}updated(){let e=this.renderRoot.querySelector(`.checkbox`);e&&(e.checked=this.selected)}_label(){return i`
1
+ import{cls as e}from"../../registry.js";import{i as t}from"../../chunks/lit.js";import{i as n,t as r}from"../../chunks/decorate.js";import{a as i,t as a}from"../../chunks/lit-html.js";import{LuxenElement as o}from"../../shared/luxen-element.js";import{LocalizeController as s}from"../../shared/localize.js";import c from"../../shared/styles/host.styles.js";import l from"../../shared/styles/checkbox-appearance.styles.js";var u=t(`@layer components{:host{--_height:var(--height,1.5rem);--_font-size:var(--font-size,var(--l-text-xs));--_padding-inline:var(--padding-inline,.5rem);--_gap:.25rem;--_background:var(--background,var(--l-color-bg-fill-neutral-subtle));--_color:var(--color,var(--l-color-text-neutral));--_border-strength:14%;vertical-align:middle;max-inline-size:100%;display:inline-flex}:host([size=sm]){--_padding-inline:var(--padding-inline,.375rem);--_gap:.125rem}:host([size=lg]){--_height:var(--height,1.75rem);--_font-size:var(--font-size,var(--l-text-sm));--_padding-inline:var(--padding-inline,.625rem)}:host([disabled]) .base{cursor:not-allowed}.base{align-items:center;gap:var(--_gap);min-block-size:var(--_height);padding-inline:var(--_padding-inline);border:1px solid color-mix(in oklab, currentColor var(--_border-strength), transparent);border-radius:var(--border-radius,var(--l-radius-full));background-color:var(--_background);color:var(--_color);font-family:var(--l-font-neo-grotesque);font-size:var(--_font-size);font-weight:var(--l-font-weight-medium);min-inline-size:0;line-height:1;transition:background-color .12s,border-color .12s;display:inline-flex}:host(:hover:not([disabled])) .base{background-color:color-mix(in oklab, var(--_background), currentColor 7%);border-color:color-mix(in oklab, currentColor calc(var(--_border-strength) + 12%), transparent)}.content{overflow-clip-margin:.2em;text-overflow:ellipsis;white-space:nowrap;align-items:center;min-inline-size:0;display:inline-flex;overflow:clip}::slotted([slot=prefix]){flex:none;display:inline-flex}::slotted([slot=suffix]){font-weight:var(--l-font-weight-normal);font-variant-numeric:tabular-nums;flex:none}:host([selectable]){--_height:var(--height,1.75rem);--_accent:var(--l-form-control-activated-color);--_selected-color:var(--selected-color,var(--lightningcss-light,color-mix(in oklab, var(--_accent), black 14%))var(--lightningcss-dark,color-mix(in oklab, var(--_accent), white 45%)));--_selected-background:var(--selected-background,color-mix(in oklab, var(--_selected-color) 10%, var(--l-color-surface)))}:host([selectable][size=lg]){--_height:var(--height,2rem)}:host([selectable][selected]){--_background:var(--_selected-background);--_color:var(--_selected-color);--_border-strength:45%}:host([selectable]) .base{cursor:pointer;-webkit-user-select:none;user-select:none}:host([selectable][disabled]) .base{cursor:not-allowed}.toggle{align-self:stretch;align-items:center;gap:var(--_gap);min-inline-size:0;margin-inline:calc(var(--_padding-inline) * -1);padding-inline:var(--_padding-inline);color:inherit;font:inherit;text-align:start;cursor:inherit;background:0 0;border:0;flex:auto;display:inline-flex}:host([removable]) .toggle{margin-inline-end:0;padding-inline-end:0}:host([selectable]) .toggle:focus-visible,:host([selectable]) .checkbox:focus-visible{outline:none}.base:has(:is(.toggle,.checkbox):focus-visible){outline:2px solid var(--l-focus-ring);outline-offset:2px}.checkbox{--accent:var(--selected-color,var(--l-form-control-activated-color));--_halo:var(--l-form-control-border-color-hover);font:inherit;flex:none;transition-property:background-color,border-color,box-shadow}.checkbox:checked,.checkbox:indeterminate{--_halo:var(--accent)}@media (hover:hover){:host([selectable]:not([disabled])) .base:hover .checkbox:not(:disabled){box-shadow:0 0 0 calc(var(--size) * var(--l-form-control-toggle-halo-ratio)) color-mix(in oklab, var(--_halo), transparent 84%)}}@media (forced-colors:active){:host([selectable][selected]) .base{border-color:highlight}}.remove{block-size:1.5rem;inline-size:1.5rem;margin-inline:calc(var(--_gap) * -.5) calc(var(--_padding-inline) * -.5);border-radius:var(--l-radius-full);color:inherit;cursor:pointer;background:radial-gradient(circle at center, color-mix(in oklab, currentColor 15%, transparent) 60%, transparent 62%) no-repeat center / 0 0;border:0;flex:none;place-items:center;padding:0;transition:scale .12s,background-size .14s;display:grid}.remove:after{content:"";opacity:.55;background-color:currentColor;block-size:.875rem;inline-size:.875rem;transition:opacity .12s;-webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\"><path d=\\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\\" /></svg>") 50%/contain no-repeat;mask:url("data:image/svg+xml;utf8,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\"><path d=\\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\\" /></svg>") 50%/contain no-repeat}.remove:hover{background-size:1.125rem 1.125rem}.remove:hover:after{opacity:1}.remove:active{background-image:radial-gradient(circle at center, color-mix(in oklab, currentColor 26%, transparent) 60%, transparent 62%);scale:.86}.remove:focus-visible{outline:2px solid var(--l-focus-ring);outline-offset:1px}.remove:disabled{cursor:not-allowed;opacity:.4}@media (prefers-reduced-motion:reduce){.base,.checkbox,.remove,.remove:after{transition-duration:0s}.remove:active{scale:1}}}`),d=class extends Event{constructor(){super(`remove`,{bubbles:!1,composed:!1,cancelable:!0})}},f=class extends Event{constructor(e){super(`change`,{bubbles:!0,composed:!1,cancelable:!1}),this.selected=e}},p=class extends o{static{this.styles=[c,l,u]}#e;get size(){return this.#e}set size(e){this.#e=e}#t;get removable(){return this.#t}set removable(e){this.#t=e}#n;get selectable(){return this.#n}set selectable(e){this.#n=e}#r;get selected(){return this.#r}set selected(e){this.#r=e}#i;get control(){return this.#i}set control(e){this.#i=e}#a;get disabled(){return this.#a}set disabled(e){this.#a=e}constructor(){super(),this._localize=new s(this),this.#e=`md`,this.#t=!1,this.#n=!1,this.#r=!1,this.#i=`none`,this.#a=!1,this._requestRemove=()=>{if(this.disabled)return;let e=new d;this.dispatchEvent(e),e.defaultPrevented||this.remove()},this._onKeyDown=e=>{!this.removable||this.disabled||(e.key===`Backspace`||e.key===`Delete`)&&(e.preventDefault(),this._requestRemove())},this._toggle=()=>{this.disabled||(this.selected=!this.selected,this.dispatchEvent(new f(this.selected)))},this._onHostClick=e=>{!this.selectable||this.disabled||e.composedPath()[0]===this&&this._toggle()},this._onCheckboxChange=e=>{this.selected=e.target.checked,this.dispatchEvent(new f(this.selected))},this.addEventListener(`click`,this._onHostClick)}willUpdate(){this.control===`checkbox`&&(this.selectable=!0)}updated(){let e=this.renderRoot.querySelector(`.checkbox`);e&&(e.checked=this.selected)}_label(){return i`
2
2
  <slot name="prefix"></slot>
3
3
  <span
4
4
  class="content"
@@ -1 +1 @@
1
- {"version":3,"file":"tag.js","names":[],"sources":["../../../src/html/elements/tag/tag.css?inline","../../../src/html/elements/tag/tag.ts"],"sourcesContent":["@layer components {\n :host {\n /* Size defaults (md). A removable tag is always ≥24px tall (target-size).\n The three public knobs are folded into the private vars here rather than\n at each use site, so an override also carries to what derives from them —\n the toggle's negative inline margins and the × button's spacing math. The\n trio mirrors `.l-button` (`--height` / `--padding-inline` / `--font-size`)\n so a chip can be tuned to a dense filter panel without ::part(base). */\n --_height: var(--height, 1.5rem);\n --_font-size: var(--font-size, var(--l-text-xs));\n --_padding-inline: var(--padding-inline, 0.5rem);\n --_gap: 0.25rem;\n\n /* Skin, resolved once so `[selected]` can swap all three at the source\n instead of re-declaring the properties that consume them. */\n --_background: var(--background, var(--l-color-bg-fill-neutral-subtle));\n --_color: var(--color, var(--l-color-text-neutral));\n --_border-strength: 14%;\n\n display: inline-flex;\n vertical-align: middle;\n max-inline-size: 100%;\n }\n\n :host([size='sm']) {\n --_padding-inline: var(--padding-inline, 0.375rem);\n --_gap: 0.125rem;\n }\n\n :host([size='lg']) {\n --_height: var(--height, 1.75rem);\n --_font-size: var(--font-size, var(--l-text-sm));\n --_padding-inline: var(--padding-inline, 0.625rem);\n }\n\n /* Disabled is conveyed by the greyed × button + cursor, NOT by washing out\n * the label — dimming the whole chip with opacity composites the text below\n * the WCAG AA contrast floor (axe flags it; a disabled <button> is exempt but\n * the label span is not). */\n :host([disabled]) .base {\n cursor: not-allowed;\n }\n\n /*\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n 🅲🅷🅸🅿\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n */\n\n .base {\n display: inline-flex;\n align-items: center;\n gap: var(--_gap);\n min-block-size: var(--_height);\n padding-inline: var(--_padding-inline);\n /* Discreet border + hover tint derive from the text color, so they adapt to\n any consumer `--color`. */\n border: 1px solid color-mix(in oklab, currentColor var(--_border-strength), transparent);\n border-radius: var(--border-radius, var(--l-radius-full));\n background-color: var(--_background);\n color: var(--_color);\n font-family: var(--l-font-neo-grotesque);\n font-size: var(--_font-size);\n font-weight: var(--l-font-weight-medium);\n line-height: 1;\n min-inline-size: 0;\n transition:\n background-color 120ms ease,\n border-color 120ms ease;\n }\n\n /* Whole-chip hover: a touch more presence, without competing with the ×. */\n :host(:hover:not([disabled])) .base {\n background-color: color-mix(in oklab, var(--_background), currentColor 7%);\n border-color: color-mix(\n in oklab,\n currentColor calc(var(--_border-strength) + 12%),\n transparent\n );\n }\n\n .content {\n display: inline-flex;\n align-items: center;\n min-inline-size: 0;\n /* `.base` sets `line-height: 1` so the chip's height comes from its own size\n tokens rather than the document's leading. That makes the line box exactly\n one em tall, and a plain `overflow: hidden` then shears every descender\n (g, j, p, q, y) at all three sizes — measured 1px short.\n\n Fixed by widening the clip rectangle, not the box: growing the line box\n instead (`line-height: normal`) moves the text half a pixel against the ×\n and the prefix icon, since the font's ascent and descent are not\n symmetrical. `clip` keeps the geometry identical and simply paints the\n tails. The margin applies to every side — there are no per-axis longhands\n in any engine yet — but 0.2em of horizontal bleed is invisible next to a\n label that is hard-cut at the edge anyway. */\n overflow: clip;\n overflow-clip-margin: 0.2em;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n ::slotted([slot='prefix']) {\n display: inline-flex;\n flex: none;\n }\n\n /* A count or trailing glyph. It sits in its own slot so `.base`'s gap gives it\n a gutter by construction — the label is a bare text node and can never be\n targeted from the outside. Lighter weight than the medium label, and\n tabular so a column of counts stays aligned. */\n ::slotted([slot='suffix']) {\n flex: none;\n font-weight: var(--l-font-weight-normal);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n 🆂🅴🅻🅴🅲🆃🅰🅱🅻🅴 (filter chip — toggle button, or a checkbox riding inside)\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n */\n\n :host([selectable]) {\n /* Taller than a display chip at the same `size`: a chip you click is a\n target (WCAG 2.5.8), and this is the dense step between `sm` and `lg` —\n more room, same 12px label, where `lg` would jump the label to body size.\n `--height` / `--font-size` tune it from there. */\n --_height: var(--height, 1.75rem);\n\n /* The selection accent is the same one the library's checkbox, radio and\n switch use, so a chip with a checkbox inside reads as a single control. */\n --_accent: var(--l-form-control-activated-color);\n /* The label is 12px, so it owes 4.5:1 over its own tint — and the raw accent\n misses it on the light one (4.35:1 measured). Darken it there, lighten it\n on the dark tint, and both modes clear 6:1. `color-mix()` resolves\n reliably inside `light-dark()` — `oklch(from …)` does not. */\n --_selected-color: var(\n --selected-color,\n light-dark(\n color-mix(in oklab, var(--_accent), black 14%),\n color-mix(in oklab, var(--_accent), white 45%)\n )\n );\n /* Derived from the text color, so overriding `--selected-color` alone\n repaints the whole chip coherently. Kept at 10%: a consumer accent skips\n the mode correction above, so the tint has to stay pale enough for a\n semantic token (`--l-color-text-success` and friends) to clear 4.5:1 on\n it unaided. The border carries the rest of the emphasis. */\n --_selected-background: var(\n --selected-background,\n color-mix(in oklab, var(--_selected-color) 10%, var(--l-color-surface))\n );\n }\n\n :host([selectable][size='lg']) {\n --_height: var(--height, 2rem);\n }\n\n :host([selectable][selected]) {\n --_background: var(--_selected-background);\n --_color: var(--_selected-color);\n --_border-strength: 45%;\n }\n\n :host([selectable]) .base {\n cursor: pointer;\n /* A chip is tapped, not read: never leave a text selection behind a\n double-click on a filter. */\n user-select: none;\n }\n\n :host([selectable][disabled]) .base {\n cursor: not-allowed;\n }\n\n /* The chip *is* the button. Negative inline margins let it cover `.base`'s\n padding so the whole pill is the target, while `.base` keeps owning the\n border, background, and the × button's spacing math. */\n .toggle {\n flex: 1 1 auto;\n display: inline-flex;\n align-items: center;\n align-self: stretch;\n gap: var(--_gap);\n min-inline-size: 0;\n margin-inline: calc(var(--_padding-inline) * -1);\n padding-inline: var(--_padding-inline);\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n text-align: start;\n cursor: inherit;\n }\n\n /* Stop before the × instead of running under it. */\n :host([removable]) .toggle {\n margin-inline-end: 0;\n padding-inline-end: 0;\n }\n\n /* One ring for the whole chip: the inner control is a detail of how the chip\n is operated, not a second thing to focus visually — and the ring drawn\n below fully encloses the box, so the indicator only grows (WCAG 2.4.7 /\n 2.4.13).\n\n Qualified by `:host(…)` on purpose. The shared appearance nests\n `&:focus-visible` under a selector list, so it lands as\n `:is(.l-checkbox, … input[type='checkbox']:not(…)):focus-visible` — 0,3,1,\n which outranks a bare `.checkbox:focus-visible` (0,2,0) and paints a second\n ring around the box. `:host([selectable])` brings this to 0,4,0. */\n :host([selectable]) .toggle:focus-visible,\n :host([selectable]) .checkbox:focus-visible {\n outline: none;\n }\n\n .base:has(:is(.toggle, .checkbox):focus-visible) {\n outline: 2px solid var(--l-focus-ring);\n outline-offset: 2px;\n }\n\n /* The skin comes from the shared `.l-checkbox` appearance, applied through\n `cls('checkbox')` rather than a static class literal: `dist` ships that CSS\n as a real file, so a consumer's PostCSS rewrites its selector under a\n renamed `cssPrefix` — a baked `l-checkbox` in the template would stop\n matching it. This rule only carries what is specific to a chip.\n\n The shared appearance sizes the box in `em`, and a UA gives `<input>` its\n own font — inheriting the chip's makes the box track the `size` step. The\n accent follows the selected color, so a themed chip keeps its checkbox in\n the same hue. */\n .checkbox {\n --accent: var(--selected-color, var(--l-form-control-activated-color));\n /* Hover halo — same ratio and same token flip as the light-DOM\n `.l-checkbox`, which keeps this rule out of the shared partial (see\n `css/elements/checkbox.css`). Repeated here rather than moved into the\n partial: `l-tree-item` shares that file and never opted into a glow. */\n --_halo: var(--l-form-control-border-color-hover);\n\n flex: none;\n font: inherit;\n transition-property: background-color, border-color, box-shadow;\n }\n\n .checkbox:checked,\n .checkbox:indeterminate {\n --_halo: var(--accent);\n }\n\n /* Driven by the chip, not by the box: the chip is the label and the whole\n click target, so a halo lighting up only over 15px of a 28px pill would\n read as a glitch. */\n @media (hover: hover) {\n :host([selectable]:not([disabled])) .base:hover .checkbox:not(:disabled) {\n box-shadow: 0 0 0 calc(var(--size) * var(--l-form-control-toggle-halo-ratio))\n color-mix(in oklab, var(--_halo), transparent 84%);\n }\n }\n\n /* Forced colors drop the tint (backgrounds are forced to Canvas), so pin the\n border to the system highlight — otherwise selection becomes invisible. */\n @media (forced-colors: active) {\n :host([selectable][selected]) .base {\n border-color: Highlight;\n }\n }\n\n /*\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n 🆁🅴🅼🅾🆅🅴 (≥24px target-size, mdi:close mask — mirrors l-close)\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n */\n\n .remove {\n flex: none;\n display: grid;\n place-items: center;\n /* 24px hit target (WCAG 2.5.8); the visible tint circle is inset to ~18px. */\n inline-size: 1.5rem;\n block-size: 1.5rem;\n margin-inline: calc(var(--_gap) * -0.5) calc(var(--_padding-inline) * -0.5);\n padding: 0;\n border: 0;\n border-radius: var(--l-radius-full);\n color: inherit;\n cursor: pointer;\n /* Tint circle behind the glyph — a gradient disc kept smaller than the 24px\n hit area, grown from 0 on hover so it reads as a neat ~18px dot. */\n background: radial-gradient(\n circle at center,\n color-mix(in oklab, currentColor 15%, transparent) 60%,\n transparent 62%\n )\n no-repeat center / 0 0;\n transition:\n scale 120ms ease,\n background-size 140ms ease;\n\n &::after {\n content: '';\n inline-size: 0.875rem;\n block-size: 0.875rem;\n background-color: currentColor;\n opacity: 0.55;\n transition: opacity 120ms ease;\n /* https://pictogrammers.com/library/mdi/icon/close */\n mask: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\" /></svg>')\n center / contain no-repeat;\n }\n\n &:hover {\n background-size: 1.125rem 1.125rem;\n }\n &:hover::after {\n opacity: 1;\n }\n\n /* Tactile press — the button dips and the disc deepens. */\n &:active {\n scale: 0.86;\n background-image: radial-gradient(\n circle at center,\n color-mix(in oklab, currentColor 26%, transparent) 60%,\n transparent 62%\n );\n }\n\n &:focus-visible {\n outline: 2px solid var(--l-focus-ring);\n outline-offset: 1px;\n }\n\n &:disabled {\n cursor: not-allowed;\n opacity: 0.4;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .base,\n .checkbox,\n .remove,\n .remove::after {\n transition-duration: 0ms;\n }\n .remove:active {\n scale: 1;\n }\n }\n}\n","import { html, nothing, unsafeCSS } from 'lit';\nimport { property } from 'lit/decorators.js';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { LocalizeController } from '../../shared/localize.js';\nimport { cls } from '../../registry.js';\nimport hostStyles from '../../shared/styles/host.styles.js';\nimport checkboxAppearance from '../../shared/styles/checkbox-appearance.styles.js';\nimport rawStyles from './tag.css?inline';\n\nconst styles = unsafeCSS(rawStyles);\n\nexport type TagSize = 'sm' | 'md' | 'lg';\nexport type TagControl = 'none' | 'checkbox';\n\n/**\n * Fired when the user asks to remove the tag — clicking the × button, or\n * pressing Backspace/Delete while it is focused. Cancelable: call\n * `preventDefault()` to keep the tag (the controlling host then manages the\n * list itself); otherwise the tag removes itself from the DOM. Does not bubble.\n */\nexport class TagRemoveEvent extends Event {\n constructor() {\n super('remove', { bubbles: false, composed: false, cancelable: true });\n }\n}\n\n/** Fired when a `selectable` tag is toggled. Bubbles; not composed. */\nexport class TagChangeEvent extends Event {\n readonly selected: boolean;\n constructor(selected: boolean) {\n super('change', { bubbles: true, composed: false, cancelable: false });\n this.selected = selected;\n }\n}\n\ninterface TagEventMap {\n change: TagChangeEvent;\n}\n\ndeclare global {\n interface GlobalEventHandlersEventMap {\n remove: TagRemoveEvent;\n }\n}\n\n/**\n * A compact chip for tokens, filters, and multi-select values. Shadow DOM so its\n * styles are self-contained and it renders correctly **anywhere**, including\n * inside another element's shadow root (e.g. the `<l-select>` multi-select\n * trigger).\n *\n * Add `selectable` to turn the chip into a filter control: the tag itself\n * becomes a toggle button (`aria-pressed`), and re-activating a selected tag\n * deselects it — the affordance a radio group cannot offer, so a facet always\n * has a way back to \"all\". Add `control=\"checkbox\"` for a multi-select axis and\n * the library's own checkbox rides inside, with the chip as its label.\n *\n * @summary A compact, optionally removable or selectable chip.\n *\n * @example\n * ```html\n * <l-tag removable>Design</l-tag>\n * <l-tag selectable selected>A3</l-tag>\n * <l-tag selectable control=\"checkbox\">Color <span slot=\"suffix\">79</span></l-tag>\n * ```\n *\n * @event remove - Fired when the user removes the tag (× click or Backspace/Delete). Cancelable; if not prevented the tag removes itself. Not composed, does not bubble.\n * @event change - Fired when a `selectable` tag is toggled. Not cancelable — like the platform's own `change`, and like `l-segmented-control`. Bubbles. Properties: `selected: boolean`. The chip is uncontrolled: it applies the new state before dispatching, so a host that vetoes a toggle (a \"max 3 filters\" rule, an async guard) sets `selected` back in the listener — the revert lands in the same render and is never painted.\n *\n * @slot - The tag label.\n * @slot prefix - Leading content, e.g. an `<l-icon>` or `<l-avatar>`.\n * @slot suffix - Trailing content, e.g. a result count. Gets the chip's own gutter, so no margin is needed.\n *\n * @csspart base - The chip container.\n * @csspart content - The label wrapper.\n * @csspart toggle - The toggle button rendered by `selectable` (not with `control=\"checkbox\"`).\n * @csspart checkbox - The native checkbox rendered by `control=\"checkbox\"`.\n * @csspart remove - The remove button.\n *\n * @cssproperty [--border-radius] - Corner radius. Defaults to a full pill.\n * @cssproperty [--height] - Chip height. Defaults to the `size` step (a selectable chip is taller, to keep a comfortable target).\n * @cssproperty [--font-size] - Label size. Defaults to the `size` step — 12px, or 14px at `size=\"lg\"`. Set it with `--height` to land between the two steps in a dense filter panel.\n * @cssproperty [--padding-inline] - Horizontal padding. Defaults to the `size` step.\n * @cssproperty [--background] - Chip background.\n * @cssproperty [--color] - Text color.\n * @cssproperty [--selected-color] - Text, border, and checkbox accent when selected. Defaults to the library's form-control accent, lightened in dark mode.\n * @cssproperty [--selected-background] - Chip background when selected. Defaults to a tint of `--selected-color`.\n *\n * @customElement l-tag\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- typed addEventListener overloads merged below; no uninitialized properties.\nexport class Tag extends LuxenElement {\n static override styles = [hostStyles, checkboxAppearance, styles];\n\n private _localize = new LocalizeController(this);\n\n /** Tag size: `sm`, `md` (default), or `lg`. */\n @property({ reflect: true })\n accessor size: TagSize = 'md';\n\n /** Show a remove button (and enable Backspace/Delete removal). */\n @property({ type: Boolean, reflect: true })\n accessor removable = false;\n\n /** Make the tag a filter control the user can toggle on and off. */\n @property({ type: Boolean, reflect: true })\n accessor selectable = false;\n\n /** Whether the tag is selected. Reflected, so `[selected]` is styleable. */\n @property({ type: Boolean, reflect: true })\n accessor selected = false;\n\n /**\n * What drives the selection: `none` (default) makes the chip itself a toggle\n * button; `checkbox` renders a checkbox inside and makes the chip its label —\n * the right choice for a multi-select facet. Implies `selectable`.\n */\n @property({ reflect: true })\n accessor control: TagControl = 'none';\n\n /** Disable the tag — dims it and blocks selection and removal. */\n @property({ type: Boolean, reflect: true })\n accessor disabled = false;\n\n override willUpdate() {\n // A checkbox is only ever rendered on a selectable chip, so the attribute\n // (which every selectable style rule keys off) is derived rather than\n // duplicated in every selector — and in the consumer's markup.\n if (this.control === 'checkbox') this.selectable = true;\n }\n\n override updated() {\n // Re-assert the box, don't rely on the `.checked` binding. Lit dirty-checks\n // a property part against the value it last wrote, and a click has the user\n // agent flip `checked` behind its back — so when a controlling host vetoes\n // the toggle by setting `selected` back to what it already was, Lit sees no\n // change, skips the write, and leaves a checked box on an unselected chip\n // (announced \"checked\" too). Cheap: the query is null on every other path.\n const box = this.renderRoot.querySelector<HTMLInputElement>('.checkbox');\n if (box) box.checked = this.selected;\n }\n\n private _requestRemove = () => {\n if (this.disabled) return;\n const event = new TagRemoveEvent();\n this.dispatchEvent(event);\n if (!event.defaultPrevented) this.remove();\n };\n\n private _onKeyDown = (e: KeyboardEvent) => {\n if (!this.removable || this.disabled) return;\n if (e.key === 'Backspace' || e.key === 'Delete') {\n e.preventDefault();\n this._requestRemove();\n }\n };\n\n private _toggle = () => {\n if (this.disabled) return;\n // Toggling, not setting: re-activating a selected tag releases it.\n this.selected = !this.selected;\n this.dispatchEvent(new TagChangeEvent(this.selected));\n };\n\n private _onCheckboxChange = (e: Event) => {\n this.selected = (e.target as HTMLInputElement).checked;\n this.dispatchEvent(new TagChangeEvent(this.selected));\n };\n\n private _label() {\n return html`\n <slot name=\"prefix\"></slot>\n <span\n class=\"content\"\n part=\"content\"\n >\n <slot></slot>\n </span>\n <slot name=\"suffix\"></slot>\n `;\n }\n\n private _removeButton() {\n return this.removable\n ? html`<button\n class=\"remove\"\n part=\"remove\"\n type=\"button\"\n ?disabled=${this.disabled}\n aria-label=${this._localize.term('remove')}\n @click=${this._requestRemove}\n ></button>`\n : nothing;\n }\n\n override render() {\n if (this.control === 'checkbox') {\n return html`\n <label\n class=\"base\"\n part=\"base\"\n @keydown=${this._onKeyDown}\n >\n <input\n class=\"checkbox ${cls('checkbox')}\"\n part=\"checkbox\"\n type=\"checkbox\"\n .checked=${this.selected}\n ?disabled=${this.disabled}\n @change=${this._onCheckboxChange}\n />\n ${this._label()}${this._removeButton()}\n </label>\n `;\n }\n\n if (this.selectable) {\n return html`\n <span\n class=\"base\"\n part=\"base\"\n @keydown=${this._onKeyDown}\n >\n <button\n class=\"toggle\"\n part=\"toggle\"\n type=\"button\"\n aria-pressed=${this.selected ? 'true' : 'false'}\n ?disabled=${this.disabled}\n @click=${this._toggle}\n >\n ${this._label()}\n </button>\n ${this._removeButton()}\n </span>\n `;\n }\n\n return html`\n <span\n class=\"base\"\n part=\"base\"\n @keydown=${this._onKeyDown}\n >\n ${this._label()}${this._removeButton()}\n </span>\n `;\n }\n}\n\n// Type `addEventListener('change', …)` so listeners receive a `TagChangeEvent`\n// (with `.selected`) instead of a bare `Event`. `change` collides with lib.dom's\n// `GlobalEventHandlersEventMap`, so it can't be augmented globally — the\n// overloads are declaration-merged onto this element's interface instead. The\n// 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 Tag {\n addEventListener<K extends keyof TagEventMap>(\n type: K,\n listener: (this: Tag, ev: TagEventMap[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 TagEventMap>(\n type: K,\n listener: (this: Tag, ev: TagEventMap[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":"saCSA,IAAM,EAAS,EAAU,i/JAAS,EAWrB,EAAb,cAAoC,KAAM,CACxC,aAAc,CACZ,MAAM,SAAU,CAAE,QAAS,GAAO,SAAU,GAAO,WAAY,EAAK,CAAC,CACvE,CACF,EAGa,EAAb,cAAoC,KAAM,CAExC,YAAY,EAAmB,CAC7B,MAAM,SAAU,CAAE,QAAS,GAAM,SAAU,GAAO,WAAY,EAAM,CAAC,EACrE,KAAK,SAAW,CAClB,CACF,EA0Da,EAAb,cAAyB,CAAa,8CAGhB,IAAI,EAAmB,IAAI,UAItB,aAIJ,WAIC,WAIF,WAQW,eAIX,2BAoBW,CAC7B,GAAI,KAAK,SAAU,OACnB,IAAM,EAAQ,IAAI,EAClB,KAAK,cAAc,CAAK,EACnB,EAAM,kBAAkB,KAAK,OAAO,CAC3C,kBAEsB,GAAqB,CACrC,CAAC,KAAK,WAAa,KAAK,WACxB,EAAE,MAAQ,aAAe,EAAE,MAAQ,YACrC,EAAE,eAAe,EACjB,KAAK,eAAe,EAExB,mBAEwB,CAClB,KAAK,WAET,KAAK,SAAW,CAAC,KAAK,SACtB,KAAK,cAAc,IAAI,EAAe,KAAK,QAAQ,CAAC,EACtD,yBAE6B,GAAa,CACxC,KAAK,SAAY,EAAE,OAA4B,QAC/C,KAAK,cAAc,IAAI,EAAe,KAAK,QAAQ,CAAC,CACtD,qBA3EyB,CAAC,EAAY,EAAoB,CAAM,SAMvD,MAAA,6CAIA,WAAA,kDAIA,YAAA,mDAIA,UAAA,iDAQA,SAAA,gDAIA,UAAA,0CAET,YAAsB,CAIhB,KAAK,UAAY,aAAY,KAAK,WAAa,GACrD,CAEA,SAAmB,CAOjB,IAAM,EAAM,KAAK,WAAW,cAAgC,WAAW,EACnE,IAAK,EAAI,QAAU,KAAK,SAC9B,CA6BA,QAAiB,CACf,MAAO,EAAI;;;;;;;;;KAUb,CAEA,eAAwB,CACtB,OAAO,KAAK,UACR,CAAI;;;;sBAIU,KAAK,SAAS;uBACb,KAAK,UAAU,KAAK,QAAQ,EAAE;mBAClC,KAAK,eAAe;oBAE/B,CACN,CAEA,QAAkB,CA2ChB,OA1CI,KAAK,UAAY,WACZ,CAAI;;;;qBAII,KAAK,WAAW;;;8BAGP,EAAI,UAAU,EAAE;;;uBAGvB,KAAK,SAAS;wBACb,KAAK,SAAS;sBAChB,KAAK,kBAAkB;;YAEjC,KAAK,OAAO,IAAI,KAAK,cAAc,EAAE;;QAKzC,KAAK,WACA,CAAI;;;;qBAII,KAAK,WAAW;;;;;;2BAMV,KAAK,SAAW,OAAS,QAAQ;wBACpC,KAAK,SAAS;qBACjB,KAAK,QAAQ;;cAEpB,KAAK,OAAO,EAAE;;YAEhB,KAAK,cAAc,EAAE;;QAKtB,CAAI;;;;mBAII,KAAK,WAAW;;UAEzB,KAAK,OAAO,IAAI,KAAK,cAAc,EAAE;;KAG7C,CACF,KAvJG,EAAS,CAAE,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,OAAA,IAAA,KAI1B,EAAS,CAAE,KAAM,QAAS,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,YAAA,IAAA,KAIzC,EAAS,CAAE,KAAM,QAAS,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,aAAA,IAAA,KAIzC,EAAS,CAAE,KAAM,QAAS,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,WAAA,IAAA,KAQzC,EAAS,CAAE,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,UAAA,IAAA,KAI1B,EAAS,CAAE,KAAM,QAAS,QAAS,EAAK,CAAC,CAAA,EAAA,EAAA,UAAA,WAAA,IAAA"}
1
+ {"version":3,"file":"tag.js","names":[],"sources":["../../../src/html/elements/tag/tag.css?inline","../../../src/html/elements/tag/tag.ts"],"sourcesContent":["@layer components {\n :host {\n /* Size defaults (md). A removable tag is always ≥24px tall (target-size).\n The three public knobs are folded into the private vars here rather than\n at each use site, so an override also carries to what derives from them —\n the toggle's negative inline margins and the × button's spacing math. The\n trio mirrors `.l-button` (`--height` / `--padding-inline` / `--font-size`)\n so a chip can be tuned to a dense filter panel without ::part(base). */\n --_height: var(--height, 1.5rem);\n --_font-size: var(--font-size, var(--l-text-xs));\n --_padding-inline: var(--padding-inline, 0.5rem);\n --_gap: 0.25rem;\n\n /* Skin, resolved once so `[selected]` can swap all three at the source\n instead of re-declaring the properties that consume them. */\n --_background: var(--background, var(--l-color-bg-fill-neutral-subtle));\n --_color: var(--color, var(--l-color-text-neutral));\n --_border-strength: 14%;\n\n display: inline-flex;\n vertical-align: middle;\n max-inline-size: 100%;\n }\n\n :host([size='sm']) {\n --_padding-inline: var(--padding-inline, 0.375rem);\n --_gap: 0.125rem;\n }\n\n :host([size='lg']) {\n --_height: var(--height, 1.75rem);\n --_font-size: var(--font-size, var(--l-text-sm));\n --_padding-inline: var(--padding-inline, 0.625rem);\n }\n\n /* Disabled is conveyed by the greyed × button + cursor, NOT by washing out\n * the label — dimming the whole chip with opacity composites the text below\n * the WCAG AA contrast floor (axe flags it; a disabled <button> is exempt but\n * the label span is not). */\n :host([disabled]) .base {\n cursor: not-allowed;\n }\n\n /*\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n 🅲🅷🅸🅿\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n */\n\n .base {\n display: inline-flex;\n align-items: center;\n gap: var(--_gap);\n min-block-size: var(--_height);\n padding-inline: var(--_padding-inline);\n /* Discreet border + hover tint derive from the text color, so they adapt to\n any consumer `--color`. */\n border: 1px solid color-mix(in oklab, currentColor var(--_border-strength), transparent);\n border-radius: var(--border-radius, var(--l-radius-full));\n background-color: var(--_background);\n color: var(--_color);\n font-family: var(--l-font-neo-grotesque);\n font-size: var(--_font-size);\n font-weight: var(--l-font-weight-medium);\n line-height: 1;\n min-inline-size: 0;\n transition:\n background-color 120ms ease,\n border-color 120ms ease;\n }\n\n /* Whole-chip hover: a touch more presence, without competing with the ×. */\n :host(:hover:not([disabled])) .base {\n background-color: color-mix(in oklab, var(--_background), currentColor 7%);\n border-color: color-mix(\n in oklab,\n currentColor calc(var(--_border-strength) + 12%),\n transparent\n );\n }\n\n .content {\n display: inline-flex;\n align-items: center;\n min-inline-size: 0;\n /* `.base` sets `line-height: 1` so the chip's height comes from its own size\n tokens rather than the document's leading. That makes the line box exactly\n one em tall, and a plain `overflow: hidden` then shears every descender\n (g, j, p, q, y) at all three sizes — measured 1px short.\n\n Fixed by widening the clip rectangle, not the box: growing the line box\n instead (`line-height: normal`) moves the text half a pixel against the ×\n and the prefix icon, since the font's ascent and descent are not\n symmetrical. `clip` keeps the geometry identical and simply paints the\n tails. The margin applies to every side — there are no per-axis longhands\n in any engine yet — but 0.2em of horizontal bleed is invisible next to a\n label that is hard-cut at the edge anyway. */\n overflow: clip;\n overflow-clip-margin: 0.2em;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n ::slotted([slot='prefix']) {\n display: inline-flex;\n flex: none;\n }\n\n /* A count or trailing glyph. It sits in its own slot so `.base`'s gap gives it\n a gutter by construction — the label is a bare text node and can never be\n targeted from the outside. Lighter weight than the medium label, and\n tabular so a column of counts stays aligned. */\n ::slotted([slot='suffix']) {\n flex: none;\n font-weight: var(--l-font-weight-normal);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n 🆂🅴🅻🅴🅲🆃🅰🅱🅻🅴 (filter chip — toggle button, or a checkbox riding inside)\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n */\n\n :host([selectable]) {\n /* Taller than a display chip at the same `size`: a chip you click is a\n target (WCAG 2.5.8), and this is the dense step between `sm` and `lg` —\n more room, same 12px label, where `lg` would jump the label to body size.\n `--height` / `--font-size` tune it from there. */\n --_height: var(--height, 1.75rem);\n\n /* The selection accent is the same one the library's checkbox, radio and\n switch use, so a chip with a checkbox inside reads as a single control. */\n --_accent: var(--l-form-control-activated-color);\n /* The label is 12px, so it owes 4.5:1 over its own tint — and the raw accent\n misses it on the light one (4.35:1 measured). Darken it there, lighten it\n on the dark tint, and both modes clear 6:1. `color-mix()` resolves\n reliably inside `light-dark()` — `oklch(from …)` does not. */\n --_selected-color: var(\n --selected-color,\n light-dark(\n color-mix(in oklab, var(--_accent), black 14%),\n color-mix(in oklab, var(--_accent), white 45%)\n )\n );\n /* Derived from the text color, so overriding `--selected-color` alone\n repaints the whole chip coherently. Kept at 10%: a consumer accent skips\n the mode correction above, so the tint has to stay pale enough for a\n semantic token (`--l-color-text-success` and friends) to clear 4.5:1 on\n it unaided. The border carries the rest of the emphasis. */\n --_selected-background: var(\n --selected-background,\n color-mix(in oklab, var(--_selected-color) 10%, var(--l-color-surface))\n );\n }\n\n :host([selectable][size='lg']) {\n --_height: var(--height, 2rem);\n }\n\n :host([selectable][selected]) {\n --_background: var(--_selected-background);\n --_color: var(--_selected-color);\n --_border-strength: 45%;\n }\n\n :host([selectable]) .base {\n cursor: pointer;\n /* A chip is tapped, not read: never leave a text selection behind a\n double-click on a filter. */\n user-select: none;\n }\n\n :host([selectable][disabled]) .base {\n cursor: not-allowed;\n }\n\n /* The chip *is* the button. Negative inline margins let it cover `.base`'s\n padding so the whole pill is the target, while `.base` keeps owning the\n border, background, and the × button's spacing math. */\n .toggle {\n flex: 1 1 auto;\n display: inline-flex;\n align-items: center;\n align-self: stretch;\n gap: var(--_gap);\n min-inline-size: 0;\n margin-inline: calc(var(--_padding-inline) * -1);\n padding-inline: var(--_padding-inline);\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n text-align: start;\n cursor: inherit;\n }\n\n /* Stop before the × instead of running under it. */\n :host([removable]) .toggle {\n margin-inline-end: 0;\n padding-inline-end: 0;\n }\n\n /* One ring for the whole chip: the inner control is a detail of how the chip\n is operated, not a second thing to focus visually — and the ring drawn\n below fully encloses the box, so the indicator only grows (WCAG 2.4.7 /\n 2.4.13).\n\n Qualified by `:host(…)` on purpose. The shared appearance nests\n `&:focus-visible` under a selector list, so it lands as\n `:is(.l-checkbox, … input[type='checkbox']:not(…)):focus-visible` — 0,3,1,\n which outranks a bare `.checkbox:focus-visible` (0,2,0) and paints a second\n ring around the box. `:host([selectable])` brings this to 0,4,0. */\n :host([selectable]) .toggle:focus-visible,\n :host([selectable]) .checkbox:focus-visible {\n outline: none;\n }\n\n .base:has(:is(.toggle, .checkbox):focus-visible) {\n outline: 2px solid var(--l-focus-ring);\n outline-offset: 2px;\n }\n\n /* The skin comes from the shared `.l-checkbox` appearance, applied through\n `cls('checkbox')` rather than a static class literal: `dist` ships that CSS\n as a real file, so a consumer's PostCSS rewrites its selector under a\n renamed `cssPrefix` — a baked `l-checkbox` in the template would stop\n matching it. This rule only carries what is specific to a chip.\n\n The shared appearance sizes the box in `em`, and a UA gives `<input>` its\n own font — inheriting the chip's makes the box track the `size` step. The\n accent follows the selected color, so a themed chip keeps its checkbox in\n the same hue. */\n .checkbox {\n --accent: var(--selected-color, var(--l-form-control-activated-color));\n /* Hover halo — same ratio and same token flip as the light-DOM\n `.l-checkbox`, which keeps this rule out of the shared partial (see\n `css/elements/checkbox.css`). Repeated here rather than moved into the\n partial: `l-tree-item` shares that file and never opted into a glow. */\n --_halo: var(--l-form-control-border-color-hover);\n\n flex: none;\n font: inherit;\n transition-property: background-color, border-color, box-shadow;\n }\n\n .checkbox:checked,\n .checkbox:indeterminate {\n --_halo: var(--accent);\n }\n\n /* Driven by the chip, not by the box: the chip is the label and the whole\n click target, so a halo lighting up only over 15px of a 28px pill would\n read as a glitch. */\n @media (hover: hover) {\n :host([selectable]:not([disabled])) .base:hover .checkbox:not(:disabled) {\n box-shadow: 0 0 0 calc(var(--size) * var(--l-form-control-toggle-halo-ratio))\n color-mix(in oklab, var(--_halo), transparent 84%);\n }\n }\n\n /* Forced colors drop the tint (backgrounds are forced to Canvas), so pin the\n border to the system highlight — otherwise selection becomes invisible. */\n @media (forced-colors: active) {\n :host([selectable][selected]) .base {\n border-color: Highlight;\n }\n }\n\n /*\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n 🆁🅴🅼🅾🆅🅴 (≥24px target-size, mdi:close mask — mirrors l-close)\n ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n */\n\n .remove {\n flex: none;\n display: grid;\n place-items: center;\n /* 24px hit target (WCAG 2.5.8); the visible tint circle is inset to ~18px. */\n inline-size: 1.5rem;\n block-size: 1.5rem;\n margin-inline: calc(var(--_gap) * -0.5) calc(var(--_padding-inline) * -0.5);\n padding: 0;\n border: 0;\n border-radius: var(--l-radius-full);\n color: inherit;\n cursor: pointer;\n /* Tint circle behind the glyph — a gradient disc kept smaller than the 24px\n hit area, grown from 0 on hover so it reads as a neat ~18px dot. */\n background: radial-gradient(\n circle at center,\n color-mix(in oklab, currentColor 15%, transparent) 60%,\n transparent 62%\n )\n no-repeat center / 0 0;\n transition:\n scale 120ms ease,\n background-size 140ms ease;\n\n &::after {\n content: '';\n inline-size: 0.875rem;\n block-size: 0.875rem;\n background-color: currentColor;\n opacity: 0.55;\n transition: opacity 120ms ease;\n /* https://pictogrammers.com/library/mdi/icon/close */\n mask: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\" /></svg>')\n center / contain no-repeat;\n }\n\n &:hover {\n background-size: 1.125rem 1.125rem;\n }\n &:hover::after {\n opacity: 1;\n }\n\n /* Tactile press — the button dips and the disc deepens. */\n &:active {\n scale: 0.86;\n background-image: radial-gradient(\n circle at center,\n color-mix(in oklab, currentColor 26%, transparent) 60%,\n transparent 62%\n );\n }\n\n &:focus-visible {\n outline: 2px solid var(--l-focus-ring);\n outline-offset: 1px;\n }\n\n &:disabled {\n cursor: not-allowed;\n opacity: 0.4;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .base,\n .checkbox,\n .remove,\n .remove::after {\n transition-duration: 0ms;\n }\n .remove:active {\n scale: 1;\n }\n }\n}\n","import { html, nothing, unsafeCSS } from 'lit';\nimport { property } from 'lit/decorators.js';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { LocalizeController } from '../../shared/localize.js';\nimport { cls } from '../../registry.js';\nimport hostStyles from '../../shared/styles/host.styles.js';\nimport checkboxAppearance from '../../shared/styles/checkbox-appearance.styles.js';\nimport rawStyles from './tag.css?inline';\n\nconst styles = unsafeCSS(rawStyles);\n\nexport type TagSize = 'sm' | 'md' | 'lg';\nexport type TagControl = 'none' | 'checkbox';\n\n/**\n * Fired when the user asks to remove the tag — clicking the × button, or\n * pressing Backspace/Delete while it is focused. Cancelable: call\n * `preventDefault()` to keep the tag (the controlling host then manages the\n * list itself); otherwise the tag removes itself from the DOM. Does not bubble.\n */\nexport class TagRemoveEvent extends Event {\n constructor() {\n super('remove', { bubbles: false, composed: false, cancelable: true });\n }\n}\n\n/** Fired when a `selectable` tag is toggled. Bubbles; not composed. */\nexport class TagChangeEvent extends Event {\n readonly selected: boolean;\n constructor(selected: boolean) {\n super('change', { bubbles: true, composed: false, cancelable: false });\n this.selected = selected;\n }\n}\n\ninterface TagEventMap {\n change: TagChangeEvent;\n}\n\ndeclare global {\n interface GlobalEventHandlersEventMap {\n remove: TagRemoveEvent;\n }\n}\n\n/**\n * A compact chip for tokens, filters, and multi-select values. Shadow DOM so its\n * styles are self-contained and it renders correctly **anywhere**, including\n * inside another element's shadow root (e.g. the `<l-select>` multi-select\n * trigger).\n *\n * Add `selectable` to turn the chip into a filter control: the tag itself\n * becomes a toggle button (`aria-pressed`), and re-activating a selected tag\n * deselects it — the affordance a radio group cannot offer, so a facet always\n * has a way back to \"all\". Add `control=\"checkbox\"` for a multi-select axis and\n * the library's own checkbox rides inside, with the chip as its label.\n *\n * @summary A compact, optionally removable or selectable chip.\n *\n * @example\n * ```html\n * <l-tag removable>Design</l-tag>\n * <l-tag selectable selected>A3</l-tag>\n * <l-tag selectable control=\"checkbox\">Color <span slot=\"suffix\">79</span></l-tag>\n * ```\n *\n * @event remove - Fired when the user removes the tag (× click or Backspace/Delete). Cancelable; if not prevented the tag removes itself. Not composed, does not bubble.\n * @event change - Fired when a `selectable` tag is toggled. Not cancelable — like the platform's own `change`, and like `l-segmented-control`. Bubbles. Properties: `selected: boolean`. The chip is uncontrolled: it applies the new state before dispatching, so a host that vetoes a toggle (a \"max 3 filters\" rule, an async guard) sets `selected` back in the listener — the revert lands in the same render and is never painted.\n *\n * @slot - The tag label.\n * @slot prefix - Leading content, e.g. an `<l-icon>` or `<l-avatar>`.\n * @slot suffix - Trailing content, e.g. a result count. Gets the chip's own gutter, so no margin is needed.\n *\n * @csspart base - The chip container.\n * @csspart content - The label wrapper.\n * @csspart toggle - The toggle button rendered by `selectable` (not with `control=\"checkbox\"`).\n * @csspart checkbox - The native checkbox rendered by `control=\"checkbox\"`.\n * @csspart remove - The remove button.\n *\n * @cssproperty [--border-radius] - Corner radius. Defaults to a full pill.\n * @cssproperty [--height] - Chip height. Defaults to the `size` step (a selectable chip is taller, to keep a comfortable target).\n * @cssproperty [--font-size] - Label size. Defaults to the `size` step — 12px, or 14px at `size=\"lg\"`. Set it with `--height` to land between the two steps in a dense filter panel.\n * @cssproperty [--padding-inline] - Horizontal padding. Defaults to the `size` step.\n * @cssproperty [--background] - Chip background.\n * @cssproperty [--color] - Text color.\n * @cssproperty [--selected-color] - Text, border, and checkbox accent when selected. Defaults to the library's form-control accent, lightened in dark mode.\n * @cssproperty [--selected-background] - Chip background when selected. Defaults to a tint of `--selected-color`.\n *\n * @customElement l-tag\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- typed addEventListener overloads merged below; no uninitialized properties.\nexport class Tag extends LuxenElement {\n static override styles = [hostStyles, checkboxAppearance, styles];\n\n private _localize = new LocalizeController(this);\n\n /** Tag size: `sm`, `md` (default), or `lg`. */\n @property({ reflect: true })\n accessor size: TagSize = 'md';\n\n /** Show a remove button (and enable Backspace/Delete removal). */\n @property({ type: Boolean, reflect: true })\n accessor removable = false;\n\n /** Make the tag a filter control the user can toggle on and off. */\n @property({ type: Boolean, reflect: true })\n accessor selectable = false;\n\n /** Whether the tag is selected. Reflected, so `[selected]` is styleable. */\n @property({ type: Boolean, reflect: true })\n accessor selected = false;\n\n /**\n * What drives the selection: `none` (default) makes the chip itself a toggle\n * button; `checkbox` renders a checkbox inside and makes the chip its label —\n * the right choice for a multi-select facet. Implies `selectable`.\n */\n @property({ reflect: true })\n accessor control: TagControl = 'none';\n\n /** Disable the tag — dims it and blocks selection and removal. */\n @property({ type: Boolean, reflect: true })\n accessor disabled = false;\n\n constructor() {\n super();\n // The chip is operated from inside the shadow root, which a click\n // dispatched on the host never reaches — see `_onHostClick`.\n this.addEventListener('click', this._onHostClick);\n }\n\n override willUpdate() {\n // A checkbox is only ever rendered on a selectable chip, so the attribute\n // (which every selectable style rule keys off) is derived rather than\n // duplicated in every selector — and in the consumer's markup.\n if (this.control === 'checkbox') this.selectable = true;\n }\n\n override updated() {\n // Re-assert the box, don't rely on the `.checked` binding. Lit dirty-checks\n // a property part against the value it last wrote, and a click has the user\n // agent flip `checked` behind its back — so when a controlling host vetoes\n // the toggle by setting `selected` back to what it already was, Lit sees no\n // change, skips the write, and leaves a checked box on an unselected chip\n // (announced \"checked\" too). Cheap: the query is null on every other path.\n const box = this.renderRoot.querySelector<HTMLInputElement>('.checkbox');\n if (box) box.checked = this.selected;\n }\n\n private _requestRemove = () => {\n if (this.disabled) return;\n const event = new TagRemoveEvent();\n this.dispatchEvent(event);\n if (!event.defaultPrevented) this.remove();\n };\n\n private _onKeyDown = (e: KeyboardEvent) => {\n if (!this.removable || this.disabled) return;\n if (e.key === 'Backspace' || e.key === 'Delete') {\n e.preventDefault();\n this._requestRemove();\n }\n };\n\n private _toggle = () => {\n if (this.disabled) return;\n // Toggling, not setting: re-activating a selected tag releases it.\n this.selected = !this.selected;\n this.dispatchEvent(new TagChangeEvent(this.selected));\n };\n\n /**\n * Events do not propagate *down* into a shadow tree, so a click dispatched on\n * the host — `tag.click()`, a test runner resolving the chip by its test id,\n * a runner computing the chip's centre (`elementFromPoint()` retargets shadow\n * content back to the host) — never reaches the toggle button or the\n * checkbox, and silently does nothing. Delegate it.\n *\n * Only when the host *is* the target: a real gesture originates at the inner\n * button, at the checkbox, or at slotted content, and is already handled\n * where it lands — so this never double-toggles. `removable` stays out of it;\n * removing is the × button's own gesture.\n */\n private _onHostClick = (e: Event) => {\n if (!this.selectable || this.disabled) return;\n if (e.composedPath()[0] !== this) return;\n // Right for `control=\"checkbox\"` too: `_toggle()` flips `selected` and\n // fires `change`, and `updated()` re-asserts `box.checked` from it.\n this._toggle();\n };\n\n private _onCheckboxChange = (e: Event) => {\n this.selected = (e.target as HTMLInputElement).checked;\n this.dispatchEvent(new TagChangeEvent(this.selected));\n };\n\n private _label() {\n return html`\n <slot name=\"prefix\"></slot>\n <span\n class=\"content\"\n part=\"content\"\n >\n <slot></slot>\n </span>\n <slot name=\"suffix\"></slot>\n `;\n }\n\n private _removeButton() {\n return this.removable\n ? html`<button\n class=\"remove\"\n part=\"remove\"\n type=\"button\"\n ?disabled=${this.disabled}\n aria-label=${this._localize.term('remove')}\n @click=${this._requestRemove}\n ></button>`\n : nothing;\n }\n\n override render() {\n if (this.control === 'checkbox') {\n return html`\n <label\n class=\"base\"\n part=\"base\"\n @keydown=${this._onKeyDown}\n >\n <input\n class=\"checkbox ${cls('checkbox')}\"\n part=\"checkbox\"\n type=\"checkbox\"\n .checked=${this.selected}\n ?disabled=${this.disabled}\n @change=${this._onCheckboxChange}\n />\n ${this._label()}${this._removeButton()}\n </label>\n `;\n }\n\n if (this.selectable) {\n return html`\n <span\n class=\"base\"\n part=\"base\"\n @keydown=${this._onKeyDown}\n >\n <button\n class=\"toggle\"\n part=\"toggle\"\n type=\"button\"\n aria-pressed=${this.selected ? 'true' : 'false'}\n ?disabled=${this.disabled}\n @click=${this._toggle}\n >\n ${this._label()}\n </button>\n ${this._removeButton()}\n </span>\n `;\n }\n\n return html`\n <span\n class=\"base\"\n part=\"base\"\n @keydown=${this._onKeyDown}\n >\n ${this._label()}${this._removeButton()}\n </span>\n `;\n }\n}\n\n// Type `addEventListener('change', …)` so listeners receive a `TagChangeEvent`\n// (with `.selected`) instead of a bare `Event`. `change` collides with lib.dom's\n// `GlobalEventHandlersEventMap`, so it can't be augmented globally — the\n// overloads are declaration-merged onto this element's interface instead. The\n// 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 Tag {\n addEventListener<K extends keyof TagEventMap>(\n type: K,\n listener: (this: Tag, ev: TagEventMap[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 TagEventMap>(\n type: K,\n listener: (this: Tag, ev: TagEventMap[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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"}
@@ -5626,6 +5626,41 @@ In dark mode, mixes the base color with black (default 15% black).
5626
5626
 
5627
5627
  & > :first-child button {
5628
5628
  padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
5629
+
5630
+ /* `isolation` keeps the pill's negative z-index inside the button: it
5631
+ paints above the button background but under the label, and can never
5632
+ slip behind an ancestor's background (a card wrapping the tabs). */
5633
+ position: relative;
5634
+ isolation: isolate;
5635
+
5636
+ /* Hover pill, detached from the track: inset on the block axis so it
5637
+ never touches the bottom border or the sliding indicator. A 4px inset
5638
+ on a 44px tab leaves a 36px pill — same height as a medium control. */
5639
+ &::after {
5640
+ content: '';
5641
+ position: absolute;
5642
+ z-index: -1;
5643
+ inset-block: var(--hover-inset, calc(var(--spacing) * 1));
5644
+ inset-inline: 0;
5645
+ border-radius: var(--l-radius-md);
5646
+ background: transparent;
5647
+ transition: background-color var(--_duration) var(--_easing);
5648
+ }
5649
+
5650
+ /* Applies to the selected tab too — the fill is click feedback, not a
5651
+ selection cue (the underline carries that), so it would read as dead
5652
+ if the active tab were the one place it stopped responding. */
5653
+ @media (hover: hover) {
5654
+ &:hover::after {
5655
+ background: var(--hover-color, var(--l-color-bg-state-hover));
5656
+ }
5657
+ }
5658
+ }
5659
+ }
5660
+
5661
+ @media (prefers-reduced-motion: reduce) {
5662
+ l-tabs[variant='line'] > :first-child button::after {
5663
+ transition-duration: 0ms;
5629
5664
  }
5630
5665
  }
5631
5666
  }
@@ -138,6 +138,41 @@
138
138
 
139
139
  & > :first-child button {
140
140
  padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
141
+
142
+ /* `isolation` keeps the pill's negative z-index inside the button: it
143
+ paints above the button background but under the label, and can never
144
+ slip behind an ancestor's background (a card wrapping the tabs). */
145
+ position: relative;
146
+ isolation: isolate;
147
+
148
+ /* Hover pill, detached from the track: inset on the block axis so it
149
+ never touches the bottom border or the sliding indicator. A 4px inset
150
+ on a 44px tab leaves a 36px pill — same height as a medium control. */
151
+ &::after {
152
+ content: '';
153
+ position: absolute;
154
+ z-index: -1;
155
+ inset-block: var(--hover-inset, calc(var(--spacing) * 1));
156
+ inset-inline: 0;
157
+ border-radius: var(--l-radius-md);
158
+ background: transparent;
159
+ transition: background-color var(--_duration) var(--_easing);
160
+ }
161
+
162
+ /* Applies to the selected tab too — the fill is click feedback, not a
163
+ selection cue (the underline carries that), so it would read as dead
164
+ if the active tab were the one place it stopped responding. */
165
+ @media (hover: hover) {
166
+ &:hover::after {
167
+ background: var(--hover-color, var(--l-color-bg-state-hover));
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ @media (prefers-reduced-motion: reduce) {
174
+ l-tabs[variant='line'] > :first-child button::after {
175
+ transition-duration: 0ms;
141
176
  }
142
177
  }
143
178
  }
@@ -138,6 +138,41 @@
138
138
 
139
139
  & > :first-child button {
140
140
  padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
141
+
142
+ /* `isolation` keeps the pill's negative z-index inside the button: it
143
+ paints above the button background but under the label, and can never
144
+ slip behind an ancestor's background (a card wrapping the tabs). */
145
+ position: relative;
146
+ isolation: isolate;
147
+
148
+ /* Hover pill, detached from the track: inset on the block axis so it
149
+ never touches the bottom border or the sliding indicator. A 4px inset
150
+ on a 44px tab leaves a 36px pill — same height as a medium control. */
151
+ &::after {
152
+ content: '';
153
+ position: absolute;
154
+ z-index: -1;
155
+ inset-block: var(--hover-inset, calc(var(--spacing) * 1));
156
+ inset-inline: 0;
157
+ border-radius: var(--l-radius-md);
158
+ background: transparent;
159
+ transition: background-color var(--_duration) var(--_easing);
160
+ }
161
+
162
+ /* Applies to the selected tab too — the fill is click feedback, not a
163
+ selection cue (the underline carries that), so it would read as dead
164
+ if the active tab were the one place it stopped responding. */
165
+ @media (hover: hover) {
166
+ &:hover::after {
167
+ background: var(--hover-color, var(--l-color-bg-state-hover));
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ @media (prefers-reduced-motion: reduce) {
174
+ l-tabs[variant='line'] > :first-child button::after {
175
+ transition-duration: 0ms;
141
176
  }
142
177
  }
143
178
  }