@aquera/nile-elements 1.6.0 → 1.6.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 (81) hide show
  1. package/README.md +3 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +301 -185
  5. package/dist/nile-floating-panel/index.cjs.js +1 -1
  6. package/dist/nile-floating-panel/index.esm.js +1 -1
  7. package/dist/nile-floating-panel/nile-floating-panel.cjs.js +1 -1
  8. package/dist/nile-floating-panel/nile-floating-panel.cjs.js.map +1 -1
  9. package/dist/nile-floating-panel/nile-floating-panel.css.cjs.js +1 -1
  10. package/dist/nile-floating-panel/nile-floating-panel.css.cjs.js.map +1 -1
  11. package/dist/nile-floating-panel/nile-floating-panel.css.esm.js +137 -21
  12. package/dist/nile-floating-panel/nile-floating-panel.esm.js +1 -1
  13. package/dist/nile-lite-tooltip/index.cjs.js +1 -1
  14. package/dist/nile-lite-tooltip/index.esm.js +1 -1
  15. package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js +1 -1
  16. package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js.map +1 -1
  17. package/dist/nile-lite-tooltip/nile-lite-tooltip.esm.js +1 -1
  18. package/dist/src/index.d.ts +1 -1
  19. package/dist/src/index.js +1 -1
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/nile-floating-panel/index.js.map +1 -1
  22. package/dist/src/nile-floating-panel/nile-floating-panel.css.d.ts +1 -1
  23. package/dist/src/nile-floating-panel/nile-floating-panel.css.js +147 -20
  24. package/dist/src/nile-floating-panel/nile-floating-panel.css.js.map +1 -1
  25. package/dist/src/nile-floating-panel/nile-floating-panel.d.ts +90 -24
  26. package/dist/src/nile-floating-panel/nile-floating-panel.js +478 -159
  27. package/dist/src/nile-floating-panel/nile-floating-panel.js.map +1 -1
  28. package/dist/src/version.js +1 -1
  29. package/dist/src/version.js.map +1 -1
  30. package/dist/tippy.esm-57628c2b.esm.js +1 -0
  31. package/dist/tippy.esm-78baa8f2.cjs.js +2 -0
  32. package/dist/tippy.esm-78baa8f2.cjs.js.map +1 -0
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +4 -3
  35. package/src/index.ts +2 -2
  36. package/src/nile-floating-panel/index.ts +0 -1
  37. package/src/nile-floating-panel/nile-floating-panel.css.ts +149 -21
  38. package/src/nile-floating-panel/nile-floating-panel.ts +489 -190
  39. package/vscode-html-custom-data.json +213 -23
  40. package/dist/nile-floating-panel/anchor-manager.cjs.js +0 -2
  41. package/dist/nile-floating-panel/anchor-manager.cjs.js.map +0 -1
  42. package/dist/nile-floating-panel/anchor-manager.esm.js +0 -1
  43. package/dist/nile-floating-panel/content-manager.cjs.js +0 -2
  44. package/dist/nile-floating-panel/content-manager.cjs.js.map +0 -1
  45. package/dist/nile-floating-panel/content-manager.esm.js +0 -1
  46. package/dist/nile-floating-panel/event-manager.cjs.js +0 -2
  47. package/dist/nile-floating-panel/event-manager.cjs.js.map +0 -1
  48. package/dist/nile-floating-panel/event-manager.esm.js +0 -1
  49. package/dist/nile-floating-panel/position-manager.cjs.js +0 -2
  50. package/dist/nile-floating-panel/position-manager.cjs.js.map +0 -1
  51. package/dist/nile-floating-panel/position-manager.esm.js +0 -1
  52. package/dist/nile-floating-panel/style-manager.cjs.js +0 -2
  53. package/dist/nile-floating-panel/style-manager.cjs.js.map +0 -1
  54. package/dist/nile-floating-panel/style-manager.esm.js +0 -1
  55. package/dist/nile-floating-panel/types.cjs.js +0 -2
  56. package/dist/nile-floating-panel/types.cjs.js.map +0 -1
  57. package/dist/nile-floating-panel/types.esm.js +0 -1
  58. package/dist/src/nile-floating-panel/anchor-manager.d.ts +0 -6
  59. package/dist/src/nile-floating-panel/anchor-manager.js +0 -27
  60. package/dist/src/nile-floating-panel/anchor-manager.js.map +0 -1
  61. package/dist/src/nile-floating-panel/content-manager.d.ts +0 -5
  62. package/dist/src/nile-floating-panel/content-manager.js +0 -44
  63. package/dist/src/nile-floating-panel/content-manager.js.map +0 -1
  64. package/dist/src/nile-floating-panel/event-manager.d.ts +0 -14
  65. package/dist/src/nile-floating-panel/event-manager.js +0 -52
  66. package/dist/src/nile-floating-panel/event-manager.js.map +0 -1
  67. package/dist/src/nile-floating-panel/position-manager.d.ts +0 -17
  68. package/dist/src/nile-floating-panel/position-manager.js +0 -72
  69. package/dist/src/nile-floating-panel/position-manager.js.map +0 -1
  70. package/dist/src/nile-floating-panel/style-manager.d.ts +0 -9
  71. package/dist/src/nile-floating-panel/style-manager.js +0 -44
  72. package/dist/src/nile-floating-panel/style-manager.js.map +0 -1
  73. package/dist/src/nile-floating-panel/types.d.ts +0 -11
  74. package/dist/src/nile-floating-panel/types.js +0 -2
  75. package/dist/src/nile-floating-panel/types.js.map +0 -1
  76. package/src/nile-floating-panel/anchor-manager.ts +0 -33
  77. package/src/nile-floating-panel/content-manager.ts +0 -54
  78. package/src/nile-floating-panel/event-manager.ts +0 -74
  79. package/src/nile-floating-panel/position-manager.ts +0 -102
  80. package/src/nile-floating-panel/style-manager.ts +0 -54
  81. package/src/nile-floating-panel/types.ts +0 -15
