luxen-ui 0.15.0 → 0.16.1

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.
Files changed (30) hide show
  1. package/cdn/custom-elements.json +50 -0
  2. package/cdn/elements/disclosure/disclosure.meta.d.ts +1 -0
  3. package/cdn/elements/disclosure/disclosure.meta.d.ts.map +1 -1
  4. package/cdn/elements/tooltip/tooltip.d.ts +17 -0
  5. package/cdn/elements/tooltip/tooltip.d.ts.map +1 -1
  6. package/cdn/elements/tooltip/tooltip.js +2 -2
  7. package/cdn/elements/tooltip/tooltip.js.map +1 -1
  8. package/cdn/shared/controllers/popover.d.ts +1 -1
  9. package/cdn/shared/controllers/popover.d.ts.map +1 -1
  10. package/cdn/shared/controllers/popover.js +1 -1
  11. package/cdn/shared/controllers/popover.js.map +1 -1
  12. package/cdn/standalone.css +16 -0
  13. package/cdn/styles/elements/disclosure.css +16 -0
  14. package/dist/css/elements/disclosure.css +16 -0
  15. package/dist/custom-elements.json +50 -0
  16. package/dist/elements/disclosure/disclosure.meta.d.ts +1 -0
  17. package/dist/elements/disclosure/disclosure.meta.d.ts.map +1 -1
  18. package/dist/elements/disclosure/disclosure.meta.js +1 -0
  19. package/dist/elements/tooltip/tooltip.d.ts +17 -0
  20. package/dist/elements/tooltip/tooltip.d.ts.map +1 -1
  21. package/dist/elements/tooltip/tooltip.js +86 -9
  22. package/dist/metadata/disclosure.json +9 -0
  23. package/dist/metadata/index.json +26 -1
  24. package/dist/metadata/tooltip.json +16 -0
  25. package/dist/shared/controllers/popover.d.ts +1 -1
  26. package/dist/shared/controllers/popover.d.ts.map +1 -1
  27. package/dist/shared/controllers/popover.js +44 -8
  28. package/dist/templates/elements/disclosure.md +28 -0
  29. package/dist/templates/elements/tooltip.md +92 -0
  30. package/package.json +1 -1
@@ -2849,6 +2849,10 @@
2849
2849
  "description": "arrow | plus — Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross.",
2850
2850
  "name": "data-marker"
2851
2851
  },
2852
+ {
2853
+ "description": "start | end — Marker side. Defaults to `end`.",
2854
+ "name": "data-marker-placement"
2855
+ },
2852
2856
  {
2853
2857
  "description": "bordered — Adds border, background, and border-radius.",
2854
2858
  "name": "data-variant"
@@ -2893,6 +2897,14 @@
2893
2897
  ],
2894
2898
  "description": "Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross."
2895
2899
  },
2900
+ {
2901
+ "name": "data-marker-placement",
2902
+ "values": [
2903
+ "start",
2904
+ "end"
2905
+ ],
2906
+ "description": "Marker side. Defaults to `end`."
2907
+ },
2896
2908
  {
2897
2909
  "name": "data-variant",
2898
2910
  "description": "bordered — Adds border, background, and border-radius."
@@ -9215,6 +9227,26 @@
9215
9227
  "description": "The distance in pixels from the target element.",
9216
9228
  "attribute": "distance"
9217
9229
  },
9230
+ {
9231
+ "kind": "field",
9232
+ "name": "showDelay",
9233
+ "type": {
9234
+ "text": "number"
9235
+ },
9236
+ "default": "0",
9237
+ "description": "Milliseconds the pointer must dwell on the trigger before the tooltip\nshows. Applies to `hover` only — focus shows immediately.",
9238
+ "attribute": "show-delay"
9239
+ },
9240
+ {
9241
+ "kind": "field",
9242
+ "name": "hideDelay",
9243
+ "type": {
9244
+ "text": "number"
9245
+ },
9246
+ "default": "0",
9247
+ "description": "Milliseconds to wait after the pointer leaves the trigger (and its safe\npolygon) before hiding. Bridges a brief exit-and-return without flicker.\nApplies to `hover` only.",
9248
+ "attribute": "hide-delay"
9249
+ },
9218
9250
  {
9219
9251
  "kind": "field",
9220
9252
  "name": "open",
@@ -9288,6 +9320,24 @@
9288
9320
  "description": "The distance in pixels from the target element.",
9289
9321
  "fieldName": "distance"
9290
9322
  },