@@ -1,285 +1,584 @@
1
- import {
2
- LitElement,
3
- html,
4
- CSSResultArray,
5
- TemplateResult,
6
- PropertyValues,
7
- } from 'lit';
1
+ /**
2
+ * Copyright Aquera Inc 2025
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { CSSResultArray, PropertyValues } from 'lit';
8
9
  import { customElement, property } from 'lit/decorators.js';
9
10
  import { styles } from './nile-floating-panel.css';
10
11
  import NileElement from '../internal/nile-element';
11
- import { PositionManager } from './position-manager';
12
- import { StyleManager } from './style-manager';
13
- import { ContentManager } from './content-manager';
14
- import { AnchorManager } from './anchor-manager';
15
- import { EventManager } from './event-manager';
16
- import { VisibilityManager } from '../utilities/visibility-manager';
17
- import type { FloatingPanelPosition, FloatingPanelAnchor } from './types';
12
+ import tippy, {
13
+ Instance,
14
+ Props,
15
+ roundArrow,
16
+ followCursor as followCursorPlugin,
17
+ } from 'tippy.js';
18
+ import {
19
+ parseFollowCursor,
20
+ parseDuration,
21
+ } from '../nile-lite-tooltip/utils';
22
+ import { VisibilityManager } from '../utilities/visibility-manager.js';
18
23
 
19
24
  /**
20
- * Nile floating panel component.
25
+ * Nile floating-panel component.
26
+ *
27
+ * A popover that supports rich content (title, body, actions).
28
+ *
29
+ * **Wrapper mode** (default): first child element is the trigger.
30
+ * **For mode**: set `for="elementId"` to attach to an external element.
21
31
  *
22
32
  * @tag nile-floating-panel
23
- * @event nile-show - Emitted when the panel opens.
24
- * @event nile-hide - Emitted when the panel closes.
33
+ *
34
+ * @fires nile-init - Component initialized.
35
+ * @fires nile-destroy - Component destroyed.
36
+ * @fires nile-show - Panel opened.
37
+ * @fires nile-hide - Panel closed.
38
+ * @fires nile-after-show - Panel fully visible after animation.
39
+ * @fires nile-after-hide - Panel fully hidden after animation.
40
+ * @fires nile-toggle - Open/close transition (detail.open).
41
+ * @fires nile-visibility-change - Hidden by scroll/tab change.
25
42
  */
26
43
  @customElement('nile-floating-panel')