9323
+ {
9324
+ "name": "show-delay",
9325
+ "type": {
9326
+ "text": "number"
9327
+ },
9328
+ "default": "0",
9329
+ "description": "Milliseconds the pointer must dwell on the trigger before the tooltip\nshows. Applies to `hover` only — focus shows immediately.",
9330
+ "fieldName": "showDelay"
9331
+ },
9332
+ {
9333
+ "name": "hide-delay",
9334
+ "type": {
9335
+ "text": "number"
9336
+ },
9337
+ "default": "0",
9338
+ "description": "Milliseconds to wait after the pointer leaves the trigger (and its safe\npolygon) before hiding. Bridges a brief exit-and-return without flicker.\nApplies to `hover` only.",
9339
+ "fieldName": "hideDelay"
9340
+ },
9291
9341
  {
9292
9342
  "name": "open",
9293
9343
  "type": {
@@ -7,6 +7,7 @@
7
7
  * @attribute open - Native attribute — starts the disclosure expanded.
8
8
  * @attribute name - Native attribute — groups disclosures into an exclusive accordion.
9
9
  * @attribute data-marker - arrow | plus — Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross.
10
+ * @attribute data-marker-placement - start | end — Marker side. Defaults to `end`.
10
11
  * @attribute data-variant - bordered — Adds border, background, and border-radius.
11
12
  * @attribute disabled - Disables interaction (set on `<details>` or `<summary>`).
12
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"disclosure.meta.d.ts","sourceRoot":"","sources":["../../../src/html/elements/disclosure/disclosure.meta.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,qBAAa,cAAc;CAAG"}
1
+ {"version":3,"file":"disclosure.meta.d.ts","sourceRoot":"","sources":["../../../src/html/elements/disclosure/disclosure.meta.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,qBAAa,cAAc;CAAG"}
@@ -28,6 +28,19 @@ export declare class Tooltip extends LuxenElement {
28
28
  accessor placement: Placement;
29
29
  /** The distance in pixels from the target element. */
30
30
  accessor distance: number;
31
+ /**
32
+ * Milliseconds the pointer must dwell on the trigger before the tooltip
33
+ * shows. Applies to `hover` only — focus shows immediately.
34
+ */
35
+ accessor showDelay: number;
36
+ /**
37
+ * Milliseconds to wait after the pointer leaves the trigger (and its safe
38
+ * polygon) before hiding. Bridges a brief exit-and-return without flicker.
39
+ * Applies to `hover` only.
40
+ */
41
+ accessor hideDelay: number;
42
+ private _showTimer?;
43
+ private _hideTimer?;
31
44
  /** Whether or not the tooltip is visible. */
32
45
  accessor open: boolean;
33
46
  /** Hide the directional arrow. */
@@ -42,12 +55,16 @@ export declare class Tooltip extends LuxenElement {
42
55
  connectedCallback(): void;
43
56
  disconnectedCallback(): void;
44
57
  updated(changed: PropertyValues<this>): void;
58
+ /** Cancel any pending delayed show/hide (trigger gone, `for` changed, disconnect). */
59
+ private _clearTimers;
45
60
  show(): void;
46
61
  hide(): void;
47
62
  toggle(): void;
48
63
  private _handleOpenChange;
49
64
  private _onPointerEnter;
50
65
  private _onPointerLeave;
66
+ /** Hide now, or after `hide-delay` if set (cancellable by a pointer return). */
67
+ private _scheduleHide;
51
68
  private _onFocusIn;
52
69
  private _onFocusOut;
53
70
  private _onClick;
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tooltip/tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAoBlD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,OAAQ,SAAQ,YAAY;IACvC,OAAgB,MAAM,4BAAwB;IAE9C,OAAO,CAAC,UAAU,CAAuB;IAEzC,OAAO,CAAC,SAAS,CAId;IAEH,yDAAyD;IAEzD,QAAQ,CAAC,GAAG,SAAM;IAElB,8CAA8C;IAE9C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAS;IAEtC,sDAAsD;IAEtD,QAAQ,CAAC,QAAQ,SAAK;IAEtB,6CAA6C;IAE7C,QAAQ,CAAC,IAAI,UAAS;IAEtB,kCAAkC;IAElC,QAAQ,CAAC,YAAY,UAAS;IAE9B,kFAAkF;IAElF,QAAQ,CAAC,OAAO,SAAiB;IAEjC,OAAO,CAAC,WAAW;IAInB,OAAO,KAAK,QAAQ,GAQnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,CAAC,YAAY;IAKX,iBAAiB;IAMjB,oBAAoB;IAOpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAU9C,IAAI;IAIJ,IAAI;IAIJ,MAAM;YAIQ,iBAAiB;IAgC/B,OAAO,CAAC,eAAe,CAUrB;IAEF,OAAO,CAAC,eAAe,CAGrB;IAEF,OAAO,CAAC,UAAU,CAEhB;IACF,OAAO,CAAC,WAAW,CAEjB;IACF,OAAO,CAAC,QAAQ,CAEd;IAEF,OAAO,CAAC,UAAU,CAKhB;IAEF,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,uBAAuB;IAOtB,MAAM;CAoBhB"}
1
+ {"version":3,"file":"tooltip.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tooltip/tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAoBlD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,OAAQ,SAAQ,YAAY;IACvC,OAAgB,MAAM,4BAAwB;IAE9C,OAAO,CAAC,UAAU,CAAuB;IAEzC,OAAO,CAAC,SAAS,CAId;IAEH,yDAAyD;IAEzD,QAAQ,CAAC,GAAG,SAAM;IAElB,8CAA8C;IAE9C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAS;IAEtC,sDAAsD;IAEtD,QAAQ,CAAC,QAAQ,SAAK;IAEtB;;;OAGG;IAEH,QAAQ,CAAC,SAAS,SAAK;IAEvB;;;;OAIG;IAEH,QAAQ,CAAC,SAAS,SAAK;IAEvB,OAAO,CAAC,UAAU,CAAC,CAAgC;IACnD,OAAO,CAAC,UAAU,CAAC,CAAgC;IAEnD,6CAA6C;IAE7C,QAAQ,CAAC,IAAI,UAAS;IAEtB,kCAAkC;IAElC,QAAQ,CAAC,YAAY,UAAS;IAE9B,kFAAkF;IAElF,QAAQ,CAAC,OAAO,SAAiB;IAEjC,OAAO,CAAC,WAAW;IAInB,OAAO,KAAK,QAAQ,GAQnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,CAAC,YAAY;IAKX,iBAAiB;IAMjB,oBAAoB;IAQpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAY9C,sFAAsF;IACtF,OAAO,CAAC,YAAY;IAOpB,IAAI;IAIJ,IAAI;IAIJ,MAAM;YAIQ,iBAAiB;IAgC/B,OAAO,CAAC,eAAe,CAwBrB;IAEF,OAAO,CAAC,eAAe,CAarB;IAEF,gFAAgF;IAChF,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,UAAU,CAOhB;IACF,OAAO,CAAC,WAAW,CAOjB;IACF,OAAO,CAAC,QAAQ,CAEd;IAEF,OAAO,CAAC,UAAU,CAKhB;IAEF,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,uBAAuB;IAOtB,MAAM;CAoBhB"}
@@ -1,4 +1,4 @@
1
- import{uniqueId 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 s from"../../shared/styles/host.styles.js";import{PopoverController as c}from"../../shared/controllers/popover.js";var l=t(`:host{--background-color:var(--l-tooltip-background-color);--border-radius:4px;--max-width:180px;--arrow-size:6px;--show-duration:.15s;--hide-duration:.15s;display:contents}[popover]{inset:unset;box-sizing:border-box;width:max-content;max-width:var(--max-width);border-radius:var(--border-radius);background:var(--background-color);--_on:calc(255 * ( 1 - sign(.2126 * (r / 255) * (r / 255) + .7152 * (g / 255) * (g / 255) + .0722 * (b / 255) * (b / 255) - .2) ) / 2);color:var(--text-color,rgb(from var(--background-color) var(--_on) var(--_on) var(--_on)));filter:drop-shadow(0 1px 2px #00000029);pointer-events:none;border:0;padding:4px 8px;font-size:.8125rem;line-height:1.4;overflow:visible}@supports (color:contrast-color(red)){[popover]{color:var(--text-color,contrast-color(var(--background-color)))}}i{width:var(--arrow-size);height:var(--arrow-size);background:var(--background-color);display:block;position:absolute;transform:rotate(45deg)}`),u=new Set,d=null,f=class extends o{constructor(...t){super(...t),this._tooltipId=e(`tooltip`),this._floating=new c(this,{getTriggerElement:()=>this._trigger,getFloatingElement:()=>this._popover,getArrowElement:()=>this._arrowEl}),this.#e=``,this.#t=`top`,this.#n=8,this.#r=!1,this.#i=!1,this.#a=`hover focus`,this._onPointerEnter=()=>{if(this._hasTrigger(`hover`)){for(let e of u)e._floating.cleanupSafePolygon();this.show()}},this._onPointerLeave=e=>{!this._hasTrigger(`hover`)||!this.open||this._floating.handlePointerLeave(e,()=>this.hide())},this._onFocusIn=()=>{this._hasTrigger(`focus`)&&this.show()},this._onFocusOut=()=>{this._hasTrigger(`focus`)&&this.hide()},this._onClick=()=>{this._hasTrigger(`click`)&&this.toggle()},this._onKeyDown=e=>{this.open&&e.key===`Escape`&&(e.stopPropagation(),this.hide())}}static{this.styles=[s,l]}#e;get for(){return this.#e}set for(e){this.#e=e}#t;get placement(){return this.#t}set placement(e){this.#t=e}#n;get distance(){return this.#n}set distance(e){this.#n=e}#r;get open(){return this.#r}set open(e){this.#r=e}#i;get withoutArrow(){return this.#i}set withoutArrow(e){this.#i=e}#a;get trigger(){return this.#a}set trigger(e){this.#a=e}_hasTrigger(e){return this.trigger.split(` `).includes(e)}get _trigger(){if(!this.for)return null;let e=this.getRootNode();return e instanceof Document||e instanceof ShadowRoot?e.getElementById(this.for):null}get _popover(){return this.shadowRoot.querySelector(`[popover]`)}get _arrowEl(){return this.withoutArrow?null:this.shadowRoot.querySelector(`i`)}_getDuration(e){let t=parseFloat(getComputedStyle(this).getPropertyValue(e));return Number.isNaN(t)?150:t}connectedCallback(){super.connectedCallback(),u.add(this),requestAnimationFrame(()=>this._addTriggerListeners())}disconnectedCallback(){super.disconnectedCallback(),u.delete(this),d===this&&(d=null),this._removeTriggerListeners()}updated(e){e.has(`open`)&&this._handleOpenChange(),e.has(`for`)&&(this._removeTriggerListeners(e.get(`for`)),this._addTriggerListeners())}show(){this.open||=!0}hide(){this.open&&=!1}toggle(){this.open=!this.open}async _handleOpenChange(){let e=this._popover;if(!e)return;let t={placement:this.placement,distance:this.distance};if(this.open){if(this._hasTrigger(`manual`)||(d&&d!==this&&d.hide(),d=this),e.showPopover(),await this._floating.updatePosition(t),!this.open)return;await this._floating.animateShow(e,this._getDuration(`--show-duration`)),this._floating.startPositioning(t),this._trigger?.setAttribute(`aria-describedby`,this._tooltipId)}else d===this&&(d=null),this._floating.stopPositioning(),this._floating.cleanupSafePolygon(),this._trigger?.removeAttribute(`aria-describedby`),await this._floating.animateHide(e,this._getDuration(`--hide-duration`)),e.matches(`:popover-open`)&&e.hidePopover()}_addTriggerListeners(){this._floating.addTriggerListeners({onPointerEnter:this._onPointerEnter,onPointerLeave:this._onPointerLeave,onFocusIn:this._onFocusIn,onFocusOut:this._onFocusOut,onClick:this._onClick,onKeyDown:this._onKeyDown})}_removeTriggerListeners(e){let t=e?this.getRootNode().getElementById(e):void 0;this._floating.removeTriggerListeners(t)}render(){return i`
1
+ import{uniqueId 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 s from"../../shared/styles/host.styles.js";import{PopoverController as c}from"../../shared/controllers/popover.js";var l=t(`:host{--background-color:var(--l-tooltip-background-color);--border-radius:4px;--max-width:180px;--arrow-size:6px;--show-duration:.15s;--hide-duration:.15s;display:contents}[popover]{inset:unset;box-sizing:border-box;width:max-content;max-width:var(--max-width);border-radius:var(--border-radius);background:var(--background-color);--_on:calc(255 * ( 1 - sign(.2126 * (r / 255) * (r / 255) + .7152 * (g / 255) * (g / 255) + .0722 * (b / 255) * (b / 255) - .2) ) / 2);color:var(--text-color,rgb(from var(--background-color) var(--_on) var(--_on) var(--_on)));filter:drop-shadow(0 1px 2px #00000029);pointer-events:none;border:0;padding:4px 8px;font-size:.8125rem;line-height:1.4;overflow:visible}@supports (color:contrast-color(red)){[popover]{color:var(--text-color,contrast-color(var(--background-color)))}}i{width:var(--arrow-size);height:var(--arrow-size);background:var(--background-color);display:block;position:absolute;transform:rotate(45deg)}`),u=new Set,d=null,f=class extends o{constructor(...t){super(...t),this._tooltipId=e(`tooltip`),this._floating=new c(this,{getTriggerElement:()=>this._trigger,getFloatingElement:()=>this._popover,getArrowElement:()=>this._arrowEl}),this.#e=``,this.#t=`top`,this.#n=8,this.#r=0,this.#i=0,this.#a=!1,this.#o=!1,this.#s=`hover focus`,this._onPointerEnter=()=>{if(this._hasTrigger(`hover`)){this._floating.cleanupSafePolygon();for(let e of u)e===this||e._hasTrigger(`manual`)||(e._floating.cleanupSafePolygon(),e.hide());clearTimeout(this._hideTimer),this._hideTimer=void 0,this.showDelay>0&&!this.open?(clearTimeout(this._showTimer),this._showTimer=setTimeout(()=>this.show(),this.showDelay)):this.show()}},this._onPointerLeave=e=>{clearTimeout(this._showTimer),this._showTimer=void 0,!(!this._hasTrigger(`hover`)||!this.open)&&this._floating.handlePointerLeave(e,e=>{e===`settled`?this.hide():this._scheduleHide()})},this._onFocusIn=()=>{this._hasTrigger(`focus`)&&(clearTimeout(this._hideTimer),this._hideTimer=void 0,this.show())},this._onFocusOut=()=>{this._hasTrigger(`focus`)&&(clearTimeout(this._showTimer),this._showTimer=void 0,this.hide())},this._onClick=()=>{this._hasTrigger(`click`)&&this.toggle()},this._onKeyDown=e=>{this.open&&e.key===`Escape`&&(e.stopPropagation(),this.hide())}}static{this.styles=[s,l]}#e;get for(){return this.#e}set for(e){this.#e=e}#t;get placement(){return this.#t}set placement(e){this.#t=e}#n;get distance(){return this.#n}set distance(e){this.#n=e}#r;get showDelay(){return this.#r}set showDelay(e){this.#r=e}#i;get hideDelay(){return this.#i}set hideDelay(e){this.#i=e}#a;get open(){return this.#a}set open(e){this.#a=e}#o;get withoutArrow(){return this.#o}set withoutArrow(e){this.#o=e}#s;get trigger(){return this.#s}set trigger(e){this.#s=e}_hasTrigger(e){return this.trigger.split(` `).includes(e)}get _trigger(){if(!this.for)return null;let e=this.getRootNode();return e instanceof Document||e instanceof ShadowRoot?e.getElementById(this.for):null}get _popover(){return this.shadowRoot.querySelector(`[popover]`)}get _arrowEl(){return this.withoutArrow?null:this.shadowRoot.querySelector(`i`)}_getDuration(e){let t=parseFloat(getComputedStyle(this).getPropertyValue(e));return Number.isNaN(t)?150:t}connectedCallback(){super.connectedCallback(),u.add(this),requestAnimationFrame(()=>this._addTriggerListeners())}disconnectedCallback(){super.disconnectedCallback(),u.delete(this),d===this&&(d=null),this._clearTimers(),this._removeTriggerListeners()}updated(e){e.has(`open`)&&this._handleOpenChange(),e.has(`for`)&&(this._clearTimers(),this._removeTriggerListeners(e.get(`for`)),this._addTriggerListeners())}_clearTimers(){clearTimeout(this._showTimer),clearTimeout(this._hideTimer),this._showTimer=void 0,this._hideTimer=void 0}show(){this.open||=!0}hide(){this.open&&=!1}toggle(){this.open=!this.open}async _handleOpenChange(){let e=this._popover;if(!e)return;let t={placement:this.placement,distance:this.distance};if(this.open){if(this._hasTrigger(`manual`)||(d&&d!==this&&d.hide(),d=this),e.showPopover(),await this._floating.updatePosition(t),!this.open)return;await this._floating.animateShow(e,this._getDuration(`--show-duration`)),this._floating.startPositioning(t),this._trigger?.setAttribute(`aria-describedby`,this._tooltipId)}else d===this&&(d=null),this._floating.stopPositioning(),this._floating.cleanupSafePolygon(),this._trigger?.removeAttribute(`aria-describedby`),await this._floating.animateHide(e,this._getDuration(`--hide-duration`)),e.matches(`:popover-open`)&&e.hidePopover()}_scheduleHide(){this.hideDelay>0?(clearTimeout(this._hideTimer),this._hideTimer=setTimeout(()=>this.hide(),this.hideDelay)):this.hide()}_addTriggerListeners(){this._floating.addTriggerListeners({onPointerEnter:this._onPointerEnter,onPointerLeave:this._onPointerLeave,onFocusIn:this._onFocusIn,onFocusOut:this._onFocusOut,onClick:this._onClick,onKeyDown:this._onKeyDown})}_removeTriggerListeners(e){let t=e?this.getRootNode().getElementById(e):void 0;this._floating.removeTriggerListeners(t)}render(){return i`
2
2
  <div
3
3
  id=${this._tooltipId}
4
4
  popover="manual"
@@ -13,5 +13,5 @@ import{uniqueId as e}from"../../registry.js";import{i as t}from"../../chunks/lit
13
13
  `}
14
14
  <slot></slot>
15
15
  </div>
16
- `}};r([n({reflect:!0})],f.prototype,`for`,null),r([n()],f.prototype,`placement`,null),r([n({type:Number})],f.prototype,`distance`,null),r([n({type:Boolean,reflect:!0})],f.prototype,`open`,null),r([n({type:Boolean,reflect:!0,attribute:`without-arrow`})],f.prototype,`withoutArrow`,null),r([n()],f.prototype,`trigger`,null);export{f as Tooltip};
16
+ `}};r([n({reflect:!0})],f.prototype,`for`,null),r([n()],f.prototype,`placement`,null),r([n({type:Number})],f.prototype,`distance`,null),r([n({type:Number,attribute:`show-delay`})],f.prototype,`showDelay`,null),r([n({type:Number,attribute:`hide-delay`})],f.prototype,`hideDelay`,null),r([n({type:Boolean,reflect:!0})],f.prototype,`open`,null),r([n({type:Boolean,reflect:!0,attribute:`without-arrow`})],f.prototype,`withoutArrow`,null),r([n()],f.prototype,`trigger`,null);export{f as Tooltip};
17
17
  //# sourceMappingURL=tooltip.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip.js","names":[],"sources":["../../../src/html/elements/tooltip/tooltip.css?inline","../../../src/html/elements/tooltip/tooltip.ts"],"sourcesContent":[":host {\n --background-color: var(--l-tooltip-background-color);\n --border-radius: 4px;\n --max-width: 180px;\n --arrow-size: 6px;\n --show-duration: 150ms;\n --hide-duration: 150ms;\n\n display: contents;\n}\n\n[popover] {\n inset: unset;\n overflow: visible;\n box-sizing: border-box;\n width: max-content;\n max-width: var(--max-width);\n padding: 4px 8px;\n border: 0;\n border-radius: var(--border-radius);\n background: var(--background-color);\n /*\n * Text over --background-color, decided by the background's own luminance —\n * never the document mode, since --background-color is consumer-set and\n * ignores light/dark. The contrast-color() block below is the primary path\n * (Baseline 2026); this --_on calc() is the fallback for browsers predating\n * it. It approximates WCAG relative luminance with squared sRGB channels\n * (Rec. 709 weights) and flips at 0.2, just above the black/white crossover\n * (~0.179) — holding AA across saturated custom colors where an\n * OKLCH-lightness threshold collapses to ≈1:1 (OKLCH L tracks hue, not\n * luminance). --text-color lets consumers force a specific text color.\n */\n --_on: calc(\n 255 *\n (\n 1 -\n sign(\n 0.2126 * (r / 255) * (r / 255) + 0.7152 * (g / 255) * (g / 255) + 0.0722 * (b / 255) *\n (b / 255) - 0.2\n )\n ) /\n 2\n );\n color: var(--text-color, rgb(from var(--background-color) var(--_on) var(--_on) var(--_on)));\n font-size: 0.8125rem;\n line-height: 1.4;\n filter: drop-shadow(0 1px 2px rgb(0 0 0 / 16%));\n pointer-events: none;\n}\n\n/* Primary path: native, guaranteed-contrast black/white. @supports (not\n * declaration order) is required — the value contains var(), so a non-supporting\n * browser treats contrast-color() as invalid-at-computed-value-time and resets to\n * inherit rather than falling back to the calc() line. */\n@supports (color: contrast-color(red)) {\n [popover] {\n color: var(--text-color, contrast-color(var(--background-color)));\n }\n}\n\ni {\n position: absolute;\n display: block;\n width: var(--arrow-size);\n height: var(--arrow-size);\n background: var(--background-color);\n transform: rotate(45deg);\n}\n","import { html, nothing, unsafeCSS, type PropertyValues } from 'lit';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { property } from 'lit/decorators.js';\nimport type { Placement } from '@floating-ui/dom';\nimport { PopoverController } from '../../shared/controllers/popover.js';\nimport { uniqueId } from '../../registry.js';\nimport hostStyles from '../../shared/styles/host.styles.js';\nimport rawStyles from './tooltip.css?inline';\n\nconst styles = unsafeCSS(rawStyles);\n\n/**\n * Module-level registry of connected tooltips. A tooltip behaves like a label,\n * not a dialog: at most one pointer/focus/click-triggered tooltip is visible at\n * a time. Without this, on a dense grid of small triggers the safe polygon of\n * the previous tooltip overlaps the neighbouring triggers and two (or more)\n * tooltips end up visible during a horizontal sweep. `trigger=\"manual\"`\n * tooltips opt out: they never claim the active slot and are never evicted, so\n * programmatic multi-tooltip scenarios (annotations, coach marks) keep working.\n */\nconst tooltipInstances = new Set<Tooltip>();\nlet activeTooltip: Tooltip | null = null;\n\n/**\n * @summary A tooltip that displays contextual text on hover or focus.\n * @customElement l-tooltip\n *\n * @slot - Tooltip content (text or rich HTML).\n *\n * @csspart body - The tooltip popover container.\n * @csspart arrow - The directional arrow element.\n *\n * @cssproperty --background-color - Background color for this tooltip instance. Defaults to the global `--l-tooltip-background-color` token (a neutral inverse surface, dark in light mode / light in dark mode) — override that token to re-skin every tooltip at once.\n * @cssproperty --text-color - Text color. If unset, auto-derived from `--background-color` luminance.\n * @cssproperty --border-radius - Border radius. Default `4px`.\n * @cssproperty --max-width - Maximum width. Default `180px`.\n * @cssproperty --arrow-size - Arrow size. Default `6px`.\n * @cssproperty --show-duration - Show animation duration. Default `150ms`.\n * @cssproperty --hide-duration - Hide animation duration. Default `150ms`.\n */\nexport class Tooltip extends LuxenElement {\n static override styles = [hostStyles, styles];\n\n private _tooltipId = uniqueId('tooltip');\n\n private _floating = new PopoverController(this, {\n getTriggerElement: () => this._trigger,\n getFloatingElement: () => this._popover,\n getArrowElement: () => this._arrowEl,\n });\n\n /** The HTML id of the element triggering the tooltip. */\n @property({ reflect: true })\n accessor for = '';\n\n /** The preferred placement of the tooltip. */\n @property()\n accessor placement: Placement = 'top';\n\n /** The distance in pixels from the target element. */\n @property({ type: Number })\n accessor distance = 8;\n\n /** Whether or not the tooltip is visible. */\n @property({ type: Boolean, reflect: true })\n accessor open = false;\n\n /** Hide the directional arrow. */\n @property({ type: Boolean, reflect: true, attribute: 'without-arrow' })\n accessor withoutArrow = false;\n\n /** Space-separated list of trigger modes: `hover`, `focus`, `click`, `manual`. */\n @property()\n accessor trigger = 'hover focus';\n\n private _hasTrigger(type: string) {\n return this.trigger.split(' ').includes(type);\n }\n\n private get _trigger(): HTMLElement | null {\n if (!this.for) return null;\n // A detached tooltip's root is a plain element with no getElementById —\n // async open/close work (e.g. an eviction) may land after removal.\n const root = this.getRootNode();\n return root instanceof Document || root instanceof ShadowRoot\n ? root.getElementById(this.for)\n : null;\n }\n\n private get _popover(): HTMLElement {\n return this.shadowRoot!.querySelector('[popover]')!;\n }\n\n private get _arrowEl(): HTMLElement | null {\n return this.withoutArrow ? null : this.shadowRoot!.querySelector('i');\n }\n\n private _getDuration(prop: '--show-duration' | '--hide-duration'): number {\n const parsed = parseFloat(getComputedStyle(this).getPropertyValue(prop));\n return Number.isNaN(parsed) ? 150 : parsed;\n }\n\n override connectedCallback() {\n super.connectedCallback();\n tooltipInstances.add(this);\n requestAnimationFrame(() => this._addTriggerListeners());\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n tooltipInstances.delete(this);\n if (activeTooltip === this) activeTooltip = null;\n this._removeTriggerListeners();\n }\n\n override updated(changed: PropertyValues<this>) {\n if (changed.has('open')) {\n void this._handleOpenChange();\n }\n if (changed.has('for')) {\n this._removeTriggerListeners(changed.get('for') as string);\n this._addTriggerListeners();\n }\n }\n\n show() {\n if (!this.open) this.open = true;\n }\n\n hide() {\n if (this.open) this.open = false;\n }\n\n toggle() {\n this.open = !this.open;\n }\n\n private async _handleOpenChange() {\n const popover = this._popover;\n if (!popover) return;\n\n const posOpts = { placement: this.placement, distance: this.distance };\n\n if (this.open) {\n // Single-open invariant: opening a non-manual tooltip evicts the current\n // one. The evicted tooltip's hide flows back through this method, which\n // also removes its safe-polygon `pointermove` listener.\n if (!this._hasTrigger('manual')) {\n if (activeTooltip && activeTooltip !== this) activeTooltip.hide();\n activeTooltip = this;\n }\n popover.showPopover();\n await this._floating.updatePosition(posOpts);\n if (!this.open) return;\n await this._floating.animateShow(popover, this._getDuration('--show-duration'));\n this._floating.startPositioning(posOpts);\n this._trigger?.setAttribute('aria-describedby', this._tooltipId);\n } else {\n if (activeTooltip === this) activeTooltip = null;\n this._floating.stopPositioning();\n this._floating.cleanupSafePolygon();\n this._trigger?.removeAttribute('aria-describedby');\n await this._floating.animateHide(popover, this._getDuration('--hide-duration'));\n if (popover.matches(':popover-open')) popover.hidePopover();\n }\n }\n\n // --- Trigger event handlers ---\n\n private _onPointerEnter = () => {\n if (!this._hasTrigger('hover')) return;\n // The safe polygon means \"the pointer is travelling toward the bubble\" —\n // provably false once the pointer enters another tooltip's trigger, so\n // invalidate every peer's polygon (and drop its stale `pointermove`\n // listener) along with our own.\n for (const tooltip of tooltipInstances) {\n tooltip._floating.cleanupSafePolygon();\n }\n this.show();\n };\n\n private _onPointerLeave = (e: PointerEvent) => {\n if (!this._hasTrigger('hover') || !this.open) return;\n this._floating.handlePointerLeave(e, () => this.hide());\n };\n\n private _onFocusIn = () => {\n if (this._hasTrigger('focus')) this.show();\n };\n private _onFocusOut = () => {\n if (this._hasTrigger('focus')) this.hide();\n };\n private _onClick = () => {\n if (this._hasTrigger('click')) this.toggle();\n };\n\n private _onKeyDown = (e: KeyboardEvent) => {\n if (this.open && e.key === 'Escape') {\n e.stopPropagation();\n this.hide();\n }\n };\n\n private _addTriggerListeners() {\n this._floating.addTriggerListeners({\n onPointerEnter: this._onPointerEnter,\n onPointerLeave: this._onPointerLeave,\n onFocusIn: this._onFocusIn,\n onFocusOut: this._onFocusOut,\n onClick: this._onClick,\n onKeyDown: this._onKeyDown,\n });\n }\n\n private _removeTriggerListeners(forId?: string) {\n const trigger = forId\n ? (this.getRootNode() as Document | ShadowRoot).getElementById(forId)\n : undefined;\n this._floating.removeTriggerListeners(trigger);\n }\n\n override render() {\n return html`\n <div\n id=${this._tooltipId}\n popover=\"manual\"\n role=\"tooltip\"\n part=\"body\"\n >\n ${this.withoutArrow\n ? nothing\n : html`\n <i\n part=\"arrow\"\n role=\"presentation\"\n ></i>\n `}\n <slot></slot>\n </div>\n `;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tooltip.js","names":[],"sources":["../../../src/html/elements/tooltip/tooltip.css?inline","../../../src/html/elements/tooltip/tooltip.ts"],"sourcesContent":[":host {\n --background-color: var(--l-tooltip-background-color);\n --border-radius: 4px;\n --max-width: 180px;\n --arrow-size: 6px;\n --show-duration: 150ms;\n --hide-duration: 150ms;\n\n display: contents;\n}\n\n[popover] {\n inset: unset;\n overflow: visible;\n box-sizing: border-box;\n width: max-content;\n max-width: var(--max-width);\n padding: 4px 8px;\n border: 0;\n border-radius: var(--border-radius);\n background: var(--background-color);\n /*\n * Text over --background-color, decided by the background's own luminance —\n * never the document mode, since --background-color is consumer-set and\n * ignores light/dark. The contrast-color() block below is the primary path\n * (Baseline 2026); this --_on calc() is the fallback for browsers predating\n * it. It approximates WCAG relative luminance with squared sRGB channels\n * (Rec. 709 weights) and flips at 0.2, just above the black/white crossover\n * (~0.179) — holding AA across saturated custom colors where an\n * OKLCH-lightness threshold collapses to ≈1:1 (OKLCH L tracks hue, not\n * luminance). --text-color lets consumers force a specific text color.\n */\n --_on: calc(\n 255 *\n (\n 1 -\n sign(\n 0.2126 * (r / 255) * (r / 255) + 0.7152 * (g / 255) * (g / 255) + 0.0722 * (b / 255) *\n (b / 255) - 0.2\n )\n ) /\n 2\n );\n color: var(--text-color, rgb(from var(--background-color) var(--_on) var(--_on) var(--_on)));\n font-size: 0.8125rem;\n line-height: 1.4;\n filter: drop-shadow(0 1px 2px rgb(0 0 0 / 16%));\n pointer-events: none;\n}\n\n/* Primary path: native, guaranteed-contrast black/white. @supports (not\n * declaration order) is required — the value contains var(), so a non-supporting\n * browser treats contrast-color() as invalid-at-computed-value-time and resets to\n * inherit rather than falling back to the calc() line. */\n@supports (color: contrast-color(red)) {\n [popover] {\n color: var(--text-color, contrast-color(var(--background-color)));\n }\n}\n\ni {\n position: absolute;\n display: block;\n width: var(--arrow-size);\n height: var(--arrow-size);\n background: var(--background-color);\n transform: rotate(45deg);\n}\n","import { html, nothing, unsafeCSS, type PropertyValues } from 'lit';\nimport { LuxenElement } from '../../shared/luxen-element.js';\nimport { property } from 'lit/decorators.js';\nimport type { Placement } from '@floating-ui/dom';\nimport { PopoverController } from '../../shared/controllers/popover.js';\nimport { uniqueId } from '../../registry.js';\nimport hostStyles from '../../shared/styles/host.styles.js';\nimport rawStyles from './tooltip.css?inline';\n\nconst styles = unsafeCSS(rawStyles);\n\n/**\n * Module-level registry of connected tooltips. A tooltip behaves like a label,\n * not a dialog: at most one pointer/focus/click-triggered tooltip is visible at\n * a time. Without this, on a dense grid of small triggers the safe polygon of\n * the previous tooltip overlaps the neighbouring triggers and two (or more)\n * tooltips end up visible during a horizontal sweep. `trigger=\"manual\"`\n * tooltips opt out: they never claim the active slot and are never evicted, so\n * programmatic multi-tooltip scenarios (annotations, coach marks) keep working.\n */\nconst tooltipInstances = new Set<Tooltip>();\nlet activeTooltip: Tooltip | null = null;\n\n/**\n * @summary A tooltip that displays contextual text on hover or focus.\n * @customElement l-tooltip\n *\n * @slot - Tooltip content (text or rich HTML).\n *\n * @csspart body - The tooltip popover container.\n * @csspart arrow - The directional arrow element.\n *\n * @cssproperty --background-color - Background color for this tooltip instance. Defaults to the global `--l-tooltip-background-color` token (a neutral inverse surface, dark in light mode / light in dark mode) — override that token to re-skin every tooltip at once.\n * @cssproperty --text-color - Text color. If unset, auto-derived from `--background-color` luminance.\n * @cssproperty --border-radius - Border radius. Default `4px`.\n * @cssproperty --max-width - Maximum width. Default `180px`.\n * @cssproperty --arrow-size - Arrow size. Default `6px`.\n * @cssproperty --show-duration - Show animation duration. Default `150ms`.\n * @cssproperty --hide-duration - Hide animation duration. Default `150ms`.\n */\nexport class Tooltip extends LuxenElement {\n static override styles = [hostStyles, styles];\n\n private _tooltipId = uniqueId('tooltip');\n\n private _floating = new PopoverController(this, {\n getTriggerElement: () => this._trigger,\n getFloatingElement: () => this._popover,\n getArrowElement: () => this._arrowEl,\n });\n\n /** The HTML id of the element triggering the tooltip. */\n @property({ reflect: true })\n accessor for = '';\n\n /** The preferred placement of the tooltip. */\n @property()\n accessor placement: Placement = 'top';\n\n /** The distance in pixels from the target element. */\n @property({ type: Number })\n accessor distance = 8;\n\n /**\n * Milliseconds the pointer must dwell on the trigger before the tooltip\n * shows. Applies to `hover` only — focus shows immediately.\n */\n @property({ type: Number, attribute: 'show-delay' })\n accessor showDelay = 0;\n\n /**\n * Milliseconds to wait after the pointer leaves the trigger (and its safe\n * polygon) before hiding. Bridges a brief exit-and-return without flicker.\n * Applies to `hover` only.\n */\n @property({ type: Number, attribute: 'hide-delay' })\n accessor hideDelay = 0;\n\n private _showTimer?: ReturnType<typeof setTimeout>;\n private _hideTimer?: ReturnType<typeof setTimeout>;\n\n /** Whether or not the tooltip is visible. */\n @property({ type: Boolean, reflect: true })\n accessor open = false;\n\n /** Hide the directional arrow. */\n @property({ type: Boolean, reflect: true, attribute: 'without-arrow' })\n accessor withoutArrow = false;\n\n /** Space-separated list of trigger modes: `hover`, `focus`, `click`, `manual`. */\n @property()\n accessor trigger = 'hover focus';\n\n private _hasTrigger(type: string) {\n return this.trigger.split(' ').includes(type);\n }\n\n private get _trigger(): HTMLElement | null {\n if (!this.for) return null;\n // A detached tooltip's root is a plain element with no getElementById —\n // async open/close work (e.g. an eviction) may land after removal.\n const root = this.getRootNode();\n return root instanceof Document || root instanceof ShadowRoot\n ? root.getElementById(this.for)\n : null;\n }\n\n private get _popover(): HTMLElement {\n return this.shadowRoot!.querySelector('[popover]')!;\n }\n\n private get _arrowEl(): HTMLElement | null {\n return this.withoutArrow ? null : this.shadowRoot!.querySelector('i');\n }\n\n private _getDuration(prop: '--show-duration' | '--hide-duration'): number {\n const parsed = parseFloat(getComputedStyle(this).getPropertyValue(prop));\n return Number.isNaN(parsed) ? 150 : parsed;\n }\n\n override connectedCallback() {\n super.connectedCallback();\n tooltipInstances.add(this);\n requestAnimationFrame(() => this._addTriggerListeners());\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n tooltipInstances.delete(this);\n if (activeTooltip === this) activeTooltip = null;\n this._clearTimers();\n this._removeTriggerListeners();\n }\n\n override updated(changed: PropertyValues<this>) {\n if (changed.has('open')) {\n void this._handleOpenChange();\n }\n if (changed.has('for')) {\n // A queued show/hide targets the old trigger — drop it before rewiring.\n this._clearTimers();\n this._removeTriggerListeners(changed.get('for') as string);\n this._addTriggerListeners();\n }\n }\n\n /** Cancel any pending delayed show/hide (trigger gone, `for` changed, disconnect). */\n private _clearTimers() {\n clearTimeout(this._showTimer);\n clearTimeout(this._hideTimer);\n this._showTimer = undefined;\n this._hideTimer = undefined;\n }\n\n show() {\n if (!this.open) this.open = true;\n }\n\n hide() {\n if (this.open) this.open = false;\n }\n\n toggle() {\n this.open = !this.open;\n }\n\n private async _handleOpenChange() {\n const popover = this._popover;\n if (!popover) return;\n\n const posOpts = { placement: this.placement, distance: this.distance };\n\n if (this.open) {\n // Single-open invariant: opening a non-manual tooltip evicts the current\n // one. The evicted tooltip's hide flows back through this method, which\n // also removes its safe-polygon `pointermove` listener.\n if (!this._hasTrigger('manual')) {\n if (activeTooltip && activeTooltip !== this) activeTooltip.hide();\n activeTooltip = this;\n }\n popover.showPopover();\n await this._floating.updatePosition(posOpts);\n if (!this.open) return;\n await this._floating.animateShow(popover, this._getDuration('--show-duration'));\n this._floating.startPositioning(posOpts);\n this._trigger?.setAttribute('aria-describedby', this._tooltipId);\n } else {\n if (activeTooltip === this) activeTooltip = null;\n this._floating.stopPositioning();\n this._floating.cleanupSafePolygon();\n this._trigger?.removeAttribute('aria-describedby');\n await this._floating.animateHide(popover, this._getDuration('--hide-duration'));\n if (popover.matches(':popover-open')) popover.hidePopover();\n }\n }\n\n // --- Trigger event handlers ---\n\n private _onPointerEnter = () => {\n if (!this._hasTrigger('hover')) return;\n // The safe polygon means \"the pointer is travelling toward the bubble\" —\n // provably false once the pointer enters another tooltip's trigger. Drop\n // our own stale polygon, and close every peer now: invalidating a peer's\n // polygon without hiding it leaves the peer stuck open, because its only\n // remaining close path was eviction when *this* tooltip opens — and with\n // `show-delay` that can be much later, or never if the pointer moves on\n // before the dwell elapses (a fast sweep across a button group).\n this._floating.cleanupSafePolygon();\n for (const tooltip of tooltipInstances) {\n if (tooltip === this || tooltip._hasTrigger('manual')) continue;\n tooltip._floating.cleanupSafePolygon();\n tooltip.hide();\n }\n // The pointer is back on the trigger: cancel a pending hide (exit-and-return).\n clearTimeout(this._hideTimer);\n this._hideTimer = undefined;\n if (this.showDelay > 0 && !this.open) {\n clearTimeout(this._showTimer);\n this._showTimer = setTimeout(() => this.show(), this.showDelay);\n } else {\n this.show();\n }\n };\n\n private _onPointerLeave = (e: PointerEvent) => {\n // Cancel a still-pending show — the pointer left before the dwell elapsed.\n clearTimeout(this._showTimer);\n this._showTimer = undefined;\n if (!this._hasTrigger('hover') || !this.open) return;\n // `exit` — the pointer actively moved off the trigger/bubble: honour\n // `hide-delay` so a brief exit-and-return doesn't flicker. `settled` — the\n // pointer left and sat motionless in the corridor: there is no in-flight\n // gesture to bridge, so hide at once (never stack the delay on top).\n this._floating.handlePointerLeave(e, (reason) => {\n if (reason === 'settled') this.hide();\n else this._scheduleHide();\n });\n };\n\n /** Hide now, or after `hide-delay` if set (cancellable by a pointer return). */\n private _scheduleHide() {\n if (this.hideDelay > 0) {\n clearTimeout(this._hideTimer);\n this._hideTimer = setTimeout(() => this.hide(), this.hideDelay);\n } else {\n this.hide();\n }\n }\n\n private _onFocusIn = () => {\n // Focus is a discrete affordance — show immediately, ignoring the dwell delay.\n if (this._hasTrigger('focus')) {\n clearTimeout(this._hideTimer);\n this._hideTimer = undefined;\n this.show();\n }\n };\n private _onFocusOut = () => {\n if (this._hasTrigger('focus')) {\n // A hover may have queued a show; blur must not resurrect it.\n clearTimeout(this._showTimer);\n this._showTimer = undefined;\n this.hide();\n }\n };\n private _onClick = () => {\n if (this._hasTrigger('click')) this.toggle();\n };\n\n private _onKeyDown = (e: KeyboardEvent) => {\n if (this.open && e.key === 'Escape') {\n e.stopPropagation();\n this.hide();\n }\n };\n\n private _addTriggerListeners() {\n this._floating.addTriggerListeners({\n onPointerEnter: this._onPointerEnter,\n onPointerLeave: this._onPointerLeave,\n onFocusIn: this._onFocusIn,\n onFocusOut: this._onFocusOut,\n onClick: this._onClick,\n onKeyDown: this._onKeyDown,\n });\n }\n\n private _removeTriggerListeners(forId?: string) {\n const trigger = forId\n ? (this.getRootNode() as Document | ShadowRoot).getElementById(forId)\n : undefined;\n this._floating.removeTriggerListeners(trigger);\n }\n\n override render() {\n return html`\n <div\n id=${this._tooltipId}\n popover=\"manual\"\n role=\"tooltip\"\n part=\"body\"\n >\n ${this.withoutArrow\n ? nothing\n : html`\n <i\n part=\"arrow\"\n role=\"presentation\"\n ></i>\n `}\n <slot></slot>\n </div>\n `;\n }\n}\n"],"mappings":"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"}
@@ -38,7 +38,7 @@ export declare class PopoverController implements ReactiveController {
38
38
  updatePosition(options: PositionOptions): Promise<void>;
39
39
  startPositioning(options: PositionOptions): void;
40
40
  stopPositioning(): void;
41
- handlePointerLeave(e: PointerEvent, onHide: () => void): void;
41
+ handlePointerLeave(e: PointerEvent, onHide: (reason: 'exit' | 'settled') => void): void;
42
42
  cleanupSafePolygon(): void;
43
43
  addTriggerListeners(handlers: TriggerHandlers): void;
44
44
  removeTriggerListeners(triggerOverride?: HTMLElement | null): void;
@@ -1 +1 @@
1
- {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["../../../src/html/shared/controllers/popover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,KAAK,CAAC;AACtE,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAoN1B,MAAM,WAAW,uBAAuB;IACtC,iBAAiB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC5C,kBAAkB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC7C,eAAe,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,KAAK,eAAe,GAAG;IACrB,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,qBAAa,iBAAkB,YAAW,kBAAkB;IAC1D,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,kBAAkB,CAAC,CAAa;IACxC,OAAO,CAAC,mBAAmB,CAAC,CAAa;IACzC,OAAO,CAAC,SAAS,CAAC,CAAkB;IACpC,OAAO,CAAC,eAAe,CAAC,CAAqB;IAC7C,OAAO,CAAC,iBAAiB,CAAM;gBAEnB,IAAI,EAAE,sBAAsB,GAAG,WAAW,EAAE,MAAM,EAAE,uBAAuB;IAMvF,aAAa;IAEb,gBAAgB;IAMhB,IAAI,gBAAgB,WAEnB;IAIK,cAAc,CAAC,OAAO,EAAE,eAAe;IAuD7C,gBAAgB,CAAC,OAAO,EAAE,eAAe;IASzC,eAAe;IAOf,kBAAkB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,IAAI;IAiDtD,kBAAkB;IAOlB,mBAAmB,CAAC,QAAQ,EAAE,eAAe;IAoB7C,sBAAsB,CAAC,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI;IAyBrD,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;IAYrE,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;CAW5E"}
1
+ {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["../../../src/html/shared/controllers/popover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,KAAK,CAAC;AACtE,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AA+N1B,MAAM,WAAW,uBAAuB;IACtC,iBAAiB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC5C,kBAAkB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC7C,eAAe,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,KAAK,eAAe,GAAG;IACrB,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,qBAAa,iBAAkB,YAAW,kBAAkB;IAC1D,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,kBAAkB,CAAC,CAAa;IACxC,OAAO,CAAC,mBAAmB,CAAC,CAAa;IACzC,OAAO,CAAC,SAAS,CAAC,CAAkB;IACpC,OAAO,CAAC,eAAe,CAAC,CAAqB;IAC7C,OAAO,CAAC,iBAAiB,CAAM;gBAEnB,IAAI,EAAE,sBAAsB,GAAG,WAAW,EAAE,MAAM,EAAE,uBAAuB;IAMvF,aAAa;IAEb,gBAAgB;IAMhB,IAAI,gBAAgB,WAEnB;IAIK,cAAc,CAAC,OAAO,EAAE,eAAe;IAuD7C,gBAAgB,CAAC,OAAO,EAAE,eAAe;IASzC,eAAe;IAOf,kBAAkB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI;IAkFhF,kBAAkB;IAOlB,mBAAmB,CAAC,QAAQ,EAAE,eAAe;IAoB7C,sBAAsB,CAAC,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI;IAyBrD,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;IAYrE,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;CAW5E"}
@@ -1,2 +1,2 @@
1
- import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"../../chunks/floating-ui.dom.js";function s([e,t],n){let r=!1;for(let i=0,a=n.length-1;i<n.length;a=i++){let[o,s]=n[i],[c,l]=n[a];s>=t!=l>=t&&e<=(c-o)*(t-s)/(l-s)+o&&(r=!r)}return r}function c([e,t],n){return e>=n.left&&e<=n.right&&t>=n.top&&t<=n.bottom}function l(e,t,n,r,i){let a=i.split(`-`)[0],o=n.width>r.width,s=n.height>r.height,c=e>n.right-n.width/2,l=t>n.bottom-n.height/2;switch(a){case`top`:return[[o?e+2/2:c?e+8:e-8,t+2+1],[o?e-2/2:c?e+8:e-8,t+2+1],[n.left,c||o?n.bottom-2:n.top],[n.right,c?o?n.bottom-2:n.top:n.bottom-2]];case`bottom`:return[[o?e+2/2:c?e+8:e-8,t-2],[o?e-2/2:c?e+8:e-8,t-2],[n.left,c||o?n.top+2:n.bottom],[n.right,c?o?n.top+2:n.bottom:n.top+2]];case`left`:{let r=[e+2+1,s?t+2/2:l?t+8:t-8],i=[e+2+1,s?t-2/2:l?t+8:t-8];return[[l||s?n.right-2:n.left,n.top],[l?s?n.right-2:n.left:n.right-2,n.bottom],r,i]}case`right`:{let r=[e-2,s?t+2/2:l?t+8:t-8],i=[e-2,s?t-2/2:l?t+8:t-8];return[[l||s?n.left+2:n.right,n.top],[l?s?n.left+2:n.right:n.left+2,n.bottom],r,i]}default:return[]}}var u={top:`bottom`,right:`left`,bottom:`top`,left:`right`},d=typeof window<`u`?window.matchMedia(`(prefers-reduced-motion: reduce)`):void 0,f=class{constructor(e,t){this._currentPlacement=``,this._host=e,this._config=t,e.addController(this)}hostConnected(){}hostDisconnected(){this.stopPositioning(),this.cleanupSafePolygon(),this.removeTriggerListeners()}get currentPlacement(){return this._currentPlacement}async updatePosition(n){let s=this._config.getTriggerElement(),c=this._config.getFloatingElement(),l=this._config.getArrowElement();if(!s||!c)return;let d=[e({mainAxis:n.distance,crossAxis:n.skidding??0}),...n.minWidth===`trigger`?[a({apply({rects:e,elements:t}){t.floating.style.minWidth=`${e.reference.width}px`}})]:[],t(),r(),...l?[o({element:l,padding:8})]:[]],{x:f,y:p,placement:m,middlewareData:h}=await i(s,c,{placement:n.placement,strategy:`fixed`,middleware:d});if(Object.assign(c.style,{position:`fixed`,left:n.fullWidth?`0px`:`${f}px`,top:`${p}px`,width:n.fullWidth?`100vw`:``}),n.minWidth!==`trigger`&&(c.style.minWidth=``),m!==this._currentPlacement&&(this._currentPlacement=m,this._config.onPlacementChange?.(m)),h.arrow&&l){let{x:e,y:t}=h.arrow,n=u[m.split(`-`)[0]];Object.assign(l.style,{left:e==null?``:`${e}px`,top:t==null?``:`${t}px`,[n]:`${-l.offsetWidth/2}px`})}}startPositioning(e){let t=this._config.getTriggerElement(),r=this._config.getFloatingElement();!t||!r||(this._cleanupAutoUpdate?.(),this._cleanupAutoUpdate=n(t,r,()=>this.updatePosition(e)))}stopPositioning(){this._cleanupAutoUpdate?.(),this._cleanupAutoUpdate=void 0}handlePointerLeave(e,t){let n=this._config.getFloatingElement(),r=this._config.getTriggerElement();if(!n||!r){t();return}let i=e.clientX,a=e.clientY,o=e=>{let{clientX:o,clientY:d}=e,f=[o,d],p=n.getBoundingClientRect(),m=r.getBoundingClientRect(),h=this._currentPlacement.split(`-`)[0];if(!c(f,p)&&!c(f,m)){if(h===`top`&&a>=m.bottom-1||h===`bottom`&&a<=m.top+1||h===`left`&&i>=m.right-1||h===`right`&&i<=m.left+1){u(),t();return}s(f,l(i,a,p,m,this._currentPlacement))||(u(),t())}},u=()=>{document.removeEventListener(`pointermove`,o)};document.addEventListener(`pointermove`,o),this._cleanupSafePolygon?.(),this._cleanupSafePolygon=u}cleanupSafePolygon(){this._cleanupSafePolygon?.(),this._cleanupSafePolygon=void 0}addTriggerListeners(e){let t=this._config.getTriggerElement();t&&(this._handlers=e,this._triggerElement=t,e.onPointerEnter&&t.addEventListener(`pointerenter`,e.onPointerEnter),e.onPointerLeave&&t.addEventListener(`pointerleave`,e.onPointerLeave),e.onFocusIn&&t.addEventListener(`focusin`,e.onFocusIn),e.onFocusOut&&t.addEventListener(`focusout`,e.onFocusOut),e.onClick&&t.addEventListener(`click`,e.onClick),e.onKeyDown&&t.addEventListener(`keydown`,e.onKeyDown))}removeTriggerListeners(e){let t=e??this._triggerElement,n=this._handlers;!t||!n||(n.onPointerEnter&&t.removeEventListener(`pointerenter`,n.onPointerEnter),n.onPointerLeave&&t.removeEventListener(`pointerleave`,n.onPointerLeave),n.onFocusIn&&t.removeEventListener(`focusin`,n.onFocusIn),n.onFocusOut&&t.removeEventListener(`focusout`,n.onFocusOut),n.onClick&&t.removeEventListener(`click`,n.onClick),n.onKeyDown&&t.removeEventListener(`keydown`,n.onKeyDown),e||(this._handlers=void 0,this._triggerElement=void 0))}async animateShow(e,t,n){let r=d?.matches?0:t;e.getAnimations().forEach(e=>e.cancel()),e.animate(n??[{opacity:0,scale:.96},{opacity:1,scale:1}],{duration:r,fill:`forwards`})}async animateHide(e,t,n){let r=d?.matches?0:t;await e.animate(n??[{opacity:1,scale:1},{opacity:0,scale:.96}],{duration:r,fill:`forwards`}).finished}};export{f as PopoverController};
1
+ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"../../chunks/floating-ui.dom.js";var s=200;function c([e,t],n){let r=!1;for(let i=0,a=n.length-1;i<n.length;a=i++){let[o,s]=n[i],[c,l]=n[a];s>=t!=l>=t&&e<=(c-o)*(t-s)/(l-s)+o&&(r=!r)}return r}function l([e,t],n){return e>=n.left&&e<=n.right&&t>=n.top&&t<=n.bottom}function u(e,t,n,r,i){let a=i.split(`-`)[0],o=n.width>r.width,s=n.height>r.height,c=e>n.right-n.width/2,l=t>n.bottom-n.height/2;switch(a){case`top`:return[[o?e+2/2:c?e+8:e-8,t+2+1],[o?e-2/2:c?e+8:e-8,t+2+1],[n.left,c||o?n.bottom-2:n.top],[n.right,c?o?n.bottom-2:n.top:n.bottom-2]];case`bottom`:return[[o?e+2/2:c?e+8:e-8,t-2],[o?e-2/2:c?e+8:e-8,t-2],[n.left,c||o?n.top+2:n.bottom],[n.right,c?o?n.top+2:n.bottom:n.top+2]];case`left`:{let r=[e+2+1,s?t+2/2:l?t+8:t-8],i=[e+2+1,s?t-2/2:l?t+8:t-8];return[[l||s?n.right-2:n.left,n.top],[l?s?n.right-2:n.left:n.right-2,n.bottom],r,i]}case`right`:{let r=[e-2,s?t+2/2:l?t+8:t-8],i=[e-2,s?t-2/2:l?t+8:t-8];return[[l||s?n.left+2:n.right,n.top],[l?s?n.left+2:n.right:n.left+2,n.bottom],r,i]}default:return[]}}var d={top:`bottom`,right:`left`,bottom:`top`,left:`right`},f=typeof window<`u`?window.matchMedia(`(prefers-reduced-motion: reduce)`):void 0,p=class{constructor(e,t){this._currentPlacement=``,this._host=e,this._config=t,e.addController(this)}hostConnected(){}hostDisconnected(){this.stopPositioning(),this.cleanupSafePolygon(),this.removeTriggerListeners()}get currentPlacement(){return this._currentPlacement}async updatePosition(n){let s=this._config.getTriggerElement(),c=this._config.getFloatingElement(),l=this._config.getArrowElement();if(!s||!c)return;let u=[e({mainAxis:n.distance,crossAxis:n.skidding??0}),...n.minWidth===`trigger`?[a({apply({rects:e,elements:t}){t.floating.style.minWidth=`${e.reference.width}px`}})]:[],t(),r(),...l?[o({element:l,padding:8})]:[]],{x:f,y:p,placement:m,middlewareData:h}=await i(s,c,{placement:n.placement,strategy:`fixed`,middleware:u});if(Object.assign(c.style,{position:`fixed`,left:n.fullWidth?`0px`:`${f}px`,top:`${p}px`,width:n.fullWidth?`100vw`:``}),n.minWidth!==`trigger`&&(c.style.minWidth=``),m!==this._currentPlacement&&(this._currentPlacement=m,this._config.onPlacementChange?.(m)),h.arrow&&l){let{x:e,y:t}=h.arrow,n=d[m.split(`-`)[0]];Object.assign(l.style,{left:e==null?``:`${e}px`,top:t==null?``:`${t}px`,[n]:`${-l.offsetWidth/2}px`})}}startPositioning(e){let t=this._config.getTriggerElement(),r=this._config.getFloatingElement();!t||!r||(this._cleanupAutoUpdate?.(),this._cleanupAutoUpdate=n(t,r,()=>this.updatePosition(e)))}stopPositioning(){this._cleanupAutoUpdate?.(),this._cleanupAutoUpdate=void 0}handlePointerLeave(e,t){let n=this._config.getFloatingElement(),r=this._config.getTriggerElement();if(!n||!r){t(`exit`);return}let i=e.clientX,a=e.clientY,o,d=e=>{h(),t(e)},f=()=>clearTimeout(o),p=()=>{clearTimeout(o),o=setTimeout(()=>d(`settled`),s)},m=e=>{let{clientX:t,clientY:o}=e,s=[t,o],m=n.getBoundingClientRect(),h=r.getBoundingClientRect(),g=this._currentPlacement.split(`-`)[0];if(l(s,m)){f();return}if(l(s,h)){f();return}if(g===`top`&&a>=h.bottom-1||g===`bottom`&&a<=h.top+1||g===`left`&&i>=h.right-1||g===`right`&&i<=h.left+1){d(`exit`);return}if(c(s,u(i,a,m,h,this._currentPlacement))){p();return}d(`exit`)},h=()=>{clearTimeout(o),document.removeEventListener(`pointermove`,m)};document.addEventListener(`pointermove`,m),p(),this._cleanupSafePolygon?.(),this._cleanupSafePolygon=h}cleanupSafePolygon(){this._cleanupSafePolygon?.(),this._cleanupSafePolygon=void 0}addTriggerListeners(e){let t=this._config.getTriggerElement();t&&(this._handlers=e,this._triggerElement=t,e.onPointerEnter&&t.addEventListener(`pointerenter`,e.onPointerEnter),e.onPointerLeave&&t.addEventListener(`pointerleave`,e.onPointerLeave),e.onFocusIn&&t.addEventListener(`focusin`,e.onFocusIn),e.onFocusOut&&t.addEventListener(`focusout`,e.onFocusOut),e.onClick&&t.addEventListener(`click`,e.onClick),e.onKeyDown&&t.addEventListener(`keydown`,e.onKeyDown))}removeTriggerListeners(e){let t=e??this._triggerElement,n=this._handlers;!t||!n||(n.onPointerEnter&&t.removeEventListener(`pointerenter`,n.onPointerEnter),n.onPointerLeave&&t.removeEventListener(`pointerleave`,n.onPointerLeave),n.onFocusIn&&t.removeEventListener(`focusin`,n.onFocusIn),n.onFocusOut&&t.removeEventListener(`focusout`,n.onFocusOut),n.onClick&&t.removeEventListener(`click`,n.onClick),n.onKeyDown&&t.removeEventListener(`keydown`,n.onKeyDown),e||(this._handlers=void 0,this._triggerElement=void 0))}async animateShow(e,t,n){let r=f?.matches?0:t;e.getAnimations().forEach(e=>e.cancel()),e.animate(n??[{opacity:0,scale:.96},{opacity:1,scale:1}],{duration:r,fill:`forwards`})}async animateHide(e,t,n){let r=f?.matches?0:t;await e.animate(n??[{opacity:1,scale:1},{opacity:0,scale:.96}],{duration:r,fill:`forwards`}).finished}};export{p as PopoverController};
2
2
  //# sourceMappingURL=popover.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"popover.js","names":[],"sources":["../../../src/html/shared/controllers/popover.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport {\n computePosition,\n autoUpdate,\n offset,\n flip,\n shift,\n arrow,\n size,\n type Placement,\n} from '@floating-ui/dom';\n\n// --- Pure geometry helpers ---\n\ntype Point = [number, number];\n\n/** Check if a point is inside a polygon using ray-casting. */\nfunction isPointInPolygon([px, py]: Point, polygon: Point[]) {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const [xi, yi] = polygon[i];\n const [xj, yj] = polygon[j];\n if (yi >= py !== yj >= py && px <= ((xj - xi) * (py - yi)) / (yj - yi) + xi) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/** Check if a point is inside a rect. */\nfunction isPointInRect([px, py]: Point, rect: DOMRect) {\n return px >= rect.left && px <= rect.right && py >= rect.top && py <= rect.bottom;\n}\n\n/**\n * Build a safe polygon (triangle/quadrilateral) from cursor to floating element edges.\n * Adapted from floating-ui/safePolygon.\n * @see https://github.com/floating-ui/floating-ui/blob/master/packages/react/src/safePolygon.ts\n */\nfunction getSafePolygon(\n cursorX: number,\n cursorY: number,\n rect: DOMRect,\n refRect: DOMRect,\n placement: string,\n): Point[] {\n const side = placement.split('-')[0] as 'top' | 'bottom' | 'left' | 'right';\n const buffer = 2;\n\n const isFloatingWider = rect.width > refRect.width;\n const isFloatingTaller = rect.height > refRect.height;\n\n const cursorLeaveFromRight = cursorX > rect.right - rect.width / 2;\n const cursorLeaveFromBottom = cursorY > rect.bottom - rect.height / 2;\n\n switch (side) {\n case 'top': {\n const c1: Point = [\n isFloatingWider\n ? cursorX + buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY + buffer + 1,\n ];\n const c2: Point = [\n isFloatingWider\n ? cursorX - buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY + buffer + 1,\n ];\n return [\n c1,\n c2,\n [\n rect.left,\n cursorLeaveFromRight\n ? rect.bottom - buffer\n : isFloatingWider\n ? rect.bottom - buffer\n : rect.top,\n ],\n [\n rect.right,\n cursorLeaveFromRight\n ? isFloatingWider\n ? rect.bottom - buffer\n : rect.top\n : rect.bottom - buffer,\n ],\n ];\n }\n case 'bottom': {\n const c1: Point = [\n isFloatingWider\n ? cursorX + buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY - buffer,\n ];\n const c2: Point = [\n isFloatingWider\n ? cursorX - buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY - buffer,\n ];\n return [\n c1,\n c2,\n [\n rect.left,\n cursorLeaveFromRight\n ? rect.top + buffer\n : isFloatingWider\n ? rect.top + buffer\n : rect.bottom,\n ],\n [\n rect.right,\n cursorLeaveFromRight\n ? isFloatingWider\n ? rect.top + buffer\n : rect.bottom\n : rect.top + buffer,\n ],\n ];\n }\n case 'left': {\n const c1: Point = [\n cursorX + buffer + 1,\n isFloatingTaller\n ? cursorY + buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n const c2: Point = [\n cursorX + buffer + 1,\n isFloatingTaller\n ? cursorY - buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n return [\n [\n cursorLeaveFromBottom\n ? rect.right - buffer\n : isFloatingTaller\n ? rect.right - buffer\n : rect.left,\n rect.top,\n ],\n [\n cursorLeaveFromBottom\n ? isFloatingTaller\n ? rect.right - buffer\n : rect.left\n : rect.right - buffer,\n rect.bottom,\n ],\n c1,\n c2,\n ];\n }\n case 'right': {\n const c1: Point = [\n cursorX - buffer,\n isFloatingTaller\n ? cursorY + buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n const c2: Point = [\n cursorX - buffer,\n isFloatingTaller\n ? cursorY - buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n return [\n [\n cursorLeaveFromBottom\n ? rect.left + buffer\n : isFloatingTaller\n ? rect.left + buffer\n : rect.right,\n rect.top,\n ],\n [\n cursorLeaveFromBottom\n ? isFloatingTaller\n ? rect.left + buffer\n : rect.right\n : rect.left + buffer,\n rect.bottom,\n ],\n c1,\n c2,\n ];\n }\n default:\n return [];\n }\n}\n\n// --- Constants ---\n\nconst STATIC_SIDE = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' } as const;\n\nconst reducedMotion =\n typeof window !== 'undefined' ? window.matchMedia('(prefers-reduced-motion: reduce)') : undefined;\n\n// --- Controller ---\n\nexport interface PopoverControllerConfig {\n getTriggerElement: () => HTMLElement | null;\n getFloatingElement: () => HTMLElement | null;\n getArrowElement: () => HTMLElement | null;\n onPlacementChange?: (placement: string) => void;\n}\n\nexport interface PositionOptions {\n placement: Placement;\n distance: number;\n /** Offset along the cross axis, in pixels (e.g. to align a submenu's first item with its trigger). */\n skidding?: number;\n fullWidth?: boolean;\n /** Floor the floating element's width at the trigger's width (`'trigger'`). */\n minWidth?: 'trigger';\n}\n\ntype TriggerHandlers = {\n onPointerEnter?: (e: PointerEvent) => void;\n onPointerLeave?: (e: PointerEvent) => void;\n onFocusIn?: (e: FocusEvent) => void;\n onFocusOut?: (e: FocusEvent) => void;\n onClick?: (e: MouseEvent) => void;\n onKeyDown?: (e: KeyboardEvent) => void;\n};\n\nexport class PopoverController implements ReactiveController {\n private _host: ReactiveControllerHost & HTMLElement;\n private _config: PopoverControllerConfig;\n private _cleanupAutoUpdate?: () => void;\n private _cleanupSafePolygon?: () => void;\n private _handlers?: TriggerHandlers;\n private _triggerElement?: HTMLElement | null;\n private _currentPlacement = '';\n\n constructor(host: ReactiveControllerHost & HTMLElement, config: PopoverControllerConfig) {\n this._host = host;\n this._config = config;\n host.addController(this);\n }\n\n hostConnected() {}\n\n hostDisconnected() {\n this.stopPositioning();\n this.cleanupSafePolygon();\n this.removeTriggerListeners();\n }\n\n get currentPlacement() {\n return this._currentPlacement;\n }\n\n // --- Positioning ---\n\n async updatePosition(options: PositionOptions) {\n const trigger = this._config.getTriggerElement();\n const floating = this._config.getFloatingElement();\n const arrowEl = this._config.getArrowElement();\n if (!trigger || !floating) return;\n\n const middleware = [\n offset({ mainAxis: options.distance, crossAxis: options.skidding ?? 0 }),\n // Size before flip/shift so the adopted floor feeds collision detection.\n ...(options.minWidth === 'trigger'\n ? [\n size({\n apply({ rects, elements }) {\n elements.floating.style.minWidth = `${rects.reference.width}px`;\n },\n }),\n ]\n : []),\n flip(),\n shift(),\n ...(arrowEl ? [arrow({ element: arrowEl, padding: 8 })] : []),\n ];\n\n const { x, y, placement, middlewareData } = await computePosition(trigger, floating, {\n placement: options.placement,\n strategy: 'fixed',\n middleware,\n });\n\n Object.assign(floating.style, {\n position: 'fixed',\n left: options.fullWidth ? '0px' : `${x}px`,\n top: `${y}px`,\n width: options.fullWidth ? '100vw' : '',\n });\n\n // The `size` middleware (above) sets minWidth when syncing; clear it otherwise.\n if (options.minWidth !== 'trigger') floating.style.minWidth = '';\n\n if (placement !== this._currentPlacement) {\n this._currentPlacement = placement;\n this._config.onPlacementChange?.(placement);\n }\n\n if (middlewareData.arrow && arrowEl) {\n const { x: ax, y: ay } = middlewareData.arrow;\n const side = STATIC_SIDE[placement.split('-')[0] as keyof typeof STATIC_SIDE];\n Object.assign(arrowEl.style, {\n left: ax != null ? `${ax}px` : '',\n top: ay != null ? `${ay}px` : '',\n [side]: `${-arrowEl.offsetWidth / 2}px`,\n });\n }\n }\n\n startPositioning(options: PositionOptions) {\n const trigger = this._config.getTriggerElement();\n const floating = this._config.getFloatingElement();\n if (!trigger || !floating) return;\n\n this._cleanupAutoUpdate?.();\n this._cleanupAutoUpdate = autoUpdate(trigger, floating, () => this.updatePosition(options));\n }\n\n stopPositioning() {\n this._cleanupAutoUpdate?.();\n this._cleanupAutoUpdate = undefined;\n }\n\n // --- Safe polygon ---\n\n handlePointerLeave(e: PointerEvent, onHide: () => void) {\n const floating = this._config.getFloatingElement();\n const trigger = this._config.getTriggerElement();\n if (!floating || !trigger) {\n onHide();\n return;\n }\n\n const cursorX = e.clientX;\n const cursorY = e.clientY;\n\n const onMove = (moveEvent: PointerEvent) => {\n const { clientX: mx, clientY: my } = moveEvent;\n const cursor: Point = [mx, my];\n const rect = floating.getBoundingClientRect();\n const refRect = trigger.getBoundingClientRect();\n const side = this._currentPlacement.split('-')[0];\n\n if (isPointInRect(cursor, rect)) return;\n if (isPointInRect(cursor, refRect)) return;\n\n if (\n (side === 'top' && cursorY >= refRect.bottom - 1) ||\n (side === 'bottom' && cursorY <= refRect.top + 1) ||\n (side === 'left' && cursorX >= refRect.right - 1) ||\n (side === 'right' && cursorX <= refRect.left + 1)\n ) {\n cleanup();\n onHide();\n return;\n }\n\n const polygon = getSafePolygon(cursorX, cursorY, rect, refRect, this._currentPlacement);\n if (isPointInPolygon(cursor, polygon)) return;\n\n cleanup();\n onHide();\n };\n\n const cleanup = () => {\n document.removeEventListener('pointermove', onMove);\n };\n\n document.addEventListener('pointermove', onMove);\n\n this._cleanupSafePolygon?.();\n this._cleanupSafePolygon = cleanup;\n }\n\n cleanupSafePolygon() {\n this._cleanupSafePolygon?.();\n this._cleanupSafePolygon = undefined;\n }\n\n // --- Trigger listener management ---\n\n addTriggerListeners(handlers: TriggerHandlers) {\n const trigger = this._config.getTriggerElement();\n if (!trigger) return;\n\n this._handlers = handlers;\n this._triggerElement = trigger;\n\n if (handlers.onPointerEnter)\n trigger.addEventListener('pointerenter', handlers.onPointerEnter as EventListener);\n if (handlers.onPointerLeave)\n trigger.addEventListener('pointerleave', handlers.onPointerLeave as EventListener);\n if (handlers.onFocusIn)\n trigger.addEventListener('focusin', handlers.onFocusIn as EventListener);\n if (handlers.onFocusOut)\n trigger.addEventListener('focusout', handlers.onFocusOut as EventListener);\n if (handlers.onClick) trigger.addEventListener('click', handlers.onClick as EventListener);\n if (handlers.onKeyDown)\n trigger.addEventListener('keydown', handlers.onKeyDown as EventListener);\n }\n\n removeTriggerListeners(triggerOverride?: HTMLElement | null) {\n const trigger = triggerOverride ?? this._triggerElement;\n const handlers = this._handlers;\n if (!trigger || !handlers) return;\n\n if (handlers.onPointerEnter)\n trigger.removeEventListener('pointerenter', handlers.onPointerEnter as EventListener);\n if (handlers.onPointerLeave)\n trigger.removeEventListener('pointerleave', handlers.onPointerLeave as EventListener);\n if (handlers.onFocusIn)\n trigger.removeEventListener('focusin', handlers.onFocusIn as EventListener);\n if (handlers.onFocusOut)\n trigger.removeEventListener('focusout', handlers.onFocusOut as EventListener);\n if (handlers.onClick) trigger.removeEventListener('click', handlers.onClick as EventListener);\n if (handlers.onKeyDown)\n trigger.removeEventListener('keydown', handlers.onKeyDown as EventListener);\n\n if (!triggerOverride) {\n this._handlers = undefined;\n this._triggerElement = undefined;\n }\n }\n\n // --- Animation ---\n\n async animateShow(el: HTMLElement, duration: number, keyframes?: Keyframe[]) {\n const d = reducedMotion?.matches ? 0 : duration;\n el.getAnimations().forEach((a) => a.cancel());\n el.animate(\n keyframes ?? [\n { opacity: 0, scale: 0.96 },\n { opacity: 1, scale: 1 },\n ],\n { duration: d, fill: 'forwards' },\n );\n }\n\n async animateHide(el: HTMLElement, duration: number, keyframes?: Keyframe[]) {\n const d = reducedMotion?.matches ? 0 : duration;\n const anim = el.animate(\n keyframes ?? [\n { opacity: 1, scale: 1 },\n { opacity: 0, scale: 0.96 },\n ],\n { duration: d, fill: 'forwards' },\n );\n await anim.finished;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"popover.js","names":[],"sources":["../../../src/html/shared/controllers/popover.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport {\n computePosition,\n autoUpdate,\n offset,\n flip,\n shift,\n arrow,\n size,\n type Placement,\n} from '@floating-ui/dom';\n\n// --- Pure geometry helpers ---\n\ntype Point = [number, number];\n\n/**\n * How long the pointer may rest, in the corridor between the trigger and the\n * bubble, before the safe polygon gives up and hides. The polygon is only\n * re-evaluated on `pointermove`, so a pointer that leaves and immediately stops\n * emits no further event to close on — this fallback guarantees the tooltip\n * still hides. Kept short so a genuine pause while travelling toward the bubble\n * re-arms it without a visible flicker. It is *not* armed while the pointer sits\n * on the bubble or the trigger themselves — those are stable \"stay open\" states.\n */\nconst SAFE_POLYGON_REST_HIDE_MS = 200;\n\n/** Check if a point is inside a polygon using ray-casting. */\nfunction isPointInPolygon([px, py]: Point, polygon: Point[]) {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const [xi, yi] = polygon[i];\n const [xj, yj] = polygon[j];\n if (yi >= py !== yj >= py && px <= ((xj - xi) * (py - yi)) / (yj - yi) + xi) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/** Check if a point is inside a rect. */\nfunction isPointInRect([px, py]: Point, rect: DOMRect) {\n return px >= rect.left && px <= rect.right && py >= rect.top && py <= rect.bottom;\n}\n\n/**\n * Build a safe polygon (triangle/quadrilateral) from cursor to floating element edges.\n * Adapted from floating-ui/safePolygon.\n * @see https://github.com/floating-ui/floating-ui/blob/master/packages/react/src/safePolygon.ts\n */\nfunction getSafePolygon(\n cursorX: number,\n cursorY: number,\n rect: DOMRect,\n refRect: DOMRect,\n placement: string,\n): Point[] {\n const side = placement.split('-')[0] as 'top' | 'bottom' | 'left' | 'right';\n const buffer = 2;\n\n const isFloatingWider = rect.width > refRect.width;\n const isFloatingTaller = rect.height > refRect.height;\n\n const cursorLeaveFromRight = cursorX > rect.right - rect.width / 2;\n const cursorLeaveFromBottom = cursorY > rect.bottom - rect.height / 2;\n\n switch (side) {\n case 'top': {\n const c1: Point = [\n isFloatingWider\n ? cursorX + buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY + buffer + 1,\n ];\n const c2: Point = [\n isFloatingWider\n ? cursorX - buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY + buffer + 1,\n ];\n return [\n c1,\n c2,\n [\n rect.left,\n cursorLeaveFromRight\n ? rect.bottom - buffer\n : isFloatingWider\n ? rect.bottom - buffer\n : rect.top,\n ],\n [\n rect.right,\n cursorLeaveFromRight\n ? isFloatingWider\n ? rect.bottom - buffer\n : rect.top\n : rect.bottom - buffer,\n ],\n ];\n }\n case 'bottom': {\n const c1: Point = [\n isFloatingWider\n ? cursorX + buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY - buffer,\n ];\n const c2: Point = [\n isFloatingWider\n ? cursorX - buffer / 2\n : cursorLeaveFromRight\n ? cursorX + buffer * 4\n : cursorX - buffer * 4,\n cursorY - buffer,\n ];\n return [\n c1,\n c2,\n [\n rect.left,\n cursorLeaveFromRight\n ? rect.top + buffer\n : isFloatingWider\n ? rect.top + buffer\n : rect.bottom,\n ],\n [\n rect.right,\n cursorLeaveFromRight\n ? isFloatingWider\n ? rect.top + buffer\n : rect.bottom\n : rect.top + buffer,\n ],\n ];\n }\n case 'left': {\n const c1: Point = [\n cursorX + buffer + 1,\n isFloatingTaller\n ? cursorY + buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n const c2: Point = [\n cursorX + buffer + 1,\n isFloatingTaller\n ? cursorY - buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n return [\n [\n cursorLeaveFromBottom\n ? rect.right - buffer\n : isFloatingTaller\n ? rect.right - buffer\n : rect.left,\n rect.top,\n ],\n [\n cursorLeaveFromBottom\n ? isFloatingTaller\n ? rect.right - buffer\n : rect.left\n : rect.right - buffer,\n rect.bottom,\n ],\n c1,\n c2,\n ];\n }\n case 'right': {\n const c1: Point = [\n cursorX - buffer,\n isFloatingTaller\n ? cursorY + buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n const c2: Point = [\n cursorX - buffer,\n isFloatingTaller\n ? cursorY - buffer / 2\n : cursorLeaveFromBottom\n ? cursorY + buffer * 4\n : cursorY - buffer * 4,\n ];\n return [\n [\n cursorLeaveFromBottom\n ? rect.left + buffer\n : isFloatingTaller\n ? rect.left + buffer\n : rect.right,\n rect.top,\n ],\n [\n cursorLeaveFromBottom\n ? isFloatingTaller\n ? rect.left + buffer\n : rect.right\n : rect.left + buffer,\n rect.bottom,\n ],\n c1,\n c2,\n ];\n }\n default:\n return [];\n }\n}\n\n// --- Constants ---\n\nconst STATIC_SIDE = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' } as const;\n\nconst reducedMotion =\n typeof window !== 'undefined' ? window.matchMedia('(prefers-reduced-motion: reduce)') : undefined;\n\n// --- Controller ---\n\nexport interface PopoverControllerConfig {\n getTriggerElement: () => HTMLElement | null;\n getFloatingElement: () => HTMLElement | null;\n getArrowElement: () => HTMLElement | null;\n onPlacementChange?: (placement: string) => void;\n}\n\nexport interface PositionOptions {\n placement: Placement;\n distance: number;\n /** Offset along the cross axis, in pixels (e.g. to align a submenu's first item with its trigger). */\n skidding?: number;\n fullWidth?: boolean;\n /** Floor the floating element's width at the trigger's width (`'trigger'`). */\n minWidth?: 'trigger';\n}\n\ntype TriggerHandlers = {\n onPointerEnter?: (e: PointerEvent) => void;\n onPointerLeave?: (e: PointerEvent) => void;\n onFocusIn?: (e: FocusEvent) => void;\n onFocusOut?: (e: FocusEvent) => void;\n onClick?: (e: MouseEvent) => void;\n onKeyDown?: (e: KeyboardEvent) => void;\n};\n\nexport class PopoverController implements ReactiveController {\n private _host: ReactiveControllerHost & HTMLElement;\n private _config: PopoverControllerConfig;\n private _cleanupAutoUpdate?: () => void;\n private _cleanupSafePolygon?: () => void;\n private _handlers?: TriggerHandlers;\n private _triggerElement?: HTMLElement | null;\n private _currentPlacement = '';\n\n constructor(host: ReactiveControllerHost & HTMLElement, config: PopoverControllerConfig) {\n this._host = host;\n this._config = config;\n host.addController(this);\n }\n\n hostConnected() {}\n\n hostDisconnected() {\n this.stopPositioning();\n this.cleanupSafePolygon();\n this.removeTriggerListeners();\n }\n\n get currentPlacement() {\n return this._currentPlacement;\n }\n\n // --- Positioning ---\n\n async updatePosition(options: PositionOptions) {\n const trigger = this._config.getTriggerElement();\n const floating = this._config.getFloatingElement();\n const arrowEl = this._config.getArrowElement();\n if (!trigger || !floating) return;\n\n const middleware = [\n offset({ mainAxis: options.distance, crossAxis: options.skidding ?? 0 }),\n // Size before flip/shift so the adopted floor feeds collision detection.\n ...(options.minWidth === 'trigger'\n ? [\n size({\n apply({ rects, elements }) {\n elements.floating.style.minWidth = `${rects.reference.width}px`;\n },\n }),\n ]\n : []),\n flip(),\n shift(),\n ...(arrowEl ? [arrow({ element: arrowEl, padding: 8 })] : []),\n ];\n\n const { x, y, placement, middlewareData } = await computePosition(trigger, floating, {\n placement: options.placement,\n strategy: 'fixed',\n middleware,\n });\n\n Object.assign(floating.style, {\n position: 'fixed',\n left: options.fullWidth ? '0px' : `${x}px`,\n top: `${y}px`,\n width: options.fullWidth ? '100vw' : '',\n });\n\n // The `size` middleware (above) sets minWidth when syncing; clear it otherwise.\n if (options.minWidth !== 'trigger') floating.style.minWidth = '';\n\n if (placement !== this._currentPlacement) {\n this._currentPlacement = placement;\n this._config.onPlacementChange?.(placement);\n }\n\n if (middlewareData.arrow && arrowEl) {\n const { x: ax, y: ay } = middlewareData.arrow;\n const side = STATIC_SIDE[placement.split('-')[0] as keyof typeof STATIC_SIDE];\n Object.assign(arrowEl.style, {\n left: ax != null ? `${ax}px` : '',\n top: ay != null ? `${ay}px` : '',\n [side]: `${-arrowEl.offsetWidth / 2}px`,\n });\n }\n }\n\n startPositioning(options: PositionOptions) {\n const trigger = this._config.getTriggerElement();\n const floating = this._config.getFloatingElement();\n if (!trigger || !floating) return;\n\n this._cleanupAutoUpdate?.();\n this._cleanupAutoUpdate = autoUpdate(trigger, floating, () => this.updatePosition(options));\n }\n\n stopPositioning() {\n this._cleanupAutoUpdate?.();\n this._cleanupAutoUpdate = undefined;\n }\n\n // --- Safe polygon ---\n\n handlePointerLeave(e: PointerEvent, onHide: (reason: 'exit' | 'settled') => void) {\n const floating = this._config.getFloatingElement();\n const trigger = this._config.getTriggerElement();\n if (!floating || !trigger) {\n onHide('exit');\n return;\n }\n\n const cursorX = e.clientX;\n const cursorY = e.clientY;\n\n // Armed only while the pointer sits in the corridor between the trigger and\n // the bubble: there we cannot tell \"paused mid-travel\" from \"gone\" without\n // waiting, so if no further `pointermove` re-evaluates the polygon the\n // pointer has settled and we hide. On the bubble or the trigger themselves\n // the answer is unambiguous — stay open — so the timer is cleared, never\n // armed (otherwise a pointer resting on the bubble to read it would hide).\n let fallback: ReturnType<typeof setTimeout> | undefined;\n\n const hide = (reason: 'exit' | 'settled') => {\n cleanup();\n onHide(reason);\n };\n\n // Stable \"stay open\" surface (bubble or trigger): cancel any pending rest-hide.\n const stayOpen = () => clearTimeout(fallback);\n\n // Transient corridor: keep open, but hide if the pointer comes to rest here.\n const armRestHide = () => {\n clearTimeout(fallback);\n fallback = setTimeout(() => hide('settled'), SAFE_POLYGON_REST_HIDE_MS);\n };\n\n const onMove = (moveEvent: PointerEvent) => {\n const { clientX: mx, clientY: my } = moveEvent;\n const cursor: Point = [mx, my];\n const rect = floating.getBoundingClientRect();\n const refRect = trigger.getBoundingClientRect();\n const side = this._currentPlacement.split('-')[0];\n\n if (isPointInRect(cursor, rect)) {\n stayOpen();\n return;\n }\n if (isPointInRect(cursor, refRect)) {\n stayOpen();\n return;\n }\n\n if (\n (side === 'top' && cursorY >= refRect.bottom - 1) ||\n (side === 'bottom' && cursorY <= refRect.top + 1) ||\n (side === 'left' && cursorX >= refRect.right - 1) ||\n (side === 'right' && cursorX <= refRect.left + 1)\n ) {\n hide('exit');\n return;\n }\n\n const polygon = getSafePolygon(cursorX, cursorY, rect, refRect, this._currentPlacement);\n if (isPointInPolygon(cursor, polygon)) {\n armRestHide();\n return;\n }\n\n hide('exit');\n };\n\n const cleanup = () => {\n clearTimeout(fallback);\n document.removeEventListener('pointermove', onMove);\n };\n\n document.addEventListener('pointermove', onMove);\n // Arm immediately: a leave with no subsequent move (flick-and-stop) lands in\n // the corridor and must still hide once the pointer has settled.\n armRestHide();\n\n this._cleanupSafePolygon?.();\n this._cleanupSafePolygon = cleanup;\n }\n\n cleanupSafePolygon() {\n this._cleanupSafePolygon?.();\n this._cleanupSafePolygon = undefined;\n }\n\n // --- Trigger listener management ---\n\n addTriggerListeners(handlers: TriggerHandlers) {\n const trigger = this._config.getTriggerElement();\n if (!trigger) return;\n\n this._handlers = handlers;\n this._triggerElement = trigger;\n\n if (handlers.onPointerEnter)\n trigger.addEventListener('pointerenter', handlers.onPointerEnter as EventListener);\n if (handlers.onPointerLeave)\n trigger.addEventListener('pointerleave', handlers.onPointerLeave as EventListener);\n if (handlers.onFocusIn)\n trigger.addEventListener('focusin', handlers.onFocusIn as EventListener);\n if (handlers.onFocusOut)\n trigger.addEventListener('focusout', handlers.onFocusOut as EventListener);\n if (handlers.onClick) trigger.addEventListener('click', handlers.onClick as EventListener);\n if (handlers.onKeyDown)\n trigger.addEventListener('keydown', handlers.onKeyDown as EventListener);\n }\n\n removeTriggerListeners(triggerOverride?: HTMLElement | null) {\n const trigger = triggerOverride ?? this._triggerElement;\n const handlers = this._handlers;\n if (!trigger || !handlers) return;\n\n if (handlers.onPointerEnter)\n trigger.removeEventListener('pointerenter', handlers.onPointerEnter as EventListener);\n if (handlers.onPointerLeave)\n trigger.removeEventListener('pointerleave', handlers.onPointerLeave as EventListener);\n if (handlers.onFocusIn)\n trigger.removeEventListener('focusin', handlers.onFocusIn as EventListener);\n if (handlers.onFocusOut)\n trigger.removeEventListener('focusout', handlers.onFocusOut as EventListener);\n if (handlers.onClick) trigger.removeEventListener('click', handlers.onClick as EventListener);\n if (handlers.onKeyDown)\n trigger.removeEventListener('keydown', handlers.onKeyDown as EventListener);\n\n if (!triggerOverride) {\n this._handlers = undefined;\n this._triggerElement = undefined;\n }\n }\n\n // --- Animation ---\n\n async animateShow(el: HTMLElement, duration: number, keyframes?: Keyframe[]) {\n const d = reducedMotion?.matches ? 0 : duration;\n el.getAnimations().forEach((a) => a.cancel());\n el.animate(\n keyframes ?? [\n { opacity: 0, scale: 0.96 },\n { opacity: 1, scale: 1 },\n ],\n { duration: d, fill: 'forwards' },\n );\n }\n\n async animateHide(el: HTMLElement, duration: number, keyframes?: Keyframe[]) {\n const d = reducedMotion?.matches ? 0 : duration;\n const anim = el.animate(\n keyframes ?? [\n { opacity: 1, scale: 1 },\n { opacity: 0, scale: 0.96 },\n ],\n { duration: d, fill: 'forwards' },\n );\n await anim.finished;\n }\n}\n"],"mappings":"yFAyBA,IAAM,EAA4B,IAGlC,SAAS,EAAiB,CAAC,EAAI,GAAY,EAAkB,CAC3D,IAAI,EAAS,GACb,IAAK,IAAI,EAAI,EAAG,EAAI,EAAQ,OAAS,EAAG,EAAI,EAAQ,OAAQ,EAAI,IAAK,CACnE,GAAM,CAAC,EAAI,GAAM,EAAQ,GACnB,CAAC,EAAI,GAAM,EAAQ,GACrB,GAAM,GAAO,GAAM,GAAM,IAAQ,EAAK,IAAO,EAAK,IAAQ,EAAK,GAAM,IACvE,EAAS,CAAC,EAEd,CACA,OAAO,CACT,CAGA,SAAS,EAAc,CAAC,EAAI,GAAY,EAAe,CACrD,OAAO,GAAM,EAAK,MAAQ,GAAM,EAAK,OAAS,GAAM,EAAK,KAAO,GAAM,EAAK,MAC7E,CAOA,SAAS,EACP,EACA,EACA,EACA,EACA,EACS,CACT,IAAM,EAAO,EAAU,MAAM,GAAG,CAAC,CAAC,GAG5B,EAAkB,EAAK,MAAQ,EAAQ,MACvC,EAAmB,EAAK,OAAS,EAAQ,OAEzC,EAAuB,EAAU,EAAK,MAAQ,EAAK,MAAQ,EAC3D,EAAwB,EAAU,EAAK,OAAS,EAAK,OAAS,EAEpE,OAAQ,EAAR,CACE,IAAK,MAiBH,MAAO,CACL,CAhBA,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,EAChB,EAAU,EAAS,CAWnB,EACA,CATA,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,EAChB,EAAU,EAAS,CAInB,EACA,CACE,EAAK,KACL,GAEI,EADA,EAAK,OAAS,EAGZ,EAAK,GACb,EACA,CACE,EAAK,MACL,EACI,EACE,EAAK,OAAS,EACd,EAAK,IACP,EAAK,OAAS,CACpB,CACF,EAEF,IAAK,SAiBH,MAAO,CACL,CAhBA,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,EAChB,EAAU,CAWV,EACA,CATA,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,EAChB,EAAU,CAIV,EACA,CACE,EAAK,KACL,GAEI,EADA,EAAK,IAAM,EAGT,EAAK,MACb,EACA,CACE,EAAK,MACL,EACI,EACE,EAAK,IAAM,EACX,EAAK,OACP,EAAK,IAAM,CACjB,CACF,EAEF,IAAK,OAAQ,CACX,IAAM,EAAY,CAChB,EAAU,EAAS,EACnB,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,CAClB,EACM,EAAY,CAChB,EAAU,EAAS,EACnB,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,CAClB,EACA,MAAO,CACL,CACE,GAEI,EADA,EAAK,MAAQ,EAGX,EAAK,KACX,EAAK,GACP,EACA,CACE,EACI,EACE,EAAK,MAAQ,EACb,EAAK,KACP,EAAK,MAAQ,EACjB,EAAK,MACP,EACA,EACA,CACF,CACF,CACA,IAAK,QAAS,CACZ,IAAM,EAAY,CAChB,EAAU,EACV,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,CAClB,EACM,EAAY,CAChB,EAAU,EACV,EACI,EAAU,EAAS,EACnB,EACE,EAAU,EACV,EAAU,CAClB,EACA,MAAO,CACL,CACE,GAEI,EADA,EAAK,KAAO,EAGV,EAAK,MACX,EAAK,GACP,EACA,CACE,EACI,EACE,EAAK,KAAO,EACZ,EAAK,MACP,EAAK,KAAO,EAChB,EAAK,MACP,EACA,EACA,CACF,CACF,CACA,QACE,MAAO,CAAC,CACZ,CACF,CAIA,IAAM,EAAc,CAAE,IAAK,SAAU,MAAO,OAAQ,OAAQ,MAAO,KAAM,OAAQ,EAE3E,EACJ,OAAO,OAAW,IAAc,OAAO,WAAW,kCAAkC,EAAI,IAAA,GA8B7E,EAAb,KAA6D,CAS3D,YAAY,EAA4C,EAAiC,wBAF7D,GAG1B,KAAK,MAAQ,EACb,KAAK,QAAU,EACf,EAAK,cAAc,IAAI,CACzB,CAEA,eAAgB,CAAC,CAEjB,kBAAmB,CACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,CAC9B,CAEA,IAAI,kBAAmB,CACrB,OAAO,KAAK,iBACd,CAIA,MAAM,eAAe,EAA0B,CAC7C,IAAM,EAAU,KAAK,QAAQ,kBAAkB,EACzC,EAAW,KAAK,QAAQ,mBAAmB,EAC3C,EAAU,KAAK,QAAQ,gBAAgB,EAC7C,GAAI,CAAC,GAAW,CAAC,EAAU,OAE3B,IAAM,EAAa,CACjB,EAAO,CAAE,SAAU,EAAQ,SAAU,UAAW,EAAQ,UAAY,CAAE,CAAC,EAEvE,GAAI,EAAQ,WAAa,UACrB,CACE,EAAK,CACH,MAAM,CAAE,QAAO,YAAY,CACzB,EAAS,SAAS,MAAM,SAAW,GAAG,EAAM,UAAU,MAAM,GAC9D,CACF,CAAC,CACH,EACA,CAAC,EACL,EAAK,EACL,EAAM,EACN,GAAI,EAAU,CAAC,EAAM,CAAE,QAAS,EAAS,QAAS,CAAE,CAAC,CAAC,EAAI,CAAC,CAC7D,EAEM,CAAE,IAAG,IAAG,YAAW,kBAAmB,MAAM,EAAgB,EAAS,EAAU,CACnF,UAAW,EAAQ,UACnB,SAAU,QACV,YACF,CAAC,EAiBD,GAfA,OAAO,OAAO,EAAS,MAAO,CAC5B,SAAU,QACV,KAAM,EAAQ,UAAY,MAAQ,GAAG,EAAE,IACvC,IAAK,GAAG,EAAE,IACV,MAAO,EAAQ,UAAY,QAAU,EACvC,CAAC,EAGG,EAAQ,WAAa,YAAW,EAAS,MAAM,SAAW,IAE1D,IAAc,KAAK,oBACrB,KAAK,kBAAoB,EACzB,KAAK,QAAQ,oBAAoB,CAAS,GAGxC,EAAe,OAAS,EAAS,CACnC,GAAM,CAAE,EAAG,EAAI,EAAG,GAAO,EAAe,MAClC,EAAO,EAAY,EAAU,MAAM,GAAG,CAAC,CAAC,IAC9C,OAAO,OAAO,EAAQ,MAAO,CAC3B,KAAM,GAAM,KAAmB,GAAZ,GAAG,EAAG,IACzB,IAAK,GAAM,KAAmB,GAAZ,GAAG,EAAG,KACvB,GAAO,GAAG,CAAC,EAAQ,YAAc,EAAE,GACtC,CAAC,CACH,CACF,CAEA,iBAAiB,EAA0B,CACzC,IAAM,EAAU,KAAK,QAAQ,kBAAkB,EACzC,EAAW,KAAK,QAAQ,mBAAmB,EAC7C,CAAC,GAAW,CAAC,IAEjB,KAAK,qBAAqB,EAC1B,KAAK,mBAAqB,EAAW,EAAS,MAAgB,KAAK,eAAe,CAAO,CAAC,EAC5F,CAEA,iBAAkB,CAChB,KAAK,qBAAqB,EAC1B,KAAK,mBAAqB,IAAA,EAC5B,CAIA,mBAAmB,EAAiB,EAA8C,CAChF,IAAM,EAAW,KAAK,QAAQ,mBAAmB,EAC3C,EAAU,KAAK,QAAQ,kBAAkB,EAC/C,GAAI,CAAC,GAAY,CAAC,EAAS,CACzB,EAAO,MAAM,EACb,MACF,CAEA,IAAM,EAAU,EAAE,QACZ,EAAU,EAAE,QAQd,EAEE,EAAQ,GAA+B,CAC3C,EAAQ,EACR,EAAO,CAAM,CACf,EAGM,MAAiB,aAAa,CAAQ,EAGtC,MAAoB,CACxB,aAAa,CAAQ,EACrB,EAAW,eAAiB,EAAK,SAAS,EAAG,CAAyB,CACxE,EAEM,EAAU,GAA4B,CAC1C,GAAM,CAAE,QAAS,EAAI,QAAS,GAAO,EAC/B,EAAgB,CAAC,EAAI,CAAE,EACvB,EAAO,EAAS,sBAAsB,EACtC,EAAU,EAAQ,sBAAsB,EACxC,EAAO,KAAK,kBAAkB,MAAM,GAAG,CAAC,CAAC,GAE/C,GAAI,EAAc,EAAQ,CAAI,EAAG,CAC/B,EAAS,EACT,MACF,CACA,GAAI,EAAc,EAAQ,CAAO,EAAG,CAClC,EAAS,EACT,MACF,CAEA,GACG,IAAS,OAAS,GAAW,EAAQ,OAAS,GAC9C,IAAS,UAAY,GAAW,EAAQ,IAAM,GAC9C,IAAS,QAAU,GAAW,EAAQ,MAAQ,GAC9C,IAAS,SAAW,GAAW,EAAQ,KAAO,EAC/C,CACA,EAAK,MAAM,EACX,MACF,CAGA,GAAI,EAAiB,EADL,EAAe,EAAS,EAAS,EAAM,EAAS,KAAK,iBACxC,CAAO,EAAG,CACrC,EAAY,EACZ,MACF,CAEA,EAAK,MAAM,CACb,EAEM,MAAgB,CACpB,aAAa,CAAQ,EACrB,SAAS,oBAAoB,cAAe,CAAM,CACpD,EAEA,SAAS,iBAAiB,cAAe,CAAM,EAG/C,EAAY,EAEZ,KAAK,sBAAsB,EAC3B,KAAK,oBAAsB,CAC7B,CAEA,oBAAqB,CACnB,KAAK,sBAAsB,EAC3B,KAAK,oBAAsB,IAAA,EAC7B,CAIA,oBAAoB,EAA2B,CAC7C,IAAM,EAAU,KAAK,QAAQ,kBAAkB,EAC1C,IAEL,KAAK,UAAY,EACjB,KAAK,gBAAkB,EAEnB,EAAS,gBACX,EAAQ,iBAAiB,eAAgB,EAAS,cAA+B,EAC/E,EAAS,gBACX,EAAQ,iBAAiB,eAAgB,EAAS,cAA+B,EAC/E,EAAS,WACX,EAAQ,iBAAiB,UAAW,EAAS,SAA0B,EACrE,EAAS,YACX,EAAQ,iBAAiB,WAAY,EAAS,UAA2B,EACvE,EAAS,SAAS,EAAQ,iBAAiB,QAAS,EAAS,OAAwB,EACrF,EAAS,WACX,EAAQ,iBAAiB,UAAW,EAAS,SAA0B,EAC3E,CAEA,uBAAuB,EAAsC,CAC3D,IAAM,EAAU,GAAmB,KAAK,gBAClC,EAAW,KAAK,UAClB,CAAC,GAAW,CAAC,IAEb,EAAS,gBACX,EAAQ,oBAAoB,eAAgB,EAAS,cAA+B,EAClF,EAAS,gBACX,EAAQ,oBAAoB,eAAgB,EAAS,cAA+B,EAClF,EAAS,WACX,EAAQ,oBAAoB,UAAW,EAAS,SAA0B,EACxE,EAAS,YACX,EAAQ,oBAAoB,WAAY,EAAS,UAA2B,EAC1E,EAAS,SAAS,EAAQ,oBAAoB,QAAS,EAAS,OAAwB,EACxF,EAAS,WACX,EAAQ,oBAAoB,UAAW,EAAS,SAA0B,EAEvE,IACH,KAAK,UAAY,IAAA,GACjB,KAAK,gBAAkB,IAAA,IAE3B,CAIA,MAAM,YAAY,EAAiB,EAAkB,EAAwB,CAC3E,IAAM,EAAI,GAAe,QAAU,EAAI,EACvC,EAAG,cAAc,CAAC,CAAC,QAAS,GAAM,EAAE,OAAO,CAAC,EAC5C,EAAG,QACD,GAAa,CACX,CAAE,QAAS,EAAG,MAAO,GAAK,EAC1B,CAAE,QAAS,EAAG,MAAO,CAAE,CACzB,EACA,CAAE,SAAU,EAAG,KAAM,UAAW,CAClC,CACF,CAEA,MAAM,YAAY,EAAiB,EAAkB,EAAwB,CAC3E,IAAM,EAAI,GAAe,QAAU,EAAI,EAQvC,MAPa,EAAG,QACd,GAAa,CACX,CAAE,QAAS,EAAG,MAAO,CAAE,EACvB,CAAE,QAAS,EAAG,MAAO,GAAK,CAC5B,EACA,CAAE,SAAU,EAAG,KAAM,UAAW,CAE5B,CAAA,CAAK,QACb,CACF"}
@@ -2289,6 +2289,22 @@ In dark mode, mixes the base color with black (default 15% black).
2289
2289
  .l-disclosure[data-marker='plus'][open] summary::after {
2290
2290
  rotate: 45deg;
2291
2291
  }
2292
+
2293
+ /*
2294
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
2295
+ Marker: placement
2296
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
2297
+ */
2298
+
2299
+ .l-disclosure[data-marker-placement='start'] summary {
2300
+ justify-content: flex-start;
2301
+ /* Match the summary's inline padding so the space around the marker is symmetric */
2302
+ gap: calc(var(--spacing) * 4);
2303
+ }
2304
+
2305
+ .l-disclosure[data-marker-placement='start'] summary::after {
2306
+ order: -1;
2307
+ }
2292
2308
  }
2293
2309
 
2294
2310
  /* divider.css */
@@ -134,4 +134,20 @@
134
134
  .l-disclosure[data-marker='plus'][open] summary::after {
135
135
  rotate: 45deg;
136
136
  }
137
+
138
+ /*
139
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
140
+ Marker: placement
141
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
142
+ */
143
+
144
+ .l-disclosure[data-marker-placement='start'] summary {
145
+ justify-content: flex-start;
146
+ /* Match the summary's inline padding so the space around the marker is symmetric */
147
+ gap: calc(var(--spacing) * 4);
148
+ }
149
+
150
+ .l-disclosure[data-marker-placement='start'] summary::after {
151
+ order: -1;
152
+ }
137
153
  }
@@ -134,4 +134,20 @@
134
134
  .l-disclosure[data-marker='plus'][open] summary::after {
135
135
  rotate: 45deg;
136
136
  }
137
+
138
+ /*
139
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
140
+ Marker: placement
141
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
142
+ */
143
+
144
+ .l-disclosure[data-marker-placement='start'] summary {
145
+ justify-content: flex-start;
146
+ /* Match the summary's inline padding so the space around the marker is symmetric */
147
+ gap: calc(var(--spacing) * 4);
148
+ }
149
+
150
+ .l-disclosure[data-marker-placement='start'] summary::after {
151
+ order: -1;
152
+ }
137
153
  }
@@ -2849,6 +2849,10 @@
2849
2849
  "description": "arrow | plus — Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross.",
2850
2850
  "name": "data-marker"
2851
2851
  },
2852
+ {
2853
+ "description": "start | end — Marker side. Defaults to `end`.",
2854
+ "name": "data-marker-placement"
2855
+ },
2852
2856
  {
2853
2857
  "description": "bordered — Adds border, background, and border-radius.",
2854
2858
  "name": "data-variant"
@@ -2893,6 +2897,14 @@
2893
2897
  ],
2894
2898
  "description": "Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross."
2895
2899
  },
2900
+ {
2901
+ "name": "data-marker-placement",
2902
+ "values": [
2903
+ "start",
2904
+ "end"
2905
+ ],
2906
+ "description": "Marker side. Defaults to `end`."
2907
+ },
2896
2908
  {
2897
2909
  "name": "data-variant",
2898
2910
  "description": "bordered — Adds border, background, and border-radius."
@@ -9215,6 +9227,26 @@
9215
9227
  "description": "The distance in pixels from the target element.",
9216
9228
  "attribute": "distance"
9217
9229
  },
9230
+ {
9231
+ "kind": "field",
9232
+ "name": "showDelay",
9233
+ "type": {
9234
+ "text": "number"
9235
+ },
9236
+ "default": "0",
9237
+ "description": "Milliseconds the pointer must dwell on the trigger before the tooltip\nshows. Applies to `hover` only — focus shows immediately.",
9238
+ "attribute": "show-delay"
9239
+ },
9240
+ {
9241
+ "kind": "field",
9242
+ "name": "hideDelay",
9243
+ "type": {
9244
+ "text": "number"
9245
+ },
9246
+ "default": "0",
9247
+ "description": "Milliseconds to wait after the pointer leaves the trigger (and its safe\npolygon) before hiding. Bridges a brief exit-and-return without flicker.\nApplies to `hover` only.",
9248
+ "attribute": "hide-delay"
9249
+ },
9218
9250
  {
9219
9251
  "kind": "field",
9220
9252
  "name": "open",
@@ -9288,6 +9320,24 @@
9288
9320
  "description": "The distance in pixels from the target element.",
9289
9321
  "fieldName": "distance"
9290
9322
  },
9323
+ {
9324
+ "name": "show-delay",
9325
+ "type": {
9326
+ "text": "number"
9327
+ },
9328
+ "default": "0",
9329
+ "description": "Milliseconds the pointer must dwell on the trigger before the tooltip\nshows. Applies to `hover` only — focus shows immediately.",
9330
+ "fieldName": "showDelay"
9331
+ },
9332
+ {
9333
+ "name": "hide-delay",
9334
+ "type": {
9335
+ "text": "number"
9336
+ },
9337
+ "default": "0",
9338
+ "description": "Milliseconds to wait after the pointer leaves the trigger (and its safe\npolygon) before hiding. Bridges a brief exit-and-return without flicker.\nApplies to `hover` only.",
9339
+ "fieldName": "hideDelay"
9340
+ },
9291
9341
  {
9292
9342
  "name": "open",
9293
9343
  "type": {
@@ -7,6 +7,7 @@
7
7
  * @attribute open - Native attribute — starts the disclosure expanded.
8
8
  * @attribute name - Native attribute — groups disclosures into an exclusive accordion.
9
9
  * @attribute data-marker - arrow | plus — Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross.
10
+ * @attribute data-marker-placement - start | end — Marker side. Defaults to `end`.
10
11
  * @attribute data-variant - bordered — Adds border, background, and border-radius.
11
12
  * @attribute disabled - Disables interaction (set on `<details>` or `<summary>`).
12
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"disclosure.meta.d.ts","sourceRoot":"","sources":["../../../src/html/elements/disclosure/disclosure.meta.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,qBAAa,cAAc;CAAG"}
1
+ {"version":3,"file":"disclosure.meta.d.ts","sourceRoot":"","sources":["../../../src/html/elements/disclosure/disclosure.meta.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,qBAAa,cAAc;CAAG"}
@@ -13,6 +13,7 @@
13
13
  * @attribute open - Native attribute — starts the disclosure expanded.
14
14
  * @attribute name - Native attribute — groups disclosures into an exclusive accordion.
15
15
  * @attribute data-marker - arrow | plus — Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross.
16
+ * @attribute data-marker-placement - start | end — Marker side. Defaults to `end`.
16
17
  * @attribute data-variant - bordered — Adds border, background, and border-radius.
17
18
  * @attribute disabled - Disables interaction (set on `<details>` or `<summary>`).
18
19
  *
@@ -28,6 +28,19 @@ export declare class Tooltip extends LuxenElement {
28
28
  accessor placement: Placement;
29
29
  /** The distance in pixels from the target element. */
30
30
  accessor distance: number;
31
+ /**
32
+ * Milliseconds the pointer must dwell on the trigger before the tooltip
33
+ * shows. Applies to `hover` only — focus shows immediately.
34
+ */
35
+ accessor showDelay: number;
36
+ /**
37
+ * Milliseconds to wait after the pointer leaves the trigger (and its safe
38
+ * polygon) before hiding. Bridges a brief exit-and-return without flicker.
39
+ * Applies to `hover` only.
40
+ */
41
+ accessor hideDelay: number;
42
+ private _showTimer?;
43
+ private _hideTimer?;
31
44
  /** Whether or not the tooltip is visible. */
32
45
  accessor open: boolean;
33
46
  /** Hide the directional arrow. */
@@ -42,12 +55,16 @@ export declare class Tooltip extends LuxenElement {
42
55
  connectedCallback(): void;
43
56
  disconnectedCallback(): void;
44
57
  updated(changed: PropertyValues<this>): void;
58
+ /** Cancel any pending delayed show/hide (trigger gone, `for` changed, disconnect). */
59
+ private _clearTimers;
45
60
  show(): void;
46
61
  hide(): void;
47
62
  toggle(): void;
48
63
  private _handleOpenChange;
49
64
  private _onPointerEnter;
50
65
  private _onPointerLeave;
66
+ /** Hide now, or after `hide-delay` if set (cancellable by a pointer return). */
67
+ private _scheduleHide;
51
68
  private _onFocusIn;
52
69
  private _onFocusOut;
53
70
  private _onClick;
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tooltip/tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAoBlD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,OAAQ,SAAQ,YAAY;IACvC,OAAgB,MAAM,4BAAwB;IAE9C,OAAO,CAAC,UAAU,CAAuB;IAEzC,OAAO,CAAC,SAAS,CAId;IAEH,yDAAyD;IAEzD,QAAQ,CAAC,GAAG,SAAM;IAElB,8CAA8C;IAE9C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAS;IAEtC,sDAAsD;IAEtD,QAAQ,CAAC,QAAQ,SAAK;IAEtB,6CAA6C;IAE7C,QAAQ,CAAC,IAAI,UAAS;IAEtB,kCAAkC;IAElC,QAAQ,CAAC,YAAY,UAAS;IAE9B,kFAAkF;IAElF,QAAQ,CAAC,OAAO,SAAiB;IAEjC,OAAO,CAAC,WAAW;IAInB,OAAO,KAAK,QAAQ,GAQnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,CAAC,YAAY;IAKX,iBAAiB;IAMjB,oBAAoB;IAOpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAU9C,IAAI;IAIJ,IAAI;IAIJ,MAAM;YAIQ,iBAAiB;IAgC/B,OAAO,CAAC,eAAe,CAUrB;IAEF,OAAO,CAAC,eAAe,CAGrB;IAEF,OAAO,CAAC,UAAU,CAEhB;IACF,OAAO,CAAC,WAAW,CAEjB;IACF,OAAO,CAAC,QAAQ,CAEd;IAEF,OAAO,CAAC,UAAU,CAKhB;IAEF,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,uBAAuB;IAOtB,MAAM;CAoBhB"}
1
+ {"version":3,"file":"tooltip.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tooltip/tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAoBlD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,OAAQ,SAAQ,YAAY;IACvC,OAAgB,MAAM,4BAAwB;IAE9C,OAAO,CAAC,UAAU,CAAuB;IAEzC,OAAO,CAAC,SAAS,CAId;IAEH,yDAAyD;IAEzD,QAAQ,CAAC,GAAG,SAAM;IAElB,8CAA8C;IAE9C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAS;IAEtC,sDAAsD;IAEtD,QAAQ,CAAC,QAAQ,SAAK;IAEtB;;;OAGG;IAEH,QAAQ,CAAC,SAAS,SAAK;IAEvB;;;;OAIG;IAEH,QAAQ,CAAC,SAAS,SAAK;IAEvB,OAAO,CAAC,UAAU,CAAC,CAAgC;IACnD,OAAO,CAAC,UAAU,CAAC,CAAgC;IAEnD,6CAA6C;IAE7C,QAAQ,CAAC,IAAI,UAAS;IAEtB,kCAAkC;IAElC,QAAQ,CAAC,YAAY,UAAS;IAE9B,kFAAkF;IAElF,QAAQ,CAAC,OAAO,SAAiB;IAEjC,OAAO,CAAC,WAAW;IAInB,OAAO,KAAK,QAAQ,GAQnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,CAAC,YAAY;IAKX,iBAAiB;IAMjB,oBAAoB;IAQpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAY9C,sFAAsF;IACtF,OAAO,CAAC,YAAY;IAOpB,IAAI;IAIJ,IAAI;IAIJ,MAAM;YAIQ,iBAAiB;IAgC/B,OAAO,CAAC,eAAe,CAwBrB;IAEF,OAAO,CAAC,eAAe,CAarB;IAEF,gFAAgF;IAChF,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,UAAU,CAOhB;IACF,OAAO,CAAC,WAAW,CAOjB;IACF,OAAO,CAAC,QAAQ,CAEd;IAEF,OAAO,CAAC,UAAU,CAKhB;IAEF,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,uBAAuB;IAOtB,MAAM;CAoBhB"}
@@ -15,7 +15,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
15
15
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
16
16
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
17
17
  };
18
- var _Tooltip_for_accessor_storage, _Tooltip_placement_accessor_storage, _Tooltip_distance_accessor_storage, _Tooltip_open_accessor_storage, _Tooltip_withoutArrow_accessor_storage, _Tooltip_trigger_accessor_storage;
18
+ var _Tooltip_for_accessor_storage, _Tooltip_placement_accessor_storage, _Tooltip_distance_accessor_storage, _Tooltip_showDelay_accessor_storage, _Tooltip_hideDelay_accessor_storage, _Tooltip_open_accessor_storage, _Tooltip_withoutArrow_accessor_storage, _Tooltip_trigger_accessor_storage;
19
19
  import { html, nothing, unsafeCSS } from 'lit';
20
20
  import { LuxenElement } from '../../shared/luxen-element.js';
21
21
  import { property } from 'lit/decorators.js';
@@ -64,6 +64,8 @@ export class Tooltip extends LuxenElement {
64
64
  _Tooltip_for_accessor_storage.set(this, '');
65
65
  _Tooltip_placement_accessor_storage.set(this, 'top');
66
66
  _Tooltip_distance_accessor_storage.set(this, 8);
67
+ _Tooltip_showDelay_accessor_storage.set(this, 0);
68
+ _Tooltip_hideDelay_accessor_storage.set(this, 0);
67
69
  _Tooltip_open_accessor_storage.set(this, false);
68
70
  _Tooltip_withoutArrow_accessor_storage.set(this, false);
69
71
  _Tooltip_trigger_accessor_storage.set(this, 'hover focus');
@@ -72,26 +74,62 @@ export class Tooltip extends LuxenElement {
72
74
  if (!this._hasTrigger('hover'))
73
75
  return;
74
76
  // The safe polygon means "the pointer is travelling toward the bubble" —
75
- // provably false once the pointer enters another tooltip's trigger, so
76
- // invalidate every peer's polygon (and drop its stale `pointermove`
77
- // listener) along with our own.
77
+ // provably false once the pointer enters another tooltip's trigger. Drop
78
+ // our own stale polygon, and close every peer now: invalidating a peer's
79
+ // polygon without hiding it leaves the peer stuck open, because its only
80
+ // remaining close path was eviction when *this* tooltip opens — and with
81
+ // `show-delay` that can be much later, or never if the pointer moves on
82
+ // before the dwell elapses (a fast sweep across a button group).
83
+ this._floating.cleanupSafePolygon();
78
84
  for (const tooltip of tooltipInstances) {
85
+ if (tooltip === this || tooltip._hasTrigger('manual'))
86
+ continue;
79
87
  tooltip._floating.cleanupSafePolygon();
88
+ tooltip.hide();
89
+ }
90
+ // The pointer is back on the trigger: cancel a pending hide (exit-and-return).
91
+ clearTimeout(this._hideTimer);
92
+ this._hideTimer = undefined;
93
+ if (this.showDelay > 0 && !this.open) {
94
+ clearTimeout(this._showTimer);
95
+ this._showTimer = setTimeout(() => this.show(), this.showDelay);
96
+ }
97
+ else {
98
+ this.show();
80
99
  }
81
- this.show();
82
100
  };
83
101
  this._onPointerLeave = (e) => {
102
+ // Cancel a still-pending show — the pointer left before the dwell elapsed.
103
+ clearTimeout(this._showTimer);
104
+ this._showTimer = undefined;
84
105
  if (!this._hasTrigger('hover') || !this.open)
85
106
  return;
86
- this._floating.handlePointerLeave(e, () => this.hide());
107
+ // `exit` the pointer actively moved off the trigger/bubble: honour
108
+ // `hide-delay` so a brief exit-and-return doesn't flicker. `settled` — the
109
+ // pointer left and sat motionless in the corridor: there is no in-flight
110
+ // gesture to bridge, so hide at once (never stack the delay on top).
111
+ this._floating.handlePointerLeave(e, (reason) => {
112
+ if (reason === 'settled')
113
+ this.hide();
114
+ else
115
+ this._scheduleHide();
116
+ });
87
117
  };
88
118
  this._onFocusIn = () => {
89
- if (this._hasTrigger('focus'))
119
+ // Focus is a discrete affordance — show immediately, ignoring the dwell delay.
120
+ if (this._hasTrigger('focus')) {
121
+ clearTimeout(this._hideTimer);
122
+ this._hideTimer = undefined;
90
123
  this.show();
124
+ }
91
125
  };
92
126
  this._onFocusOut = () => {
93
- if (this._hasTrigger('focus'))
127
+ if (this._hasTrigger('focus')) {
128
+ // A hover may have queued a show; blur must not resurrect it.
129
+ clearTimeout(this._showTimer);
130
+ this._showTimer = undefined;
94
131
  this.hide();
132
+ }
95
133
  };
96
134
  this._onClick = () => {
97
135
  if (this._hasTrigger('click'))
@@ -113,6 +151,19 @@ export class Tooltip extends LuxenElement {
113
151
  /** The distance in pixels from the target element. */
114
152
  get distance() { return __classPrivateFieldGet(this, _Tooltip_distance_accessor_storage, "f"); }
115
153
  set distance(value) { __classPrivateFieldSet(this, _Tooltip_distance_accessor_storage, value, "f"); }
154
+ /**
155
+ * Milliseconds the pointer must dwell on the trigger before the tooltip
156
+ * shows. Applies to `hover` only — focus shows immediately.
157
+ */
158
+ get showDelay() { return __classPrivateFieldGet(this, _Tooltip_showDelay_accessor_storage, "f"); }
159
+ set showDelay(value) { __classPrivateFieldSet(this, _Tooltip_showDelay_accessor_storage, value, "f"); }
160
+ /**
161
+ * Milliseconds to wait after the pointer leaves the trigger (and its safe
162
+ * polygon) before hiding. Bridges a brief exit-and-return without flicker.
163
+ * Applies to `hover` only.
164
+ */
165
+ get hideDelay() { return __classPrivateFieldGet(this, _Tooltip_hideDelay_accessor_storage, "f"); }
166
+ set hideDelay(value) { __classPrivateFieldSet(this, _Tooltip_hideDelay_accessor_storage, value, "f"); }
116
167
  /** Whether or not the tooltip is visible. */
117
168
  get open() { return __classPrivateFieldGet(this, _Tooltip_open_accessor_storage, "f"); }
118
169
  set open(value) { __classPrivateFieldSet(this, _Tooltip_open_accessor_storage, value, "f"); }
@@ -155,6 +206,7 @@ export class Tooltip extends LuxenElement {
155
206
  tooltipInstances.delete(this);
156
207
  if (activeTooltip === this)
157
208
  activeTooltip = null;
209
+ this._clearTimers();
158
210
  this._removeTriggerListeners();
159
211
  }
160
212
  updated(changed) {
@@ -162,10 +214,19 @@ export class Tooltip extends LuxenElement {
162
214
  void this._handleOpenChange();
163
215
  }
164
216
  if (changed.has('for')) {
217
+ // A queued show/hide targets the old trigger — drop it before rewiring.
218
+ this._clearTimers();
165
219
  this._removeTriggerListeners(changed.get('for'));
166
220
  this._addTriggerListeners();
167
221
  }
168
222
  }
223
+ /** Cancel any pending delayed show/hide (trigger gone, `for` changed, disconnect). */
224
+ _clearTimers() {
225
+ clearTimeout(this._showTimer);
226
+ clearTimeout(this._hideTimer);
227
+ this._showTimer = undefined;
228
+ this._hideTimer = undefined;
229
+ }
169
230
  show() {
170
231
  if (!this.open)
171
232
  this.open = true;
@@ -210,6 +271,16 @@ export class Tooltip extends LuxenElement {
210
271
  popover.hidePopover();
211
272
  }
212
273
  }
274
+ /** Hide now, or after `hide-delay` if set (cancellable by a pointer return). */
275
+ _scheduleHide() {
276
+ if (this.hideDelay > 0) {
277
+ clearTimeout(this._hideTimer);
278
+ this._hideTimer = setTimeout(() => this.hide(), this.hideDelay);
279
+ }
280
+ else {
281
+ this.hide();
282
+ }
283
+ }
213
284
  _addTriggerListeners() {
214
285
  this._floating.addTriggerListeners({
215
286
  onPointerEnter: this._onPointerEnter,
@@ -247,7 +318,7 @@ export class Tooltip extends LuxenElement {
247
318
  `;
248
319
  }
249
320
  }
250
- _Tooltip_for_accessor_storage = new WeakMap(), _Tooltip_placement_accessor_storage = new WeakMap(), _Tooltip_distance_accessor_storage = new WeakMap(), _Tooltip_open_accessor_storage = new WeakMap(), _Tooltip_withoutArrow_accessor_storage = new WeakMap(), _Tooltip_trigger_accessor_storage = new WeakMap();
321
+ _Tooltip_for_accessor_storage = new WeakMap(), _Tooltip_placement_accessor_storage = new WeakMap(), _Tooltip_distance_accessor_storage = new WeakMap(), _Tooltip_showDelay_accessor_storage = new WeakMap(), _Tooltip_hideDelay_accessor_storage = new WeakMap(), _Tooltip_open_accessor_storage = new WeakMap(), _Tooltip_withoutArrow_accessor_storage = new WeakMap(), _Tooltip_trigger_accessor_storage = new WeakMap();
251
322
  Tooltip.styles = [hostStyles, styles];
252
323
  __decorate([
253
324
  property({ reflect: true })
@@ -258,6 +329,12 @@ __decorate([
258
329
  __decorate([
259
330
  property({ type: Number })
260
331
  ], Tooltip.prototype, "distance", null);
332
+ __decorate([
333
+ property({ type: Number, attribute: 'show-delay' })
334
+ ], Tooltip.prototype, "showDelay", null);
335
+ __decorate([
336
+ property({ type: Number, attribute: 'hide-delay' })
337
+ ], Tooltip.prototype, "hideDelay", null);
261
338
  __decorate([
262
339
  property({ type: Boolean, reflect: true })
263
340
  ], Tooltip.prototype, "open", null);
@@ -37,6 +37,15 @@
37
37
  ],
38
38
  "description": "Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross."
39
39
  },
40
+ {
41
+ "name": "data-marker-placement",
42
+ "data": true,
43
+ "values": [
44
+ "start",
45
+ "end"
46
+ ],
47
+ "description": "Marker side. Defaults to `end`."
48
+ },
40
49
  {
41
50
  "name": "data-variant",
42
51
  "data": true,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.15.0",
2
+ "version": "0.16.1",
3
3
  "elements": [
4
4
  {
5
5
  "name": "alert",
@@ -1762,6 +1762,15 @@
1762
1762
  ],
1763
1763
  "description": "Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross."
1764
1764
  },
1765
+ {
1766
+ "name": "data-marker-placement",
1767
+ "data": true,
1768
+ "values": [
1769
+ "start",
1770
+ "end"
1771
+ ],
1772
+ "description": "Marker side. Defaults to `end`."
1773
+ },
1765
1774
  {
1766
1775
  "name": "data-variant",
1767
1776
  "data": true,
@@ -5621,6 +5630,22 @@
5621
5630
  "reflects": false,
5622
5631
  "description": "The distance in pixels from the target element."
5623
5632
  },
5633
+ {
5634
+ "name": "showDelay",
5635
+ "attribute": "show-delay",
5636
+ "type": "number",
5637
+ "default": "0",
5638
+ "reflects": false,
5639
+ "description": "Milliseconds the pointer must dwell on the trigger before the tooltip\nshows. Applies to `hover` only — focus shows immediately."
5640
+ },
5641
+ {
5642
+ "name": "hideDelay",
5643
+ "attribute": "hide-delay",
5644
+ "type": "number",
5645
+ "default": "0",
5646
+ "reflects": false,
5647
+ "description": "Milliseconds to wait after the pointer leaves the trigger (and its safe\npolygon) before hiding. Bridges a brief exit-and-return without flicker.\nApplies to `hover` only."
5648
+ },
5624
5649
  {
5625
5650
  "name": "open",
5626
5651
  "attribute": "open",
@@ -39,6 +39,22 @@
39
39
  "reflects": false,
40
40
  "description": "The distance in pixels from the target element."
41
41
  },
42
+ {
43
+ "name": "showDelay",
44
+ "attribute": "show-delay",
45
+ "type": "number",
46
+ "default": "0",
47
+ "reflects": false,
48
+ "description": "Milliseconds the pointer must dwell on the trigger before the tooltip\nshows. Applies to `hover` only — focus shows immediately."
49
+ },
50
+ {
51
+ "name": "hideDelay",
52
+ "attribute": "hide-delay",
53
+ "type": "number",
54
+ "default": "0",
55
+ "reflects": false,
56
+ "description": "Milliseconds to wait after the pointer leaves the trigger (and its safe\npolygon) before hiding. Bridges a brief exit-and-return without flicker.\nApplies to `hover` only."
57
+ },
42
58
  {
43
59
  "name": "open",
44
60
  "attribute": "open",
@@ -38,7 +38,7 @@ export declare class PopoverController implements ReactiveController {
38
38
  updatePosition(options: PositionOptions): Promise<void>;
39
39
  startPositioning(options: PositionOptions): void;
40
40
  stopPositioning(): void;
41
- handlePointerLeave(e: PointerEvent, onHide: () => void): void;
41
+ handlePointerLeave(e: PointerEvent, onHide: (reason: 'exit' | 'settled') => void): void;
42
42
  cleanupSafePolygon(): void;
43
43
  addTriggerListeners(handlers: TriggerHandlers): void;
44
44
  removeTriggerListeners(triggerOverride?: HTMLElement | null): void;
@@ -1 +1 @@
1
- {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["../../../src/html/shared/controllers/popover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,KAAK,CAAC;AACtE,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAoN1B,MAAM,WAAW,uBAAuB;IACtC,iBAAiB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC5C,kBAAkB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC7C,eAAe,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,KAAK,eAAe,GAAG;IACrB,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,qBAAa,iBAAkB,YAAW,kBAAkB;IAC1D,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,kBAAkB,CAAC,CAAa;IACxC,OAAO,CAAC,mBAAmB,CAAC,CAAa;IACzC,OAAO,CAAC,SAAS,CAAC,CAAkB;IACpC,OAAO,CAAC,eAAe,CAAC,CAAqB;IAC7C,OAAO,CAAC,iBAAiB,CAAM;gBAEnB,IAAI,EAAE,sBAAsB,GAAG,WAAW,EAAE,MAAM,EAAE,uBAAuB;IAMvF,aAAa;IAEb,gBAAgB;IAMhB,IAAI,gBAAgB,WAEnB;IAIK,cAAc,CAAC,OAAO,EAAE,eAAe;IAuD7C,gBAAgB,CAAC,OAAO,EAAE,eAAe;IASzC,eAAe;IAOf,kBAAkB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,IAAI;IAiDtD,kBAAkB;IAOlB,mBAAmB,CAAC,QAAQ,EAAE,eAAe;IAoB7C,sBAAsB,CAAC,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI;IAyBrD,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;IAYrE,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;CAW5E"}
1
+ {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["../../../src/html/shared/controllers/popover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,KAAK,CAAC;AACtE,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AA+N1B,MAAM,WAAW,uBAAuB;IACtC,iBAAiB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC5C,kBAAkB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC7C,eAAe,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,KAAK,eAAe,GAAG;IACrB,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,qBAAa,iBAAkB,YAAW,kBAAkB;IAC1D,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,kBAAkB,CAAC,CAAa;IACxC,OAAO,CAAC,mBAAmB,CAAC,CAAa;IACzC,OAAO,CAAC,SAAS,CAAC,CAAkB;IACpC,OAAO,CAAC,eAAe,CAAC,CAAqB;IAC7C,OAAO,CAAC,iBAAiB,CAAM;gBAEnB,IAAI,EAAE,sBAAsB,GAAG,WAAW,EAAE,MAAM,EAAE,uBAAuB;IAMvF,aAAa;IAEb,gBAAgB;IAMhB,IAAI,gBAAgB,WAEnB;IAIK,cAAc,CAAC,OAAO,EAAE,eAAe;IAuD7C,gBAAgB,CAAC,OAAO,EAAE,eAAe;IASzC,eAAe;IAOf,kBAAkB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI;IAkFhF,kBAAkB;IAOlB,mBAAmB,CAAC,QAAQ,EAAE,eAAe;IAoB7C,sBAAsB,CAAC,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI;IAyBrD,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;IAYrE,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE;CAW5E"}
@@ -1,4 +1,14 @@
1
1
  import { computePosition, autoUpdate, offset, flip, shift, arrow, size, } from '@floating-ui/dom';
2
+ /**
3
+ * How long the pointer may rest, in the corridor between the trigger and the
4
+ * bubble, before the safe polygon gives up and hides. The polygon is only
5
+ * re-evaluated on `pointermove`, so a pointer that leaves and immediately stops
6
+ * emits no further event to close on — this fallback guarantees the tooltip
7
+ * still hides. Kept short so a genuine pause while travelling toward the bubble
8
+ * re-arms it without a visible flicker. It is *not* armed while the pointer sits
9
+ * on the bubble or the trigger themselves — those are stable "stay open" states.
10
+ */
11
+ const SAFE_POLYGON_REST_HIDE_MS = 200;
2
12
  /** Check if a point is inside a polygon using ray-casting. */
3
13
  function isPointInPolygon([px, py], polygon) {
4
14
  let inside = false;
@@ -271,39 +281,65 @@ export class PopoverController {
271
281
  const floating = this._config.getFloatingElement();
272
282
  const trigger = this._config.getTriggerElement();
273
283
  if (!floating || !trigger) {
274
- onHide();
284
+ onHide('exit');
275
285
  return;
276
286
  }
277
287
  const cursorX = e.clientX;
278
288
  const cursorY = e.clientY;
289
+ // Armed only while the pointer sits in the corridor between the trigger and
290
+ // the bubble: there we cannot tell "paused mid-travel" from "gone" without
291
+ // waiting, so if no further `pointermove` re-evaluates the polygon the
292
+ // pointer has settled and we hide. On the bubble or the trigger themselves
293
+ // the answer is unambiguous — stay open — so the timer is cleared, never
294
+ // armed (otherwise a pointer resting on the bubble to read it would hide).
295
+ let fallback;
296
+ const hide = (reason) => {
297
+ cleanup();
298
+ onHide(reason);
299
+ };
300
+ // Stable "stay open" surface (bubble or trigger): cancel any pending rest-hide.
301
+ const stayOpen = () => clearTimeout(fallback);
302
+ // Transient corridor: keep open, but hide if the pointer comes to rest here.
303
+ const armRestHide = () => {
304
+ clearTimeout(fallback);
305
+ fallback = setTimeout(() => hide('settled'), SAFE_POLYGON_REST_HIDE_MS);
306
+ };
279
307
  const onMove = (moveEvent) => {
280
308
  const { clientX: mx, clientY: my } = moveEvent;
281
309
  const cursor = [mx, my];
282
310
  const rect = floating.getBoundingClientRect();
283
311
  const refRect = trigger.getBoundingClientRect();
284
312
  const side = this._currentPlacement.split('-')[0];
285
- if (isPointInRect(cursor, rect))
313
+ if (isPointInRect(cursor, rect)) {
314
+ stayOpen();
286
315
  return;
287
- if (isPointInRect(cursor, refRect))
316
+ }
317
+ if (isPointInRect(cursor, refRect)) {
318
+ stayOpen();
288
319
  return;
320
+ }
289
321
  if ((side === 'top' && cursorY >= refRect.bottom - 1) ||
290
322
  (side === 'bottom' && cursorY <= refRect.top + 1) ||
291
323
  (side === 'left' && cursorX >= refRect.right - 1) ||
292
324
  (side === 'right' && cursorX <= refRect.left + 1)) {
293
- cleanup();
294
- onHide();
325
+ hide('exit');
295
326
  return;
296
327
  }
297
328
  const polygon = getSafePolygon(cursorX, cursorY, rect, refRect, this._currentPlacement);
298
- if (isPointInPolygon(cursor, polygon))
329
+ if (isPointInPolygon(cursor, polygon)) {
330
+ armRestHide();
299
331
  return;
300
- cleanup();
301
- onHide();
332
+ }
333
+ hide('exit');
302
334
  };
303
335
  const cleanup = () => {
336
+ clearTimeout(fallback);
304
337
  document.removeEventListener('pointermove', onMove);
305
338
  };
306
339
  document.addEventListener('pointermove', onMove);
340
+ // Arm immediately: a leave with no subsequent move (flick-and-stop) lands in
341
+ // the corridor and must still hide once the pointer has settled.
342
+ armRestHide();
307
343
  this._cleanupSafePolygon?.();
308
344
  this._cleanupSafePolygon = cleanup;
309
345
  }
@@ -66,6 +66,33 @@ Add `data-marker="arrow"` or `data-marker="plus"` for an animated icon indicator
66
66
  </div>
67
67
  ```
68
68
 
69
+ ### Marker placement
70
+
71
+ Add `data-marker-placement="start"` to move the marker before the label (defaults to `end`).
72
+
73
+ ```html
74
+ <div class="flex flex-col gap-3">
75
+ <details
76
+ class="l-disclosure"
77
+ data-variant="bordered"
78
+ data-marker="arrow"
79
+ data-marker-placement="start"
80
+ >
81
+ <summary>Arrow at the start</summary>
82
+ <div>The marker sits before the label instead of after it.</div>
83
+ </details>
84
+ <details
85
+ class="l-disclosure"
86
+ data-variant="bordered"
87
+ data-marker="plus"
88
+ data-marker-placement="start"
89
+ >
90
+ <summary>Plus at the start</summary>
91
+ <div>Works with any marker icon.</div>
92
+ </details>
93
+ </div>
94
+ ```
95
+
69
96
  ## Examples
70
97
 
71
98
  ### FAQ
@@ -154,6 +181,7 @@ Use the native `name` attribute to create an exclusive accordion — only one it
154
181
  - **open** — Native attribute — starts the disclosure expanded.
155
182
  - **name** — Native attribute — groups disclosures into an exclusive accordion.
156
183
  - **data-marker**: `arrow | plus` — Marker icon: `arrow` rotates 180° when open; `plus` rotates 45° into a cross.
184
+ - **data-marker-placement**: `start | end` — Marker side. Defaults to `end`.
157
185
  - **data-variant** — bordered — Adds border, background, and border-radius.
158
186
  - **disabled** — Disables interaction (set on `<details>` or `<summary>`).
159
187
 
@@ -100,6 +100,35 @@ At most one `hover`/`focus`/`click` tooltip is visible at a time — opening one
100
100
  >
101
101
  ```
102
102
 
103
+ ### Delay
104
+
105
+ Set `show-delay` to require the pointer to dwell before the tooltip shows — filtering out tooltips for controls the pointer merely passes over (toolbars, table action columns, dense grids). Set `hide-delay` to bridge a brief exit-and-return without flicker. Both are in milliseconds and apply to `hover` only; focus always shows immediately.
106
+
107
+ ```html
108
+ <div class="flex gap-3">
109
+ <button
110
+ id="tooltip-delay-a"
111
+ class="l-button"
112
+ >
113
+ Immediate
114
+ </button>
115
+ <l-tooltip for="tooltip-delay-a">Shows right away</l-tooltip>
116
+
117
+ <button
118
+ id="tooltip-delay-b"
119
+ class="l-button"
120
+ >
121
+ Dwell 400ms
122
+ </button>
123
+ <l-tooltip
124
+ for="tooltip-delay-b"
125
+ show-delay="400"
126
+ hide-delay="150"
127
+ >Only after the pointer rests</l-tooltip
128
+ >
129
+ </div>
130
+ ```
131
+
103
132
  ### Without arrow
104
133
 
105
134
  Add `without-arrow` to hide the directional arrow.
@@ -167,6 +196,64 @@ Set `--background-color` to a base color. Text color is auto-derived from its lu
167
196
 
168
197
  ## Examples
169
198
 
199
+ ### Icon toolbar
200
+
201
+ Give each icon button its own tooltip and a `show-delay` so sweeping across the group doesn't flash a tooltip on every control the pointer passes over — only the one the pointer rests on opens. Keep an `aria-label` on each button: the tooltip supplements the accessible name, it doesn't replace it.
202
+
203
+ ```html
204
+ <l-button-group label="Text formatting">
205
+ <button
206
+ id="tooltip-tb-bold"
207
+ class="l-button"
208
+ aria-label="Bold"
209
+ >
210
+ <iconify-icon icon="lucide:bold"></iconify-icon>
211
+ </button>
212
+ <button
213
+ id="tooltip-tb-italic"
214
+ class="l-button"
215
+ aria-label="Italic"
216
+ >
217
+ <iconify-icon icon="lucide:italic"></iconify-icon>
218
+ </button>
219
+ <button
220
+ id="tooltip-tb-underline"
221
+ class="l-button"
222
+ aria-label="Underline"
223
+ >
224
+ <iconify-icon icon="lucide:underline"></iconify-icon>
225
+ </button>
226
+ <button
227
+ id="tooltip-tb-link"
228
+ class="l-button"
229
+ aria-label="Insert link"
230
+ >
231
+ <iconify-icon icon="lucide:link"></iconify-icon>
232
+ </button>
233
+ </l-button-group>
234
+
235
+ <l-tooltip
236
+ for="tooltip-tb-bold"
237
+ show-delay="400"
238
+ >Bold</l-tooltip
239
+ >
240
+ <l-tooltip
241
+ for="tooltip-tb-italic"
242
+ show-delay="400"
243
+ >Italic</l-tooltip
244
+ >
245
+ <l-tooltip
246
+ for="tooltip-tb-underline"
247
+ show-delay="400"
248
+ >Underline</l-tooltip
249
+ >
250
+ <l-tooltip
251
+ for="tooltip-tb-link"
252
+ show-delay="400"
253
+ >Insert link</l-tooltip
254
+ >
255
+ ```
256
+
170
257
  ### All placements
171
258
 
172
259
  All 12 placement options with aligned variants (`-start`, `-end`).
@@ -378,6 +465,11 @@ import 'luxen-ui/tooltip';
378
465
  - **for**: `string` — The HTML id of the element triggering the tooltip.
379
466
  - **placement**: `Placement` (default: `'top'`) — The preferred placement of the tooltip.
380
467
  - **distance**: `number` (default: `8`) — The distance in pixels from the target element.
468
+ - **show-delay**: `number` (default: `0`) — Milliseconds the pointer must dwell on the trigger before the tooltip
469
+ shows. Applies to `hover` only — focus shows immediately.
470
+ - **hide-delay**: `number` (default: `0`) — Milliseconds to wait after the pointer leaves the trigger (and its safe
471
+ polygon) before hiding. Bridges a brief exit-and-return without flicker.
472
+ Applies to `hover` only.
381
473
  - **open**: `boolean` (default: `false`) — Whether or not the tooltip is visible.
382
474
  - **without-arrow**: `boolean` (default: `false`) — Hide the directional arrow.
383
475
  - **trigger**: `string` (default: `'hover focus'`) — Space-separated list of trigger modes: `hover`, `focus`, `click`, `manual`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxen-ui",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Modern web components and CSS-first UI library built with Lit. Framework-agnostic, customizable prefix, design tokens.",
5
5
  "keywords": [
6
6
  "custom-elements",