27
44
  export class NileFloatingPanel extends NileElement {
45
+ private static _groups = new Map<string, Set<NileFloatingPanel>>();
46
+
47
+ private static _reducedMotionQuery: MediaQueryList | null = null;
48
+
49
+ private static get prefersReducedMotion(): boolean {
50
+ if (!NileFloatingPanel._reducedMotionQuery) {
51
+ NileFloatingPanel._reducedMotionQuery =
52
+ window.matchMedia('(prefers-reduced-motion: reduce)');
53
+ }
54
+ return NileFloatingPanel._reducedMotionQuery.matches;
55
+ }
56
+
28
57
  public static get styles(): CSSResultArray {
29
58
  return [styles];
30
59
  }
31
60
 
32
- @property() anchor: FloatingPanelAnchor = null;
61
+ protected createRenderRoot() {
62
+ return this;
63
+ }
64
+
65
+ // ─── Tippy.js props ───
66
+
67
+ @property({ type: String })
68
+ placement:
69
+ | 'top'
70
+ | 'top-start'
71
+ | 'top-end'
72
+ | 'right'
73
+ | 'right-start'
74
+ | 'right-end'
75
+ | 'bottom'
76
+ | 'bottom-start'
77
+ | 'bottom-end'
78
+ | 'left'
79
+ | 'left-start'
80
+ | 'left-end'
81
+ | 'auto'
82
+ | 'auto-start'
83
+ | 'auto-end' = 'bottom';
84
+
85
+ @property({ type: String }) trigger: string = 'click';
86
+
87
+ @property({ type: Number }) distance = 12;
88
+
89
+ @property({ type: Number }) skidding = 0;
90
+
91
+ @property({ type: String, reflect: true })
92
+ arrow: 'round' | 'default' | 'none' = 'round';
93
+
94
+ @property({ type: String, reflect: true }) animation: string = 'fade';
95
+
96
+ @property({ type: String, reflect: true }) duration:
97
+ | string
98
+ | number
99
+ | [number, number] = 200;
100
+
101
+ @property({ type: String, reflect: true }) delay:
102
+ | number
103
+ | [number, number] = 0;
104
+
105
+ @property({ type: Boolean, reflect: true }) interactive = true;
106
+
107
+ @property({ type: Number, reflect: true }) interactiveBorder = 2;
108
+
109
+ @property({ type: String, reflect: true }) maxWidth: string | number = 'none';
110
+
111
+ @property({ type: Number, reflect: true }) zIndex = 9999;
112
+
113
+ @property({ type: String, reflect: true })
114
+ followCursor:
115
+ | boolean
116
+ | 'initial'
117
+ | 'horizontal'
118
+ | 'vertical'
119
+ | 'true'
120
+ | 'false' = false;
121
+
122
+ @property({ type: Boolean, reflect: true }) hideOnClick:
123
+ | boolean
124
+ | 'toggle' = true;
125
+
126
+ @property({ type: Boolean, reflect: true }) inertia = false;
127
+
128
+ @property({ type: Boolean, reflect: true }) allowHTML = false;
129
+
130
+ @property({ type: Boolean, reflect: true }) flip = true;
131
+
132
+ // ─── Popover-like props ───
133
+
134
+ @property({ type: String, attribute: 'for' }) for: string | null = null;
135
+
136
+ @property({ type: Boolean, reflect: true }) open = false;
137
+
138
+ @property({ type: Boolean, reflect: true }) preventOverlayClose = false;
139
+
140
+ @property({ type: String, reflect: true }) title = '';
33
141
 
34
- @property() position: FloatingPanelPosition = 'bottom';
142
+ @property({ type: Boolean, reflect: true }) disabled = false;
35
143
 
36
- @property({ type: Boolean, reflect: true, attribute: true }) open = false;
144
+ @property({ type: String, reflect: true }) width?: string;
37
145
 
38
- @property({
39
- type: Boolean,
40
- reflect: true,
41
- attribute: 'close-on-outside-click',
42
- converter: {
43
- fromAttribute: (value: string | null) => (!value || value === 'false' ? false : true),
44
- toAttribute: (value: boolean) => (value ? 'true' : 'false'),
45
- },
46
- })
47
- closeOnOutsideClick = true;
146
+ @property({ type: String, reflect: true }) height?: string;
48
147
 
49
- @property({ type: Boolean, reflect: true }) enableVisibilityEffect = true;
148
+ /** When set, only one panel in the same group can be open at a time. */
149
+ @property({ type: String, reflect: true }) group: string | null = null;
150
+
151
+ /** Close the panel when Escape is pressed. */
152
+ @property({ type: Boolean, reflect: true }) closeOnEscape = true;
153
+
154
+ // ─── Visibility manager props ───
155
+
156
+ @property({ type: Boolean, reflect: true }) enableVisibilityEffect = false;
50
157
 
51
158
  @property({ type: Boolean, reflect: true }) enableTabClose = false;
52
159
 
53
- private panelContainer: HTMLElement | null = null;
54
- private positionManager: PositionManager | null = null;
55
- private styleManager: StyleManager = new StyleManager();
56
- private eventManager: EventManager = new EventManager(this);
160
+ // ─── Internal state ───
161
+
162
+ private tippyInstance: Instance | null = null;
57
163
  private visibilityManager?: VisibilityManager;
164
+ private panelContainer: HTMLElement | null = null;
165
+ private anchorEl: HTMLElement | null = null;
166
+ private _suppressOpenWatch = false;
167
+ private _panelId = `nile-fp-${Math.random().toString(36).slice(2, 9)}`;
168
+ private _boundEscHandler = this._handleEscapeKey.bind(this);
169
+ private _pendingShowListener: (() => void) | null = null;
170
+ private _pendingHideListener: (() => void) | null = null;
171
+
172
+ // ─── Lifecycle ───
173
+
174
+ protected firstUpdated(): void {
175
+ this._buildDOM();
176
+ this._attachTippy();
177
+ this._joinGroup();
58
178
 
59
- connectedCallback() {
60
- super.connectedCallback();
179
+ this.visibilityManager = new VisibilityManager({
180
+ host: this,
181
+ target: this.anchorEl || null,
182
+ enableVisibilityEffect: this.enableVisibilityEffect,
183
+ enableTabClose: this.enableTabClose,
184
+ isOpen: () => this.open,
185
+ onAnchorOutOfView: () => {
186
+ this._setOpen(false);
187
+ this.tippyInstance?.hide();
188
+ this.emit('nile-visibility-change', {
189
+ visible: false,
190
+ reason: 'anchor-out-of-view',
191
+ });
192
+ },
193
+ onDocumentHidden: () => {
194
+ this._setOpen(false);
195
+ this.tippyInstance?.hide();
196
+ this.emit('nile-visibility-change', {
197
+ visible: false,
198
+ reason: 'document-hidden',
199
+ });
200
+ },
201
+ emit: (event, detail) => this.emit(`nile-${event}`, detail),
202
+ });
203
+
204
+ this.emit('nile-init');
61
205
  }
62
206
 
63
- disconnectedCallback() {
207
+ disconnectedCallback(): void {
64
208
  super.disconnectedCallback();
65
- this.cleanupPanel();
209
+ this._cleanupPendingShowListener();
210
+ this._cleanupPendingHideListener();
66
211
  this.visibilityManager?.cleanup();
212
+ this._leaveGroup();
213
+ this._removeEscListener();
214
+ this._destroyTippy();
215
+ this.emit('nile-destroy');
67
216
  }
68
217
 
69
- updated(changedProperties: Map<string, unknown>) {
70
- super.updated(changedProperties);
218
+ updated(changed: PropertyValues): void {
219
+ super.updated(changed);
71
220
 
72
- if (changedProperties.has('open')) {
221
+ if (!this.panelContainer) return;
222
+
223
+ if (changed.has('open') && !this._suppressOpenWatch) {
73
224
  if (this.open) {
74
- this.emit('nile-show');
75
- this.setupPanel();
76
225
  this.visibilityManager?.setup();
226
+ queueMicrotask(() => this.tippyInstance?.show());
77
227
  } else {
78
- this.emit('nile-hide');
79
228
  this.visibilityManager?.cleanup();
80
- this.cleanupPanel();
229
+ this.tippyInstance?.hide();
81
230
  }
82
231
  }
83
232
 
84
- if (
85
- changedProperties.has('closeOnOutsideClick') &&
86
- this.open &&
87
- this.panelContainer
88
- ) {
89
- this.eventManager.updateOutsideClickHandler(
90
- this.panelContainer,
91
- this.closeOnOutsideClick,
92
- this.open
93
- );
233
+ if (changed.has('group')) {
234
+ this._leaveGroup(changed.get('group') as string | null);
235
+ this._joinGroup();
94
236
  }
95
237
 
96
- if (changedProperties.has('anchor') && this.open) {
97
- this.cleanupPanel();
98
- this.setupPanel();
99
- }
238
+ const rebuildProps: string[] = [
239
+ 'placement', 'trigger', 'distance', 'skidding', 'arrow',
240
+ 'animation', 'duration', 'delay', 'interactive', 'interactiveBorder',
241
+ 'maxWidth', 'zIndex', 'followCursor', 'hideOnClick', 'inertia',
242
+ 'allowHTML', 'flip', 'preventOverlayClose', 'disabled', 'width', 'height',
243
+ ];
100
244
 
101
- if (
102
- (changedProperties.has('enableVisibilityEffect') ||
103
- changedProperties.has('enableTabClose')) &&
104
- this.open
105
- ) {
106
- this.setupVisibilityManager();
245
+ if (rebuildProps.some(p => changed.has(p))) {
246
+ this._attachTippy();
107
247
  }
248
+ }
108
249
 
109
- if (
110
- changedProperties.has('position') &&
111
- this.open &&
112
- this.positionManager
113
- ) {
114
- this.positionManager.updatePosition(this.position);
115
- }
250
+ // ─── Public API ───
251
+
252
+ /** Programmatically shows the panel. Returns a promise that resolves after the show animation. */
253
+ public show(): Promise<void> {
254
+ this.open = true;
255
+ return new Promise<void>(resolve => {
256
+ this._cleanupPendingShowListener();
257
+ const handler = () => {
258
+ this._pendingShowListener = null;
259
+ resolve();
260
+ };
261
+ this._pendingShowListener = handler;
262
+ this.addEventListener('nile-after-show', handler, { once: true });
263
+ });
264
+ }
116
265
 
117
- if (changedProperties.has('open') && this.open && this.panelContainer) {
118
- this.updatePanelContent();
119
- }
266
+ /** Programmatically hides the panel. Returns a promise that resolves after the hide animation. */
267
+ public hide(): Promise<void> {
268
+ this.open = false;
269
+ return new Promise<void>(resolve => {
270
+ this._cleanupPendingHideListener();
271
+ const handler = () => {
272
+ this._pendingHideListener = null;
273
+ resolve();
274
+ };
275
+ this._pendingHideListener = handler;
276
+ this.addEventListener('nile-after-hide', handler, { once: true });
277
+ });
120
278
  }
121
279
 
122
- private setupPanel() {
123
- if (this.panelContainer) {
124
- return;
280
+ private _cleanupPendingShowListener(): void {
281
+ if (this._pendingShowListener) {
282
+ this.removeEventListener('nile-after-show', this._pendingShowListener);
283
+ this._pendingShowListener = null;
125
284
  }
285
+ }
126
286
 
127
- const componentStyles = (this.constructor as typeof NileFloatingPanel)
128
- .styles;
129
- if (componentStyles) {
130
- this.styleManager.injectStyles(componentStyles);
287
+ private _cleanupPendingHideListener(): void {
288
+ if (this._pendingHideListener) {
289
+ this.removeEventListener('nile-after-hide', this._pendingHideListener);
290
+ this._pendingHideListener = null;
131
291
  }
292
+ }
132
293
 
133
- this.panelContainer = document.createElement('div');
134
- this.panelContainer.setAttribute('part', 'panel');
135
- this.panelContainer.className = 'nile-floating-panel__container';
294
+ public toggle(): void {
295
+ this.open = !this.open;
296
+ }
136
297
 
137
- this.updatePanelContent();
298
+ public refresh(): void {
299
+ this._attachTippy();
300
+ }
138
301
 
139
- const anchorElement = AnchorManager.resolveAnchor(this.anchor);
140
- AnchorManager.appendToAnchor(anchorElement, this.panelContainer);
302
+ /** Returns the current resolved placement from Tippy/Popper. */
303
+ public getCurrentPlacement(): string {
304
+ const popper = this.tippyInstance?.popper;
305
+ const box = popper?.querySelector('.tippy-box') as HTMLElement | null;
306
+ return box?.dataset.placement ?? this.placement;
307
+ }
141
308
 
142
- requestAnimationFrame(() => {
143
- this.setupPositionManager();
144
- this.setupEventHandlers();
145
- this.setupVisibilityManager();
146
- });
309
+ /** Returns true if the resolved placement matches the requested placement. */
310
+ public isPositioningOptimal(): boolean {
311
+ return this.getCurrentPlacement() === this.placement;
147
312
  }
148
313
 
149
- private setupEventHandlers() {
150
- if (!this.panelContainer) {
151
- return;
314
+ // ─── Group management ───
315
+
316
+ private _joinGroup(): void {
317
+ if (!this.group) return;
318
+ let set = NileFloatingPanel._groups.get(this.group);
319
+ if (!set) {
320
+ set = new Set();
321
+ NileFloatingPanel._groups.set(this.group, set);
152
322
  }
323
+ set.add(this);
324
+ }
153
325
 
154
- this.eventManager.setupOutsideClickHandler(
155
- this.panelContainer,
156
- this.closeOnOutsideClick,
157
- this.open,
158
- () => {
159
- this.open = false;
326
+ private _leaveGroup(oldGroup?: string | null): void {
327
+ const key = oldGroup ?? this.group;
328
+ if (!key) return;
329
+ const set = NileFloatingPanel._groups.get(key);
330
+ if (set) {
331
+ set.delete(this);
332
+ if (set.size === 0) NileFloatingPanel._groups.delete(key);
333
+ }
334
+ }
335
+
336
+ private _hideGroupSiblings(): void {
337
+ if (!this.group) return;
338
+ const set = NileFloatingPanel._groups.get(this.group);
339
+ if (!set) return;
340
+ set.forEach(panel => {
341
+ if (panel !== this && panel.open) {
342
+ panel._setOpen(false);
343
+ panel.tippyInstance?.hide();
160
344
  }
161
- );
345
+ });
162
346
  }
163
347
 
164
- private setupPositionManager() {
165
- if (!this.panelContainer) {
166
- return;
348
+ // ─── Escape key ───
349
+
350
+ private _addEscListener(): void {
351
+ if (this.closeOnEscape) {
352
+ document.addEventListener('keydown', this._boundEscHandler);
167
353
  }
354
+ }
168
355
 
169
- const referenceElement = this.findTriggerElement() || this;
170
- this.positionManager = new PositionManager(
171
- referenceElement,
172
- this.panelContainer,
173
- this.position
174
- );
356
+ private _removeEscListener(): void {
357
+ document.removeEventListener('keydown', this._boundEscHandler);
358
+ }
175
359
 
176
- this.positionManager.reposition();
177
- this.positionManager.setupAutoUpdate();
360
+ private _handleEscapeKey(e: KeyboardEvent): void {
361
+ if (e.key === 'Escape' && this.open) {
362
+ this._setOpen(false);
363
+ this.tippyInstance?.hide();
364
+ }
178
365
  }
179
366
 
180
- private findTriggerElement(): HTMLElement | null {
181
- // Try to find the next sibling element as the trigger
182
- let nextSibling = this.nextElementSibling;
183
- while (nextSibling) {
184
- if (nextSibling instanceof HTMLElement) {
185
- return nextSibling;
186
- }
187
- nextSibling = nextSibling.nextElementSibling;
367
+ // ─── ARIA ───
368
+
369
+ private _applyAria(): void {
370
+ if (!this.anchorEl || !this.panelContainer) return;
371
+ this.panelContainer.setAttribute('role', 'dialog');
372
+ this.panelContainer.id = this._panelId;
373
+ this.anchorEl.setAttribute('aria-haspopup', 'dialog');
374
+ this._syncAriaExpanded();
375
+ }
376
+
377
+ private _syncAriaExpanded(): void {
378
+ this.anchorEl?.setAttribute('aria-expanded', String(this.open));
379
+ if (this.open) {
380
+ this.anchorEl?.setAttribute('aria-describedby', this._panelId);
381
+ } else {
382
+ this.anchorEl?.removeAttribute('aria-describedby');
188
383
  }
384
+ }
189
385
 
190
- // Try to find the previous sibling element
191
- let previousSibling = this.previousElementSibling;
192
- while (previousSibling) {
193
- if (previousSibling instanceof HTMLElement) {
194
- return previousSibling;
386
+ // ─── DOM construction ───
387
+
388
+ private _buildDOM(): void {
389
+ const children = Array.from(this.childNodes);
390
+
391
+ this.anchorEl = null;
392
+ const titleNodes: Node[] = [];
393
+ const actionNodes: Node[] = [];
394
+ const bodyNodes: Node[] = [];
395
+ let firstElementSeen = false;
396
+
397
+ for (const child of children) {
398
+ if (child instanceof HTMLElement) {
399
+ const slot = child.getAttribute('slot');
400
+ if (slot === 'title') {
401
+ child.removeAttribute('slot');
402
+ titleNodes.push(child);
403
+ continue;
404
+ }
405
+ if (slot === 'action') {
406
+ child.removeAttribute('slot');
407
+ actionNodes.push(child);
408
+ continue;
409
+ }
410
+ if (!firstElementSeen && !this.for) {
411
+ this.anchorEl = child;
412
+ firstElementSeen = true;
413
+ continue;
414
+ }
195
415
  }
196
- previousSibling = previousSibling.previousElementSibling;
416
+ bodyNodes.push(child);
197
417
  }
198
418
 
199
- return null;
200
- }
419
+ if (this.for) {
420
+ const anchor = document.getElementById(this.for);
421
+ if (anchor) {
422
+ this.anchorEl = anchor;
423
+ }
424
+ }
201
425
 
202
- private setupVisibilityManager(): void {
203
- if (!this.enableVisibilityEffect) {
204
- return;
426
+ while (this.firstChild) {
427
+ this.removeChild(this.firstChild);
205
428
  }
206
429
 
207
- const triggerElement = this.findTriggerElement();
208
-
209
- // Cleanup existing visibility manager if it exists
210
- if (this.visibilityManager) {
211
- this.visibilityManager.cleanup();
430
+ if (this.anchorEl && !this.for) {
431
+ this.appendChild(this.anchorEl);
212
432
  }
213
433
 
214
- this.visibilityManager = new VisibilityManager({
215
- host: this,
216
- target: triggerElement || null,
217
- enableVisibilityEffect: this.enableVisibilityEffect,
218
- enableTabClose: this.enableTabClose,
219
- isOpen: () => this.open,
220
- onAnchorOutOfView: () => {
221
- this.open = false;
222
- this.emit('nile-visibility-change', {
223
- visible: false,
224
- reason: 'anchor-out-of-view',
225
- });
226
- },
227
- onDocumentHidden: () => {
228
- this.open = false;
229
- this.emit('nile-visibility-change', {
230
- visible: false,
231
- reason: 'document-hidden',
232
- });
233
- },
234
- emit: (event, detail) => this.emit(`nile-${event}`, detail),
235
- });
434
+ this.panelContainer = document.createElement('div');
435
+ this.panelContainer.className = 'nile-floating-panel__content';
436
+ this.panelContainer.style.display = 'none';
236
437
 
237
- if (this.open) {
238
- this.visibilityManager.setup();
239
- }
240
- }
438
+ const body = document.createElement('div');
439
+ body.className = 'nile-floating-panel__body';
241
440
 
242
- private updatePanelContent() {
243
- if (!this.panelContainer) {
244
- return;
441
+ if (titleNodes.length > 0 || this.title) {
442
+ const titleDiv = document.createElement('div');
443
+ titleDiv.className = 'nile-floating-panel__title';
444
+ if (this.title) {
445
+ titleDiv.textContent = this.title;
446
+ } else {
447
+ titleNodes.forEach(n => titleDiv.appendChild(n));
448
+ }
449
+ body.appendChild(titleDiv);
245
450
  }
246
451
 
247
- const slot = this.shadowRoot?.querySelector('slot') || null;
248
- ContentManager.updatePanelContent(this.panelContainer, slot, '');
249
- }
452
+ if (bodyNodes.length > 0) {
453
+ const mainDiv = document.createElement('div');
454
+ mainDiv.className = 'nile-floating-panel__main';
455
+ bodyNodes.forEach(n => mainDiv.appendChild(n));
456
+ body.appendChild(mainDiv);
457
+ }
250
458
 
251
- private reposition() {
252
- if (this.positionManager) {
253
- this.positionManager.reposition();
459
+ if (actionNodes.length > 0) {
460
+ const actionDiv = document.createElement('div');
461
+ actionDiv.className = 'nile-floating-panel__action';
462
+ actionNodes.forEach(n => actionDiv.appendChild(n));
463
+ body.appendChild(actionDiv);
254
464
  }
465
+
466
+ this.panelContainer.appendChild(body);
467
+ this.appendChild(this.panelContainer);
468
+
469
+ this._applyAria();
255
470
  }
256
471
 
257
- private cleanupPanel() {
258
- this.eventManager.destroy();
472
+ // ─── Tippy management ───
259
473
 
260
- if (this.positionManager) {
261
- this.positionManager.destroy();
262
- this.positionManager = null;
474
+ private _resolveArrow() {
475
+ switch (this.arrow) {
476
+ case 'round': return roundArrow;
477
+ case 'none': return false as const;
478
+ default: return true as const;
263
479
  }
480
+ }
264
481
 
265
- if (this.panelContainer) {
266
- AnchorManager.removeFromAnchor(this.panelContainer);
267
- }
482
+ private _setOpen(value: boolean): void {
483
+ this._suppressOpenWatch = true;
484
+ this.open = value;
485
+ this._syncAriaExpanded();
486
+ this._suppressOpenWatch = false;
487
+ }
268
488
 
269
- this.panelContainer = null;
489
+ private _getEffectiveDuration(): number | [number, number] {
490
+ if (NileFloatingPanel.prefersReducedMotion) return 0;
491
+ return parseDuration(this.duration);
492
+ }
270
493
 
271
- this.styleManager.cleanupStyles();
494
+ private _getEffectiveAnimation(): string | false {
495
+ if (NileFloatingPanel.prefersReducedMotion) return false;
496
+ return this.animation;
272
497
  }
273
498
 
274
- public render(): TemplateResult {
275
- return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
499
+ private _attachTippy(): void {
500
+ this._destroyTippy();
501
+
502
+ if (this.disabled || !this.anchorEl || !this.panelContainer) return;
503
+
504
+ const resolvedFollowCursor = parseFollowCursor(this.followCursor);
505
+ const effectiveHideOnClick = this.preventOverlayClose ? false : this.hideOnClick;
506
+
507
+ const options: Partial<Props> = {
508
+ content: this.panelContainer,
509
+ placement: this.placement,
510
+ trigger: this.trigger,
511
+ offset: [this.skidding, this.distance],
512
+ theme: 'floating-panel',
513
+ animation: this._getEffectiveAnimation(),
514
+ interactive: this.interactive,
515
+ arrow: this._resolveArrow(),
516
+ duration: this._getEffectiveDuration(),
517
+ allowHTML: this.allowHTML,
518
+ delay: this.delay as any,
519
+ maxWidth: this.maxWidth,
520
+ zIndex: this.zIndex,
521
+ hideOnClick: effectiveHideOnClick,
522
+ inertia: NileFloatingPanel.prefersReducedMotion ? false : this.inertia,
523
+ interactiveBorder: this.interactiveBorder,
524
+ appendTo: document.body,
525
+ followCursor: resolvedFollowCursor,
526
+ plugins: resolvedFollowCursor ? [followCursorPlugin] : [],
527
+ popperOptions: {
528
+ modifiers: [{ name: 'flip', enabled: this.flip }],
529
+ },
530
+ onMount: () => {
531
+ if (this.panelContainer) this.panelContainer.style.display = '';
532
+ },
533
+ onShow: (instance) => {
534
+ if (this.panelContainer) this.panelContainer.style.display = '';
535
+ const tc = instance.popper.querySelector('.tippy-content') as HTMLElement | null;
536
+ if (tc) {
537
+ if (this.width) tc.style.width = this.width;
538
+ if (this.height) { tc.style.height = this.height; tc.style.overflow = 'auto'; }
539
+ }
540
+ this._hideGroupSiblings();
541
+ this._setOpen(true);
542
+ this._addEscListener();
543
+ this.dispatchEvent(new CustomEvent('nile-show', { detail: { instance, target: instance.reference } }));
544
+ this.dispatchEvent(new CustomEvent('nile-toggle', { detail: { open: true, instance, target: instance.reference } }));
545
+ return undefined;
546
+ },
547
+ onShown: (instance) => {
548
+ this.dispatchEvent(new CustomEvent('nile-after-show', { detail: { instance, target: instance.reference } }));
549
+ },
550
+ onHide: (instance) => {
551
+ this._setOpen(false);
552
+ this._removeEscListener();
553
+ this.dispatchEvent(new CustomEvent('nile-hide', { detail: { instance, target: instance.reference } }));
554
+ this.dispatchEvent(new CustomEvent('nile-toggle', { detail: { open: false, instance, target: instance.reference } }));
555
+ return undefined;
556
+ },
557
+ onHidden: (instance) => {
558
+ if (this.panelContainer) this.panelContainer.style.display = 'none';
559
+ this.dispatchEvent(new CustomEvent('nile-after-hide', { detail: { instance, target: instance.reference } }));
560
+ },
561
+ };
562
+
563
+ this.tippyInstance = tippy(this.anchorEl, options);
564
+
565
+ if (this.open) {
566
+ queueMicrotask(() => this.tippyInstance?.show());
567
+ }
276
568
  }
277
569
 
278
- private handleSlotChange = () => {
279
- if (this.open && this.panelContainer) {
280
- this.updatePanelContent();
570
+ private _destroyTippy(): void {
571
+ if (this.tippyInstance) {
572
+ this.tippyInstance.destroy();
573
+ this.tippyInstance = null;
281
574
  }
282
- };
575
+ if (this.panelContainer) {
576
+ this.panelContainer.style.display = 'none';
577
+ if (this.panelContainer.parentElement !== this) {
578
+ this.appendChild(this.panelContainer);
579
+ }
580
+ }
581
+ }
283
582
  }
284
583
 
285
584
  export default NileFloatingPanel;